diff --git a/README.md b/README.md index 451d649a..4928ede9 100644 --- a/README.md +++ b/README.md @@ -1,132 +1,200 @@ -# get-flutter-fire +--- -This codebase provides a boilerplate code utilizing the following three technologies: +# Flutter Firebase GetX - (Sheru) -1. Flutter 3.0 - For UX and uses Dart languange. See [https://flutter.dev/] -2. GetX - State management for Flutter. See [https://github.com/jonataslaw/getx/tree/4.6.1] -3. Firebase - For Backend as a Service. See [https://firebase.google.com/] - 1. Easy Authentication flow - 2. Server side functions - 3. Remote Configurations which can be used for A/B testing +Welcome to the **Sheru, Your Own Ecommerce Platform**! This application is made on top of the give code structure using flutter-firebase-getx. The flow and structure is maintained throught the app. For better experience and web capabilities, the admin part is made seperately as an admin panel that can be used for controlling the app. -This was created as part of my own learning process and you will find that git commits were made according to the Steps listed below. You can use the git version history to check each commit and learn step by step, as I did. +

+ Screenshot_3 + Screenshot_1 +

+

+ Screenshot_2 + Screenshot_7 +

+

+ Screenshot_8 + Screenshot_6 +

+

+ Screenshot_5 + Screenshot_4 +

+

+ Screenshot_14 + Screenshot_13 +

+

+ Screenshot_9 + Screenshot_10 +

+

+ Screenshot_11 + Screenshot_12 +

-I am also using this codebase as an experiment towards hiring people (freshers especially but not limited to them) for my development team. If you are in Mumbai and are interested to join my team, you can use this codebase in the following manner: +## Getting Started -* Fork the codebase -* Add your own firebase_options.dart (follow steps and see firebase_options.template) -* **Build your own application using this as a base (integrating any existing project of yours also works)**, or complete a TODO or fix a bug (only if you have no other ideas) -* Send me a Pull Request. Mention a way of connecting with you in the commit message along with details of commit. Also modify ReadMe to say what you have changed in detail. -* I will go through the request and then connect with you if I find the entry to be interesting and take an interview round. +### 1. Add the Android Folder -## The Steps +If your project doesn't already include an `android` folder, you can generate it by running the following command in your main project directory: -Step 1: Use Get CLI [https://pub.dev/packages/get_cli] +```bash +flutter create . +``` -`get create project` +### 2. Firebase Setup -Step 2: Copy code from [https://github.com/jonataslaw/getx/tree/4.6.1/example_nav2/lib] +To fully leverage Firebase features within your app, you’ll need to configure Firebase. Choose one of the following methods: -Step 3: Integrate FlutterFire Authentication +#### Option A: Add `firebase.json` File -- Tutorials [https://firebase.google.com/codelabs/firebase-auth-in-flutter-apps#0] for inspiration -- Firebase Documentation [https://firebase.google.com/docs/auth/flutter/start] -- Blog [www.medium.com/TBD] -- To compile the code ensure that you generate your own firebase_options.dart by running +1. Download the `firebase.json` configuration file from the Firebase Console. +2. Place the file in the root directory of your project. +3. Alternatively, you can use the Firebase CLI to configure Firebase. - `flutterfire configure` +#### Option B: Use Firebase Emulator -Step 4: Add Google OAuth [https://firebase.google.com/codelabs/firebase-auth-in-flutter-apps#6]. Note ensure you do the steps for Android and iOS as the code for it is not in Github +Set up the Firebase Emulator to simulate Firebase services locally: -Step 5: Add Guest User/Anonymous login with a Cart and Checkout use case [https://firebase.google.com/docs/auth/flutter/anonymous-auth] +1. Install the Firebase CLI if you haven't already: + ```bash + npm install -g firebase-tools + ``` -* delete unlinked anonymous user post logout +2. Initialize Firebase in your project: + ```bash + firebase init + ``` -Step 6: Add ImagePicker and Firebase Storage for profile image +3. Start the Firebase Emulator with Initial Data: + ```bash + firebase emulators:start --import=./emulator-data + ``` -* Create PopupMenu button for web [https://api.flutter.dev/flutter/material/PopupMenuButton-class.html] -* BottomSheet for phones and single file button for desktops -* GetX and Image Picker [https://stackoverflow.com/questions/66559553/flutter-imagepicker-with-getx] -* Add FilePicker [https://medium.com/@onuaugustine07/pick-any-file-file-picker-flutter-f82c0144e27c] -* Firebase Storage [https://mercyjemosop.medium.com/select-and-upload-images-to-firebase-storage-flutter-6fac855970a9] and [https://firebase.google.com/docs/storage/flutter/start] +### 3. OTP Verification and Notifications - Modify the Firebase Rules - `service firebase.storage { match /b/{bucket}/o { match /{allPaths=**} { allow write: if request.auth.uid != null; allow read: if true; } } }` +![image](https://github.com/user-attachments/assets/272b99cb-efeb-4052-ba31-59bb4e23b340) +![image](https://github.com/user-attachments/assets/81ffa5c3-a239-40e8-9a89-5f15ef63054f) - Fix CORS [https://stackoverflow.com/questions/37760695/firebase-storage-and-access-control-allow-origin] -* PList additions [https://medium.com/unitechie/flutter-tutorial-image-picker-from-camera-gallery-c27af5490b74] -Step 7: Additional Auth flow items +This app integrates OTP verification and real-time notifications using Firebase Authentication and Firebase Functions. These features are also supported when running the app with Firebase Emulators. -1. Add a Change Password Screen. The Flutter Fire package does not have this screen. -2. TODO: Add ReCaptcha to login flow for password authentication for Web only - * Phone Auth on Web has a ReCaptcha already [https://firebase.flutter.dev/docs/auth/phone/]. Tried to use that library but it is very cryptic. - * Use the following instead [https://stackoverflow.com/questions/60675575/how-to-implement-recaptcha-into-a-flutter-app] or [https://medium.com/cloudcraftz/securing-your-flutter-web-app-with-google-recaptcha-b556c567f409] or [https://pub.dev/packages/g_recaptcha_v3] -3. TODO: Ensure Reset Password has Email verification -4. TODO: Add Phone verification [https://firebase.google.com/docs/auth/flutter/phone-auth] - * See [https://github.com/firebase/flutterfire/issues/4189]. -5. TODO: Add 2FA with SMS Pin. This screen is available in the Flutter Fire package +#### OTP Verification: +- **Simulate OTP Verification**: With the Firebase Auth Emulator, you can test the OTP verification process without needing an actual phone number. This helps in testing the entire user authentication flow during development. -Step 8: Add Firebase Emulator to test on laptop instead of server so that we can use Functions without upgrading the plan to Blaze. See [https://firebase.google.com/docs/emulator-suite/install_and_configure] +#### Notifications: +- **Real-time Notifications**: Firebase Functions, in conjunction with the Firebase Emulator, allow you to trigger notifications for various events (e.g., order status updates). This enables you to develop and test notification workflows without interacting with the live Firebase environment. -Step 9: Add User Roles using Custom Claims. This requires upgrade of plan as we need to use Firebase Functions. Instead using Emulator. -1. In Emulator we can add user via http://127.0.0.1:4000/auth and add custom claim via UI as {"role":"admin"}. The effect is shown via Product page in Nav instead of Cart page for admin user. -2. Add Function to add the custom claim to make the first user the admin using the Admin SDK -3. Registeration form to collect some data post signUp and enforce email verification from Client side. +### 4. App Code Structure and Completed Features - * Note! for Emulator check the console to verify using the link provided as email is not sent. -4. Enforcing verify email using a button which appears when SignIn fails due to non verification. +The app's code structure follows a modular approach, with separate directories for different features and user flows. The following features have been implemented: - * Fixed the error handling message during login. - * Coverted server side to Typescript - * Enabled Resend verification mail button - * Approach 1 - Use Email Link Authentication and signIn, assuming it marks email as verified also. We cannot send the verification mail as is, since that can be sent only if signed in (which was allowed only for first login post signup) - * Refer https://firebase.google.com/docs/auth/flutter/email-link-auth - * TODO Enable Deep Linking: According to https://firebase.google.com/docs/dynamic-links/flutter/receive, the Flutter feature is being deprecated and we should use the AppLinks (Android), UniversalLinks(iOS) instead. Leaving this for future as adding complexity. - * We could use the server side handling instead of deep linking. See [https://firebase.google.com/docs/auth/custom-email-handler?hl=en&authuser=0#web]. However, this requires changing the email template for the URL which is not possible in Emulator. Using the continueURL instead does not work as oobCode is already expired. This handling also uses the web client sdk. Thus it is better to go with the below method instead. - * Approach 2 - (Hack) send a create request with suffix ".verify" added in email when button clicked. Use the server side beforeCreate to catch this and send verification mail - * Note that the Server side beforeCreate function can also bypass user creation till verification but the user record (esp password) needs to be stored anyways, so bypassing user creation is not a good idea. Instead, we should use the verified tag in subsequent processing - * Sending emails from server side is possible but by using the web client SDK. -5. TODO: Other Items +- **Home Screen**: + - **Banners**: Display promotional banners at the top of the screen. + - **Product Listings**: Showcase various products across different categories. + - **Categories Listing**: Provide users with an easy way to browse products by categories. - * TODO: Using autocomplete for emails is throwing error log in terminal. No impact but need to fix when all is done. - * TODO: Add a job that removes all unverified users after a while automatically. This could be useful if you were victim of bot attacks, but adding the Recaptcha is better precaution. See [https://stackoverflow.com/questions/67148672/how-to-delete-unverified-e-mail-addresses-in-firebase-authentication-flutter/67150606#67150606] -6. Added Roles of Buyer and Seller. +- **Cart Flow**: + - **Checkout**: Complete the checkout process with selected items. + - **Address Selection**: Users can select or add a new address during checkout. + - **Payment Method Selection**: Multiple payment methods are supported for a seamless checkout experience. - 1. Added Access level in increasing order of role order => Buyer then Seller then Admin - 2. Created Navigation for each of Admin, Buyer, Seller screens - 3. Allowed switch from lower role Navigation to Navigation view till the given role of the user +- **Past Orders**: View a history of past orders, allowing users to track their previous purchases. -Step 10: Firebase Remote Config for A/B testing. See [https://firebase.google.com/docs/remote-config] +- **Profile Section**: Manage user profile details, including personal information and settings. -1. Complete the Screen enum based Navigation framework -2. Config useBottomSheetForProfileOptions for Navigation element to be one of the following - * False: Drawer for Account, Settings, Sign Out - * True: Hamburger that opens BottomSheet (Context Menu in larger screen) for the same options -3. TODO: Config for adding Search Bar at the Top vs a Bottom Navigation button +- **Seller Section**: For users with a seller account, an additional section allows them to add or edit their products, enabling them to manage their listings directly within the app. +--- -Step 11: TODO: CRUD +# Flutter Firebase E-Commerce Admin Panel -* Users request role upgrade -* Add this request data to Firebase Datastore -* Create ListView with slidable tiles for approvals -* Admin SDK used by admin user via workflow on this request data and is approved from app - * Allow a Plan attribute via Custome Claims (e.g. Premium user flag) for Buyer and Seller, to add features which are not Navigation linked. Add a button Upgrade to Plan in Drawer that leads to Payment screen. Also certain aspects of premium plan can be visible that leads to upgrade plan screen via middleware -* Nested Category, Sub-Category tree creation +Welcome to the **Flutter Firebase E-Commerce Admin Panel**! This admin panel is designed to provide seamless control over the e-commerce platform, enabling administrators to manage products, sellers, coupons, banners, and more. -Step 12: TODO: Theming and Custom Settings +## Features Overview -* Add Persona (like that in Netflix) and create a Persona selection only for Buyer Role -* Add Minimal (Three Color Gradient Pallette) Material Theme. Align it with Persona Templates (like Kids Template in Netflix) -* Dark theme toggle setting based on each Persona of the logged in User +### 1. Special Product Management +- **Upload Special Products**: Administrators can upload exclusive products directly from the admin panel. These products will be highlighted on the platform for special promotions or featured categories. + +### 2. Seller Product Approval +- **Approve Seller Products**: Review and approve products submitted by sellers before they appear on the platform. This ensures quality control and adherence to platform guidelines. + +### 3. Coupon and Banner Management +- **Add Coupons**: Create and manage discount coupons to offer special deals and promotions to customers. +- **Add Banners**: Upload and manage promotional banners that will be displayed throughout the app. Banners can be used to highlight special events, sales, or new arrivals. + +### 4. App Management +- **Comprehensive Control**: The admin panel offers full control over various aspects of the app, allowing administrators to ensure the smooth operation of the platform. + +### 5. Export Data as CSV +- **Admin CSV Export**: Easily export product, seller, or order data as CSV files for analysis or reporting purposes. This feature enables administrators to keep records and perform data-driven decisions. + +## Approval Process Explanation + +The approval process within the admin panel is critical for maintaining the quality and integrity of the e-commerce platform. When sellers submit new products, these products must first be reviewed by an administrator. Only after approval are the products listed on the platform for customers to purchase. This step ensures that all products meet the platform's standards, helping to maintain a high-quality shopping experience for users. + +## Screenshots + +Here's a visual overview of the admin panel features: + +

+ Admin Dashboard + Product Management +

+

+ Seller Approvals + Coupon Management +

+

+ Banner Management + Data Export +

+ +## Getting Started + +### 1. Prerequisites + +- Ensure that the main e-commerce app is set up and configured with Firebase. +- The admin panel should have the appropriate Firebase configurations (e.g., `firebase.json`) in place to interact with the backend. + +### 2. Installation + +1. Navigate to the project directory: + ```bash + cd admin_panel_directory + ``` + +2. Install the required dependencies: + ```bash + flutter pub get + ``` + +### 3. Running the Admin Panel + +To start the admin panel, run the following command: + +```bash +flutter run --web-renderer html +``` + +## Usage + +- **Special Products**: Use the "Special Products" section to add new items that will be featured on the platform. +- **Seller Approvals**: Navigate to the "Seller Approvals" section + + to review and approve products submitted by third-party sellers. +- **Coupons and Banners**: Access the "Coupons" and "Banners" sections to create and manage promotional content. +- **Export as CSV**: In the admin panel, locate the export functionality to download relevant data as CSV files for offline analysis and reporting. + +--- + +## Submission Details +- **Name**: Manan Kabra +- **College**: K.J. Somaiya College of Engineering +- **Email**: manan.kabra@somaiya.edu +- **Personal Email**: manankabra200318@gmail.com +- **Roll No**: 16010421041 -Step 13: TODO: Large vs Small screen responsiveness -* Drawer: Triggered by Top Left Icon (App Logo). For iOS this icon changes to back button when required. Contains allowed Role List, Screens specified as Drawer. Becomes Left Side Navigation for Horizontal Screens. Can have additional extreme left vertical Navigation Strip. Bottom Navigation Bar also folds into this strip in Horizontal Screens. -* Top Right Icon: used for Login and post Login triggers BottomSheet/Context Menu for Persona Change, Profile, Settings, Change Password, Logout -* Search Bar (Toggle Button for phones) on Top Center with Title -* Status Bottom Bar for desktops only instead of SnackBars -* FAB vs Main Menu -Step 14: TODO: Make own login flow screens. Remove firebase library reference from all but auth_service diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 00000000..564c915a --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "1091785317203", + "project_id": "sheru-2eb0f", + "storage_bucket": "sheru-2eb0f.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:1091785317203:android:8903f5a04aebcb58649fa6", + "android_client_info": { + "package_name": "com.example.get_flutter_fire" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyAdliZ_GAfcFAjalNUKtw_xIt4LJc_jVSw" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/example/get_flutter_fire/MainActivity.kt b/android/app/src/main/kotlin/com/example/get_flutter_fire/MainActivity.kt new file mode 100644 index 00000000..018e286b --- /dev/null +++ b/android/app/src/main/kotlin/com/example/get_flutter_fire/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.get_flutter_fire + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 00000000..c704f54b --- /dev/null +++ b/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,6 @@ + + + + 10.0.2.2 + + diff --git a/assets/animations/loader.gif b/assets/animations/loader.gif new file mode 100644 index 00000000..afeb11b6 Binary files /dev/null and b/assets/animations/loader.gif differ diff --git a/assets/icons/icon_arrow_go.png b/assets/icons/icon_arrow_go.png new file mode 100644 index 00000000..8264361b Binary files /dev/null and b/assets/icons/icon_arrow_go.png differ diff --git a/assets/icons/icon_cart.png b/assets/icons/icon_cart.png new file mode 100644 index 00000000..36aa375f Binary files /dev/null and b/assets/icons/icon_cart.png differ diff --git a/assets/icons/icon_cart_checkout.png b/assets/icons/icon_cart_checkout.png new file mode 100644 index 00000000..4f4f3c08 Binary files /dev/null and b/assets/icons/icon_cart_checkout.png differ diff --git a/assets/icons/icon_category.png b/assets/icons/icon_category.png new file mode 100644 index 00000000..88135a73 Binary files /dev/null and b/assets/icons/icon_category.png differ diff --git a/assets/icons/icon_check_circle.png b/assets/icons/icon_check_circle.png new file mode 100644 index 00000000..63b5681d Binary files /dev/null and b/assets/icons/icon_check_circle.png differ diff --git a/assets/icons/icon_chevron_left.png b/assets/icons/icon_chevron_left.png new file mode 100644 index 00000000..44dfd9cb Binary files /dev/null and b/assets/icons/icon_chevron_left.png differ diff --git a/assets/icons/icon_chevron_right.png b/assets/icons/icon_chevron_right.png new file mode 100644 index 00000000..e54751cc Binary files /dev/null and b/assets/icons/icon_chevron_right.png differ diff --git a/assets/icons/icon_file.png b/assets/icons/icon_file.png new file mode 100644 index 00000000..d5fa5032 Binary files /dev/null and b/assets/icons/icon_file.png differ diff --git a/assets/icons/icon_history.png b/assets/icons/icon_history.png new file mode 100644 index 00000000..7869a36c Binary files /dev/null and b/assets/icons/icon_history.png differ diff --git a/assets/icons/icon_home.png b/assets/icons/icon_home.png new file mode 100644 index 00000000..99c3889e Binary files /dev/null and b/assets/icons/icon_home.png differ diff --git a/assets/icons/icon_location.png b/assets/icons/icon_location.png new file mode 100644 index 00000000..bd7b4b1f Binary files /dev/null and b/assets/icons/icon_location.png differ diff --git a/assets/icons/icon_mail.png b/assets/icons/icon_mail.png new file mode 100644 index 00000000..4bd2d309 Binary files /dev/null and b/assets/icons/icon_mail.png differ diff --git a/assets/icons/icon_notification.png b/assets/icons/icon_notification.png new file mode 100644 index 00000000..3ba638dc Binary files /dev/null and b/assets/icons/icon_notification.png differ diff --git a/assets/icons/icon_order.png b/assets/icons/icon_order.png new file mode 100644 index 00000000..4235462c Binary files /dev/null and b/assets/icons/icon_order.png differ diff --git a/assets/icons/icon_order_delivered.png b/assets/icons/icon_order_delivered.png new file mode 100644 index 00000000..9ac6d7ab Binary files /dev/null and b/assets/icons/icon_order_delivered.png differ diff --git a/assets/icons/icon_order_dispatched.png b/assets/icons/icon_order_dispatched.png new file mode 100644 index 00000000..613bf845 Binary files /dev/null and b/assets/icons/icon_order_dispatched.png differ diff --git a/assets/icons/icon_order_placed.png b/assets/icons/icon_order_placed.png new file mode 100644 index 00000000..d69a3e32 Binary files /dev/null and b/assets/icons/icon_order_placed.png differ diff --git a/assets/icons/icon_order_shipped.png b/assets/icons/icon_order_shipped.png new file mode 100644 index 00000000..d76ad83c Binary files /dev/null and b/assets/icons/icon_order_shipped.png differ diff --git a/assets/icons/icon_payment.png b/assets/icons/icon_payment.png new file mode 100644 index 00000000..26a072d1 Binary files /dev/null and b/assets/icons/icon_payment.png differ diff --git a/assets/icons/icon_phone.png b/assets/icons/icon_phone.png new file mode 100644 index 00000000..f3667bf7 Binary files /dev/null and b/assets/icons/icon_phone.png differ diff --git a/assets/icons/icon_profile.png b/assets/icons/icon_profile.png new file mode 100644 index 00000000..b39cbbd9 Binary files /dev/null and b/assets/icons/icon_profile.png differ diff --git a/assets/icons/icon_search.png b/assets/icons/icon_search.png new file mode 100644 index 00000000..57158337 Binary files /dev/null and b/assets/icons/icon_search.png differ diff --git a/assets/icons/icon_share.png b/assets/icons/icon_share.png new file mode 100644 index 00000000..83afc77f Binary files /dev/null and b/assets/icons/icon_share.png differ diff --git a/assets/icons/icon_signout.png b/assets/icons/icon_signout.png new file mode 100644 index 00000000..f0a3f22a Binary files /dev/null and b/assets/icons/icon_signout.png differ diff --git a/assets/icons/icon_support.png b/assets/icons/icon_support.png new file mode 100644 index 00000000..2a9d83bc Binary files /dev/null and b/assets/icons/icon_support.png differ diff --git a/assets/icons/icon_whatsapp.png b/assets/icons/icon_whatsapp.png new file mode 100644 index 00000000..0696fa9b Binary files /dev/null and b/assets/icons/icon_whatsapp.png differ diff --git a/assets/icons/loader.gif b/assets/icons/loader.gif new file mode 100644 index 00000000..afeb11b6 Binary files /dev/null and b/assets/icons/loader.gif differ diff --git a/assets/icons/sheru.png b/assets/icons/sheru.png new file mode 100644 index 00000000..1508ae40 Binary files /dev/null and b/assets/icons/sheru.png differ diff --git a/assets/images/main_image.jpg b/assets/images/main_image.jpg new file mode 100644 index 00000000..c5c8fd0e Binary files /dev/null and b/assets/images/main_image.jpg differ diff --git a/emulator-data/auth_export/accounts.json b/emulator-data/auth_export/accounts.json new file mode 100644 index 00000000..46504a78 --- /dev/null +++ b/emulator-data/auth_export/accounts.json @@ -0,0 +1 @@ +{"kind":"identitytoolkit#DownloadAccountResponse","users":[{"localId":"2o4IqZAlTCNDvG5v1HR4y7sGXt8a","createdAt":"1724196478356","lastLoginAt":"1724197318611","providerUserInfo":[{"providerId":"phone","phoneNumber":"+919876543210","rawId":"+919876543210"}],"phoneNumber":"+919876543210","validSince":"1725104440","emailVerified":false,"disabled":false},{"localId":"MBiHTLHEnVEMZVScwqnwJoYRlwNX","createdAt":"1723826216262","lastLoginAt":"1723826216262","providerUserInfo":[{"providerId":"phone","phoneNumber":"+8955278952","rawId":"+8955278952"}],"phoneNumber":"+8955278952","validSince":"1725104440","emailVerified":false,"disabled":false},{"localId":"PbhEpQ93ptTJONgsIoRqIDkG33HP","createdAt":"1723936021229","lastLoginAt":"1723936021229","providerUserInfo":[{"providerId":"phone","phoneNumber":"+911235555555","rawId":"+911235555555"}],"phoneNumber":"+911235555555","validSince":"1725104440","emailVerified":false,"disabled":false},{"localId":"T5nmRtAtKOZIB7PgZkkOkDQlTWDu","createdAt":"1723935544529","lastLoginAt":"1723935793226","providerUserInfo":[{"providerId":"phone","phoneNumber":"+911234567890","rawId":"+911234567890"}],"phoneNumber":"+911234567890","validSince":"1725104440","emailVerified":false,"disabled":false},{"localId":"YibfATv35LFQXZ53xNqwvvQDvC5N","createdAt":"1723804151058","lastLoginAt":"1725116112701","providerUserInfo":[{"providerId":"phone","phoneNumber":"+918955278952","rawId":"+918955278952"}],"phoneNumber":"+918955278952","validSince":"1725104440","emailVerified":false,"disabled":false,"lastRefreshAt":"2024-08-31T15:50:12.062Z"},{"localId":"dZs1nag8rafrZ3660SIflLAUdkAs","createdAt":"1723902232917","lastLoginAt":"1724197291811","providerUserInfo":[{"providerId":"phone","phoneNumber":"+919324366823","rawId":"+919324366823"}],"phoneNumber":"+919324366823","validSince":"1725104440","emailVerified":false,"disabled":false},{"localId":"ejK7BJ9oMXkgVsqXoVU83I2krOgH","phoneNumber":"+918955278951","lastLoginAt":"1725115492319","createdAt":"1725115492319","providerUserInfo":[{"providerId":"phone","phoneNumber":"+918955278951","rawId":"+918955278951"}],"lastRefreshAt":"2024-08-31T14:44:52.331Z"}]} \ No newline at end of file diff --git a/emulator-data/auth_export/config.json b/emulator-data/auth_export/config.json new file mode 100644 index 00000000..6f240f7e --- /dev/null +++ b/emulator-data/auth_export/config.json @@ -0,0 +1 @@ +{"signIn":{"allowDuplicateEmails":false},"emailPrivacyConfig":{"enableImprovedEmailPrivacy":false}} \ No newline at end of file diff --git a/emulator-data/firebase-export-metadata.json b/emulator-data/firebase-export-metadata.json new file mode 100644 index 00000000..e915df29 --- /dev/null +++ b/emulator-data/firebase-export-metadata.json @@ -0,0 +1,16 @@ +{ + "version": "13.16.0", + "firestore": { + "version": "1.19.8", + "path": "firestore_export", + "metadata_file": "firestore_export/firestore_export.overall_export_metadata" + }, + "auth": { + "version": "13.16.0", + "path": "auth_export" + }, + "storage": { + "version": "13.16.0", + "path": "storage_export" + } +} \ No newline at end of file diff --git a/emulator-data/firestore_export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata b/emulator-data/firestore_export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata new file mode 100644 index 00000000..931082af Binary files /dev/null and b/emulator-data/firestore_export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata differ diff --git a/emulator-data/firestore_export/all_namespaces/all_kinds/output-0 b/emulator-data/firestore_export/all_namespaces/all_kinds/output-0 new file mode 100644 index 00000000..c83ce35c Binary files /dev/null and b/emulator-data/firestore_export/all_namespaces/all_kinds/output-0 differ diff --git a/emulator-data/firestore_export/firestore_export.overall_export_metadata b/emulator-data/firestore_export/firestore_export.overall_export_metadata new file mode 100644 index 00000000..8826a0ab Binary files /dev/null and b/emulator-data/firestore_export/firestore_export.overall_export_metadata differ diff --git a/emulator-data/storage_export/blobs/0a379120-8156-4a2a-8cbe-4dccf801f7f0 b/emulator-data/storage_export/blobs/0a379120-8156-4a2a-8cbe-4dccf801f7f0 new file mode 100644 index 00000000..af87dcc2 Binary files /dev/null and b/emulator-data/storage_export/blobs/0a379120-8156-4a2a-8cbe-4dccf801f7f0 differ diff --git a/emulator-data/storage_export/blobs/27a8be48-cf00-4d4b-8149-e4b6b336b96d b/emulator-data/storage_export/blobs/27a8be48-cf00-4d4b-8149-e4b6b336b96d new file mode 100644 index 00000000..2686ab60 Binary files /dev/null and b/emulator-data/storage_export/blobs/27a8be48-cf00-4d4b-8149-e4b6b336b96d differ diff --git a/emulator-data/storage_export/blobs/590e77a5-abf4-4c06-b545-9c1283d98fb0 b/emulator-data/storage_export/blobs/590e77a5-abf4-4c06-b545-9c1283d98fb0 new file mode 100644 index 00000000..26b15418 Binary files /dev/null and b/emulator-data/storage_export/blobs/590e77a5-abf4-4c06-b545-9c1283d98fb0 differ diff --git a/emulator-data/storage_export/blobs/65b07372-50c2-40ac-b22e-7d1dc935d8bc b/emulator-data/storage_export/blobs/65b07372-50c2-40ac-b22e-7d1dc935d8bc new file mode 100644 index 00000000..497997e5 --- /dev/null +++ b/emulator-data/storage_export/blobs/65b07372-50c2-40ac-b22e-7d1dc935d8bc @@ -0,0 +1,8 @@ +--boundary +Content-Type: application/json + +{"contentType":"text/plain"} +--boundary +Content-Type: text/plain + +--boundary-- diff --git a/emulator-data/storage_export/buckets.json b/emulator-data/storage_export/buckets.json new file mode 100644 index 00000000..87f9e328 --- /dev/null +++ b/emulator-data/storage_export/buckets.json @@ -0,0 +1,7 @@ +{ + "buckets": [ + { + "id": "sheru-2eb0f.appspot.com" + } + ] +} \ No newline at end of file diff --git a/emulator-data/storage_export/metadata/0a379120-8156-4a2a-8cbe-4dccf801f7f0.json b/emulator-data/storage_export/metadata/0a379120-8156-4a2a-8cbe-4dccf801f7f0.json new file mode 100644 index 00000000..5d48aecb --- /dev/null +++ b/emulator-data/storage_export/metadata/0a379120-8156-4a2a-8cbe-4dccf801f7f0.json @@ -0,0 +1,19 @@ +{ + "name": "offers/jaipuroff.png", + "bucket": "sheru-2eb0f.appspot.com", + "metageneration": 1, + "generation": 1723920111416, + "contentType": "image/png", + "storageClass": "STANDARD", + "contentDisposition": "inline", + "downloadTokens": [ + "0db860a4-af9d-42da-a339-db98d0e11906" + ], + "etag": "d+hZfkrqiMNWE8feNtOo1kTczsw", + "customMetadata": {}, + "timeCreated": "2024-08-17T18:41:51.416Z", + "updated": "2024-08-17T18:41:51.416Z", + "size": 11516356, + "md5Hash": "u6gSjg5RJteuMcEHHk0SWA==", + "crc32c": "968695224" +} \ No newline at end of file diff --git a/emulator-data/storage_export/metadata/27a8be48-cf00-4d4b-8149-e4b6b336b96d.json b/emulator-data/storage_export/metadata/27a8be48-cf00-4d4b-8149-e4b6b336b96d.json new file mode 100644 index 00000000..ad8dd3ea --- /dev/null +++ b/emulator-data/storage_export/metadata/27a8be48-cf00-4d4b-8149-e4b6b336b96d.json @@ -0,0 +1,19 @@ +{ + "name": "offers/thaneoff.png", + "bucket": "sheru-2eb0f.appspot.com", + "metageneration": 1, + "generation": 1723920111752, + "contentType": "image/png", + "storageClass": "STANDARD", + "contentDisposition": "inline", + "downloadTokens": [ + "81617c14-3427-48be-aa61-bdb8d9f55816" + ], + "etag": "MaHojOH+wuWwC03gA2l1zoabmjU", + "customMetadata": {}, + "timeCreated": "2024-08-17T18:41:51.753Z", + "updated": "2024-08-17T18:41:51.753Z", + "size": 11510211, + "md5Hash": "fNRTeN20xGzrKYJth011Zw==", + "crc32c": "2788926427" +} \ No newline at end of file diff --git a/emulator-data/storage_export/metadata/590e77a5-abf4-4c06-b545-9c1283d98fb0.json b/emulator-data/storage_export/metadata/590e77a5-abf4-4c06-b545-9c1283d98fb0.json new file mode 100644 index 00000000..bcf45074 --- /dev/null +++ b/emulator-data/storage_export/metadata/590e77a5-abf4-4c06-b545-9c1283d98fb0.json @@ -0,0 +1,19 @@ +{ + "name": "offers/mumbaioff.png", + "bucket": "sheru-2eb0f.appspot.com", + "metageneration": 1, + "generation": 1723920111616, + "contentType": "image/png", + "storageClass": "STANDARD", + "contentDisposition": "inline", + "downloadTokens": [ + "80537372-ae12-4439-a8ac-2c21475497f6" + ], + "etag": "rZrnBGqguIjwTueF/hKcP4UZqbc", + "customMetadata": {}, + "timeCreated": "2024-08-17T18:41:51.616Z", + "updated": "2024-08-17T18:41:51.616Z", + "size": 11529849, + "md5Hash": "hL7Glbq68YrUzqDnLbr+xA==", + "crc32c": "2962489906" +} \ No newline at end of file diff --git a/emulator-data/storage_export/metadata/65b07372-50c2-40ac-b22e-7d1dc935d8bc.json b/emulator-data/storage_export/metadata/65b07372-50c2-40ac-b22e-7d1dc935d8bc.json new file mode 100644 index 00000000..63ceed9c --- /dev/null +++ b/emulator-data/storage_export/metadata/65b07372-50c2-40ac-b22e-7d1dc935d8bc.json @@ -0,0 +1,15 @@ +{ + "name": "offers/", + "bucket": "sheru-2eb0f.appspot.com", + "metageneration": 1, + "generation": 1723920081089, + "contentType": "application/octet-stream", + "storageClass": "STANDARD", + "downloadTokens": [], + "etag": "J4lOEN7ye+i5ng2JhIgjEZVVCIU", + "timeCreated": "2024-08-17T18:41:21.089Z", + "updated": "2024-08-17T18:41:21.089Z", + "size": 130, + "md5Hash": "iUpceipDkyQhwwUflucD5w==", + "crc32c": "3108464614" +} \ No newline at end of file diff --git a/functions/.gitignore b/functions/.gitignore new file mode 100644 index 00000000..9be0f014 --- /dev/null +++ b/functions/.gitignore @@ -0,0 +1,10 @@ +# Compiled JavaScript files +lib/**/*.js +lib/**/*.js.map + +# TypeScript v1 declaration files +typings/ + +# Node.js dependency directory +node_modules/ +*.local \ No newline at end of file diff --git a/functions/package-lock.json b/functions/package-lock.json new file mode 100644 index 00000000..d78ff9ed --- /dev/null +++ b/functions/package-lock.json @@ -0,0 +1,5929 @@ +{ + "name": "functions", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "functions", + "dependencies": { + "firebase-admin": "^12.1.0", + "firebase-functions": "^5.0.0" + }, + "devDependencies": { + "firebase-functions-test": "^3.1.0", + "typescript": "^4.9.0" + }, + "engines": { + "node": "18" + } + }, + "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, + "peer": true, + "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.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", + "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dev": true, + "peer": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "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/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "peer": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "peer": true + }, + "node_modules/@babel/generator": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", + "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.25.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", + "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "peer": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "peer": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "peer": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "peer": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "peer": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", + "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.25.2" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", + "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.2", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "peer": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "peer": true + }, + "node_modules/@babel/types": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "peer": true + }, + "node_modules/@fastify/busboy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.0.0.tgz", + "integrity": "sha512-83rnH2nCvclWaPQQKvkJ2pdOjG4TZyEVuFDnlOF6KP08lDaaceVyw/W63mDuafQT+MKHCvXIPpE5uYWeM0rT4w==" + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.2.tgz", + "integrity": "sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ==" + }, + "node_modules/@firebase/app-types": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.2.tgz", + "integrity": "sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ==" + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.3.tgz", + "integrity": "sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ==" + }, + "node_modules/@firebase/component": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.8.tgz", + "integrity": "sha512-LcNvxGLLGjBwB0dJUsBGCej2fqAepWyBubs4jt1Tiuns7QLbXHuyObZ4aMeBjZjWx4m8g1LoVI9QFpSaq/k4/g==", + "dependencies": { + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.7.tgz", + "integrity": "sha512-wjXr5AO8RPxVVg7rRCYffT7FMtBjHRfJ9KMwi19MbOf0vBf0H9YqW3WCgcnLpXI6ehiUcU3z3qgPnnU0nK6SnA==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.2", + "@firebase/auth-interop-types": "0.2.3", + "@firebase/component": "0.6.8", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.7", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.7.tgz", + "integrity": "sha512-R/3B+VVzEFN5YcHmfWns3eitA8fHLTL03io+FIoMcTYkajFnrBdS3A+g/KceN9omP7FYYYGTQWF9lvbEx6eMEg==", + "dependencies": { + "@firebase/component": "0.6.8", + "@firebase/database": "1.0.7", + "@firebase/database-types": "1.0.4", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.4.tgz", + "integrity": "sha512-mz9ZzbH6euFXbcBo+enuJ36I5dR5w+enJHHjy9Y5ThCdKUseqfDjW3vCp1YxE9zygFCSjJJ/z1cQ+zodvUcwPQ==", + "dependencies": { + "@firebase/app-types": "0.9.2", + "@firebase/util": "1.9.7" + } + }, + "node_modules/@firebase/logger": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.2.tgz", + "integrity": "sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/util": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.7.tgz", + "integrity": "sha512-fBVNH/8bRbYjqlbIhZ+lBtdAAS4WqZumx03K06/u7fJSpz1TGjEMm1ImvKD47w+xaFKIP2ori6z8BrbakRfjJA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@google-cloud/firestore": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.9.0.tgz", + "integrity": "sha512-c4ALHT3G08rV7Zwv8Z2KG63gZh66iKdhCBeDfCpIkLrjX6EAjTD/szMdj14M+FnQuClZLFfW5bAgoOjfNmLtJg==", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^4.3.3", + "protobufjs": "^7.2.6" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", + "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", + "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.12.1.tgz", + "integrity": "sha512-Z3ZzOnF3YKLuvpkvF+TjQ6lztxcAyTILp+FjKonmVpEwPa9vFvxpZjubLR4sB6bf19i/8HL2AXRjA0YFgHFRmQ==", + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "^4.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "duplexify": "^4.1.3", + "fast-xml-parser": "^4.4.1", + "gaxios": "^6.0.2", + "google-auth-library": "^9.6.3", + "html-entities": "^2.5.2", + "mime": "^3.0.0", + "p-limit": "^3.0.1", + "retry-request": "^7.0.0", + "teeny-request": "^9.0.0", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.11.1.tgz", + "integrity": "sha512-gyt/WayZrVPH2w/UTLansS7F9Nwld472JxxaETamrM8HNlsa+jSLNyKAZmhxI2Me4c3mQHFiS1wWHDY1g1Kthw==", + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "optional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "peer": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "peer": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "peer": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "peer": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "peer": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "peer": true, + "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, + "peer": true, + "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, + "peer": true, + "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, + "peer": true + }, + "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, + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "peer": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "peer": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "peer": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "optional": true + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", + "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "peer": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "peer": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "peer": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz", + "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "dev": true + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "optional": true + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "node_modules/@types/node": { + "version": "22.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.3.0.tgz", + "integrity": "sha512-nrWpWVaDZuaVc5X84xJ0vNrLvomM205oQyLsRt7OHNZbSHslcWsvgFR7O7hire2ZonjLrWBbedmotmIlJDVd6g==", + "dependencies": { + "undici-types": "~6.18.2" + } + }, + "node_modules/@types/qs": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + }, + "node_modules/@types/request": { + "version": "2.48.12", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", + "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", + "optional": true, + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "peer": true + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "optional": true + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "peer": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "peer": true + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "optional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "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==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "optional": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "peer": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "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==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "peer": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "peer": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "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==" + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "optional": true + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "peer": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "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, + "peer": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "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.11.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/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "peer": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "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" + } + ], + "peer": true, + "dependencies": { + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "peer": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "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==", + "dev": true, + "peer": true + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "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" + } + ], + "peer": true + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", + "dev": true, + "peer": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "devOptional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "peer": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "peer": true + }, + "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==", + "devOptional": true, + "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==", + "devOptional": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "optional": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "peer": true + }, + "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==", + "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==", + "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, + "peer": true + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "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==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "peer": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "peer": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "optional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "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==", + "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==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "optional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "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==" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.9", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.9.tgz", + "integrity": "sha512-HfkT8ndXR0SEkU8gBQQM3rz035bpE/hxkZ1YIt4KJPEFES68HfIU6LzKukH0H794Lm83WJtkSAMfEToxCs15VA==", + "dev": true, + "peer": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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==", + "devOptional": true + }, + "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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "peer": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "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==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "devOptional": true, + "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==" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "peer": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "peer": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "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" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "optional": true + }, + "node_modules/farmhash-modern": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/farmhash-modern/-/farmhash-modern-1.1.0.tgz", + "integrity": "sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA==", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "optional": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "peer": true + }, + "node_modules/fast-xml-parser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "optional": true, + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "peer": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "peer": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "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/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "peer": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-admin": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-12.3.1.tgz", + "integrity": "sha512-vEr3s3esl8nPIA9r/feDT4nzIXCfov1CyyCSpMQWp6x63Q104qke0MEGZlrHUZVROtl8FLus6niP/M9I1s4VBA==", + "dependencies": { + "@fastify/busboy": "^3.0.0", + "@firebase/database-compat": "^1.0.2", + "@firebase/database-types": "^1.0.0", + "@types/node": "^22.0.1", + "farmhash-modern": "^1.1.0", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.1.0", + "node-forge": "^1.3.1", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^7.7.0", + "@google-cloud/storage": "^7.7.0" + } + }, + "node_modules/firebase-functions": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-5.0.1.tgz", + "integrity": "sha512-1m+crtgAR8Tl36gjpM02KCY5zduAejFmDSXvih/DB93apg39f0U/WwRgT7sitGIRqyCcIpktNUbXJv7Y9JOF4A==", + "dependencies": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "protobufjs": "^7.2.2" + }, + "bin": { + "firebase-functions": "lib/bin/firebase-functions.js" + }, + "engines": { + "node": ">=14.10.0" + }, + "peerDependencies": { + "firebase-admin": "^11.10.0 || ^12.0.0" + } + }, + "node_modules/firebase-functions-test": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/firebase-functions-test/-/firebase-functions-test-3.3.0.tgz", + "integrity": "sha512-X+OOA34MGrsTimFXTDnWT0psAqnmBkJ85bGCoLMwjgei5Prfkqh3bv5QASnXC/cmIVBSF2Qw9uW1+mF/t3kFlw==", + "dev": true, + "dependencies": { + "@types/lodash": "^4.14.104", + "lodash": "^4.17.5", + "ts-deepmerge": "^2.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "firebase-admin": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "firebase-functions": ">=4.9.0", + "jest": ">=28.0.0" + } + }, + "node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "optional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "peer": true + }, + "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, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "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==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "optional": true + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "optional": true, + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "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, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "devOptional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "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, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/google-auth-library": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.13.0.tgz", + "integrity": "sha512-p9Y03Uzp/Igcs36zAaB0XTSwZ8Y0/tpYiz5KIde5By+H9DCVUSYtDWZu6aFXsWTqENMb8BD/pDT3hR8NVrPkfA==", + "optional": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.3.9.tgz", + "integrity": "sha512-tcjQr7sXVGMdlvcG25wSv98ap1dtF4Z6mcV0rztGIddOcezw4YMb/uTXg72JPrLep+kXcVjaJjg6oo3KLf4itQ==", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "^1.10.9", + "@grpc/proto-loader": "^0.7.13", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^9.3.0", + "node-fetch": "^2.7.0", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^2.0.2", + "protobufjs": "^7.3.2", + "retry-request": "^7.0.0", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "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, + "peer": true + }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "optional": true, + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "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==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "optional": true + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "peer": true + }, + "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==", + "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/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "optional": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10.17.0" + } + }, + "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==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "peer": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "peer": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "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==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "peer": true + }, + "node_modules/is-core-module": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", + "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "dev": true, + "peer": true, + "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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "peer": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "peer": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "peer": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "peer": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "peer": true + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "peer": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "peer": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "peer": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "peer": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "peer": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "peer": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "peer": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "peer": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "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, + "peer": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "peer": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "peer": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "optional": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "peer": true + }, + "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, + "peer": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/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==" + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "optional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz", + "integrity": "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==", + "dependencies": { + "@types/express": "^4.17.17", + "@types/jsonwebtoken": "^9.0.2", + "debug": "^4.3.4", + "jose": "^4.14.6", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jwks-rsa/node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/jwks-rsa/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/jwks-rsa/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "optional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "peer": true + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "peer": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "optional": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "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, + "peer": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru-memoizer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "peer": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "peer": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "peer": true + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "peer": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "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==", + "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==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "peer": true + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "peer": true + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true, + "peer": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "peer": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "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==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "devOptional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "peer": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "devOptional": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "peer": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "peer": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "peer": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true, + "peer": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "peer": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "peer": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proto3-json-serializer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", + "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", + "optional": true, + "dependencies": { + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.2.tgz", + "integrity": "sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "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==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "peer": true + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "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==", + "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==", + "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-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "peer": true + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "peer": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "peer": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "optional": true, + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "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" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "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, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "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/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/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==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "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, + "peer": true, + "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, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "peer": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "peer": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "peer": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "peer": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "optional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "optional": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "peer": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "devOptional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "devOptional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "optional": true + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "optional": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "optional": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/teeny-request/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/teeny-request/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "peer": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "peer": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "peer": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "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==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "optional": true + }, + "node_modules/ts-deepmerge": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-2.0.7.tgz", + "integrity": "sha512-3phiGcxPSSR47RBubQxPoZ+pqXsEsozLo4G4AlSrsMKTFg9TA3l+3he5BqpUi9wiuDbaHWXH/amlzQ49uEdXtg==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/undici-types": { + "version": "6.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.18.2.tgz", + "integrity": "sha512-5ruQbENj95yDYJNS3TvcaxPMshV7aizdv/hWYjGIKoANWKjhWNBsr2YEuYZKodQulB1b8l7ILOuDQep3afowQQ==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "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" + } + ], + "peer": true, + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "optional": true + }, + "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==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "peer": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "optional": true + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "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, + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/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==", + "devOptional": true, + "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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "devOptional": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "peer": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "peer": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "devOptional": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "devOptional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/functions/package.json b/functions/package.json new file mode 100644 index 00000000..e27e3f2f --- /dev/null +++ b/functions/package.json @@ -0,0 +1,25 @@ +{ + "name": "functions", + "scripts": { + "build": "tsc", + "build:watch": "tsc --watch", + "serve": "npm run build && firebase emulators:start --only functions", + "shell": "npm run build && firebase functions:shell", + "start": "npm run shell", + "deploy": "firebase deploy --only functions", + "logs": "firebase functions:log" + }, + "engines": { + "node": "18" + }, + "main": "lib/index.js", + "dependencies": { + "firebase-admin": "^12.1.0", + "firebase-functions": "^5.0.0" + }, + "devDependencies": { + "typescript": "^4.9.0", + "firebase-functions-test": "^3.1.0" + }, + "private": true +} \ No newline at end of file diff --git a/functions/src/config.ts b/functions/src/config.ts new file mode 100644 index 00000000..de4dac95 --- /dev/null +++ b/functions/src/config.ts @@ -0,0 +1,7 @@ +import * as admin from "firebase-admin"; + +if (!admin.apps.length) { + admin.initializeApp(); +} + +export { admin }; diff --git a/functions/src/helpers/firebase_globals.ts b/functions/src/helpers/firebase_globals.ts new file mode 100644 index 00000000..8b67a793 --- /dev/null +++ b/functions/src/helpers/firebase_globals.ts @@ -0,0 +1,13 @@ +import { admin } from "../config"; + +export const ordersRef = () => { + return admin.firestore().collection("orders"); +}; + +export const usersRef = () => { + return admin.firestore().collection("users"); +}; + +export const notificationsRef = () => { + return admin.firestore().collection("notifications"); +}; diff --git a/functions/src/helpers/get_user.ts b/functions/src/helpers/get_user.ts new file mode 100644 index 00000000..ac7ee358 --- /dev/null +++ b/functions/src/helpers/get_user.ts @@ -0,0 +1,6 @@ +import { usersRef } from "./firebase_globals"; + +export async function getUser(userID: string) { + const userSnapshot = await usersRef().doc(userID).get(); + return userSnapshot.exists ? userSnapshot.data() : null; +} diff --git a/functions/src/helpers/interfaces.ts b/functions/src/helpers/interfaces.ts new file mode 100644 index 00000000..99358f1f --- /dev/null +++ b/functions/src/helpers/interfaces.ts @@ -0,0 +1,30 @@ +//order type +export interface Order { + id: string; + userID: string; + currentStatus: string; +} +//user type +export interface User { + id: string; + phoneNumber: string; + fcmTokens: string[]; + fullName: string; +} +//notification type +export interface NotificationData { + id: string; + userID: string; + title: string; + body: string; + isRead: boolean; + imageUrl: string | null; + notificationType: string; + url: string; +} +//product type +export interface Product { + id: string; + name_en: string; + images: string[]; +} diff --git a/functions/src/helpers/message_data.ts b/functions/src/helpers/message_data.ts new file mode 100644 index 00000000..a5261d53 --- /dev/null +++ b/functions/src/helpers/message_data.ts @@ -0,0 +1,49 @@ +export function createNotificationMessage( + status: string, + tokens: string[], + routeName: string, + parameterID: string +) { + let title = "Order Update!"; + let body = ""; + + switch (status) { + case "placed": + title = "Order Placed!"; + body = "Exciting news! Your order has been placed!"; + break; + case "processed": + title = "Order Processed!"; + body = + "Exciting news! Your order has been processed and is getting ready!"; + break; + case "shipped": + title = "Order Shipped!"; + body = "Great! Your order is on its way!"; + break; + case "delivered": + title = "Order Delivered!"; + body = "Hooray! Your order has been delivered! Enjoy your purchase!"; + break; + case "cancelled": + title = "Order Cancelled"; + body = + "We're sorry. Your order has been cancelled. If you have any questions, please contact us."; + break; + default: + body = `Your order status has changed to ${status}.`; + } + + return { + notification: { + title: title, + body: body, + }, + data: { + routeName: routeName, + parameterID: parameterID, + click_action: "FLUTTER_NOTIFICATION_CLICK", + }, + tokens: tokens, + }; +} diff --git a/functions/src/index.ts b/functions/src/index.ts new file mode 100644 index 00000000..6e557c0e --- /dev/null +++ b/functions/src/index.ts @@ -0,0 +1,4 @@ +import { onOrderStatusUpdate } from "./order_status_change"; +import { onOrderCreate } from "./order_create"; + +export { onOrderStatusUpdate, onOrderCreate }; diff --git a/functions/src/order_create.ts b/functions/src/order_create.ts new file mode 100644 index 00000000..b1926924 --- /dev/null +++ b/functions/src/order_create.ts @@ -0,0 +1,58 @@ +import * as functions from "firebase-functions"; +import { admin } from "./config"; +import { createNotificationMessage } from "./helpers/message_data"; +import { getUser } from "./helpers/get_user"; +import { notificationsRef } from "./helpers/firebase_globals"; +import { Order, User, NotificationData } from "./helpers/interfaces"; + +export const onOrderCreate = functions.firestore + .document("orders/{orderId}") + .onCreate(async (snapshot, context) => { + const order = snapshot.data() as Order; + const user = (await getUser(order.userID)) as User; + + if (!user) { + console.error(`User with ID ${order.userID} not found`); + return; + } + + await Promise.all([ + sendOrderPlacedNotification(order, context.eventId, user), + ]); + }); + +async function sendOrderPlacedNotification( + order: Order, + eventID: string, + user: User +) { + try { + if (user.fcmTokens.length > 0) { + const message = createNotificationMessage( + order.currentStatus, + user.fcmTokens, + "orderDetail", + order.id + ); + + const notificationData: NotificationData = { + id: eventID, + userID: user.id, + title: message.notification.title, + body: message.notification.body, + isRead: false, + imageUrl: null, + notificationType: "order", + url: `/orderDetail?id=${order.id}`, + }; + + await notificationsRef().doc(eventID).set(notificationData); + console.log("Notification data stored in Firestore."); + + const response = await admin.messaging().sendEachForMulticast(message); + console.log("Notification sent successfully:", response); + } + } catch (error) { + console.error("Error sending order placed notification:", error); + } +} diff --git a/functions/src/order_status_change.ts b/functions/src/order_status_change.ts new file mode 100644 index 00000000..79e15131 --- /dev/null +++ b/functions/src/order_status_change.ts @@ -0,0 +1,62 @@ +import * as functions from "firebase-functions"; +import { admin } from "./config"; +import { createNotificationMessage } from "./helpers/message_data"; +import { getUser } from "./helpers/get_user"; +import { notificationsRef } from "./helpers/firebase_globals"; +import { Order, User, NotificationData } from "./helpers/interfaces"; + +export const onOrderStatusUpdate = functions.firestore + .document("orders/{orderId}") + .onUpdate(async (change, context) => { + const newValue = change.after.data() as Order; + const oldValue = change.before.data() as Order; + + if (newValue.currentStatus !== oldValue.currentStatus) { + const user = (await getUser(newValue.userID)) as User; + + if (!user) { + console.error(`User with ID ${newValue.userID} not found`); + return; + } + + await Promise.all([ + sendOrderStatusNotification(newValue, user, context.eventId), + ]); + } + }); + +async function sendOrderStatusNotification( + order: Order, + user: User, + eventID: string +): Promise { + try { + if (user.fcmTokens.length > 0) { + const message = createNotificationMessage( + order.currentStatus, + user.fcmTokens, + "orderDetail", + order.id + ); + + const notificationData: NotificationData = { + id: eventID, + userID: user.id, + title: message.notification.title, + body: message.notification.body, + isRead: false, + imageUrl: null, + notificationType: "order", + url: `/orderDetail?id=${order.id}`, + }; + + await notificationsRef().doc(eventID).set(notificationData); + console.log("Notification data stored in Firestore."); + + const response = await admin.messaging().sendEachForMulticast(message); + console.log("Notification sent successfully:", response); + } + } catch (error) { + console.error("Error sending order status notification:", error); + } +} diff --git a/functions/tsconfig.json b/functions/tsconfig.json new file mode 100644 index 00000000..7ce05d03 --- /dev/null +++ b/functions/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "commonjs", + "noImplicitReturns": true, + "noUnusedLocals": true, + "outDir": "lib", + "sourceMap": true, + "strict": true, + "target": "es2017" + }, + "compileOnSave": true, + "include": [ + "src" + ] +} diff --git a/lib/app/modules/auth/bindings/auth_bindings.dart b/lib/app/modules/auth/bindings/auth_bindings.dart new file mode 100644 index 00000000..1329b197 --- /dev/null +++ b/lib/app/modules/auth/bindings/auth_bindings.dart @@ -0,0 +1,11 @@ +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; + +class AuthBindings extends Bindings { + @override + void dependencies() { + Get.lazyPut( + () => AuthController(), + ); + } +} diff --git a/lib/app/modules/auth/controllers/auth_controller.dart b/lib/app/modules/auth/controllers/auth_controller.dart new file mode 100644 index 00000000..5b15c6fd --- /dev/null +++ b/lib/app/modules/auth/controllers/auth_controller.dart @@ -0,0 +1,126 @@ +import 'package:get/get.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; +import 'package:get_flutter_fire/models/user_model.dart'; +import 'package:get_flutter_fire/constants.dart'; +import 'package:get_flutter_fire/services/get_storage_service.dart'; +import 'package:get_flutter_fire/app/modules/seller/controllers/seller_controller.dart'; + +class AuthController extends GetxController { + final GetStorageService _storageService = GetStorageService(); + + var isLoading = false.obs; + final Rxn _user = Rxn(); + + UserModel? get user => _user.value; + + Rxn get currentUser => _user; + + @override + void onInit() { + super.onInit(); + _loadUserData(); + _checkForUserRoleUpdate(); + } + + void _loadUserData() { + final storedUser = _storageService.getUserData(); + if (storedUser != null) { + _user.value = storedUser; + } + } + + Future _checkForUserRoleUpdate() async { + if (_user.value == null) return; + + isLoading.value = true; + try { + var doc = await usersRef.doc(_user.value!.id).get(); + if (doc.exists) { + UserModel updatedUser = UserModel.fromMap(doc.data()!); + if (updatedUser.userType != _user.value!.userType) { + _user.value = updatedUser; + _storageService.saveUserData(updatedUser); + + if (updatedUser.userType == UserType.seller) { + final SellerController sellerController = + Get.put(SellerController()); + await sellerController.onUserRoleChanged(updatedUser); + } + } + } + } catch (error) { + _handleError('Failed to check user role'); + } finally { + isLoading.value = false; + } + } + + Future fetchUserData(String userID) async { + isLoading.value = true; + try { + var doc = await usersRef.doc(userID).get(); + if (doc.exists) { + _user.value = UserModel.fromMap(doc.data()!); + _storageService.saveUserData(_user.value!); + + if (_user.value!.userType == UserType.seller) { + final SellerController sellerController = Get.put(SellerController()); + await sellerController.onUserRoleChanged(_user.value!); + } + } + } catch (error) { + _user.value = null; + _handleError('Failed to fetch user data'); + } finally { + isLoading.value = false; + } + } + + Future registerUser(UserModel user) async { + isLoading.value = true; + try { + await usersRef.doc(user.id).set(user.toMap()); + _user.value = user; + _storageService.saveUserData(user); + } catch (error) { + _handleError('Failed to register user'); + } finally { + isLoading.value = false; + } + } + + // Updated method to set default address + Future updateDefaultAddressID(String addressID) async { + if (_user.value == null) return; + + try { + await usersRef + .doc(_user.value!.id) + .update({'defaultAddressID': addressID}); + _user.value = _user.value!.copyWith(defaultAddressID: addressID); + _storageService.saveUserData(_user.value!); + } catch (e) { + _handleError('Failed to update user address: $e'); + } + } + + // Updated method to handle user address + Future updateUserAddress(UserModel user, String addressID) async { + try { + await usersRef.doc(user.id).update({'defaultAddressID': addressID}); + _user.value = _user.value!.copyWith(defaultAddressID: addressID); + _storageService.saveUserData(_user.value!); + } catch (e) { + _handleError('Failed to update user address: $e'); + } + } + + void _handleError(String message) { + Get.snackbar('Error', message); + } + + void clearUserData() { + _user.value = null; + _storageService.clearUserData(); + } +} diff --git a/lib/app/modules/auth/controllers/login_controller.dart b/lib/app/modules/auth/controllers/login_controller.dart new file mode 100644 index 00000000..69bcd32d --- /dev/null +++ b/lib/app/modules/auth/controllers/login_controller.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/services/auth_service.dart'; + +// ui contollers +class LoginController extends GetxController { + final TextEditingController phoneController = TextEditingController(); + + final isDisabled = true.obs; + final isInitialPosition = true.obs; + final phoneNumber = ''.obs; + + @override + void onInit() { + super.onInit(); + phoneController.addListener(_checkRequiredFields); + + Future.delayed(const Duration(milliseconds: 400), () { + isInitialPosition.value = false; + }); + } + + void _checkRequiredFields() { + final phone = phoneController.text; + + isDisabled.value = phone.length != 10; + phoneNumber.value = phone; + } + + void verifyPhoneNumber(AuthService authService) { + authService.verifyPhoneNumber("+91${phoneController.text}"); + } + + @override + void onClose() { + phoneController.dispose(); + super.onClose(); + } +} diff --git a/lib/app/modules/auth/controllers/otp_controller.dart b/lib/app/modules/auth/controllers/otp_controller.dart new file mode 100644 index 00000000..d7e21104 --- /dev/null +++ b/lib/app/modules/auth/controllers/otp_controller.dart @@ -0,0 +1,83 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/services/auth_service.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/widgets/common/show_loader.dart'; +import 'package:get_flutter_fire/app/widgets/common/show_toast.dart'; + +class OtpController extends GetxController { + final AuthController authController = Get.find(); + final AuthService authService = Get.find(); + + var otp = ''.obs; + var isDisabled = true.obs; + var timerSeconds = 30.obs; + var isResendButtonEnabled = false.obs; + + Timer? _resendTimer; + + @override + void onInit() { + super.onInit(); + startTimer(); + } + + void startTimer() { + _resendTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (timerSeconds.value > 0) { + timerSeconds.value--; + } else { + isResendButtonEnabled.value = true; + _resendTimer?.cancel(); + } + }); + } + + void checkOtpFields() { + isDisabled.value = otp.value.length != 6; + } + + Future submitOtp() async { + if (otp.value.isEmpty || otp.value.length < 6) { + showToast('Please enter OTP'); + return; + } + + showLoader(); + if (kDebugMode) { + print("Phone number: ${authService.phoneNumber}"); + } + + bool success = await authService.verifyOTP(otp.value); + dismissLoader(); + + if (success) { + await authController.fetchUserData(authService.userID); + if (authController.user == null) { + Get.offNamed(Routes.REGISTER, + arguments: {'phoneNumber': authService.phoneNumber}); + } else { + Get.offAllNamed(Routes.ROOT); // to remove screens behind home screens + } + } else { + showToast('Invalid OTP'); + } + } + + void resendCode() { + isResendButtonEnabled.value = false; + timerSeconds.value = 30; + startTimer(); + + // Logic to resend OTP + authService.verifyPhoneNumber(authService.phoneNumber); + } + + @override + void onClose() { + _resendTimer?.cancel(); + super.onClose(); + } +} diff --git a/lib/app/modules/auth/controllers/regster_controller.dart b/lib/app/modules/auth/controllers/regster_controller.dart new file mode 100644 index 00000000..4c80b1f8 --- /dev/null +++ b/lib/app/modules/auth/controllers/regster_controller.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; +import 'package:get_flutter_fire/models/user_model.dart'; +import 'package:get_flutter_fire/services/auth_service.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; + +class RegisterController extends GetxController { + final AuthService authService = Get.find(); + + final nameController = TextEditingController(); + final emailController = TextEditingController(); + final businessNameController = TextEditingController(); + final businessTypeController = TextEditingController(); + final gstNumberController = TextEditingController(); + final panNumberController = TextEditingController(); + + final isBusiness = false.obs; + + void toggleBusiness(bool value) { + isBusiness.value = value; + } + + void registerUser(String phoneNumber) { + String userID = authService.userID; + + UserModel user = UserModel( + id: userID, + name: nameController.text, + phoneNumber: phoneNumber, + email: emailController.text.isEmpty ? null : emailController.text, + isBusiness: isBusiness.value, + businessName: isBusiness.value ? businessNameController.text : null, + businessType: isBusiness.value ? businessTypeController.text : null, + gstNumber: isBusiness.value ? gstNumberController.text : null, + panNumber: isBusiness.value ? panNumberController.text : null, + userType: UserType.buyer, + defaultAddressID: '', + createdAt: DateTime.now(), + lastSeenAt: DateTime.now(), + ); + + Get.find().registerUser(user); + Get.toNamed(Routes.ADDRESS, arguments: {'user': user}); + } + + @override + void onClose() { + nameController.dispose(); + emailController.dispose(); + businessNameController.dispose(); + businessTypeController.dispose(); + gstNumberController.dispose(); + panNumberController.dispose(); + super.onClose(); + } +} diff --git a/lib/app/modules/auth/views/address_screen.dart b/lib/app/modules/auth/views/address_screen.dart new file mode 100644 index 00000000..46016811 --- /dev/null +++ b/lib/app/modules/auth/views/address_screen.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/profile/controllers/address_controller.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_textfield.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_button.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class AddressScreen extends StatelessWidget { + final AddressController controller = Get.put(AddressController()); + + AddressScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: const BoxDecoration( + gradient: AppTheme.primaryGradient, + ), + padding: AppTheme.paddingDefault, + child: Center( + child: SingleChildScrollView( + child: Card( + shape: AppTheme.rrShape, + elevation: 10, + shadowColor: AppTheme.colorBlack.withOpacity(0.15), + child: Padding( + padding: const EdgeInsets.all(AppTheme.spacingLarge), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Address Details', + style: AppTheme.fontStyleLarge.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + ), + const Spacing(size: AppTheme.spacingMedium), + CustomTextField( + labelText: 'Address Line 1', + controller: controller.line1Controller, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Address Line 2', + controller: controller.line2Controller, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'City', + controller: controller.cityController, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'District', + controller: controller.districtController, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Latitude (optional)', + keyboardType: TextInputType.number, + controller: controller.latitudeController, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Longitude (optional)', + keyboardType: TextInputType.number, + controller: controller.longitudeController, + ), + const Spacing(size: AppTheme.spacingExtraLarge), + CustomButton( + onPressed: controller.saveAddress, + text: 'Save Address', + isDisabled: false, + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/app/modules/auth/views/login_screen.dart b/lib/app/modules/auth/views/login_screen.dart new file mode 100644 index 00000000..e62df13d --- /dev/null +++ b/lib/app/modules/auth/views/login_screen.dart @@ -0,0 +1,102 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_button.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_phone_textfield.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/services/auth_service.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/theme/assets.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/login_controller.dart'; + +class LoginScreen extends StatelessWidget { + const LoginScreen({super.key}); + + @override + Widget build(BuildContext context) { + final authService = Get.find(); + final LoginController loginController = Get.put(LoginController()); + + return Scaffold( + backgroundColor: AppTheme.colorWhite, + body: Padding( + padding: AppTheme.paddingDefault, + child: Stack( + children: [ + Obx(() => AnimatedPositioned( + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + top: loginController.isInitialPosition.value + ? -MediaQuery.of(context).size.height * 0.628 + : MediaQuery.of(context).size.height * 0.05, + left: 0, + right: 0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Center( + child: Image.asset( + logo, + height: 100, + width: 100, + color: AppTheme.colorMain, + ), + ), + const Spacing(size: AppTheme.spacingLarge), + Center( + child: Text('Enter Number', + style: AppTheme.fontStyleLarge.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + )), + ), + const SizedBox(height: 10), + Obx(() => Center( + child: RichText( + text: TextSpan( + text: 'An OTP will be sent to this number: ', + style: AppTheme.fontStyleSmall.copyWith( + color: AppTheme.greyTextColor, + ), + children: [ + TextSpan( + text: loginController.phoneNumber.value, + style: AppTheme.fontStyleSmall.copyWith( + color: AppTheme.colorMain, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + )), + const SizedBox(height: 10), + Container( + decoration: AppTheme.cardDecoration, + child: PhoneTextField( + hintText: 'Phone Number', + readOnly: false, + controller: loginController.phoneController, + ), + ), + const SizedBox(height: 20), + Obx(() => CustomButton( + onPressed: () { + loginController.verifyPhoneNumber(authService); + if (kDebugMode) { + print( + "The phone number is ${loginController.phoneController.text}"); + } + }, + text: 'Get OTP', + isDisabled: loginController.isDisabled.value)), + ], + ), + )), + ], + ), + ), + ); + } +} diff --git a/lib/app/modules/auth/views/otp_screen.dart b/lib/app/modules/auth/views/otp_screen.dart new file mode 100644 index 00000000..54a21753 --- /dev/null +++ b/lib/app/modules/auth/views/otp_screen.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_button.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:pin_code_fields/pin_code_fields.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/otp_controller.dart'; +import 'package:get_flutter_fire/theme/assets.dart'; + +class OtpScreen extends StatelessWidget { + final String phoneNumber; + OtpScreen({super.key, required this.phoneNumber}); + + final OtpController otpController = Get.put(OtpController()); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: Padding( + padding: AppTheme.paddingDefault, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Image.asset( + logo, + height: 100, + width: 100, + color: AppTheme.colorMain, + ), + ), + const Spacing(size: AppTheme.spacingMedium), + Center( + child: Text( + 'Enter OTP', + style: AppTheme.fontStyleLarge.copyWith( + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ), + const Spacing(size: AppTheme.spacingSmall), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + children: [ + const TextSpan( + text: + 'An OTP has been sent to your registered mobile number ', + style: AppTheme.fontStyleDefault, + ), + TextSpan( + text: phoneNumber, + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorMain, + decoration: TextDecoration.underline, + ), + ), + ], + ), + ), + const Spacing(size: AppTheme.spacingDefault), + PinCodeTextField( + length: 6, + appContext: context, + keyboardType: TextInputType.number, + textStyle: AppTheme.fontStyleDefault, + animationType: AnimationType.fade, + pinTheme: PinTheme( + shape: PinCodeFieldShape.underline, + fieldHeight: 50, + fieldWidth: 40, + inactiveColor: AppTheme.greyTextColor, + activeColor: AppTheme.greyTextColor, + selectedColor: AppTheme.colorMain, + ), + onChanged: (value) { + otpController.otp.value = value; + otpController.checkOtpFields(); + }, + ), + const Spacing(size: AppTheme.spacingTiny), + Obx(() => Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + otpController.isResendButtonEnabled.value + ? InkWell( + onTap: otpController.resendCode, + child: Text( + 'Resend OTP', + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorMain, + decoration: TextDecoration.underline, + decorationColor: AppTheme.colorMain), + ), + ) + : Row( + children: [ + Text( + 'Resend OTP ', + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.greyTextColor, + decoration: TextDecoration.underline, + ), + ), + Obx(() => Text( + 'in ${otpController.timerSeconds.value}s', + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + )), + ], + ), + ], + )), + const Spacing(size: AppTheme.spacingLarge), + Obx(() => CustomButton( + isDisabled: otpController.isDisabled.value, + onPressed: otpController.submitOtp, + text: 'Submit', + )), + ], + ), + ), + ), + ); + } +} diff --git a/lib/app/modules/auth/views/register_screen.dart b/lib/app/modules/auth/views/register_screen.dart new file mode 100644 index 00000000..57a8b2a9 --- /dev/null +++ b/lib/app/modules/auth/views/register_screen.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/regster_controller.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_phone_textfield.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_textfield.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_button.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/theme/assets.dart'; + +class RegisterScreen extends StatelessWidget { + final String phoneNumber; + + RegisterScreen({super.key, required this.phoneNumber}); + + final RegisterController controller = Get.put(RegisterController()); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + padding: AppTheme.paddingDefault, + child: Center( + child: SingleChildScrollView( + child: Card( + shape: AppTheme.rrShape, + elevation: 10, + shadowColor: AppTheme.colorBlack.withOpacity(0.15), + child: Padding( + padding: const EdgeInsets.all(AppTheme.spacingLarge), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Image.asset( + logo, + height: 100, + width: 100, + color: AppTheme.colorMain, + ), + ), + const Spacing(size: AppTheme.spacingLarge), + Center( + child: Text( + 'Register', + style: AppTheme.fontStyleLarge.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + const Spacing(size: AppTheme.spacingMedium), + CustomTextField( + labelText: 'Full Name', + controller: controller.nameController, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Email (optional)', + keyboardType: TextInputType.emailAddress, + controller: controller.emailController, + ), + const Spacing(size: AppTheme.spacingSmall), + PhoneTextField( + hintText: 'Enter your 10-digit mobile number', + readOnly: true, + controller: TextEditingController(text: phoneNumber), + ), + const Spacing(size: AppTheme.spacingMedium), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Here for Selling?', + style: AppTheme.fontStyleMedium), + Obx(() => Switch( + activeColor: Colors.white, + inactiveTrackColor: Colors.white, + inactiveThumbColor: Colors.black, + activeTrackColor: AppTheme.colorMain, + trackOutlineColor: null, + value: controller.isBusiness.value, + onChanged: controller.toggleBusiness, + )), + ], + ), + Obx(() => Visibility( + visible: controller.isBusiness.value, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Business Details', + style: AppTheme.fontStyleDefaultBold.copyWith( + fontSize: 16, + color: AppTheme.colorBlack, + ), + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Business Name', + controller: controller.businessNameController, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Business Type', + controller: controller.businessTypeController, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'GST Number', + controller: controller.gstNumberController, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'PAN Number', + controller: controller.panNumberController, + ), + ], + ), + )), + const Spacing(size: AppTheme.spacingLarge), + CustomButton( + onPressed: () { + controller.registerUser(phoneNumber); + }, + text: 'Register', + isDisabled: false, + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/app/modules/auth/views/welcome_screen.dart b/lib/app/modules/auth/views/welcome_screen.dart new file mode 100644 index 00000000..a84c7eb2 --- /dev/null +++ b/lib/app/modules/auth/views/welcome_screen.dart @@ -0,0 +1,114 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_button.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/services/auth_service.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/theme/assets.dart'; + +class WelcomeScreen extends StatefulWidget { + const WelcomeScreen({super.key}); + + @override + State createState() => _WelcomeScreenState(); +} + +class _WelcomeScreenState extends State { + final isInitialPosition = true.obs; + + @override + void initState() { + super.initState(); + + Future.delayed(const Duration(milliseconds: 400), () { + isInitialPosition.value = false; + }); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Get.find(); + + return Scaffold( + body: Stack( + children: [ + Obx(() => AnimatedPositioned( + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + top: isInitialPosition.value + ? MediaQuery.of(context).size.height * 0.4 + : MediaQuery.of(context).size.height * 0.2, + left: 0, + right: 0, + child: Center( + child: Image.asset( + logo, + height: 94, + width: 94, + color: AppTheme.colorMain, + ), + ), + )), + Obx(() => AnimatedPositioned( + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOut, + top: isInitialPosition.value + ? MediaQuery.of(context).size.height + : MediaQuery.of(context).size.height * 0.74, + left: 0, + right: 0, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacing(size: AppTheme.spacingLarge), + const Spacing(size: AppTheme.spacingLarge), + CustomButton( + onPressed: () { + Get.toNamed(Routes.LOGIN); + }, + text: 'Get Started', + ), + const SizedBox(height: 20), + Center( + child: Text.rich( + TextSpan( + text: 'New to the app and in doubt? ', + style: const TextStyle( + fontSize: 14, + color: Colors.black54, + ), + children: [ + TextSpan( + text: 'Explore Now', + style: const TextStyle( + color: AppTheme.colorMain, + fontWeight: FontWeight.bold, + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () {}, + ), + ], + ), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 40), + ], + ), + ), + )), + ], + ), + ); + } +} diff --git a/lib/app/modules/cart/controllers/cart_controller.dart b/lib/app/modules/cart/controllers/cart_controller.dart index c938ec4c..ad2ab54c 100644 --- a/lib/app/modules/cart/controllers/cart_controller.dart +++ b/lib/app/modules/cart/controllers/cart_controller.dart @@ -1,23 +1,133 @@ import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/constants.dart'; +import 'package:get_flutter_fire/models/cart_model.dart'; +import 'package:get_flutter_fire/models/order_model.dart'; class CartController extends GetxController { - //TODO: Implement CartController + final Rx _cart = CartModel(items: [], id: '').obs; + CartModel get cart => _cart.value; - final count = 0.obs; - @override - void onInit() { - super.onInit(); + int get totalPrice => _cart.value.items + .fold(0, (total, item) => total + item.price * item.quantity); + + final Rx _pageIndex = 0.obs; + int get pageIndex => _pageIndex.value; + + final Rx _selectedAddress = ''.obs; + String get selectedAddress => _selectedAddress.value; + + final Rx _selectedPaymentMethod = 'cash'.obs; + String get selectedPaymentMethod => _selectedPaymentMethod.value; + + void handlePaymentMethodChange(String paymentMethod) { + _selectedPaymentMethod.value = paymentMethod; + } + + void selectAddress(String addressID) { + _selectedAddress.value = addressID; + } + + void changePageIndex(int newIndex) { + if (newIndex == 1 && _cart.value.itemCount == 0) { + Get.snackbar('Cart Empty', 'Please add some items to your cart'); + return; + } + if (newIndex != _pageIndex.value) { + _pageIndex.value = newIndex; + } + } + + Future fetchCartData(String userID) async { + try { + List cartItems = []; + final snapshot = await db.collection('carts').doc(userID).get(); + if (snapshot.exists) { + Map cartData = snapshot.data() as Map; + if (cartData.containsKey('cartItems')) { + List cartItemsData = cartData['cartItems']; + cartItems = cartItemsData + .map((itemData) => CartItem.fromMap(itemData)) + .toList(); + } + } + _cart.value = CartModel(items: cartItems, id: userID); + } catch (e) { + Get.snackbar('Error', 'An error occurred while fetching cart data'); + } + } + + bool isProductInCart(String productID) { + return _cart.value.items.any((item) => item.id == productID); + } + + void incrementQuantity(CartItem item) { + int index = _cart.value.items.indexWhere((i) => i.id == item.id); + if (index != -1) { + _cart.update((cart) { + cart!.items[index].quantity++; + }); + syncCartwithDB(); + } } - @override - void onReady() { - super.onReady(); + void decrementQuantity(CartItem item) { + int index = _cart.value.items.indexWhere((i) => i.id == item.id); + if (index != -1) { + _cart.update((cart) { + cart!.items[index].quantity--; + }); + syncCartwithDB(); + } } - @override - void onClose() { - super.onClose(); + void addItem(CartItem item) { + int index = _cart.value.items.indexWhere((i) => i.id == item.id); + if (index != -1) { + _cart.update((cart) { + cart!.items[index].quantity += item.quantity; + }); + } else { + _cart.update((cart) { + cart!.items.add(item); + }); + } + syncCartwithDB(); } - void increment() => count.value++; + void removeItem(CartItem item) { + int index = _cart.value.items.indexWhere((i) => i.id == item.id); + if (index != -1) { + _cart.update((cart) { + cart!.items.removeAt(index); + }); + } + syncCartwithDB(); + } + + Future syncCartwithDB() async { + List> cartItemsData = + _cart.value.items.map((item) => item.toMap()).toList(); + await db + .collection('carts') + .doc(_cart.value.id) + .set({'cartItems': cartItemsData, 'id': _cart.value.id}); + } + + void clearCart() { + _cart.value = CartModel(items: [], id: cart.id); + syncCartwithDB(); + } + + Future placeOrder(OrderModel order) async { + try { + await db.collection('orders').doc(order.id).set(order.toMap()); + clearCart(); + Get.snackbar('Success', 'Order placed successfully'); + + Get.offAllNamed(Routes.ORDER_CONFIRMED, arguments: order.id); + } catch (e) { + Get.snackbar('Error', 'Failed to place order: $e'); + } + } } diff --git a/lib/app/modules/cart/controllers/order_controller.dart b/lib/app/modules/cart/controllers/order_controller.dart new file mode 100644 index 00000000..b7bed958 --- /dev/null +++ b/lib/app/modules/cart/controllers/order_controller.dart @@ -0,0 +1,66 @@ +import 'package:get/get.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:get_flutter_fire/models/order_model.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; + +class OrderController extends GetxController { + var orders = [].obs; + var filteredOrders = [].obs; + var isLoading = false.obs; + var currentOrder = Rxn(); + + final AuthController authController = Get.find(); + + OrderModel getOrderByID(String id) { + return orders.firstWhere((order) => order.id == id); + } + + Future fetchOrders() async { + if (authController.user == null) { + return; + } + + try { + isLoading.value = true; + final snapshot = await FirebaseFirestore.instance + .collection('orders') + .where('userID', isEqualTo: authController.user!.id) + .get(); + final fetchedOrders = + snapshot.docs.map((doc) => OrderModel.fromMap(doc.data())).toList(); + orders.assignAll(fetchedOrders); + filteredOrders.assignAll(fetchedOrders); + } catch (e) { + Get.snackbar('Error', 'Failed to fetch orders: $e'); + } finally { + isLoading.value = false; + } + } + + Future loadOrder(String id) async { + try { + isLoading.value = true; + await fetchOrders(); + currentOrder.value = getOrderByID(id); + } catch (e) { + Get.snackbar('Error', 'Failed to fetch order: $e'); + } finally { + isLoading.value = false; + } + } + + void filterOrders(String searchQuery, List statusFilter) { + filteredOrders.assignAll(orders.where((order) { + final matchesSearch = order.id.contains(searchQuery.toUpperCase()); + final matchesStatus = statusFilter.isEmpty || + statusFilter.contains(order.currentStatus.name); + return matchesSearch && matchesStatus; + }).toList()); + } + + @override + void onInit() { + super.onInit(); + fetchOrders(); + } +} diff --git a/lib/app/modules/cart/controllers/product_controller.dart b/lib/app/modules/cart/controllers/product_controller.dart new file mode 100644 index 00000000..a7d5db41 --- /dev/null +++ b/lib/app/modules/cart/controllers/product_controller.dart @@ -0,0 +1,29 @@ +import 'package:get/get.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:get_flutter_fire/models/product_model.dart'; + +class ProductController extends GetxController { + var products = [].obs; + + ProductModel getProductByID(String id) { + return products.firstWhere((product) => product.id == id); + } + + Future fetchProducts() async { + try { + final snapshot = + await FirebaseFirestore.instance.collection('products').get(); + final fetchedProducts = + snapshot.docs.map((doc) => ProductModel.fromMap(doc.data())).toList(); + products.assignAll(fetchedProducts); + } catch (e) { + Get.snackbar('Error', 'Failed to fetch products: $e'); + } + } + + @override + void onInit() { + super.onInit(); + fetchProducts(); + } +} diff --git a/lib/app/modules/cart/views/cart_root_view.dart b/lib/app/modules/cart/views/cart_root_view.dart new file mode 100644 index 00000000..303e2aa4 --- /dev/null +++ b/lib/app/modules/cart/views/cart_root_view.dart @@ -0,0 +1,180 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/cart_controller.dart'; +import 'package:get_flutter_fire/app/modules/cart/views/checkout_view.dart'; +import 'package:get_flutter_fire/app/modules/cart/views/select_address_view.dart'; +import 'package:get_flutter_fire/app/modules/cart/views/select_payment_method_view.dart'; +import 'package:get_flutter_fire/app/modules/profile/controllers/address_controller.dart'; +import 'package:get_flutter_fire/app/widgets/cart/cart_bottom_button.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; +import 'package:get_flutter_fire/models/address_model.dart'; +import 'package:get_flutter_fire/models/order_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/utils/get_uuid.dart'; + +class CartRootView extends StatelessWidget { + const CartRootView({super.key}); + + @override + Widget build(BuildContext context) { + final List pages = [ + const CheckoutView(), + const SelectAddressView(), + const SelectPaymentMethodView(), + ]; + + final cartController = Get.find(); + String getButtonLabel() { + if (cartController.pageIndex == 0) return "Proceeed To Checkout"; + if (cartController.pageIndex == 1) return "Select Address"; + if (cartController.pageIndex == 2) return "Confirm Order"; + return ''; + } + + void onBottomButtonPressed() async { + switch (cartController.pageIndex) { + case 0: + if (cartController.cart.itemCount == 0) { + Get.snackbar( + 'Cart Empty', + 'Please add some items to your cart', + ); + return; + } + if (cartController.selectedAddress.isEmpty) { + cartController.selectAddress( + Get.find().user!.defaultAddressID); + } + cartController.changePageIndex(1); + case 1: + cartController.changePageIndex(2); + case 2: + final addressController = Get.find(); + final authController = Get.find(); + + String orderID = getUUID(); + AddressModel address = addressController.addresses.firstWhere( + (element) => element.id == cartController.selectedAddress); + List products = cartController.cart.items + .map((e) => ProductData( + id: e.id, + quantity: e.quantity, + price: e.price, + )) + .toList(); + OrderModel order = OrderModel( + id: orderID, + address: address, + totalPrice: cartController.totalPrice, + couponDiscount: 0, + couponID: '', + createdAt: DateTime.now(), + currentStatus: OrderStatus.placed, + paymentMethod: cartController.selectedPaymentMethod, + statusUpdates: [ + OrderStatusUpdate( + status: OrderStatus.placed, timestamp: DateTime.now()) + ], + totalWeight: 0, + userID: authController.user!.id, + products: products, + ); + cartController.placeOrder(order); + default: + break; + } + } + + return Obx(() => Scaffold( + body: SingleChildScrollView( + child: Padding( + padding: AppTheme.paddingSmall, + child: Column( + children: [ + const Spacing(size: AppTheme.spacingLarge), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: GestureDetector( + onTap: () => cartController.changePageIndex(0), + child: Column( + children: [ + Container( + height: 4, + decoration: BoxDecoration( + borderRadius: AppTheme.borderRadius, + color: cartController.pageIndex >= 0 + ? AppTheme.colorMain + : AppTheme.backgroundColor, + ), + ), + const Spacing(size: AppTheme.spacingTiny), + const Text("Checkout"), + ], + ), + ), + ), + const Spacing( + size: AppTheme.spacingTiny, isHorizontal: true), + Expanded( + child: GestureDetector( + onTap: () => cartController.changePageIndex(1), + child: Column( + children: [ + Container( + height: 4, + decoration: BoxDecoration( + borderRadius: AppTheme.borderRadius, + color: cartController.pageIndex >= 1 + ? AppTheme.colorMain + : AppTheme.backgroundColor, + ), + ), + const Spacing(size: AppTheme.spacingTiny), + const Text("Address"), + ], + ), + ), + ), + const Spacing( + size: AppTheme.spacingTiny, isHorizontal: true), + Expanded( + child: GestureDetector( + onTap: () => cartController.changePageIndex(2), + child: Column( + children: [ + Container( + height: 4, + decoration: BoxDecoration( + borderRadius: AppTheme.borderRadius, + color: cartController.pageIndex == 2 + ? AppTheme.colorMain + : AppTheme.backgroundColor, + ), + ), + const Spacing(size: AppTheme.spacingTiny), + const Text("Payment"), + ], + ), + ), + ), + ], + ), + const Spacing(size: AppTheme.spacingDefault), + if (cartController.pageIndex < 3) + pages[cartController.pageIndex], + ], + ), + ), + ), + bottomNavigationBar: CartBottomButton( + input: 'Rs. ${cartController.totalPrice}', + label: getButtonLabel(), + onPressed: onBottomButtonPressed, + ), + )); + } +} diff --git a/lib/app/modules/cart/views/checkout_view.dart b/lib/app/modules/cart/views/checkout_view.dart new file mode 100644 index 00000000..ccdd01cc --- /dev/null +++ b/lib/app/modules/cart/views/checkout_view.dart @@ -0,0 +1,158 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/cart_controller.dart'; +import 'package:get_flutter_fire/app/modules/home/controllers/home_controller.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class CheckoutView extends StatelessWidget { + const CheckoutView({super.key}); + + @override + Widget build(BuildContext context) { + final cartController = Get.find(); + final homeController = Get.find(); + return Obx(() => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Text("Total Items", style: AppTheme.fontStyleDefault), + Text(" (${cartController.cart.itemCount})", + style: AppTheme.fontStyleDefaultBold), + const Spacer(), + IconButton( + onPressed: () { + cartController.clearCart(); + }, + icon: const Icon( + Icons.delete_outline, + color: AppTheme.colorMain, + )), + ], + ), + const SizedBox(height: AppTheme.spacingTiny), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: cartController.cart.items.length, + itemBuilder: (context, index) { + final item = cartController.cart.items[index]; + final product = homeController.products.firstWhere( + (element) => element.id == item.id, + ); + return Padding( + padding: const EdgeInsets.only(bottom: AppTheme.spacingSmall), + child: Container( + decoration: AppTheme.cardDecoration, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + onTap: () {}, + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: AppTheme.borderRadius.topLeft, + bottomLeft: AppTheme.borderRadius.bottomLeft, + ), + child: Image.network( + product.images.first, + height: 152, + width: 146, + fit: BoxFit.cover, + ), + ), + ), + Flexible( + child: Padding( + padding: const EdgeInsets.all( + AppTheme.spacingExtraSmall), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + style: AppTheme.fontStyleDefault, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + "Rs.${item.price}", + style: AppTheme.fontStyleDefaultBold, + ), + const Spacing(size: AppTheme.spacingTiny), + Container( + width: double.infinity, + decoration: AppTheme.cardDecoration.copyWith( + border: Border.all( + color: AppTheme.colorBlue, + ), + ), + child: Row( + children: [ + InkWell( + onTap: () => cartController + .decrementQuantity(item), + child: Container( + padding: AppTheme.paddingTiny, + child: const Center( + child: Icon(Icons.remove), + ), + ), + ), + Expanded( + child: Container( + padding: AppTheme.paddingTiny, + decoration: const BoxDecoration( + border: Border( + left: BorderSide( + color: AppTheme.colorBlue, + ), + right: BorderSide( + color: AppTheme.colorBlue, + ), + ), + ), + child: Center( + child: Text( + item.quantity.toString(), + style: AppTheme.fontStyleDefault, + ), + ), + ), + ), + InkWell( + onTap: () { + cartController + .incrementQuantity(item); + }, + child: Container( + padding: AppTheme.paddingTiny, + child: const Center( + child: Icon(Icons.add), + ), + ), + ), + ], + ), + ), + const Spacing(size: AppTheme.spacingTiny), + // SecondaryButton( + // label: context.loc.removeItem, + // onPressed: () => _removeItem(item), + // ), + ], + ), + ), + ), + ], + ), + ), + ); + }, + ), + ], + )); + } +} diff --git a/lib/app/modules/cart/views/order_confirmed.dart b/lib/app/modules/cart/views/order_confirmed.dart new file mode 100644 index 00000000..66737d5c --- /dev/null +++ b/lib/app/modules/cart/views/order_confirmed.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/order_controller.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/product_controller.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/app/widgets/cart/order_detail_card.dart'; +import 'package:get_flutter_fire/app/widgets/cart/order_summary_widget.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_bottom_button.dart'; +import 'package:get_flutter_fire/app/widgets/common/overlay_loader.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class OrderConfirmedScreen extends StatelessWidget { + const OrderConfirmedScreen({super.key}); + + @override + Widget build(BuildContext context) { + final String id = Get.arguments ?? ''; + final OrderController orderController = Get.find(); + final ProductController productController = Get.find(); + + return FutureBuilder( + future: orderController.loadOrder(id), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const LoadingWidget(); + } + + if (snapshot.hasError) { + return Scaffold( + body: Center( + child: Text('Error loading order: ${snapshot.error}'), + ), + ); + } + + final order = orderController.currentOrder.value; + + if (order == null) { + return const Scaffold( + body: Center( + child: Text('Order not found', style: AppTheme.fontStyleLarge), + ), + ); + } + + return SafeArea( + child: Scaffold( + body: SingleChildScrollView( + child: Padding( + padding: AppTheme.paddingSmall, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Order Confirmed", + style: AppTheme.fontStyleLarge), + const Spacing(size: AppTheme.spacingDefault), + Text("${order.createdAt}: #${order.id}", + style: AppTheme.fontStyleMedium), + const Spacing(size: AppTheme.spacingSmall), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: order.products.length, + itemBuilder: (context, index) { + final item = order.products[index]; + final product = + productController.getProductByID(item.id); + + return OrderDetailProductCard( + product: product, + productData: item, + ); + }, + ), + OrderSummaryWidget( + couponDiscount: order.couponDiscount, + couponCode: "None", + priceDiscount: 0, + subTotalPrice: 0, + totalPrice: order.totalPrice, + ), + ], + ), + ), + ), + bottomNavigationBar: Padding( + padding: AppTheme.paddingDefault, + child: CustomBottomButton( + label: "Continue Shopping", + onPressed: () { + Get.offAllNamed(Routes.ROOT); + }, + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/app/modules/cart/views/select_address_view.dart b/lib/app/modules/cart/views/select_address_view.dart new file mode 100644 index 00000000..eaac0d0b --- /dev/null +++ b/lib/app/modules/cart/views/select_address_view.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/cart_controller.dart'; +import 'package:get_flutter_fire/app/modules/profile/controllers/address_controller.dart'; +import 'package:get_flutter_fire/app/widgets/cart/select_address_card.dart'; +import 'package:get_flutter_fire/models/address_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class SelectAddressView extends StatelessWidget { + const SelectAddressView({super.key}); + + @override + Widget build(BuildContext context) { + final addressController = Get.find(); + final cartController = Get.find(); + final user = Get.find().user; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Select Address", + style: AppTheme.fontStyleDefaultBold, + ), + const SizedBox(height: AppTheme.spacingSmall), + ListView.builder( + itemCount: addressController.addresses.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + AddressModel address = addressController.addresses[index]; + return Padding( + padding: const EdgeInsets.only(bottom: AppTheme.spacingSmall), + child: Obx(() => SelectAddressCard( + isDefault: user!.defaultAddressID == address.id, + selectedAddressID: cartController.selectedAddress, + address: address, + onSelect: (address) { + cartController.selectAddress(address.id); + }, + )), + ); + }, + ), + // Center( + // child: SecondaryButton( + // label: context.loc.addAddress, + // onPressed: () => context.go(Routes.addAddress), + // ), + // ), + ], + ); + } +} diff --git a/lib/app/modules/cart/views/select_payment_method_view.dart b/lib/app/modules/cart/views/select_payment_method_view.dart new file mode 100644 index 00000000..8e1daefe --- /dev/null +++ b/lib/app/modules/cart/views/select_payment_method_view.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/cart_controller.dart'; +import 'package:get_flutter_fire/app/widgets/cart/payment_selection_card.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class SelectPaymentMethodView extends StatelessWidget { + const SelectPaymentMethodView({super.key}); + + @override + Widget build(BuildContext context) { + final cartController = Get.find(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text("Select Payment", style: AppTheme.fontStyleDefaultBold), + const SizedBox(height: AppTheme.spacingSmall), + Obx( + () => PaymentMethodSelectionCard( + paymentMethod: "cash", + selectedPaymentMethod: cartController.selectedPaymentMethod, + displayText: "Cash On Delivery", + onChanged: cartController.handlePaymentMethodChange, + label: "(free)", + ), + ), + const Spacing(size: AppTheme.spacingExtraSmall), + Obx( + () => PaymentMethodSelectionCard( + paymentMethod: "online", + selectedPaymentMethod: cartController.selectedPaymentMethod, + displayText: "Pay Online", + onChanged: cartController.handlePaymentMethodChange, + ), + ), + ], + ); + } +} diff --git a/lib/app/modules/home/controllers/home_controller.dart b/lib/app/modules/home/controllers/home_controller.dart index f058de2a..95268aba 100644 --- a/lib/app/modules/home/controllers/home_controller.dart +++ b/lib/app/modules/home/controllers/home_controller.dart @@ -1,14 +1,182 @@ -import 'package:get/get.dart'; +import 'dart:convert'; -import '../../../../models/role.dart'; -import '../../../../services/auth_service.dart'; +import 'package:carousel_slider/carousel_options.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/cart_controller.dart'; +import 'package:get_flutter_fire/main.dart'; +import 'package:get_flutter_fire/models/banner_model.dart'; +import 'package:get_flutter_fire/models/category_model.dart'; +import 'package:get_flutter_fire/models/offer_model.dart'; +import 'package:get_flutter_fire/models/address_model.dart'; +import 'package:get_flutter_fire/models/product_model.dart'; class HomeController extends GetxController { - final Rx chosenRole = Rx(AuthService.to.maxRole); + RxList banners = [].obs; + RxList categories = [].obs; + RxList offers = [].obs; + RxList products = [].obs; + RxBool isLoading = true.obs; + RxInt currentCarouselIndex = 0.obs; + + @override + void onInit() { + super.onInit(); + _fetchAllData(); + FirebaseMessaging.instance.getInitialMessage().then( + (value) async {}, + ); + + FirebaseMessaging.onMessage.listen(showFlutterNotification); + + FirebaseMessaging.onMessageOpenedApp + .listen((RemoteMessage message) async {}); + + flutterLocalNotificationsPlugin.initialize( + const InitializationSettings( + android: AndroidInitializationSettings('launch_background'), + ), + onDidReceiveNotificationResponse: + (NotificationResponse notificationResponse) { + debugPrint('Notification Received : $notificationResponse'); + }, + ); + } + + void showFlutterNotification(RemoteMessage message) { + RemoteNotification? notification = message.notification; + AndroidNotification? android = message.notification?.android; + if (notification != null && android != null) { + flutterLocalNotificationsPlugin.show( + notification.hashCode, + notification.title, + notification.body, + NotificationDetails( + android: AndroidNotificationDetails( + channel.id, + channel.name, + channelDescription: channel.description, + icon: 'launch_background', + ), + ), + payload: jsonEncode(message.data), + ); + } + } + + Future _fetchAllData() async { + isLoading(true); + try { + final cartController = Get.put(CartController()); + final authController = Get.put(AuthController()); + await Future.wait([ + fetchBanners(), + fetchCategories(), + fetchOffersForUserLocation(), + fetchProducts(), + cartController.fetchCartData(authController.user!.id), + ]); + } catch (e) { + Get.snackbar('Error', 'An error occurred while fetching data'); + } finally { + isLoading(false); + } + } + + Future fetchBanners() async { + try { + QuerySnapshot snapshot = await FirebaseFirestore.instance + .collection('banners') + .where('isActive', isEqualTo: true) + .get(); + + List fetchedBanners = snapshot.docs.map((doc) { + return BannerModel.fromMap(doc.data() as Map); + }).toList(); + + banners.value = fetchedBanners; + } catch (e) { + Get.snackbar('Error', 'An error occurred while fetching banners'); + rethrow; + } + } + + Future fetchCategories() async { + try { + QuerySnapshot snapshot = + await FirebaseFirestore.instance.collection('categories').get(); + + List fetchedCategories = snapshot.docs.map((doc) { + final data = doc.data() as Map; + return CategoryModel.fromMap(data); + }).toList(); + + categories.value = fetchedCategories; + } catch (e) { + Get.snackbar('Error', 'An error occurred while fetching categories'); + rethrow; + } + } + + Future fetchOffersForUserLocation() async { + try { + final authController = Get.find(); + final addressSnapshot = await FirebaseFirestore.instance + .collection('addresses') + .doc(authController.user!.defaultAddressID) + .get(); + + if (addressSnapshot.exists) { + final address = AddressModel.fromMap(addressSnapshot.data()!); + QuerySnapshot offerSnapshot = await FirebaseFirestore.instance + .collection('offers') + .where('city', isEqualTo: address.city) + .get(); + + List fetchedOffers = offerSnapshot.docs.map((doc) { + return OfferModel.fromMap(doc.data() as Map); + }).toList(); + + offers.value = fetchedOffers; + } else { + Get.snackbar('Error', 'No address found for this user'); + throw 'No address found'; + } + } catch (e) { + Get.snackbar('Error', 'An error occurred while fetching offers'); + rethrow; + } + } + + Future fetchProducts() async { + try { + QuerySnapshot snapshot = + await FirebaseFirestore.instance.collection('products').get(); - // Role get role => AuthService.to.maxRole; + List fetchedProducts = snapshot.docs.map((doc) { + final data = doc.data() as Map; + return ProductModel.fromMap(data); + }).toList(); - get isBuyer => chosenRole.value == Role.buyer; + products.value = fetchedProducts + + fetchedProducts + + fetchedProducts + + fetchedProducts + + fetchedProducts + + fetchedProducts + + fetchedProducts + + fetchedProducts; + } catch (e) { + Get.snackbar('Error', 'An error occurred while fetching products'); + rethrow; + } + } - get isAdmin => chosenRole.value == Role.admin; + void onPageChanged(int index, CarouselPageChangedReason reason) { + currentCarouselIndex.value = index; + } } diff --git a/lib/app/modules/home/view/home.dart b/lib/app/modules/home/view/home.dart new file mode 100644 index 00000000..0b2bc2fa --- /dev/null +++ b/lib/app/modules/home/view/home.dart @@ -0,0 +1,519 @@ +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/home/controllers/home_controller.dart'; +import 'package:get_flutter_fire/app/widgets/common/overlay_loader.dart'; +import 'package:get_flutter_fire/app/widgets/common/secondary_button.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/app/widgets/product/add_to_cart_button.dart'; +import 'package:get_flutter_fire/models/banner_model.dart'; +import 'package:get_flutter_fire/models/category_model.dart'; +import 'package:get_flutter_fire/models/product_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + final HomeController homeController = Get.put(HomeController()); + + return Scaffold( + body: Obx(() { + if (homeController.isLoading.value) { + return const LoadingWidget(); + } else { + return SingleChildScrollView( + child: Column( + children: [ + const Spacing(size: AppTheme.spacingMedium), + _buildCarousel(homeController.banners), + _buildSheruProducts( + homeController.products + .where((product) => product.isSheruSpecial) + .toList(), + MediaQuery.of(context).size, + 'Sheru Special'), + _buildCategories(homeController.categories), + _buildProducts( + homeController.products + .where((product) => product.isApproved) + .toList(), + MediaQuery.of(context).size, + 'All Products'), + ], + ), + ); + } + }), + ); + } + + Widget _buildCarousel(List banners) { + return CarouselSlider( + options: CarouselOptions( + height: 200.0, + autoPlay: true, + enlargeCenterPage: true, + aspectRatio: 16 / 9, + viewportFraction: 0.8, + ), + items: banners.map((banner) { + return Builder( + builder: (BuildContext context) { + return Container( + margin: const EdgeInsets.all(5.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + image: DecorationImage( + image: NetworkImage(banner.imageUrl), + fit: BoxFit.cover, + ), + ), + ); + }, + ); + }).toList(), + ); + } + + Widget _buildCategories(List categories) { + return Padding( + padding: AppTheme.paddingDefault, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Categories", + style: AppTheme.fontStyleLarge.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const Spacing(size: AppTheme.fontSizeDefault), + SizedBox( + height: 100, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: categories.length, + itemBuilder: (context, index) { + final category = categories[index]; + return Column( + children: [ + Container( + width: 70.0, + height: 70.0, + margin: const EdgeInsets.only(right: 10.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + image: DecorationImage( + image: NetworkImage(category.imageUrl), + fit: BoxFit.cover, + ), + ), + ), + const SizedBox(height: 5), + Text( + category.name, + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ], + ); + }, + ), + ), + ], + ), + ); + } + + Widget _buildSheruProducts( + List products, Size size, String title) { + return Padding( + padding: AppTheme.paddingDefault, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: AppTheme.fontStyleLarge.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + ), + const Spacing(size: AppTheme.fontSizeDefault), + SizedBox( + height: 322, + child: ListView.builder( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + itemCount: products.length, + itemBuilder: (context, index) { + final product = products[index]; + + return Padding( + padding: const EdgeInsets.only(right: AppTheme.spacingSmall), + child: SizedBox( + width: 150, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 150.0, + height: 150.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + image: DecorationImage( + image: NetworkImage(product.images.first), + fit: BoxFit.contain, + ), + ), + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + product.name, + style: AppTheme.fontStyleHeadingDefault.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + product.description, + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + "Rs. ${product.unitPrice}", + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const Spacing(size: AppTheme.spacingTiny), + AddToCartButton(product: product), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ); + } + + Widget _buildProducts(List products, Size size, String title) { + return Padding( + padding: AppTheme.paddingDefault, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: AppTheme.fontStyleLarge.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + ), + // SecondaryButton( + // label: "See All", + // onPressed: () {}, + // ), + ], + ), + const Spacing(size: AppTheme.fontSizeDefault), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: products.length ~/ 2, + itemBuilder: (context, index) { + final productLeft = products[index * 2]; + final productRight = products[index * 2 + 1]; + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox( + width: 150, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 150.0, + height: 150.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + image: DecorationImage( + image: NetworkImage(productLeft.images.first), + fit: BoxFit.contain, + ), + ), + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + productLeft.name, + style: AppTheme.fontStyleHeadingDefault.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + productLeft.description, + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + "Rs. ${productLeft.unitPrice}", + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + ), + const Spacing(size: AppTheme.spacingTiny), + AddToCartButton(product: productLeft), + const Spacing(size: AppTheme.spacingMedium), + ], + ), + ), + SizedBox( + width: 150, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 150.0, + height: 150.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + image: DecorationImage( + image: NetworkImage(productRight.images.first), + fit: BoxFit.cover, + ), + ), + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + productRight.name, + style: AppTheme.fontStyleHeadingDefault.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + productRight.description, + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + "Rs. ${productRight.unitPrice}", + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + ), + const Spacing(size: AppTheme.spacingTiny), + AddToCartButton(product: productRight), + const Spacing(size: AppTheme.spacingMedium), + ], + ), + ), + ], + ); + }, + ), + if (products.length % 2 != 0) + SizedBox( + width: 150.0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 150.0, + height: 150.0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10.0), + image: DecorationImage( + image: NetworkImage(products.last.images.first), + fit: BoxFit.contain, + ), + ), + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + products.last.name, + style: AppTheme.fontStyleHeadingDefault.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + products.last.description, + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + "Rs. ${products.last.unitPrice}", + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + ), + const Spacing(size: AppTheme.spacingTiny), + AddToCartButton(product: products.last), + const Spacing(size: AppTheme.spacingMedium), + ], + ), + ), + ], + ), + ); + } + + Widget _buildOffers(HomeController homeController) { + return Padding( + padding: AppTheme.paddingDefault, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Offers For You", + style: AppTheme.fontStyleLarge.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + ), + SecondaryButton( + label: "See All", + onPressed: () {}, + ), + ], + ), + const Spacing(size: AppTheme.fontSizeDefault), + Obx(() { + // Check if offers list is empty + if (homeController.offers.isEmpty) { + return Text( + "No offers available at the moment.", + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.colorBlack, + ), + ); + } + + return CarouselSlider.builder( + itemCount: homeController.offers.length, + options: CarouselOptions( + height: 180.0, + autoPlay: true, + enlargeCenterPage: true, + viewportFraction: 1.0, + aspectRatio: 16 / 9, + onPageChanged: (index, reason) { + homeController.onPageChanged(index, reason); + }, + ), + itemBuilder: (BuildContext context, int index, int realIndex) { + final offer = homeController.offers[index]; + return Column( + children: [ + Container( + width: double.infinity, + height: 150.0, + margin: const EdgeInsets.symmetric(horizontal: 10.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15.0), + image: DecorationImage( + image: NetworkImage(offer.imageUrl), + fit: BoxFit.fill, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + spreadRadius: 2, + blurRadius: 7, + offset: const Offset(0, 3), + ), + ], + ), + ), + const SizedBox(height: 5), + Flexible( + child: Text( + offer.title, + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ); + }, + ); + }), + const SizedBox(height: 10), + Obx(() { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: homeController.offers.map((offer) { + int index = homeController.offers.indexOf(offer); + return Container( + width: 8.0, + height: 8.0, + margin: const EdgeInsets.symmetric( + vertical: 10.0, horizontal: 2.0), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: homeController.currentCarouselIndex.value == index + ? AppTheme.colorMain + : Colors.grey, + ), + ); + }).toList(), + ); + }), + ], + ), + ); + } +} diff --git a/lib/app/modules/orders/views/order_detail_screen.dart b/lib/app/modules/orders/views/order_detail_screen.dart new file mode 100644 index 00000000..4ee3e759 --- /dev/null +++ b/lib/app/modules/orders/views/order_detail_screen.dart @@ -0,0 +1,207 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/order_controller.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/product_controller.dart'; +import 'package:get_flutter_fire/app/widgets/cart/order_detail_card.dart'; +import 'package:get_flutter_fire/app/widgets/cart/order_summary_widget.dart'; +import 'package:get_flutter_fire/app/widgets/common/overlay_loader.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/app/widgets/orders/order_status_indicator.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class OrderDetailScreen extends StatelessWidget { + const OrderDetailScreen({ + super.key, + }); + + @override + Widget build(BuildContext context) { + final String orderID = Get.arguments['id']; + final OrderController orderController = Get.find(); + final ProductController productController = Get.find(); + + return Scaffold( + body: Padding( + padding: const EdgeInsets.all(AppTheme.spacingDefault), + child: SingleChildScrollView( + child: Obx(() { + if (orderController.isLoading.value) { + return const LoadingWidget(); + } + + final order = orderController.getOrderByID(orderID); + final coupon = orderController.orders + .firstWhere((o) => o.id == orderID) + .couponID; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacing(size: AppTheme.spacingLarge), + OrderStatusIndicator(currentStatus: order.currentStatus), + const Spacing(size: AppTheme.spacingSmall), + Center( + child: Text( + "Order ${order.currentStatus.name.capitalizeFirst}", + style: AppTheme.fontStyleLarge.copyWith( + color: AppTheme.colorMain, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: AppTheme.spacingDefault), + const Text( + 'Item Information', + style: TextStyle( + fontSize: 18, + color: AppTheme.colorMain, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: AppTheme.spacingSmall), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: order.products.length, + itemBuilder: (context, index) { + final item = order.products[index]; + final product = productController.getProductByID(item.id); + + return OrderDetailProductCard( + product: product, + productData: order.products[index], + ); + }, + ), + const SizedBox(height: AppTheme.spacingDefault), + const Text( + 'Payment Information', + style: TextStyle( + fontSize: 18, + color: AppTheme.colorMain, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: AppTheme.spacingSmall), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.3), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + border: Border.all( + color: AppTheme.colorMain, + ), + ), + padding: AppTheme.paddingSmall, + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + (order.paymentMethod == "cash") ? 'Cash' : 'Paid', + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorBlue, + fontWeight: FontWeight.bold, + ), + ), + Text( + (order.paymentMethod == "cash") + ? 'Cash on Delivery' + : 'Online Payment', + style: AppTheme.fontStyleDefault, + ), + ], + ), + ], + ), + ), + const SizedBox(height: AppTheme.spacingDefault), + const Text( + 'Delivery Address', + style: TextStyle( + fontSize: 18, + color: AppTheme.colorMain, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: AppTheme.spacingSmall), + Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.3), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + border: Border.all( + color: AppTheme.colorMain, + ), + ), + padding: AppTheme.paddingSmall, + child: SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${order.address.name} (${order.address.phoneNumber})", + style: AppTheme.fontStyleDefaultBold, + ), + const Spacing(size: AppTheme.spacingSmall), + Text( + "${order.address.line1}\n${order.address.line2}", + style: AppTheme.fontStyleDefault, + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + "${order.address.district}, ${order.address.city}", + style: AppTheme.fontStyleDefault, + ), + const Spacing(size: AppTheme.spacingSmall), + ], + ), + ), + ), + const SizedBox(height: AppTheme.spacingDefault), + const Text( + 'Order Summary', + style: TextStyle( + fontSize: 18, + color: AppTheme.colorMain, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: AppTheme.spacingSmall), + Container( + decoration: BoxDecoration( + border: Border.all( + color: AppTheme.colorMain, + ), + borderRadius: AppTheme.borderRadius), + child: OrderSummaryWidget( + couponDiscount: order.couponDiscount, + couponCode: coupon, + priceDiscount: 0, + subTotalPrice: order.totalPrice, + totalPrice: order.totalPrice, + ), + ), + const SizedBox(height: AppTheme.spacingLarge), + ], + ); + }), + ), + ), + ); + } +} diff --git a/lib/app/modules/orders/views/orders.dart b/lib/app/modules/orders/views/orders.dart new file mode 100644 index 00000000..8dfdcaff --- /dev/null +++ b/lib/app/modules/orders/views/orders.dart @@ -0,0 +1,258 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/order_controller.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/product_controller.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/app/widgets/common/overlay_loader.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/app/widgets/orders/primary_button.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class OrdersScreen extends StatefulWidget { + const OrdersScreen({super.key}); + + @override + OrdersScreenState createState() => OrdersScreenState(); +} + +class OrdersScreenState extends State { + final TextEditingController _searchController = TextEditingController(); + final List _orderStatusFilter = []; + + final OrderController orderController = Get.find(); + final ProductController productController = Get.find(); + + @override + void initState() { + super.initState(); + _searchController.addListener(_onSearchChanged); + } + + void _onSearchChanged() { + orderController.filterOrders(_searchController.text, _orderStatusFilter); + } + + void _toggleFilter(String status) { + setState(() { + if (_orderStatusFilter.contains(status)) { + _orderStatusFilter.remove(status); + } else { + _orderStatusFilter.add(status); + } + orderController.filterOrders(_searchController.text, _orderStatusFilter); + }); + } + + @override + void dispose() { + _searchController.removeListener(_onSearchChanged); + _searchController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Obx(() { + if (orderController.isLoading.value) { + return const LoadingWidget(); + } + + final filteredOrders = orderController.filteredOrders; + + return Scaffold( + body: SingleChildScrollView( + child: Padding( + padding: AppTheme.paddingSmall, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Container( + decoration: AppTheme.cardDecoration, + child: TextField( + controller: _searchController, + decoration: InputDecoration( + prefixIcon: const Padding( + padding: EdgeInsets.symmetric(vertical: 12), + child: Icon(Icons.search, + color: AppTheme.greyTextColor), + ), + hintText: 'Search for Orders', + hintStyle: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + vertical: AppTheme.spacingTiny), + ), + textAlignVertical: TextAlignVertical.center, + ), + ), + ), + const Spacing( + isHorizontal: true, size: AppTheme.spacingTiny), + _filterByOrderWidget(), + ], + ), + filteredOrders.isEmpty + ? const Column( + children: [ + Spacing(size: AppTheme.spacingSmall), + Text( + 'No Orders Found', + style: AppTheme.fontStyleDefault, + ), + ], + ) + : const SizedBox.shrink(), + const Spacing(size: AppTheme.spacingSmall), + ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + padding: EdgeInsets.zero, + itemCount: filteredOrders.length, + itemBuilder: (context, index) { + final order = filteredOrders[index]; + final products = order.products + .map((product) => + productController.getProductByID(product.id)) + .toList(); + + return Padding( + padding: + const EdgeInsets.only(bottom: AppTheme.spacingSmall), + child: Container( + decoration: AppTheme.cardDecoration, + child: Padding( + padding: AppTheme.paddingSmall, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.network( + products + .where((p) => p.id == order.products[0].id) + .first + .images[0], + ), + RichText( + text: TextSpan( + children: [ + const TextSpan( + text: "Order ID: ", + style: AppTheme.fontStyleDefault, + ), + TextSpan( + text: "#${order.id}", + style: AppTheme.fontStyleDefaultBold + .copyWith( + color: AppTheme.colorDarkBlue, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + const Spacing(size: AppTheme.spacingTiny), + RichText( + text: TextSpan( + children: [ + const TextSpan( + text: "Status: ", + style: AppTheme.fontStyleDefault, + ), + TextSpan( + text: order.currentStatus.name, + style: AppTheme.fontStyleDefaultBold + .copyWith( + color: AppTheme.colorDarkBlue, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + 'Total Products: ${order.products.length}', + style: AppTheme.fontStyleDefault, + ), + const Spacing(size: AppTheme.spacingSmall), + PrimaryButton( + onPressed: () => Get.toNamed( + Routes.ORDER_DETAILS, + arguments: {'id': order.id}), + label: 'View Details', + ), + ], + ), + ), + ), + ); + }, + ), + ], + ), + ), + ), + ); + }); + } + + Widget _filterByOrderWidget() { + return InkWell( + onTap: () { + Get.dialog( + Dialog( + child: Container( + padding: AppTheme.paddingSmall, + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.8, + maxHeight: MediaQuery.of(context).size.height * 0.4, + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: Colors.white, + ), + child: _buildFilterDialog(), + ), + ), + ); + }, + child: Container( + height: 50, + padding: AppTheme.paddingTiny, + decoration: AppTheme.cardDecoration, + child: const Row( + children: [ + Text("Filter By", style: AppTheme.fontStyleDefault), + Spacing(size: AppTheme.spacingTiny, isHorizontal: true), + Icon(Icons.chevron_right), + ], + ), + ), + ); + } + + Widget _buildFilterDialog() { + final statusItems = ['Pending', 'Delivered', 'Cancelled', 'placed']; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: statusItems.map((status) { + return CheckboxListTile( + title: Text(status.capitalizeFirst!), + value: _orderStatusFilter.contains(status), + onChanged: (bool? value) { + _toggleFilter(status); + Get.back(); + }, + controlAffinity: ListTileControlAffinity.leading, + ); + }).toList(), + ); + } +} diff --git a/lib/app/modules/profile/controllers/address_controller.dart b/lib/app/modules/profile/controllers/address_controller.dart new file mode 100644 index 00000000..df2d8d7c --- /dev/null +++ b/lib/app/modules/profile/controllers/address_controller.dart @@ -0,0 +1,120 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/modules/home/controllers/home_controller.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/app/widgets/common/show_loader.dart'; +import 'package:get_flutter_fire/models/address_model.dart'; +import 'package:get_flutter_fire/models/user_model.dart'; +import 'package:uuid/uuid.dart'; + +class AddressController extends GetxController { + final AuthController authController = Get.find(); + + final line1Controller = TextEditingController(); + final line2Controller = TextEditingController(); + final cityController = TextEditingController(); + final districtController = TextEditingController(); + + final latitudeController = TextEditingController(text: ''); + final longitudeController = TextEditingController(text: ''); + + late UserModel user; + final CollectionReference addressesRef = + FirebaseFirestore.instance.collection('addresses'); + + var addresses = [].obs; + + @override + void onInit() { + super.onInit(); + fetchAddresses(); + } + + Future fetchAddresses() async { + try { + if (Get.arguments != null && Get.arguments.containsKey('user')) { + user = Get.arguments['user'] as UserModel; + } else { + if (authController.currentUser.value != null) { + user = authController.currentUser.value!; + } + } + final querySnapshot = + await addressesRef.where('userID', isEqualTo: user.id).get(); + + addresses.assignAll(querySnapshot.docs + .map( + (doc) => AddressModel.fromMap(doc.data() as Map)) + .toList()); + } catch (e) { + Get.snackbar('Error', 'Failed to load addresses: $e'); + } + } + + void saveAddress() async { + const uuid = Uuid(); + String addressID = uuid.v4(); + + AddressModel address = AddressModel( + name: user.name, + phoneNumber: user.phoneNumber, + line1: line1Controller.text, + line2: line2Controller.text, + city: cityController.text, + district: districtController.text, + latitude: latitudeController.text.isEmpty + ? 0.0 + : double.parse(latitudeController.text), + longitude: longitudeController.text.isEmpty + ? 0.0 + : double.parse(longitudeController.text), + id: addressID, + userID: user.id, + ); + + try { + await addressesRef.doc(addressID).set(address.toMap()); + + if (user.defaultAddressID.isEmpty) { + await authController.updateDefaultAddressID(addressID); + + user = user.copyWith(defaultAddressID: addressID); + } + + Get.offAllNamed(Routes.ROOT); + } catch (e) { + Get.snackbar('Error', 'Failed to save address: $e'); + } + } + + Future setDefaultAddress(AddressModel address) async { + showLoader(); + await authController.updateDefaultAddressID(address.id); + await fetchAddresses(); + final homeController = Get.find(); + await homeController.fetchOffersForUserLocation(); + dismissLoader(); + } + + Future deleteAddress(String id) async { + try { + await addressesRef.doc(id).delete(); + addresses.removeWhere((address) => address.id == id); + } catch (e) { + Get.snackbar('Error', 'Failed to delete address: $e'); + } + } + + @override + void onClose() { + line1Controller.dispose(); + line2Controller.dispose(); + cityController.dispose(); + districtController.dispose(); + latitudeController.dispose(); + longitudeController.dispose(); + super.onClose(); + } +} diff --git a/lib/app/modules/profile/controllers/contact_controller.dart b/lib/app/modules/profile/controllers/contact_controller.dart new file mode 100644 index 00000000..88d36c18 --- /dev/null +++ b/lib/app/modules/profile/controllers/contact_controller.dart @@ -0,0 +1,77 @@ +import 'package:get/get.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; +import 'package:get_flutter_fire/models/contact_enquiry_model.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; + +class ContactController extends GetxController { + final AuthController authController = Get.find(); + final CollectionReference supportRef = + FirebaseFirestore.instance.collection('support'); + + var enquiries = [].obs; + var filteredEnquiries = [].obs; + var isLoading = false.obs; + var selectedTab = 'Completed'.obs; + + @override + void onInit() { + super.onInit(); + getEnquiries(); + } + + Future getEnquiries() async { + if (authController.user == null) return; + + isLoading(true); + try { + final snapshot = await supportRef + .where('userID', isEqualTo: authController.user!.id) + .orderBy('timestamp', descending: true) + .get(); + + enquiries.assignAll(snapshot.docs + .map((doc) => + ContactEnquiryModel.fromMap(doc.data() as Map)) + .toList()); + + filterEnquiries(); + } catch (e) { + Get.snackbar('Error', 'Failed to fetch enquiries'); + } finally { + isLoading(false); + } + } + + void filterEnquiries() { + EnquiryStatus status; + switch (selectedTab.value) { + case 'Pending': + status = EnquiryStatus.pending; + break; + case 'In-Progress': + status = EnquiryStatus.inProgress; + break; + default: + status = EnquiryStatus.completed; + } + + filteredEnquiries.value = + enquiries.where((enquiry) => enquiry.status == status).toList(); + } + + void changeTab(String tab) { + selectedTab.value = tab; + filterEnquiries(); + } + + Future addEnquiry(ContactEnquiryModel enquiry) async { + try { + await supportRef.doc(enquiry.id).set(enquiry.toMap()); + enquiries.insert(0, enquiry); + filterEnquiries(); + } catch (e) { + Get.snackbar('Error', 'Failed to submit enquiry'); + } + } +} diff --git a/lib/app/modules/profile/views/account_detail.dart b/lib/app/modules/profile/views/account_detail.dart new file mode 100644 index 00000000..609f2e84 --- /dev/null +++ b/lib/app/modules/profile/views/account_detail.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_bottom_button.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_phone_textfield.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_textfield.dart'; +import 'package:get_flutter_fire/app/widgets/common/show_loader.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; + +class AccountDetailsScreen extends StatefulWidget { + const AccountDetailsScreen({super.key}); + + @override + AccountDetailsScreenState createState() => AccountDetailsScreenState(); +} + +class AccountDetailsScreenState extends State { + final _fullNameController = TextEditingController(); + final _emailController = TextEditingController(); + final _phoneNumberController = TextEditingController(); + final _businessNameController = TextEditingController(); + final _businessTINController = TextEditingController(); + final _businessVATController = TextEditingController(); + String? selectedBusinessType; + + final List businessTypes = ['Fashion', 'Electronics', 'Groceries']; + + @override + void initState() { + super.initState(); + init(); + } + + void init() { + final authController = Get.find(); + final user = authController.user; + if (user == null) return; + _fullNameController.text = user.name; + _emailController.text = user.email ?? ''; + _phoneNumberController.text = user.phoneNumber; + _businessNameController.text = user.businessName ?? ''; + _businessTINController.text = user.gstNumber ?? ''; + _businessVATController.text = user.panNumber ?? ''; + selectedBusinessType = + businessTypes.contains(user.businessType) ? user.businessType : null; + } + + void _updateProfile() { + showLoader(); + final authController = Get.find(); + final user = authController.user; + + if (user == null) return; + + final newUser = user.copyWith( + name: _fullNameController.text, + email: _emailController.text.isNotEmpty ? _emailController.text : null, + businessName: _businessNameController.text, + businessType: selectedBusinessType, + gstNumber: _businessTINController.text, + panNumber: _businessVATController.text, + ); + + authController.registerUser(newUser); + + dismissLoader(); + Get.back(); + } + + @override + void dispose() { + _fullNameController.dispose(); + _emailController.dispose(); + _phoneNumberController.dispose(); + _businessNameController.dispose(); + _businessTINController.dispose(); + _businessVATController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new_sharp, + color: AppTheme.greyTextColor), + onPressed: () { + Get.back(); + }, + ), + title: Text( + 'Account Details', + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.greyTextColor, + ), + ), + ), + body: Padding( + padding: const EdgeInsets.all(AppTheme.spacingDefault), + child: GetBuilder( + builder: (authController) { + if (authController.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + return ListView( + children: [ + const Text( + 'Personal Details', + style: AppTheme.fontStyleDefaultBold, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Name', + controller: _fullNameController, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Email (Optional)', + controller: _emailController, + ), + const Spacing(size: AppTheme.spacingSmall), + PhoneTextField( + hintText: 'Enter 9-digit mobile number', + readOnly: true, + controller: _phoneNumberController, + ), + const Spacing(size: AppTheme.spacingDefault), + const Text( + 'Business Details', + style: AppTheme.fontStyleDefaultBold, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Business Name', + controller: _businessNameController, + ), + const Spacing(size: AppTheme.spacingSmall), + DropdownButtonFormField( + decoration: const InputDecoration( + labelText: 'Business Type', + border: AppTheme.textfieldUnderlineBorder, + ), + value: selectedBusinessType, + items: businessTypes + .map((label) => DropdownMenuItem( + value: label, + child: Text(label), + )) + .toList(), + onChanged: (value) { + setState(() { + selectedBusinessType = value; + }); + }, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'TIN Number (Optional)', + controller: _businessTINController, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'VRN Number (Optional)', + controller: _businessVATController, + ), + const Spacing(size: AppTheme.spacingMedium), + ], + ); + }, + ), + ), + bottomNavigationBar: BottomAppBar( + elevation: 0, + color: AppTheme.colorWhite, + child: CustomBottomButton( + label: 'Save Changes', + onPressed: _updateProfile, + ), + ), + ); + } +} diff --git a/lib/app/modules/profile/views/add_addresses.dart b/lib/app/modules/profile/views/add_addresses.dart new file mode 100644 index 00000000..93aac1d1 --- /dev/null +++ b/lib/app/modules/profile/views/add_addresses.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/profile/controllers/address_controller.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_textfield.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_button.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class AddAddressScreen extends StatelessWidget { + final AddressController controller = Get.put(AddressController()); + + AddAddressScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: const BoxDecoration( + gradient: AppTheme.primaryGradient, + ), + padding: AppTheme.paddingDefault, + child: Center( + child: SingleChildScrollView( + child: Card( + shape: AppTheme.rrShape, + elevation: 10, + shadowColor: AppTheme.colorBlack.withOpacity(0.15), + child: Padding( + padding: const EdgeInsets.all(AppTheme.spacingLarge), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Address Details', + style: AppTheme.fontStyleLarge.copyWith( + color: AppTheme.colorBlack, + fontWeight: FontWeight.bold, + ), + ), + const Spacing(size: AppTheme.spacingMedium), + CustomTextField( + labelText: 'Address Line 1', + controller: controller.line1Controller, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Address Line 2', + controller: controller.line2Controller, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'City', + controller: controller.cityController, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'District', + controller: controller.districtController, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Latitude (optional)', + keyboardType: TextInputType.number, + controller: controller.latitudeController, + ), + const Spacing(size: AppTheme.spacingSmall), + CustomTextField( + labelText: 'Longitude (optional)', + keyboardType: TextInputType.number, + controller: controller.longitudeController, + ), + const Spacing(size: AppTheme.spacingExtraLarge), + CustomButton( + onPressed: controller.saveAddress, // Use saveAddress here + text: 'Save Address', + isDisabled: false, + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/app/modules/profile/views/manage_address.dart b/lib/app/modules/profile/views/manage_address.dart new file mode 100644 index 00000000..7392243c --- /dev/null +++ b/lib/app/modules/profile/views/manage_address.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/profile/controllers/address_controller.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_bottom_button.dart'; +import 'package:get_flutter_fire/app/widgets/common/overlay_loader.dart'; +import 'package:get_flutter_fire/app/widgets/common/show_loader.dart'; +import 'package:get_flutter_fire/app/widgets/profile/address_container.dart'; +import 'package:get_flutter_fire/models/address_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class ManageAddressScreen extends StatefulWidget { + const ManageAddressScreen({super.key}); + + @override + State createState() => _ManageAddressScreenState(); +} + +class _ManageAddressScreenState extends State { + final AddressController addressController = Get.put(AddressController()); + + final AuthController authController = Get.find(); + + @override + void initState() { + super.initState(); + addressController.fetchAddresses(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new_sharp, + color: AppTheme.greyTextColor, size: 18), + onPressed: () { + Get.back(); + }, + ), + title: Text( + 'Manage Address', + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.greyTextColor, + ), + ), + ), + body: Obx(() { + if (addressController.addresses.isEmpty) { + return const LoadingWidget(); + } + + return Padding( + padding: AppTheme.paddingSmall, + child: SingleChildScrollView( + child: Column( + children: [ + ListView.builder( + padding: EdgeInsets.zero, + itemCount: addressController.addresses.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + AddressModel address = addressController.addresses[index]; + bool isDefault = address.id == + authController.currentUser.value?.defaultAddressID; + + return Column( + children: [ + if (index != 0) + const SizedBox(height: AppTheme.spacingTiny), + AddressContainer( + address: address, + onDelete: (id) { + showLoader(); + addressController.deleteAddress(id).then((_) { + dismissLoader(); + }); + }, + isDefault: isDefault, + onSetAsDefault: (id) { + showLoader(); + addressController + .setDefaultAddress(address) + .then((_) { + dismissLoader(); + }); + }, + ), + const Divider( + color: AppTheme.greyTextColor, + height: 0.1, + ), + ], + ); + }, + ), + ], + ), + ), + ); + }), + bottomNavigationBar: BottomAppBar( + elevation: 0, + color: AppTheme.colorWhite, + child: CustomBottomButton( + label: 'Add New Address', + onPressed: () { + Get.toNamed(Routes.ADD_ADDRESS); + }, + ), + ), + ); + } +} diff --git a/lib/app/modules/profile/views/past_queries_screen.dart b/lib/app/modules/profile/views/past_queries_screen.dart new file mode 100644 index 00000000..442fb0a9 --- /dev/null +++ b/lib/app/modules/profile/views/past_queries_screen.dart @@ -0,0 +1,163 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/profile/controllers/contact_controller.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/utils/months.dart'; + +class PastQueriesScreen extends StatelessWidget { + final ContactController contactController = Get.put(ContactController()); + + PastQueriesScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios_new_sharp, + color: AppTheme.greyTextColor, size: 18), + onPressed: () { + Get.back(); + }, + ), + title: Text( + 'Past Queries', + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + ), + ), + body: Padding( + padding: AppTheme.paddingDefault, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacing(size: AppTheme.spacingTiny), + Row( + children: [ + _buildTabButton('Pending'), + const Spacing( + size: AppTheme.spacingDefault, + isHorizontal: true, + ), + _buildTabButton('In-Progress'), + const Spacing( + size: AppTheme.spacingDefault, + isHorizontal: true, + ), + _buildTabButton('Completed'), + ], + ), + const Spacing(size: AppTheme.spacingMedium), + Expanded( + child: Obx(() { + if (contactController.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + if (contactController.filteredEnquiries.isEmpty) { + return const Center(child: Text('No queries found.')); + } + + return ListView.builder( + itemCount: contactController.filteredEnquiries.length, + itemBuilder: (context, index) { + final enquiry = contactController.filteredEnquiries[index]; + return Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Icons.insert_drive_file, + color: AppTheme.greyTextColor), + const Spacing( + size: AppTheme.spacingSmall, + isHorizontal: true), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(enquiry.message, + style: AppTheme.fontStyleDefaultBold), + const Spacing(size: AppTheme.spacingTiny), + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.symmetric( + vertical: 4.0, horizontal: 8.0), + decoration: BoxDecoration( + borderRadius: + BorderRadius.circular(6.0), + border: Border.all( + color: AppTheme.colorMain), + ), + child: Text(enquiry.queryType.name, + style: AppTheme.fontStyleDefault + .copyWith( + color: AppTheme.colorMain, + )), + ), + const Spacing( + size: AppTheme.spacingSmall), + Text( + '${enquiry.timestamp.day} ${monthString(enquiry.timestamp.month)}, ${enquiry.timestamp.year}, ${enquiry.timestamp.hour}:${enquiry.timestamp.minute} ${enquiry.timestamp.hour >= 12 ? "PM" : "AM"}', + style: AppTheme.fontStyleDefault, + ), + ], + ), + ], + ), + ), + ], + ), + const Divider( + height: 32, color: AppTheme.greyTextColor), + ], + ); + }, + ); + }), + ), + ], + ), + ), + ); + } + + Widget _buildTabButton(String tab) { + return Expanded( + child: Obx(() { + final isSelected = contactController.selectedTab.value == tab; + return InkWell( + onTap: () { + contactController.changeTab(tab); + }, + child: Container( + padding: AppTheme.paddingSmall, + decoration: BoxDecoration( + color: isSelected ? AppTheme.colorMain : AppTheme.colorWhite, + borderRadius: BorderRadius.circular(12.0), + border: Border.all( + color: isSelected ? AppTheme.colorMain : AppTheme.greyTextColor, + ), + ), + child: Center( + child: Text( + tab, + style: AppTheme.fontStyleDefaultBold.copyWith( + fontSize: 12, + color: isSelected ? AppTheme.colorWhite : AppTheme.colorBlack, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ), + ); + }), + ); + } +} diff --git a/lib/app/modules/profile/views/profile.dart b/lib/app/modules/profile/views/profile.dart new file mode 100644 index 00000000..2f545307 --- /dev/null +++ b/lib/app/modules/profile/views/profile.dart @@ -0,0 +1,157 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/app/widgets/common/show_loader.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/app/widgets/profile/profile_list_widget.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/theme/assets.dart'; + +class ProfileScreen extends StatelessWidget { + const ProfileScreen({super.key}); + + @override + Widget build(BuildContext context) { + final AuthController authController = Get.find(); + + String getInitials(String name) { + List nameParts = name.split(' '); + String initials = ''; + if (nameParts.isNotEmpty) { + initials = nameParts.map((part) => part[0].toUpperCase()).join(); + } + return initials; + } + + final List> profileItems = [ + { + 'imagePath': iconProfile, + 'text': 'Account Details', + 'onTap': () { + Get.toNamed(Routes.ACCOUNT_DETAILS); + } + }, + { + 'imagePath': iconLocation, + 'text': 'Manage Address', + 'onTap': () { + Get.toNamed(Routes.MANAGE_ADDRESS, + arguments: {'user': authController.currentUser.value}); + } + }, + { + 'imagePath': iconFile, + 'text': 'Terms and Conditions', + 'onTap': () {}, + }, + { + 'imagePath': iconFile, + 'text': 'Privacy Policy', + 'onTap': () {}, + }, + { + 'imagePath': iconSignout, + 'text': 'Sign Out', + 'onTap': () async { + showLoader(); + authController.clearUserData(); + Get.offAllNamed(Routes.WELCOME); + dismissLoader(); + }, + }, + ]; + + return Scaffold( + body: Column( + children: [ + const Spacing(size: AppTheme.spacingMedium), + Container( + padding: AppTheme.paddingDefault, + child: Row( + children: [ + Container( + decoration: BoxDecoration( + color: AppTheme.colorMain, + borderRadius: BorderRadius.circular(6), + ), + width: 60, + height: 60, + child: Center( + child: Obx(() { + final user = authController.user; + return Text( + user != null ? getInitials(user.name) : 'JD', + style: AppTheme.fontStyleLarge + .copyWith(color: AppTheme.colorWhite), + ); + }), + ), + ), + const Spacing(size: AppTheme.spacingSmall, isHorizontal: true), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Obx(() { + final user = authController.user; + return Text( + user?.name ?? 'Unknown User', + style: AppTheme.fontStyleMedium + .copyWith(color: AppTheme.colorMain), + ); + }), + const SizedBox(height: AppTheme.spacingTiny), + Obx(() { + final user = authController.user; + return Text( + user?.phoneNumber ?? '+225 123 456 789', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorMain), + ); + }), + ], + ), + ], + ), + ), + Expanded( + child: Container( + padding: AppTheme.paddingTiny, + decoration: const BoxDecoration( + color: AppTheme.colorWhite, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + ), + child: Column( + children: [ + Expanded( + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: profileItems.length, + itemBuilder: (context, index) { + final item = profileItems[index]; + return Column( + children: [ + if (index == 0) + const Spacing(size: AppTheme.spacingTiny), + ProfileListItem( + imagePath: item['imagePath'], + text: item['text'], + onTap: item['onTap'], + ), + ], + ); + }, + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/app/modules/root/controllers/root_controller.dart b/lib/app/modules/root/controllers/root_controller.dart index 7f160fc6..3c534f09 100644 --- a/lib/app/modules/root/controllers/root_controller.dart +++ b/lib/app/modules/root/controllers/root_controller.dart @@ -1,14 +1,23 @@ -import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/modules/seller/controllers/seller_controller.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; class RootController extends GetxController { - GlobalKey scaffoldKey = GlobalKey(); + var selectedIndex = 0.obs; - void openDrawer() { - scaffoldKey.currentState!.openDrawer(); + final AuthController authController = Get.find(); + + @override + void onInit() { + super.onInit(); + + if (authController.user?.userType == UserType.seller) { + Get.put(SellerController()); + } } - void closeDrawer() { - scaffoldKey.currentState!.openEndDrawer(); + void changeTabIndex(int index) { + selectedIndex.value = index; } } diff --git a/lib/app/modules/root/root_view.dart b/lib/app/modules/root/root_view.dart new file mode 100644 index 00000000..e6de73e0 --- /dev/null +++ b/lib/app/modules/root/root_view.dart @@ -0,0 +1,217 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/cart_controller.dart'; +import 'package:get_flutter_fire/app/modules/home/view/home.dart'; +import 'package:get_flutter_fire/app/modules/orders/views/orders.dart'; +import 'package:get_flutter_fire/app/modules/profile/views/profile.dart'; +import 'package:get_flutter_fire/app/modules/root/controllers/root_controller.dart'; +import 'package:get_flutter_fire/app/modules/seller/views/seller.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class RootView extends StatelessWidget { + final AuthController authController; + final RootController rootController; + + RootView({super.key}) + : authController = Get.find(), + rootController = Get.put(RootController()); + + @override + Widget build(BuildContext context) { + return Obx(() { + if (authController.user == null) { + Future.microtask(() => Get.offAllNamed(Routes.WELCOME)); + return const SizedBox.shrink(); + } + final showAppBar = rootController.selectedIndex.value != 2; + + return Scaffold( + appBar: showAppBar ? _buildAppBar() : null, + body: _getTabContent(rootController.selectedIndex.value), + bottomNavigationBar: _buildBottomNavigationBar(), + ); + }); + } + + AppBar _buildAppBar() { + return AppBar( + title: _buildAppBarTitle(), + backgroundColor: AppTheme.colorMain, + actions: _buildAppBarActions(), + ); + } + + Widget _buildAppBarTitle() { + final user = authController.user!; + return Padding( + padding: const EdgeInsets.only(bottom: AppTheme.spacingTiny), + child: Row( + children: [ + const SizedBox(width: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Welcome to Sheru,', + style: TextStyle(fontSize: 14, color: Colors.white)), + Text( + user.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ], + ), + ], + ), + ); + } + + List _buildAppBarActions() { + return [ + IconButton( + icon: const Icon(Icons.shopping_cart, color: Colors.white), + onPressed: () { + final cartController = Get.find(); + if (cartController.cart.itemCount == 0) { + Get.snackbar('Cart', 'Cart is empty'); + return; + } + Get.toNamed(Routes.CART); + }, + ), + if (authController.user!.userType == UserType.seller) + IconButton( + icon: const Icon(Icons.store, color: Colors.white), + onPressed: () {}, + ), + ]; + } + + Widget _getTabContent(int index) { + final buyerTabs = [ + const HomeScreen(), + const OrdersScreen(), + const ProfileScreen() + ]; + + final sellerTabs = [ + const HomeScreen(), + const OrdersScreen(), + const ProfileScreen(), + SellerPage(), + ]; + + final userType = authController.user!.userType; + + return userType == UserType.seller ? sellerTabs[index] : buyerTabs[index]; + } + + Widget _buildBottomNavigationBar() { + return Obx(() { + final items = _buildBottomNavItems(); + + return Container( + padding: const EdgeInsets.only(bottom: AppTheme.spacingTiny), + decoration: BoxDecoration( + color: AppTheme.colorWhite, + boxShadow: AppTheme.bottomBoxShadow, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: items.map((item) { + return Expanded( + child: CustomBottomNavigationBarItem( + icon: item.icon, + label: item.label!, + index: items.indexOf(item), + currentIndex: rootController.selectedIndex.value, + onTap: rootController.changeTabIndex, + ), + ); + }).toList(), + ), + const SizedBox(height: AppTheme.spacingTiny), + ], + ), + ); + }); + } + + List _buildBottomNavItems() { + final items = [ + const BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'), + const BottomNavigationBarItem( + icon: Icon(Icons.shopping_cart), label: 'Orders'), + const BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'), + ]; + + if (authController.user!.userType == UserType.seller) { + items.add(const BottomNavigationBarItem( + icon: Icon(Icons.store), label: 'Sell')); + } + + return items; + } +} + +class CustomBottomNavigationBarItem extends StatelessWidget { + final Widget icon; + final String label; + final int index; + final int currentIndex; + final Function(int) onTap; + + const CustomBottomNavigationBarItem({ + super.key, + required this.icon, + required this.label, + required this.index, + required this.currentIndex, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final isSelected = currentIndex == index; + + return GestureDetector( + onTap: () => onTap(index), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (isSelected) + Container( + height: 4, + width: 40, + color: AppTheme.colorMain, + ), + const SizedBox(height: AppTheme.spacingTiny), + IconTheme( + data: IconThemeData( + color: isSelected ? AppTheme.colorMain : AppTheme.greyTextColor, + size: 28, + ), + child: icon, + ), + const SizedBox(height: AppTheme.spacingTiny), + Text( + label, + style: TextStyle( + color: isSelected ? AppTheme.colorMain : AppTheme.greyTextColor, + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + ), + ), + ], + ), + ); + } +} diff --git a/lib/app/modules/seller/controllers/seller_controller.dart b/lib/app/modules/seller/controllers/seller_controller.dart new file mode 100644 index 00000000..b3e838f9 --- /dev/null +++ b/lib/app/modules/seller/controllers/seller_controller.dart @@ -0,0 +1,187 @@ +import 'dart:io'; +import 'package:get/get.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:get_flutter_fire/constants.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; +import 'package:get_flutter_fire/models/product_model.dart'; +import 'package:get_flutter_fire/models/seller_model.dart'; +import 'package:get_flutter_fire/models/user_model.dart'; +import 'package:get_flutter_fire/services/auth_service.dart'; +import 'package:get_flutter_fire/utils/get_uuid.dart'; + +class SellerController extends GetxController { + final AuthService authService = Get.find(); + var products = [].obs; + var isLoading = false.obs; + + String get sellerId { + final user = authService.userID; + return "seller_$user"; + } + + Future assignSellerRole(UserModel user) async { + if (user.userType != UserType.seller) { + return; + } + + SellerModel seller = SellerModel( + id: user.id, + name: user.name, + phoneNumber: user.phoneNumber, + email: user.email, + isBusiness: user.isBusiness, + businessName: user.businessName, + businessType: user.businessType, + gstNumber: user.gstNumber, + panNumber: user.panNumber, + userType: UserType.seller, + defaultAddressID: user.defaultAddressID, + createdAt: user.createdAt, + lastSeenAt: user.lastSeenAt, + sellerId: sellerId, + products: [], + ); + + await FirebaseFirestore.instance + .collection('sellers') + .doc(sellerId) + .set(seller.toMap()); + } + + Future onUserRoleChanged(UserModel user) async { + if (user.userType == UserType.seller) { + await assignSellerRole(user); + } + } + + Future fetchSellerProducts() async { + isLoading.value = true; + try { + FirebaseFirestore.instance + .collection('products') + .where('sellerId', isEqualTo: sellerId) + .snapshots() + .listen((event) { + final fetchedProducts = + event.docs.map((doc) => ProductModel.fromMap(doc.data())).toList(); + products.assignAll(fetchedProducts); + }); + } catch (e) { + Get.snackbar('Error', 'Failed to fetch products: $e'); + } finally { + isLoading.value = false; + } + } + + Future> _uploadImages(List images) async { + List imageUrls = []; + for (var image in images) { + final productId = getUUID(); + final storageRef = firebaseStorage.ref(); + final imagesRef = storageRef.child('products/$productId.png'); + await imagesRef.putFile(File(image)); + final imageUrl = await imagesRef.getDownloadURL(); + imageUrls.add(imageUrl); + } + + return imageUrls; + } + + Future addSellerProduct({ + required String name, + required String description, + required int unitPrice, + required int remainingQuantity, + required int unitWeight, + required List images, + }) async { + isLoading.value = true; + try { + final productId = getUUID(); + final imagesLinks = await _uploadImages(images); + final newProduct = ProductModel( + id: productId, + categoryID: 'eef6bb5d-b07a-4ec1-bf0d-91636580ca90', + name: name, + description: description, + unitPrice: unitPrice, + remainingQuantity: remainingQuantity, + unitWeight: unitWeight, + images: imagesLinks, + isActive: true, + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + sellerId: sellerId, + isApproved: false, + isSheruSpecial: false, + ); + + await FirebaseFirestore.instance + .collection('products') + .doc(productId) + .set(newProduct.toMap()); + + Get.snackbar('Success', 'Product added successfully'); + } catch (e) { + Get.snackbar('Error', 'Failed to add product: $e'); + } finally { + isLoading.value = false; + } + } + + Future updateSellerProduct({ + required String id, + required String name, + required String description, + required int unitPrice, + required int remainingQuantity, + required int unitWeight, + required List images, + }) async { + isLoading.value = true; + try { + final updatedProduct = ProductModel( + id: id, + categoryID: 'eef6bb5d-b07a-4ec1-bf0d-91636580ca90', + name: name, + description: description, + unitPrice: unitPrice, + remainingQuantity: remainingQuantity, + unitWeight: unitWeight, + images: images, + isActive: true, + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + sellerId: sellerId, + isApproved: false, + isSheruSpecial: false, + ); + + await FirebaseFirestore.instance + .collection('products') + .doc(id) + .update(updatedProduct.toMap()); + + Get.snackbar('Success', 'Product updated successfully'); + } catch (e) { + Get.snackbar('Error', 'Failed to update product: $e'); + } finally { + isLoading.value = false; + } + } + + Future deleteSellerProduct(String productId) async { + isLoading.value = true; + try { + await FirebaseFirestore.instance + .collection('products') + .doc(productId) + .delete(); + Get.snackbar('Success', 'Product deleted successfully'); + } catch (e) { + Get.snackbar('Error', 'Failed to delete product: $e'); + } finally { + isLoading.value = false; + } + } +} diff --git a/lib/app/modules/seller/views/add_product.dart b/lib/app/modules/seller/views/add_product.dart new file mode 100644 index 00000000..583cac21 --- /dev/null +++ b/lib/app/modules/seller/views/add_product.dart @@ -0,0 +1,182 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/seller/controllers/seller_controller.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_textfield.dart'; +import 'package:get_flutter_fire/app/widgets/common/overlay_loader.dart'; +import 'package:image_picker/image_picker.dart'; +import 'dart:io'; + +class AddProductPage extends StatelessWidget { + final SellerController sellerController = Get.find(); + + AddProductPage({super.key}); + + final TextEditingController nameController = TextEditingController(); + final TextEditingController descriptionController = TextEditingController(); + final TextEditingController priceController = TextEditingController(); + final TextEditingController quantityController = TextEditingController(); + final TextEditingController weightController = TextEditingController(); + final RxList images = [].obs; + final RxBool isUploading = false.obs; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Add Product'), + backgroundColor: AppTheme.colorMain, + ), + body: SafeArea( + child: Padding( + padding: AppTheme.paddingDefault, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomTextField( + labelText: 'Product Name', + controller: nameController, + ), + const SizedBox(height: AppTheme.spacingDefault), + CustomTextField( + labelText: 'Description', + controller: descriptionController, + ), + const SizedBox(height: AppTheme.spacingDefault), + CustomTextField( + labelText: 'Price', + controller: priceController, + keyboardType: TextInputType.number, + ), + const SizedBox(height: AppTheme.spacingDefault), + CustomTextField( + labelText: 'Quantity', + controller: quantityController, + keyboardType: TextInputType.number, + ), + const SizedBox(height: AppTheme.spacingDefault), + CustomTextField( + labelText: 'Unit Weight (grams)', + controller: weightController, + keyboardType: TextInputType.number, + ), + const SizedBox(height: AppTheme.spacingDefault), + _buildImagePicker(images, isUploading), + const SizedBox(height: AppTheme.spacingDefault), + ElevatedButton( + onPressed: () { + if (nameController.text.isEmpty || + descriptionController.text.isEmpty || + priceController.text.isEmpty || + quantityController.text.isEmpty || + weightController.text.isEmpty || + images.isEmpty) { + Get.snackbar('Error', + 'Please fill all fields and add at least one image.'); + return; + } + sellerController.addSellerProduct( + name: nameController.text, + description: descriptionController.text, + unitPrice: int.parse(priceController.text), + remainingQuantity: int.parse(quantityController.text), + unitWeight: int.parse(weightController.text), + images: images.toList(), + ); + + Navigator.pop(context); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.colorMain, + minimumSize: const Size.fromHeight(50), + ), + child: Text( + 'Add Product', + style: AppTheme.fontStyleMedium + .copyWith(color: AppTheme.colorWhite), + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildImagePicker(RxList images, RxBool isUploading) { + return Obx(() => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Product Images', style: AppTheme.fontStyleMedium), + const SizedBox(height: AppTheme.spacingSmall), + Wrap( + spacing: 10, + children: [ + ...images.map((image) => _buildImagePreview(image, images)), + _buildAddImageButton(images, isUploading), + ], + ), + if (isUploading.value) + const Padding( + padding: EdgeInsets.only(top: 16), + child: Center( + child: LoadingWidget(), + ), + ), + ], + )); + } + + Widget _buildImagePreview(String imagePath, RxList images) { + return Stack( + children: [ + Image.file( + File(imagePath), + width: 100, + height: 100, + fit: BoxFit.cover, + ), + Positioned( + top: 0, + right: 0, + child: GestureDetector( + onTap: () => images.remove(imagePath), + child: Container( + color: Colors.black54, + child: const Icon(Icons.close, color: Colors.white, size: 20), + ), + ), + ), + ], + ); + } + + Widget _buildAddImageButton(RxList images, RxBool isUploading) { + return GestureDetector( + onTap: () async { + final ImagePicker picker = ImagePicker(); + isUploading.value = true; + try { + final pickedFile = + await picker.pickImage(source: ImageSource.gallery); + if (pickedFile != null) { + images.add(pickedFile.path); + } + } finally { + isUploading.value = false; + } + }, + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: AppTheme.colorDisabled, + borderRadius: AppTheme.borderRadius, + ), + child: const Icon(Icons.add_a_photo, color: Colors.white), + ), + ); + } +} diff --git a/lib/app/modules/seller/views/edit_product_page.dart b/lib/app/modules/seller/views/edit_product_page.dart new file mode 100644 index 00000000..bafea295 --- /dev/null +++ b/lib/app/modules/seller/views/edit_product_page.dart @@ -0,0 +1,206 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/seller/controllers/seller_controller.dart'; +import 'package:get_flutter_fire/models/product_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/app/widgets/common/custom_textfield.dart'; +import 'package:get_flutter_fire/app/widgets/common/overlay_loader.dart'; +import 'package:image_picker/image_picker.dart'; +// import 'dart:io'; + +class EditProductPage extends StatelessWidget { + final ProductModel product; + final SellerController sellerController = Get.find(); + + EditProductPage({super.key, required this.product}); + + final TextEditingController nameController = TextEditingController(); + final TextEditingController descriptionController = TextEditingController(); + final TextEditingController priceController = TextEditingController(); + final TextEditingController quantityController = TextEditingController(); + final TextEditingController weightController = TextEditingController(); + final RxList images = [].obs; + final RxBool isUploading = false.obs; + + @override + Widget build(BuildContext context) { + nameController.text = product.name; + descriptionController.text = product.description; + priceController.text = product.unitPrice.toString(); + quantityController.text = product.remainingQuantity.toString(); + weightController.text = product.unitWeight.toString(); + images.assignAll(product.images); + + return Scaffold( + appBar: AppBar( + title: const Text('Edit Product'), + backgroundColor: AppTheme.colorMain, + ), + body: SafeArea( + child: Padding( + padding: AppTheme.paddingDefault, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomTextField( + labelText: 'Product Name', + controller: nameController, + ), + const SizedBox(height: AppTheme.spacingDefault), + CustomTextField( + labelText: 'Description', + controller: descriptionController, + ), + const SizedBox(height: AppTheme.spacingDefault), + CustomTextField( + labelText: 'Price', + controller: priceController, + keyboardType: TextInputType.number, + ), + const SizedBox(height: AppTheme.spacingDefault), + CustomTextField( + labelText: 'Quantity', + controller: quantityController, + keyboardType: TextInputType.number, + ), + const SizedBox(height: AppTheme.spacingDefault), + CustomTextField( + labelText: 'Unit Weight (grams)', + controller: weightController, + keyboardType: TextInputType.number, + ), + const SizedBox(height: AppTheme.spacingDefault), + _buildImagePicker(images, isUploading), + const SizedBox(height: AppTheme.spacingDefault), + ElevatedButton( + onPressed: () { + if (nameController.text.isEmpty || + descriptionController.text.isEmpty || + priceController.text.isEmpty || + quantityController.text.isEmpty || + weightController.text.isEmpty || + images.isEmpty) { + Get.snackbar('Error', + 'Please fill all fields and add at least one image.'); + return; + } + + sellerController.updateSellerProduct( + id: product.id, + name: nameController.text, + description: descriptionController.text, + unitPrice: int.parse(priceController.text), + remainingQuantity: int.parse(quantityController.text), + unitWeight: int.parse(weightController.text), + images: images.toList(), + ); + + Navigator.pop(context); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.colorMain, + minimumSize: const Size.fromHeight(50), + ), + child: const Text('Update Product'), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildImagePicker(RxList images, RxBool isUploading) { + return Obx(() => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Product Images', style: AppTheme.fontStyleMedium), + const SizedBox(height: AppTheme.spacingSmall), + Wrap( + spacing: 10, + children: [ + ...images.map((image) => _buildImagePreview(image, images)), + _buildAddImageButton(images, isUploading), + ], + ), + if (isUploading.value) + const Padding( + padding: EdgeInsets.only(top: 16), + child: Center( + child: LoadingWidget(), + ), + ), + ], + )); + } + + Widget _buildImagePreview(String imagePath, RxList images) { + return Stack( + children: [ + Image.network( + imagePath, + width: 100, + height: 100, + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Center( + child: CircularProgressIndicator( + value: loadingProgress.expectedTotalBytes != null + ? loadingProgress.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : null, + ), + ); + }, + ), + // Image.file( + // File(imagePath), + // width: 100, + // height: 100, + // fit: BoxFit.cover, + // ), + Positioned( + top: 0, + right: 0, + child: GestureDetector( + onTap: () => images.remove(imagePath), + child: Container( + color: Colors.black54, + child: const Icon(Icons.close, color: Colors.white, size: 20), + ), + ), + ), + ], + ); + } + + Widget _buildAddImageButton(RxList images, RxBool isUploading) { + return GestureDetector( + onTap: () async { + final ImagePicker picker = ImagePicker(); + isUploading.value = true; + try { + final pickedFile = + await picker.pickImage(source: ImageSource.gallery); + if (pickedFile != null) { + images.add(pickedFile.path); + } + } finally { + isUploading.value = false; + } + }, + child: Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: AppTheme.colorDisabled, + borderRadius: AppTheme.borderRadius, + ), + child: const Icon(Icons.add_a_photo, color: Colors.white), + ), + ); + } +} diff --git a/lib/app/modules/seller/views/seller.dart b/lib/app/modules/seller/views/seller.dart new file mode 100644 index 00000000..a532b816 --- /dev/null +++ b/lib/app/modules/seller/views/seller.dart @@ -0,0 +1,219 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/modules/seller/controllers/seller_controller.dart'; +import 'package:get_flutter_fire/app/modules/seller/views/add_product.dart'; +import 'package:get_flutter_fire/app/modules/seller/views/edit_product_page.dart'; +import 'package:get_flutter_fire/models/product_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class SellerPage extends StatelessWidget { + final AuthController authController = Get.find(); + final SellerController sellerController = Get.put(SellerController()); + + SellerPage({super.key}); + + @override + Widget build(BuildContext context) { + sellerController.fetchSellerProducts(); + + return Scaffold( + body: Obx(() { + if (sellerController.isLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + if (sellerController.products.isEmpty) { + return const Center( + child: Text( + 'No Products Available', + style: TextStyle(color: Colors.grey, fontSize: 18), + ), + ); + } + return _buildProductList(sellerController.products, context); + }), + floatingActionButton: FloatingActionButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => AddProductPage()), //TODO: AddProductPage + ), + backgroundColor: Colors.black, + child: const Icon(Icons.add, color: Colors.white), + ), + ); + } + + Widget _buildProductList(List products, BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 16), + Text( + "Manage Products", + style: AppTheme.fontStyleLarge.copyWith( + color: Colors.black, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: products.length, + itemBuilder: (context, index) { + final product = products[index]; + return _buildProductCard(product, context); + }, + ), + ], + ), + ); + } + + Widget _buildProductCard(ProductModel product, BuildContext context) { + return GestureDetector( + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => EditProductPage(product: product)), + ), + child: Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + ), + child: Image.network( + product.images.isNotEmpty ? product.images.first : '', + width: double.infinity, + height: 150, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + width: double.infinity, + height: 150, + color: Colors.grey[200], + child: const Icon( + Icons.image_not_supported, + size: 80, + color: Colors.grey, + ), + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 6), + Text( + product.description, + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 6), + Text( + '₹${product.unitPrice}', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + const SizedBox(height: 6), + Text( + 'Quantity Left: ${product.remainingQuantity}', + style: const TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + EditProductPage(product: product)), + ), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueAccent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 16), + ), + child: const Text( + 'Edit', + style: TextStyle(fontSize: 14, color: Colors.white), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton( + onPressed: () => + sellerController.deleteSellerProduct(product.id), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + padding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 16), + ), + child: const Text( + 'Delete', + style: TextStyle(fontSize: 14, color: Colors.white), + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/app/modules/splash/splash_screen.dart b/lib/app/modules/splash/splash_screen.dart new file mode 100644 index 00000000..afabee12 --- /dev/null +++ b/lib/app/modules/splash/splash_screen.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/widgets/common/show_toast.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/services/notification_service.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/theme/assets.dart'; + +class SplashScreen extends StatefulWidget { + const SplashScreen({super.key}); + + @override + State createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State { + late final AuthController _authController; + + @override + void initState() { + super.initState(); + _authController = Get.put(AuthController()); + + Future.delayed(const Duration(seconds: 2), () async { + if (_authController.user != null) { + await NotificationService().storeToken(_authController.user!.id); + showToast("Welcome", isShort: true); + Get.offNamed(Routes.ROOT); + } else { + showToast("Welcome", isShort: true); + Get.offNamed(Routes.WELCOME); + } + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + fit: StackFit.expand, + children: [ + Column( + children: [ + SizedBox( + height: MediaQuery.of(context).size.height * 0.4, + ), + const Spacing(size: AppTheme.spacingDefault), + Image.asset( + logo, + height: 94, + width: 94, + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/app/routes/app_pages.dart b/lib/app/routes/app_pages.dart index 7269755d..00180a6f 100644 --- a/lib/app/routes/app_pages.dart +++ b/lib/app/routes/app_pages.dart @@ -1,154 +1,90 @@ -import 'package:flutter/material.dart'; import 'package:get/get.dart'; - -import '../../models/access_level.dart'; -import '../../models/role.dart'; -import '../middleware/auth_middleware.dart'; -import '../modules/cart/bindings/cart_binding.dart'; -import '../modules/cart/views/cart_view.dart'; -import '../modules/categories/bindings/categories_binding.dart'; -import '../modules/categories/views/categories_view.dart'; -import '../modules/checkout/bindings/checkout_binding.dart'; -import '../modules/checkout/views/checkout_view.dart'; -import '../modules/dashboard/bindings/dashboard_binding.dart'; -import '../modules/dashboard/views/dashboard_view.dart'; -import '../modules/home/bindings/home_binding.dart'; -import '../modules/home/views/home_view.dart'; -import '../modules/login/bindings/login_binding.dart'; -import '../modules/login/views/login_view.dart'; -import '../modules/my_products/bindings/my_products_binding.dart'; -import '../modules/my_products/views/my_products_view.dart'; -import '../modules/product_details/bindings/product_details_binding.dart'; -import '../modules/product_details/views/product_details_view.dart'; -import '../modules/products/bindings/products_binding.dart'; -import '../modules/products/views/products_view.dart'; -import '../modules/profile/bindings/profile_binding.dart'; -import '../modules/profile/views/profile_view.dart'; -import '../modules/register/bindings/register_binding.dart'; -import '../modules/register/views/register_view.dart'; -import '../modules/root/bindings/root_binding.dart'; -import '../modules/root/views/root_view.dart'; -import '../modules/settings/bindings/settings_binding.dart'; -import '../modules/settings/views/settings_view.dart'; -import '../modules/task_details/bindings/task_details_binding.dart'; -import '../modules/task_details/views/task_details_view.dart'; -import '../modules/tasks/bindings/tasks_binding.dart'; -import '../modules/tasks/views/tasks_view.dart'; -import '../modules/users/bindings/users_binding.dart'; -import '../modules/users/views/users_view.dart'; -import '../../models/screens.dart'; - -part 'app_routes.dart'; -part 'screen_extension.dart'; +import 'package:get_flutter_fire/app/modules/auth/views/address_screen.dart'; +import 'package:get_flutter_fire/app/modules/auth/views/login_screen.dart'; +import 'package:get_flutter_fire/app/modules/auth/views/otp_screen.dart'; +import 'package:get_flutter_fire/app/modules/auth/views/register_screen.dart'; +import 'package:get_flutter_fire/app/modules/auth/views/welcome_screen.dart'; +import 'package:get_flutter_fire/app/modules/cart/views/cart_root_view.dart'; +import 'package:get_flutter_fire/app/modules/cart/views/order_confirmed.dart'; +import 'package:get_flutter_fire/app/modules/orders/views/order_detail_screen.dart'; +import 'package:get_flutter_fire/app/modules/profile/views/account_detail.dart'; +import 'package:get_flutter_fire/app/modules/profile/views/add_addresses.dart'; +import 'package:get_flutter_fire/app/modules/profile/views/manage_address.dart'; +import 'package:get_flutter_fire/app/modules/profile/views/past_queries_screen.dart'; +import 'package:get_flutter_fire/app/modules/root/root_view.dart'; +import 'package:get_flutter_fire/app/modules/splash/splash_screen.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; class AppPages { AppPages._(); - static const INITIAL = Routes.HOME; + static const INITIAL = Routes.SPLASH; - //TODO create this using the information from Screen and Role data - //can use https://pub.dev/packages/freezed static final routes = [ GetPage( - name: '/', - page: () => const RootView(), - binding: RootBinding(), - participatesInRootNavigator: true, - preventDuplicates: true, - children: [ - Screen.LOGIN.getPage( - page: () => const LoginView(), - binding: LoginBinding(), - ), - Screen.REGISTER.getPage( - page: () => const RegisterView(), - binding: RegisterBinding(), - ), - Screen.PROFILE.getPage( - page: () => const ProfileView(), - binding: ProfileBinding(), - ), - Screen.SETTINGS.getPage( - page: () => const SettingsView(), - binding: SettingsBinding(), - ), - Screen.HOME.getPage( - page: () => const HomeView(), - bindings: [ - HomeBinding(), - ], - children: [ - Screen.DASHBOARD.getPage( - page: () => const DashboardView(), - binding: DashboardBinding(), - ), - Screen.USERS.getPage( - role: Role.admin, - page: () => const UsersView(), - binding: UsersBinding(), - children: [ - Screen.USER_PROFILE.getPage( - page: () => const ProfileView(), - binding: ProfileBinding(), - ) - ], - ), - Screen.PRODUCTS.getPage( - page: () => const ProductsView(), - binding: ProductsBinding(), - children: [ - Screen.PRODUCT_DETAILS.getPages( - page: () => const ProductDetailsView(), - binding: ProductDetailsBinding(), - ), - ], - ), - Screen.CATEGORIES.getPage( - role: Role.admin, - page: () => const CategoriesView(), - binding: CategoriesBinding(), - ), - Screen.CART.getPage( - page: () => const CartView(), - binding: CartBinding(), - role: Role.buyer, - children: [ - Screen.CHECKOUT.getPage( - //if this is after cart details, it never gets reached - page: () => const CheckoutView(), - binding: CheckoutBinding(), - ), - Screen.CART_DETAILS.getPages( - page: () => const ProductDetailsView(), - binding: ProductDetailsBinding(), - ), - ], - ), - Screen.MY_PRODUCTS.getPage( - page: () => const MyProductsView(), - binding: MyProductsBinding(), - role: Role.seller, - children: [ - Screen.MY_PRODUCT_DETAILS.getPages( - page: () => const ProductDetailsView(), - binding: ProductDetailsBinding(), - ), - ], - ), - Screen.TASKS.getPage( - role: Role.admin, - page: () => const TasksView(), - binding: TasksBinding(), - children: [ - Screen.TASK_DETAILS.getPage( - page: () => const TaskDetailsView(), - binding: TaskDetailsBinding(), - ), - ], - ), - ], - ) - ], + name: Routes.SPLASH, + page: () => const SplashScreen(), + ), + GetPage( + name: Routes.WELCOME, + page: () => const WelcomeScreen(), + ), + GetPage( + name: Routes.LOGIN, + page: () => const LoginScreen(), + ), + GetPage( + name: Routes.OTP, + page: () => OtpScreen(phoneNumber: Get.arguments['phoneNumber']), + ), + GetPage( + name: Routes.REGISTER, + page: () => RegisterScreen( + phoneNumber: Get.arguments['phoneNumber'], + ), + ), + GetPage( + name: Routes.ADDRESS, + page: () => AddressScreen(), + ), + GetPage( + name: Routes.ROOT, + page: () => RootView(), + // binding: AuthBindings(), + ), + + //profile routes + GetPage( + name: Routes.ACCOUNT_DETAILS, + page: () => const AccountDetailsScreen(), + ), + GetPage( + name: Routes.MANAGE_ADDRESS, + page: () => const ManageAddressScreen(), + ), + GetPage( + name: Routes.ADD_ADDRESS, + page: () => AddAddressScreen(), + ), + + GetPage( + name: Routes.PAST_QUERIES, + page: () => PastQueriesScreen(), + ), + + //Cart Routes + GetPage( + name: Routes.CART, + page: () => const CartRootView(), + ), + //Order + GetPage( + name: Routes.ORDER_CONFIRMED, + page: () => const OrderConfirmedScreen(), + ), + GetPage( + name: Routes.ORDER_DETAILS, + page: () => const OrderDetailScreen(), ), ]; } diff --git a/lib/app/routes/app_routes.dart b/lib/app/routes/app_routes.dart index f3129d21..5cd01ec8 100644 --- a/lib/app/routes/app_routes.dart +++ b/lib/app/routes/app_routes.dart @@ -1,54 +1,42 @@ -// ignore_for_file: non_constant_identifier_names, constant_identifier_names - -part of 'app_pages.dart'; -// DO NOT EDIT. This is code generated via package:get_cli/get_cli.dart - abstract class Routes { - static const HOME = _Paths.HOME; - // static String PROFILE = Screen.PROFILE.fullPath; - // static String SETTINGS = Screen.SETTINGS.fullPath; - static String LOGIN = Screen.LOGIN.route; - static String REGISTER = Screen.REGISTER.route; - // static String DASHBOARD = Screen.DASHBOARD.fullPath; - // static String PRODUCTS = Screen.PRODUCTS.fullPath; - // static String CART = Screen.CART.fullPath; - // static String CHECKOUT = Screen.CHECKOUT.fullPath; - // static const CATEGORIES = _Paths.HOME + _Paths.CATEGORIES; - // static const TASKS = _Paths.HOME + _Paths.TASKS; - // static const USERS = _Paths.HOME + _Paths.USERS; - // static const MY_PRODUCTS = _Paths.HOME + _Paths.MY_PRODUCTS; - - static String PRODUCT_DETAILS(String productId) => - '${Screen.PRODUCTS.route}/$productId'; - static String CART_DETAILS(String productId) => - '${Screen.CART.route}/$productId'; - static String TASK_DETAILS(String taskId) => '${Screen.TASKS.route}/$taskId'; - static String USER_PROFILE(String uId) => '${Screen.USERS.route}/$uId'; - Routes._(); - static String LOGIN_THEN(String afterSuccessfulLogin) => - '${Screen.LOGIN.route}?then=${Uri.encodeQueryComponent(afterSuccessfulLogin)}'; - static String REGISTER_THEN(String afterSuccessfulLogin) => - '${Screen.REGISTER.route}?then=${Uri.encodeQueryComponent(afterSuccessfulLogin)}'; + static const SPLASH = _Paths.SPLASH; + static const WELCOME = _Paths.WELCOME; + static const LOGIN = _Paths.LOGIN; + static const OTP = _Paths.OTP; + static const REGISTER = _Paths.REGISTER; + static const ADDRESS = _Paths.ADDRESS; + static const ROOT = _Paths.ROOT; +//profile routes + static const ACCOUNT_DETAILS = _Paths.ACCOUNT_DETAILS; + static const MANAGE_ADDRESS = _Paths.MANAGE_ADDRESS; + static const ADD_ADDRESS = _Paths.ADD_ADDRESS; + static const PAST_QUERIES = _Paths.PAST_QUERIES; + //Cart Routes + static const CART = _Paths.CART; + + //Order + static const ORDER_CONFIRMED = _Paths.ORDER_CONFIRMED; + static const ORDER_DETAILS = _Paths.ORDER_DETAILS; } -// Keeping this as Get_Cli will require it. Any addition can later be added to Screen abstract class _Paths { - static const String HOME = '/home'; - // static const DASHBOARD = '/dashboard'; - // static const PRODUCTS = '/products'; - // static const PROFILE = '/profile'; - // static const SETTINGS = '/settings'; - // static const PRODUCT_DETAILS = '/:productId'; - // static const CART_DETAILS = '/:productId'; - // static const LOGIN = '/login'; - // static const CART = '/cart'; - // static const CHECKOUT = '/checkout'; - // static const REGISTER = '/register'; - // static const CATEGORIES = '/categories'; - // static const TASKS = '/tasks'; - // static const TASK_DETAILS = '/:taskId'; - // static const USERS = '/users'; - // static const USER_PROFILE = '/:uId'; - // static const MY_PRODUCTS = '/my-products'; + static const String SPLASH = '/'; + static const String WELCOME = '/welcome'; + static const String LOGIN = '/login'; + static const String OTP = '/otp'; + static const String REGISTER = '/register'; + static const String ADDRESS = '/address'; + static const String ROOT = '/root'; + //Profile Routes + static const ACCOUNT_DETAILS = '/account_details'; + static const MANAGE_ADDRESS = '/manage_address'; + static const ADD_ADDRESS = '/add_address'; + static const PAST_QUERIES = '/past_queries'; + //Cart Routes + static const CART = '/cart'; + + //Order + static const ORDER_CONFIRMED = '/order_confirmed'; + static const ORDER_DETAILS = '/order_details'; } diff --git a/lib/app/widgets/cart/cart_bottom_button.dart b/lib/app/widgets/cart/cart_bottom_button.dart new file mode 100644 index 00000000..beb8996c --- /dev/null +++ b/lib/app/widgets/cart/cart_bottom_button.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class CartBottomButton extends StatelessWidget { + final VoidCallback onPressed; + final String label; + final String input; + final bool disabled; + + const CartBottomButton({ + super.key, + required this.onPressed, + required this.label, + required this.input, + this.disabled = false, + }); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: AppTheme.paddingDefault, + decoration: BoxDecoration( + color: AppTheme.colorWhite, + boxShadow: AppTheme.cardBoxShadow, + ), + child: Material( + color: disabled ? AppTheme.backgroundColor : AppTheme.colorMain, + borderRadius: AppTheme.borderRadius, + child: InkWell( + onTap: disabled ? null : onPressed, + child: Container( + padding: AppTheme.paddingTiny, + height: AppTheme.spacingExtraLarge, + decoration: BoxDecoration( + borderRadius: AppTheme.borderRadius, + ), + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: AppTheme.fontStyleHeadingDefault.copyWith( + color: AppTheme.colorWhite, + ), + ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4.0), + color: AppTheme.colorMain, + ), + padding: AppTheme.paddingTiny, + child: Text( + input, + style: AppTheme.fontStyleHeadingDefault + .copyWith(color: AppTheme.colorWhite), + ), + ), + ], + ), + ), + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/app/widgets/cart/order_detail_card.dart b/lib/app/widgets/cart/order_detail_card.dart new file mode 100644 index 00000000..c4bbe2b8 --- /dev/null +++ b/lib/app/widgets/cart/order_detail_card.dart @@ -0,0 +1,86 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/models/order_model.dart'; +import 'package:get_flutter_fire/models/product_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class OrderDetailProductCard extends StatelessWidget { + final ProductModel product; + final ProductData productData; + + const OrderDetailProductCard({ + super.key, + required this.productData, + required this.product, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: AppTheme.spacingSmall), + child: Container( + decoration: AppTheme.cardDecoration, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.only( + topLeft: AppTheme.borderRadius.topLeft, + bottomLeft: AppTheme.borderRadius.bottomLeft, + ), + child: CachedNetworkImage( + imageUrl: product.images[0], + height: 152, + width: 146, + fit: BoxFit.cover, + ), + ), + Flexible( + child: Padding( + padding: const EdgeInsets.all(AppTheme.spacingExtraSmall), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + style: AppTheme.fontStyleDefaultBold, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + Text( + product.description, + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const Spacing(size: AppTheme.spacingSmall), + Text( + productData.price.toString(), + style: AppTheme.fontStyleDefaultBold, + ), + const Spacing(size: AppTheme.spacingExtraSmall), + RichText( + text: TextSpan( + children: [ + const TextSpan( + text: "Quantity: ", + style: AppTheme.fontStyleDefault, + ), + TextSpan( + text: productData.quantity.toString(), + style: AppTheme.fontStyleDefaultBold), + ], + )), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/app/widgets/cart/order_summary_widget.dart b/lib/app/widgets/cart/order_summary_widget.dart new file mode 100644 index 00000000..fb0d7660 --- /dev/null +++ b/lib/app/widgets/cart/order_summary_widget.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class OrderSummaryWidget extends StatelessWidget { + final int subTotalPrice; + final int priceDiscount; + final int couponDiscount; + final int totalPrice; + final String? couponCode; + + const OrderSummaryWidget({ + super.key, + required this.subTotalPrice, + required this.priceDiscount, + required this.couponDiscount, + required this.totalPrice, + this.couponCode, + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: AppTheme.cardDecoration, + padding: AppTheme.paddingSmall, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Subtotal", + style: AppTheme.fontStyleDefault, + ), + Text( + subTotalPrice.toString(), + style: AppTheme.fontStyleDefaultBold, + ), + ], + ), + const SizedBox(height: AppTheme.spacingTiny), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Discount", + style: AppTheme.fontStyleDefault, + ), + Text( + priceDiscount.toString(), + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorMain, + ), + ), + ], + ), + if (couponCode != null && couponCode!.isNotEmpty) ...[ + const SizedBox(height: AppTheme.spacingTiny), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + RichText( + text: TextSpan( + text: "Coupon (", + style: AppTheme.fontStyleDefault, + children: [ + TextSpan( + text: couponCode, + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorBlue, + ), + ), + const TextSpan( + text: ")", + style: AppTheme.fontStyleDefault, + ), + ], + ), + ), + Text( + "-$couponDiscount", + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorMain, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ], + const SizedBox(height: AppTheme.spacingTiny), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + "Total", + style: AppTheme.fontStyleDefault, + ), + Text( + totalPrice.toString(), + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorBlue, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/app/widgets/cart/payment_selection_card.dart b/lib/app/widgets/cart/payment_selection_card.dart new file mode 100644 index 00000000..00f675fd --- /dev/null +++ b/lib/app/widgets/cart/payment_selection_card.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class PaymentMethodSelectionCard extends StatelessWidget { + final String paymentMethod; + final String selectedPaymentMethod; + final String? label; + final ValueChanged onChanged; + final String displayText; + + const PaymentMethodSelectionCard({ + super.key, + required this.paymentMethod, + required this.selectedPaymentMethod, + required this.onChanged, + required this.displayText, + this.label, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + onChanged(paymentMethod); + }, + child: Container( + decoration: AppTheme.cardDecoration.copyWith( + border: Border.all( + color: AppTheme.borderColor, + width: 2.0, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Radio( + value: paymentMethod, + groupValue: selectedPaymentMethod, + onChanged: (value) { + if (value != null) { + onChanged(value); + } + }, + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return AppTheme.colorMain; + } + return Colors.grey; + }), + ), + Text( + displayText, + style: AppTheme.fontStyleDefaultBold, + ), + const Spacing( + size: AppTheme.spacingTiny, + isHorizontal: true, + ), + Text( + label ?? '', + style: AppTheme.fontStyleDefaultBold + .copyWith(color: AppTheme.colorYellow), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/app/widgets/cart/select_address_card.dart b/lib/app/widgets/cart/select_address_card.dart new file mode 100644 index 00000000..d4600807 --- /dev/null +++ b/lib/app/widgets/cart/select_address_card.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/models/address_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class SelectAddressCard extends StatelessWidget { + final AddressModel address; + final bool isDefault; + final Function(AddressModel) onSelect; + final String selectedAddressID; + + const SelectAddressCard({ + super.key, + required this.address, + required this.onSelect, + required this.selectedAddressID, + required this.isDefault, + }); + + @override + Widget build(BuildContext context) { + bool isSelected = selectedAddressID == address.id; + + return GestureDetector( + onTap: () => onSelect(address), + child: Container( + decoration: AppTheme.cardDecoration.copyWith( + border: Border.all( + color: isSelected ? AppTheme.colorMain : AppTheme.borderColor, + width: 2.0, + ), + ), + padding: const EdgeInsets.all(AppTheme.spacingExtraSmall), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(right: AppTheme.spacingTiny), + child: Radio( + activeColor: AppTheme.colorMain, + value: address.id, + groupValue: selectedAddressID, + onChanged: (value) => onSelect(address), + ), + ), + const Spacing(size: AppTheme.spacingExtraLarge), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + address.name, + style: AppTheme.fontStyleDefault.copyWith( + fontWeight: FontWeight.bold, + overflow: TextOverflow.ellipsis, + ), + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + "${address.line1}\n${address.line2}", + maxLines: 2, + style: AppTheme.fontStyleDefault, + overflow: TextOverflow.ellipsis, + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + "${address.district}, ${address.city}", + maxLines: 1, + style: AppTheme.fontStyleDefault, + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + "PhoneNumber ${address.phoneNumber}", + maxLines: 2, + style: AppTheme.fontStyleDefault, + overflow: TextOverflow.ellipsis, + ), + if (isDefault) + const Text( + 'Default Address', + style: TextStyle( + color: AppTheme.colorMain, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/app/widgets/common/custom_bottom_button.dart b/lib/app/widgets/common/custom_bottom_button.dart new file mode 100644 index 00000000..b959cc87 --- /dev/null +++ b/lib/app/widgets/common/custom_bottom_button.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class CustomBottomButton extends StatelessWidget { + final VoidCallback onPressed; + final String label; + final bool isDisabled; + + const CustomBottomButton({ + super.key, + required this.onPressed, + required this.label, + this.isDisabled = false, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(4.0), + child: Material( + color: isDisabled ? AppTheme.colorDisabled : AppTheme.colorMain, + borderRadius: BorderRadius.circular(12), + child: InkWell( + onTap: isDisabled ? null : onPressed, + borderRadius: BorderRadius.circular(12), + child: Container( + height: 40, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + ), + child: Text( + label, + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorWhite, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/app/widgets/common/custom_button.dart b/lib/app/widgets/common/custom_button.dart new file mode 100644 index 00000000..c1b16e64 --- /dev/null +++ b/lib/app/widgets/common/custom_button.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class CustomButton extends StatelessWidget { + final VoidCallback onPressed; + final String text; + final bool isDisabled; + + const CustomButton( + {super.key, + required this.onPressed, + required this.text, + this.isDisabled = false}); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: isDisabled ? null : onPressed, + child: Container( + width: double.infinity, + height: 50, + decoration: BoxDecoration( + color: isDisabled ? AppTheme.colorDisabled : AppTheme.colorMain, + borderRadius: BorderRadius.circular(12), + ), + alignment: Alignment.center, + child: Text( + text, + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorWhite, + ), + ), + ), + ); + } +} diff --git a/lib/app/widgets/common/custom_dropdown.dart b/lib/app/widgets/common/custom_dropdown.dart new file mode 100644 index 00000000..6808269d --- /dev/null +++ b/lib/app/widgets/common/custom_dropdown.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class CustomDropdown extends StatefulWidget { + final String hintText; + final List> items; + final ValueChanged onChanged; + final T? value; + + const CustomDropdown({ + super.key, + required this.hintText, + required this.items, + required this.onChanged, + this.value, + }); + + @override + CustomDropdownState createState() => CustomDropdownState(); +} + +class CustomDropdownState extends State> { + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: AppTheme.borderColor, width: 1.0), + ), + ), + child: DropdownButtonFormField( + value: widget.value, + items: widget.items, + isExpanded: true, + iconEnabledColor: AppTheme.borderColor, + iconDisabledColor: AppTheme.borderColor, + onChanged: widget.onChanged, + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + hint: Text( + widget.hintText, + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + ), + decoration: InputDecoration( + isDense: true, + contentPadding: const EdgeInsets.only( + top: AppTheme.spacingExtraSmall, + left: AppTheme.spacingTiny, + bottom: AppTheme.spacingExtraSmall), + hintStyle: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: InputBorder.none, + ), + ), + ); + } +} diff --git a/lib/app/widgets/common/custom_phone_textfield.dart b/lib/app/widgets/common/custom_phone_textfield.dart new file mode 100644 index 00000000..64d69511 --- /dev/null +++ b/lib/app/widgets/common/custom_phone_textfield.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class PhoneTextField extends StatefulWidget { + final TextEditingController? controller; + final String hintText; + final bool readOnly; + const PhoneTextField( + {super.key, + this.controller, + required this.hintText, + required this.readOnly}); + + @override + State createState() => _PhoneTextFieldState(); +} + +class _PhoneTextFieldState extends State { + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: widget.readOnly ? AppTheme.backgroundColor : Colors.transparent, + border: null, + ), + child: widget.readOnly ? _buildReadOnlyField() : _buildEditableField(), + ); + } + + Widget _buildReadOnlyField() { + final phoneNumber = widget.controller?.text ?? ''; + + return Padding( + padding: const EdgeInsets.all(AppTheme.fontSizeSmall), + child: Row( + children: [ + const Spacing(size: AppTheme.spacingSmall, isHorizontal: true), + Text( + "+91", + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.colorBlack, + ), + ), + const Spacing(size: AppTheme.spacingSmall, isHorizontal: true), + Container( + width: 1, + height: 20, + color: AppTheme.greyTextColor, + ), + const Spacing(size: AppTheme.spacingSmall, isHorizontal: true), + Expanded( + child: Text( + phoneNumber.substring(3), + style: AppTheme.fontStyleDefault, + ), + ), + const SizedBox(width: 8), + // const CustomIcon(asset: iconCheckCircle), + ], + ), + ); + } + + Widget _buildEditableField() { + return TextField( + textAlignVertical: TextAlignVertical.center, + style: AppTheme.fontStyleDefault, + controller: widget.controller, + keyboardType: TextInputType.phone, + cursorColor: AppTheme.colorMain, + maxLength: 10, + readOnly: widget.readOnly, + onChanged: (value) { + if (value.length == 10) { + FocusScope.of(context).unfocus(); + } + }, + decoration: InputDecoration( + counterText: '', + isDense: true, + hintText: widget.hintText, + hintStyle: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldUnderlineBorder, + focusedBorder: AppTheme.textfieldUnderlineBorder, + enabledBorder: AppTheme.textfieldUnderlineBorder, + prefixIcon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Spacing(size: AppTheme.spacingSmall, isHorizontal: true), + Text( + "+91", + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.colorBlack, + ), + ), + const Spacing(size: AppTheme.spacingSmall, isHorizontal: true), + Container( + width: 1, + height: 20, + color: AppTheme.greyTextColor, + ), + const Spacing(size: AppTheme.spacingSmall, isHorizontal: true), + ], + ), + ), + ); + } +} diff --git a/lib/app/widgets/common/custom_textfield.dart b/lib/app/widgets/common/custom_textfield.dart new file mode 100644 index 00000000..fc709838 --- /dev/null +++ b/lib/app/widgets/common/custom_textfield.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class CustomTextField extends StatelessWidget { + final String labelText; + final bool obscureText; + final TextInputType keyboardType; + final TextEditingController controller; + + const CustomTextField({ + super.key, + required this.labelText, + this.obscureText = false, + this.keyboardType = TextInputType.text, + required this.controller, + }); + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: controller, + obscureText: obscureText, + keyboardType: keyboardType, + decoration: InputDecoration( + labelText: labelText, + labelStyle: + AppTheme.fontStyleDefault.copyWith(color: AppTheme.greyTextColor), + contentPadding: const EdgeInsets.only(left: 8.0), + enabledBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: AppTheme.borderColor), + ), + focusedBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: AppTheme.colorMain), + ), + ), + style: AppTheme.fontStyleDefault, + ); + } +} diff --git a/lib/app/widgets/common/overlay_loader.dart b/lib/app/widgets/common/overlay_loader.dart new file mode 100644 index 00000000..0664737a --- /dev/null +++ b/lib/app/widgets/common/overlay_loader.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/theme/assets.dart'; + +class LoadingWidget extends StatefulWidget { + const LoadingWidget({super.key}); + + @override + State createState() => _LoadingWidgetState(); +} + +class _LoadingWidgetState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset(rhombusLoader, height: 150), + const Text( + "Setting You UP...", + style: AppTheme.fontStyleDefault, + ), + ], + ), + ), + ); + } +} diff --git a/lib/app/widgets/common/secondary_button.dart b/lib/app/widgets/common/secondary_button.dart new file mode 100644 index 00000000..186a8bdb --- /dev/null +++ b/lib/app/widgets/common/secondary_button.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class SecondaryButton extends StatelessWidget { + final String label; + final VoidCallback onPressed; + final bool disabled; + + const SecondaryButton({ + super.key, + required this.label, + required this.onPressed, + this.disabled = false, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: disabled ? null : onPressed, + child: Text( + label, + textAlign: TextAlign.center, + style: AppTheme.fontStyleHeadingDefault.copyWith( + fontWeight: FontWeight.bold, + color: disabled ? AppTheme.greyTextColor : AppTheme.colorMain, + decoration: TextDecoration.underline, + decorationColor: + disabled ? AppTheme.greyTextColor : AppTheme.colorMain), + ), + ); + } +} diff --git a/lib/app/widgets/common/show_loader.dart b/lib/app/widgets/common/show_loader.dart new file mode 100644 index 00000000..042dcc5d --- /dev/null +++ b/lib/app/widgets/common/show_loader.dart @@ -0,0 +1,9 @@ +import 'package:flutter_easyloading/flutter_easyloading.dart'; + +showLoader() async { + await EasyLoading.show(); +} + +dismissLoader() async { + await EasyLoading.dismiss(); +} diff --git a/lib/app/widgets/common/show_toast.dart b/lib/app/widgets/common/show_toast.dart new file mode 100644 index 00000000..13a18c5d --- /dev/null +++ b/lib/app/widgets/common/show_toast.dart @@ -0,0 +1,14 @@ +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +void showToast(message, {bool isError = false, bool isShort = false}) { +//TODO: implement showToast correctly + Fluttertoast.showToast( + msg: message, + toastLength: isShort ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG, + gravity: ToastGravity.TOP, + timeInSecForIosWeb: isShort ? 1 : 5, + backgroundColor: isError ? AppTheme.colorMain : AppTheme.borderColor, + textColor: isError ? AppTheme.colorBlack : AppTheme.colorWhite, + fontSize: AppTheme.fontSizeDefault); +} diff --git a/lib/app/widgets/common/spacing.dart b/lib/app/widgets/common/spacing.dart new file mode 100644 index 00000000..bcf4d28d --- /dev/null +++ b/lib/app/widgets/common/spacing.dart @@ -0,0 +1,20 @@ +import 'package:flutter/widgets.dart'; + +class Spacing extends StatelessWidget { + final double size; + final bool isHorizontal; + + const Spacing({ + super.key, + required this.size, + this.isHorizontal = false, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: isHorizontal ? null : size, + width: isHorizontal ? size : null, + ); + } +} diff --git a/lib/app/widgets/orders/order_status_indicator.dart b/lib/app/widgets/orders/order_status_indicator.dart new file mode 100644 index 00000000..bb8583a6 --- /dev/null +++ b/lib/app/widgets/orders/order_status_indicator.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class OrderStatusIndicator extends StatefulWidget { + final OrderStatus currentStatus; + + const OrderStatusIndicator({super.key, required this.currentStatus}); + + @override + OrderStatusIndicatorState createState() => OrderStatusIndicatorState(); +} + +class OrderStatusIndicatorState extends State + with TickerProviderStateMixin { + late AnimationController _controller; + late List> _animations; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 3200), + vsync: this, + )..forward(); + + _animations = List.generate(4, (index) { + final start = index * 0.25; + final end = start + 0.25; + return Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation( + parent: _controller, + curve: Interval(start, end, curve: Curves.easeIn), + ), + ); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + Color _getBarColor(OrderStatus status, int index) { + if (index <= status.index) { + return AppTheme.colorMain; + } + return AppTheme.greyTextColor; + } + + @override + Widget build(BuildContext context) { + return Row( + children: List.generate(4, (index) { + return Expanded( + child: Padding( + padding: const EdgeInsets.only(right: AppTheme.spacingTiny), + child: AnimatedBuilder( + animation: _animations[index], + builder: (context, child) { + double fillPercentage = (index <= widget.currentStatus.index) + ? _animations[index].value + : 0.0; + return Stack( + children: [ + Container( + height: 4, + decoration: BoxDecoration( + borderRadius: AppTheme.borderRadius, + color: AppTheme.backgroundColor, + ), + ), + FractionallySizedBox( + widthFactor: fillPercentage, + child: Container( + height: 4, + decoration: BoxDecoration( + borderRadius: AppTheme.borderRadius, + color: _getBarColor(widget.currentStatus, index), + ), + ), + ), + ], + ); + }, + ), + ), + ); + }), + ); + } +} diff --git a/lib/app/widgets/orders/primary_button.dart b/lib/app/widgets/orders/primary_button.dart new file mode 100644 index 00000000..d1e6c491 --- /dev/null +++ b/lib/app/widgets/orders/primary_button.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class PrimaryButton extends StatelessWidget { + final String label; + final VoidCallback onPressed; + final bool disabled; + final bool smallBorder; + + final bool isOutlined; + const PrimaryButton({ + super.key, + required this.label, + required this.onPressed, + this.disabled = false, + this.smallBorder = false, + this.isOutlined = false, + }); + + @override + Widget build(BuildContext context) { + return Material( + borderRadius: AppTheme.borderRadius, + color: disabled + ? AppTheme.backgroundColor + : isOutlined + ? Colors.transparent + : AppTheme.colorMain, + child: InkWell( + onTap: disabled ? null : onPressed, + child: Container( + height: AppTheme.spacingExtraLarge, + decoration: BoxDecoration( + borderRadius: AppTheme.borderRadius, + border: isOutlined + ? Border.all( + color: AppTheme.colorMain, + ) + : null, + ), + child: Center( + child: Text( + label, + textAlign: TextAlign.center, + style: AppTheme.fontStyleHeadingDefault.copyWith( + color: disabled + ? AppTheme.greyTextColor + : isOutlined + ? AppTheme.colorBlue + : AppTheme.colorWhite, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/app/widgets/product/add_to_cart_button.dart b/lib/app/widgets/product/add_to_cart_button.dart new file mode 100644 index 00000000..d851249e --- /dev/null +++ b/lib/app/widgets/product/add_to_cart_button.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/cart_controller.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/models/cart_model.dart'; +import 'package:get_flutter_fire/models/product_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class AddToCartButton extends StatelessWidget { + final ProductModel product; + final bool disableButton; + + const AddToCartButton( + {super.key, required this.product, this.disableButton = false}); + + @override + Widget build(BuildContext context) { + final CartController cartController = Get.find(); + + return Obx(() { + final isInCart = cartController.isProductInCart(product.id); + return Material( + borderRadius: AppTheme.borderRadius, + color: disableButton + ? AppTheme.backgroundColor + : isInCart + ? Colors.white + : AppTheme.colorMain, + child: InkWell( + onTap: () { + if (!disableButton) { + if (isInCart) { + Get.toNamed(Routes.CART); + } else { + cartController.addItem(CartItem( + id: product.id, + quantity: 1, + price: product.unitPrice, + )); + } + } + }, + child: Container( + height: AppTheme.spacingExtraLarge, + decoration: BoxDecoration( + borderRadius: AppTheme.borderRadius, + border: isInCart ? Border.all(color: AppTheme.colorMain) : null, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + isInCart ? Icons.shopping_cart : Icons.add, + color: isInCart ? AppTheme.colorMain : AppTheme.colorWhite, + ), + const Spacing(size: AppTheme.spacingTiny, isHorizontal: true), + Text( + isInCart ? 'Go to Cart' : 'Add to Cart', + textAlign: TextAlign.center, + style: AppTheme.fontStyleHeadingDefault.copyWith( + color: isInCart ? AppTheme.colorMain : AppTheme.colorWhite, + ), + ), + ], + ), + ), + ), + ); + }); + } +} diff --git a/lib/app/widgets/profile/address_container.dart b/lib/app/widgets/profile/address_container.dart new file mode 100644 index 00000000..cacd7cfa --- /dev/null +++ b/lib/app/widgets/profile/address_container.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/app/widgets/common/spacing.dart'; +import 'package:get_flutter_fire/models/address_model.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class AddressContainer extends StatelessWidget { + final AddressModel address; + + final bool isDefault; + final Function(String) onDelete; + final Function(String) onSetAsDefault; + + const AddressContainer({ + super.key, + required this.address, + required this.isDefault, + required this.onDelete, + required this.onSetAsDefault, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: AppTheme.paddingTiny, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + address.name, + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorBlack, + ), + ), + if (isDefault) + Text( + ' (Default)', + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.colorMain, + ), + ), + ], + ), + Text( + '${address.line1}, ${address.line2}', + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + '${address.district}, ${address.city}', + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + ), + Text.rich( + TextSpan( + text: 'Phone Number: ', + style: AppTheme.fontStyleMedium, + children: [ + TextSpan( + text: address.phoneNumber, + style: AppTheme.fontStyleDefaultBold, + ), + ], + ), + ), + const Spacing(size: AppTheme.spacingSmall), + if (!isDefault) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + InkWell( + onTap: () { + onSetAsDefault(address.id); + }, + child: Text( + 'Set as default address', + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.colorMain, + decoration: TextDecoration.underline, + ), + ), + ), + InkWell( + onTap: () { + onDelete(address.id); + }, + child: const Text('Delete', style: AppTheme.fontStyleDefault), + ), + ], + ), + const Spacing(size: AppTheme.spacingTiny), + ], + ), + ); + } +} diff --git a/lib/app/widgets/profile/profile_list_widget.dart b/lib/app/widgets/profile/profile_list_widget.dart new file mode 100644 index 00000000..df10d938 --- /dev/null +++ b/lib/app/widgets/profile/profile_list_widget.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; + +class ProfileListItem extends StatelessWidget { + final IconData? icon; + final String? imagePath; + final String text; + final Function() onTap; + + const ProfileListItem({ + super.key, + this.icon, + this.imagePath, + required this.text, + required this.onTap, + }) : assert(icon != null || imagePath != null, + 'Either icon or image must be provided'); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + ListTile( + leading: icon != null + ? Icon(icon, color: AppTheme.greyTextColor) + : imagePath != null + ? Image.asset(imagePath!, width: 24, height: 24) + : null, + title: Text(text, + style: AppTheme.fontStyleDefaultBold.copyWith( + color: AppTheme.greyTextColor, + )), + trailing: const Icon(Icons.arrow_forward_ios, + color: AppTheme.colorBlack, size: AppTheme.fontSizeSmall), + onTap: onTap, + ), + const Divider(color: AppTheme.borderColor, height: 0.5), + ], + ); + } +} diff --git a/lib/constants.dart b/lib/constants.dart index dd5d17ce..66f5e70e 100644 --- a/lib/constants.dart +++ b/lib/constants.dart @@ -1,21 +1,16 @@ -import 'package:flutter/material.dart'; -// import 'package:get/get_utils/src/platform/platform.dart'; - -const kPrimaryColor = Color(0xFF6F35A5); -const kPrimaryLightColor = Color(0xFFF1E6FF); - -const double defaultPadding = 16.0; - -const useEmulator = false; - -const useRecaptcha = false; - -const sendMailFromClient = - true; // set this true if in server using custom claim status - -const emulatorHost = - "127.0.0.1"; // GetPlatform.isAndroid ? "10.0.2.2" : "127.0.0.1"; //This is not required due to automaticHostMapping - -const baseUrl = useEmulator ? "http://127.0.0.1" : "your domain"; - -const bundleID = "com.example"; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:cloud_functions/cloud_functions.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:firebase_storage/firebase_storage.dart'; + +// Firebase Instance +final db = FirebaseFirestore.instance; +final auth = FirebaseAuth.instance; +final functions = FirebaseFunctions.instance; +final firebaseStorage = FirebaseStorage.instance; + +// Database References +final usersRef = db.collection('users'); + +// Development +const development = true; diff --git a/lib/enums/enum_parser.dart b/lib/enums/enum_parser.dart new file mode 100644 index 00000000..749e9fc1 --- /dev/null +++ b/lib/enums/enum_parser.dart @@ -0,0 +1,59 @@ +import 'package:get_flutter_fire/enums/enums.dart'; + +UserType parseUserType(String userType) { + switch (userType) { + case 'buyer': + return UserType.buyer; + case 'seller': + return UserType.seller; + default: + return UserType.buyer; + } +} + +QueryType parseQueryType(String queryType) { + switch (queryType) { + case 'product': + return QueryType.product; + case 'delivery': + return QueryType.delivery; + case 'general': + return QueryType.general; + case 'payment': + return QueryType.payment; + case 'app': + return QueryType.app; + default: + return QueryType.general; + } +} + +EnquiryStatus parseQueryStatus(String queryStatus) { + switch (queryStatus) { + case 'pending': + return EnquiryStatus.pending; + case 'in-progress': + return EnquiryStatus.inProgress; + case 'completed': + return EnquiryStatus.completed; + default: + return EnquiryStatus.pending; + } +} + +OrderStatus parseOrderStatus(String orderStatus) { + switch (orderStatus) { + case 'placed': + return OrderStatus.placed; + case 'processed': + return OrderStatus.processed; + case 'shipped': + return OrderStatus.shipped; + case 'delivered': + return OrderStatus.delivered; + case 'cancelled': + return OrderStatus.cancelled; + default: + return OrderStatus.placed; + } +} diff --git a/lib/enums/enums.dart b/lib/enums/enums.dart new file mode 100644 index 00000000..8d7397ea --- /dev/null +++ b/lib/enums/enums.dart @@ -0,0 +1,17 @@ +enum UserType { buyer, seller, admin } + +enum AccessLevel { + public, + guest, + notAuthed, + authenticated, + roleBased, + masked, + secret +} + +enum EnquiryStatus { pending, inProgress, completed } + +enum QueryType { product, delivery, general, payment, app } + +enum OrderStatus { placed, processed, shipped, delivered, cancelled } diff --git a/lib/main.dart b/lib/main.dart index 30c258f2..baef6bd1 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,13 +1,44 @@ -// ignore_for_file: inference_failure_on_instance_creation - import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:get/get.dart'; +import 'package:get_flutter_fire/app/modules/auth/controllers/auth_controller.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/cart_controller.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/order_controller.dart'; +import 'package:get_flutter_fire/app/modules/cart/controllers/product_controller.dart'; +import 'package:get_flutter_fire/app/modules/profile/controllers/address_controller.dart'; +import 'package:get_flutter_fire/app/routes/app_pages.dart'; +import 'package:get_flutter_fire/constants.dart'; +import 'package:get_flutter_fire/services/auth_service.dart'; +import 'package:get_flutter_fire/theme/app_theme.dart'; +import 'package:get_flutter_fire/theme/assets.dart'; import 'package:get_storage/get_storage.dart'; - -import 'app/routes/app_pages.dart'; import 'firebase_options.dart'; -import 'services/auth_service.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; + +late AndroidNotificationChannel channel; +late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; + +Future setupFlutterNotifications() async { + channel = const AndroidNotificationChannel( + 'high_importance_channel', + 'High Importance Notifications', + description: 'This channel is used for important notifications.', + importance: Importance.high, + ); + + flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + await flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.createNotificationChannel(channel); + await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions( + alert: true, + badge: true, + sound: true, + ); +} void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -15,30 +46,50 @@ void main() async { await Firebase.initializeApp( options: DefaultFirebaseOptions.currentPlatform, ); + await setupFlutterNotifications(); + if (development) { + functions.useFunctionsEmulator('localhost', 5001); + auth.useAuthEmulator('localhost', 9099); + db.useFirestoreEmulator('localhost', 8080); + } + + _configLoading(); runApp( - GetMaterialApp.router( - debugShowCheckedModeBanner: - false, //the debug banner will automatically disappear in prod build - title: 'Application', + GetMaterialApp( + debugShowCheckedModeBanner: false, + title: 'Sheru', + getPages: AppPages.routes, + initialRoute: AppPages.INITIAL, initialBinding: BindingsBuilder( () { + // TODO: Handle binding later Get.put(AuthService()); + Get.put(AuthController()); + Get.put(AddressController()); + Get.put(CartController()); + Get.put(OrderController()); + Get.put(ProductController()); }, ), - getPages: AppPages.routes, - // routeInformationParser: GetInformationParser( - // // initialRoute: Routes.HOME, - // ), - // routerDelegate: GetDelegate( - // backButtonPopMode: PopMode.History, - // preventDuplicateHandlingMode: - // PreventDuplicateHandlingMode.ReorderRoutes, - // ), theme: ThemeData( - highlightColor: Colors.black.withOpacity(0.5), - bottomSheetTheme: - const BottomSheetThemeData(surfaceTintColor: Colors.blue)), + colorScheme: ColorScheme.fromSeed(seedColor: AppTheme.colorWhite), + useMaterial3: true, + ), + builder: EasyLoading.init(), ), ); } + +_configLoading() { + EasyLoading.instance + ..loadingStyle = EasyLoadingStyle.custom + ..indicatorWidget = Image.asset(rhombusLoader, height: 150) + ..maskType = EasyLoadingMaskType.custom + ..maskColor = AppTheme.colorBlack.withOpacity(0.7) + ..backgroundColor = Colors.transparent + ..textColor = AppTheme.colorWhite + ..indicatorColor = AppTheme.colorWhite + ..userInteractions = false + ..boxShadow = []; +} diff --git a/lib/models/address_model.dart b/lib/models/address_model.dart new file mode 100644 index 00000000..83e536a7 --- /dev/null +++ b/lib/models/address_model.dart @@ -0,0 +1,92 @@ +import 'dart:convert'; + +class AddressModel { + final String name; + final String phoneNumber; + final String line1; + final String line2; + final String city; + final String district; + final double latitude; + final double longitude; + final String id; + final String userID; + AddressModel({ + required this.name, + required this.phoneNumber, + required this.line1, + required this.line2, + required this.city, + required this.district, + required this.latitude, + required this.longitude, + required this.id, + required this.userID, + }); + + AddressModel copyWith({ + String? name, + String? phoneNumber, + String? line1, + String? line2, + String? city, + String? district, + double? latitude, + double? longitude, + String? id, + String? userID, + }) { + return AddressModel( + name: name ?? this.name, + phoneNumber: phoneNumber ?? this.phoneNumber, + line1: line1 ?? this.line1, + line2: line2 ?? this.line2, + city: city ?? this.city, + district: district ?? this.district, + latitude: latitude ?? this.latitude, + longitude: longitude ?? this.longitude, + id: id ?? this.id, + userID: userID ?? this.userID, + ); + } + + Map toMap() { + return { + 'name': name, + 'phoneNumber': phoneNumber, + 'line1': line1, + 'line2': line2, + 'city': city, + 'district': district, + 'latitude': latitude, + 'longitude': longitude, + 'id': id, + 'userID': userID, + }; + } + + factory AddressModel.fromMap(Map map) { + return AddressModel( + name: map['name'] as String, + phoneNumber: map['phoneNumber'] as String, + line1: map['line1'] as String, + line2: map['line2'] as String, + city: map['city'] as String, + district: map['district'] as String, + latitude: map['latitude'] as double, + longitude: map['longitude'] as double, + id: map['id'] as String, + userID: map['userID'] as String, + ); + } + + String toJson() => json.encode(toMap()); + + factory AddressModel.fromJson(String source) => + AddressModel.fromMap(json.decode(source) as Map); + + @override + String toString() { + return '$line1, $line2, $district, $city'; + } +} diff --git a/lib/models/banner_model.dart b/lib/models/banner_model.dart new file mode 100644 index 00000000..45ca3e6d --- /dev/null +++ b/lib/models/banner_model.dart @@ -0,0 +1,56 @@ +import 'dart:convert'; + +class BannerModel { + final String id; + final String imageUrl; + final String productID; + final bool isActive; + BannerModel({ + required this.id, + required this.imageUrl, + required this.productID, + required this.isActive, + }); + + BannerModel copyWith({ + String? id, + String? imageUrl, + String? productID, + bool? isActive, + }) { + return BannerModel( + id: id ?? this.id, + imageUrl: imageUrl ?? this.imageUrl, + productID: productID ?? this.productID, + isActive: isActive ?? this.isActive, + ); + } + + Map toMap() { + return { + 'id': id, + 'imageUrl': imageUrl, + 'productID': productID, + 'isActive': isActive, + }; + } + + factory BannerModel.fromMap(Map map) { + return BannerModel( + id: map['id'] as String, + imageUrl: map['imageUrl'] as String, + productID: map['productID'] as String, + isActive: map['isActive'] as bool, + ); + } + + String toJson() => json.encode(toMap()); + + factory BannerModel.fromJson(String source) => + BannerModel.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'BannerModel(id: $id, imageUrl: $imageUrl, productID: $productID, isActive: $isActive)'; + } +} diff --git a/lib/models/cart_model.dart b/lib/models/cart_model.dart new file mode 100644 index 00000000..67c2ae58 --- /dev/null +++ b/lib/models/cart_model.dart @@ -0,0 +1,97 @@ +import 'dart:convert'; + +class CartModel { + List items; + final String id; + + CartModel({ + required this.items, + required this.id, + }); + + int get itemCount => items.length; + + CartModel copyWith({ + List? items, + String? id, + }) { + return CartModel( + items: items ?? this.items, + id: id ?? this.id, + ); + } + + Map toMap() { + return { + 'items': items.map((x) => x.toMap()).toList(), + 'id': id, + }; + } + + factory CartModel.fromMap(Map map) { + return CartModel( + items: List.from( + map['items']?.map((x) => CartItem.fromMap(x as Map)) ?? + [], + ), + id: map['id'] as String, + ); + } + + String toJson() => json.encode(toMap()); + + factory CartModel.fromJson(String source) => + CartModel.fromMap(json.decode(source) as Map); + + @override + String toString() => 'Cart(items: $items, id: $id)'; +} + +class CartItem { + String id; + final int price; + int quantity; + CartItem({ + required this.id, + required this.price, + required this.quantity, + }); + + CartItem copyWith({ + String? id, + int? price, + int? quantity, + }) { + return CartItem( + id: id ?? this.id, + price: price ?? this.price, + quantity: quantity ?? this.quantity, + ); + } + + Map toMap() { + return { + 'id': id, + 'price': price, + 'quantity': quantity, + }; + } + + factory CartItem.fromMap(Map map) { + return CartItem( + id: map['id'] as String, + price: map['price'] as int, + quantity: map['quantity'] as int, + ); + } + + String toJson() => json.encode(toMap()); + + factory CartItem.fromJson(String source) => + CartItem.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'CartItem(id: $id, price: $price, quantity: $quantity)'; + } +} diff --git a/lib/models/category_model.dart b/lib/models/category_model.dart new file mode 100644 index 00000000..2afb304c --- /dev/null +++ b/lib/models/category_model.dart @@ -0,0 +1,56 @@ +import 'dart:convert'; + +class CategoryModel { + final String id; + final String name; + final String imageUrl; + final String? bannerImageUrl; + + CategoryModel({ + required this.id, + required this.name, + required this.imageUrl, + this.bannerImageUrl, // Make this field nullable + }); + + CategoryModel copyWith({ + String? id, + String? name, + String? imageUrl, + String? bannerImageUrl, + }) { + return CategoryModel( + id: id ?? this.id, + name: name ?? this.name, + imageUrl: imageUrl ?? this.imageUrl, + bannerImageUrl: bannerImageUrl ?? this.bannerImageUrl, + ); + } + + Map toMap() { + return { + 'id': id, + 'name': name, + 'imageUrl': imageUrl, + 'bannerImageUrl': bannerImageUrl, + }; + } + + factory CategoryModel.fromMap(Map map) { + return CategoryModel( + id: map['id'] as String, + name: map['name'] as String, + imageUrl: map['imageUrl'] as String, + bannerImageUrl: map['bannerImageUrl'] as String?, // Safely handle null + ); + } + + String toJson() => json.encode(toMap()); + + factory CategoryModel.fromJson(String source) => + CategoryModel.fromMap(json.decode(source) as Map); + + @override + String toString() => + 'CategoryModel(id: $id, name: $name, imageUrl: $imageUrl, bannerImageUrl: $bannerImageUrl)'; +} diff --git a/lib/models/contact_enquiry_model.dart b/lib/models/contact_enquiry_model.dart new file mode 100644 index 00000000..0f07b9a0 --- /dev/null +++ b/lib/models/contact_enquiry_model.dart @@ -0,0 +1,77 @@ +import 'dart:convert'; + +import 'package:get_flutter_fire/enums/enum_parser.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; + +class ContactEnquiryModel { + final String id; + final String message; + final String userID; + final EnquiryStatus status; + final DateTime timestamp; + final String reference; + final QueryType queryType; + ContactEnquiryModel({ + required this.id, + required this.message, + required this.userID, + required this.status, + required this.timestamp, + required this.reference, + required this.queryType, + }); + + ContactEnquiryModel copyWith({ + String? id, + String? message, + String? userID, + EnquiryStatus? status, + DateTime? timestamp, + String? reference, + QueryType? queryType, + }) { + return ContactEnquiryModel( + id: id ?? this.id, + message: message ?? this.message, + userID: userID ?? this.userID, + status: status ?? this.status, + timestamp: timestamp ?? this.timestamp, + reference: reference ?? this.reference, + queryType: queryType ?? this.queryType, + ); + } + + Map toMap() { + return { + 'id': id, + 'message': message, + 'userID': userID, + 'status': status.name, + 'timestamp': timestamp, + 'reference': reference, + 'queryType': queryType.name, + }; + } + + factory ContactEnquiryModel.fromMap(Map map) { + return ContactEnquiryModel( + id: map['id'] as String, + message: map['message'] as String, + userID: map['userID'] as String, + status: parseQueryStatus(map['status'] as String), + timestamp: map['timestamp'].toDate(), + reference: map['reference'] as String, + queryType: parseQueryType(map['queryType'] as String), + ); + } + + String toJson() => json.encode(toMap()); + + factory ContactEnquiryModel.fromJson(String source) => + ContactEnquiryModel.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'Contact(id: $id, message: $message, userID: $userID, status: $status, timestamp: $timestamp, reference: $reference, queryType: $queryType)'; + } +} diff --git a/lib/models/offer_model.dart b/lib/models/offer_model.dart new file mode 100644 index 00000000..afccbdaf --- /dev/null +++ b/lib/models/offer_model.dart @@ -0,0 +1,75 @@ +import 'dart:convert'; + +class OfferModel { + final String id; + final String title; + final String description; + final String imageUrl; + final String city; + final DateTime validFrom; + final DateTime validTo; + + OfferModel({ + required this.id, + required this.title, + required this.description, + required this.imageUrl, + required this.city, + required this.validFrom, + required this.validTo, + }); + + OfferModel copyWith({ + String? id, + String? title, + String? description, + String? imageUrl, + String? city, + DateTime? validFrom, + DateTime? validTo, + }) { + return OfferModel( + id: id ?? this.id, + title: title ?? this.title, + description: description ?? this.description, + imageUrl: imageUrl ?? this.imageUrl, + city: city ?? this.city, + validFrom: validFrom ?? this.validFrom, + validTo: validTo ?? this.validTo, + ); + } + + Map toMap() { + return { + 'id': id, + 'title': title, + 'description': description, + 'imageUrl': imageUrl, + 'city': city, + 'validFrom': validFrom.toIso8601String(), + 'validTo': validTo.toIso8601String(), + }; + } + + factory OfferModel.fromMap(Map map) { + return OfferModel( + id: map['id'] as String, + title: map['title'] as String, + description: map['description'] as String, + imageUrl: map['imageUrl'] as String, + city: map['city'] as String, + validFrom: DateTime.parse(map['validFrom'] as String), + validTo: DateTime.parse(map['validTo'] as String), + ); + } + + String toJson() => json.encode(toMap()); + + factory OfferModel.fromJson(String source) => + OfferModel.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'OfferModel(id: $id, title: $title, description: $description, imageUrl: $imageUrl, city: $city, validFrom: $validFrom, validTo: $validTo)'; + } +} diff --git a/lib/models/order_model.dart b/lib/models/order_model.dart new file mode 100644 index 00000000..e97f7d80 --- /dev/null +++ b/lib/models/order_model.dart @@ -0,0 +1,206 @@ +import 'dart:convert'; +import 'package:get_flutter_fire/enums/enum_parser.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; +import 'package:get_flutter_fire/models/address_model.dart'; + +class OrderModel { + final String id; + final List products; + final double totalWeight; + final int totalPrice; + final String userID; + final AddressModel address; + final List statusUpdates; + final OrderStatus currentStatus; + final String paymentMethod; + final DateTime createdAt; + final String couponID; + final int couponDiscount; + OrderModel({ + required this.id, + required this.products, + required this.totalWeight, + required this.totalPrice, + required this.userID, + required this.address, + required this.statusUpdates, + required this.currentStatus, + required this.paymentMethod, + required this.createdAt, + required this.couponID, + required this.couponDiscount, + }); + + OrderModel copyWith({ + String? id, + List? products, + double? totalWeight, + int? totalPrice, + String? userID, + AddressModel? address, + List? statusUpdates, + OrderStatus? currentStatus, + String? paymentMethod, + DateTime? createdAt, + String? couponID, + int? couponDiscount, + }) { + return OrderModel( + id: id ?? this.id, + products: products ?? this.products, + totalWeight: totalWeight ?? this.totalWeight, + totalPrice: totalPrice ?? this.totalPrice, + userID: userID ?? this.userID, + address: address ?? this.address, + statusUpdates: statusUpdates ?? this.statusUpdates, + currentStatus: currentStatus ?? this.currentStatus, + paymentMethod: paymentMethod ?? this.paymentMethod, + createdAt: createdAt ?? this.createdAt, + couponID: couponID ?? this.couponID, + couponDiscount: couponDiscount ?? this.couponDiscount, + ); + } + + Map toMap() { + return { + 'id': id, + 'products': products.map((x) => x.toMap()).toList(), + 'totalWeight': totalWeight, + 'totalPrice': totalPrice, + 'userID': userID, + 'address': address.toMap(), + 'statusUpdates': statusUpdates.map((x) => x.toMap()).toList(), + 'currentStatus': currentStatus.name, + 'paymentMethod': paymentMethod, + 'createdAt': createdAt, + 'couponID': couponID, + 'couponDiscount': couponDiscount, + }; + } + + factory OrderModel.fromMap(Map map) { + return OrderModel( + id: map['id'] as String, + products: List.from( + (map['products'] as List).map( + (x) => ProductData.fromMap(x as Map), + ), + ), + totalWeight: map['totalWeight'] as double, + totalPrice: map['totalPrice'] as int, + userID: map['userID'] as String, + address: AddressModel.fromMap(map['address'] as Map), + statusUpdates: List.from( + (map['statusUpdates'] as List).map( + (x) => OrderStatusUpdate.fromMap(x as Map), + ), + ), + currentStatus: parseOrderStatus(map['currentStatus'] as String), + paymentMethod: map['paymentMethod'] as String, + createdAt: map['createdAt'].toDate(), + couponID: map['couponID'] as String, + couponDiscount: map['couponDiscount'] as int, + ); + } + + String toJson() => json.encode(toMap()); + + factory OrderModel.fromJson(String source) => + OrderModel.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'Order(id: $id, products: $products, totalWeight: $totalWeight, totalPrice: $totalPrice, userID: $userID, address: $address, statusUpdates: $statusUpdates, currentStatus: $currentStatus, paymentMethod: $paymentMethod, createdAt: $createdAt, couponID: $couponID, couponDiscount: $couponDiscount)'; + } +} + +class ProductData { + final String id; + final int price; + final int quantity; + ProductData({ + required this.id, + required this.price, + required this.quantity, + }); + + ProductData copyWith({ + String? id, + int? price, + int? priceRrp, + int? quantity, + }) { + return ProductData( + id: id ?? this.id, + price: price ?? this.price, + quantity: quantity ?? this.quantity, + ); + } + + Map toMap() { + return { + 'id': id, + 'price': price, + 'quantity': quantity, + }; + } + + factory ProductData.fromMap(Map map) { + return ProductData( + id: map['id'] as String, + price: map['price'] as int, + quantity: map['quantity'] as int, + ); + } + + String toJson() => json.encode(toMap()); + + factory ProductData.fromJson(String source) => + ProductData.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'ProductData(id: $id, price: $price, quantity: $quantity)'; + } +} + +class OrderStatusUpdate { + final OrderStatus status; + final DateTime timestamp; + OrderStatusUpdate({ + required this.status, + required this.timestamp, + }); + + OrderStatusUpdate copyWith({ + OrderStatus? status, + DateTime? timestamp, + }) { + return OrderStatusUpdate( + status: status ?? this.status, + timestamp: timestamp ?? this.timestamp, + ); + } + + Map toMap() { + return { + 'status': status.name, + 'timestamp': timestamp, + }; + } + + factory OrderStatusUpdate.fromMap(Map map) { + return OrderStatusUpdate( + status: parseOrderStatus(map['status'] as String), + timestamp: map['timestamp'].toDate(), + ); + } + + String toJson() => json.encode(toMap()); + + factory OrderStatusUpdate.fromJson(String source) => + OrderStatusUpdate.fromMap(json.decode(source) as Map); + + @override + String toString() => 'Status(status: $status, timestamp: $timestamp)'; +} diff --git a/lib/models/product_model.dart b/lib/models/product_model.dart new file mode 100644 index 00000000..686d809c --- /dev/null +++ b/lib/models/product_model.dart @@ -0,0 +1,120 @@ +import 'dart:convert'; + +class ProductModel { + final String id; + final String categoryID; + final List images; + final String name; + final String description; + final int unitWeight; + final int unitPrice; + final int remainingQuantity; + final bool isActive; + final DateTime createdAt; + final DateTime updatedAt; + final String sellerId; + final bool isSheruSpecial; + final bool isApproved; + ProductModel({ + required this.id, + required this.categoryID, + required this.images, + required this.name, + required this.description, + required this.unitWeight, + required this.unitPrice, + required this.remainingQuantity, + required this.isActive, + required this.createdAt, + required this.updatedAt, + required this.sellerId, + required this.isSheruSpecial, + required this.isApproved, + }); + + ProductModel copyWith({ + String? id, + String? categoryID, + List? images, + String? name, + String? description, + int? unitWeight, + int? unitPrice, + int? wholesalePrice, + int? specialPrice, + int? wholesaleQuantity, + int? specialQuantity, + int? remainingQuantity, + bool? isActive, + DateTime? createdAt, + DateTime? updatedAt, + String? sellerId, + bool? isSheruSpecial, + bool? isApproved, + }) { + return ProductModel( + id: id ?? this.id, + categoryID: categoryID ?? this.categoryID, + images: images ?? this.images, + name: name ?? this.name, + description: description ?? this.description, + unitWeight: unitWeight ?? this.unitWeight, + unitPrice: unitPrice ?? this.unitPrice, + remainingQuantity: remainingQuantity ?? this.remainingQuantity, + isActive: isActive ?? this.isActive, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + sellerId: sellerId ?? this.sellerId, + isSheruSpecial: isSheruSpecial ?? this.isSheruSpecial, + isApproved: isApproved ?? this.isApproved, + ); + } + + Map toMap() { + return { + 'id': id, + 'categoryID': categoryID, + 'images': images, + 'name': name, + 'description': description, + 'unitWeight': unitWeight, + 'unitPrice': unitPrice, + 'remainingQuantity': remainingQuantity, + 'isActive': isActive, + 'createdAt': createdAt, + 'updatedAt': updatedAt, + 'sellerId': sellerId, + 'isSheruSpecial': isSheruSpecial, + 'isApproved': isApproved, + }; + } + + factory ProductModel.fromMap(Map map) { + return ProductModel( + id: map['id'] as String, + categoryID: map['categoryID'] as String, + images: List.from((map['images'])), + name: map['name'] as String, + description: map['description'] as String, + unitWeight: map['unitWeight'] as int, + unitPrice: map['unitPrice'] as int, + remainingQuantity: map['remainingQuantity'] as int, + isActive: map['isActive'] as bool, + createdAt: map['createdAt'].toDate(), + updatedAt: map['updatedAt'].toDate(), + sellerId: map['sellerId'] as String, + isSheruSpecial: map['isSheruSpecial'] as bool, + isApproved: map['isApproved'] as bool, + ); + } + + String toJson() => json.encode(toMap()); + + factory ProductModel.fromJson(String source) => + ProductModel.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'ProductModel(id: $id, categoryID: $categoryID, images: $images, name: $name, description: $description, unitWeight: $unitWeight, unitPrice: $unitPrice, remainingQuantity: $remainingQuantity, isActive: $isActive. createdAt: $createdAt, updatedAt: $updatedAt, sellerId: $sellerId, isSheruSpecial: $isSheruSpecial, isApproved: $isApproved)'; + } +} diff --git a/lib/models/seller_model.dart b/lib/models/seller_model.dart new file mode 100644 index 00000000..8ada5235 --- /dev/null +++ b/lib/models/seller_model.dart @@ -0,0 +1,111 @@ +import 'dart:convert'; + +import 'package:get_flutter_fire/enums/enum_parser.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; +import 'package:get_flutter_fire/models/product_model.dart'; +import 'package:get_flutter_fire/models/user_model.dart'; + +//TODO : add more fields like location/city available and so on +class SellerModel extends UserModel { + final String sellerId; + final List products; + + SellerModel({ + required super.id, + required super.name, + required super.phoneNumber, + super.email, + required super.isBusiness, + super.businessName, + super.businessType, + super.gstNumber, + super.panNumber, + required super.userType, + required super.defaultAddressID, + required super.createdAt, + required super.lastSeenAt, + required this.sellerId, + required this.products, + }); + + @override + SellerModel copyWith({ + String? sellerId, + List? products, + String? id, + String? name, + String? phoneNumber, + String? email, + bool? isBusiness, + String? businessName, + String? businessType, + String? gstNumber, + String? panNumber, + UserType? userType, + String? defaultAddressID, + DateTime? createdAt, + DateTime? lastSeenAt, + }) { + return SellerModel( + sellerId: sellerId ?? this.sellerId, + products: products ?? this.products, + id: id ?? this.id, + name: name ?? this.name, + phoneNumber: phoneNumber ?? this.phoneNumber, + email: email ?? this.email, + isBusiness: isBusiness ?? this.isBusiness, + businessName: businessName ?? this.businessName, + businessType: businessType ?? this.businessType, + gstNumber: gstNumber ?? this.gstNumber, + panNumber: panNumber ?? this.panNumber, + userType: userType ?? this.userType, + defaultAddressID: defaultAddressID ?? this.defaultAddressID, + createdAt: createdAt ?? this.createdAt, + lastSeenAt: lastSeenAt ?? this.lastSeenAt, + ); + } + + @override + Map toMap() { + final map = super.toMap(); + map.addAll({ + 'sellerId': sellerId, + 'products': products.map((product) => product.toMap()).toList(), + }); + return map; + } + + factory SellerModel.fromMap(Map map) { + return SellerModel( + id: map['id'] as String, + name: map['name'] as String, + phoneNumber: map['phoneNumber'] as String, + email: map['email'] != null ? map['email'] as String : null, + isBusiness: map['isBusiness'] as bool, + businessName: + map['businessName'] != null ? map['businessName'] as String : null, + businessType: + map['businessType'] != null ? map['businessType'] as String : null, + gstNumber: map['gstNumber'] != null ? map['gstNumber'] as String : null, + panNumber: map['panNumber'] != null ? map['panNumber'] as String : null, + userType: parseUserType(map['userType'] as String), + defaultAddressID: map['defaultAddressID'] as String, + createdAt: DateTime.parse(map['createdAt'] as String), + lastSeenAt: DateTime.parse(map['lastSeenAt'] as String), + sellerId: map['sellerId'] as String, + products: List.from( + map['products']?.map((x) => ProductModel.fromMap(x))), + ); + } + + @override + String toJson() => json.encode(toMap()); + + factory SellerModel.fromJson(String source) => + SellerModel.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'SellerModel(sellerId: $sellerId, products: $products, ${super.toString()})'; + } +} diff --git a/lib/models/user_model.dart b/lib/models/user_model.dart new file mode 100644 index 00000000..dc95ce10 --- /dev/null +++ b/lib/models/user_model.dart @@ -0,0 +1,117 @@ +import 'dart:convert'; +import 'package:get_flutter_fire/enums/enum_parser.dart'; +import 'package:get_flutter_fire/enums/enums.dart'; + +class UserModel { + final String id; + final String name; + final String phoneNumber; + final String? email; + final bool isBusiness; + final String? businessName; + final String? businessType; + final String? gstNumber; + final String? panNumber; + final UserType userType; + final String defaultAddressID; + final DateTime createdAt; + final DateTime lastSeenAt; + + UserModel({ + required this.id, + required this.name, + required this.phoneNumber, + this.email, + required this.isBusiness, + this.businessName, + this.businessType, + this.gstNumber, + this.panNumber, + required this.userType, + required this.defaultAddressID, + required this.createdAt, + required this.lastSeenAt, + }); + + get role => null; + + UserModel copyWith({ + String? id, + String? name, + String? phoneNumber, + String? email, + bool? isBusiness, + String? businessName, + String? businessType, + String? gstNumber, + String? panNumber, + UserType? userType, + String? defaultAddressID, + DateTime? createdAt, + DateTime? lastSeenAt, + }) { + return UserModel( + id: id ?? this.id, + name: name ?? this.name, + phoneNumber: phoneNumber ?? this.phoneNumber, + email: email ?? this.email, + isBusiness: isBusiness ?? this.isBusiness, + businessName: businessName ?? this.businessName, + businessType: businessType ?? this.businessType, + gstNumber: gstNumber ?? this.gstNumber, + panNumber: panNumber ?? this.panNumber, + userType: userType ?? this.userType, + defaultAddressID: defaultAddressID ?? this.defaultAddressID, + createdAt: createdAt ?? this.createdAt, + lastSeenAt: lastSeenAt ?? this.lastSeenAt, + ); + } + + Map toMap() { + return { + 'id': id, + 'name': name, + 'phoneNumber': phoneNumber, + 'email': email, + 'isBusiness': isBusiness, + 'businessName': businessName, + 'businessType': businessType, + 'gstNumber': gstNumber, + 'panNumber': panNumber, + 'userType': userType.name, + 'defaultAddressID': defaultAddressID, + 'createdAt': createdAt.toIso8601String(), + 'lastSeenAt': lastSeenAt.toIso8601String(), + }; + } + + factory UserModel.fromMap(Map map) { + return UserModel( + id: map['id'] as String, + name: map['name'] as String, + phoneNumber: map['phoneNumber'] as String, + email: map['email'] != null ? map['email'] as String : null, + isBusiness: map['isBusiness'] as bool, + businessName: + map['businessName'] != null ? map['businessName'] as String : null, + businessType: + map['businessType'] != null ? map['businessType'] as String : null, + gstNumber: map['gstNumber'] != null ? map['gstNumber'] as String : null, + panNumber: map['panNumber'] != null ? map['panNumber'] as String : null, + userType: parseUserType(map['userType'] as String), + defaultAddressID: map['defaultAddressID'] as String, + createdAt: DateTime.parse(map['createdAt'] as String), + lastSeenAt: DateTime.parse(map['lastSeenAt'] as String), + ); + } + + String toJson() => json.encode(toMap()); + + factory UserModel.fromJson(String source) => + UserModel.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'UserModel(id: $id, name: $name, phoneNumber: $phoneNumber, email: $email, isBusiness: $isBusiness, businessName: $businessName, businessType: $businessType, gstNumber: $gstNumber, panNumber: $panNumber, userType: $userType, defaultAddressID: $defaultAddressID, createdAt: $createdAt, lastSeenAt: $lastSeenAt)'; + } +} diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart index 8bf72aaa..326ec88c 100644 --- a/lib/services/auth_service.dart +++ b/lib/services/auth_service.dart @@ -1,201 +1,64 @@ -// ignore_for_file: avoid_print - +import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; -import 'package:firebase_ui_auth/firebase_ui_auth.dart' as fbui; -import 'package:firebase_ui_localizations/firebase_ui_localizations.dart'; -import 'package:flutter/material.dart'; import 'package:get/get.dart'; - -import '../models/screens.dart'; -import '../constants.dart'; -import '../models/role.dart'; +import 'package:get_flutter_fire/app/routes/app_routes.dart'; +import 'package:get_flutter_fire/models/address_model.dart'; +import 'package:get_flutter_fire/constants.dart'; class AuthService extends GetxService { static AuthService get to => Get.find(); - final FirebaseAuth _auth = FirebaseAuth.instance; - late Rxn credential = Rxn(); - final Rxn _firebaseUser = Rxn(); - final Rx _userRole = Rx(Role.buyer); - final Rx robot = RxBool(useRecaptcha); - final RxBool registered = false.obs; - - User? get user => _firebaseUser.value; - Role get maxRole => _userRole.value; - - @override - onInit() { - super.onInit(); - if (useEmulator) _auth.useAuthEmulator(emulatorHost, 9099); - _firebaseUser.bindStream(_auth.authStateChanges()); - _auth.authStateChanges().listen((User? user) { - if (user != null) { - user.getIdTokenResult().then((token) { - _userRole.value = Role.fromString(token.claims?["role"]); - }); - } - }); - } - - bool get isEmailVerified => - user != null && (user!.email == null || user!.emailVerified); - - bool get isLoggedInValue => user != null; - - bool get isAdmin => user != null && _userRole.value == Role.admin; + String _verificationId = ''; + String _phoneNumber = ''; + String get phoneNumber => _phoneNumber; - bool hasRole(Role role) => user != null && _userRole.value == role; + String get userID => auth.currentUser!.uid; - bool get isAnon => user != null && user!.isAnonymous; - - String? get userName => (user != null && !user!.isAnonymous) - ? (user!.displayName ?? user!.email) - : 'Guest'; - - void login() { - // this is not needed as we are using Firebase UI for the login part - } - - void sendVerificationMail({EmailAuthCredential? emailAuth}) async { - if (sendMailFromClient) { - if (_auth.currentUser != null) { - await _auth.currentUser?.sendEmailVerification(); - } else if (emailAuth != null) { - // Approach 1: sending email auth link requires deep linking which is - // a TODO as the current Flutter methods are deprecated - // sendSingInLink(emailAuth); - - // Approach 2: This is a hack. - // We are using createUser to send the verification link from the server side by adding suffix .verify in the email - // if the user already exists and the password is also same and sign in occurs via custom token on server side - try { - await _auth.createUserWithEmailAndPassword( - email: "${credential.value!.email}.verify", - password: credential.value!.password!); - } on FirebaseAuthException catch (e) { - int i = e.message!.indexOf("message") + 10; - int j = e.message!.indexOf('"', i); - Get.snackbar( - e.message!.substring(i, j), - 'Please verify your email by clicking the link on the Email sent', - ); - } - } + // Save the address to Firestore + Future saveAddress(AddressModel address) async { + try { + await FirebaseFirestore.instance + .collection('addresses') + .doc(address.id) + .set(address.toMap()); + } catch (e) { + Get.snackbar('Error', 'Failed to save address: $e'); } } - void sendSingInLink(EmailAuthCredential emailAuth) { - var acs = ActionCodeSettings( - // URL you want to redirect back to. The domain (www.example.com) for this - // URL must be whitelisted in the Firebase Console. - url: - '$baseUrl:5001/flutterfast-92c25/us-central1/handleEmailLinkVerification', - // // This must be true if deep linking. - // // If deeplinking. See [https://firebase.google.com/docs/dynamic-links/flutter/receive] - handleCodeInApp: true, - // iOSBundleId: '$bundleID.ios', - // androidPackageName: '$bundleID.android', - // // installIfNotAvailable - // androidInstallApp: true, - // // minimumVersion - // androidMinimumVersion: '12' + Future verifyPhoneNumber(String phoneNumber) async { + _phoneNumber = phoneNumber; + await auth.verifyPhoneNumber( + phoneNumber: phoneNumber, + verificationCompleted: (PhoneAuthCredential credential) {}, + verificationFailed: (FirebaseAuthException e) { + if (e.code == 'invalid-phone-number') { + Get.snackbar('Error', 'Invalid Phone Number. Please try again.'); + } + }, + codeSent: (String verificationId, int? resendToken) { + _verificationId = verificationId; + Get.toNamed(Routes.OTP, arguments: {'phoneNumber': _phoneNumber}); + }, + codeAutoRetrievalTimeout: (String verificationId) {}, ); - _auth - .sendSignInLinkToEmail(email: emailAuth.email, actionCodeSettings: acs) - .catchError( - (onError) => print('Error sending email verification $onError')) - .then((value) => print('Successfully sent email verification')); } - void register() { - registered.value = true; - // logout(); // Uncomment if we need to enforce relogin - final thenTo = - Get.rootDelegate.currentConfiguration!.currentPage!.parameters?['then']; - Get.rootDelegate - .offAndToNamed(thenTo ?? Screen.PROFILE.route); //Profile has the forms - } - - void logout() { - _auth.signOut(); - if (isAnon) _auth.currentUser?.delete(); - _firebaseUser.value = null; - } - - Future guest() async { - return await Get.defaultDialog( - middleText: 'Sign in as Guest', - barrierDismissible: true, - onConfirm: loginAsGuest, - onCancel: () => Get.back(result: false), - textConfirm: 'Yes, will SignUp later', - textCancel: 'No, will SignIn now'); - } - - void loginAsGuest() async { + Future verifyOTP(String otp) async { try { - await FirebaseAuth.instance.signInAnonymously(); - Get.back(result: true); - Get.snackbar( - 'Alert!', - 'Signed in with temporary account.', - ); - } on FirebaseAuthException catch (e) { - switch (e.code) { - case "operation-not-allowed": - print("Anonymous auth hasn't been enabled for this project."); - break; - default: - print("Unknown error."); + PhoneAuthCredential credential = PhoneAuthProvider.credential( + verificationId: _verificationId, smsCode: otp); + UserCredential userCredential = + await auth.signInWithCredential(credential); + if (userCredential.user != null) { + return true; } - Get.back(result: false); - } - } - - void errorMessage(BuildContext context, fbui.AuthFailed state, - Function(bool, EmailAuthCredential?) callback) { - fbui.ErrorText.localizeError = - (BuildContext context, FirebaseAuthException e) { - final defaultLabels = FirebaseUILocalizations.labelsOf(context); - - // for verification error, also set a boolean flag to trigger button visibility to resend verification mail - String? verification; - if (e.code == "internal-error" && - e.message!.contains('"status":"UNAUTHENTICATED"')) { - // Note that (possibly in Emulator only) the e.email is always coming as null - // String? email = e.email ?? parseEmail(e.message!); - callback(true, credential.value); - verification = - "Please verify email id by clicking the link on the email sent"; - } else { - callback(false, credential.value); + return false; + } on FirebaseAuthException catch (error) { + if (error.code == 'invalid-verification-code') { + Get.snackbar('Error', 'Invalid OTP. Please try again.'); } - - return switch (e.code) { - 'invalid-credential' => 'User ID or Password incorrect', - 'user-not-found' => 'Please create an account first.', - 'credential-already-in-use' => 'This email is already in use.', - _ => fbui.localizedErrorText(e.code, defaultLabels) ?? - verification ?? - 'Oh no! Something went wrong.', - }; - }; - } -} - -class MyCredential extends AuthCredential { - final EmailAuthCredential cred; - MyCredential(this.cred) - : super(providerId: "custom", signInMethod: cred.signInMethod); - - @override - Map asMap() { - return cred.asMap(); + return false; + } } } - -parseEmail(String message) { - int i = message.indexOf('"message":') + 13; - int j = message.indexOf('"', i); - return message.substring(i, j - 1); -} diff --git a/lib/services/get_storage_service.dart b/lib/services/get_storage_service.dart new file mode 100644 index 00000000..7871d9bf --- /dev/null +++ b/lib/services/get_storage_service.dart @@ -0,0 +1,40 @@ +import 'package:get_storage/get_storage.dart'; +import 'package:get_flutter_fire/models/user_model.dart'; + +class GetStorageService { + final GetStorage _storage = GetStorage(); + + // Save user data to storage + void saveUserData(UserModel user) { + _storage.write('user', user.toMap()); + } + + // Load user data from storage + UserModel? getUserData() { + final storedUser = _storage.read>('user'); + if (storedUser != null) { + return UserModel.fromMap(storedUser); + } + return null; + } + + // Clear user data from storage + void clearUserData() { + _storage.remove('user'); + } + + // Save user role + void saveUserRole(String role) { + _storage.write('role', role); + } + + // Load user role + String? getUserRole() { + return _storage.read('role'); + } + + // Clear user role + void clearUserRole() { + _storage.remove('role'); + } +} diff --git a/lib/services/notification_service.dart b/lib/services/notification_service.dart new file mode 100644 index 00000000..438fec2c --- /dev/null +++ b/lib/services/notification_service.dart @@ -0,0 +1,13 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:get_flutter_fire/constants.dart'; + +class NotificationService { + Future storeToken(String userID) async { + String? token = await FirebaseMessaging.instance.getToken(); + if (token == null) return; + await usersRef.doc(userID).update({ + 'fcmTokens': FieldValue.arrayUnion([token]) + }); + } +} diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart new file mode 100644 index 00000000..7bea9b18 --- /dev/null +++ b/lib/theme/app_theme.dart @@ -0,0 +1,167 @@ +import 'package:flutter/material.dart'; + +class AppTheme { + // Colors + static const Color colorBlack = Color(0xff040520); + static const Color colorWhite = Colors.white; + static const Color colorBlue = Color(0xff1B499C); + static const Color colorDarkBlue = Color(0xff113370); + static const Color colorYellow = Color(0xffFDD30B); + static const Color greyTextColor = Color(0xffA7A7AF); + static const Color backgroundColor = Color(0xffF2F2F3); + static const Color borderColor = Color(0xffDCDCE0); + static const Color colorDisabled = Color(0xFFCFCFCF); + static const Color colorMain = Color(0xff1B499C); + + //Font Size + static const double fontSizeSmall = 12.0; + static const double fontSizeDefault = 14.0; + static const double fontSizeMedium = 16.0; + static const double fontSizeLarge = 24.0; + + //Spacing + static const double spacingTiny = 8.0; + static const double spacingExtraSmall = 12.0; + static const double spacingSmall = 16.0; + static const double spacingSemiMedium = 20.0; + static const double spacingDefault = 24.0; + static const double spacingMedium = 32.0; + static const double spacingLarge = 40.0; + static const double spacingExtraLarge = 48.0; + + // Font Styles + + static const TextStyle fontStyleSmall = TextStyle( + fontFamily: 'Mulish', + fontSize: fontSizeSmall, + fontWeight: FontWeight.w400, + color: colorBlack, + ); + + static const TextStyle fontStyleDefault = TextStyle( + fontFamily: 'Mulish', + fontSize: fontSizeDefault, + fontWeight: FontWeight.w400, + color: colorBlack, + ); + + static const TextStyle fontStyleDefaultBold = TextStyle( + fontFamily: 'Mulish', + fontSize: fontSizeDefault, + fontWeight: FontWeight.bold, + color: colorBlack, + ); + + static const TextStyle fontStyleMedium = TextStyle( + fontFamily: 'Mulish', + fontSize: fontSizeMedium, + fontWeight: FontWeight.w400, + color: colorBlack, + ); + + static const TextStyle fontStyleHeadingDefault = TextStyle( + fontFamily: 'Nexa-Bold', + fontSize: fontSizeDefault, + fontWeight: FontWeight.w400, + color: colorBlack, + ); + + static const TextStyle fontStyleHeadingMedium = TextStyle( + fontFamily: 'Nexa-Bold', + fontSize: fontSizeMedium, + fontWeight: FontWeight.w400, + color: colorBlack, + ); + + static const TextStyle fontStyleLarge = TextStyle( + fontFamily: 'Nexa-Bold', + fontSize: fontSizeLarge, + fontWeight: FontWeight.w400, + color: colorMain, + ); + + // Padding + static const EdgeInsets paddingDefault = EdgeInsets.all(spacingDefault); + static const EdgeInsets paddingSmall = EdgeInsets.all(spacingSmall); + static const EdgeInsets paddingTiny = EdgeInsets.all(spacingTiny); + + // BoxShadow + static List cardBoxShadow = [ + BoxShadow( + offset: const Offset(4, 4), + blurRadius: 16, + color: AppTheme.colorBlack.withOpacity(0.05), + ) + ]; + + static List secondaryBoxShadow = [ + BoxShadow( + offset: const Offset(0, 2), + blurRadius: 8, + color: AppTheme.colorBlack.withOpacity(0.05), + ) + ]; + + static List bottomBoxShadow = [ + BoxShadow( + offset: const Offset(-2, -2), + blurRadius: 8, + color: AppTheme.colorBlack.withOpacity(0.05), + ) + ]; + + static List appBarShadow = [ + BoxShadow( + offset: const Offset(0, 2), + blurRadius: 20, + color: AppTheme.colorBlack.withOpacity(0.06), + ) + ]; + + // Border Radius + static BorderRadius borderRadiusSmall = BorderRadius.circular(4); + static BorderRadius borderRadius = BorderRadius.circular(8); + + // Shapes + static ShapeBorder rrShapeSmall = RoundedRectangleBorder( + borderRadius: borderRadiusSmall, + ); + + static ShapeBorder rrShape = RoundedRectangleBorder( + borderRadius: borderRadius, + ); + + // Borders + static Border cardBorder = Border.all(color: borderColor, width: 1); + static OutlineInputBorder textfieldBorder = OutlineInputBorder( + borderSide: const BorderSide(color: borderColor, width: 1), + borderRadius: borderRadius, + ); + + // Decorations + static BoxDecoration cardDecoration = + BoxDecoration(borderRadius: borderRadius, color: colorWhite); + + // Gradients + static const LinearGradient appBarGradient = LinearGradient( + begin: Alignment.topRight, + end: Alignment.bottomLeft, + colors: [ + Color(0xFFFCF1BF), + Color(0xFFB2C1D8), + ], + ); + static const UnderlineInputBorder textfieldUnderlineBorder = + UnderlineInputBorder( + borderSide: BorderSide(color: borderColor, width: 1), + ); + + static const LinearGradient primaryGradient = LinearGradient( + begin: Alignment.topRight, + end: Alignment.bottomLeft, + colors: [ + Color.fromARGB(159, 178, 193, 216), + Color.fromARGB(145, 252, 241, 191), + ], + ); +} diff --git a/lib/theme/assets.dart b/lib/theme/assets.dart new file mode 100644 index 00000000..5c46aea6 --- /dev/null +++ b/lib/theme/assets.dart @@ -0,0 +1,37 @@ +const assets = "assets/images/"; +const icons = "assets/icons/"; +const animation = "assets/animations/"; + +const logo = "${icons}sheru.png"; +const mainImage = "${assets}main_image.jpg"; + +//currently used loader(change later on) +const rhombusLoader = "${animation}loader.gif"; + +// icons +const iconArrowGo = "${icons}icon_arrow_go.png"; +const iconCartCheckout = "${icons}icon_cart_checkout.png"; +const iconCart = "${icons}icon_cart.png"; +const iconCategory = "${icons}icon_category.png"; +const iconCheckCircle = "${icons}icon_check_circle.png"; +const iconChevronLeft = "${icons}icon_chevron_left.png"; +const iconChevronRight = "${icons}icon_chevron_right.png"; +const iconFile = "${icons}icon_file.png"; +const iconHistory = "${icons}icon_history.png"; +const iconHome = "${icons}icon_home.png"; +const iconLocation = "${icons}icon_location.png"; +const iconMail = "${icons}icon_mail.png"; +const iconNotification = "${icons}icon_notification.png"; +const iconOrderDelivered = "${icons}icon_order_delivered.png"; +const iconOrderDispatched = "${icons}icon_order_dispatched.png"; +const iconOrderPlaced = "${icons}icon_order_placed.png"; +const iconOrderShipped = "${icons}icon_order_shipped.png"; +const iconOrder = "${icons}icon_order.png"; +const iconPayment = "${icons}icon_payment.png"; +const iconPhone = "${icons}icon_phone.png"; +const iconProfile = "${icons}icon_profile.png"; +const iconSearch = "${icons}icon_search.png"; +const iconShare = "${icons}icon_share.png"; +const iconSignout = "${icons}icon_signout.png"; +const iconSupport = "${icons}icon_support.png"; +const iconWhatsapp = "${icons}icon_whatsapp.png"; diff --git a/lib/utils/get_reference.dart b/lib/utils/get_reference.dart new file mode 100644 index 00000000..96de71d6 --- /dev/null +++ b/lib/utils/get_reference.dart @@ -0,0 +1,9 @@ +import 'dart:math'; + +String getReference() { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + Random rnd = Random(); + String result = String.fromCharCodes( + Iterable.generate(6, (_) => chars.codeUnitAt(rnd.nextInt(chars.length)))); + return result; +} diff --git a/lib/utils/get_uuid.dart b/lib/utils/get_uuid.dart new file mode 100644 index 00000000..1bbccbb2 --- /dev/null +++ b/lib/utils/get_uuid.dart @@ -0,0 +1,5 @@ +import 'package:uuid/uuid.dart'; + +String getUUID() { + return const Uuid().v4(); +} diff --git a/lib/utils/months.dart b/lib/utils/months.dart new file mode 100644 index 00000000..8497dd32 --- /dev/null +++ b/lib/utils/months.dart @@ -0,0 +1,30 @@ +String monthString(int month) { + switch (month) { + case 1: + return 'January'; + case 2: + return 'February'; + case 3: + return 'March'; + case 4: + return 'April'; + case 5: + return 'May'; + case 6: + return 'June'; + case 7: + return 'July'; + case 8: + return 'August'; + case 9: + return 'September'; + case 10: + return 'October'; + case 11: + return 'November'; + case 12: + return 'December'; + default: + return ''; + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 2909a374..9c0955d6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,35 +2,46 @@ name: get_flutter_fire version: 1.0.0+1 publish_to: none description: Boilerplate for Flutter with GetX, showing sample utilization of Firebase capabilities -environment: - sdk: '>=3.3.4 <4.0.0' +environment: + sdk: ">=3.3.4 <4.0.0" -dependencies: +dependencies: cupertino_icons: ^1.0.6 get: 4.6.6 - flutter: + flutter: sdk: flutter firebase_core: ^2.31.0 - firebase_ui_auth: ^1.14.0 - firebase_auth: ^4.19.5 + firebase_auth: ^4.20.0 google_sign_in: ^6.2.1 firebase_ui_oauth_google: ^1.3.2 google_fonts: ^6.2.1 - firebase_storage: ^11.7.5 - image_picker: ^1.1.1 + firebase_storage: ^11.7.7 + image_picker: ^1.1.2 file_picker: ^8.0.3 path: ^1.9.0 get_storage: ^2.1.1 firebase_ui_localizations: ^1.12.0 firebase_remote_config: ^4.4.7 firebase_analytics: ^10.10.7 + cloud_firestore: ^4.17.5 + pin_code_fields: ^8.0.1 + uuid: ^4.4.2 + cloud_functions: ^4.7.6 + fluttertoast: ^8.2.8 + flutter_easyloading: ^3.0.5 + carousel_slider: ^5.0.0 + cached_network_image: ^3.4.0 + flutter_staggered_animations: ^1.1.1 + flutter_staggered_grid_view: ^0.7.0 + firebase_messaging: ^14.9.4 + flutter_local_notifications: ^17.2.2 -dev_dependencies: +dev_dependencies: flutter_lints: 3.0.2 - flutter_test: + flutter_test: sdk: flutter -flutter: +flutter: fonts: - family: SocialIcons fonts: @@ -39,5 +50,7 @@ flutter: - assets/images/flutterfire_300x.png - assets/images/dash.png - assets/icons/logo.png + - assets/animations/ + - assets/images/ + - assets/icons/ uses-material-design: true - diff --git a/sharekhan_admin_panel/.firebaserc b/sharekhan_admin_panel/.firebaserc new file mode 100644 index 00000000..08bf793d --- /dev/null +++ b/sharekhan_admin_panel/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "sheru-2eb0f" + } +} diff --git a/sharekhan_admin_panel/.gitignore b/sharekhan_admin_panel/.gitignore new file mode 100644 index 00000000..29a3a501 --- /dev/null +++ b/sharekhan_admin_panel/.gitignore @@ -0,0 +1,43 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/sharekhan_admin_panel/.metadata b/sharekhan_admin_panel/.metadata new file mode 100644 index 00000000..8ca14df4 --- /dev/null +++ b/sharekhan_admin_panel/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "5dcb86f68f239346676ceb1ed1ea385bd215fba1" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + - platform: android + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + - platform: ios + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + - platform: linux + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + - platform: macos + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + - platform: web + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + - platform: windows + create_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + base_revision: 5dcb86f68f239346676ceb1ed1ea385bd215fba1 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/sharekhan_admin_panel/README.md b/sharekhan_admin_panel/README.md new file mode 100644 index 00000000..751bb626 Binary files /dev/null and b/sharekhan_admin_panel/README.md differ diff --git a/sharekhan_admin_panel/analysis_options.yaml b/sharekhan_admin_panel/analysis_options.yaml new file mode 100644 index 00000000..0d290213 --- /dev/null +++ b/sharekhan_admin_panel/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/sharekhan_admin_panel/assets/icons/icon_add.png b/sharekhan_admin_panel/assets/icons/icon_add.png new file mode 100644 index 00000000..3d0b46fd Binary files /dev/null and b/sharekhan_admin_panel/assets/icons/icon_add.png differ diff --git a/sharekhan_admin_panel/assets/icons/icon_back.png b/sharekhan_admin_panel/assets/icons/icon_back.png new file mode 100644 index 00000000..309608f5 Binary files /dev/null and b/sharekhan_admin_panel/assets/icons/icon_back.png differ diff --git a/sharekhan_admin_panel/assets/icons/icon_chevron_right.png b/sharekhan_admin_panel/assets/icons/icon_chevron_right.png new file mode 100644 index 00000000..e54751cc Binary files /dev/null and b/sharekhan_admin_panel/assets/icons/icon_chevron_right.png differ diff --git a/sharekhan_admin_panel/assets/icons/icon_export.png b/sharekhan_admin_panel/assets/icons/icon_export.png new file mode 100644 index 00000000..62a33227 Binary files /dev/null and b/sharekhan_admin_panel/assets/icons/icon_export.png differ diff --git a/sharekhan_admin_panel/assets/icons/icon_save.png b/sharekhan_admin_panel/assets/icons/icon_save.png new file mode 100644 index 00000000..49f89ebc Binary files /dev/null and b/sharekhan_admin_panel/assets/icons/icon_save.png differ diff --git a/sharekhan_admin_panel/assets/icons/icon_trial.png b/sharekhan_admin_panel/assets/icons/icon_trial.png new file mode 100644 index 00000000..4235462c Binary files /dev/null and b/sharekhan_admin_panel/assets/icons/icon_trial.png differ diff --git a/sharekhan_admin_panel/assets/images/image_splash_banner.png b/sharekhan_admin_panel/assets/images/image_splash_banner.png new file mode 100644 index 00000000..2cdf7e31 Binary files /dev/null and b/sharekhan_admin_panel/assets/images/image_splash_banner.png differ diff --git a/sharekhan_admin_panel/assets/images/logo_main.png b/sharekhan_admin_panel/assets/images/logo_main.png new file mode 100644 index 00000000..8631d74a Binary files /dev/null and b/sharekhan_admin_panel/assets/images/logo_main.png differ diff --git a/sharekhan_admin_panel/emulator-data/auth_export/accounts.json b/sharekhan_admin_panel/emulator-data/auth_export/accounts.json new file mode 100644 index 00000000..83d09c61 --- /dev/null +++ b/sharekhan_admin_panel/emulator-data/auth_export/accounts.json @@ -0,0 +1 @@ +{"kind":"identitytoolkit#DownloadAccountResponse","users":[{"localId":"MBiHTLHEnVEMZVScwqnwJoYRlwNX","createdAt":"1723826216262","lastLoginAt":"1723826216262","providerUserInfo":[{"providerId":"phone","phoneNumber":"+8955278952","rawId":"+8955278952"}],"phoneNumber":"+8955278952","validSince":"1723886177","emailVerified":false,"disabled":false},{"localId":"YibfATv35LFQXZ53xNqwvvQDvC5N","createdAt":"1723804151058","lastLoginAt":"1723874905886","providerUserInfo":[{"providerId":"phone","phoneNumber":"+918955278952","rawId":"+918955278952"}],"phoneNumber":"+918955278952","validSince":"1723886177","emailVerified":false,"disabled":false}]} \ No newline at end of file diff --git a/sharekhan_admin_panel/emulator-data/auth_export/config.json b/sharekhan_admin_panel/emulator-data/auth_export/config.json new file mode 100644 index 00000000..6f240f7e --- /dev/null +++ b/sharekhan_admin_panel/emulator-data/auth_export/config.json @@ -0,0 +1 @@ +{"signIn":{"allowDuplicateEmails":false},"emailPrivacyConfig":{"enableImprovedEmailPrivacy":false}} \ No newline at end of file diff --git a/sharekhan_admin_panel/emulator-data/firebase-export-metadata.json b/sharekhan_admin_panel/emulator-data/firebase-export-metadata.json new file mode 100644 index 00000000..e98ad264 --- /dev/null +++ b/sharekhan_admin_panel/emulator-data/firebase-export-metadata.json @@ -0,0 +1,16 @@ +{ + "version": "13.15.2", + "firestore": { + "version": "1.19.7", + "path": "firestore_export", + "metadata_file": "firestore_export/firestore_export.overall_export_metadata" + }, + "auth": { + "version": "13.15.2", + "path": "auth_export" + }, + "storage": { + "version": "13.15.2", + "path": "storage_export" + } +} \ No newline at end of file diff --git a/sharekhan_admin_panel/emulator-data/firestore_export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata b/sharekhan_admin_panel/emulator-data/firestore_export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata new file mode 100644 index 00000000..e94af60c Binary files /dev/null and b/sharekhan_admin_panel/emulator-data/firestore_export/all_namespaces/all_kinds/all_namespaces_all_kinds.export_metadata differ diff --git a/sharekhan_admin_panel/emulator-data/firestore_export/all_namespaces/all_kinds/output-0 b/sharekhan_admin_panel/emulator-data/firestore_export/all_namespaces/all_kinds/output-0 new file mode 100644 index 00000000..1abff109 Binary files /dev/null and b/sharekhan_admin_panel/emulator-data/firestore_export/all_namespaces/all_kinds/output-0 differ diff --git a/sharekhan_admin_panel/emulator-data/firestore_export/firestore_export.overall_export_metadata b/sharekhan_admin_panel/emulator-data/firestore_export/firestore_export.overall_export_metadata new file mode 100644 index 00000000..cb63cd78 Binary files /dev/null and b/sharekhan_admin_panel/emulator-data/firestore_export/firestore_export.overall_export_metadata differ diff --git a/sharekhan_admin_panel/emulator-data/storage_export/buckets.json b/sharekhan_admin_panel/emulator-data/storage_export/buckets.json new file mode 100644 index 00000000..87f9e328 --- /dev/null +++ b/sharekhan_admin_panel/emulator-data/storage_export/buckets.json @@ -0,0 +1,7 @@ +{ + "buckets": [ + { + "id": "sheru-2eb0f.appspot.com" + } + ] +} \ No newline at end of file diff --git a/sharekhan_admin_panel/firebase.json b/sharekhan_admin_panel/firebase.json new file mode 100644 index 00000000..49fb975a --- /dev/null +++ b/sharekhan_admin_panel/firebase.json @@ -0,0 +1,55 @@ +{ + "flutter": { + "platforms": { + "dart": { + "lib/firebase_options.dart": { + "projectId": "sheru-2eb0f", + "configurations": { + "web": "1:1091785317203:web:0560ec13f743516f649fa6" + } + } + } + } + }, + "firestore": { + "rules": "firestore.rules", + "indexes": "firestore.indexes.json" + }, + "functions": [ + { + "source": "functions", + "codebase": "default", + "ignore": [ + "node_modules", + ".git", + "firebase-debug.log", + "firebase-debug.*.log", + "*.local" + ], + "predeploy": [ + "npm --prefix \"$RESOURCE_DIR\" run build" + ] + } + ], + "storage": { + "rules": "storage.rules" + }, + "emulators": { + "auth": { + "port": 9099 + }, + "functions": { + "port": 5001 + }, + "firestore": { + "port": 8080 + }, + "storage": { + "port": 9199 + }, + "ui": { + "enabled": true + }, + "singleProjectMode": true + } +} diff --git a/sharekhan_admin_panel/firestore.indexes.json b/sharekhan_admin_panel/firestore.indexes.json new file mode 100644 index 00000000..415027e5 --- /dev/null +++ b/sharekhan_admin_panel/firestore.indexes.json @@ -0,0 +1,4 @@ +{ + "indexes": [], + "fieldOverrides": [] +} diff --git a/sharekhan_admin_panel/firestore.rules b/sharekhan_admin_panel/firestore.rules new file mode 100644 index 00000000..ba401f4f --- /dev/null +++ b/sharekhan_admin_panel/firestore.rules @@ -0,0 +1,9 @@ +rules_version = '2'; + +service cloud.firestore { + match /databases/{database}/documents { + match /{document=**} { + allow read, write: if true; + } + } +} diff --git a/sharekhan_admin_panel/functions/.gitignore b/sharekhan_admin_panel/functions/.gitignore new file mode 100644 index 00000000..9be0f014 --- /dev/null +++ b/sharekhan_admin_panel/functions/.gitignore @@ -0,0 +1,10 @@ +# Compiled JavaScript files +lib/**/*.js +lib/**/*.js.map + +# TypeScript v1 declaration files +typings/ + +# Node.js dependency directory +node_modules/ +*.local \ No newline at end of file diff --git a/sharekhan_admin_panel/functions/package-lock.json b/sharekhan_admin_panel/functions/package-lock.json new file mode 100644 index 00000000..bf273ea2 --- /dev/null +++ b/sharekhan_admin_panel/functions/package-lock.json @@ -0,0 +1,5929 @@ +{ + "name": "functions", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "functions", + "dependencies": { + "firebase-admin": "^12.1.0", + "firebase-functions": "^5.0.0" + }, + "devDependencies": { + "firebase-functions-test": "^3.1.0", + "typescript": "^4.9.0" + }, + "engines": { + "node": "18" + } + }, + "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, + "peer": true, + "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.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", + "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dev": true, + "peer": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "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/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "peer": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "peer": true + }, + "node_modules/@babel/generator": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", + "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.25.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", + "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "peer": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "peer": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "peer": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "peer": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "peer": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", + "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.25.2" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", + "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.2", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "peer": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "peer": true + }, + "node_modules/@babel/types": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "peer": true + }, + "node_modules/@fastify/busboy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.0.0.tgz", + "integrity": "sha512-83rnH2nCvclWaPQQKvkJ2pdOjG4TZyEVuFDnlOF6KP08lDaaceVyw/W63mDuafQT+MKHCvXIPpE5uYWeM0rT4w==" + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.2.tgz", + "integrity": "sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ==" + }, + "node_modules/@firebase/app-types": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.2.tgz", + "integrity": "sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ==" + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.3.tgz", + "integrity": "sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ==" + }, + "node_modules/@firebase/component": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.8.tgz", + "integrity": "sha512-LcNvxGLLGjBwB0dJUsBGCej2fqAepWyBubs4jt1Tiuns7QLbXHuyObZ4aMeBjZjWx4m8g1LoVI9QFpSaq/k4/g==", + "dependencies": { + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.7.tgz", + "integrity": "sha512-wjXr5AO8RPxVVg7rRCYffT7FMtBjHRfJ9KMwi19MbOf0vBf0H9YqW3WCgcnLpXI6ehiUcU3z3qgPnnU0nK6SnA==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.2", + "@firebase/auth-interop-types": "0.2.3", + "@firebase/component": "0.6.8", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.7", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.7.tgz", + "integrity": "sha512-R/3B+VVzEFN5YcHmfWns3eitA8fHLTL03io+FIoMcTYkajFnrBdS3A+g/KceN9omP7FYYYGTQWF9lvbEx6eMEg==", + "dependencies": { + "@firebase/component": "0.6.8", + "@firebase/database": "1.0.7", + "@firebase/database-types": "1.0.4", + "@firebase/logger": "0.4.2", + "@firebase/util": "1.9.7", + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.4.tgz", + "integrity": "sha512-mz9ZzbH6euFXbcBo+enuJ36I5dR5w+enJHHjy9Y5ThCdKUseqfDjW3vCp1YxE9zygFCSjJJ/z1cQ+zodvUcwPQ==", + "dependencies": { + "@firebase/app-types": "0.9.2", + "@firebase/util": "1.9.7" + } + }, + "node_modules/@firebase/logger": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.2.tgz", + "integrity": "sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@firebase/util": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.9.7.tgz", + "integrity": "sha512-fBVNH/8bRbYjqlbIhZ+lBtdAAS4WqZumx03K06/u7fJSpz1TGjEMm1ImvKD47w+xaFKIP2ori6z8BrbakRfjJA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@google-cloud/firestore": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.9.0.tgz", + "integrity": "sha512-c4ALHT3G08rV7Zwv8Z2KG63gZh66iKdhCBeDfCpIkLrjX6EAjTD/szMdj14M+FnQuClZLFfW5bAgoOjfNmLtJg==", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^4.3.3", + "protobufjs": "^7.2.6" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/paginator": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", + "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", + "optional": true, + "dependencies": { + "arrify": "^2.0.0", + "extend": "^3.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@google-cloud/promisify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", + "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.12.1.tgz", + "integrity": "sha512-Z3ZzOnF3YKLuvpkvF+TjQ6lztxcAyTILp+FjKonmVpEwPa9vFvxpZjubLR4sB6bf19i/8HL2AXRjA0YFgHFRmQ==", + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "^4.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "duplexify": "^4.1.3", + "fast-xml-parser": "^4.4.1", + "gaxios": "^6.0.2", + "google-auth-library": "^9.6.3", + "html-entities": "^2.5.2", + "mime": "^3.0.0", + "p-limit": "^3.0.1", + "retry-request": "^7.0.0", + "teeny-request": "^9.0.0", + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.11.1.tgz", + "integrity": "sha512-gyt/WayZrVPH2w/UTLansS7F9Nwld472JxxaETamrM8HNlsa+jSLNyKAZmhxI2Me4c3mQHFiS1wWHDY1g1Kthw==", + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "optional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "peer": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "peer": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "peer": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "peer": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "peer": true, + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "peer": true, + "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, + "peer": true, + "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, + "peer": true, + "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, + "peer": true + }, + "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, + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "peer": true + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "peer": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "peer": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "optional": true + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.3.tgz", + "integrity": "sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "peer": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "peer": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "peer": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz", + "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "dev": true + }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "optional": true + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "node_modules/@types/node": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.4.0.tgz", + "integrity": "sha512-49AbMDwYUz7EXxKU/r7mXOsxwFr4BYbvB7tWYxVuLdb2ibd30ijjXINSMAHiEEZk5PCRBmW1gUeisn2VMKt3cQ==", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/qs": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + }, + "node_modules/@types/request": { + "version": "2.48.12", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", + "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", + "optional": true, + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "peer": true + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "optional": true + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "peer": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "peer": true + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "optional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "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==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "optional": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "peer": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "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==", + "devOptional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "peer": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "peer": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "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==" + }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "optional": true + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "peer": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "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, + "peer": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "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.11.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/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "peer": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "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" + } + ], + "peer": true, + "dependencies": { + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "peer": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "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==", + "dev": true, + "peer": true + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "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" + } + ], + "peer": true + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", + "dev": true, + "peer": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "devOptional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "peer": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "peer": true + }, + "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==", + "devOptional": true, + "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==", + "devOptional": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "optional": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "peer": true + }, + "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==", + "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==", + "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, + "peer": true + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "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==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "peer": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "peer": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "optional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "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==", + "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==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "optional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "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==" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.10.tgz", + "integrity": "sha512-C3RDERDjrNW262GCRvpoer3a0Ksd66CtgDLxMHhzShQ8fhL4kwnpVXsJPAKg9xJjIROXUbLBrvtOzVAjALMIWA==", + "dev": true, + "peer": true + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "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==", + "devOptional": true + }, + "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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "peer": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "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==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "devOptional": true, + "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==" + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "peer": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "peer": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "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" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "optional": true + }, + "node_modules/farmhash-modern": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/farmhash-modern/-/farmhash-modern-1.1.0.tgz", + "integrity": "sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA==", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "optional": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "peer": true + }, + "node_modules/fast-xml-parser": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", + "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "optional": true, + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "peer": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "peer": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "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/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "peer": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/firebase-admin": { + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-12.3.1.tgz", + "integrity": "sha512-vEr3s3esl8nPIA9r/feDT4nzIXCfov1CyyCSpMQWp6x63Q104qke0MEGZlrHUZVROtl8FLus6niP/M9I1s4VBA==", + "dependencies": { + "@fastify/busboy": "^3.0.0", + "@firebase/database-compat": "^1.0.2", + "@firebase/database-types": "^1.0.0", + "@types/node": "^22.0.1", + "farmhash-modern": "^1.1.0", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.1.0", + "node-forge": "^1.3.1", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">=14" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^7.7.0", + "@google-cloud/storage": "^7.7.0" + } + }, + "node_modules/firebase-functions": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/firebase-functions/-/firebase-functions-5.0.1.tgz", + "integrity": "sha512-1m+crtgAR8Tl36gjpM02KCY5zduAejFmDSXvih/DB93apg39f0U/WwRgT7sitGIRqyCcIpktNUbXJv7Y9JOF4A==", + "dependencies": { + "@types/cors": "^2.8.5", + "@types/express": "4.17.3", + "cors": "^2.8.5", + "express": "^4.17.1", + "protobufjs": "^7.2.2" + }, + "bin": { + "firebase-functions": "lib/bin/firebase-functions.js" + }, + "engines": { + "node": ">=14.10.0" + }, + "peerDependencies": { + "firebase-admin": "^11.10.0 || ^12.0.0" + } + }, + "node_modules/firebase-functions-test": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/firebase-functions-test/-/firebase-functions-test-3.3.0.tgz", + "integrity": "sha512-X+OOA34MGrsTimFXTDnWT0psAqnmBkJ85bGCoLMwjgei5Prfkqh3bv5QASnXC/cmIVBSF2Qw9uW1+mF/t3kFlw==", + "dev": true, + "dependencies": { + "@types/lodash": "^4.14.104", + "lodash": "^4.17.5", + "ts-deepmerge": "^2.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "firebase-admin": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^12.0.0", + "firebase-functions": ">=4.9.0", + "jest": ">=28.0.0" + } + }, + "node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "optional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "peer": true + }, + "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, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "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==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "optional": true + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "optional": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "optional": true, + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "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, + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "devOptional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "peer": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "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, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/google-auth-library": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.13.0.tgz", + "integrity": "sha512-p9Y03Uzp/Igcs36zAaB0XTSwZ8Y0/tpYiz5KIde5By+H9DCVUSYtDWZu6aFXsWTqENMb8BD/pDT3hR8NVrPkfA==", + "optional": true, + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.3.9.tgz", + "integrity": "sha512-tcjQr7sXVGMdlvcG25wSv98ap1dtF4Z6mcV0rztGIddOcezw4YMb/uTXg72JPrLep+kXcVjaJjg6oo3KLf4itQ==", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "^1.10.9", + "@grpc/proto-loader": "^0.7.13", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^9.3.0", + "node-fetch": "^2.7.0", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^2.0.2", + "protobufjs": "^7.3.2", + "retry-request": "^7.0.0", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "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, + "peer": true + }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "optional": true, + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "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==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "optional": true + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "peer": true + }, + "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==", + "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/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-agent/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "optional": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10.17.0" + } + }, + "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==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "peer": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "peer": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "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==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "peer": true + }, + "node_modules/is-core-module": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", + "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "dev": true, + "peer": true, + "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==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "devOptional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "peer": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "peer": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "peer": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "peer": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "peer": true + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "peer": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "peer": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "peer": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "peer": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "peer": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "peer": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "peer": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "peer": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "peer": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "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, + "peer": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "peer": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "peer": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "optional": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "peer": true + }, + "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, + "peer": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/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==" + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "optional": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwks-rsa": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz", + "integrity": "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==", + "dependencies": { + "@types/express": "^4.17.17", + "@types/jsonwebtoken": "^9.0.2", + "debug": "^4.3.4", + "jose": "^4.14.6", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jwks-rsa/node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/jwks-rsa/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/jwks-rsa/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "optional": true, + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "peer": true + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "peer": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "optional": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "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, + "peer": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lru-memoizer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "peer": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "peer": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "peer": true + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "peer": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "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==", + "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==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "peer": true + }, + "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==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "peer": true + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true, + "peer": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "peer": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "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==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "devOptional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "peer": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "devOptional": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "peer": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "peer": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "peer": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true, + "peer": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "peer": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "peer": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proto3-json-serializer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", + "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", + "optional": true, + "dependencies": { + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.2.tgz", + "integrity": "sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "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==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "peer": true + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "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==", + "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==", + "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-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "peer": true + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "peer": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "peer": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "optional": true, + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "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" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "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, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "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/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/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==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "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, + "peer": true, + "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, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "peer": true + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "peer": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "peer": true + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "peer": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "optional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "optional": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "peer": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "devOptional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "devOptional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "optional": true + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "optional": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "optional": true, + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/teeny-request/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/teeny-request/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "optional": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/teeny-request/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/teeny-request/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "optional": true + }, + "node_modules/teeny-request/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "peer": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "peer": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "peer": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "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==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "optional": true + }, + "node_modules/ts-deepmerge": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-2.0.7.tgz", + "integrity": "sha512-3phiGcxPSSR47RBubQxPoZ+pqXsEsozLo4G4AlSrsMKTFg9TA3l+3he5BqpUi9wiuDbaHWXH/amlzQ49uEdXtg==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/undici-types": { + "version": "6.19.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.6.tgz", + "integrity": "sha512-e/vggGopEfTKSvj4ihnOLTsqhrKRN3LeO6qSN/GxohhuRv8qH9bNQ4B8W7e/vFL+0XTnmHPB4/kegunZGA4Org==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "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" + } + ], + "peer": true, + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "optional": true + }, + "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==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.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==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "peer": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "optional": true + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "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, + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/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==", + "devOptional": true, + "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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "devOptional": true + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "peer": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "devOptional": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "peer": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "devOptional": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "devOptional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "devOptional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/sharekhan_admin_panel/functions/package.json b/sharekhan_admin_panel/functions/package.json new file mode 100644 index 00000000..e27e3f2f --- /dev/null +++ b/sharekhan_admin_panel/functions/package.json @@ -0,0 +1,25 @@ +{ + "name": "functions", + "scripts": { + "build": "tsc", + "build:watch": "tsc --watch", + "serve": "npm run build && firebase emulators:start --only functions", + "shell": "npm run build && firebase functions:shell", + "start": "npm run shell", + "deploy": "firebase deploy --only functions", + "logs": "firebase functions:log" + }, + "engines": { + "node": "18" + }, + "main": "lib/index.js", + "dependencies": { + "firebase-admin": "^12.1.0", + "firebase-functions": "^5.0.0" + }, + "devDependencies": { + "typescript": "^4.9.0", + "firebase-functions-test": "^3.1.0" + }, + "private": true +} \ No newline at end of file diff --git a/sharekhan_admin_panel/functions/src/index.ts b/sharekhan_admin_panel/functions/src/index.ts new file mode 100644 index 00000000..9462d2c7 --- /dev/null +++ b/sharekhan_admin_panel/functions/src/index.ts @@ -0,0 +1,19 @@ +/** + * Import function triggers from their respective submodules: + * + * import {onCall} from "firebase-functions/v2/https"; + * import {onDocumentWritten} from "firebase-functions/v2/firestore"; + * + * See a full list of supported triggers at https://firebase.google.com/docs/functions + */ + +import {onRequest} from "firebase-functions/v2/https"; +import * as logger from "firebase-functions/logger"; + +// Start writing functions +// https://firebase.google.com/docs/functions/typescript + +// export const helloWorld = onRequest((request, response) => { +// logger.info("Hello logs!", {structuredData: true}); +// response.send("Hello from Firebase!"); +// }); diff --git a/sharekhan_admin_panel/functions/tsconfig.json b/sharekhan_admin_panel/functions/tsconfig.json new file mode 100644 index 00000000..7ce05d03 --- /dev/null +++ b/sharekhan_admin_panel/functions/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "commonjs", + "noImplicitReturns": true, + "noUnusedLocals": true, + "outDir": "lib", + "sourceMap": true, + "strict": true, + "target": "es2017" + }, + "compileOnSave": true, + "include": [ + "src" + ] +} diff --git a/sharekhan_admin_panel/lib/constants/asset_constants.dart b/sharekhan_admin_panel/lib/constants/asset_constants.dart new file mode 100644 index 00000000..896e95b8 --- /dev/null +++ b/sharekhan_admin_panel/lib/constants/asset_constants.dart @@ -0,0 +1,14 @@ +const icons = "assets/icons/"; +const images = "assets/images/"; + +// icons +const iconTrial = "${icons}icon_trial.png"; +const iconChevronRight = "${icons}icon_chevron_right.png"; +const iconExport = "${icons}icon_export.png"; +const iconAdd = "${icons}icon_add.png"; +const iconBack = "${icons}icon_back.png"; +const iconSave = "${icons}icon_save.png"; + +// images +const imageLogoMain = "${images}logo_main.png"; +const imageSplashBanner = "${images}image_splash_banner.png"; diff --git a/sharekhan_admin_panel/lib/enums/enum_parser.dart b/sharekhan_admin_panel/lib/enums/enum_parser.dart new file mode 100644 index 00000000..9a3bfc52 --- /dev/null +++ b/sharekhan_admin_panel/lib/enums/enum_parser.dart @@ -0,0 +1,12 @@ +import 'package:sharekhan_admin_panel/enums/enums.dart'; + +UserType parseUserType(String userType) { + switch (userType) { + case 'buyer': + return UserType.buyer; + case 'seller': + return UserType.seller; + default: + return UserType.buyer; + } +} diff --git a/sharekhan_admin_panel/lib/enums/enums.dart b/sharekhan_admin_panel/lib/enums/enums.dart new file mode 100644 index 00000000..da1248e6 --- /dev/null +++ b/sharekhan_admin_panel/lib/enums/enums.dart @@ -0,0 +1,11 @@ +enum ScreenType { + banners, + categories, + coupons, + sheruProducts, + sellerProducts, + settings, + users +} + +enum UserType { buyer, seller } diff --git a/sharekhan_admin_panel/lib/firebase_options.dart b/sharekhan_admin_panel/lib/firebase_options.dart new file mode 100644 index 00000000..5e804c62 --- /dev/null +++ b/sharekhan_admin_panel/lib/firebase_options.dart @@ -0,0 +1,65 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for android - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.iOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for ios - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'AIzaSyCOWHsPPXodq5EuhaEXSfoLw9vfGYGTjj8', + appId: '1:1091785317203:web:0560ec13f743516f649fa6', + messagingSenderId: '1091785317203', + projectId: 'sheru-2eb0f', + authDomain: 'sheru-2eb0f.firebaseapp.com', + storageBucket: 'sheru-2eb0f.appspot.com', + measurementId: 'G-BSWZH3HRMY', + ); + +} \ No newline at end of file diff --git a/sharekhan_admin_panel/lib/globals.dart b/sharekhan_admin_panel/lib/globals.dart new file mode 100644 index 00000000..65286e6d --- /dev/null +++ b/sharekhan_admin_panel/lib/globals.dart @@ -0,0 +1,17 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_storage/firebase_storage.dart'; + +// Firebase +final firestore = FirebaseFirestore.instance; +final firebaseStorage = FirebaseStorage.instance; + +// Database References +final bannersRef = firestore.collection('banners'); +final categoriesRef = firestore.collection('categories'); +final couponsRef = firestore.collection('coupons'); +final productsRef = firestore.collection('products'); +final settingsRef = firestore.collection('settings'); +final usersRef = firestore.collection('users'); + +// Development Mode +const bool development = true; diff --git a/sharekhan_admin_panel/lib/main.dart b/sharekhan_admin_panel/lib/main.dart new file mode 100644 index 00000000..5d366324 --- /dev/null +++ b/sharekhan_admin_panel/lib/main.dart @@ -0,0 +1,54 @@ +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:sharekhan_admin_panel/firebase_options.dart'; +import 'package:sharekhan_admin_panel/globals.dart'; +import 'package:sharekhan_admin_panel/navigation/go_router.dart'; +import 'package:sharekhan_admin_panel/providers/banner_provider.dart'; +import 'package:sharekhan_admin_panel/providers/category_provider.dart'; +import 'package:sharekhan_admin_panel/providers/coupon_provider.dart'; +import 'package:sharekhan_admin_panel/providers/product_provider.dart'; +import 'package:sharekhan_admin_panel/providers/setting_provider.dart'; +import 'package:sharekhan_admin_panel/providers/user_provider.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; + +void main() async { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + if (development) { + // functions.useFunctionsEmulator('localhost', 5001); + // auth.useAuthEmulator('localhost', 9099); + firestore.useFirestoreEmulator('localhost', 8080); + } + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => BannerProvider()), + ChangeNotifierProvider(create: (_) => CategoryProvider()), + ChangeNotifierProvider(create: (_) => CouponProvider()), + ChangeNotifierProvider(create: (_) => ProductProvider()), + ChangeNotifierProvider(create: (_) => SettingProvider()), + ChangeNotifierProvider(create: (_) => UserProvider()), + ], + child: MaterialApp.router( + debugShowCheckedModeBanner: false, + title: 'Sharekhan Admin', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: AppTheme.colorRed), + useMaterial3: true, + ), + routerDelegate: router.routerDelegate, + routeInformationParser: router.routeInformationParser, + routeInformationProvider: router.routeInformationProvider, + ), + ); + } +} diff --git a/sharekhan_admin_panel/lib/models/banner_model.dart b/sharekhan_admin_panel/lib/models/banner_model.dart new file mode 100644 index 00000000..6db34882 --- /dev/null +++ b/sharekhan_admin_panel/lib/models/banner_model.dart @@ -0,0 +1,74 @@ +import 'dart:convert'; + +class BannerModel { + final String id; + final String imageUrl; + final String productID; + final bool isActive; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + BannerModel({ + required this.id, + required this.imageUrl, + required this.productID, + required this.isActive, + required this.description, + required this.createdAt, + required this.updatedAt, + }); + + BannerModel copyWith({ + String? id, + String? imageUrl, + String? productID, + bool? isActive, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + }) { + return BannerModel( + id: id ?? this.id, + imageUrl: imageUrl ?? this.imageUrl, + productID: productID ?? this.productID, + isActive: isActive ?? this.isActive, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + } + + Map toMap() { + return { + 'id': id, + 'imageUrl': imageUrl, + 'productID': productID, + 'isActive': isActive, + 'description': description, + 'createdAt': createdAt, + 'updatedAt': updatedAt, + }; + } + + factory BannerModel.fromMap(Map map) { + return BannerModel( + id: map['id'] as String, + imageUrl: map['imageUrl'] as String, + productID: map['productID'] as String, + isActive: map['isActive'] as bool, + description: map['description'] as String, + createdAt: map['createdAt'].toDate(), + updatedAt: map['updatedAt'].toDate(), + ); + } + + String toJson() => json.encode(toMap()); + + factory BannerModel.fromJson(String source) => + BannerModel.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'BannerModel(id: $id, imageUrl: $imageUrl, productID: $productID, isActive: $isActive, description: $description, createdAt: $createdAt, updatedAt: $updatedAt)'; + } +} diff --git a/sharekhan_admin_panel/lib/models/category_model.dart b/sharekhan_admin_panel/lib/models/category_model.dart new file mode 100644 index 00000000..67e5a6fe --- /dev/null +++ b/sharekhan_admin_panel/lib/models/category_model.dart @@ -0,0 +1,67 @@ +import 'dart:convert'; + +class CategoryModel { + final String id; + final String name; + final String imageUrl; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + CategoryModel({ + required this.id, + required this.name, + required this.imageUrl, + required this.description, + required this.createdAt, + required this.updatedAt, + }); + + CategoryModel copyWith({ + String? id, + String? name, + String? imageUrl, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + }) { + return CategoryModel( + id: id ?? this.id, + name: name ?? this.name, + imageUrl: imageUrl ?? this.imageUrl, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + } + + Map toMap() { + return { + 'id': id, + 'name': name, + 'imageUrl': imageUrl, + 'description': description, + 'createdAt': createdAt, + 'updatedAt': updatedAt, + }; + } + + factory CategoryModel.fromMap(Map map) { + return CategoryModel( + id: map['id'] as String, + name: map['name'] as String, + imageUrl: map['imageUrl'] as String, + description: map['description'] as String, + createdAt: map['createdAt'].toDate(), + updatedAt: map['updatedAt'].toDate(), + ); + } + + String toJson() => json.encode(toMap()); + + factory CategoryModel.fromJson(String source) => + CategoryModel.fromMap(json.decode(source) as Map); + + @override + String toString() => + 'CategoryModel(id: $id, name: $name, imageUrl: $imageUrl, description: $description, createdAt: $createdAt, updatedAt: $updatedAt)'; +} diff --git a/sharekhan_admin_panel/lib/models/coupon_model.dart b/sharekhan_admin_panel/lib/models/coupon_model.dart new file mode 100644 index 00000000..d1ac9d8e --- /dev/null +++ b/sharekhan_admin_panel/lib/models/coupon_model.dart @@ -0,0 +1,86 @@ +import 'dart:convert'; + +class CouponModel { + final String id; + final String couponCode; + final String description; + final double percentage; + final int maxAmount; + final List userIDs; + final bool isActive; + final DateTime createdAt; + final DateTime updatedAt; + CouponModel({ + required this.id, + required this.couponCode, + required this.description, + required this.percentage, + required this.maxAmount, + required this.userIDs, + required this.isActive, + required this.createdAt, + required this.updatedAt, + }); + + CouponModel copyWith({ + String? id, + String? couponCode, + String? description, + double? percentage, + int? maxAmount, + List? userIDs, + bool? isActive, + DateTime? createdAt, + DateTime? updatedAt, + }) { + return CouponModel( + id: id ?? this.id, + couponCode: couponCode ?? this.couponCode, + description: description ?? this.description, + percentage: percentage ?? this.percentage, + maxAmount: maxAmount ?? this.maxAmount, + userIDs: userIDs ?? this.userIDs, + isActive: isActive ?? this.isActive, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ); + } + + Map toMap() { + return { + 'id': id, + 'couponCode': couponCode, + 'description': description, + 'percentage': percentage, + 'maxAmount': maxAmount, + 'userIDs': userIDs, + 'isActive': isActive, + 'createdAt': createdAt, + 'updatedAt': updatedAt, + }; + } + + factory CouponModel.fromMap(Map map) { + return CouponModel( + id: map['id'] as String, + couponCode: map['couponCode'] as String, + description: map['description'] as String, + percentage: map['percentage'] as double, + maxAmount: map['maxAmount'] as int, + userIDs: List.from((map['userIDs'])), + isActive: map['isActive'] as bool, + createdAt: map['createdAt'].toDate(), + updatedAt: map['updatedAt'].toDate(), + ); + } + + String toJson() => json.encode(toMap()); + + factory CouponModel.fromJson(String source) => + CouponModel.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'CouponModel(id: $id, couponCode: $couponCode, description: $description, percentage: $percentage, maxAmount: $maxAmount, userIDs: $userIDs, isActive: $isActive , createdAt: $createdAt, updatedAt: $updatedAt)'; + } +} diff --git a/sharekhan_admin_panel/lib/models/product_model.dart b/sharekhan_admin_panel/lib/models/product_model.dart new file mode 100644 index 00000000..89c4873f --- /dev/null +++ b/sharekhan_admin_panel/lib/models/product_model.dart @@ -0,0 +1,120 @@ +import 'dart:convert'; + +class ProductModel { + final String id; + final String categoryID; + final List images; + final String name; + final String description; + final double unitWeight; + final int unitPrice; + final int remainingQuantity; + final bool isActive; + final DateTime createdAt; + final DateTime updatedAt; + final String sellerId; + final bool isSheruSpecial; + final bool isApproved; + ProductModel({ + required this.id, + required this.categoryID, + required this.images, + required this.name, + required this.description, + required this.unitWeight, + required this.unitPrice, + required this.remainingQuantity, + required this.isActive, + required this.createdAt, + required this.updatedAt, + required this.sellerId, + required this.isSheruSpecial, + required this.isApproved, + }); + + ProductModel copyWith({ + String? id, + String? categoryID, + List? images, + String? name, + String? description, + double? unitWeight, + int? unitPrice, + int? wholesalePrice, + int? specialPrice, + int? wholesaleQuantity, + int? specialQuantity, + int? remainingQuantity, + bool? isActive, + DateTime? createdAt, + DateTime? updatedAt, + String? sellerId, + bool? isSheruSpecial, + bool? isApproved, + }) { + return ProductModel( + id: id ?? this.id, + categoryID: categoryID ?? this.categoryID, + images: images ?? this.images, + name: name ?? this.name, + description: description ?? this.description, + unitWeight: unitWeight ?? this.unitWeight, + unitPrice: unitPrice ?? this.unitPrice, + remainingQuantity: remainingQuantity ?? this.remainingQuantity, + isActive: isActive ?? this.isActive, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + sellerId: sellerId ?? this.sellerId, + isSheruSpecial: isSheruSpecial ?? this.isSheruSpecial, + isApproved: isApproved ?? this.isApproved, + ); + } + + Map toMap() { + return { + 'id': id, + 'categoryID': categoryID, + 'images': images, + 'name': name, + 'description': description, + 'unitWeight': unitWeight, + 'unitPrice': unitPrice, + 'remainingQuantity': remainingQuantity, + 'isActive': isActive, + 'createdAt': createdAt, + 'updatedAt': updatedAt, + 'sellerId': sellerId, + 'isSheruSpecial': isSheruSpecial, + 'isApproved': isApproved, + }; + } + + factory ProductModel.fromMap(Map map) { + return ProductModel( + id: map['id'] as String, + categoryID: map['categoryID'] as String, + images: List.from((map['images'])), + name: map['name'] as String, + description: map['description'] as String, + unitWeight: map['unitWeight'] as double, + unitPrice: map['unitPrice'] as int, + remainingQuantity: map['remainingQuantity'] as int, + isActive: map['isActive'] as bool, + createdAt: map['createdAt'].toDate(), + updatedAt: map['updatedAt'].toDate(), + sellerId: map['sellerId'] as String, + isSheruSpecial: map['isSheruSpecial'] as bool, + isApproved: map['isApproved'] as bool, + ); + } + + String toJson() => json.encode(toMap()); + + factory ProductModel.fromJson(String source) => + ProductModel.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'ProductModel(id: $id, categoryID: $categoryID, images: $images, name: $name, description: $description, unitWeight: $unitWeight, unitPrice: $unitPrice, remainingQuantity: $remainingQuantity, isActive: $isActive. createdAt: $createdAt, updatedAt: $updatedAt, sellerId: $sellerId, isSheruSpecial: $isSheruSpecial, isApproved: $isApproved)'; + } +} diff --git a/sharekhan_admin_panel/lib/models/settings_model.dart b/sharekhan_admin_panel/lib/models/settings_model.dart new file mode 100644 index 00000000..43e08d1d --- /dev/null +++ b/sharekhan_admin_panel/lib/models/settings_model.dart @@ -0,0 +1,127 @@ +import 'dart:convert'; + +class SettingsModel { + final AppVersion version; + SettingsModel({ + required this.version, + }); + + SettingsModel copyWith({ + AppVersion? version, + }) { + return SettingsModel( + version: version ?? this.version, + ); + } + + Map toMap() { + return { + 'version': version.toMap(), + }; + } + + factory SettingsModel.fromMap(Map map) { + return SettingsModel( + version: AppVersion.fromMap(map['version'] as Map), + ); + } + + String toJson() => json.encode(toMap()); + + factory SettingsModel.fromJson(String source) => + SettingsModel.fromMap(json.decode(source) as Map); + + @override + String toString() => 'SettingsModel(version: $version)'; +} + +class AppVersion { + final Version androidVersion; + final Version iosVersion; + final bool isMaintenance; + AppVersion({ + required this.androidVersion, + required this.iosVersion, + required this.isMaintenance, + }); + + AppVersion copyWith({ + Version? androidVersion, + Version? iosVersion, + bool? isMaintenance, + }) { + return AppVersion( + androidVersion: androidVersion ?? this.androidVersion, + iosVersion: iosVersion ?? this.iosVersion, + isMaintenance: isMaintenance ?? this.isMaintenance, + ); + } + + Map toMap() { + return { + 'androidVersion': androidVersion.toMap(), + 'iosVersion': iosVersion.toMap(), + 'isMaintenance': isMaintenance, + }; + } + + factory AppVersion.fromMap(Map map) { + return AppVersion( + androidVersion: + Version.fromMap(map['androidVersion'] as Map), + iosVersion: Version.fromMap(map['iosVersion'] as Map), + isMaintenance: map['isMaintenance'] as bool, + ); + } + + String toJson() => json.encode(toMap()); + + factory AppVersion.fromJson(String source) => + AppVersion.fromMap(json.decode(source) as Map); + + @override + String toString() => + 'AppVersion(androidVersion: $androidVersion, iosVersion: $iosVersion, isMaintenance: $isMaintenance)'; +} + +class Version { + final String versionName; + final bool mandatory; + Version({ + required this.versionName, + required this.mandatory, + }); + + Version copyWith({ + String? versionName, + bool? mandatory, + }) { + return Version( + versionName: versionName ?? this.versionName, + mandatory: mandatory ?? this.mandatory, + ); + } + + Map toMap() { + return { + 'versionName': versionName, + 'mandatory': mandatory, + }; + } + + factory Version.fromMap(Map map) { + return Version( + versionName: map['versionName'] as String, + mandatory: map['mandatory'] as bool, + ); + } + + String toJson() => json.encode(toMap()); + + factory Version.fromJson(String source) => + Version.fromMap(json.decode(source) as Map); + + @override + String toString() => + 'Version(versionName: $versionName, mandatory: $mandatory)'; +} diff --git a/sharekhan_admin_panel/lib/models/user_model.dart b/sharekhan_admin_panel/lib/models/user_model.dart new file mode 100644 index 00000000..bca4ffed --- /dev/null +++ b/sharekhan_admin_panel/lib/models/user_model.dart @@ -0,0 +1,115 @@ +import 'dart:convert'; +import 'package:sharekhan_admin_panel/enums/enum_parser.dart'; +import 'package:sharekhan_admin_panel/enums/enums.dart'; + +class UserModel { + final String id; + final String name; + final String phoneNumber; + final String? email; + final bool isBusiness; + final String? businessName; + final String? businessType; + final String? gstNumber; + final String? panNumber; + final UserType userType; + final String defaultAddressID; + final DateTime createdAt; + final DateTime lastSeenAt; + + UserModel({ + required this.id, + required this.name, + required this.phoneNumber, + this.email, + required this.isBusiness, + this.businessName, + this.businessType, + this.gstNumber, + this.panNumber, + required this.userType, + required this.defaultAddressID, + required this.createdAt, + required this.lastSeenAt, + }); + + UserModel copyWith({ + String? id, + String? name, + String? phoneNumber, + String? email, + bool? isBusiness, + String? businessName, + String? businessType, + String? gstNumber, + String? panNumber, + UserType? userType, + String? defaultAddressID, + DateTime? createdAt, + DateTime? lastSeenAt, + }) { + return UserModel( + id: id ?? this.id, + name: name ?? this.name, + phoneNumber: phoneNumber ?? this.phoneNumber, + email: email ?? this.email, + isBusiness: isBusiness ?? this.isBusiness, + businessName: businessName ?? this.businessName, + businessType: businessType ?? this.businessType, + gstNumber: gstNumber ?? this.gstNumber, + panNumber: panNumber ?? this.panNumber, + userType: userType ?? this.userType, + defaultAddressID: defaultAddressID ?? this.defaultAddressID, + createdAt: createdAt ?? this.createdAt, + lastSeenAt: lastSeenAt ?? this.lastSeenAt, + ); + } + + Map toMap() { + return { + 'id': id, + 'name': name, + 'phoneNumber': phoneNumber, + 'email': email, + 'isBusiness': isBusiness, + 'businessName': businessName, + 'businessType': businessType, + 'gstNumber': gstNumber, + 'panNumber': panNumber, + 'userType': userType.name, + 'defaultAddressID': defaultAddressID, + 'createdAt': createdAt.toIso8601String(), + 'lastSeenAt': lastSeenAt.toIso8601String(), + }; + } + + factory UserModel.fromMap(Map map) { + return UserModel( + id: map['id'] as String, + name: map['name'] as String, + phoneNumber: map['phoneNumber'] as String, + email: map['email'] != null ? map['email'] as String : null, + isBusiness: map['isBusiness'] as bool, + businessName: + map['businessName'] != null ? map['businessName'] as String : null, + businessType: + map['businessType'] != null ? map['businessType'] as String : null, + gstNumber: map['gstNumber'] != null ? map['gstNumber'] as String : null, + panNumber: map['panNumber'] != null ? map['panNumber'] as String : null, + userType: parseUserType(map['userType'] as String), + defaultAddressID: map['defaultAddressID'] as String, + createdAt: DateTime.parse(map['createdAt'] as String), + lastSeenAt: DateTime.parse(map['lastSeenAt'] as String), + ); + } + + String toJson() => json.encode(toMap()); + + factory UserModel.fromJson(String source) => + UserModel.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'UserModel(id: $id, name: $name, phoneNumber: $phoneNumber, email: $email, isBusiness: $isBusiness, businessName: $businessName, businessType: $businessType, gstNumber: $gstNumber, panNumber: $panNumber, userType: $userType, defaultAddressID: $defaultAddressID, createdAt: $createdAt, lastSeenAt: $lastSeenAt)'; + } +} diff --git a/sharekhan_admin_panel/lib/navigation/go_router.dart b/sharekhan_admin_panel/lib/navigation/go_router.dart new file mode 100644 index 00000000..eb9cbac8 --- /dev/null +++ b/sharekhan_admin_panel/lib/navigation/go_router.dart @@ -0,0 +1,145 @@ +import 'package:sharekhan_admin_panel/models/banner_model.dart'; +import 'package:sharekhan_admin_panel/models/category_model.dart'; +import 'package:sharekhan_admin_panel/models/coupon_model.dart'; +import 'package:sharekhan_admin_panel/models/product_model.dart'; +import 'package:sharekhan_admin_panel/navigation/routes.dart'; +import 'package:sharekhan_admin_panel/screens/banners/add_banner_screen.dart'; +import 'package:sharekhan_admin_panel/screens/banners/banners_screen.dart'; +import 'package:sharekhan_admin_panel/screens/banners/edit_banner_screen.dart'; +import 'package:sharekhan_admin_panel/screens/categories/add_category_screen.dart'; +import 'package:sharekhan_admin_panel/screens/categories/categories_screen.dart'; +import 'package:sharekhan_admin_panel/screens/categories/edit_category_screen.dart'; +import 'package:sharekhan_admin_panel/screens/coupons/add_coupon.dart'; +import 'package:sharekhan_admin_panel/screens/coupons/coupons_screen.dart'; +import 'package:sharekhan_admin_panel/screens/coupons/edit_coupon.dart'; +import 'package:sharekhan_admin_panel/screens/main_screen.dart'; +import 'package:sharekhan_admin_panel/screens/products/add_product_screen.dart'; +import 'package:sharekhan_admin_panel/screens/products/edit_product_screen.dart'; +import 'package:sharekhan_admin_panel/screens/products/products_screen.dart'; +import 'package:sharekhan_admin_panel/screens/sellerPoducts/products_screen.dart'; +import 'package:sharekhan_admin_panel/screens/settings/edit_settings_screen.dart'; +import 'package:sharekhan_admin_panel/screens/settings/settings_screen.dart'; +import 'package:sharekhan_admin_panel/screens/splash_screen.dart'; +import 'package:sharekhan_admin_panel/screens/users/users_screen.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +final GoRouter router = GoRouter( + routes: [ + GoRoute( + path: Routes.splash, + pageBuilder: (BuildContext context, GoRouterState state) => + const NoTransitionPage(child: SplashScreen()), + ), + + // Main App Routes + ShellRoute( + builder: (context, state, child) { + return MainScreen( + child: child, + ); + }, + routes: [ + // Banners + GoRoute( + path: Routes.banners, + pageBuilder: (BuildContext context, GoRouterState state) => + const NoTransitionPage(child: BannersScreen()), + ), + GoRoute( + path: Routes.bannerAdd, + pageBuilder: (BuildContext context, GoRouterState state) => + const NoTransitionPage(child: AddBannerScreen()), + ), + GoRoute( + path: Routes.bannerEdit, + pageBuilder: (BuildContext context, GoRouterState state) { + final BannerModel banner = state.extra! as BannerModel; + return NoTransitionPage( + child: EditBannerScreen(banner: banner), + ); + }), + + // Categories + GoRoute( + path: Routes.categories, + pageBuilder: (BuildContext context, GoRouterState state) => + const NoTransitionPage(child: CategoriesScreen()), + ), + GoRoute( + path: Routes.categoryAdd, + pageBuilder: (BuildContext context, GoRouterState state) => + const NoTransitionPage(child: AddCategoryScreen()), + ), + GoRoute( + path: Routes.categoryEdit, + pageBuilder: (BuildContext context, GoRouterState state) { + final CategoryModel category = state.extra! as CategoryModel; + return NoTransitionPage( + child: EditCategoryScreen(category: category), + ); + }), + // Coupons + GoRoute( + path: Routes.coupons, + pageBuilder: (BuildContext context, GoRouterState state) => + const NoTransitionPage(child: CouponsScreen()), + ), + GoRoute( + path: Routes.couponAdd, + pageBuilder: (BuildContext context, GoRouterState state) => + const NoTransitionPage(child: AddCouponScreen()), + ), + GoRoute( + path: Routes.couponEdit, + pageBuilder: (BuildContext context, GoRouterState state) { + final CouponModel coupon = state.extra! as CouponModel; + return NoTransitionPage( + child: EditCouponScreen(coupon: coupon), + ); + }), + // Products + GoRoute( + path: Routes.sellerProducts, + pageBuilder: (BuildContext context, GoRouterState state) => + const NoTransitionPage(child: SellerProductsScreen()), + ), + GoRoute( + path: Routes.sheruProducts, + pageBuilder: (BuildContext context, GoRouterState state) => + const NoTransitionPage(child: ProductsScreen()), + ), + GoRoute( + path: Routes.productAdd, + pageBuilder: (BuildContext context, GoRouterState state) => + const NoTransitionPage(child: AddProductScreen()), + ), + GoRoute( + path: Routes.productEdit, + pageBuilder: (BuildContext context, GoRouterState state) { + final ProductModel product = state.extra! as ProductModel; + return NoTransitionPage( + child: EditProductScreen(product: product), + ); + }), + // Settings + GoRoute( + path: Routes.settings, + pageBuilder: (BuildContext context, GoRouterState state) => + const NoTransitionPage(child: SettingsScreen()), + ), + GoRoute( + path: Routes.settingsEdit, + pageBuilder: (BuildContext context, GoRouterState state) => + const NoTransitionPage(child: EditSettingsScreen()), + ), + // Users + GoRoute( + path: Routes.users, + pageBuilder: (BuildContext context, GoRouterState state) => + const NoTransitionPage(child: UsersScreen()), + ), + ], + ), + ], +); diff --git a/sharekhan_admin_panel/lib/navigation/routes.dart b/sharekhan_admin_panel/lib/navigation/routes.dart new file mode 100644 index 00000000..07a662b7 --- /dev/null +++ b/sharekhan_admin_panel/lib/navigation/routes.dart @@ -0,0 +1,20 @@ +class Routes { + static const String splash = '/'; + static const String main = '/main'; + static const String banners = '/banners'; + static const String bannerAdd = '/bannerAdd'; + static const String bannerEdit = '/bannerEdit'; + static const String categories = '/categories'; + static const String categoryAdd = '/categoryAdd'; + static const String categoryEdit = '/categoryEdit'; + static const String coupons = '/coupons'; + static const String couponAdd = '/couponAdd'; + static const String couponEdit = '/couponEdit'; + static const String sellerProducts = '/sellerProducts'; + static const String sheruProducts = '/sheruProducts'; + static const String productAdd = '/productAdd'; + static const String productEdit = '/productEdit'; + static const String settings = '/settings'; + static const String settingsEdit = '/settingsEdit'; + static const String users = '/users'; +} diff --git a/sharekhan_admin_panel/lib/providers/banner_provider.dart b/sharekhan_admin_panel/lib/providers/banner_provider.dart new file mode 100644 index 00000000..192fb554 --- /dev/null +++ b/sharekhan_admin_panel/lib/providers/banner_provider.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:sharekhan_admin_panel/globals.dart'; +import 'package:sharekhan_admin_panel/models/banner_model.dart'; + +class BannerProvider extends ChangeNotifier { + List _banners = []; + List get banners => _banners; + + Future fetchBanners() async { + final querySnapshot = await bannersRef.get(); + _banners = querySnapshot.docs + .map((doc) => BannerModel.fromMap(doc.data())) + .toList(); + notifyListeners(); + } + + Future addBanner(BannerModel banner) async { + await bannersRef.doc(banner.id).set(banner.toMap()); + _banners.add(banner); + notifyListeners(); + } + + Future updateBanner(BannerModel banner) async { + await bannersRef.doc(banner.id).update(banner.toMap()); + _banners.removeWhere((element) => element.id == banner.id); + _banners.add(banner); + notifyListeners(); + } + + Future deleteBanner(BannerModel banner) async { + await bannersRef.doc(banner.id).delete(); + _banners.removeWhere((element) => element.id == banner.id); + notifyListeners(); + } + + Future toggleActive(BannerModel banner, bool isActive) async { + try { + int index = _banners.indexWhere((element) => element.id == banner.id); + _banners[index] = banner.copyWith(isActive: isActive); + notifyListeners(); + await bannersRef.doc(banner.id).update({'isActive': isActive}); + } catch (e) { + int index = _banners.indexWhere((element) => element.id == banner.id); + _banners[index] = banner.copyWith(isActive: !isActive); + notifyListeners(); + } + } +} diff --git a/sharekhan_admin_panel/lib/providers/category_provider.dart b/sharekhan_admin_panel/lib/providers/category_provider.dart new file mode 100644 index 00000000..e8cf5eea --- /dev/null +++ b/sharekhan_admin_panel/lib/providers/category_provider.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:sharekhan_admin_panel/globals.dart'; +import 'package:sharekhan_admin_panel/models/category_model.dart'; + +class CategoryProvider extends ChangeNotifier { + List _categories = []; + List get categories => _categories; + + Future fetchCategories() async { + final querySnapshot = await categoriesRef.get(); + _categories = querySnapshot.docs + .map((doc) => CategoryModel.fromMap(doc.data())) + .toList(); + notifyListeners(); + } + + Future addCategory(CategoryModel category) async { + await categoriesRef.doc(category.id).set(category.toMap()); + _categories.add(category); + notifyListeners(); + } + + Future updateCategory(CategoryModel category) async { + await categoriesRef.doc(category.id).update(category.toMap()); + _categories.removeWhere((element) => element.id == category.id); + _categories.add(category); + notifyListeners(); + } + + Future deleteCategory(CategoryModel category) async { + await categoriesRef.doc(category.id).delete(); + _categories.removeWhere((element) => element.id == category.id); + notifyListeners(); + } +} diff --git a/sharekhan_admin_panel/lib/providers/coupon_provider.dart b/sharekhan_admin_panel/lib/providers/coupon_provider.dart new file mode 100644 index 00000000..ecb76eff --- /dev/null +++ b/sharekhan_admin_panel/lib/providers/coupon_provider.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:sharekhan_admin_panel/globals.dart'; +import 'package:sharekhan_admin_panel/models/coupon_model.dart'; + +class CouponProvider extends ChangeNotifier { + List _coupons = []; + List get coupons => _coupons; + + Future fetchCoupons() async { + final querySnapshot = await couponsRef.get(); + _coupons = querySnapshot.docs + .map((doc) => CouponModel.fromMap(doc.data())) + .toList(); + notifyListeners(); + } + + Future addCoupon(CouponModel coupon) async { + await couponsRef.doc(coupon.id).set(coupon.toMap()); + _coupons.add(coupon); + notifyListeners(); + } + + Future updateCoupon(CouponModel coupon) async { + await couponsRef.doc(coupon.id).update(coupon.toMap()); + _coupons.removeWhere((element) => element.id == coupon.id); + _coupons.add(coupon); + notifyListeners(); + } + + Future deleteCoupon(CouponModel coupon) async { + await couponsRef.doc(coupon.id).delete(); + _coupons.removeWhere((element) => element.id == coupon.id); + notifyListeners(); + } + + Future toggleActive(CouponModel coupon, bool isActive) async { + try { + int index = _coupons.indexWhere((element) => element.id == coupon.id); + _coupons[index] = coupon.copyWith(isActive: isActive); + notifyListeners(); + await couponsRef.doc(coupon.id).update({'isActive': isActive}); + } catch (e) { + int index = _coupons.indexWhere((element) => element.id == coupon.id); + _coupons[index] = coupon.copyWith(isActive: !isActive); + notifyListeners(); + } + } +} diff --git a/sharekhan_admin_panel/lib/providers/product_provider.dart b/sharekhan_admin_panel/lib/providers/product_provider.dart new file mode 100644 index 00000000..2372f51f --- /dev/null +++ b/sharekhan_admin_panel/lib/providers/product_provider.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:sharekhan_admin_panel/globals.dart'; +import 'package:sharekhan_admin_panel/models/product_model.dart'; + +class ProductProvider extends ChangeNotifier { + List _products = []; + List get products => _products; + + Future fetchProducts() async { + final querySnapshot = await productsRef.get(); + _products = querySnapshot.docs + .map((doc) => ProductModel.fromMap(doc.data())) + .toList(); + notifyListeners(); + } + + Future addProduct(ProductModel product) async { + await productsRef.doc(product.id).set(product.toMap()); + _products.add(product); + notifyListeners(); + } + + Future updateProduct(ProductModel product) async { + await productsRef.doc(product.id).update(product.toMap()); + _products.removeWhere((element) => element.id == product.id); + _products.add(product); + notifyListeners(); + } + + Future deleteProduct(ProductModel product) async { + await productsRef.doc(product.id).delete(); + _products.removeWhere((element) => element.id == product.id); + notifyListeners(); + } + + Future toggleActive(ProductModel product, bool isActive) async { + try { + int index = _products.indexWhere((element) => element.id == product.id); + _products[index] = product.copyWith(isActive: isActive); + notifyListeners(); + await productsRef.doc(product.id).update({'isActive': isActive}); + } catch (e) { + int index = _products.indexWhere((element) => element.id == product.id); + _products[index] = product.copyWith(isActive: !isActive); + notifyListeners(); + } + } +} diff --git a/sharekhan_admin_panel/lib/providers/setting_provider.dart b/sharekhan_admin_panel/lib/providers/setting_provider.dart new file mode 100644 index 00000000..f54d238c --- /dev/null +++ b/sharekhan_admin_panel/lib/providers/setting_provider.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:sharekhan_admin_panel/globals.dart'; +import 'package:sharekhan_admin_panel/models/settings_model.dart'; + +class SettingProvider extends ChangeNotifier { + late SettingsModel _settings; + SettingsModel get settings => _settings; + + Future fetchSettings() async { + final querySnapshot = await settingsRef.doc('appSettings').get(); + _settings = SettingsModel.fromMap(querySnapshot.data()!); + notifyListeners(); + } + + Future updateSettings(SettingsModel settings) async { + await settingsRef.doc('appSettings').update(settings.toMap()); + _settings = settings; + notifyListeners(); + } + + Future updateMaintainance(bool isMaintenance) async { + _settings = settings.copyWith( + version: settings.version.copyWith(isMaintenance: isMaintenance)); + await settingsRef.doc('appSettings').update(_settings.toMap()); + notifyListeners(); + } +} diff --git a/sharekhan_admin_panel/lib/providers/user_provider.dart b/sharekhan_admin_panel/lib/providers/user_provider.dart new file mode 100644 index 00000000..d02432e1 --- /dev/null +++ b/sharekhan_admin_panel/lib/providers/user_provider.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; +import 'package:sharekhan_admin_panel/globals.dart'; +import 'package:sharekhan_admin_panel/models/user_model.dart'; + +class UserProvider extends ChangeNotifier { + List _users = []; + List get users => _users; + + Future fetchUsers() async { + final querySnapshot = await usersRef.get(); + _users = + querySnapshot.docs.map((doc) => UserModel.fromMap(doc.data())).toList(); + notifyListeners(); + } +} diff --git a/sharekhan_admin_panel/lib/screens/banners/add_banner_screen.dart b/sharekhan_admin_panel/lib/screens/banners/add_banner_screen.dart new file mode 100644 index 00000000..3fca43dd --- /dev/null +++ b/sharekhan_admin_panel/lib/screens/banners/add_banner_screen.dart @@ -0,0 +1,243 @@ +import 'dart:typed_data'; +import 'package:sharekhan_admin_panel/globals.dart'; +import 'package:sharekhan_admin_panel/navigation/go_router.dart'; +import 'package:sharekhan_admin_panel/navigation/routes.dart'; +import 'package:sharekhan_admin_panel/providers/product_provider.dart'; +import 'package:sharekhan_admin_panel/widgets/common/add_tab_footer.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_loading.dart'; +import 'package:sharekhan_admin_panel/widgets/common/secondary_tab_header.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:image_picker_web/image_picker_web.dart'; +import 'package:sharekhan_admin_panel/models/banner_model.dart'; +import 'package:sharekhan_admin_panel/providers/banner_provider.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:sharekhan_admin_panel/utils/get_uuid.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_button.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_dropdown.dart'; +import 'package:sharekhan_admin_panel/widgets/common/spacing.dart'; + +class AddBannerScreen extends StatefulWidget { + const AddBannerScreen({super.key}); + + @override + State createState() => _AddBannerScreenState(); +} + +class _AddBannerScreenState extends State { + String _fileName = 'File Name'; + String _productID = ''; + List _products = []; + Uint8List? _imageData; + String? _imageUrl; + final TextEditingController _descriptionController = TextEditingController(); + bool _isLoading = false; + + Future _pickImage() async { + final MediaInfo? pickedFile = await ImagePickerWeb.getImageInfo(); + if (pickedFile != null) { + setState(() { + _imageData = pickedFile.data; + _fileName = pickedFile.fileName!; + }); + } + } + + Future _uploadImage(Uint8List imageData, String bannerID) async { + final storageRef = firebaseStorage.ref(); + final imagesRef = storageRef.child('banners/$bannerID.png'); + await imagesRef.putData(imageData); + final imageUrl = await imagesRef.getDownloadURL(); + return imageUrl; + } + + _addBanner() async { + setState(() { + _isLoading = true; + }); + final bannerProvider = context.read(); + String bannerID = getUUID(); + if (_imageData != null) { + _imageUrl = await _uploadImage(_imageData!, bannerID); + } + + BannerModel banner = BannerModel( + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + description: _descriptionController.text.trim(), + id: bannerID, + productID: _productID, + isActive: true, + imageUrl: _imageUrl ?? '', + ); + await bannerProvider.addBanner(banner); + setState(() { + _isLoading = false; + }); + router.go(Routes.banners); + } + + void _onReset() { + setState(() { + _fileName = 'File Name'; + _productID = ''; + _imageData = null; + _imageUrl = null; + _descriptionController.clear(); + }); + } + + init() async { + final productProvider = context.read(); + setState(() { + _products = productProvider.products.map((e) => e.name).toList(); + }); + } + + @override + void initState() { + super.initState(); + init(); + } + + @override + Widget build(BuildContext context) { + final productProvider = context.watch(); + return !_isLoading + ? Scaffold( + backgroundColor: AppTheme.colorWhite, + body: Padding( + padding: AppTheme.paddingSmall, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + children: [ + const Spacing(size: AppTheme.spacingLarge), + const BreadcrumbTabHeader( + goBackRoute: Routes.banners, + mainTitle: 'Banners', + secondaryTitle: 'Add Banner', + ), + const Spacing(size: AppTheme.spacingSmall), + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Image*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + Row( + children: [ + Container( + height: 40, + width: 200, + padding: AppTheme.paddingTiny, + decoration: AppTheme.cardDecoration, + child: Text( + _fileName, + style: AppTheme.fontStyleSmall + .copyWith(color: AppTheme.colorGrey), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const Spacing( + size: AppTheme.spacingTiny, + isHorizontal: true), + CustomButton( + onTap: _pickImage, + text: 'Upload Image', + fillColor: AppTheme.colorRed, + textColor: AppTheme.colorWhite, + ), + ], + ), + ], + ), + const Spacing( + size: AppTheme.spacingMedium, isHorizontal: true), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Product*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + SizedBox( + width: 300, + child: CustomDropdown( + hintText: 'Choose an option', + value: _productID.isNotEmpty + ? productProvider.products + .firstWhere((element) => + element.id == _productID) + .name + : null, + items: _products + .map((e) => DropdownMenuItem( + value: e, + child: Text(e), + )) + .toList(), + onChanged: (val) { + setState(() { + _productID = productProvider.products + .firstWhere( + (element) => element.name == val) + .id; + }); + }, + ), + ), + ], + ), + ], + ), + const Spacing(size: AppTheme.spacingMedium), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Description*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + TextField( + controller: _descriptionController, + decoration: InputDecoration( + hintText: 'Enter the banner description here', + hintStyle: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ], + ), + ], + ), + AddTabFooter( + goBackrouteName: Routes.banners, + onAdd: _addBanner, + onReset: _onReset, + buttonText: 'Add Banner', + ), + ], + ), + ), + ) + : const CustomLoading(); + } +} diff --git a/sharekhan_admin_panel/lib/screens/banners/banners_screen.dart b/sharekhan_admin_panel/lib/screens/banners/banners_screen.dart new file mode 100644 index 00000000..cfc6cd6a --- /dev/null +++ b/sharekhan_admin_panel/lib/screens/banners/banners_screen.dart @@ -0,0 +1,196 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; +import 'package:syncfusion_flutter_datagrid/datagrid.dart'; +import 'package:sharekhan_admin_panel/models/banner_model.dart'; +import 'package:sharekhan_admin_panel/navigation/go_router.dart'; +import 'package:sharekhan_admin_panel/navigation/routes.dart'; +import 'package:sharekhan_admin_panel/providers/banner_provider.dart'; +import 'package:sharekhan_admin_panel/providers/product_provider.dart'; +import 'package:sharekhan_admin_panel/tables/banner_data_source.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_button.dart'; +import 'package:sharekhan_admin_panel/widgets/common/main_tab_header.dart'; +import 'package:sharekhan_admin_panel/widgets/common/spacing.dart'; +import 'package:csv/csv.dart'; +// ignore: avoid_web_libraries_in_flutter +import 'dart:html' as html; + +class BannersScreen extends StatefulWidget { + const BannersScreen({super.key}); + + @override + State createState() => _BannersScreenState(); +} + +class _BannersScreenState extends State { + void _onEdit(BannerModel banner) { + context.go(Routes.bannerEdit, extra: banner); + } + + void _onDelete(BannerModel banner) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Delete Banner', style: AppTheme.fontStyleMedium), + content: const Text( + 'Are you sure you want to delete this banner?', + style: AppTheme.fontStyleDefault, + ), + shape: AppTheme.rrShape, + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CustomButton( + onTap: () => router.pop(), + text: 'No, Cancel', + fillColor: AppTheme.colorBlack, + textColor: AppTheme.colorWhite, + ), + const Spacing(size: AppTheme.spacingSmall, isHorizontal: true), + CustomButton( + onTap: () async { + final bannerProvider = context.read(); + bannerProvider.deleteBanner(banner); + router.pop(); + }, + text: 'Yes, Delete', + fillColor: AppTheme.colorRed, + textColor: AppTheme.colorWhite, + ), + ], + ), + ], + ); + }, + ); + } + + void _onToggleActive(BannerModel banner, bool isActive) async { + final bannerProvider = context.read(); + await bannerProvider.toggleActive(banner, isActive); + } + + Future _exportToCSV() async { + final bannerProvider = context.read(); + List> rows = []; + rows.add([ + "Image URL", + "Product ID", + "Description", + "Is Active", + "Created At", + "Updated At" + ]); + for (var banner in bannerProvider.banners) { + rows.add([ + banner.imageUrl, + banner.productID, + banner.description, + banner.isActive, + banner.createdAt, + banner.updatedAt, + ]); + } + + String csv = const ListToCsvConverter().convert(rows); + final bytes = utf8.encode(csv); + final blob = html.Blob([bytes]); + final url = html.Url.createObjectUrlFromBlob(blob); + html.AnchorElement(href: url) + ..setAttribute("download", "banners.csv") + ..click(); + html.Url.revokeObjectUrl(url); + } + + @override + Widget build(BuildContext context) { + final bannerProvider = context.watch(); + final productProvider = context.watch(); + return Scaffold( + backgroundColor: AppTheme.colorWhite, + body: Padding( + padding: AppTheme.paddingSmall, + child: Column( + children: [ + const Spacing(size: AppTheme.spacingLarge), + MainTabHeader( + title: 'Banners', + routeName: Routes.bannerAdd, + secondButtonText: 'Add Banner', + onExport: _exportToCSV, + ), + const Spacing(size: AppTheme.spacingLarge), + Expanded( + child: SfDataGrid( + source: BannerDataSource( + banners: bannerProvider.banners, + products: productProvider.products, + onEdit: _onEdit, + onDelete: _onDelete, + onToggleActive: _onToggleActive, + ), + columnWidthMode: ColumnWidthMode.fill, + gridLinesVisibility: GridLinesVisibility.both, + headerGridLinesVisibility: GridLinesVisibility.both, + columns: [ + GridColumn( + columnName: 'imageUrl', + width: 100, + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: + const Text('Image', style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'productID', + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: const Text('Product Name', + style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'description', + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: const Text('Description', + style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'isActive', + width: 100, + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: + const Text('Show', style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'actions', + width: 100, + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: const Text('Actions', + style: AppTheme.fontStyleDefault), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/sharekhan_admin_panel/lib/screens/banners/edit_banner_screen.dart b/sharekhan_admin_panel/lib/screens/banners/edit_banner_screen.dart new file mode 100644 index 00000000..3d97183b --- /dev/null +++ b/sharekhan_admin_panel/lib/screens/banners/edit_banner_screen.dart @@ -0,0 +1,232 @@ +import 'dart:typed_data'; +import 'package:sharekhan_admin_panel/globals.dart'; +import 'package:sharekhan_admin_panel/navigation/go_router.dart'; +import 'package:sharekhan_admin_panel/navigation/routes.dart'; +import 'package:sharekhan_admin_panel/providers/product_provider.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_loading.dart'; +import 'package:sharekhan_admin_panel/widgets/common/edit_tab_footer.dart'; +import 'package:sharekhan_admin_panel/widgets/common/secondary_tab_header.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:image_picker_web/image_picker_web.dart'; +import 'package:sharekhan_admin_panel/models/banner_model.dart'; +import 'package:sharekhan_admin_panel/providers/banner_provider.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_button.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_dropdown.dart'; +import 'package:sharekhan_admin_panel/widgets/common/spacing.dart'; + +class EditBannerScreen extends StatefulWidget { + final BannerModel banner; + const EditBannerScreen({super.key, required this.banner}); + + @override + State createState() => _EditBannerScreenState(); +} + +class _EditBannerScreenState extends State { + String _fileName = 'File Name'; + String _productID = ''; + List _products = []; + Uint8List? _imageData; + String? _imageUrl; + final TextEditingController _descriptionController = TextEditingController(); + bool _isLoading = false; + + Future _pickImage() async { + final MediaInfo? pickedFile = await ImagePickerWeb.getImageInfo(); + if (pickedFile != null) { + setState(() { + _imageData = pickedFile.data; + _fileName = pickedFile.fileName!; + }); + } + } + + Future _uploadImage(Uint8List imageData, String bannerID) async { + final storageRef = firebaseStorage.ref(); + final imagesRef = storageRef.child('banners/$bannerID.png'); + await imagesRef.putData(imageData); + final imageUrl = await imagesRef.getDownloadURL(); + return imageUrl; + } + + _editBanner() async { + setState(() { + _isLoading = true; + }); + final bannerProvider = context.read(); + if (_imageData != null) { + _imageUrl = await _uploadImage(_imageData!, widget.banner.id); + } + + BannerModel banner = BannerModel( + createdAt: widget.banner.createdAt, + updatedAt: DateTime.now(), + description: _descriptionController.text.trim(), + id: widget.banner.id, + productID: _productID, + isActive: true, + imageUrl: _imageUrl ?? '', + ); + await bannerProvider.updateBanner(banner); + setState(() { + _isLoading = false; + }); + router.go(Routes.banners); + } + + init() { + final productProvider = context.read(); + setState(() { + _fileName = widget.banner.imageUrl; + _descriptionController.text = widget.banner.description; + _productID = widget.banner.productID; + _imageUrl = widget.banner.imageUrl; + _products = productProvider.products.map((e) => e.name).toList(); + }); + } + + @override + void initState() { + super.initState(); + init(); + } + + @override + Widget build(BuildContext context) { + final productProvider = context.watch(); + return !_isLoading + ? Scaffold( + backgroundColor: AppTheme.colorWhite, + body: Padding( + padding: AppTheme.paddingSmall, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + children: [ + const Spacing(size: AppTheme.spacingLarge), + const BreadcrumbTabHeader( + goBackRoute: Routes.banners, + mainTitle: 'Banners', + secondaryTitle: 'Edit Banner', + ), + const Spacing(size: AppTheme.spacingSmall), + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Image*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + Row( + children: [ + Container( + height: 40, + width: 200, + padding: AppTheme.paddingTiny, + decoration: AppTheme.cardDecoration, + child: Text( + _fileName, + style: AppTheme.fontStyleSmall + .copyWith(color: AppTheme.colorGrey), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const Spacing( + size: AppTheme.spacingTiny, + isHorizontal: true), + CustomButton( + onTap: _pickImage, + text: 'Upload Image', + fillColor: AppTheme.colorRed, + textColor: AppTheme.colorWhite, + ), + ], + ), + ], + ), + const Spacing( + size: AppTheme.spacingMedium, isHorizontal: true), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Product*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + SizedBox( + width: 300, + child: CustomDropdown( + value: productProvider.products + .firstWhere( + (element) => element.id == _productID) + .name, + hintText: 'Choose an option', + items: _products + .map((e) => DropdownMenuItem( + value: e, + child: Text(e), + )) + .toList(), + onChanged: (val) { + setState(() { + _productID = productProvider.products + .firstWhere( + (element) => element.name == val) + .id; + }); + }, + ), + ), + ], + ), + ], + ), + const Spacing(size: AppTheme.spacingMedium), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Description*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + TextField( + controller: _descriptionController, + decoration: InputDecoration( + hintText: 'Enter the banner description here', + hintStyle: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ], + ), + ], + ), + EditTabFooter( + goBackrouteName: Routes.banners, + onSave: _editBanner, + ), + ], + ), + ), + ) + : const CustomLoading(); + } +} diff --git a/sharekhan_admin_panel/lib/screens/categories/add_category_screen.dart b/sharekhan_admin_panel/lib/screens/categories/add_category_screen.dart new file mode 100644 index 00000000..7ff6fd76 --- /dev/null +++ b/sharekhan_admin_panel/lib/screens/categories/add_category_screen.dart @@ -0,0 +1,214 @@ +import 'dart:typed_data'; +import 'package:sharekhan_admin_panel/globals.dart'; +import 'package:sharekhan_admin_panel/models/category_model.dart'; +import 'package:sharekhan_admin_panel/navigation/go_router.dart'; +import 'package:sharekhan_admin_panel/navigation/routes.dart'; +import 'package:sharekhan_admin_panel/providers/category_provider.dart'; +import 'package:sharekhan_admin_panel/widgets/common/add_tab_footer.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_loading.dart'; +import 'package:sharekhan_admin_panel/widgets/common/secondary_tab_header.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:image_picker_web/image_picker_web.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:sharekhan_admin_panel/utils/get_uuid.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_button.dart'; +import 'package:sharekhan_admin_panel/widgets/common/spacing.dart'; + +class AddCategoryScreen extends StatefulWidget { + const AddCategoryScreen({super.key}); + + @override + State createState() => _AddCategoryScreenState(); +} + +class _AddCategoryScreenState extends State { + String _fileName = 'File Name'; + Uint8List? _imageData; + String? _imageUrl; + final TextEditingController _descriptionController = TextEditingController(); + final TextEditingController _nameController = TextEditingController(); + bool _isLoading = false; + + Future _pickImage() async { + final MediaInfo? pickedFile = await ImagePickerWeb.getImageInfo(); + if (pickedFile != null) { + setState(() { + _imageData = pickedFile.data; + _fileName = pickedFile.fileName!; + }); + } + } + + Future _uploadImage(Uint8List imageData, String categoryID) async { + final storageRef = firebaseStorage.ref(); + final imagesRef = storageRef.child('categories/$categoryID.png'); + await imagesRef.putData(imageData); + final imageUrl = await imagesRef.getDownloadURL(); + return imageUrl; + } + + _addCategory() async { + setState(() { + _isLoading = true; + }); + final categoryProvider = context.read(); + String categoryID = getUUID(); + if (_imageData != null) { + _imageUrl = await _uploadImage(_imageData!, categoryID); + } + + CategoryModel category = CategoryModel( + id: categoryID, + description: _descriptionController.text, + name: _nameController.text, + imageUrl: _imageUrl!, + createdAt: DateTime.now(), + updatedAt: DateTime.now()); + await categoryProvider.addCategory(category); + setState(() { + _isLoading = false; + }); + router.go(Routes.categories); + } + + void _onReset() { + setState(() { + _fileName = 'File Name'; + _imageData = null; + _imageUrl = null; + _descriptionController.clear(); + _nameController.clear(); + }); + } + + @override + Widget build(BuildContext context) { + return !_isLoading + ? Scaffold( + backgroundColor: AppTheme.colorWhite, + body: Padding( + padding: AppTheme.paddingSmall, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + children: [ + const Spacing(size: AppTheme.spacingLarge), + const BreadcrumbTabHeader( + goBackRoute: Routes.categories, + mainTitle: 'Categories', + secondaryTitle: 'Add Category'), + const Spacing(size: AppTheme.spacingSmall), + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Image*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + Row( + children: [ + Container( + height: 40, + width: 200, + padding: AppTheme.paddingTiny, + decoration: AppTheme.cardDecoration, + child: Text( + _fileName, + style: AppTheme.fontStyleSmall + .copyWith(color: AppTheme.colorGrey), + ), + ), + const Spacing( + size: AppTheme.spacingTiny, + isHorizontal: true), + CustomButton( + onTap: _pickImage, + text: 'Upload Image', + fillColor: AppTheme.colorRed, + textColor: AppTheme.colorWhite, + ), + ], + ), + ], + ), + const Spacing( + size: AppTheme.spacingMedium, isHorizontal: true), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Name*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + SizedBox( + width: 600, + child: TextField( + controller: _nameController, + decoration: InputDecoration( + hintText: + 'Enter the Category description here', + hintStyle: + AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ), + ], + ), + ], + ), + const Spacing(size: AppTheme.spacingMedium), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Description*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + TextField( + controller: _descriptionController, + decoration: InputDecoration( + hintText: 'Enter the Category description here', + hintStyle: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ], + ), + ], + ), + AddTabFooter( + goBackrouteName: Routes.categories, + onAdd: _addCategory, + onReset: _onReset, + buttonText: 'Add Category', + ), + ], + ), + ), + ) + : const CustomLoading(); + } +} diff --git a/sharekhan_admin_panel/lib/screens/categories/categories_screen.dart b/sharekhan_admin_panel/lib/screens/categories/categories_screen.dart new file mode 100644 index 00000000..a4dd215e --- /dev/null +++ b/sharekhan_admin_panel/lib/screens/categories/categories_screen.dart @@ -0,0 +1,169 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; +import 'package:syncfusion_flutter_datagrid/datagrid.dart'; +import 'package:sharekhan_admin_panel/models/category_model.dart'; +import 'package:sharekhan_admin_panel/navigation/go_router.dart'; +import 'package:sharekhan_admin_panel/navigation/routes.dart'; +import 'package:sharekhan_admin_panel/providers/category_provider.dart'; +import 'package:sharekhan_admin_panel/tables/category_data_source.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_button.dart'; +import 'package:sharekhan_admin_panel/widgets/common/main_tab_header.dart'; +import 'package:sharekhan_admin_panel/widgets/common/spacing.dart'; +import 'package:csv/csv.dart'; +// ignore: avoid_web_libraries_in_flutter +import 'dart:html' as html; + +class CategoriesScreen extends StatefulWidget { + const CategoriesScreen({super.key}); + + @override + State createState() => _CategoriesScreenState(); +} + +class _CategoriesScreenState extends State { + void _onEdit(CategoryModel category) { + context.go(Routes.categoryEdit, extra: category); + } + + void _onDelete(CategoryModel category) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Delete Category', style: AppTheme.fontStyleMedium), + content: const Text( + 'Are you sure you want to delete this category?', + style: AppTheme.fontStyleDefault, + ), + shape: AppTheme.rrShape, + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CustomButton( + onTap: () => router.pop(), + text: 'No, Cancel', + fillColor: AppTheme.colorBlack, + textColor: AppTheme.colorWhite, + ), + const Spacing(size: AppTheme.spacingSmall, isHorizontal: true), + CustomButton( + onTap: () async { + final categoryProvider = context.read(); + categoryProvider.deleteCategory(category); + router.pop(); + }, + text: 'Yes, Delete', + fillColor: AppTheme.colorRed, + textColor: AppTheme.colorWhite, + ), + ], + ), + ], + ); + }, + ); + } + + Future _exportToCSV() async { + final categoryProvider = context.read(); + List> rows = []; + rows.add(["Image URL", "Name", "Description", "Created At", "Updated At"]); + for (var category in categoryProvider.categories) { + rows.add([ + category.imageUrl, + category.name, + category.description, + category.createdAt, + category.updatedAt, + ]); + } + + String csv = const ListToCsvConverter().convert(rows); + final bytes = utf8.encode(csv); + final blob = html.Blob([bytes]); + final url = html.Url.createObjectUrlFromBlob(blob); + html.AnchorElement(href: url) + ..setAttribute("download", "categories.csv") + ..click(); + html.Url.revokeObjectUrl(url); + } + + @override + Widget build(BuildContext context) { + final categoryProvider = context.watch(); + return Scaffold( + backgroundColor: AppTheme.colorWhite, + body: Padding( + padding: AppTheme.paddingSmall, + child: Column( + children: [ + const Spacing(size: AppTheme.spacingLarge), + MainTabHeader( + title: 'Categories', + routeName: Routes.categoryAdd, + secondButtonText: 'Add Category', + onExport: _exportToCSV, + ), + const Spacing(size: AppTheme.spacingLarge), + Expanded( + child: SfDataGrid( + source: CategoryDataSource( + categories: categoryProvider.categories, + onEdit: _onEdit, + onDelete: _onDelete, + ), + columnWidthMode: ColumnWidthMode.fill, + gridLinesVisibility: GridLinesVisibility.both, + headerGridLinesVisibility: GridLinesVisibility.both, + columns: [ + GridColumn( + columnName: 'imageUrl', + width: 100, + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: + const Text('Image', style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'name', + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: + const Text('Name', style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'description', + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: const Text('Description', + style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'actions', + width: 100, + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: const Text('Actions', + style: AppTheme.fontStyleDefault), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/sharekhan_admin_panel/lib/screens/categories/edit_category_screen.dart b/sharekhan_admin_panel/lib/screens/categories/edit_category_screen.dart new file mode 100644 index 00000000..9eb64bba --- /dev/null +++ b/sharekhan_admin_panel/lib/screens/categories/edit_category_screen.dart @@ -0,0 +1,218 @@ +import 'dart:typed_data'; +import 'package:sharekhan_admin_panel/globals.dart'; +import 'package:sharekhan_admin_panel/models/category_model.dart'; +import 'package:sharekhan_admin_panel/navigation/go_router.dart'; +import 'package:sharekhan_admin_panel/navigation/routes.dart'; +import 'package:sharekhan_admin_panel/providers/category_provider.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_loading.dart'; +import 'package:sharekhan_admin_panel/widgets/common/edit_tab_footer.dart'; +import 'package:sharekhan_admin_panel/widgets/common/secondary_tab_header.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:image_picker_web/image_picker_web.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_button.dart'; +import 'package:sharekhan_admin_panel/widgets/common/spacing.dart'; + +class EditCategoryScreen extends StatefulWidget { + final CategoryModel category; + const EditCategoryScreen({super.key, required this.category}); + + @override + State createState() => _EditCategoryScreenState(); +} + +class _EditCategoryScreenState extends State { + String _fileName = 'File Name'; + Uint8List? _imageData; + String? _imageUrl; + final TextEditingController _descriptionController = TextEditingController(); + final TextEditingController _nameController = TextEditingController(); + bool _isLoading = false; + + Future _pickImage() async { + final MediaInfo? pickedFile = await ImagePickerWeb.getImageInfo(); + if (pickedFile != null) { + setState(() { + _imageData = pickedFile.data; + _fileName = pickedFile.fileName!; + }); + } + } + + Future _uploadImage(Uint8List imageData, String categoryID) async { + final storageRef = firebaseStorage.ref(); + final imagesRef = storageRef.child('categories/$categoryID.png'); + await imagesRef.putData(imageData); + final imageUrl = await imagesRef.getDownloadURL(); + return imageUrl; + } + + _updateCategory() async { + setState(() { + _isLoading = true; + }); + final categoryProvider = context.read(); + if (_imageData != null) { + _imageUrl = await _uploadImage(_imageData!, widget.category.id); + } + + CategoryModel category = CategoryModel( + id: widget.category.id, + description: _descriptionController.text, + name: _nameController.text, + imageUrl: _imageUrl!, + createdAt: widget.category.createdAt, + updatedAt: DateTime.now()); + await categoryProvider.updateCategory(category); + setState(() { + _isLoading = false; + }); + router.go(Routes.categories); + } + + init() { + setState(() { + _fileName = widget.category.imageUrl; + _nameController.text = widget.category.name; + _descriptionController.text = widget.category.description; + _imageUrl = widget.category.imageUrl; + }); + } + + @override + void initState() { + super.initState(); + init(); + } + + @override + Widget build(BuildContext context) { + return !_isLoading + ? Scaffold( + backgroundColor: AppTheme.colorWhite, + body: Padding( + padding: AppTheme.paddingSmall, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + children: [ + const Spacing(size: AppTheme.spacingLarge), + const BreadcrumbTabHeader( + goBackRoute: Routes.categories, + mainTitle: 'Categories', + secondaryTitle: 'Edit Category'), + const Spacing(size: AppTheme.spacingSmall), + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Image*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + Row( + children: [ + Container( + height: 40, + width: 200, + padding: AppTheme.paddingTiny, + decoration: AppTheme.cardDecoration, + child: Text( + _fileName, + style: AppTheme.fontStyleSmall + .copyWith(color: AppTheme.colorGrey), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const Spacing( + size: AppTheme.spacingTiny, + isHorizontal: true), + CustomButton( + onTap: _pickImage, + text: 'Upload Image', + fillColor: AppTheme.colorRed, + textColor: AppTheme.colorWhite, + ), + ], + ), + ], + ), + const Spacing( + size: AppTheme.spacingMedium, isHorizontal: true), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Name*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + SizedBox( + width: 600, + child: TextField( + controller: _nameController, + decoration: InputDecoration( + hintText: + 'Enter the Category description here', + hintStyle: + AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ), + ], + ), + ], + ), + const Spacing(size: AppTheme.spacingMedium), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Description*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + TextField( + controller: _descriptionController, + decoration: InputDecoration( + hintText: 'Enter the Category description here', + hintStyle: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ], + ), + ], + ), + EditTabFooter( + goBackrouteName: Routes.categories, + onSave: _updateCategory, + ), + ], + ), + ), + ) + : const CustomLoading(); + } +} diff --git a/sharekhan_admin_panel/lib/screens/coupons/add_coupon.dart b/sharekhan_admin_panel/lib/screens/coupons/add_coupon.dart new file mode 100644 index 00000000..42075a18 --- /dev/null +++ b/sharekhan_admin_panel/lib/screens/coupons/add_coupon.dart @@ -0,0 +1,220 @@ +import 'package:sharekhan_admin_panel/models/coupon_model.dart'; +import 'package:sharekhan_admin_panel/navigation/go_router.dart'; +import 'package:sharekhan_admin_panel/navigation/routes.dart'; +import 'package:sharekhan_admin_panel/providers/coupon_provider.dart'; +import 'package:sharekhan_admin_panel/widgets/common/add_tab_footer.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_loading.dart'; +import 'package:sharekhan_admin_panel/widgets/common/secondary_tab_header.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:sharekhan_admin_panel/utils/get_uuid.dart'; +import 'package:sharekhan_admin_panel/widgets/common/spacing.dart'; + +class AddCouponScreen extends StatefulWidget { + const AddCouponScreen({super.key}); + + @override + State createState() => _AddCouponScreenState(); +} + +class _AddCouponScreenState extends State { + final TextEditingController _couponCodeController = TextEditingController(); + final TextEditingController _descriptionController = TextEditingController(); + final TextEditingController _percentageController = TextEditingController(); + final TextEditingController _maxAmountController = TextEditingController(); + bool _isLoading = false; + + _addCoupon() async { + setState(() { + _isLoading = true; + }); + final couponProvider = context.read(); + + String couponID = getUUID(); + + CouponModel coupon = CouponModel( + id: couponID, + couponCode: _couponCodeController.text.toUpperCase(), + description: _descriptionController.text, + percentage: double.parse(_percentageController.text), + maxAmount: int.parse(_maxAmountController.text), + userIDs: [], + isActive: true, + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + ); + + await couponProvider.addCoupon(coupon); + setState(() { + _isLoading = false; + }); + router.go(Routes.coupons); + } + + void _onReset() { + setState(() { + _couponCodeController.clear(); + _descriptionController.clear(); + _percentageController.clear(); + _maxAmountController.clear(); + }); + } + + @override + Widget build(BuildContext context) { + return !_isLoading + ? Scaffold( + backgroundColor: AppTheme.colorWhite, + body: Padding( + padding: AppTheme.paddingSmall, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + children: [ + const Spacing(size: AppTheme.spacingLarge), + const BreadcrumbTabHeader( + goBackRoute: Routes.coupons, + mainTitle: 'Coupons', + secondaryTitle: 'Add Coupon', + ), + const Spacing(size: AppTheme.spacingSmall), + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Coupon Code*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + SizedBox( + width: 300, + child: TextField( + controller: _couponCodeController, + decoration: InputDecoration( + hintText: 'Enter the Coupon code here', + hintStyle: + AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ), + ], + ), + const Spacing( + size: AppTheme.spacingMedium, isHorizontal: true), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Percentage*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + SizedBox( + width: 200, + child: TextField( + controller: _percentageController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: 'Enter percentage', + hintStyle: + AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ), + ], + ), + const Spacing( + size: AppTheme.spacingMedium, isHorizontal: true), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Max Amount*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + SizedBox( + width: 200, + child: TextField( + controller: _maxAmountController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: 'Enter max amount', + hintStyle: + AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ), + ], + ), + ], + ), + const Spacing(size: AppTheme.spacingMedium), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Description*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + TextField( + controller: _descriptionController, + decoration: InputDecoration( + hintText: 'Enter the Coupon description here', + hintStyle: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ], + ), + ], + ), + AddTabFooter( + goBackrouteName: Routes.coupons, + onAdd: _addCoupon, + onReset: _onReset, + buttonText: 'Add Coupon', + ), + ], + ), + ), + ) + : const CustomLoading(); + } +} diff --git a/sharekhan_admin_panel/lib/screens/coupons/coupons_screen.dart b/sharekhan_admin_panel/lib/screens/coupons/coupons_screen.dart new file mode 100644 index 00000000..25ea0ced --- /dev/null +++ b/sharekhan_admin_panel/lib/screens/coupons/coupons_screen.dart @@ -0,0 +1,205 @@ +import 'dart:convert'; +import 'package:sharekhan_admin_panel/providers/coupon_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; +import 'package:syncfusion_flutter_datagrid/datagrid.dart'; +import 'package:sharekhan_admin_panel/models/coupon_model.dart'; +import 'package:sharekhan_admin_panel/navigation/go_router.dart'; +import 'package:sharekhan_admin_panel/navigation/routes.dart'; +import 'package:sharekhan_admin_panel/tables/coupon_data_source.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_button.dart'; +import 'package:sharekhan_admin_panel/widgets/common/main_tab_header.dart'; +import 'package:sharekhan_admin_panel/widgets/common/spacing.dart'; +import 'package:csv/csv.dart'; +// ignore: avoid_web_libraries_in_flutter +import 'dart:html' as html; + +class CouponsScreen extends StatefulWidget { + const CouponsScreen({super.key}); + + @override + State createState() => _CouponsScreenState(); +} + +class _CouponsScreenState extends State { + void _onEdit(CouponModel coupon) { + context.go(Routes.couponEdit, extra: coupon); + } + + void _onDelete(CouponModel coupon) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Delete Coupon', style: AppTheme.fontStyleMedium), + content: const Text( + 'Are you sure you want to delete this coupon?', + style: AppTheme.fontStyleDefault, + ), + shape: AppTheme.rrShape, + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CustomButton( + onTap: () => router.pop(), + text: 'No, Cancel', + fillColor: AppTheme.colorBlack, + textColor: AppTheme.colorWhite, + ), + const Spacing(size: AppTheme.spacingSmall, isHorizontal: true), + CustomButton( + onTap: () async { + final couponProvider = context.read(); + couponProvider.deleteCoupon(coupon); + router.pop(); + }, + text: 'Yes, Delete', + fillColor: AppTheme.colorRed, + textColor: AppTheme.colorWhite, + ), + ], + ), + ], + ); + }, + ); + } + + void _onToggleActive(CouponModel coupon, bool isActive) async { + final couponProvider = context.read(); + await couponProvider.toggleActive(coupon, isActive); + } + + Future _exportToCSV() async { + final couponProvider = context.read(); + List> rows = []; + rows.add([ + "Coupon Code", + "Description", + "Max Amount", + "Percentage", + "Is Active", + "Created At", + "Updated At" + ]); + for (var coupon in couponProvider.coupons) { + rows.add([ + coupon.couponCode, + coupon.description, + coupon.maxAmount, + coupon.percentage, + coupon.isActive, + coupon.createdAt, + coupon.updatedAt, + ]); + } + + String csv = const ListToCsvConverter().convert(rows); + final bytes = utf8.encode(csv); + final blob = html.Blob([bytes]); + final url = html.Url.createObjectUrlFromBlob(blob); + html.AnchorElement(href: url) + ..setAttribute("download", "coupons.csv") + ..click(); + html.Url.revokeObjectUrl(url); + } + + @override + Widget build(BuildContext context) { + final couponProvider = context.watch(); + return Scaffold( + backgroundColor: AppTheme.colorWhite, + body: Padding( + padding: AppTheme.paddingSmall, + child: Column( + children: [ + const Spacing(size: AppTheme.spacingLarge), + MainTabHeader( + title: 'Coupons', + routeName: Routes.couponAdd, + secondButtonText: 'Add Coupon', + onExport: _exportToCSV, + ), + const Spacing(size: AppTheme.spacingLarge), + Expanded( + child: SfDataGrid( + source: CouponDataSource( + coupons: couponProvider.coupons, + onEdit: _onEdit, + onDelete: _onDelete, + onToggleActive: _onToggleActive, + ), + columnWidthMode: ColumnWidthMode.fill, + gridLinesVisibility: GridLinesVisibility.both, + headerGridLinesVisibility: GridLinesVisibility.both, + columns: [ + GridColumn( + columnName: 'couponCode', + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: const Text('Coupon Code', + style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'description', + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: const Text('Description', + style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + width: 200, + columnName: 'maxAmount', + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: const Text('Max Amount', + style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + width: 100, + columnName: 'percentage', + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: const Text('Percentage', + style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'isActive', + width: 100, + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: + const Text('Show', style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'actions', + width: 100, + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: const Text('Actions', + style: AppTheme.fontStyleDefault), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/sharekhan_admin_panel/lib/screens/coupons/edit_coupon.dart b/sharekhan_admin_panel/lib/screens/coupons/edit_coupon.dart new file mode 100644 index 00000000..e1e6fa3d --- /dev/null +++ b/sharekhan_admin_panel/lib/screens/coupons/edit_coupon.dart @@ -0,0 +1,216 @@ +import 'package:sharekhan_admin_panel/models/coupon_model.dart'; +import 'package:sharekhan_admin_panel/navigation/go_router.dart'; +import 'package:sharekhan_admin_panel/navigation/routes.dart'; +import 'package:sharekhan_admin_panel/providers/coupon_provider.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_loading.dart'; +import 'package:sharekhan_admin_panel/widgets/common/edit_tab_footer.dart'; +import 'package:sharekhan_admin_panel/widgets/common/secondary_tab_header.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:sharekhan_admin_panel/widgets/common/spacing.dart'; + +class EditCouponScreen extends StatefulWidget { + final CouponModel coupon; + const EditCouponScreen({super.key, required this.coupon}); + + @override + State createState() => _EditCouponScreenState(); +} + +class _EditCouponScreenState extends State { + final TextEditingController _couponCodeController = TextEditingController(); + final TextEditingController _descriptionController = TextEditingController(); + final TextEditingController _percentageController = TextEditingController(); + final TextEditingController _maxAmountController = TextEditingController(); + bool _isLoading = false; + + _editCoupon() async { + setState(() { + _isLoading = true; + }); + final couponProvider = context.read(); + + CouponModel coupon = CouponModel( + id: widget.coupon.id, + couponCode: _couponCodeController.text.toUpperCase(), + description: _descriptionController.text, + percentage: double.parse(_percentageController.text), + maxAmount: int.parse(_maxAmountController.text), + userIDs: [], + isActive: true, + createdAt: widget.coupon.createdAt, + updatedAt: DateTime.now(), + ); + + await couponProvider.updateCoupon(coupon); + setState(() { + _isLoading = false; + }); + router.go(Routes.coupons); + } + + @override + void initState() { + _couponCodeController.text = widget.coupon.couponCode; + _descriptionController.text = widget.coupon.description; + _percentageController.text = widget.coupon.percentage.toString(); + _maxAmountController.text = widget.coupon.maxAmount.toString(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return !_isLoading + ? Scaffold( + backgroundColor: AppTheme.colorWhite, + body: Padding( + padding: AppTheme.paddingSmall, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + children: [ + const Spacing(size: AppTheme.spacingLarge), + const BreadcrumbTabHeader( + goBackRoute: Routes.coupons, + mainTitle: 'Coupons', + secondaryTitle: 'Edit Coupons', + ), + const Spacing(size: AppTheme.spacingSmall), + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Coupon Code*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + SizedBox( + width: 300, + child: TextField( + controller: _couponCodeController, + decoration: InputDecoration( + hintText: 'Enter the Coupon code here', + hintStyle: + AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ), + ], + ), + const Spacing( + size: AppTheme.spacingMedium, isHorizontal: true), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Percentage*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + SizedBox( + width: 200, + child: TextField( + controller: _percentageController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: 'Enter percentage', + hintStyle: + AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ), + ], + ), + const Spacing( + size: AppTheme.spacingMedium, isHorizontal: true), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Max Amount*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + SizedBox( + width: 200, + child: TextField( + controller: _maxAmountController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: 'Enter max amount', + hintStyle: + AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ), + ], + ), + ], + ), + const Spacing(size: AppTheme.spacingMedium), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Description*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + TextField( + controller: _descriptionController, + decoration: InputDecoration( + hintText: 'Enter the Coupon description here', + hintStyle: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ], + ), + ], + ), + EditTabFooter( + goBackrouteName: Routes.coupons, + onSave: _editCoupon, + ), + ], + ), + ), + ) + : const CustomLoading(); + } +} diff --git a/sharekhan_admin_panel/lib/screens/main_screen.dart b/sharekhan_admin_panel/lib/screens/main_screen.dart new file mode 100644 index 00000000..37eb08fb --- /dev/null +++ b/sharekhan_admin_panel/lib/screens/main_screen.dart @@ -0,0 +1,363 @@ +import 'package:sharekhan_admin_panel/constants/asset_constants.dart'; +import 'package:sharekhan_admin_panel/enums/enums.dart'; +import 'package:sharekhan_admin_panel/navigation/routes.dart'; +import 'package:sharekhan_admin_panel/providers/banner_provider.dart'; +import 'package:sharekhan_admin_panel/providers/category_provider.dart'; +import 'package:sharekhan_admin_panel/providers/coupon_provider.dart'; +import 'package:sharekhan_admin_panel/providers/product_provider.dart'; +import 'package:sharekhan_admin_panel/providers/setting_provider.dart'; +import 'package:sharekhan_admin_panel/providers/user_provider.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_icon.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_loading.dart'; +import 'package:sharekhan_admin_panel/widgets/common/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; + +class MainScreen extends StatefulWidget { + final Widget child; + const MainScreen({super.key, required this.child}); + + @override + State createState() => _MainScreenState(); +} + +class _MainScreenState extends State { + bool _isLoading = true; + ScreenType _selectedScreen = ScreenType.banners; + + void _onTabSelected(ScreenType screen) { + setState(() { + _selectedScreen = screen; + }); + switch (screen) { + case ScreenType.banners: + context.go(Routes.banners); + break; + case ScreenType.sheruProducts: + context.go(Routes.sheruProducts); + break; + case ScreenType.sellerProducts: + context.go(Routes.sellerProducts); + break; + case ScreenType.categories: + context.go(Routes.categories); + break; + case ScreenType.coupons: + context.go(Routes.coupons); + break; + case ScreenType.settings: + context.go(Routes.settings); + break; + case ScreenType.users: + context.go(Routes.users); + break; + } + } + + init() async { + final bannerProvider = context.read(); + final categoryProvider = context.read(); + final couponProvider = context.read(); + final productProvider = context.read(); + final settingProvider = context.read(); + final userProvider = context.read(); + await userProvider.fetchUsers(); + await settingProvider.fetchSettings(); + await productProvider.fetchProducts(); + await couponProvider.fetchCoupons(); + await bannerProvider.fetchBanners(); + await categoryProvider.fetchCategories(); + setState(() { + _isLoading = false; + }); + } + + @override + void initState() { + super.initState(); + init(); + } + + @override + Widget build(BuildContext context) { + return !_isLoading + ? Scaffold( + body: Row( + children: [ + Container( + width: 200, + height: double.infinity, + color: AppTheme.colorWhite, + padding: AppTheme.paddingSmall, + child: SingleChildScrollView( + child: Column( + children: [ + Image.asset( + imageLogoMain, + width: 100, + ), + const Spacing(size: AppTheme.spacingSmall), + InkWell( + onTap: () { + _onTabSelected(ScreenType.banners); + }, + child: Container( + decoration: _selectedScreen == ScreenType.banners + ? BoxDecoration( + color: AppTheme.colorRed, + borderRadius: AppTheme.borderRadiusSmall, + ) + : null, + padding: AppTheme.paddingTiny, + child: Row( + children: [ + CustomIcon( + asset: iconTrial, + color: _selectedScreen == ScreenType.banners + ? AppTheme.colorWhite + : AppTheme.colorGrey, + ), + const Spacing( + size: AppTheme.spacingTiny, + isHorizontal: true), + Text('Banners', + style: AppTheme.fontStyleDefault.copyWith( + color: + _selectedScreen == ScreenType.banners + ? AppTheme.colorWhite + : AppTheme.colorGrey, + )), + ], + ), + ), + ), + const Spacing(size: AppTheme.spacingSmall), + InkWell( + onTap: () { + _onTabSelected(ScreenType.sheruProducts); + }, + child: Container( + decoration: _selectedScreen == + ScreenType.sheruProducts + ? BoxDecoration( + color: AppTheme.colorRed, + borderRadius: AppTheme.borderRadiusSmall, + ) + : null, + padding: AppTheme.paddingTiny, + child: Row( + children: [ + CustomIcon( + asset: iconTrial, + color: _selectedScreen == + ScreenType.sheruProducts + ? AppTheme.colorWhite + : AppTheme.colorGrey, + ), + const Spacing( + size: AppTheme.spacingTiny, + isHorizontal: true), + Text('Sheru Products', + style: AppTheme.fontStyleDefault.copyWith( + color: _selectedScreen == + ScreenType.sheruProducts + ? AppTheme.colorWhite + : AppTheme.colorGrey, + )), + ], + ), + ), + ), + const Spacing(size: AppTheme.spacingSmall), + InkWell( + onTap: () { + _onTabSelected(ScreenType.sellerProducts); + }, + child: Container( + decoration: _selectedScreen == + ScreenType.sellerProducts + ? BoxDecoration( + color: AppTheme.colorRed, + borderRadius: AppTheme.borderRadiusSmall, + ) + : null, + padding: AppTheme.paddingTiny, + child: Row( + children: [ + CustomIcon( + asset: iconTrial, + color: _selectedScreen == + ScreenType.sellerProducts + ? AppTheme.colorWhite + : AppTheme.colorGrey, + ), + const Spacing( + size: AppTheme.spacingTiny, + isHorizontal: true), + Text('Seller Products', + style: AppTheme.fontStyleDefault.copyWith( + color: _selectedScreen == + ScreenType.sellerProducts + ? AppTheme.colorWhite + : AppTheme.colorGrey, + )), + ], + ), + ), + ), + const Spacing(size: AppTheme.spacingSmall), + InkWell( + onTap: () { + _onTabSelected(ScreenType.users); + }, + child: Container( + decoration: _selectedScreen == ScreenType.users + ? BoxDecoration( + color: AppTheme.colorRed, + borderRadius: AppTheme.borderRadiusSmall, + ) + : null, + padding: AppTheme.paddingTiny, + child: Row( + children: [ + CustomIcon( + asset: iconTrial, + color: _selectedScreen == ScreenType.users + ? AppTheme.colorWhite + : AppTheme.colorGrey, + ), + const Spacing( + size: AppTheme.spacingTiny, + isHorizontal: true), + Text('Users', + style: AppTheme.fontStyleDefault.copyWith( + color: _selectedScreen == ScreenType.users + ? AppTheme.colorWhite + : AppTheme.colorGrey, + )), + ], + ), + ), + ), + const Spacing(size: AppTheme.spacingSmall), + InkWell( + onTap: () { + _onTabSelected(ScreenType.categories); + }, + child: Container( + decoration: _selectedScreen == ScreenType.categories + ? BoxDecoration( + color: AppTheme.colorRed, + borderRadius: AppTheme.borderRadiusSmall, + ) + : null, + padding: AppTheme.paddingTiny, + child: Row( + children: [ + CustomIcon( + asset: iconTrial, + color: + _selectedScreen == ScreenType.categories + ? AppTheme.colorWhite + : AppTheme.colorGrey, + ), + const Spacing( + size: AppTheme.spacingTiny, + isHorizontal: true), + Text('Categories', + style: AppTheme.fontStyleDefault.copyWith( + color: _selectedScreen == + ScreenType.categories + ? AppTheme.colorWhite + : AppTheme.colorGrey, + )), + ], + ), + ), + ), + const Spacing(size: AppTheme.spacingSmall), + InkWell( + onTap: () { + _onTabSelected(ScreenType.coupons); + }, + child: Container( + decoration: _selectedScreen == ScreenType.coupons + ? BoxDecoration( + color: AppTheme.colorRed, + borderRadius: AppTheme.borderRadiusSmall, + ) + : null, + padding: AppTheme.paddingTiny, + child: Row( + children: [ + CustomIcon( + asset: iconTrial, + color: _selectedScreen == ScreenType.coupons + ? AppTheme.colorWhite + : AppTheme.colorGrey, + ), + const Spacing( + size: AppTheme.spacingTiny, + isHorizontal: true), + Text('Coupons', + style: AppTheme.fontStyleDefault.copyWith( + color: + _selectedScreen == ScreenType.coupons + ? AppTheme.colorWhite + : AppTheme.colorGrey, + )), + ], + ), + ), + ), + const Spacing(size: AppTheme.spacingSmall), + InkWell( + onTap: () { + _onTabSelected(ScreenType.settings); + }, + child: Container( + decoration: _selectedScreen == ScreenType.settings + ? BoxDecoration( + color: AppTheme.colorRed, + borderRadius: AppTheme.borderRadiusSmall, + ) + : null, + padding: AppTheme.paddingTiny, + child: Row( + children: [ + CustomIcon( + asset: iconTrial, + color: _selectedScreen == ScreenType.settings + ? AppTheme.colorWhite + : AppTheme.colorGrey, + ), + const Spacing( + size: AppTheme.spacingTiny, + isHorizontal: true), + Text('Settings', + style: AppTheme.fontStyleDefault.copyWith( + color: + _selectedScreen == ScreenType.settings + ? AppTheme.colorWhite + : AppTheme.colorGrey, + )), + ], + ), + ), + ), + ], + ), + ), + ), + const Spacing(size: AppTheme.spacingSmall, isHorizontal: true), + Expanded( + child: widget.child, + ), + ], + ), + ) + : const CustomLoading(); + } +} diff --git a/sharekhan_admin_panel/lib/screens/products/add_product_screen.dart b/sharekhan_admin_panel/lib/screens/products/add_product_screen.dart new file mode 100644 index 00000000..be037836 --- /dev/null +++ b/sharekhan_admin_panel/lib/screens/products/add_product_screen.dart @@ -0,0 +1,424 @@ +import 'dart:typed_data'; +import 'package:sharekhan_admin_panel/globals.dart'; +import 'package:sharekhan_admin_panel/models/product_model.dart'; +import 'package:sharekhan_admin_panel/navigation/go_router.dart'; +import 'package:sharekhan_admin_panel/navigation/routes.dart'; +import 'package:sharekhan_admin_panel/providers/category_provider.dart'; +import 'package:sharekhan_admin_panel/providers/product_provider.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_dropdown.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_loading.dart'; +import 'package:sharekhan_admin_panel/widgets/common/add_tab_footer.dart'; +import 'package:sharekhan_admin_panel/widgets/common/secondary_tab_header.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:image_picker_web/image_picker_web.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:sharekhan_admin_panel/utils/get_uuid.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_button.dart'; +import 'package:sharekhan_admin_panel/widgets/common/spacing.dart'; + +class AddProductScreen extends StatefulWidget { + const AddProductScreen({super.key}); + + @override + State createState() => _AddProductScreenState(); +} + +class _AddProductScreenState extends State { + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _descriptionController = TextEditingController(); + final TextEditingController _unitWeightController = TextEditingController(); + final TextEditingController _unitPriceController = TextEditingController(); + final TextEditingController _remainingQuantityController = + TextEditingController(); + + List _categories = []; + final List _imageDataList = []; + final List _imageUrls = []; + final List _fileNames = []; + String _categoryID = ''; + bool _isLoading = false; + + Future _pickImage(int index) async { + final MediaInfo? pickedFile = await ImagePickerWeb.getImageInfo(); + if (pickedFile != null) { + setState(() { + if (index < _imageDataList.length) { + _imageDataList[index] = pickedFile.data; + _fileNames[index] = pickedFile.fileName!; + } else { + _imageDataList.add(pickedFile.data); + _fileNames.add(pickedFile.fileName!); + } + }); + } + } + + Future _uploadImage( + Uint8List imageData, String productID, int index) async { + final storageRef = firebaseStorage.ref(); + final imagesRef = storageRef.child('products/$productID-$index.png'); + await imagesRef.putData(imageData); + final imageUrl = await imagesRef.getDownloadURL(); + return imageUrl; + } + + _addProduct() async { + setState(() { + _isLoading = true; + }); + + final productProvider = context.read(); + String productID = getUUID(); + + for (int i = 0; i < _imageDataList.length; i++) { + if (_imageDataList[i] != null) { + final imageUrl = await _uploadImage(_imageDataList[i]!, productID, i); + _imageUrls.add(imageUrl); + } + } + + ProductModel product = ProductModel( + id: productID, + categoryID: _categoryID, + images: _imageUrls, + name: _nameController.text.trim(), + description: _descriptionController.text.trim(), + unitWeight: double.parse(_unitWeightController.text), + unitPrice: int.parse(_unitPriceController.text), + remainingQuantity: int.parse(_remainingQuantityController.text), + isActive: true, + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + sellerId: 'admin', + isSheruSpecial: true, + isApproved: true, + ); + + await productProvider.addProduct(product); + setState(() { + _isLoading = false; + }); + router.go(Routes.sheruProducts); + } + + void _onReset() { + setState(() { + _nameController.clear(); + _descriptionController.clear(); + _unitWeightController.clear(); + _unitPriceController.clear(); + _categoryID = ''; + _imageDataList.clear(); + _imageUrls.clear(); + _fileNames.clear(); + }); + } + + init() async { + final categoryProvider = context.read(); + setState(() { + _categories = categoryProvider.categories.map((e) => e.name).toList(); + }); + } + + @override + void initState() { + super.initState(); + init(); + } + + @override + Widget build(BuildContext context) { + final categoryProvider = context.watch(); + return !_isLoading + ? Scaffold( + backgroundColor: AppTheme.colorWhite, + body: Padding( + padding: AppTheme.paddingSmall, + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + children: [ + const Spacing(size: AppTheme.spacingLarge), + const BreadcrumbTabHeader( + goBackRoute: Routes.sheruProducts, + mainTitle: 'Products', + secondaryTitle: 'Add Products', + ), + const Spacing(size: AppTheme.spacingSmall), + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Product Name*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + SizedBox( + width: 300, + child: TextField( + controller: _nameController, + decoration: InputDecoration( + hintText: 'Enter the Product name here', + hintStyle: + AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ), + ], + ), + const Spacing( + size: AppTheme.spacingMedium, + isHorizontal: true), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Category ID*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + SizedBox( + width: 300, + child: CustomDropdown( + hintText: 'Choose an option', + value: _categoryID.isNotEmpty + ? categoryProvider.categories + .firstWhere((element) => + element.id == _categoryID) + .name + : null, + items: _categories + .map((e) => DropdownMenuItem( + value: e, + child: Text(e), + )) + .toList(), + onChanged: (val) { + setState(() { + _categoryID = categoryProvider + .categories + .firstWhere((element) => + element.name == val) + .id; + }); + }, + ), + ), + ], + ), + ], + ), + const Spacing(size: AppTheme.spacingMedium), + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Unit Weight*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + SizedBox( + width: 200, + child: TextField( + controller: _unitWeightController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: 'Enter weight', + hintStyle: + AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ), + ], + ), + const Spacing( + size: AppTheme.spacingMedium, + isHorizontal: true), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Unit Price*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + SizedBox( + width: 200, + child: TextField( + controller: _unitPriceController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: 'Enter price', + hintStyle: + AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ), + ], + ), + const Spacing( + size: AppTheme.spacingMedium, + isHorizontal: true), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Remaining Quantity*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + SizedBox( + width: 200, + child: TextField( + controller: _remainingQuantityController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: 'Enter quantity', + hintStyle: + AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ), + ], + ), + ], + ), + const Spacing(size: AppTheme.spacingMedium), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Description*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + TextField( + controller: _descriptionController, + decoration: InputDecoration( + hintText: 'Enter the Product description here', + hintStyle: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ], + ), + const Spacing(size: AppTheme.spacingMedium), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; + i < (_imageDataList.length + 1); + i++) + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + 'Image ${i + 1}${i == 0 ? '*' : ''}', + style: AppTheme.fontStyleDefault + .copyWith( + color: AppTheme.colorGrey), + ), + const Spacing( + size: AppTheme.spacingTiny), + Container( + height: 40, + width: 300, + padding: AppTheme.paddingTiny, + decoration: AppTheme.cardDecoration, + child: Text( + i < _imageDataList.length + ? _fileNames[i] + : 'File Name', + style: AppTheme.fontStyleSmall + .copyWith( + color: AppTheme.colorGrey), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const Spacing( + size: AppTheme.spacingTiny, + isHorizontal: true), + CustomButton( + onTap: () => _pickImage(i), + text: 'Upload Image', + fillColor: AppTheme.colorRed, + textColor: AppTheme.colorWhite, + ), + ], + ), + ), + ], + ), + ], + ), + AddTabFooter( + goBackrouteName: Routes.sheruProducts, + onAdd: _addProduct, + onReset: _onReset, + buttonText: 'Add Product', + ), + ], + ), + ), + ), + ) + : const CustomLoading(); + } +} diff --git a/sharekhan_admin_panel/lib/screens/products/edit_product_screen.dart b/sharekhan_admin_panel/lib/screens/products/edit_product_screen.dart new file mode 100644 index 00000000..2756c620 --- /dev/null +++ b/sharekhan_admin_panel/lib/screens/products/edit_product_screen.dart @@ -0,0 +1,414 @@ +import 'dart:typed_data'; +import 'package:sharekhan_admin_panel/globals.dart'; +import 'package:sharekhan_admin_panel/models/product_model.dart'; +import 'package:sharekhan_admin_panel/navigation/go_router.dart'; +import 'package:sharekhan_admin_panel/navigation/routes.dart'; +import 'package:sharekhan_admin_panel/providers/category_provider.dart'; +import 'package:sharekhan_admin_panel/providers/product_provider.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_dropdown.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_loading.dart'; +import 'package:sharekhan_admin_panel/widgets/common/edit_tab_footer.dart'; +import 'package:sharekhan_admin_panel/widgets/common/secondary_tab_header.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:image_picker_web/image_picker_web.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_button.dart'; +import 'package:sharekhan_admin_panel/widgets/common/spacing.dart'; + +class EditProductScreen extends StatefulWidget { + final ProductModel product; + const EditProductScreen({super.key, required this.product}); + + @override + State createState() => _EditProductScreenState(); +} + +class _EditProductScreenState extends State { + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _descriptionController = TextEditingController(); + final TextEditingController _unitWeightController = TextEditingController(); + final TextEditingController _unitPriceController = TextEditingController(); + final TextEditingController _remainingQuantityController = + TextEditingController(); + + List _categories = []; + final List _imageDataList = []; + final List _imageUrls = []; + final List _fileNames = []; + String? _categoryID; + bool _isLoading = false; + + init() async { + final categoryProvider = context.read(); + setState(() { + _categories = categoryProvider.categories.map((e) => e.name).toList(); + _nameController.text = widget.product.name; + _descriptionController.text = widget.product.description; + _unitWeightController.text = widget.product.unitWeight.toString(); + _unitPriceController.text = widget.product.unitPrice.toString(); + _remainingQuantityController.text = + widget.product.remainingQuantity.toString(); + _categoryID = widget.product.categoryID; + _imageUrls.addAll(widget.product.images); + _fileNames.addAll(List.generate( + _imageUrls.length, (index) => 'Image ${index + 1}.png')); + }); + } + + @override + void initState() { + super.initState(); + init(); + } + + Future _pickImage(int index) async { + final MediaInfo? pickedFile = await ImagePickerWeb.getImageInfo(); + if (pickedFile != null) { + setState(() { + if (index < _imageDataList.length) { + _imageDataList[index] = pickedFile.data; + _fileNames[index] = pickedFile.fileName!; + } else { + _imageDataList.add(pickedFile.data); + _fileNames.add(pickedFile.fileName!); + } + }); + } + } + + Future _uploadImage( + Uint8List imageData, String productID, int index) async { + final storageRef = firebaseStorage.ref(); + final imagesRef = storageRef.child('products/$productID-$index.png'); + await imagesRef.putData(imageData); + final imageUrl = await imagesRef.getDownloadURL(); + return imageUrl; + } + + _editProduct() async { + setState(() { + _isLoading = true; + }); + + final productProvider = context.read(); + String productID = widget.product.id; + for (int i = 0; i < _imageDataList.length; i++) { + if (_imageDataList[i] != null) { + final imageUrl = await _uploadImage(_imageDataList[i]!, productID, i); + _imageUrls[i] = imageUrl; + } + } + + ProductModel updatedProduct = ProductModel( + id: productID, + categoryID: _categoryID ?? '', + images: _imageUrls, + name: _nameController.text.trim(), + description: _descriptionController.text.trim(), + unitWeight: double.parse(_unitWeightController.text), + unitPrice: int.parse(_unitPriceController.text), + remainingQuantity: int.parse(_remainingQuantityController.text), + isActive: widget.product.isActive, + createdAt: widget.product.createdAt, + updatedAt: DateTime.now(), + sellerId: widget.product.sellerId, + isSheruSpecial: widget.product.isSheruSpecial, + isApproved: widget.product.isApproved, + ); + + await productProvider.updateProduct(updatedProduct); + setState(() { + _isLoading = false; + }); + router.go(Routes.sheruProducts); + } + + @override + Widget build(BuildContext context) { + final categoryProvider = context.watch(); + return !_isLoading + ? Scaffold( + backgroundColor: AppTheme.colorWhite, + body: Padding( + padding: AppTheme.paddingSmall, + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + children: [ + const Spacing(size: AppTheme.spacingLarge), + const BreadcrumbTabHeader( + goBackRoute: Routes.sheruProducts, + mainTitle: 'Products', + secondaryTitle: 'Edit Products', + ), + const Spacing(size: AppTheme.spacingSmall), + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Product Name*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + SizedBox( + width: 300, + child: TextField( + controller: _nameController, + decoration: InputDecoration( + hintText: 'Enter the Product name here', + hintStyle: + AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ), + ], + ), + const Spacing( + size: AppTheme.spacingMedium, + isHorizontal: true), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Category ID*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + SizedBox( + width: 300, + child: CustomDropdown( + hintText: 'Choose an option', + value: categoryProvider.categories + .firstWhere((e) => e.id == _categoryID, + orElse: () => + categoryProvider.categories[0]) + .name, + items: _categories + .map((e) => DropdownMenuItem( + value: e, + child: Text(e), + )) + .toList(), + onChanged: (val) { + setState(() { + _categoryID = categoryProvider + .categories + .firstWhere((e) => e.name == val) + .id; + }); + }, + ), + ), + ], + ), + ], + ), + const Spacing(size: AppTheme.spacingMedium), + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Unit Weight*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + SizedBox( + width: 200, + child: TextField( + controller: _unitWeightController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: 'Enter weight', + hintStyle: + AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ), + ], + ), + const Spacing( + size: AppTheme.spacingMedium, + isHorizontal: true), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Unit Price*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + SizedBox( + width: 200, + child: TextField( + controller: _unitPriceController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: 'Enter price', + hintStyle: + AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ), + ], + ), + const Spacing( + size: AppTheme.spacingMedium, + isHorizontal: true), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Remaining Quantity*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + SizedBox( + width: 200, + child: TextField( + controller: _remainingQuantityController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: 'Enter quantity', + hintStyle: + AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ), + ], + ), + ], + ), + const Spacing(size: AppTheme.spacingMedium), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Description*', + style: AppTheme.fontStyleDefault + .copyWith(color: AppTheme.colorGrey), + ), + const Spacing(size: AppTheme.spacingTiny), + TextField( + controller: _descriptionController, + decoration: InputDecoration( + hintText: 'Enter the Product description here', + hintStyle: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ], + ), + const Spacing(size: AppTheme.spacingMedium), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (int i = 0; i < (_imageUrls.length + 1); i++) + Padding( + padding: const EdgeInsets.only(bottom: 16.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + 'Image ${i + 1}${i == 0 ? '*' : ''}', + style: AppTheme.fontStyleDefault + .copyWith( + color: AppTheme.colorGrey), + ), + const Spacing( + size: AppTheme.spacingTiny), + Container( + height: 40, + width: 300, + padding: AppTheme.paddingTiny, + decoration: AppTheme.cardDecoration, + child: Text( + i < _imageUrls.length + ? _fileNames[i] + : 'File Name', + style: AppTheme.fontStyleSmall + .copyWith( + color: AppTheme.colorGrey), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const Spacing( + size: AppTheme.spacingTiny, + isHorizontal: true), + CustomButton( + onTap: () => _pickImage(i), + text: 'Upload Image', + fillColor: AppTheme.colorRed, + textColor: AppTheme.colorWhite, + ), + ], + ), + ), + ], + ), + ], + ), + EditTabFooter( + goBackrouteName: Routes.sheruProducts, + onSave: _editProduct, + ), + ], + ), + ), + ), + ) + : const CustomLoading(); + } +} diff --git a/sharekhan_admin_panel/lib/screens/products/products_screen.dart b/sharekhan_admin_panel/lib/screens/products/products_screen.dart new file mode 100644 index 00000000..dd4e3019 --- /dev/null +++ b/sharekhan_admin_panel/lib/screens/products/products_screen.dart @@ -0,0 +1,240 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; +import 'package:syncfusion_flutter_datagrid/datagrid.dart'; +import 'package:sharekhan_admin_panel/models/product_model.dart'; +import 'package:sharekhan_admin_panel/navigation/go_router.dart'; +import 'package:sharekhan_admin_panel/navigation/routes.dart'; +import 'package:sharekhan_admin_panel/providers/category_provider.dart'; +import 'package:sharekhan_admin_panel/providers/product_provider.dart'; +import 'package:sharekhan_admin_panel/tables/product_data_source.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_button.dart'; +import 'package:sharekhan_admin_panel/widgets/common/main_tab_header.dart'; +import 'package:sharekhan_admin_panel/widgets/common/spacing.dart'; +import 'package:csv/csv.dart'; +// ignore: avoid_web_libraries_in_flutter +import 'dart:html' as html; + +class ProductsScreen extends StatefulWidget { + const ProductsScreen({super.key}); + + @override + State createState() => _ProductsScreenState(); +} + +class _ProductsScreenState extends State { + void _onEdit(ProductModel product) { + context.go(Routes.productEdit, extra: product); + } + + void _onDelete(ProductModel product) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Delete Product', style: AppTheme.fontStyleMedium), + content: const Text( + 'Are you sure you want to delete this product?', + style: AppTheme.fontStyleDefault, + ), + shape: AppTheme.rrShape, + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CustomButton( + onTap: () => router.pop(), + text: 'No, Cancel', + fillColor: AppTheme.colorBlack, + textColor: AppTheme.colorWhite, + ), + const Spacing(size: AppTheme.spacingSmall, isHorizontal: true), + CustomButton( + onTap: () async { + final productProvider = context.read(); + productProvider.deleteProduct(product); + router.pop(); + }, + text: 'Yes, Delete', + fillColor: AppTheme.colorRed, + textColor: AppTheme.colorWhite, + ), + ], + ), + ], + ); + }, + ); + } + + void _onToggleActive(ProductModel product, bool isActive) async { + final productProvider = context.read(); + productProvider.updateProduct(product.copyWith(isActive: isActive)); + } + + Future _exportToCSV() async { + final productProvider = context.read(); + List> rows = []; + rows.add([ + "Image", + "Name", + "Category", + "Unit Weight", + "Unit Price", + "Remaining Quantity", + "Is Active", + "Created At", + "Updated At" + ]); + for (var product in productProvider.products) { + rows.add([ + product.images.isNotEmpty ? product.images.first : 'No Image', + product.name, + product.categoryID, + product.unitWeight, + product.unitPrice, + product.remainingQuantity, + product.isActive ? "Active" : "Inactive", + product.createdAt.toIso8601String(), + product.updatedAt.toIso8601String(), + ]); + } + + String csv = const ListToCsvConverter().convert(rows); + final bytes = utf8.encode(csv); + final blob = html.Blob([bytes]); + final url = html.Url.createObjectUrlFromBlob(blob); + html.AnchorElement(href: url) + ..setAttribute("download", "products.csv") + ..click(); + html.Url.revokeObjectUrl(url); + } + + @override + Widget build(BuildContext context) { + final productProvider = context.watch(); + final categoryProvider = context.watch(); + return Scaffold( + backgroundColor: AppTheme.colorWhite, + body: Padding( + padding: AppTheme.paddingSmall, + child: Column( + children: [ + const Spacing(size: AppTheme.spacingLarge), + MainTabHeader( + title: 'Products', + routeName: Routes.productAdd, + secondButtonText: 'Add Product', + onExport: _exportToCSV, + ), + const Spacing(size: AppTheme.spacingLarge), + Expanded( + child: SfDataGrid( + source: ProductDataSource( + products: productProvider.products + .where((product) => product.isSheruSpecial) + .toList(), + categories: categoryProvider.categories, + onEdit: _onEdit, + onDelete: _onDelete, + onToggleActive: _onToggleActive, + ), + columnWidthMode: ColumnWidthMode.fill, + gridLinesVisibility: GridLinesVisibility.both, + headerGridLinesVisibility: GridLinesVisibility.both, + columns: [ + GridColumn( + columnName: 'images', + width: 150, + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: + const Text('Image', style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'name', + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: + const Text('Name', style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'categoryID', + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: const Text('Category', + style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'description', + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: const Text('Description', + style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'unitWeight', + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: const Text('Unit Weight', + style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'unitPrice', + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: const Text('Unit Price', + style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'remainingQuantity', + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: const Text('Remaining Quantity', + style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'isActive', + width: 100, + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: + const Text('Show', style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'actions', + width: 100, + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: const Text('Actions', + style: AppTheme.fontStyleDefault), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/sharekhan_admin_panel/lib/screens/sellerPoducts/products_screen.dart b/sharekhan_admin_panel/lib/screens/sellerPoducts/products_screen.dart new file mode 100644 index 00000000..b17d2807 --- /dev/null +++ b/sharekhan_admin_panel/lib/screens/sellerPoducts/products_screen.dart @@ -0,0 +1,181 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:sharekhan_admin_panel/tables/seller_product_data_source.dart'; +import 'package:syncfusion_flutter_datagrid/datagrid.dart'; +import 'package:sharekhan_admin_panel/models/product_model.dart'; +import 'package:sharekhan_admin_panel/navigation/routes.dart'; +import 'package:sharekhan_admin_panel/providers/category_provider.dart'; +import 'package:sharekhan_admin_panel/providers/product_provider.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:sharekhan_admin_panel/widgets/common/main_tab_header.dart'; +import 'package:sharekhan_admin_panel/widgets/common/spacing.dart'; +import 'package:csv/csv.dart'; +// ignore: avoid_web_libraries_in_flutter +import 'dart:html' as html; + +class SellerProductsScreen extends StatefulWidget { + const SellerProductsScreen({super.key}); + + @override + State createState() => _SellerProductsScreenState(); +} + +class _SellerProductsScreenState extends State { + void _onToggleActive(ProductModel product, bool isApproved) async { + final productProvider = context.read(); + productProvider.updateProduct(product.copyWith(isApproved: isApproved)); + } + + Future _exportToCSV() async { + final productProvider = context.read(); + List> rows = []; + rows.add([ + "Image", + "Name", + "Category", + "Unit Weight", + "Unit Price", + "Remaining Quantity", + "Is Active", + "Created At", + "Updated At" + ]); + for (var product in productProvider.products) { + rows.add([ + product.images.isNotEmpty ? product.images.first : 'No Image', + product.name, + product.categoryID, + product.unitWeight, + product.unitPrice, + product.remainingQuantity, + product.isActive ? "Active" : "Inactive", + product.createdAt.toIso8601String(), + product.updatedAt.toIso8601String(), + ]); + } + + String csv = const ListToCsvConverter().convert(rows); + final bytes = utf8.encode(csv); + final blob = html.Blob([bytes]); + final url = html.Url.createObjectUrlFromBlob(blob); + html.AnchorElement(href: url) + ..setAttribute("download", "products.csv") + ..click(); + html.Url.revokeObjectUrl(url); + } + + @override + Widget build(BuildContext context) { + final productProvider = context.watch(); + final categoryProvider = context.watch(); + return Scaffold( + backgroundColor: AppTheme.colorWhite, + body: Padding( + padding: AppTheme.paddingSmall, + child: Column( + children: [ + const Spacing(size: AppTheme.spacingLarge), + MainTabHeader( + title: 'Products', + routeName: Routes.productAdd, + secondButtonText: 'Add Product', + onExport: _exportToCSV, + ), + const Spacing(size: AppTheme.spacingLarge), + Expanded( + child: SfDataGrid( + source: SellerProductDataSource( + products: productProvider.products + .where((product) => !product.isSheruSpecial) + .toList(), + categories: categoryProvider.categories, + onToggleActive: _onToggleActive, + ), + columnWidthMode: ColumnWidthMode.fill, + gridLinesVisibility: GridLinesVisibility.both, + headerGridLinesVisibility: GridLinesVisibility.both, + columns: [ + GridColumn( + columnName: 'images', + width: 150, + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: + const Text('Image', style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'name', + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: + const Text('Name', style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'categoryID', + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: const Text('Category', + style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'description', + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: const Text('Description', + style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'unitWeight', + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: const Text('Unit Weight', + style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'unitPrice', + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: const Text('Unit Price', + style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'remainingQuantity', + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: const Text('Remaining Quantity', + style: AppTheme.fontStyleDefault), + ), + ), + GridColumn( + columnName: 'isApproved', + width: 100, + label: Container( + padding: AppTheme.paddingTiny, + alignment: Alignment.center, + child: const Text('Approve', + style: AppTheme.fontStyleDefault), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/sharekhan_admin_panel/lib/screens/settings/edit_settings_screen.dart b/sharekhan_admin_panel/lib/screens/settings/edit_settings_screen.dart new file mode 100644 index 00000000..20f445ff --- /dev/null +++ b/sharekhan_admin_panel/lib/screens/settings/edit_settings_screen.dart @@ -0,0 +1,276 @@ +import 'package:sharekhan_admin_panel/models/settings_model.dart'; +import 'package:sharekhan_admin_panel/navigation/go_router.dart'; +import 'package:sharekhan_admin_panel/navigation/routes.dart'; +import 'package:sharekhan_admin_panel/providers/setting_provider.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_loading.dart'; +import 'package:sharekhan_admin_panel/widgets/common/edit_tab_footer.dart'; +import 'package:sharekhan_admin_panel/widgets/common/secondary_tab_header.dart'; +import 'package:sharekhan_admin_panel/widgets/common/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class EditSettingsScreen extends StatefulWidget { + const EditSettingsScreen({super.key}); + + @override + State createState() => _EditSettingsScreenState(); +} + +class _EditSettingsScreenState extends State { + bool _isLoading = true; + late TextEditingController _androidVersionController; + late TextEditingController _iosVersionController; + bool _isAndroidMandatory = false; + bool _isIOSMandatory = false; + bool _isMaintenanceMode = false; + + Future _initializeSettings() async { + final settingProvider = context.read(); + await settingProvider.fetchSettings(); + final settings = settingProvider.settings; + + setState(() { + _androidVersionController = TextEditingController( + text: settings.version.androidVersion.versionName); + _iosVersionController = + TextEditingController(text: settings.version.iosVersion.versionName); + _isAndroidMandatory = settings.version.androidVersion.mandatory; + _isIOSMandatory = settings.version.iosVersion.mandatory; + _isMaintenanceMode = settings.version.isMaintenance; + _isLoading = false; + }); + } + + Future _saveSettings() async { + final settingProvider = context.read(); + final updatedSettings = SettingsModel( + version: AppVersion( + androidVersion: Version( + versionName: _androidVersionController.text, + mandatory: _isAndroidMandatory, + ), + iosVersion: Version( + versionName: _iosVersionController.text, + mandatory: _isIOSMandatory, + ), + isMaintenance: _isMaintenanceMode, + ), + ); + + await settingProvider.updateSettings(updatedSettings); + router.go(Routes.settings); + } + + @override + void initState() { + super.initState(); + _initializeSettings(); + } + + @override + void dispose() { + _androidVersionController.dispose(); + _iosVersionController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return !_isLoading + ? Scaffold( + backgroundColor: AppTheme.colorWhite, + body: SingleChildScrollView( + padding: AppTheme.paddingSmall, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacing(size: AppTheme.spacingLarge), + const BreadcrumbTabHeader( + goBackRoute: Routes.sheruProducts, + mainTitle: 'Settings', + secondaryTitle: 'Edit Settings', + ), + const Spacing(size: AppTheme.spacingLarge), + const Divider(color: AppTheme.borderColor, thickness: 1), + const Spacing(size: AppTheme.spacingSmall), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Android Version', + style: AppTheme.fontStyleDefaultBold, + ), + const Spacing(size: AppTheme.spacingTiny), + TextField( + controller: _androidVersionController, + decoration: InputDecoration( + hintText: 'Enter Android Version', + hintStyle: + AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ], + ), + ), + const Spacing( + size: AppTheme.spacingMedium, isHorizontal: true), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Mandatory Update for Android', + style: AppTheme.fontStyleDefaultBold, + ), + const Spacing(size: AppTheme.spacingTiny), + Row( + children: [ + Switch( + value: _isAndroidMandatory, + onChanged: (value) { + setState(() { + _isAndroidMandatory = value; + }); + }, + activeColor: AppTheme.colorRed, + ), + Text( + _isAndroidMandatory ? 'Yes' : 'No', + style: AppTheme.fontStyleDefault, + ), + ], + ), + ], + ), + ), + ], + ), + const Spacing(size: AppTheme.spacingMedium), + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'iOS Version', + style: AppTheme.fontStyleDefaultBold, + ), + const Spacing(size: AppTheme.spacingTiny), + TextField( + controller: _iosVersionController, + decoration: InputDecoration( + hintText: 'Enter IOS Version', + hintStyle: + AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + filled: true, + fillColor: AppTheme.colorWhite, + ), + ), + ], + ), + ), + const Spacing( + size: AppTheme.spacingMedium, isHorizontal: true), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Mandatory Update for iOS', + style: AppTheme.fontStyleDefaultBold, + ), + const Spacing(size: AppTheme.spacingTiny), + Row( + children: [ + Switch( + value: _isIOSMandatory, + onChanged: (value) { + setState(() { + _isIOSMandatory = value; + }); + }, + activeColor: AppTheme.colorRed, + ), + Text( + _isIOSMandatory ? 'Yes' : 'No', + style: AppTheme.fontStyleDefault, + ), + ], + ), + ], + ), + ), + ], + ), + const Spacing(size: AppTheme.spacingMedium), + const Divider(color: AppTheme.borderColor, thickness: 1), + const Spacing(size: AppTheme.spacingSmall), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Maintenance Mode', + style: AppTheme.fontStyleDefaultBold, + ), + const Spacing(size: AppTheme.spacingTiny), + Row( + children: [ + Switch( + value: _isMaintenanceMode, + onChanged: (value) { + setState(() { + _isMaintenanceMode = value; + }); + }, + activeColor: AppTheme.colorRed, + ), + Text( + _isMaintenanceMode ? 'Enabled' : 'Disabled', + style: AppTheme.fontStyleDefault, + ), + ], + ), + const Spacing(size: AppTheme.spacingSmall), + Text( + _isMaintenanceMode + ? 'The app is currently in maintenance mode.' + : 'The app is not in maintenance mode.', + style: AppTheme.fontStyleDefault, + ), + ], + ), + ], + ), + const Spacing(size: AppTheme.spacingLarge), + EditTabFooter( + goBackrouteName: Routes.settings, + onSave: _saveSettings, + ), + ], + ), + ), + ) + : const CustomLoading(); + } +} diff --git a/sharekhan_admin_panel/lib/screens/settings/settings_screen.dart b/sharekhan_admin_panel/lib/screens/settings/settings_screen.dart new file mode 100644 index 00000000..41938b2e --- /dev/null +++ b/sharekhan_admin_panel/lib/screens/settings/settings_screen.dart @@ -0,0 +1,166 @@ +import 'package:sharekhan_admin_panel/navigation/routes.dart'; +import 'package:sharekhan_admin_panel/providers/setting_provider.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:sharekhan_admin_panel/widgets/common/main_tab_header.dart'; +import 'package:sharekhan_admin_panel/widgets/common/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class SettingsScreen extends StatefulWidget { + const SettingsScreen({super.key}); + + @override + State createState() => SettingsScreenState(); +} + +class SettingsScreenState extends State { + bool _isMaintenanceMode = false; + + init() async { + final settingProvider = context.read(); + setState(() { + _isMaintenanceMode = settingProvider.settings.version.isMaintenance; + }); + } + + @override + void initState() { + super.initState(); + init(); + } + + void _toggleMaintenanceMode(bool value) { + setState(() { + _isMaintenanceMode = value; + }); + final settingProvider = context.read(); + settingProvider.updateMaintainance(value); + } + + @override + Widget build(BuildContext context) { + final settingsProvider = context.watch(); + return Scaffold( + backgroundColor: AppTheme.colorWhite, + body: Padding( + padding: AppTheme.paddingSmall, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacing(size: AppTheme.spacingLarge), + MainTabHeader( + title: 'Settings', + routeName: Routes.settingsEdit, + secondButtonText: 'Update Settings', + showExportButton: false, + onExport: () {}, + ), + const Spacing(size: AppTheme.spacingLarge), + const Divider(color: AppTheme.borderColor, thickness: 1), + const Spacing(size: AppTheme.spacingSmall), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Android Version', + style: AppTheme.fontStyleDefaultBold, + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + settingsProvider + .settings.version.androidVersion.versionName, + style: AppTheme.fontStyleDefault, + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Mandatory to push update on Android', + style: AppTheme.fontStyleDefaultBold, + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + settingsProvider.settings.version.androidVersion.mandatory + ? 'Yes' + : 'No', + style: AppTheme.fontStyleDefault, + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'IOS Version', + style: AppTheme.fontStyleDefaultBold, + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + settingsProvider.settings.version.iosVersion.versionName, + style: AppTheme.fontStyleDefault, + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Mandatory to push update on IOS', + style: AppTheme.fontStyleDefaultBold, + ), + const Spacing(size: AppTheme.spacingTiny), + Text( + settingsProvider.settings.version.iosVersion.mandatory + ? 'Yes' + : 'No', + style: AppTheme.fontStyleDefault, + ), + ], + ), + ], + ), + const Spacing(size: AppTheme.spacingMedium), + const Divider(color: AppTheme.borderColor, thickness: 1), + const Spacing(size: AppTheme.spacingSmall), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const Text( + 'Maintenance Mode', + style: AppTheme.fontStyleDefaultBold, + ), + const Spacing( + size: AppTheme.spacingTiny, isHorizontal: true), + Switch( + value: _isMaintenanceMode, + onChanged: _toggleMaintenanceMode, + activeColor: AppTheme.colorRed, + ), + ], + ), + const Spacing(size: AppTheme.spacingSmall), + Text( + _isMaintenanceMode + ? 'App is in maintenance mode' + : 'App is not in maintenance mode', + style: AppTheme.fontStyleDefault, + ), + ], + ), + const Spacing(size: AppTheme.spacingMedium), + const Divider(color: AppTheme.borderColor, thickness: 1), + const Spacing(size: AppTheme.spacingSmall), + ], + ), + ), + ); + } +} diff --git a/sharekhan_admin_panel/lib/screens/splash_screen.dart b/sharekhan_admin_panel/lib/screens/splash_screen.dart new file mode 100644 index 00000000..b5139fa2 --- /dev/null +++ b/sharekhan_admin_panel/lib/screens/splash_screen.dart @@ -0,0 +1,49 @@ +import 'package:sharekhan_admin_panel/constants/asset_constants.dart'; +import 'package:sharekhan_admin_panel/navigation/go_router.dart'; +import 'package:sharekhan_admin_panel/navigation/routes.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:sharekhan_admin_panel/widgets/common/spacing.dart'; +import 'package:flutter/material.dart'; + +class SplashScreen extends StatefulWidget { + const SplashScreen({super.key}); + + @override + State createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State { + @override + void initState() { + super.initState(); + Future.delayed(const Duration(seconds: 2), () { + router.go(Routes.banners); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppTheme.colorRed, + body: SizedBox( + width: double.infinity, + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text('Designed for the Serious', + style: AppTheme.fontStyleLarge.copyWith( + color: AppTheme.colorWhite, + fontSize: 60, + fontWeight: FontWeight.bold, + )), + const Spacing(size: AppTheme.spacingLarge), + Image.asset(imageSplashBanner, width: 500), + // const Spacing(size: AppTheme.spacingExtraLarge), + // Image.asset(imageLogoTagline, width: 300), + ], + ), + ), + ); + } +} diff --git a/sharekhan_admin_panel/lib/screens/users/users_screen.dart b/sharekhan_admin_panel/lib/screens/users/users_screen.dart new file mode 100644 index 00000000..09fed228 --- /dev/null +++ b/sharekhan_admin_panel/lib/screens/users/users_screen.dart @@ -0,0 +1,149 @@ +import 'dart:convert'; +import 'package:sharekhan_admin_panel/providers/user_provider.dart'; +import 'package:sharekhan_admin_panel/tables/user_data_source.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:sharekhan_admin_panel/widgets/common/main_tab_header.dart'; +import 'package:sharekhan_admin_panel/widgets/common/spacing.dart'; +import 'package:csv/csv.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:syncfusion_flutter_datagrid/datagrid.dart'; +// ignore: avoid_web_libraries_in_flutter +import 'dart:html' as html; + +class UsersScreen extends StatefulWidget { + const UsersScreen({super.key}); + + @override + State createState() => _UsersScreenState(); +} + +class _UsersScreenState extends State { + Future _exportToCSV() async { + final userProvider = context.read(); + List> rows = []; + rows.add([ + "Name", + "Phone Number", + "Email", + "Business Name", + "Business Type", + "GST Number", + "PAN Number", + "User Type", + "Last Seen At" + ]); + for (var user in userProvider.users) { + rows.add([ + user.name, + user.phoneNumber, + user.email ?? '', + user.businessName ?? '', + user.businessType ?? '', + user.gstNumber ?? '', + user.panNumber ?? '', + user.userType.name, + user.lastSeenAt.toIso8601String(), + ]); + } + + String csv = const ListToCsvConverter().convert(rows); + final bytes = utf8.encode(csv); + final blob = html.Blob([bytes]); + final url = html.Url.createObjectUrlFromBlob(blob); + html.AnchorElement(href: url) + ..setAttribute("download", "users.csv") + ..click(); + html.Url.revokeObjectUrl(url); + } + + @override + Widget build(BuildContext context) { + final userProvider = context.watch(); + return Scaffold( + backgroundColor: AppTheme.colorWhite, + body: Padding( + padding: AppTheme.paddingSmall, + child: Column( + children: [ + const Spacing(size: AppTheme.spacingLarge), + MainTabHeader( + title: 'Users', + routeName: '', + secondButtonText: '', + onExport: _exportToCSV, + showSecondButton: false, + ), + const Spacing(size: AppTheme.spacingLarge), + Expanded( + child: SfDataGrid( + source: UserDataSource( + users: userProvider.users, + ), + columnWidthMode: ColumnWidthMode.fill, + gridLinesVisibility: GridLinesVisibility.both, + headerGridLinesVisibility: GridLinesVisibility.both, + columns: [ + GridColumn( + columnName: 'name', + label: Container( + padding: const EdgeInsets.all(16.0), + alignment: Alignment.center, + child: const Text('Name'))), + GridColumn( + columnName: 'phoneNumber', + label: Container( + padding: const EdgeInsets.all(16.0), + alignment: Alignment.center, + child: const Text('Phone Number'))), + GridColumn( + columnName: 'email', + label: Container( + padding: const EdgeInsets.all(16.0), + alignment: Alignment.center, + child: const Text('Email'))), + GridColumn( + columnName: 'businessName', + label: Container( + padding: const EdgeInsets.all(16.0), + alignment: Alignment.center, + child: const Text('Business Name'))), + GridColumn( + columnName: 'businessType', + label: Container( + padding: const EdgeInsets.all(16.0), + alignment: Alignment.center, + child: const Text('Business Type'))), + GridColumn( + columnName: 'gstNumber', + label: Container( + padding: const EdgeInsets.all(16.0), + alignment: Alignment.center, + child: const Text('GST Number'))), + GridColumn( + columnName: 'panNumber', + label: Container( + padding: const EdgeInsets.all(16.0), + alignment: Alignment.center, + child: const Text('PAN Number'))), + GridColumn( + columnName: 'userType', + label: Container( + padding: const EdgeInsets.all(16.0), + alignment: Alignment.center, + child: const Text('User Type'))), + GridColumn( + columnName: 'lastSeenAt', + label: Container( + padding: const EdgeInsets.all(16.0), + alignment: Alignment.center, + child: const Text('Last Seen'))), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/sharekhan_admin_panel/lib/tables/banner_data_source.dart b/sharekhan_admin_panel/lib/tables/banner_data_source.dart new file mode 100644 index 00000000..b2ba903f --- /dev/null +++ b/sharekhan_admin_panel/lib/tables/banner_data_source.dart @@ -0,0 +1,107 @@ +import 'package:sharekhan_admin_panel/models/banner_model.dart'; +import 'package:sharekhan_admin_panel/models/product_model.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:syncfusion_flutter_datagrid/datagrid.dart'; + +class BannerDataSource extends DataGridSource { + BannerDataSource({ + required List banners, + required List products, + required this.onEdit, + required this.onDelete, + required this.onToggleActive, + }) { + _banners = banners + .map((e) => DataGridRow(cells: [ + DataGridCell(columnName: 'imageUrl', value: e.imageUrl), + DataGridCell(columnName: 'productID', value: e.productID), + DataGridCell( + columnName: 'description', value: e.description), + DataGridCell(columnName: 'isActive', value: e.isActive), + DataGridCell(columnName: 'id', value: e.id), + DataGridCell( + columnName: 'createdAt', value: e.createdAt), + DataGridCell( + columnName: 'updatedAt', value: e.updatedAt), + ])) + .toList(); + _products = products; + } + + final Function(BannerModel) onEdit; + final Function(BannerModel) onDelete; + final Function(BannerModel, bool) onToggleActive; + + List _banners = []; + List _products = []; + + @override + List get rows => _banners; + + @override + DataGridRowAdapter buildRow(DataGridRow row) { + final banner = BannerModel( + imageUrl: row.getCells()[0].value, + productID: row.getCells()[1].value, + description: row.getCells()[2].value, + isActive: row.getCells()[3].value, + id: row.getCells()[4].value, + createdAt: row.getCells()[5].value, + updatedAt: row.getCells()[6].value, + ); + + return DataGridRowAdapter( + cells: [ + Container( + padding: AppTheme.paddingTiny, + child: Image.network(banner.imageUrl, + width: 100, height: 100, fit: BoxFit.cover), + ), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Text( + _products + .firstWhere((element) => element.id == banner.productID) + .name, + style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Text(banner.description, style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.center, + padding: AppTheme.paddingTiny, + child: Transform.scale( + scale: 0.8, + child: Switch( + activeTrackColor: AppTheme.colorRed, + value: banner.isActive, + onChanged: (value) => onToggleActive(banner, value), + ), + ), + ), + Container( + alignment: Alignment.center, + padding: AppTheme.paddingTiny, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: const Icon(Icons.edit, color: AppTheme.colorGrey), + onPressed: () => onEdit(banner), + ), + IconButton( + icon: const Icon(Icons.delete, color: AppTheme.colorRed), + onPressed: () => onDelete(banner), + ), + ], + ), + ), + ], + ); + } +} diff --git a/sharekhan_admin_panel/lib/tables/category_data_source.dart b/sharekhan_admin_panel/lib/tables/category_data_source.dart new file mode 100644 index 00000000..05da23db --- /dev/null +++ b/sharekhan_admin_panel/lib/tables/category_data_source.dart @@ -0,0 +1,83 @@ +import 'package:sharekhan_admin_panel/models/category_model.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:flutter/material.dart'; +import 'package:syncfusion_flutter_datagrid/datagrid.dart'; + +class CategoryDataSource extends DataGridSource { + CategoryDataSource({ + required List categories, + required this.onEdit, + required this.onDelete, + }) { + _categories = categories + .map((e) => DataGridRow(cells: [ + DataGridCell(columnName: 'imageUrl', value: e.imageUrl), + DataGridCell(columnName: 'name', value: e.name), + DataGridCell( + columnName: 'description', value: e.description), + DataGridCell(columnName: 'id', value: e.id), + DataGridCell( + columnName: 'createdAt', value: e.createdAt), + DataGridCell( + columnName: 'updatedAt', value: e.updatedAt), + ])) + .toList(); + } + + final Function(CategoryModel) onEdit; + final Function(CategoryModel) onDelete; + + List _categories = []; + + @override + List get rows => _categories; + + @override + DataGridRowAdapter buildRow(DataGridRow row) { + final category = CategoryModel( + imageUrl: row.getCells()[0].value, + name: row.getCells()[1].value, + description: row.getCells()[2].value, + id: row.getCells()[3].value, + createdAt: row.getCells()[4].value, + updatedAt: row.getCells()[5].value, + ); + return DataGridRowAdapter( + cells: [ + Container( + padding: AppTheme.paddingTiny, + child: Image.network(category.imageUrl, + width: 100, height: 100, fit: BoxFit.cover), + ), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Text(category.name, style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: + Text(category.description, style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.center, + padding: AppTheme.paddingTiny, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: const Icon(Icons.edit, color: AppTheme.colorGrey), + onPressed: () => onEdit(category), + ), + IconButton( + icon: const Icon(Icons.delete, color: AppTheme.colorRed), + onPressed: () => onDelete(category), + ), + ], + ), + ), + ], + ); + } +} diff --git a/sharekhan_admin_panel/lib/tables/coupon_data_source.dart b/sharekhan_admin_panel/lib/tables/coupon_data_source.dart new file mode 100644 index 00000000..c859634a --- /dev/null +++ b/sharekhan_admin_panel/lib/tables/coupon_data_source.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:syncfusion_flutter_datagrid/datagrid.dart'; +import 'package:sharekhan_admin_panel/models/coupon_model.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; + +class CouponDataSource extends DataGridSource { + CouponDataSource({ + required List coupons, + required this.onEdit, + required this.onDelete, + required this.onToggleActive, + }) { + _coupons = coupons + .map((e) => DataGridRow(cells: [ + DataGridCell( + columnName: 'couponCode', value: e.couponCode), + DataGridCell( + columnName: 'description', value: e.description), + DataGridCell(columnName: 'maxAmount', value: e.maxAmount), + DataGridCell( + columnName: 'percentage', value: e.percentage), + DataGridCell(columnName: 'isActive', value: e.isActive), + DataGridCell(columnName: 'id', value: e.id), + DataGridCell( + columnName: 'createdAt', value: e.createdAt), + DataGridCell( + columnName: 'updatedAt', value: e.updatedAt), + ])) + .toList(); + } + + final Function(CouponModel) onEdit; + final Function(CouponModel) onDelete; + final Function(CouponModel, bool) onToggleActive; + + List _coupons = []; + + @override + List get rows => _coupons; + + @override + DataGridRowAdapter buildRow(DataGridRow row) { + final coupon = CouponModel( + id: row.getCells()[5].value, + couponCode: row.getCells()[0].value, + description: row.getCells()[1].value, + maxAmount: row.getCells()[2].value, + percentage: row.getCells()[3].value, + isActive: row.getCells()[4].value, + userIDs: [], + createdAt: row.getCells()[6].value, + updatedAt: row.getCells()[7].value, + ); + return DataGridRowAdapter( + cells: [ + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + horizontal: AppTheme.spacingSmall, + vertical: AppTheme.spacingTiny), + child: Text(coupon.couponCode, style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + horizontal: AppTheme.spacingSmall, + vertical: AppTheme.spacingTiny), + child: Text(coupon.description, style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.center, + padding: const EdgeInsets.all(AppTheme.spacingTiny), + child: Text(coupon.maxAmount.toString(), + style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.center, + padding: const EdgeInsets.all(AppTheme.spacingTiny), + child: Text(coupon.percentage.toStringAsFixed(2), + style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.center, + padding: const EdgeInsets.all(AppTheme.spacingTiny), + child: Transform.scale( + scale: 0.8, + child: Switch( + activeTrackColor: AppTheme.colorRed, + value: coupon.isActive, + onChanged: (value) => onToggleActive(coupon, value), + ), + ), + ), + Container( + alignment: Alignment.center, + padding: const EdgeInsets.all(AppTheme.spacingTiny), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: const Icon(Icons.edit, color: AppTheme.colorGrey), + onPressed: () => onEdit(coupon), + ), + IconButton( + icon: const Icon(Icons.delete, color: AppTheme.colorRed), + onPressed: () => onDelete(coupon), + ), + ], + ), + ), + ], + ); + } +} diff --git a/sharekhan_admin_panel/lib/tables/product_data_source.dart b/sharekhan_admin_panel/lib/tables/product_data_source.dart new file mode 100644 index 00000000..a02f6174 --- /dev/null +++ b/sharekhan_admin_panel/lib/tables/product_data_source.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; +import 'package:syncfusion_flutter_datagrid/datagrid.dart'; +import 'package:sharekhan_admin_panel/models/category_model.dart'; +import 'package:sharekhan_admin_panel/models/product_model.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; + +class ProductDataSource extends DataGridSource { + ProductDataSource({ + required List products, + required List categories, + required this.onEdit, + required this.onDelete, + required this.onToggleActive, + }) { + _products = products + .map((product) => DataGridRow(cells: [ + DataGridCell(columnName: 'name', value: product.name), + DataGridCell( + columnName: 'categoryID', value: product.categoryID), + DataGridCell( + columnName: 'images', value: product.images.join(", ")), + DataGridCell( + columnName: 'description', value: product.description), + DataGridCell( + columnName: 'unitWeight', value: product.unitWeight), + DataGridCell( + columnName: 'unitPrice', value: product.unitPrice), + DataGridCell( + columnName: 'remainingQuantity', + value: product.remainingQuantity), + DataGridCell( + columnName: 'isActive', value: product.isActive), + DataGridCell(columnName: 'id', value: product.id), + DataGridCell( + columnName: 'createdAt', value: product.createdAt), + DataGridCell( + columnName: 'updatedAt', value: product.updatedAt), + DataGridCell( + columnName: 'sellerId', + value: product.sellerId, + ), + DataGridCell( + columnName: 'isSheruSpecial', + value: product.isSheruSpecial, + ), + DataGridCell( + columnName: 'isApproved', + value: product.isApproved, + ), + ])) + .toList(); + + _categories = categories; + } + + final Function(ProductModel) onEdit; + final Function(ProductModel) onDelete; + final Function(ProductModel, bool) onToggleActive; + + List _products = []; + List _categories = []; + + @override + List get rows => _products; + + @override + DataGridRowAdapter buildRow(DataGridRow row) { + final product = ProductModel( + id: row.getCells()[8].value, + categoryID: row.getCells()[1].value, + images: (row.getCells()[2].value as String).split(", "), + name: row.getCells()[0].value, + description: row.getCells()[3].value, + unitWeight: row.getCells()[4].value, + unitPrice: row.getCells()[5].value, + remainingQuantity: row.getCells()[6].value, + isActive: row.getCells()[7].value, + createdAt: row.getCells()[9].value, + updatedAt: row.getCells()[10].value, + sellerId: row.getCells()[11].value, + isSheruSpecial: row.getCells()[12].value, + isApproved: row.getCells()[13].value, + ); + + return DataGridRowAdapter( + cells: [ + Container( + padding: AppTheme.paddingTiny, + child: Image.network(product.images[0], + width: 100, height: 100, fit: BoxFit.cover), + ), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + horizontal: AppTheme.spacingSmall, + vertical: AppTheme.spacingTiny), + child: Text(product.name, style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + horizontal: AppTheme.spacingSmall, + vertical: AppTheme.spacingTiny), + child: Text( + _categories + .firstWhere((element) => element.id == product.categoryID) + .name, + style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + horizontal: AppTheme.spacingSmall, + vertical: AppTheme.spacingTiny), + child: + Text(product.description, style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.center, + padding: AppTheme.paddingTiny, + child: Text(product.unitWeight.toStringAsFixed(2), + style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.center, + padding: AppTheme.paddingTiny, + child: Text(product.unitPrice.toString(), + style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.center, + padding: AppTheme.paddingTiny, + child: Text(product.remainingQuantity.toString(), + style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.center, + padding: AppTheme.paddingTiny, + child: Transform.scale( + scale: 0.8, + child: Switch( + activeTrackColor: AppTheme.colorRed, + value: product.isActive, + onChanged: (value) => onToggleActive(product, value), + ), + ), + ), + Container( + alignment: Alignment.center, + padding: AppTheme.paddingTiny, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: const Icon(Icons.edit, color: AppTheme.colorGrey), + onPressed: () => onEdit(product), + ), + IconButton( + icon: const Icon(Icons.delete, color: AppTheme.colorRed), + onPressed: () => onDelete(product), + ), + ], + ), + ), + ], + ); + } +} diff --git a/sharekhan_admin_panel/lib/tables/seller_product_data_source.dart b/sharekhan_admin_panel/lib/tables/seller_product_data_source.dart new file mode 100644 index 00000000..b3d4570c --- /dev/null +++ b/sharekhan_admin_panel/lib/tables/seller_product_data_source.dart @@ -0,0 +1,147 @@ +import 'package:flutter/material.dart'; +import 'package:syncfusion_flutter_datagrid/datagrid.dart'; +import 'package:sharekhan_admin_panel/models/category_model.dart'; +import 'package:sharekhan_admin_panel/models/product_model.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; + +class SellerProductDataSource extends DataGridSource { + SellerProductDataSource({ + required List products, + required List categories, + required this.onToggleActive, + }) { + _products = products + .map((product) => DataGridRow(cells: [ + DataGridCell(columnName: 'name', value: product.name), + DataGridCell( + columnName: 'categoryID', value: product.categoryID), + DataGridCell( + columnName: 'images', value: product.images.join(", ")), + DataGridCell( + columnName: 'description', value: product.description), + DataGridCell( + columnName: 'unitWeight', value: product.unitWeight), + DataGridCell( + columnName: 'unitPrice', value: product.unitPrice), + DataGridCell( + columnName: 'remainingQuantity', + value: product.remainingQuantity), + DataGridCell( + columnName: 'isActive', value: product.isActive), + DataGridCell(columnName: 'id', value: product.id), + DataGridCell( + columnName: 'createdAt', value: product.createdAt), + DataGridCell( + columnName: 'updatedAt', value: product.updatedAt), + DataGridCell( + columnName: 'sellerId', + value: product.sellerId, + ), + DataGridCell( + columnName: 'isSheruSpecial', + value: product.isSheruSpecial, + ), + DataGridCell( + columnName: 'isApproved', + value: product.isApproved, + ), + ])) + .toList(); + + _categories = categories; + } + + final Function(ProductModel, bool) onToggleActive; + + List _products = []; + List _categories = []; + + @override + List get rows => _products; + + @override + DataGridRowAdapter buildRow(DataGridRow row) { + final product = ProductModel( + id: row.getCells()[8].value, + categoryID: row.getCells()[1].value, + images: (row.getCells()[2].value as String).split(", "), + name: row.getCells()[0].value, + description: row.getCells()[3].value, + unitWeight: row.getCells()[4].value, + unitPrice: row.getCells()[5].value, + remainingQuantity: row.getCells()[6].value, + isActive: row.getCells()[7].value, + createdAt: row.getCells()[9].value, + updatedAt: row.getCells()[10].value, + sellerId: row.getCells()[11].value, + isSheruSpecial: row.getCells()[12].value, + isApproved: row.getCells()[13].value, + ); + + return DataGridRowAdapter( + cells: [ + Container( + padding: AppTheme.paddingTiny, + child: Image.network(product.images[0], + width: 100, height: 100, fit: BoxFit.cover), + ), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + horizontal: AppTheme.spacingSmall, + vertical: AppTheme.spacingTiny), + child: Text(product.name, style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + horizontal: AppTheme.spacingSmall, + vertical: AppTheme.spacingTiny), + child: Text( + _categories + .firstWhere((element) => element.id == product.categoryID) + .name, + style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + horizontal: AppTheme.spacingSmall, + vertical: AppTheme.spacingTiny), + child: + Text(product.description, style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.center, + padding: AppTheme.paddingTiny, + child: Text(product.unitWeight.toStringAsFixed(2), + style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.center, + padding: AppTheme.paddingTiny, + child: Text(product.unitPrice.toString(), + style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.center, + padding: AppTheme.paddingTiny, + child: Text(product.remainingQuantity.toString(), + style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.center, + padding: AppTheme.paddingTiny, + child: Transform.scale( + scale: 0.8, + child: Switch( + activeTrackColor: AppTheme.colorRed, + value: product.isApproved, + onChanged: (value) => onToggleActive(product, value), + ), + ), + ), + ], + ); + } +} diff --git a/sharekhan_admin_panel/lib/tables/user_data_source.dart b/sharekhan_admin_panel/lib/tables/user_data_source.dart new file mode 100644 index 00000000..d6a12805 --- /dev/null +++ b/sharekhan_admin_panel/lib/tables/user_data_source.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:sharekhan_admin_panel/enums/enum_parser.dart'; +import 'package:sharekhan_admin_panel/models/user_model.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:syncfusion_flutter_datagrid/datagrid.dart'; + +class UserDataSource extends DataGridSource { + UserDataSource({ + required List users, + }) { + _users = users + .map((user) => DataGridRow(cells: [ + DataGridCell(columnName: 'name', value: user.name), + DataGridCell( + columnName: 'phoneNumber', value: user.phoneNumber), + DataGridCell(columnName: 'email', value: user.email), + DataGridCell( + columnName: 'businessName', value: user.businessName), + DataGridCell( + columnName: 'businessType', value: user.businessType), + DataGridCell( + columnName: 'gstNumber', value: user.gstNumber), + DataGridCell( + columnName: 'panNumber', value: user.panNumber), + DataGridCell( + columnName: 'userType', value: user.userType.name), + DataGridCell( + columnName: 'lastSeenAt', value: user.lastSeenAt), + ])) + .toList(); + } + + List _users = []; + + @override + List get rows => _users; + + @override + DataGridRowAdapter buildRow(DataGridRow row) { + final user = UserModel( + id: '', + name: row.getCells()[0].value, + phoneNumber: row.getCells()[1].value, + email: row.getCells()[2].value, + isBusiness: row.getCells()[3].value != null, + businessName: row.getCells()[3].value, + businessType: row.getCells()[4].value, + gstNumber: row.getCells()[5].value, + panNumber: row.getCells()[6].value, + userType: parseUserType(row.getCells()[7].value), + defaultAddressID: '', + createdAt: DateTime.now(), + lastSeenAt: row.getCells()[8].value, + ); + + return DataGridRowAdapter( + cells: [ + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + horizontal: AppTheme.spacingSmall, + vertical: AppTheme.spacingTiny), + child: Text(user.name, style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + horizontal: AppTheme.spacingSmall, + vertical: AppTheme.spacingTiny), + child: Text(user.phoneNumber, style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + horizontal: AppTheme.spacingSmall, + vertical: AppTheme.spacingTiny), + child: Text(user.email ?? '-', style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + horizontal: AppTheme.spacingSmall, + vertical: AppTheme.spacingTiny), + child: Text(user.businessName ?? '-', + style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + horizontal: AppTheme.spacingSmall, + vertical: AppTheme.spacingTiny), + child: Text(user.businessType ?? '-', + style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + horizontal: AppTheme.spacingSmall, + vertical: AppTheme.spacingTiny), + child: + Text(user.gstNumber ?? '-', style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + horizontal: AppTheme.spacingSmall, + vertical: AppTheme.spacingTiny), + child: + Text(user.panNumber ?? '-', style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + horizontal: AppTheme.spacingSmall, + vertical: AppTheme.spacingTiny), + child: Text(user.userType.name, style: const TextStyle(fontSize: 16)), + ), + Container( + alignment: Alignment.center, + padding: const EdgeInsets.all(AppTheme.spacingTiny), + child: Text(user.lastSeenAt.toString(), + style: const TextStyle(fontSize: 16)), + ), + ], + ); + } +} diff --git a/sharekhan_admin_panel/lib/theme/app_theme.dart b/sharekhan_admin_panel/lib/theme/app_theme.dart new file mode 100644 index 00000000..cd0db576 --- /dev/null +++ b/sharekhan_admin_panel/lib/theme/app_theme.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; + +class AppTheme { + // Colors + static const Color colorBlack = Color(0xff040520); + static const Color colorWhite = Colors.white; + static const Color greyTextColor = Color(0xffA7A7AF); + static const Color colorRed = Color(0xffAC1E2F); + static const Color colorGrey = Color(0xff8A8A8A); + static const Color colorDisabled = Color(0xFFCFCFCF); + static const Color backgroundColor = Color(0xffF2F2F3); + static const Color borderColor = Color(0xffDCDCE0); + + //Font Size + static const double fontSizeSmall = 12.0; + static const double fontSizeDefault = 14.0; + static const double fontSizeMedium = 16.0; + static const double fontSizeLarge = 24.0; + + //Spacing + static const double spacingTiny = 8.0; + static const double spacingExtraSmall = 12.0; + static const double spacingSmall = 16.0; + static const double spacingSemiMedium = 20.0; + static const double spacingDefault = 24.0; + static const double spacingMedium = 32.0; + static const double spacingLarge = 40.0; + static const double spacingExtraLarge = 48.0; + + // Font Styles + + static const TextStyle fontStyleSmall = TextStyle( + fontFamily: 'Nunito', + fontSize: fontSizeSmall, + fontWeight: FontWeight.w400, + color: colorBlack, + ); + + static const TextStyle fontStyleDefault = TextStyle( + fontFamily: 'Nunito', + fontSize: fontSizeDefault, + fontWeight: FontWeight.w400, + color: colorBlack, + ); + + static const TextStyle fontStyleDefaultBold = TextStyle( + fontFamily: 'Nunito', + fontSize: fontSizeDefault, + fontWeight: FontWeight.bold, + color: colorBlack, + ); + + static const TextStyle fontStyleMedium = TextStyle( + fontFamily: 'Nunito', + fontSize: fontSizeMedium, + fontWeight: FontWeight.w400, + color: colorBlack, + ); + + static const TextStyle fontStyleHeadingDefault = TextStyle( + fontFamily: 'PT Serif', + fontSize: fontSizeDefault, + fontWeight: FontWeight.w400, + color: colorBlack, + ); + + static const TextStyle fontStyleHeadingMedium = TextStyle( + fontFamily: 'PT Serif', + fontSize: fontSizeMedium, + fontWeight: FontWeight.w400, + color: colorBlack, + ); + + static const TextStyle fontStyleLarge = TextStyle( + fontFamily: 'PT Serif', + fontSize: fontSizeLarge, + fontWeight: FontWeight.w400, + color: colorRed, + ); + + // Padding + static const EdgeInsets paddingDefault = EdgeInsets.all(spacingDefault); + static const EdgeInsets paddingSmall = EdgeInsets.all(spacingSmall); + static const EdgeInsets paddingTiny = EdgeInsets.all(spacingTiny); + static const EdgeInsets paddingBottom = + EdgeInsets.only(bottom: spacingDefault); + static const EdgeInsets paddingSymetricHorizontal = + EdgeInsets.symmetric(horizontal: spacingDefault); + static const EdgeInsets paddingSymetricVertical = + EdgeInsets.symmetric(vertical: spacingDefault); + + // Border Radius + static BorderRadius borderRadiusSmall = BorderRadius.circular(4); + static BorderRadius borderRadius = BorderRadius.circular(8); + + // Shapes + static ShapeBorder rrShapeSmall = RoundedRectangleBorder( + borderRadius: borderRadiusSmall, + ); + + static ShapeBorder rrShape = RoundedRectangleBorder( + borderRadius: borderRadius, + ); + + // Borders + static Border cardBorder = Border.all(color: borderColor, width: 1); + static OutlineInputBorder textfieldBorder = OutlineInputBorder( + borderSide: const BorderSide(color: borderColor, width: 1), + borderRadius: borderRadius, + ); + + static const UnderlineInputBorder textfieldUnderlineBorder = + UnderlineInputBorder( + borderSide: BorderSide(color: borderColor, width: 1), + ); + + // Decorations + static BoxDecoration cardDecoration = BoxDecoration( + borderRadius: borderRadius, border: cardBorder, color: colorWhite); +} diff --git a/sharekhan_admin_panel/lib/utils/get_uuid.dart b/sharekhan_admin_panel/lib/utils/get_uuid.dart new file mode 100644 index 00000000..1bbccbb2 --- /dev/null +++ b/sharekhan_admin_panel/lib/utils/get_uuid.dart @@ -0,0 +1,5 @@ +import 'package:uuid/uuid.dart'; + +String getUUID() { + return const Uuid().v4(); +} diff --git a/sharekhan_admin_panel/lib/widgets/common/add_tab_footer.dart b/sharekhan_admin_panel/lib/widgets/common/add_tab_footer.dart new file mode 100644 index 00000000..1b0ebcdc --- /dev/null +++ b/sharekhan_admin_panel/lib/widgets/common/add_tab_footer.dart @@ -0,0 +1,64 @@ +import 'package:sharekhan_admin_panel/constants/asset_constants.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_icon_button.dart'; +import 'package:sharekhan_admin_panel/widgets/common/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class AddTabFooter extends StatelessWidget { + final String goBackrouteName; + final String buttonText; + final VoidCallback onAdd; + final VoidCallback onReset; + const AddTabFooter({ + super.key, + required this.goBackrouteName, + required this.onAdd, + required this.onReset, + required this.buttonText, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const Divider(color: AppTheme.borderColor, thickness: 1), + const Spacing(size: AppTheme.spacingSmall), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomIconButton( + onTap: () => context.go(goBackrouteName), + text: 'Go Back', + fillColor: AppTheme.colorWhite, + textColor: AppTheme.colorBlack, + borderColor: AppTheme.colorBlack, + iconColor: AppTheme.colorBlack, + icon: iconBack, + ), + Row( + children: [ + CustomIconButton( + onTap: onReset, + text: 'Reset Information', + fillColor: AppTheme.colorBlack, + textColor: AppTheme.colorWhite, + icon: iconTrial, + ), + const Spacing(size: AppTheme.spacingSmall, isHorizontal: true), + CustomIconButton( + onTap: onAdd, + text: buttonText, + fillColor: AppTheme.colorRed, + textColor: AppTheme.colorWhite, + icon: iconAdd, + ), + ], + ) + ], + ), + const Spacing(size: AppTheme.spacingMedium), + ], + ); + } +} diff --git a/sharekhan_admin_panel/lib/widgets/common/custom_button.dart b/sharekhan_admin_panel/lib/widgets/common/custom_button.dart new file mode 100644 index 00000000..c01bbb7b --- /dev/null +++ b/sharekhan_admin_panel/lib/widgets/common/custom_button.dart @@ -0,0 +1,40 @@ +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:flutter/material.dart'; + +class CustomButton extends StatelessWidget { + final VoidCallback onTap; + final String text; + final Color fillColor; + final Color textColor; + final Color borderColor; + const CustomButton({ + super.key, + required this.onTap, + required this.text, + required this.fillColor, + this.borderColor = Colors.transparent, + required this.textColor, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Container( + height: 40, + padding: AppTheme.paddingTiny, + decoration: BoxDecoration( + color: fillColor, + borderRadius: AppTheme.borderRadius, + border: Border.all(color: borderColor), + ), + child: Center( + child: Text( + text, + style: AppTheme.fontStyleSmall.copyWith(color: textColor), + ), + ), + ), + ); + } +} diff --git a/sharekhan_admin_panel/lib/widgets/common/custom_dropdown.dart b/sharekhan_admin_panel/lib/widgets/common/custom_dropdown.dart new file mode 100644 index 00000000..cf4e9e9c --- /dev/null +++ b/sharekhan_admin_panel/lib/widgets/common/custom_dropdown.dart @@ -0,0 +1,64 @@ +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:flutter/material.dart'; + +class CustomDropdown extends StatefulWidget { + final String hintText; + final List> items; + final ValueChanged onChanged; + final T? value; + + const CustomDropdown({ + super.key, + required this.hintText, + required this.items, + required this.onChanged, + this.value, + }); + + @override + CustomDropdownState createState() => CustomDropdownState(); +} + +class CustomDropdownState extends State> { + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + border: Border.all(color: AppTheme.borderColor), + borderRadius: AppTheme.borderRadius, + color: AppTheme.colorWhite, + ), + child: DropdownButtonFormField( + value: widget.value, + items: widget.items, + isExpanded: true, + iconEnabledColor: AppTheme.borderColor, + iconDisabledColor: AppTheme.borderColor, + onChanged: widget.onChanged, + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + hint: Text( + widget.hintText, + style: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + ), + decoration: InputDecoration( + isDense: true, + contentPadding: const EdgeInsets.only( + top: AppTheme.spacingExtraSmall, + left: AppTheme.spacingTiny, + bottom: AppTheme.spacingExtraSmall, + ), + hintStyle: AppTheme.fontStyleDefault.copyWith( + color: AppTheme.greyTextColor, + ), + border: AppTheme.textfieldBorder, + enabledBorder: AppTheme.textfieldBorder, + focusedBorder: AppTheme.textfieldBorder, + ), + ), + ); + } +} diff --git a/sharekhan_admin_panel/lib/widgets/common/custom_icon.dart b/sharekhan_admin_panel/lib/widgets/common/custom_icon.dart new file mode 100644 index 00000000..3ec45e6a --- /dev/null +++ b/sharekhan_admin_panel/lib/widgets/common/custom_icon.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +class CustomIcon extends StatelessWidget { + final String asset; + final double width; + final double height; + final Color? color; + + const CustomIcon({ + super.key, + required this.asset, + this.width = 24.0, + this.height = 24.0, + this.color, + }); + + @override + Widget build(BuildContext context) { + return Image.asset( + asset, + height: height, + width: width, + color: color, + ); + } +} diff --git a/sharekhan_admin_panel/lib/widgets/common/custom_icon_button.dart b/sharekhan_admin_panel/lib/widgets/common/custom_icon_button.dart new file mode 100644 index 00000000..a7b7cb00 --- /dev/null +++ b/sharekhan_admin_panel/lib/widgets/common/custom_icon_button.dart @@ -0,0 +1,59 @@ +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_icon.dart'; +import 'package:sharekhan_admin_panel/widgets/common/spacing.dart'; +import 'package:flutter/material.dart'; + +class CustomIconButton extends StatelessWidget { + final VoidCallback onTap; + final String text; + final String icon; + final Color fillColor; + final Color textColor; + final Color borderColor; + final Color iconColor; + const CustomIconButton({ + super.key, + required this.onTap, + required this.text, + required this.fillColor, + required this.textColor, + this.borderColor = Colors.transparent, + required this.icon, + this.iconColor = AppTheme.colorWhite, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: AppTheme.spacingSmall, + vertical: AppTheme.spacingTiny, + ), + decoration: BoxDecoration( + color: fillColor, + borderRadius: AppTheme.borderRadius, + border: Border.all(color: borderColor), + ), + child: Row( + children: [ + CustomIcon( + asset: icon, + color: iconColor, + ), + const Spacing( + size: AppTheme.spacingTiny, + isHorizontal: true, + ), + Text( + text, + style: + AppTheme.fontStyleHeadingDefault.copyWith(color: textColor), + ) + ], + ), + ), + ); + } +} diff --git a/sharekhan_admin_panel/lib/widgets/common/custom_loading.dart b/sharekhan_admin_panel/lib/widgets/common/custom_loading.dart new file mode 100644 index 00000000..2053be1e --- /dev/null +++ b/sharekhan_admin_panel/lib/widgets/common/custom_loading.dart @@ -0,0 +1,18 @@ +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:flutter/material.dart'; + +class CustomLoading extends StatelessWidget { + const CustomLoading({super.key}); + + @override + Widget build(BuildContext context) { + return const Scaffold( + backgroundColor: AppTheme.colorWhite, + body: Center( + child: CircularProgressIndicator( + color: AppTheme.colorRed, + ), + ), + ); + } +} diff --git a/sharekhan_admin_panel/lib/widgets/common/edit_tab_footer.dart b/sharekhan_admin_panel/lib/widgets/common/edit_tab_footer.dart new file mode 100644 index 00000000..8d5b3944 --- /dev/null +++ b/sharekhan_admin_panel/lib/widgets/common/edit_tab_footer.dart @@ -0,0 +1,45 @@ +import 'package:sharekhan_admin_panel/constants/asset_constants.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_icon_button.dart'; +import 'package:sharekhan_admin_panel/widgets/common/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class EditTabFooter extends StatelessWidget { + final String goBackrouteName; + final VoidCallback onSave; + const EditTabFooter( + {super.key, required this.goBackrouteName, required this.onSave}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const Divider(color: AppTheme.borderColor, thickness: 1), + const Spacing(size: AppTheme.spacingSmall), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + CustomIconButton( + onTap: () => context.go(goBackrouteName), + text: 'Go Back', + fillColor: AppTheme.colorWhite, + textColor: AppTheme.colorBlack, + borderColor: AppTheme.colorBlack, + iconColor: AppTheme.colorBlack, + icon: iconBack, + ), + CustomIconButton( + onTap: onSave, + text: 'Save', + fillColor: AppTheme.colorRed, + textColor: AppTheme.colorWhite, + icon: iconSave, + ) + ], + ), + const Spacing(size: AppTheme.spacingMedium), + ], + ); + } +} diff --git a/sharekhan_admin_panel/lib/widgets/common/main_tab_header.dart b/sharekhan_admin_panel/lib/widgets/common/main_tab_header.dart new file mode 100644 index 00000000..161ee3ea --- /dev/null +++ b/sharekhan_admin_panel/lib/widgets/common/main_tab_header.dart @@ -0,0 +1,61 @@ +import 'package:sharekhan_admin_panel/constants/asset_constants.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_icon_button.dart'; +import 'package:sharekhan_admin_panel/widgets/common/spacing.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class MainTabHeader extends StatelessWidget { + final String title; + final String secondButtonText; + final String routeName; + final VoidCallback onExport; + final bool showExportButton; + final bool showSecondButton; + const MainTabHeader({ + super.key, + required this.title, + required this.routeName, + required this.secondButtonText, + required this.onExport, + this.showExportButton = true, + this.showSecondButton = true, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: AppTheme.fontStyleDefaultBold.copyWith( + fontSize: AppTheme.fontSizeLarge, + ), + ), + Row( + children: [ + if (showExportButton) + CustomIconButton( + onTap: onExport, + text: 'Export', + icon: iconExport, + fillColor: AppTheme.colorBlack, + textColor: AppTheme.colorWhite, + ), + if (showSecondButton) ...[ + const Spacing(size: AppTheme.spacingSmall, isHorizontal: true), + CustomIconButton( + onTap: () => context.go(routeName), + text: secondButtonText, + icon: iconAdd, + fillColor: AppTheme.colorRed, + textColor: AppTheme.colorWhite, + ), + ], + ], + ) + ], + ); + } +} diff --git a/sharekhan_admin_panel/lib/widgets/common/secondary_tab_header.dart b/sharekhan_admin_panel/lib/widgets/common/secondary_tab_header.dart new file mode 100644 index 00000000..f154e286 --- /dev/null +++ b/sharekhan_admin_panel/lib/widgets/common/secondary_tab_header.dart @@ -0,0 +1,47 @@ +import 'package:sharekhan_admin_panel/constants/asset_constants.dart'; +import 'package:sharekhan_admin_panel/theme/app_theme.dart'; +import 'package:sharekhan_admin_panel/widgets/common/custom_icon.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +class BreadcrumbTabHeader extends StatelessWidget { + final String mainTitle; + final String secondaryTitle; + final String goBackRoute; + const BreadcrumbTabHeader( + {super.key, + required this.goBackRoute, + required this.mainTitle, + required this.secondaryTitle}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + InkWell( + onTap: () => context.go(goBackRoute), + hoverColor: Colors.transparent, + child: Text( + mainTitle, + style: AppTheme.fontStyleDefaultBold.copyWith( + fontSize: AppTheme.fontSizeLarge, color: AppTheme.colorGrey), + ), + ), + Padding( + padding: AppTheme.paddingSymetricHorizontal.copyWith( + left: AppTheme.spacingTiny, right: AppTheme.spacingTiny), + child: const CustomIcon( + asset: iconChevronRight, + color: AppTheme.colorGrey, + ), + ), + Text( + secondaryTitle, + style: AppTheme.fontStyleDefaultBold.copyWith( + fontSize: AppTheme.fontSizeLarge, + ), + ) + ], + ); + } +} diff --git a/sharekhan_admin_panel/lib/widgets/common/spacing.dart b/sharekhan_admin_panel/lib/widgets/common/spacing.dart new file mode 100644 index 00000000..bcf4d28d --- /dev/null +++ b/sharekhan_admin_panel/lib/widgets/common/spacing.dart @@ -0,0 +1,20 @@ +import 'package:flutter/widgets.dart'; + +class Spacing extends StatelessWidget { + final double size; + final bool isHorizontal; + + const Spacing({ + super.key, + required this.size, + this.isHorizontal = false, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: isHorizontal ? null : size, + width: isHorizontal ? size : null, + ); + } +} diff --git a/sharekhan_admin_panel/pubspec.yaml b/sharekhan_admin_panel/pubspec.yaml new file mode 100644 index 00000000..0e32b971 --- /dev/null +++ b/sharekhan_admin_panel/pubspec.yaml @@ -0,0 +1,100 @@ +name: sharekhan_admin_panel +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: "none" # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ">=3.4.0 <4.0.0" + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.6 + firebase_core: ^3.3.0 + cloud_firestore: ^5.2.1 + syncfusion_flutter_datagrid: ^26.2.8 + intl: ^0.19.0 + uuid: ^4.4.2 + image_picker_web: ^4.0.0 + firebase_storage: ^12.1.3 + go_router: ^14.2.3 + csv: ^6.0.0 + html: ^0.15.4 + get: ^4.6.6 + provider: ^6.1.2 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^3.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + assets: + - assets/icons/ + - assets/images/ + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/sharekhan_admin_panel/storage.rules b/sharekhan_admin_panel/storage.rules new file mode 100644 index 00000000..f08744f0 --- /dev/null +++ b/sharekhan_admin_panel/storage.rules @@ -0,0 +1,12 @@ +rules_version = '2'; + +// Craft rules based on data in your Firestore database +// allow write: if firestore.get( +// /databases/(default)/documents/users/$(request.auth.uid)).data.isAdmin; +service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + allow read, write: if false; + } + } +} diff --git a/sharekhan_admin_panel/test/widget_test.dart b/sharekhan_admin_panel/test/widget_test.dart new file mode 100644 index 00000000..ba27e28c --- /dev/null +++ b/sharekhan_admin_panel/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:sharekhan_admin_panel/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/sharekhan_admin_panel/web/favicon.png b/sharekhan_admin_panel/web/favicon.png new file mode 100644 index 00000000..8aaa46ac Binary files /dev/null and b/sharekhan_admin_panel/web/favicon.png differ diff --git a/sharekhan_admin_panel/web/icons/Icon-192.png b/sharekhan_admin_panel/web/icons/Icon-192.png new file mode 100644 index 00000000..b749bfef Binary files /dev/null and b/sharekhan_admin_panel/web/icons/Icon-192.png differ diff --git a/sharekhan_admin_panel/web/icons/Icon-512.png b/sharekhan_admin_panel/web/icons/Icon-512.png new file mode 100644 index 00000000..88cfd48d Binary files /dev/null and b/sharekhan_admin_panel/web/icons/Icon-512.png differ diff --git a/sharekhan_admin_panel/web/icons/Icon-maskable-192.png b/sharekhan_admin_panel/web/icons/Icon-maskable-192.png new file mode 100644 index 00000000..eb9b4d76 Binary files /dev/null and b/sharekhan_admin_panel/web/icons/Icon-maskable-192.png differ diff --git a/sharekhan_admin_panel/web/icons/Icon-maskable-512.png b/sharekhan_admin_panel/web/icons/Icon-maskable-512.png new file mode 100644 index 00000000..d69c5669 Binary files /dev/null and b/sharekhan_admin_panel/web/icons/Icon-maskable-512.png differ diff --git a/sharekhan_admin_panel/web/index.html b/sharekhan_admin_panel/web/index.html new file mode 100644 index 00000000..232b0602 --- /dev/null +++ b/sharekhan_admin_panel/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + sharekhan_admin_panel + + + + + + diff --git a/sharekhan_admin_panel/web/manifest.json b/sharekhan_admin_panel/web/manifest.json new file mode 100644 index 00000000..6c5af5bd --- /dev/null +++ b/sharekhan_admin_panel/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "sharekhan_admin_panel", + "short_name": "sharekhan_admin_panel", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +}