commit db55c10f430d6b24380eace0cf60f25008bcff45 Author: undefined Date: Fri Jan 24 20:05:48 2025 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..633ec24 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dev-dist + +*.local +.env +env +release +config.php +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.sln +*.njsproj +*.sw? + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2346826 --- /dev/null +++ b/README.md @@ -0,0 +1,202 @@ +# ERGO BOOKING SYSTEM + +## Table of Contents +1. [User Credentials](#user-credentials) +2. [Environment Variables](#environment-variables) +3. [Booking Status](#booking-status) +4. [Archive Status](#archive-status) +5. [Active Booking Details](#active-booking-details) +6. [Project Overview](#project-overview) +7. [Issues](#issues) + +## User Credentials + +### Host +- **Email:** fugnomuydi@gufum.com +- **Password:** Tamash!555 + +### Customer +- **Email:** joecus@gmail.com +- **Password:** Tamash!555 + +### Admin +- **Email:** adminergo@manaknight.com +- **Password:** a123456 + +## Environment Variables +- **VITE_REACT_STRIPE_PUBLIC_KEY:** `pk_test_51Ll5ukBgOlWo0lDUrBhA2W7EX2MwUH9AR5Y3KQoujf7PTQagZAJylWP1UOFbtH4UwxoufZbInwehQppWAq53kmNC00UIKSmebO` +- **VITE_GOOGLE_API_KEY:** CREATE ONE YOURSELF +- **VITE_RECAPTCHA_SITE_KEY:** CREATE ONE YOURSELF + +## Booking Status +- **PENDING** +- **UPCOMING** +- **ONGOING** +- **COMPLETED** +- **DECLINED** +- **CANCELLED** +- **DELETED** + +## Archive Status +- **IS_ARCHIVE:** 1 +- **NOT_ARCHIVE:** 0 + +## Active Booking Details +- An active booking has the following details: + { + "addon_cost", + "id", + "create_at", + "update_at", + "property_space_id", + "customer_id", + "host_id", + "stripe_payment_intent_id", + "booked_unit", + "payment_method", + "status", + "payment_status", + "booking_start_time", + "booking_end_time", + "duration", + "queued", + "tax_rate", + "commission_rate", + "num_guests", + "reason", + "deleted_at", + "host_first_name", + "host_last_name", + "customer_first_name", + "customer_last_name", + "property_id", + "property_name", + "space_category", + "image_url", + "hourly_rate", + "rate", + "tax", + "commission", + "total", + "address_line_1", + "address_line_2", + "property_spaces_id", + "property_city", + "property_country", + "email", + "host_email" + } + + +#### The Issues Are Listed Below + +1. When u log in using the host credentials, In the host property space page, u will find two active property spaces. + Then Log in via another browser (or Incognito mode) using the customer credentials,on the Home page, navigate to "/explore?section=new-spaces" and search for "Lili" property space (which belongs to the host). + + Create a booking as a customer on the "Lili" Property space, select the available time slots, proceed to make payment and confirm booking. + + Then switch to the host account and navigate to "/account/my-bookings" route, You will see the booking you just created as a customer. Approve the bookings made from customers. + + Now the issues here are that: + - **a.** It allows the fromTime to be greater than ToTime (For example, u can select a booking time by 6pm and end by 5pm). There is a comment that tells you to add a code that will should fix that. + + - **b.** Upcoming/Ongoing customers bookings slot time for the property space should be disabled when a customer is selecting time slots for a new booking to prevent new bookings clashing with such slot times + + Please fix these issues. (Tip - There are in DateTimePicker.jsx file) + +2. Inside AddCardMethodModal.jsx (route - "/account/billing"), add a simple UI Modal for adding new cards. + On filling it, make sure errors are checked, cards should be submitted to DB through "addNewCard" function. + +3. SpaceDetailsTwo.jsx is a file that contains logic for completing the second step when creating a property space. + (In this step, we add images, amenities, addons, FAQs for the property space) + + - **a.** Using FileUploader library, allow max of 6 images, each image should not exceed + 1MB, and allow preview of images when selected. + + - **b.** using the #append_faq_btn btn, add logic that allows more faqs UI (question and answer) to be appended to the fields prop from useFieldArray when the btn is clicked. + +4. In Signup Page, either as a host or a customer. One step is missing - A Recaptcha feature. + + Please add a recaptcha feature, that disallows the submit button if user hasn't perform the recaptcha step. + + Also please make sure that a valid email is submitted. + +5. The CustomStaticLocationAutoCompleteV2.jsx file component currently has an issue. + It doesn't show the user typed in text. When I type in a location, Is not visible in the searh input. Please Fix it. + +6. As a logged in User (Either as Host or Customer) - From the HostProfilePage.jsx and CustomerProfilePage.jsx. + + - **a.** Please add a prompt modal to comfirm if user wants to remove a profile picture + + - **b.** Please add a modal to preview selected image from user device (when user intends to update profile picture) and then proceed to update profile picture OR close modal if otherwise + +7. Please make sure u initiate chat from the bookings page (/account/my-bookings). + + Inside MessagePage.jsx file. There is a sendMessage function. Before a message is sent, check to make sure that the following are adhere to: + + - **a.** No sharing of links, urls or the use of profane langauges + + - **b.** if selected booking (activeBooking) ISN'T ongoing (ONGOING state), don't allow sharing of phone number or email + + - **c.** if selected booking (activeBooking) messages between host and customer is up to three(3) and booking is not ongoing, don't allow new messages to be sent + + - **d.** from the badWords.json file, make sure the message to be sent doesn't contain any word from the file. + + Note that, with the host and customer account credentials, u can login and create bookings. + +8. For every account, there is a message feature. + For example, if u login using the customer credentials and proceed to this route - "/account/messages?message_tab=inbox", you will find the messages between the user and other users. + + Now each chat can be archived and unarchived. We have the UI for it. But we need the API implementation. Under MessagePage.jsx file, you will find two defined functions (archiveRoom and UnarchiveRoom) + + Payload sample + { + "id": active_room_id, + "is_archive": 0 || 1 + } + + SO please implement them, and then update selected room chat on API success. And switching of tabs, showing the selected room chat. + +9. When logged in as a Host User, Navigate to the host spaces tab. You will find a draft property space ("Draft Space") + + Click on "view details", You will then be redirected to the "property space details page", + Click on "About location", and then the space details page (name, location, rate, rules, etc) will be fetched. + + Now the issue here, is that the details are not loaded into the input UI that we have. (since it is a draft space that has these details saved already and its been fetched). + + Please fix the issue - preload the space details into the UI that we have + +10. Tour Guide Issue - Logged in as either host or customer. + Click on the avater icon on the far right hand, and on the drop down menu, click on "Help me get started", You will be given a tour of the App. But we got issues: + + Step 6 - should navigate to "/account/verification", and place the modal at the bottom of the page + + Step 7 - + - **a.** should highlight the "Submit Document" Button in "/account/verification" route. + + - **b.** Add heading text to the tour modal - "Click the submit button" + + - **c.** A body text - "Once approved, you will receive an email with approval confirmation from our support team and your account will be activated. For questions or concerns, please navigate to the FAQs page." + + - **d.** allow going to both next and previous steps + +11. When user is signing up. On the "Finish Signing up page" + - **a.** User needs to agree to "Terms and Conditions" + + - **b.** open Privacy and Policy Modal and read to the end + + - **c.** DOB must be at least 18 years + + - **d.** first and lastName are required + + - **e.** Password must be + i. at least 10 characters long + ii. Contain at least one digit, one lowercase, one upercase, one symbol + iii. from the common-passwords.json file, make sure password doesn't contain any word from there. + iv. mustn't contain the entered first name, last name or DOB. + + Show these errors as user is inputting the password (incase it matches any) + + In simpler terms, disabled the button till the red flags are passed. + + diff --git a/ReactSSRService.js b/ReactSSRService.js new file mode 100644 index 0000000..ff4530c --- /dev/null +++ b/ReactSSRService.js @@ -0,0 +1,41 @@ +const path = require("path"); +const fs = require("fs"); + +const DEFAULT_OPTIONS = { + withSSR: { metadata: { title: "", description: "" } }, +}; + +module.exports = { + withSSR(options = DEFAULT_OPTIONS.withSSR) { + options = { ...DEFAULT_OPTIONS.withSSR, ...options }; + + return async function (_, res, next) { + try { + const file = fs.readFileSync( + path.join(__dirname, "dist", "index.html"), + "utf-8" + ); + + if (!file) { + return next(); + } + + const title = options.metadata?.title; + const description = options.metadata?.description; + const version = options.version ?? "1.0.0"; + console.log(options); + const final = file + ?.replace(new RegExp("{{{title}}}", "g"), title) + ?.replace(new RegExp("{{{description}}}", "g"), description); + // ?.replace("__BUILDNUMBER__", version); + + return res.status(200).send(final); + } catch (error) { + if (process.env.DEBUG === "TRUE") { + console.log("React SSR Error", error, __filename); + } + return next(); + } + }; + }, +}; diff --git a/app.js b/app.js new file mode 100644 index 0000000..0dd2768 --- /dev/null +++ b/app.js @@ -0,0 +1,43 @@ +const express = require("express"); +const path = require("path"); +const cors = require("cors"); +const clientMetadata = require("./metadata.json"); +const { withSSR } = require("./ReactSSRService"); + +let app = express(); + +app.use(express.json()); +app.use( + express.urlencoded({ + extended: false, + }) +); +app.use(cors()); + +if (process.env.IS_HTTPS === "true") { + app.set("trust proxy", 1); + session.cookie.secure = true; + session.cookie.sameSite = "strict"; +} + +// SSR +const clientMetadataArray = Object.entries(clientMetadata); +clientMetadataArray.forEach(([route, metadata]) => { + app.get(route, withSSR({ metadata })); +}); + +app.use(express.static(path.join(__dirname, "dist"))); + +app.use((err, req, res, next) => { + res.locals.message = err.message; + res.locals.error = req.app.get("env") === "development" ? err : {}; + + return res.status(err.status || 500).json({ + message: err.message, + }); +}); + +//404 +app.use(withSSR({ metadata: clientMetadata[""] })); + +module.exports = app; diff --git a/index.html b/index.html new file mode 100644 index 0000000..646d4d9 --- /dev/null +++ b/index.html @@ -0,0 +1,59 @@ + + + + + + + + + + + Ergo App + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + \ No newline at end of file diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..ffb7a53 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + } + } +} \ No newline at end of file diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..ae626a2 --- /dev/null +++ b/metadata.json @@ -0,0 +1,21 @@ +{ + "/": {"title": "Something else", "description": "Here are a set of free tools to use for you company", "twitter_image": ""}, + "/login": {"title": "Login Title", "description": ""}, + "/account": {"title": "Account Title", "description": ""}, + "/explore": {"title": "Explore Title", "description": ""}, + "/favorites": {"title": "Your Favorites", "description": ""}, + "/faq": {"title": "FAQ", "description": ""}, + "/contact-us": {"title": "Contact Us", "description": ""}, + "/account/my-bookings": {"title": "Bookings", "description": ""}, + "/account/my-spaces": {"title": "My Spaces", "description": ""}, + "/account/profile": {"title": "Profile", "description": ""}, + "/account/payments": {"title": "Payments", "description": ""}, + "/account/billings": {"title": "Billings", "description": ""}, + "/account/reviews": {"title": "Reviews", "description": ""}, + "/account/my-spaces/:id": {"title": "Dynamic", "description": ""}, +"": { + "title": "404 Page not found", + "description": "Oops. Looks like this page doesn't exists" +} + +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6e96643 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8328 @@ +{ + "name": "adminportal", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "adminportal", + "version": "0.0.0", + "dependencies": { + "@headlessui/react": "^1.7.13", + "@headlessui/tailwindcss": "^0.1.2", + "@heroicons/react": "^2.0.17", + "@hookform/resolvers": "^2.8.10", + "@mantine/core": "^7.3.1", + "@mantine/form": "^7.3.2", + "@mantine/hooks": "^7.3.1", + "@mantine/notifications": "^7.3.2", + "@reactour/tour": "^3.6.1", + "@stripe/react-stripe-js": "^1.9.0", + "@stripe/stripe-js": "^1.32.0", + "@uppy/aws-s3": "^2.1.0", + "@uppy/core": "^2.2.0", + "@uppy/dashboard": "^2.1.4", + "@uppy/drag-drop": "^2.1.0", + "@uppy/dropbox": "^2.0.5", + "@uppy/google-drive": "^2.0.5", + "@uppy/onedrive": "^2.0.6", + "@uppy/react": "^2.2.0", + "@uppy/tus": "^2.3.0", + "@uppy/xhr-upload": "^2.1.0", + "axios": "^1.1.3", + "body-scroll-lock": "^4.0.0-beta.0", + "cors": "^2.8.5", + "device-uuid": "^1.0.4", + "dompurify": "^3.0.3", + "ejs": "^3.1.8", + "emoji-picker-react": "^4.4.7", + "express": "^4.18.2", + "intro.js": "^7.2.0", + "intro.js-react": "^1.0.0", + "linkifyjs": "^4.0.2", + "moment": "^2.29.3", + "react": "^18.0.0", + "react-calendar": "^4.0.0", + "react-dom": "^18.0.0", + "react-drag-drop-files": "^2.3.8", + "react-google-autocomplete": "^2.7.1", + "react-google-recaptcha": "^2.1.0", + "react-hook-form": "^7.34.2", + "react-html-table-to-excel": "^2.0.0", + "react-infinite-scroll-component": "^6.1.0", + "react-joyride": "^2.5.5", + "react-json-to-csv": "^1.2.0", + "react-loading-skeleton": "^3.1.0", + "react-router": "^6.2.2", + "react-router-dom": "^6.2.2", + "react-shepherd": "^4.2.0", + "react-tooltip": "^5.4.0", + "reactour": "^1.19.0", + "sanitize-html": "^2.10.0", + "suneditor": "^2.44.3", + "suneditor-react": "^3.4.1", + "swiper": "^8.4.4", + "uppy": "^2.9.1", + "uuid": "^9.0.0", + "vite-plugin-html": "^3.2.0", + "yup": "^0.32.11" + }, + "devDependencies": { + "@tailwindcss/custom-forms": "^0.2.1", + "@types/node": "^18.11.17", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@vitejs/plugin-react": "^1.3.0", + "autoprefixer": "^10.4.7", + "postcss": "^8.4.14", + "prettier": "^2.8.4", + "prettier-plugin-tailwindcss": "^0.2.5", + "tailwindcss": "^3.2.7", + "vite": "^2.9.9", + "vite-plugin-svgr": "^2.2.2" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.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==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz", + "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz", + "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-compilation-targets": "^7.24.7", + "@babel/helper-module-transforms": "^7.24.7", + "@babel/helpers": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/template": "^7.24.7", + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7", + "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/generator": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz", + "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7", + "@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-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz", + "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.24.7", + "@babel/helper-validator-option": "^7.24.7", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "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==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz", + "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz", + "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==", + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz", + "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz", + "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "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==", + "license": "MIT", + "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/parser": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.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==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.24.7.tgz", + "integrity": "sha512-+Dj06GDZEFRYvclU6k4bme55GKBEWUmByM/eoKuqg4zTNQHiApWRhQph5fxQB2wAEFvRzL1tOEj1RJ19wJrhoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-jsx": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.24.7.tgz", + "integrity": "sha512-QG9EnzoGn+Qar7rxuW+ZOsbWOt56FvvI93xInqsZDC5fsekx1AlIO4KIJ5M+D0p0SqSH156EpmZyXq630B8OlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.7.tgz", + "integrity": "sha512-fOPQYbGSgH0HUp4UJO4sMBFjY6DuWq+2i8rixyUMb3CdGixs/gccURvYOAhajBdKDoGajFr3mUq5rH3phtkGzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.7.tgz", + "integrity": "sha512-J2z+MWzZHVOemyLweMqngXrgGC42jQ//R0KdxqkIz/OrbVIIlhFI3WigZ5fO+nwFvBlncr4MGapd8vTyc7RPNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz", + "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz", + "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.24.7", + "@babel/helper-environment-visitor": "^7.24.7", + "@babel/helper-function-name": "^7.24.7", + "@babel/helper-hoist-variables": "^7.24.7", + "@babel/helper-split-export-declaration": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/types": "^7.24.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz", + "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT" + }, + "node_modules/@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz", + "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz", + "integrity": "sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.5.tgz", + "integrity": "sha512-Nsdud2X65Dz+1RHjAIP0t8z5e2ff/IRbei6BqFrl1urT8sDVzM1HMQ+R0XcU5ceRfyO3I6ayeqIfh+6Wb8LGTw==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.0.0", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.26.17", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.17.tgz", + "integrity": "sha512-ESD+jYWwqwVzaIgIhExrArdsCL1rOAzryG/Sjlu8yaD3Mtqi3uVyhbE2V7jD58Mo52qbzKz2eUY/Xgh5I86FCQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.0", + "@floating-ui/utils": "^0.2.0", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.0.tgz", + "integrity": "sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.2.tgz", + "integrity": "sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw==", + "license": "MIT" + }, + "node_modules/@gilbarbara/deep-equal": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@gilbarbara/deep-equal/-/deep-equal-0.3.1.tgz", + "integrity": "sha512-I7xWjLs2YSVMc5gGx1Z3ZG1lgFpITPndpi8Ku55GeEIKpACCPQNS/OTqQbxgTCfq0Ncvcc+CrFov96itVh6Qvw==", + "license": "MIT" + }, + "node_modules/@headlessui/react": { + "version": "1.7.19", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.19.tgz", + "integrity": "sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw==", + "license": "MIT", + "dependencies": { + "@tanstack/react-virtual": "^3.0.0-beta.60", + "client-only": "^0.0.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18", + "react-dom": "^16 || ^17 || ^18" + } + }, + "node_modules/@headlessui/tailwindcss": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@headlessui/tailwindcss/-/tailwindcss-0.1.3.tgz", + "integrity": "sha512-3aMdDyYZx9A15euRehpppSyQnb2gIw2s/Uccn2ELIoLQ9oDy0+9oRygNWNjXCD5Dt+w1pxo7C+XoiYvGcqA4Kg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "tailwindcss": "^3.0" + } + }, + "node_modules/@heroicons/react": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.1.4.tgz", + "integrity": "sha512-ju0wj0wwrUTMQ2Yceyrma7TKuI3BpSjp+qKqV81K9KGcUHdvTMdiwfRc2cwXBp3uXtKuDZkh0v03nWOQnJFv2Q==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16" + } + }, + "node_modules/@hookform/resolvers": { + "version": "2.9.11", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.9.11.tgz", + "integrity": "sha512-bA3aZ79UgcHj7tFV7RlgThzwSSHZgvfbt2wprldRkYBcMopdMvHyO17Wwp/twcJasNFischFfS7oz8Katz8DdQ==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@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==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mantine/core": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.10.2.tgz", + "integrity": "sha512-sPqJY2A+zHAhi7/mJKL2EH92jKc6JDACJY17gXS+FcbIQgiaY1rxA/tdcybpq8FbswSgUYZO6CRL6XWEhatw5w==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.26.9", + "clsx": "^2.1.1", + "react-number-format": "^5.3.1", + "react-remove-scroll": "^2.5.7", + "react-textarea-autosize": "8.5.3", + "type-fest": "^4.12.0" + }, + "peerDependencies": { + "@mantine/hooks": "7.10.2", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } + }, + "node_modules/@mantine/form": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@mantine/form/-/form-7.10.2.tgz", + "integrity": "sha512-OlXQ04orkwQO+AEeA4OihYtfxpaoK/LC1r2/nnUQmChG/GO1X9MoEW8oTQYKyYDIpQc8+lHhos4gl9dEF5YAWw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "klona": "^2.0.6" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/@mantine/hooks": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.10.2.tgz", + "integrity": "sha512-3m4flbR2yv3Bl21pHl5BKOOnqrInp/gVD72rozLeu/jzIZqQy8yFRTY2bUWCebPwNem//OD1rCORsBXNXvq31g==", + "license": "MIT", + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/@mantine/notifications": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.10.2.tgz", + "integrity": "sha512-wX6qNBvpV7iqlH98AkGuS9plq02yYhTG7bkzP3Y7jd7o2ognLPoN83YeIaxzuZ/qVnWrwZrOHOx87Ox2e9Qyxw==", + "license": "MIT", + "dependencies": { + "@mantine/store": "7.10.2", + "react-transition-group": "4.4.5" + }, + "peerDependencies": { + "@mantine/core": "7.10.2", + "@mantine/hooks": "7.10.2", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } + }, + "node_modules/@mantine/store": { + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.10.2.tgz", + "integrity": "sha512-izT4ivE2Ka2NBTjy5Ck31F3sSybCBLXJhX/ESDCasVR9MKD2Ci2Y6nbm0UtBdPf4+PrDPZtaPwqQzL92uZLtCQ==", + "license": "MIT", + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@reactour/mask": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@reactour/mask/-/mask-1.1.0.tgz", + "integrity": "sha512-GkJMLuTs3vTsm4Ryq2uXcE4sMzRP1p4xSd6juSOMqbHa7IVD/UiLCLqJWHR9xGSQPbYhpZAZAORUG5cS0U5tBA==", + "license": "MIT", + "dependencies": { + "@reactour/utils": "*" + }, + "peerDependencies": { + "react": "16.x || 17.x || 18.x" + } + }, + "node_modules/@reactour/popover": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@reactour/popover/-/popover-1.2.0.tgz", + "integrity": "sha512-1JMykZ+MmDmRlEVC5+DwlvK1exwV5bFHtiSFwVXnoPZmsSfwId6SLyjo9H6bybeuNtNEzviKdsF/ZBC1UQbDqg==", + "license": "MIT", + "dependencies": { + "@reactour/utils": "*" + }, + "peerDependencies": { + "react": "16.x || 17.x || 18.x" + } + }, + "node_modules/@reactour/tour": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@reactour/tour/-/tour-3.7.0.tgz", + "integrity": "sha512-p0USaOBc5fcNBS5ZiQ2lsmztAhIGCUfx913Zw14FbEM8bhSXpR1F2JD0alVj9Ya1N+pnTNYatf14rSNGJsEnCg==", + "license": "MIT", + "dependencies": { + "@reactour/mask": "*", + "@reactour/popover": "*", + "@reactour/utils": "*" + }, + "peerDependencies": { + "react": "16.x || 17.x || 18.x" + } + }, + "node_modules/@reactour/utils": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@reactour/utils/-/utils-0.5.0.tgz", + "integrity": "sha512-yQs5Nm/Dg1xRM7d/S/UILBV5OInrTgrjGzgc81/RP5khqdO5KnpOaC46yF83kDtCalte8X3RCwp+F2YA509k1w==", + "license": "MIT", + "dependencies": { + "@rooks/use-mutation-observer": "^4.11.2", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "react": "16.x || 17.x || 18.x" + } + }, + "node_modules/@remix-run/router": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", + "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "license": "MIT", + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@rooks/use-mutation-observer": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@rooks/use-mutation-observer/-/use-mutation-observer-4.11.2.tgz", + "integrity": "sha512-vpsdrZdr6TkB1zZJcHx+fR1YC/pHs2BaqcuYiEGjBVbwY5xcC49+h0hAUtQKHth3oJqXfIX/Ng8S7s5HFHdM/A==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@stripe/react-stripe-js": { + "version": "1.16.5", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-1.16.5.tgz", + "integrity": "sha512-lVPW3IfwdacyS22pP+nBB6/GNFRRhT/4jfgAK6T2guQmtzPwJV1DogiGGaBNhiKtSY18+yS8KlHSu+PvZNclvQ==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@stripe/stripe-js": "^1.44.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@stripe/stripe-js": { + "version": "1.54.2", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-1.54.2.tgz", + "integrity": "sha512-R1PwtDvUfs99cAjfuQ/WpwJ3c92+DAMy9xGApjqlWQMj0FKQabUAys2swfTRNzuYAYJh7NqK2dzcYVNkKLEKUg==", + "license": "MIT" + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.5.1.tgz", + "integrity": "sha512-9PYGcXrAxitycIjRmZB+Q0JaN07GZIWaTBIGQzfaZv+qr1n8X1XUEJ5rZ/vx6OVD9RRYlrNnXWExQXcmZeD/BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-6.5.1.tgz", + "integrity": "sha512-8DPaVVE3fd5JKuIC29dqyMB54sA6mfgki2H2+swh+zNJoynC8pMPzOkidqHOSc6Wj032fhl8Z0TVn1GiPpAiJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-6.5.1.tgz", + "integrity": "sha512-FwOEi0Il72iAzlkaHrlemVurgSQRDFbk0OC8dSvD5fSBPHltNh7JtLsxmZUhjYBZo2PpcU/RJvvi6Q0l7O7ogw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-6.5.1.tgz", + "integrity": "sha512-gWGsiwjb4tw+ITOJ86ndY/DZZ6cuXMNE/SjcDRg+HLuCmwpcjOktwRF9WgAiycTqJD/QXqL2f8IzE2Rzh7aVXA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-6.5.1.tgz", + "integrity": "sha512-2jT3nTayyYP7kI6aGutkyfJ7UMGtuguD72OjeGLwVNyfPRBD8zQthlvL+fAbAKk5n9ZNcvFkp/b1lZ7VsYqVJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-6.5.1.tgz", + "integrity": "sha512-a1p6LF5Jt33O3rZoVRBqdxL350oge54iZWHNI6LJB5tQ7EelvD/Mb1mfBiZNAan0dt4i3VArkFRjA4iObuNykQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-preset": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-6.5.1.tgz", + "integrity": "sha512-6127fvO/FF2oi5EzSQOAjo1LE3OtNVh11R+/8FXa+mHx1ptAaS4cknIjnUA7e6j6fwGGJ17NzaTJFUwOV2zwCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "^6.5.1", + "@svgr/babel-plugin-remove-jsx-attribute": "*", + "@svgr/babel-plugin-remove-jsx-empty-expression": "*", + "@svgr/babel-plugin-replace-jsx-attribute-value": "^6.5.1", + "@svgr/babel-plugin-svg-dynamic-title": "^6.5.1", + "@svgr/babel-plugin-svg-em-dimensions": "^6.5.1", + "@svgr/babel-plugin-transform-react-native-svg": "^6.5.1", + "@svgr/babel-plugin-transform-svg-component": "^6.5.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/core": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-6.5.1.tgz", + "integrity": "sha512-/xdLSWxK5QkqG524ONSjvg3V/FkNyCv538OIBdQqPNaAta3AsXj/Bd2FbvR87yMbXO2hFSWiAe/Q6IkVPDw+mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.19.6", + "@svgr/babel-preset": "^6.5.1", + "@svgr/plugin-jsx": "^6.5.1", + "camelcase": "^6.2.0", + "cosmiconfig": "^7.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-6.5.1.tgz", + "integrity": "sha512-1hnUxxjd83EAxbL4a0JDJoD3Dao3hmjvyvyEV8PzWmLK3B9m9NPlW7GKjFyoWE8nM7HnXzPcmmSyOW8yOddSXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.0", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } + }, + "node_modules/@svgr/plugin-jsx": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-6.5.1.tgz", + "integrity": "sha512-+UdQxI3jgtSjCykNSlEMuy1jSRQlGC7pqBCPvkG/2dATdWo082zHTTK3uhnAju2/6XpE6B5mZ3z4Z8Ns01S8Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.19.6", + "@svgr/babel-preset": "^6.5.1", + "@svgr/hast-util-to-babel-ast": "^6.5.1", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "^6.0.0" + } + }, + "node_modules/@tailwindcss/custom-forms": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/custom-forms/-/custom-forms-0.2.1.tgz", + "integrity": "sha512-XdP5XY6kxo3x5o50mWUyoYWxOPV16baagLoZ5uM41gh6IhXzhz/vJYzqrTb/lN58maGIKlpkxgVsQUNSsbAS3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.11", + "mini-svg-data-uri": "^1.0.3", + "traverse": "^0.6.6" + }, + "peerDependencies": { + "tailwindcss": "^1.0" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.5.1.tgz", + "integrity": "sha512-jIsuhfgy8GqA67PdWqg73ZB2LFE+HD9hjWL1L6ifEIZVyZVAKpYmgUG4WsKQ005aEyImJmbuimPiEvc57IY0Aw==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.5.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.5.1.tgz", + "integrity": "sha512-046+AUSiDru/V9pajE1du8WayvBKeCvJ2NmKPy/mR8/SbKKrqmSbj7LJBfXE+nSq4f5TBXvnCzu0kcYebI9WdQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@transloadit/prettier-bytes": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz", + "integrity": "sha512-VeJbUb0wEKbcwaSlj5n+LscBl9IPgLPkHVGBkh00cztv6X4L/TJXK58LzFuBKX7/GAfiGhIwH67YTLTlzvIzBA==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "18.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.37.tgz", + "integrity": "sha512-Pi53fdVMk7Ig5IfAMltQQMgtY7xLzHaEous8IQasYsdQbYK3v90FkxI3XYQCe/Qme58pqp14lXJIsFmGP8VoZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@uppy/audio": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@uppy/audio/-/audio-0.3.3.tgz", + "integrity": "sha512-HmIE3berOiHZko0G8cZyIx6B0xY3EphjlLqUM3Q5xPJk6iU4ELIQWXjdgfYs7AFCSOPLcOf5IxldoSxAFCr+Cw==", + "license": "MIT", + "dependencies": { + "@uppy/utils": "^4.1.2", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/aws-s3": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@uppy/aws-s3/-/aws-s3-2.2.4.tgz", + "integrity": "sha512-kdyrm79fWm1uMUvza4LwD76zh7gmV41VlB5S5WojtT5JjT/hHlfsYZIuXNqqt7oWSzHUNPwAKxPvp9fHmEJ/tw==", + "license": "MIT", + "dependencies": { + "@uppy/companion-client": "^2.2.2", + "@uppy/utils": "^4.1.3", + "@uppy/xhr-upload": "^2.1.3", + "nanoid": "^3.1.25" + }, + "peerDependencies": { + "@uppy/core": "^2.3.4" + } + }, + "node_modules/@uppy/aws-s3-multipart": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@uppy/aws-s3-multipart/-/aws-s3-multipart-2.4.3.tgz", + "integrity": "sha512-2z/mTmDceQimsHGEXuhdUL6v7Twsj1TKLDTxp+YPEtf9cuSBhzwkUd/YltHHa8tH/ocdDXs4rwLuMZBXNIo0Qw==", + "license": "MIT", + "dependencies": { + "@uppy/companion-client": "^2.2.2", + "@uppy/utils": "^4.1.2" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/box": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@uppy/box/-/box-1.0.8.tgz", + "integrity": "sha512-ACgfD1o+f+Tu+K99605p5HhziYyHqa8B5wx3Lf/e5XqqdaHBi99qyJ902I8UVQQpdr/KCvkF1hbxtsPep5+Geg==", + "license": "MIT", + "dependencies": { + "@uppy/companion-client": "^2.2.2", + "@uppy/provider-views": "^2.1.3", + "@uppy/utils": "^4.1.2", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/companion-client": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@uppy/companion-client/-/companion-client-2.2.2.tgz", + "integrity": "sha512-5mTp2iq97/mYSisMaBtFRry6PTgZA6SIL7LePteOV5x0/DxKfrZW3DEiQERJmYpHzy7k8johpm2gHnEKto56Og==", + "license": "MIT", + "dependencies": { + "@uppy/utils": "^4.1.2", + "namespace-emitter": "^2.0.1" + } + }, + "node_modules/@uppy/compressor": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@uppy/compressor/-/compressor-0.3.3.tgz", + "integrity": "sha512-EsaKXOksToMDjTmrJhWJ8xKJsZyJCQysHwo5aoLQ/7lq+wVifF8TqvglT1Z8c8nSRkgcDum39vw8oQqvlmdyNQ==", + "license": "MIT", + "dependencies": { + "@transloadit/prettier-bytes": "^0.0.9", + "@uppy/utils": "^4.1.2", + "compressorjs": "^1.1.1", + "preact": "^10.5.13", + "promise-queue": "^2.2.5" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/compressor/node_modules/@transloadit/prettier-bytes": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@transloadit/prettier-bytes/-/prettier-bytes-0.0.9.tgz", + "integrity": "sha512-pCvdmea/F3Tn4hAtHqNXmjcixSaroJJ+L3STXlYJdir1g1m2mRQpWbN8a4SvgQtaw2930Ckhdx8qXdXBFMKbAA==", + "license": "MIT" + }, + "node_modules/@uppy/core": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@uppy/core/-/core-2.3.4.tgz", + "integrity": "sha512-iWAqppC8FD8mMVqewavCz+TNaet6HPXitmGXpGGREGrakZ4FeuWytVdrelydzTdXx6vVKkOmI2FLztGg73sENQ==", + "license": "MIT", + "dependencies": { + "@transloadit/prettier-bytes": "0.0.7", + "@uppy/store-default": "^2.1.1", + "@uppy/utils": "^4.1.3", + "lodash.throttle": "^4.1.1", + "mime-match": "^1.0.2", + "namespace-emitter": "^2.0.1", + "nanoid": "^3.1.25", + "preact": "^10.5.13" + } + }, + "node_modules/@uppy/dashboard": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/@uppy/dashboard/-/dashboard-2.4.3.tgz", + "integrity": "sha512-OPpvosiRaZXN873mraDmiM8T8c+2rIl86Ho7lQPsq+aQfjLUiPML+Y2rjmwDPE6eo7EiBszV5dQkO6vPjGO8/g==", + "license": "MIT", + "dependencies": { + "@transloadit/prettier-bytes": "0.0.7", + "@uppy/informer": "^2.1.1", + "@uppy/provider-views": "^2.1.3", + "@uppy/status-bar": "^2.2.2", + "@uppy/thumbnail-generator": "^2.2.2", + "@uppy/utils": "^4.1.3", + "classnames": "^2.2.6", + "is-shallow-equal": "^1.0.1", + "lodash.debounce": "^4.0.8", + "memoize-one": "^5.0.4", + "nanoid": "^3.1.25", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^2.3.4" + } + }, + "node_modules/@uppy/drag-drop": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@uppy/drag-drop/-/drag-drop-2.1.2.tgz", + "integrity": "sha512-J6hBiYcBc8p6U9PylqtZ+eMJ48yT1qP1Xzon2Pou5AQxQ4D7UAL97OvcjnONpOfp8P7uGmaqXFUubBNgEUCfQg==", + "license": "MIT", + "dependencies": { + "@uppy/utils": "^4.1.2", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/drop-target": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@uppy/drop-target/-/drop-target-1.1.4.tgz", + "integrity": "sha512-TCFTLqBnHGDJTV0DOjiT8HYO/YKm39Sg3tXNdubhidWAy/S4UVAj73X634UVOQascK5Fo4iHSwWaj8052xAETw==", + "license": "MIT", + "dependencies": { + "@uppy/utils": "^4.1.2" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/dropbox": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@uppy/dropbox/-/dropbox-2.0.8.tgz", + "integrity": "sha512-nvzRTW38sEsj0jhtMizLq0aEQbkY1fd5rDQy6phobt07aiJ8T/KT2NrefuIUaT1ECkz929l0yqDbrdSk7iynZw==", + "license": "MIT", + "dependencies": { + "@uppy/companion-client": "^2.2.2", + "@uppy/provider-views": "^2.1.3", + "@uppy/utils": "^4.1.2", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/facebook": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@uppy/facebook/-/facebook-2.0.8.tgz", + "integrity": "sha512-2sz8IFowl/7nOH0Sx4OK6Uo/Wps7ARUYmJs5vjeYUNzODdnQM6WZPmDpQ0vfJW/5cgj8ilx87kUaCLZul1Ey1A==", + "license": "MIT", + "dependencies": { + "@uppy/companion-client": "^2.2.2", + "@uppy/provider-views": "^2.1.3", + "@uppy/utils": "^4.1.2", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/file-input": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@uppy/file-input/-/file-input-2.1.2.tgz", + "integrity": "sha512-tdn6HNMnLOC2xpdZYbdXNSjTS9EpLvY97vOfq9SZWkoX/cmZiOf6JfFc3Qm8pS3RRnVmwsdi5usEPbzMQ5RAEg==", + "license": "MIT", + "dependencies": { + "@uppy/utils": "^4.1.2", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/form": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@uppy/form/-/form-2.0.7.tgz", + "integrity": "sha512-FsZ97NRUeXCi5iAJB2VAxxCa+WOizrP8y42g2dD36S783e81a4iZqSm2N7WSC3PQHNu7OKkQnPYN+9xU0w5emw==", + "license": "MIT", + "dependencies": { + "@uppy/utils": "^4.1.2", + "get-form-data": "^2.0.0" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/golden-retriever": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@uppy/golden-retriever/-/golden-retriever-2.1.3.tgz", + "integrity": "sha512-G1zBjgsfzMjebQ9KMLTE5H6AX/fuMAe6izFx6B6FE0M4mJ9ZZ+ClbHQeFzomepagGq8dJoIB4M85CzRyq5Igbw==", + "license": "MIT", + "dependencies": { + "@transloadit/prettier-bytes": "0.0.7", + "@uppy/utils": "^4.1.3", + "lodash.throttle": "^4.1.1" + }, + "peerDependencies": { + "@uppy/core": "^2.3.4" + } + }, + "node_modules/@uppy/google-drive": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@uppy/google-drive/-/google-drive-2.1.2.tgz", + "integrity": "sha512-78JAaoV3MNeaNDCzJpym5nIY0NynWivyjcbJ6Ool2xyAKqS5nBUXnAG+ciEl8ZMAhFdv94kI9pLtrRNezYI7vg==", + "license": "MIT", + "dependencies": { + "@uppy/companion-client": "^2.2.2", + "@uppy/provider-views": "^2.1.3", + "@uppy/utils": "^4.1.2", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/image-editor": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@uppy/image-editor/-/image-editor-1.4.2.tgz", + "integrity": "sha512-oqcvIphTQVhRJiMpi7aRe5gx2O+GqVOp9G+/kNPDjFZTZoS6vTrGWdmlr27dt0JkZarckjOK7+2iboCSIe9Qqg==", + "license": "MIT", + "dependencies": { + "@uppy/utils": "^4.1.2", + "cropperjs": "1.5.7", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/informer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@uppy/informer/-/informer-2.1.1.tgz", + "integrity": "sha512-aSdtJO0QvDGzcWHQ1Kd1hOFLyn+0e8LY82708WGkt8BwYwjmKhCJUuxdPDsCu3I2wpFUSUzpvQ9pik7AajBFjw==", + "license": "MIT", + "dependencies": { + "@uppy/utils": "^4.1.2", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/instagram": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@uppy/instagram/-/instagram-2.1.2.tgz", + "integrity": "sha512-dvQJ6PeX9hFN5f0+hi7qx2zDtZz0AyNYqB/O41VUulO0eDgfh8c4r4w4EUp676o9aY/0Ia71il7D5y3vtysqIA==", + "license": "MIT", + "dependencies": { + "@uppy/companion-client": "^2.2.2", + "@uppy/provider-views": "^2.1.3", + "@uppy/utils": "^4.1.2", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/onedrive": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@uppy/onedrive/-/onedrive-2.1.2.tgz", + "integrity": "sha512-rlRNPdOT+AWmeDkiGqQODjATQw6w3Xn/Uod6Ct2JvJhLhxImbTsxDedQotwE18m3AnQ9wLdLM9cCGAcvKeEsHg==", + "license": "MIT", + "dependencies": { + "@uppy/companion-client": "^2.2.2", + "@uppy/provider-views": "^2.1.3", + "@uppy/utils": "^4.1.2", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/progress-bar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@uppy/progress-bar/-/progress-bar-2.1.2.tgz", + "integrity": "sha512-BzO+LSMDj+daT93yoUhNdkQ1Bq79lSm+hTUcuFpUt397B0ETzUeHUg3wUj39Zu3r7BlO/JmQLbH4NkejK4rYGg==", + "license": "MIT", + "dependencies": { + "@uppy/utils": "^4.1.2", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/provider-views": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@uppy/provider-views/-/provider-views-2.1.3.tgz", + "integrity": "sha512-IXk8j+0nXxsTLV1KwUJbholiwMYXJ9H2r7pJlBRiu/lB/hgd5t7ENqt2susnepBFQJ+XlaIsuM5YVLgppBwc5w==", + "license": "MIT", + "dependencies": { + "@uppy/utils": "^4.1.2", + "classnames": "^2.2.6", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/react": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@uppy/react/-/react-2.2.3.tgz", + "integrity": "sha512-Zr8KRHATAPeYGI10Xty+KS4rSh3mvoE5l++9Ondo0Qu7TbFPIrmLlq52d9yMI1dV4hd98kshv6kRa394Ex4UbQ==", + "license": "MIT", + "dependencies": { + "@uppy/dashboard": "^2.4.2", + "@uppy/drag-drop": "^2.1.2", + "@uppy/file-input": "^2.1.2", + "@uppy/progress-bar": "^2.1.2", + "@uppy/status-bar": "^2.2.2", + "@uppy/utils": "^4.1.2", + "prop-types": "^15.6.1" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3", + "react": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@uppy/redux-dev-tools": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@uppy/redux-dev-tools/-/redux-dev-tools-2.1.1.tgz", + "integrity": "sha512-dLJv/gofRIkyw6RIUFQxCOhtvplRjQNcp7BlbSfThH0AkaRtm+dH/0hjCpl6wED5r746u+ZUoZyMi7P4STGC0g==", + "license": "MIT", + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/remote-sources": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@uppy/remote-sources/-/remote-sources-0.1.2.tgz", + "integrity": "sha512-MBsFAvXH8tEMzl3E+5s0F27K3iwewV/7QaXeHrgOcToxlAuk7f+3xmYCKIt9vK3pp7iSebJ0SQ/qaHSHZizL2g==", + "license": "MIT", + "dependencies": { + "@uppy/box": "^1.0.8", + "@uppy/dashboard": "^2.4.2", + "@uppy/dropbox": "^2.0.8", + "@uppy/facebook": "^2.0.8", + "@uppy/google-drive": "^2.1.2", + "@uppy/instagram": "^2.1.2", + "@uppy/onedrive": "^2.1.2", + "@uppy/unsplash": "^2.1.1", + "@uppy/url": "^2.2.1", + "@uppy/zoom": "^1.1.2" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/screen-capture": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@uppy/screen-capture/-/screen-capture-2.1.2.tgz", + "integrity": "sha512-gsVtosu/3rHe2W2oJQUwcNFdBZuJ6DQmILrtdMicBN4SuQck0ClmztOTjQXGuCurjSErS5HHvxz3G98I/F5Epw==", + "license": "MIT", + "dependencies": { + "@uppy/utils": "^4.1.2", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/status-bar": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@uppy/status-bar/-/status-bar-2.2.2.tgz", + "integrity": "sha512-XV4/3RyNF42enqPc4wWZupqI1KuGtfdt49waux7kebxaGqNzV+T72o/C+QDDqY/h4mKadrp6p98/BnMefC5QtQ==", + "license": "MIT", + "dependencies": { + "@transloadit/prettier-bytes": "0.0.7", + "@uppy/utils": "^4.1.2", + "classnames": "^2.2.6", + "lodash.throttle": "^4.1.1", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/store-default": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@uppy/store-default/-/store-default-2.1.1.tgz", + "integrity": "sha512-xnpTxvot2SeAwGwbvmJ899ASk5tYXhmZzD/aCFsXePh/v8rNvR2pKlcQUH7cF/y4baUGq3FHO/daKCok/mpKqQ==", + "license": "MIT" + }, + "node_modules/@uppy/store-redux": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@uppy/store-redux/-/store-redux-2.1.1.tgz", + "integrity": "sha512-RFb/fi4BBlC+l1TC/Z76Wo1ZsssAr4Su23C0ZgB1KCA2RrXRIu7dkwQuFHvy+HokJhhyDPm3Sm474ulPr5wpzA==", + "license": "MIT", + "dependencies": { + "nanoid": "^3.1.25" + } + }, + "node_modules/@uppy/thumbnail-generator": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@uppy/thumbnail-generator/-/thumbnail-generator-2.2.2.tgz", + "integrity": "sha512-5VwwzzvKRqXJNz28U/VwXu9K9dHY5vXQvzljxqkeCJrKIMgu/8vzKEFndAPY6sJZkUcF0jtAb3gUU2q5TGRlJg==", + "license": "MIT", + "dependencies": { + "@uppy/utils": "^4.1.3", + "exifr": "^7.0.0" + }, + "peerDependencies": { + "@uppy/core": "^2.3.4" + } + }, + "node_modules/@uppy/transloadit": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/@uppy/transloadit/-/transloadit-2.3.7.tgz", + "integrity": "sha512-LETmEvMX/Am3Gu/thxttHGdIy7oOjU2+G5+VHozzyBF8Z1kqW0p+LUS0HwWM9w7cz4S/Yi+ZI+rP0aR5J2txhg==", + "license": "MIT", + "dependencies": { + "@uppy/companion-client": "^2.2.2", + "@uppy/provider-views": "^2.1.3", + "@uppy/tus": "^2.4.3", + "@uppy/utils": "^4.1.2", + "component-emitter": "^1.2.1", + "socket.io-client": "^4.1.3" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/tus": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@uppy/tus/-/tus-2.4.6.tgz", + "integrity": "sha512-0R8D65YKZRyvx+SKNKPkgVYrgAcZiz6vtzc+ZmcCePFXNZj945kFIoeqNUuQ9aQlkIasihKB4kut6X2F4G93IA==", + "license": "MIT", + "dependencies": { + "@uppy/companion-client": "^2.2.2", + "@uppy/utils": "^4.1.3", + "tus-js-client": "^2.1.1" + }, + "peerDependencies": { + "@uppy/core": "^2.3.4" + } + }, + "node_modules/@uppy/unsplash": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@uppy/unsplash/-/unsplash-2.1.1.tgz", + "integrity": "sha512-S6pX12ierlx9iBkTXvPlljmPI32CYdp61WoLrGXrhmZmCrFsk64ngZK7PVmkJ4avsokNq5vDjkZvgjxZ5wmtzQ==", + "license": "MIT", + "dependencies": { + "@uppy/companion-client": "^2.2.2", + "@uppy/provider-views": "^2.1.3", + "@uppy/utils": "^4.1.2", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/url": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@uppy/url/-/url-2.2.1.tgz", + "integrity": "sha512-OY85TLyaQG0ELd3+fX9KTJ41EOmsWvVYMYQvNXSs/+K+xe52mzEqV8PK85dxlQwEIuPAGtQQGz+6IICuwsWDdA==", + "license": "MIT", + "dependencies": { + "@uppy/companion-client": "^2.2.2", + "@uppy/utils": "^4.1.2", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/utils": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@uppy/utils/-/utils-4.1.3.tgz", + "integrity": "sha512-nTuMvwWYobnJcytDO3t+D6IkVq/Qs4Xv3vyoEZ+Iaf8gegZP+rEyoaFT2CK5XLRMienPyqRqNbIfRuFaOWSIFw==", + "license": "MIT", + "dependencies": { + "lodash.throttle": "^4.1.1" + } + }, + "node_modules/@uppy/webcam": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@uppy/webcam/-/webcam-2.2.2.tgz", + "integrity": "sha512-7pYFhzYRj7AcwXg0onrcDMUjW5uNC8ImF9PhUzaZBtp9pnXMKpxfbiHv3MobqH8XtCZWYAvPizPc4dqDMY7iJA==", + "license": "MIT", + "dependencies": { + "@uppy/utils": "^4.1.2", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/xhr-upload": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@uppy/xhr-upload/-/xhr-upload-2.1.3.tgz", + "integrity": "sha512-YWOQ6myBVPs+mhNjfdWsQyMRWUlrDLMoaG7nvf/G6Y3GKZf8AyjFDjvvJ49XWQ+DaZOftGkHmF1uh/DBeGivJQ==", + "license": "MIT", + "dependencies": { + "@uppy/companion-client": "^2.2.2", + "@uppy/utils": "^4.1.2", + "nanoid": "^3.1.25" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@uppy/zoom": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@uppy/zoom/-/zoom-1.1.2.tgz", + "integrity": "sha512-b19x6jnEqCnm7UNyM/vO+AVq4xaaPuVD3wzpkzh47aROtLm/DGVKhTFSJAFVfae7iE4JmeA80JHXYYfb7dgUFA==", + "license": "MIT", + "dependencies": { + "@uppy/companion-client": "^2.2.2", + "@uppy/provider-views": "^2.1.3", + "@uppy/utils": "^4.1.2", + "preact": "^10.5.13" + }, + "peerDependencies": { + "@uppy/core": "^2.3.3" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-1.3.2.tgz", + "integrity": "sha512-aurBNmMo0kz1O4qRoY+FM4epSA39y3ShWGuqfLRA/3z0oEJAdtoSfgA3aO98/PCCHAqMaduLxIxErWrVKIFzXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.17.10", + "@babel/plugin-transform-react-jsx": "^7.17.3", + "@babel/plugin-transform-react-jsx-development": "^7.16.7", + "@babel/plugin-transform-react-jsx-self": "^7.16.7", + "@babel/plugin-transform-react-jsx-source": "^7.16.7", + "@rollup/pluginutils": "^4.2.1", + "react-refresh": "^0.13.0", + "resolve": "^1.22.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@wojtekmaj/date-utils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-1.5.1.tgz", + "integrity": "sha512-+i7+JmNiE/3c9FKxzWFi2IjRJ+KzZl1QPu6QNrsgaa2MuBgXvUy4gA1TVzf/JMdIIloB76xSKikTWuyYAIVLww==", + "license": "MIT", + "funding": { + "url": "https://github.com/wojtekmaj/date-utils?sponsor=1" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", + "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001599", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-plugin-styled-components": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz", + "integrity": "sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.22.5", + "lodash": "^4.17.21", + "picomatch": "^2.3.1" + }, + "peerDependencies": { + "styled-components": ">= 2" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/blueimp-canvas-to-blob": { + "version": "3.29.0", + "resolved": "https://registry.npmjs.org/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.29.0.tgz", + "integrity": "sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.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/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/body-scroll-lock": { + "version": "4.0.0-beta.0", + "resolved": "https://registry.npmjs.org/body-scroll-lock/-/body-scroll-lock-4.0.0-beta.0.tgz", + "integrity": "sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ==", + "license": "MIT" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.16" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001636", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001636.tgz", + "integrity": "sha512-bMg2vmr8XBsbL6Lr0UHXy/21m84FTxDLWn2FSqMd5PrlbMxwJlQnC2YWYxVgp66PZE+BBNF2jYQUBKCo1FDeZg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "license": "MIT", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "node_modules/combine-errors": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/combine-errors/-/combine-errors-3.0.3.tgz", + "integrity": "sha512-C8ikRNRMygCwaTx+Ek3Yr+OuZzgZjduCOfSQBjbM8V3MfgcjSTeto/GXP6PAwKvJz/v15b7GHZvx5rOlczFw/Q==", + "dependencies": { + "custom-error-instance": "2.1.1", + "lodash.uniqby": "4.5.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/compressorjs": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/compressorjs/-/compressorjs-1.2.1.tgz", + "integrity": "sha512-+geIjeRnPhQ+LLvvA7wxBQE5ddeLU7pJ3FsKFWirDw6veY3s9iLxAQEw7lXGHnhCJvBujEQWuNnGzZcvCvdkLQ==", + "license": "MIT", + "dependencies": { + "blueimp-canvas-to-blob": "^3.29.0", + "is-blob": "^2.1.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/consola": { + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", + "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cropperjs": { + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.5.7.tgz", + "integrity": "sha512-sGj+G/ofKh+f6A4BtXLJwtcKJgMUsXYVUubfTo9grERiDGXncttefmue/fyQFvn8wfdyoD1KhDRYLfjkJFl0yw==", + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-select/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/css-select/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/css-select/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/css-select/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/custom-error-instance": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/custom-error-instance/-/custom-error-instance-2.1.1.tgz", + "integrity": "sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg==", + "license": "ISC" + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-diff": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz", + "integrity": "sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/device-uuid": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/device-uuid/-/device-uuid-1.0.4.tgz", + "integrity": "sha512-nNbUKlCuXZ1BV4zKFr9z8VxypL1PCNExRoNtRx/mNKFcOEDWUdT3E1B4vw7LBWftBmixkrjQIjpdLov3EmEJjA==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom7": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/dom7/-/dom7-4.0.6.tgz", + "integrity": "sha512-emjdpPLhpNubapLFdjNL9tP06Sr+GZkrIHEXLWvOGsytACUrkbeIdjO5g77m00BrHTznnlcNqgmn7pCN192TBA==", + "license": "MIT", + "dependencies": { + "ssr-window": "^4.0.0" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.5.tgz", + "integrity": "sha512-lwG+n5h8QNpxtyrJW/gJWckL+1/DQiYMX8f7t8Z2AZTPw1esVrqjI63i7Zc2Gz0aKzLVMYC1V1PL/ky+aY/NgA==", + "license": "(MPL-2.0 OR Apache-2.0)" + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-8.0.3.tgz", + "integrity": "sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.807", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.807.tgz", + "integrity": "sha512-kSmJl2ZwhNf/bcIuCH/imtNOKlpkLDn2jqT5FJ+/0CXjhnFaOa9cOe9gHKKy71eM49izwuQjZhKk+lWQ1JxB7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-picker-react": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.10.0.tgz", + "integrity": "sha512-EfvOsGbyweMNcJ1F99XUv+XPdfkpa2NRAYkhwdIeYS6DWeISu3kHWX+iwvFLUVAc533aWbsGpETbxwbhzsiMnw==", + "license": "MIT", + "dependencies": { + "flairup": "0.0.39" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io-client": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", + "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz", + "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/linux-loong64": "0.14.54", + "esbuild-android-64": "0.14.54", + "esbuild-android-arm64": "0.14.54", + "esbuild-darwin-64": "0.14.54", + "esbuild-darwin-arm64": "0.14.54", + "esbuild-freebsd-64": "0.14.54", + "esbuild-freebsd-arm64": "0.14.54", + "esbuild-linux-32": "0.14.54", + "esbuild-linux-64": "0.14.54", + "esbuild-linux-arm": "0.14.54", + "esbuild-linux-arm64": "0.14.54", + "esbuild-linux-mips64le": "0.14.54", + "esbuild-linux-ppc64le": "0.14.54", + "esbuild-linux-riscv64": "0.14.54", + "esbuild-linux-s390x": "0.14.54", + "esbuild-netbsd-64": "0.14.54", + "esbuild-openbsd-64": "0.14.54", + "esbuild-sunos-64": "0.14.54", + "esbuild-windows-32": "0.14.54", + "esbuild-windows-64": "0.14.54", + "esbuild-windows-arm64": "0.14.54" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz", + "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz", + "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz", + "integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz", + "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz", + "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz", + "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz", + "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz", + "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz", + "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz", + "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz", + "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz", + "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz", + "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz", + "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz", + "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz", + "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz", + "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz", + "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz", + "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz", + "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/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==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/exifr": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/exifr/-/exifr-7.1.3.tgz", + "integrity": "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==", + "license": "MIT" + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "license": "MIT", + "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/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/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==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/flairup": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/flairup/-/flairup-0.0.39.tgz", + "integrity": "sha512-UVPkzZmZeBWBx1+Ovo++kYKk9Wi32Jxt+c7HsxnEY80ExwFV54w+NyquFziqMLS0BnGVE43yGD4OvIwaAm/WiQ==", + "license": "MIT" + }, + "node_modules/focus-lock": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/focus-lock/-/focus-lock-1.3.5.tgz", + "integrity": "sha512-QFaHbhv9WPUeLYBDe/PAuLKJ4Dd9OPvKs9xZBr3yLXnUrDNaVXKu2baDBXe3naPY30hgHYSsf2JW4jzas2mDEQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/focus-outline-manager": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/focus-outline-manager/-/focus-outline-manager-1.0.2.tgz", + "integrity": "sha512-bHWEmjLsTjGP9gVs7P3Hyl+oY5NlMW8aTSPdTJ+X2GKt6glDctt9fUCLbRV+d/l8NDC40+FxMjp9WlTQXaQALw==", + "license": "BSD-3-Clause" + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-form-data": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-form-data/-/get-form-data-2.0.0.tgz", + "integrity": "sha512-YUpw0aTWeGliifqMYrTohe/YdqVmKLmaNwuscd2WlRNGfba57JHGuuvvv2c6LiZdFys285POVWANTh6SqcwFag==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-user-locale": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/get-user-locale/-/get-user-locale-2.3.2.tgz", + "integrity": "sha512-O2GWvQkhnbDoWFUJfaBlDIKUEdND8ATpBXD6KXcbhxlfktyD/d8w6mkzM/IlQEqGZAMz/PW6j6Hv53BiigKLUQ==", + "license": "MIT", + "dependencies": { + "mem": "^8.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/get-user-locale?sponsor=1" + } + }, + "node_modules/glob": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz", + "integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "license": "MIT", + "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==", + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "^5.2.2", + "commander": "^8.3.0", + "he": "^1.2.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.10.0" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-minifier-terser/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/intro.js": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/intro.js/-/intro.js-7.2.0.tgz", + "integrity": "sha512-qbMfaB70rOXVBceIWNYnYTpVTiZsvQh/MIkfdQbpA9di9VBfj1GigUPfcCv3aOfsbrtPcri8vTLTA4FcEDcHSQ==", + "license": "AGPL-3.0" + }, + "node_modules/intro.js-react": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/intro.js-react/-/intro.js-react-1.0.0.tgz", + "integrity": "sha512-zR8pbTyX20RnCZpJMc0nuHBpsjcr1wFkj3ZookV6Ly4eE/LGpFTQwPsaA61Cryzwiy/tTFsusf4hPU9NpI9UOg==", + "license": "MIT", + "peerDependencies": { + "intro.js": ">=2.5.0", + "react": ">=0.14.0" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT" + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-blob": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-blob/-/is-blob-2.1.0.tgz", + "integrity": "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-lite": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-lite/-/is-lite-1.2.1.tgz", + "integrity": "sha512-pgF+L5bxC+10hLBgf6R2P4ZZUBOQIIacbdo8YvuCP8/JvsWxG7aZ9p10DYuLtifFci4l3VITphhMlMV4Y+urPw==", + "license": "MIT" + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shallow-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-shallow-equal/-/is-shallow-equal-1.0.1.tgz", + "integrity": "sha512-lq5RvK+85Hs5J3p4oA4256M1FEffzmI533ikeDHvJd42nouRRx5wBzt36JuviiGe5dIPyHON/d0/Up+PBo6XkQ==", + "license": "MIT" + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.0.tgz", + "integrity": "sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", + "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/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==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "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/jake/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==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/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==", + "license": "MIT" + }, + "node_modules/jake/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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/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==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-base64": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", + "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==", + "license": "BSD-3-Clause" + }, + "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==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "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, + "license": "MIT" + }, + "node_modules/json-to-csv-export": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/json-to-csv-export/-/json-to-csv-export-2.1.0.tgz", + "integrity": "sha512-xAkBf6f0uLqG7gtCuulqp6BtbIswYMiSTCJ2ctuh2w1W5XvQKywX4Ppkr5Tgm3JUHLgeOspli8zA9Hy494X5WA==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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, + "license": "MIT" + }, + "node_modules/linkifyjs": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.3.tgz", + "integrity": "sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg==", + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/lodash._baseiteratee": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash._baseiteratee/-/lodash._baseiteratee-4.7.0.tgz", + "integrity": "sha512-nqB9M+wITz0BX/Q2xg6fQ8mLkyfF7MU7eE+MNBNjTHFKeKaZAPEzEg+E8LWxKWf1DQVflNEn9N49yAuqKh2mWQ==", + "license": "MIT", + "dependencies": { + "lodash._stringtopath": "~4.8.0" + } + }, + "node_modules/lodash._basetostring": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-4.12.0.tgz", + "integrity": "sha512-SwcRIbyxnN6CFEEK4K1y+zuApvWdpQdBHM/swxP962s8HIxPO3alBH5t3m/dl+f4CMUug6sJb7Pww8d13/9WSw==", + "license": "MIT" + }, + "node_modules/lodash._baseuniq": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz", + "integrity": "sha512-Ja1YevpHZctlI5beLA7oc5KNDhGcPixFhcqSiORHNsp/1QTv7amAXzw+gu4YOvErqVlMVyIJGgtzeepCnnur0A==", + "license": "MIT", + "dependencies": { + "lodash._createset": "~4.0.0", + "lodash._root": "~3.0.0" + } + }, + "node_modules/lodash._createset": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/lodash._createset/-/lodash._createset-4.0.3.tgz", + "integrity": "sha512-GTkC6YMprrJZCYU3zcqZj+jkXkrXzq3IPBcF/fIPpNEAB4hZEtXU8zp/RwKOvZl43NUmwDbyRk3+ZTbeRdEBXA==", + "license": "MIT" + }, + "node_modules/lodash._root": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", + "integrity": "sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ==", + "license": "MIT" + }, + "node_modules/lodash._stringtopath": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/lodash._stringtopath/-/lodash._stringtopath-4.8.0.tgz", + "integrity": "sha512-SXL66C731p0xPDC5LZg4wI5H+dJo/EO4KTqOMwLYCH3+FmmfAKJEZCm6ohGpI+T1xwsDsJCfL4OnhorllvlTPQ==", + "license": "MIT", + "dependencies": { + "lodash._basetostring": "~4.12.0" + } + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT" + }, + "node_modules/lodash.uniqby": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.5.0.tgz", + "integrity": "sha512-IRt7cfTtHy6f1aRVA5n7kT8rgN3N1nH6MOWLcHfpWG2SH19E3JksLK38MktLxZDhlAjCP9jpIXkOnRXlu6oByQ==", + "license": "MIT", + "dependencies": { + "lodash._baseiteratee": "~4.7.0", + "lodash._baseuniq": "~4.6.0" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/map-age-cleaner": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", + "license": "MIT", + "dependencies": { + "p-defer": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mem": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/mem/-/mem-8.1.1.tgz", + "integrity": "sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==", + "license": "MIT", + "dependencies": { + "map-age-cleaner": "^0.1.3", + "mimic-fn": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/mem?sponsor=1" + } + }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mime-match/-/mime-match-1.0.2.tgz", + "integrity": "sha512-VXp/ugGDVh3eCLOBCiHZMYWQaTNUHv2IJrut+yXA6+JbLPXHglHwfS/5A5L0ll+jkCY7fIzRJcH6OIunF+c6Cg==", + "license": "ISC", + "dependencies": { + "wildcard": "^1.1.0" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "license": "MIT", + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/namespace-emitter": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/namespace-emitter/-/namespace-emitter-2.0.1.tgz", + "integrity": "sha512-N/sMKHniSDJBjfrkbS/tpkPj4RAbvW3mr8UAzvlMHyun93XEm83IAvhWtJVHo+RHn/oO8Job5YN4b+wRjSVp5g==", + "license": "MIT" + }, + "node_modules/nanoclone": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz", + "integrity": "sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-html-parser": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-5.4.2.tgz", + "integrity": "sha512-RaBPP3+51hPne/OolXxcz89iYvQvKOydaqoePpOgXcrOKZhjVIzmpKZz+Hd/RBO2/zN2q6CNJhQzucVz+u3Jyw==", + "license": "MIT", + "dependencies": { + "css-select": "^4.2.1", + "he": "1.2.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "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==", + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/p-defer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", + "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "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, + "license": "MIT", + "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/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", + "license": "MIT" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-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, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "14 || >=16.14" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-0.2.0.tgz", + "integrity": "sha512-sTitTPYnn23esFR3RlqYBWn4c45WGeLcsKzQiUpXJAyfcWkolvlYpV8FLo7JishK946oQwMFUCHXQ9AjGPKExw==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", + "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/preact": { + "version": "10.22.0", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.22.0.tgz", + "integrity": "sha512-RRurnSjJPj4rp5K6XoP45Ui33ncb7e4H7WiOHVpjbkvqvA3U+N8Z6Qbo0AE6leGYBV66n8EhEaFixvIu3SkxFw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.2.8.tgz", + "integrity": "sha512-KgPcEnJeIijlMjsA6WwYgRs5rh3/q76oInqtMXBA/EMcamrcYJpyhtRhyX1ayT9hnHlHTuO8sIifHF10WuSDKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.17.0" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@shufo/prettier-plugin-blade": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "prettier": ">=2.2.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*", + "prettier-plugin-twig-melody": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@shufo/prettier-plugin-blade": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + }, + "prettier-plugin-twig-melody": { + "optional": true + } + } + }, + "node_modules/promise-queue": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/promise-queue/-/promise-queue-2.2.5.tgz", + "integrity": "sha512-p/iXrPSVfnqPft24ZdNNLECw/UrtLTpT3jpAAMzl/o5/rDsGCPo3/CQS2611flL6LkoEJ3oQZw7C8Q80ZISXRQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proper-lockfile": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-2.0.1.tgz", + "integrity": "sha512-rjaeGbsmhNDcDInmwi4MuI6mRwJu6zq8GjYCLuSuE7GF+4UjgzkL69sVKKJ2T2xH61kK7rXvGYpvaTu909oXaQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "retry": "^0.10.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-async-script": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/react-async-script/-/react-async-script-1.2.0.tgz", + "integrity": "sha512-bCpkbm9JiAuMGhkqoAiC0lLkb40DJ0HOEJIku+9JDjxX3Rcs+ztEOG13wbrOskt3n2DTrjshhaQ/iay+SnGg5Q==", + "license": "MIT", + "dependencies": { + "hoist-non-react-statics": "^3.3.0", + "prop-types": "^15.5.0" + }, + "peerDependencies": { + "react": ">=16.4.1" + } + }, + "node_modules/react-calendar": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/react-calendar/-/react-calendar-4.8.0.tgz", + "integrity": "sha512-qFgwo+p58sgv1QYMI1oGNaop90eJVKuHTZ3ZgBfrrpUb+9cAexxsKat0sAszgsizPMVo7vOXedV7Lqa0GQGMvA==", + "license": "MIT", + "dependencies": { + "@wojtekmaj/date-utils": "^1.1.3", + "clsx": "^2.0.0", + "get-user-locale": "^2.2.1", + "prop-types": "^15.6.0", + "warning": "^4.0.0" + }, + "funding": { + "url": "https://github.com/wojtekmaj/react-calendar?sponsor=1" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-clientside-effect": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/react-clientside-effect/-/react-clientside-effect-1.2.6.tgz", + "integrity": "sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.13" + }, + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-drag-drop-files": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/react-drag-drop-files/-/react-drag-drop-files-2.3.10.tgz", + "integrity": "sha512-Fv614W9+OtXFB5O+gjompTxQZLYGO7wJeT4paETGiXtiADB9yPOMGYD4A3PMCTY9Be874/wcpl+2dm3MvCIRzg==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2", + "styled-components": "^5.3.0" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/react-floater": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/react-floater/-/react-floater-0.7.9.tgz", + "integrity": "sha512-NXqyp9o8FAXOATOEo0ZpyaQ2KPb4cmPMXGWkx377QtJkIXHlHRAGer7ai0r0C1kG5gf+KJ6Gy+gdNIiosvSicg==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.3.1", + "is-lite": "^0.8.2", + "popper.js": "^1.16.0", + "prop-types": "^15.8.1", + "tree-changes": "^0.9.1" + }, + "peerDependencies": { + "react": "15 - 18", + "react-dom": "15 - 18" + } + }, + "node_modules/react-floater/node_modules/@gilbarbara/deep-equal": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@gilbarbara/deep-equal/-/deep-equal-0.1.2.tgz", + "integrity": "sha512-jk+qzItoEb0D0xSSmrKDDzf9sheQj/BAPxlgNxgmOaA3mxpUa6ndJLYGZKsJnIVEQSD8zcTbyILz7I0HcnBCRA==", + "license": "MIT" + }, + "node_modules/react-floater/node_modules/is-lite": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/is-lite/-/is-lite-0.8.2.tgz", + "integrity": "sha512-JZfH47qTsslwaAsqbMI3Q6HNNjUuq6Cmzzww50TdP5Esb6e1y2sK2UAaZZuzfAzpoI2AkxoPQapZdlDuP6Vlsw==", + "license": "MIT" + }, + "node_modules/react-floater/node_modules/tree-changes": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/tree-changes/-/tree-changes-0.9.3.tgz", + "integrity": "sha512-vvvS+O6kEeGRzMglTKbc19ltLWNtmNt1cpBoSYLj/iEcPVvpJasemKOlxBrmZaCtDJoF+4bwv3m01UKYi8mukQ==", + "license": "MIT", + "dependencies": { + "@gilbarbara/deep-equal": "^0.1.1", + "is-lite": "^0.8.2" + } + }, + "node_modules/react-focus-lock": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.12.1.tgz", + "integrity": "sha512-lfp8Dve4yJagkHiFrC1bGtib3mF2ktqwPJw4/WGcgPW+pJ/AVQA5X2vI7xgp13FcxFEpYBBHpXai/N2DBNC0Jw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.0.0", + "focus-lock": "^1.3.5", + "prop-types": "^15.6.2", + "react-clientside-effect": "^1.2.6", + "use-callback-ref": "^1.3.2", + "use-sidecar": "^1.1.2" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-google-autocomplete": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/react-google-autocomplete/-/react-google-autocomplete-2.7.3.tgz", + "integrity": "sha512-Nm+7/VDe7/NDWb8p/a39is7ktNqt5bNqAOoQv2Ev/XkuEvjsRk08VAPFmXUH03xKuM8IUuDrk2Lwfge44YEj6Q==", + "license": "ISC", + "dependencies": { + "lodash.debounce": "^4.0.8", + "prop-types": "^15.5.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/react-google-recaptcha": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-2.1.0.tgz", + "integrity": "sha512-K9jr7e0CWFigi8KxC3WPvNqZZ47df2RrMAta6KmRoE4RUi7Ys6NmNjytpXpg4HI/svmQJLKR+PncEPaNJ98DqQ==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.5.0", + "react-async-script": "^1.1.1" + }, + "peerDependencies": { + "react": ">=16.4.1" + } + }, + "node_modules/react-hook-form": { + "version": "7.52.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.52.0.tgz", + "integrity": "sha512-mJX506Xc6mirzLsmXUJyqlAI3Kj9Ph2RhplYhUVffeOQSnubK2uVqBFOBJmvKikvbFV91pxVXmDiR+QMF19x6A==", + "license": "MIT", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-html-table-to-excel": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/react-html-table-to-excel/-/react-html-table-to-excel-2.0.0.tgz", + "integrity": "sha512-afvcCtQWZPfen3D+UFu9YMHleQqpVm3manmiWaxr0wv76hx+BkQ0/nkOI1UyQs29pzAUWtZDiD9Eu/MqsDOEJw==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.5.10" + }, + "peerDependencies": { + "react": "^15.x.x" + } + }, + "node_modules/react-infinite-scroll-component": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz", + "integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==", + "license": "MIT", + "dependencies": { + "throttle-debounce": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, + "node_modules/react-innertext": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/react-innertext/-/react-innertext-1.1.5.tgz", + "integrity": "sha512-PWAqdqhxhHIv80dT9znP2KvS+hfkbRovFp4zFYHFFlOoQLRiawIic81gKb3U1wEyJZgMwgs3JoLtwryASRWP3Q==", + "license": "MIT", + "peerDependencies": { + "@types/react": ">=0.0.0 <=99", + "react": ">=0.0.0 <=99" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-joyride": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/react-joyride/-/react-joyride-2.8.2.tgz", + "integrity": "sha512-2QY8HB1G0I2OT0PKMUz7gg2HAjdkG2Bqi13r0Bb1V16PAwfb9khn4wWBTOJsGsjulbAWiQ3/0YrgNUHGFmuifw==", + "license": "MIT", + "dependencies": { + "@gilbarbara/deep-equal": "^0.3.1", + "deep-diff": "^1.0.2", + "deepmerge": "^4.3.1", + "is-lite": "^1.2.1", + "react-floater": "^0.7.9", + "react-innertext": "^1.1.5", + "react-is": "^16.13.1", + "scroll": "^3.0.1", + "scrollparent": "^2.1.0", + "tree-changes": "^0.11.2", + "type-fest": "^4.18.2" + }, + "peerDependencies": { + "react": "15 - 18", + "react-dom": "15 - 18" + } + }, + "node_modules/react-json-to-csv": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/react-json-to-csv/-/react-json-to-csv-1.2.0.tgz", + "integrity": "sha512-4yBorgPrkRQDRpbTKDq5vXIb4+CYBorwFxbYyKddmTnFlUGQb/2EOyIrSq6Elp/ygYCVKJK2IwfhEYUaDVyCpw==", + "license": "MIT", + "dependencies": { + "json-to-csv-export": "2.1.0" + } + }, + "node_modules/react-loading-skeleton": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.4.0.tgz", + "integrity": "sha512-1oJEBc9+wn7BbkQQk7YodlYEIjgeR+GrRjD+QXkVjwZN7LGIcAFHrx4NhT7UHGBxNY1+zax3c+Fo6XQM4R7CgA==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/react-number-format": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.0.tgz", + "integrity": "sha512-NWdICrqLhI7rAS8yUeLVd6Wr4cN7UjJ9IBTS0f/a9i7UB4x4Ti70kGnksBtZ7o4Z7YRbvCMMR/jQmkoOBa/4fg==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-refresh": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz", + "integrity": "sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.5.10", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.10.tgz", + "integrity": "sha512-m3zvBRANPBw3qxVVjEIPEQinkcwlFZ4qyomuWVpNJdv4c6MvHfXV0C3L9Jx5rr3HeBHKNRX+1jreB5QloDIJjA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.6", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", + "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", + "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.16.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.23.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", + "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.16.1", + "react-router": "6.23.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-shepherd": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/react-shepherd/-/react-shepherd-4.3.0.tgz", + "integrity": "sha512-zJnFfn9llfyULPPJO/hjD9U1gCH/ylmMcgORt+IcxZ2Yx1DTvUJfsclK/UzIt6Stt63KcqlUCMB4nAMqTecFcQ==", + "license": "MIT", + "dependencies": { + "resize-observer-polyfill": "^1.5.1", + "shepherd.js": "^11.0.1" + }, + "engines": { + "node": ">=16", + "npm": ">=7" + }, + "peerDependencies": { + "react": "^17.0.2 || 18.x", + "react-dom": "^17.0.2 || 18.x" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", + "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "invariant": "^2.2.4", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-textarea-autosize": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.3.tgz", + "integrity": "sha512-XT1024o2pqCuZSuBt9FwHlaDeNtVrtCXu0Rnz88t1jUGheCLa3PhjE1GH8Ctm2axEtvdCl5SUHYschyQ0L5QHQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.20.13", + "use-composed-ref": "^1.3.0", + "use-latest": "^1.2.1" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-tooltip": { + "version": "5.27.0", + "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.27.0.tgz", + "integrity": "sha512-JXROcdfCEbCqkAkh8LyTSP3guQ0dG53iY2E2o4fw3D8clKzziMpE6QG6CclDaHELEKTzpMSeAOsdtg0ahoQosw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.6.1", + "classnames": "^2.3.0" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/reactour": { + "version": "1.19.4", + "resolved": "https://registry.npmjs.org/reactour/-/reactour-1.19.4.tgz", + "integrity": "sha512-cMIaUQazGkdXt03m7AXAYXrCdyQl+uvH4nQBGP/oEjIaeSTZqj92C3W3y6doPakIIu21WeoGh1b0hBRKOxIViA==", + "license": "MIT", + "dependencies": { + "@rooks/use-mutation-observer": "4.11.2", + "classnames": "2.3.1", + "focus-outline-manager": "^1.0.2", + "lodash.debounce": "4.0.8", + "prop-types": "15.7.2", + "react-focus-lock": "^2.12.1", + "scroll-smooth": "1.1.1", + "scrollparent": "2.0.1" + }, + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0-0 || ^18.0.0-0", + "react-dom": "^16.3.0 || ^17.0.0-0 || ^18.0.0-0", + "react-is": "^16.8 || ^17.0.0-0 || ^18.0.0-0", + "styled-components": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/reactour/node_modules/classnames": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==", + "license": "MIT" + }, + "node_modules/reactour/node_modules/prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "node_modules/reactour/node_modules/scrollparent": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/scrollparent/-/scrollparent-2.0.1.tgz", + "integrity": "sha512-HSdN78VMvFCSGCkh0oYX/tY4R3P1DW61f8+TeZZ4j2VLgfwvw0bpRSOv4PCVKisktIwbzHCfZsx+rLbbDBqIBA==", + "license": "ISC" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" + }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, + "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, + "license": "MIT", + "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-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/retry": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz", + "integrity": "sha512-ZXUSQYTHdl3uS7IuCehYfMzKyIDBNoAuUblvy5oGO5UJSUTmStUUVPXbA9Qxd173Bgre53yCQczQuHgRWAdvJQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "2.77.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.77.3.tgz", + "integrity": "sha512-/qxNTG7FbmefJWoeeYJFbHehJ2HNWnjkAFRKzWN/45eNBBF/r8lo992CwcJXEzyVxs5FmfId+vTSTQDb+bxA+g==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sanitize-html": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.13.0.tgz", + "integrity": "sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==", + "license": "MIT", + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, + "node_modules/sanitize-html/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/scroll": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/scroll/-/scroll-3.0.1.tgz", + "integrity": "sha512-pz7y517OVls1maEzlirKO5nPYle9AXsFzTMNJrRGmT951mzpIBy7sNHOg5o/0MQd/NqliCiWnAi0kZneMPFLcg==", + "license": "MIT" + }, + "node_modules/scroll-smooth": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/scroll-smooth/-/scroll-smooth-1.1.1.tgz", + "integrity": "sha512-i9e/hJf0ODPEsy+AubE0zES6xdOuIvtebe5MvdSI1lB4t91k+O+8kV15CYfPN0yPH4j4hZUoKM3rVaPVcmiOoQ==", + "license": "MIT" + }, + "node_modules/scrollparent": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/scrollparent/-/scrollparent-2.1.0.tgz", + "integrity": "sha512-bnnvJL28/Rtz/kz2+4wpBjHzWoEzXhVg/TE8BeVGJHUqE8THNIRnDxDWMktwM+qahvlRdvlLdsQfYe+cuqfZeA==", + "license": "ISC" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/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==", + "license": "MIT", + "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==", + "license": "MIT", + "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/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "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==", + "license": "ISC" + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shepherd.js": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/shepherd.js/-/shepherd.js-11.2.0.tgz", + "integrity": "sha512-2hbz3N7GuuTjI7y3sfnoqKnH0cNhExx67IJtCTGQI2KhBEyvegsDYW5qjj5BlvvVtQjmL/O/J1GQEciwfoZWpw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.5.1", + "deepmerge": "^4.3.1" + }, + "engines": { + "node": "16.* || >= 18" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/RobbieTheWagner" + } + }, + "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==", + "license": "MIT", + "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": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/socket.io-client": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", + "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/ssr-window": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/ssr-window/-/ssr-window-4.0.2.tgz", + "integrity": "sha512-ISv/Ch+ig7SOtw7G2+qkwfVASzazUnvlDTwypdLoPoySv+6MqlOV10VwPSE6EWkGjhW50lUmghPmpYZXMu/+AQ==", + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/styled-components": { + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.11.tgz", + "integrity": "sha512-uuzIIfnVkagcVHv9nE0VPlHPSCmXIUGKfJ42LNjxCCTDTL5sgnJ8Z7GZBq0EnLYGln77tPpEpExt2+qa+cZqSw==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/traverse": "^7.4.5", + "@emotion/is-prop-valid": "^1.1.0", + "@emotion/stylis": "^0.8.4", + "@emotion/unitless": "^0.7.4", + "babel-plugin-styled-components": ">= 1.12.0", + "css-to-react-native": "^3.0.0", + "hoist-non-react-statics": "^3.0.0", + "shallowequal": "^1.1.0", + "supports-color": "^5.5.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0", + "react-is": ">= 16.8.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/suneditor": { + "version": "2.46.3", + "resolved": "https://registry.npmjs.org/suneditor/-/suneditor-2.46.3.tgz", + "integrity": "sha512-KBk9sq91mg026CYaJHoWD5a3hljXVSm6Hmh6671peVUBgaDa6GvneirY9ZAvRU+pW+rh1wiGVDjHBwRxIrnqrw==", + "license": "MIT" + }, + "node_modules/suneditor-react": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/suneditor-react/-/suneditor-react-3.6.1.tgz", + "integrity": "sha512-12f9KLnEB6pAdyHJINTzRBg3UOWVZZ+jVYSEtwdBTDYQW4amUZr0xOnpikbBAlxb9rcTYV5RHAsad3gnNhLsuA==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0", + "suneditor": "^2.44.10" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/swiper": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/swiper/-/swiper-8.4.7.tgz", + "integrity": "sha512-VwO/KU3i9IV2Sf+W2NqyzwWob4yX9Qdedq6vBtS0rFqJ6Fa5iLUJwxQkuD4I38w0WDJwmFl8ojkdcRFPHWD+2g==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/swiperjs" + }, + { + "type": "open_collective", + "url": "http://opencollective.com/swiper" + } + ], + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "dom7": "^4.0.4", + "ssr-window": "^4.0.2" + }, + "engines": { + "node": ">= 4.7.0" + } + }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", + "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/terser": { + "version": "5.31.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.1.tgz", + "integrity": "sha512-37upzU1+viGvuFtBo9NPufCb9dwM0+l9hMxYyWfBA+fbwrPqNJAhbZ6W47bBFnZHKHTUBnMvi87434qq+qnxOg==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/throttle-debounce": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz", + "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", + "license": "MIT" + }, + "node_modules/traverse": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.9.tgz", + "integrity": "sha512-7bBrcF+/LQzSgFmT0X5YclVqQxtv7TDJ1f8Wj7ibBu/U6BMLeOpUxuZjV7rMc44UtKxlnMFigdhFAIszSX1DMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "gopd": "^1.0.1", + "typedarray.prototype.slice": "^1.0.3", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tree-changes": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/tree-changes/-/tree-changes-0.11.2.tgz", + "integrity": "sha512-4gXlUthrl+RabZw6lLvcCDl6KfJOCmrC16BC5CRdut1EAH509Omgg0BfKLY+ViRlzrvYOTWR0FMS2SQTwzumrw==", + "license": "MIT", + "dependencies": { + "@gilbarbara/deep-equal": "^0.3.1", + "is-lite": "^1.2.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "license": "0BSD" + }, + "node_modules/tus-js-client": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tus-js-client/-/tus-js-client-2.3.2.tgz", + "integrity": "sha512-5a2rm7gp+G7Z+ZB0AO4PzD/dwczB3n1fZeWO5W8AWLJ12RRk1rY4Aeb2VAYX9oKGE+/rGPrdxoFPA/vDSVKnpg==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.1.2", + "combine-errors": "^3.0.3", + "is-stream": "^2.0.0", + "js-base64": "^2.6.1", + "lodash.throttle": "^4.1.1", + "proper-lockfile": "^2.0.1", + "url-parse": "^1.5.7" + } + }, + "node_modules/type-fest": { + "version": "4.20.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.20.1.tgz", + "integrity": "sha512-R6wDsVsoS9xYOpy8vgeBlqpdOyzJ12HNfQhC/aAKWM3YoCV9TtunJzh/QpkMgeDhkoynDcw5f1y+qF9yc/HHyg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "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==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedarray.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typedarray.prototype.slice/-/typedarray.prototype.slice-1.0.3.tgz", + "integrity": "sha512-8WbVAQAUlENo1q3c3zZYuy5k9VzBQvp8AX9WOtbvyWlLM1v5JaSRmjubLjzHF4JFtptjH/5c/i95yaElvcjC0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-errors": "^1.3.0", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-offset": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uppy": { + "version": "2.13.8", + "resolved": "https://registry.npmjs.org/uppy/-/uppy-2.13.8.tgz", + "integrity": "sha512-qsEvSil1ebnBj+WgkCKn3GLNCKN4xyGKjtrjz6BUlGWyaw6kv47Kb8q1VmBl85z17tkedA/OLfPd0pXZ8RfQNg==", + "license": "MIT", + "dependencies": { + "@uppy/audio": "^0.3.3", + "@uppy/aws-s3": "^2.2.4", + "@uppy/aws-s3-multipart": "^2.4.3", + "@uppy/box": "^1.0.8", + "@uppy/companion-client": "^2.2.2", + "@uppy/compressor": "^0.3.3", + "@uppy/core": "^2.3.4", + "@uppy/dashboard": "^2.4.3", + "@uppy/drag-drop": "^2.1.2", + "@uppy/drop-target": "^1.1.4", + "@uppy/dropbox": "^2.0.8", + "@uppy/facebook": "^2.0.8", + "@uppy/file-input": "^2.1.2", + "@uppy/form": "^2.0.7", + "@uppy/golden-retriever": "^2.1.3", + "@uppy/google-drive": "^2.1.2", + "@uppy/image-editor": "^1.4.2", + "@uppy/informer": "^2.1.1", + "@uppy/instagram": "^2.1.2", + "@uppy/onedrive": "^2.1.2", + "@uppy/progress-bar": "^2.1.2", + "@uppy/provider-views": "^2.1.3", + "@uppy/redux-dev-tools": "^2.1.1", + "@uppy/remote-sources": "^0.1.2", + "@uppy/screen-capture": "^2.1.2", + "@uppy/status-bar": "^2.2.2", + "@uppy/store-default": "^2.1.1", + "@uppy/store-redux": "^2.1.1", + "@uppy/thumbnail-generator": "^2.2.2", + "@uppy/transloadit": "^2.3.7", + "@uppy/tus": "^2.4.6", + "@uppy/unsplash": "^2.1.1", + "@uppy/url": "^2.2.1", + "@uppy/webcam": "^2.2.2", + "@uppy/xhr-upload": "^2.1.3", + "@uppy/zoom": "^1.1.2" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", + "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-composed-ref": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz", + "integrity": "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-latest": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-latest/-/use-latest-1.2.1.tgz", + "integrity": "sha512-xA+AVm/Wlg3e2P/JiItTziwS7FK92LWrDB0p+hgXloIMuVCeJJ8v6f0eeHyPZaJrM+usM1FkFfbNCrJGs8A/zw==", + "license": "MIT", + "dependencies": { + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", + "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/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" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "2.9.18", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.18.tgz", + "integrity": "sha512-sAOqI5wNM9QvSEE70W3UGMdT8cyEn0+PmJMTFvTB8wB0YbYUWw3gUbY62AOyrXosGieF2htmeLATvNxpv/zNyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.14.27", + "postcss": "^8.4.13", + "resolve": "^1.22.0", + "rollup": ">=2.59.0 <2.78.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": ">=12.2.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "less": "*", + "sass": "*", + "stylus": "*" + }, + "peerDependenciesMeta": { + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + } + } + }, + "node_modules/vite-plugin-html": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/vite-plugin-html/-/vite-plugin-html-3.2.2.tgz", + "integrity": "sha512-vb9C9kcdzcIo/Oc3CLZVS03dL5pDlOFuhGlZYDCJ840BhWl/0nGeZWf3Qy7NlOayscY4Cm/QRgULCQkEZige5Q==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^4.2.0", + "colorette": "^2.0.16", + "connect-history-api-fallback": "^1.6.0", + "consola": "^2.15.3", + "dotenv": "^16.0.0", + "dotenv-expand": "^8.0.2", + "ejs": "^3.1.6", + "fast-glob": "^3.2.11", + "fs-extra": "^10.0.1", + "html-minifier-terser": "^6.1.0", + "node-html-parser": "^5.3.3", + "pathe": "^0.2.0" + }, + "peerDependencies": { + "vite": ">=2.0.0" + } + }, + "node_modules/vite-plugin-svgr": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-2.4.0.tgz", + "integrity": "sha512-q+mJJol6ThvqkkJvvVFEndI4EaKIjSI0I3jNFgSoC9fXAz1M7kYTVUin8fhUsFojFDKZ9VHKtX6NXNaOLpbsHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.2", + "@svgr/core": "^6.5.1" + }, + "peerDependencies": { + "vite": "^2.6.0 || 3 || 4" + } + }, + "node_modules/vite-plugin-svgr/node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.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, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wildcard": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-1.1.2.tgz", + "integrity": "sha512-DXukZJxpHA8LuotRwL0pP1+rS6CS7FF2qStDDE1C7DDg2rLud2PXRMuEDYIPhgEezwnlHNL4c+N6MfMTjCGTng==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", + "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yup": { + "version": "0.32.11", + "resolved": "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz", + "integrity": "sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@types/lodash": "^4.14.175", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "nanoclone": "^0.2.1", + "property-expr": "^2.0.4", + "toposort": "^2.0.2" + }, + "engines": { + "node": ">=10" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..8062e0c --- /dev/null +++ b/package.json @@ -0,0 +1,85 @@ +{ + "name": "adminportal", + "private": true, + "version": "0.0.0", + "scripts": { + "dev": "vite", + "tw": "npx tailwindcss -i ./src/index.css -o ./src/output.css --watch", + "build": "vite build; cp metadata.json dist;", + "preview": "vite preview" + }, + "dependencies": { + "@headlessui/react": "^1.7.13", + "@headlessui/tailwindcss": "^0.1.2", + "@heroicons/react": "^2.0.17", + "@hookform/resolvers": "^2.8.10", + "@mantine/core": "^7.3.1", + "@mantine/form": "^7.3.2", + "@mantine/hooks": "^7.3.1", + "@mantine/notifications": "^7.3.2", + "@reactour/tour": "^3.6.1", + "@stripe/react-stripe-js": "^1.9.0", + "@stripe/stripe-js": "^1.32.0", + "@uppy/aws-s3": "^2.1.0", + "@uppy/core": "^2.2.0", + "@uppy/dashboard": "^2.1.4", + "@uppy/drag-drop": "^2.1.0", + "@uppy/dropbox": "^2.0.5", + "@uppy/google-drive": "^2.0.5", + "@uppy/onedrive": "^2.0.6", + "@uppy/react": "^2.2.0", + "@uppy/tus": "^2.3.0", + "@uppy/xhr-upload": "^2.1.0", + "axios": "^1.1.3", + "body-scroll-lock": "^4.0.0-beta.0", + "cors": "^2.8.5", + "device-uuid": "^1.0.4", + "dompurify": "^3.0.3", + "ejs": "^3.1.8", + "emoji-picker-react": "^4.4.7", + "express": "^4.18.2", + "intro.js": "^7.2.0", + "intro.js-react": "^1.0.0", + "linkifyjs": "^4.0.2", + "moment": "^2.29.3", + "react": "^18.0.0", + "react-calendar": "^4.0.0", + "react-dom": "^18.0.0", + "react-drag-drop-files": "^2.3.8", + "react-google-autocomplete": "^2.7.1", + "react-google-recaptcha": "^2.1.0", + "react-hook-form": "^7.34.2", + "react-html-table-to-excel": "^2.0.0", + "react-infinite-scroll-component": "^6.1.0", + "react-joyride": "^2.5.5", + "react-json-to-csv": "^1.2.0", + "react-loading-skeleton": "^3.1.0", + "react-router": "^6.2.2", + "react-router-dom": "^6.2.2", + "react-shepherd": "^4.2.0", + "react-tooltip": "^5.4.0", + "reactour": "^1.19.0", + "sanitize-html": "^2.10.0", + "suneditor": "^2.44.3", + "suneditor-react": "^3.4.1", + "swiper": "^8.4.4", + "uppy": "^2.9.1", + "uuid": "^9.0.0", + "vite-plugin-html": "^3.2.0", + "yup": "^0.32.11" + }, + "devDependencies": { + "@tailwindcss/custom-forms": "^0.2.1", + "@types/node": "^18.11.17", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@vitejs/plugin-react": "^1.3.0", + "autoprefixer": "^10.4.7", + "postcss": "^8.4.14", + "prettier": "^2.8.4", + "prettier-plugin-tailwindcss": "^0.2.5", + "tailwindcss": "^3.2.7", + "vite": "^2.9.9", + "vite-plugin-svgr": "^2.2.2" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/public/american-express.png b/public/american-express.png new file mode 100644 index 0000000..b70ba62 Binary files /dev/null and b/public/american-express.png differ diff --git a/public/apple-icon.png b/public/apple-icon.png new file mode 100644 index 0000000..0ff9243 Binary files /dev/null and b/public/apple-icon.png differ diff --git a/public/button-spinner-6.svg b/public/button-spinner-6.svg new file mode 100644 index 0000000..e5e763a --- /dev/null +++ b/public/button-spinner-6.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/public/contact.png b/public/contact.png new file mode 100644 index 0000000..18df61e Binary files /dev/null and b/public/contact.png differ diff --git a/public/date-placeholder.png b/public/date-placeholder.png new file mode 100644 index 0000000..0b73d38 Binary files /dev/null and b/public/date-placeholder.png differ diff --git a/public/default-property.jpg b/public/default-property.jpg new file mode 100644 index 0000000..1736f55 Binary files /dev/null and b/public/default-property.jpg differ diff --git a/public/default.png b/public/default.png new file mode 100644 index 0000000..21eef5f Binary files /dev/null and b/public/default.png differ diff --git a/public/discover.png b/public/discover.png new file mode 100644 index 0000000..70d88b9 Binary files /dev/null and b/public/discover.png differ diff --git a/public/facebook-icon.png b/public/facebook-icon.png new file mode 100644 index 0000000..f237300 Binary files /dev/null and b/public/facebook-icon.png differ diff --git a/public/google-icon.png b/public/google-icon.png new file mode 100644 index 0000000..94ebaca Binary files /dev/null and b/public/google-icon.png differ diff --git a/public/invisible.png b/public/invisible.png new file mode 100644 index 0000000..465070c Binary files /dev/null and b/public/invisible.png differ diff --git a/public/jumbotron1.jpg b/public/jumbotron1.jpg new file mode 100644 index 0000000..ff5f160 Binary files /dev/null and b/public/jumbotron1.jpg differ diff --git a/public/login-bg.jpg b/public/login-bg.jpg new file mode 100644 index 0000000..0339ddf Binary files /dev/null and b/public/login-bg.jpg differ diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..6265d4f Binary files /dev/null and b/public/logo.png differ diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..930089d --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,39 @@ +{ + "name": "Ergo", + "short_name": "Ergo", + "description": "Ergo App", + "start_url": "/", + "scope": "/", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#000000", + "icons": [ + { + "src": "./logo.png", + "sizes": "124x124", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "./logo.png", + "sizes": "124x124", + "type": "image/png" + }, + { + "src": "./logo.png", + "sizes": "124x124", + "type": "image/png" + }, + { + "src": "./logo.png", + "sizes": "124x124", + "type": "image/png" + }, + { + "src": "./logo.png", + "sizes": "124x124", + "type": "image/png" + } + ], + "purpose": "any" +} \ No newline at end of file diff --git a/public/mastercard.jpg b/public/mastercard.jpg new file mode 100644 index 0000000..9da28f1 Binary files /dev/null and b/public/mastercard.jpg differ diff --git a/public/people-placeholder.png b/public/people-placeholder.png new file mode 100644 index 0000000..28e3cc6 Binary files /dev/null and b/public/people-placeholder.png differ diff --git a/public/propertyspace.jpg b/public/propertyspace.jpg new file mode 100644 index 0000000..0692bbf Binary files /dev/null and b/public/propertyspace.jpg differ diff --git a/public/propertyspace2.jpg b/public/propertyspace2.jpg new file mode 100644 index 0000000..6effdc2 Binary files /dev/null and b/public/propertyspace2.jpg differ diff --git a/public/show.png b/public/show.png new file mode 100644 index 0000000..a659b82 Binary files /dev/null and b/public/show.png differ diff --git a/public/sign-up-bg.jpg b/public/sign-up-bg.jpg new file mode 100644 index 0000000..8bca078 Binary files /dev/null and b/public/sign-up-bg.jpg differ diff --git a/public/visa.jpg b/public/visa.jpg new file mode 100644 index 0000000..f5652d0 Binary files /dev/null and b/public/visa.jpg differ diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..c2aab7e --- /dev/null +++ b/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file diff --git a/server.js b/server.js new file mode 100644 index 0000000..f6124b5 --- /dev/null +++ b/server.js @@ -0,0 +1,13 @@ +"use strict"; +const app = require("./app"); + +const PORT = 3001; + +const onListen = async () => { + console.log("Server running at ", `http://localhost:${PORT}`); + console.log("\n"); +}; + +const server = app.listen(PORT, onListen); + +module.exports = { server, onListen, PORT }; diff --git a/src/App.css b/src/App.css new file mode 100644 index 0000000..a6ed5a8 --- /dev/null +++ b/src/App.css @@ -0,0 +1,9 @@ +.hidden-scrollbar { + overflow: -moz-scrollbars-none; /* For older Firefox versions */ + scrollbar-width: none; /* For Firefox */ + -ms-overflow-style: none; /* For Internet Explorer and Edge */ + } + + .hidden-scrollbar::-webkit-scrollbar { + display: none; /* For WebKit browsers */ + } \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..db959a1 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,45 @@ +import React, { useContext, useEffect, useState } from "react"; +import AuthProvider, { AuthContext } from "./authContext"; +import GlobalProvider, { GlobalContext } from "./globalContext"; +import Main from "./main"; +import { BrowserRouter as Router } from "react-router-dom"; +import { loadStripe } from "@stripe/stripe-js"; +import { Elements } from "@stripe/react-stripe-js"; +import "@uppy/core/dist/style.css"; +import "@uppy/dashboard/dist/style.css"; +// Import Swiper styles +import "swiper/css"; +import "swiper/css/navigation"; +import "swiper/css/pagination"; +import "react-loading-skeleton/dist/skeleton.css"; +import '@mantine/core/styles.css'; +import '@mantine/notifications/styles.css'; +import { MantineProvider } from "@mantine/core"; +import { Notifications } from '@mantine/notifications'; + +const stripePromise = loadStripe(import.meta.env.VITE_REACT_STRIPE_PUBLIC_KEY); + + +function App() { + + return ( + + + + + + +
+ + + + + + ); +} + +export default App; diff --git a/src/assets/arrow-narrow-left.svg b/src/assets/arrow-narrow-left.svg new file mode 100644 index 0000000..ee284f0 --- /dev/null +++ b/src/assets/arrow-narrow-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/arrow-narrow-right.svg b/src/assets/arrow-narrow-right.svg new file mode 100644 index 0000000..52442a8 --- /dev/null +++ b/src/assets/arrow-narrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/bank-note-one.svg b/src/assets/bank-note-one.svg new file mode 100644 index 0000000..cd91369 --- /dev/null +++ b/src/assets/bank-note-one.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/booking-receipt.svg b/src/assets/booking-receipt.svg new file mode 100644 index 0000000..a5f0ac7 --- /dev/null +++ b/src/assets/booking-receipt.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/building-one.svg b/src/assets/building-one.svg new file mode 100644 index 0000000..ed074cb --- /dev/null +++ b/src/assets/building-one.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/calender.svg b/src/assets/calender.svg new file mode 100644 index 0000000..fdca76f --- /dev/null +++ b/src/assets/calender.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/chevron-down.svg b/src/assets/chevron-down.svg new file mode 100644 index 0000000..bd19468 --- /dev/null +++ b/src/assets/chevron-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/dots.svg b/src/assets/dots.svg new file mode 100644 index 0000000..9ba1f89 --- /dev/null +++ b/src/assets/dots.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/file-check-three.svg b/src/assets/file-check-three.svg new file mode 100644 index 0000000..aa2b3f7 --- /dev/null +++ b/src/assets/file-check-three.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/file-plus-three.svg b/src/assets/file-plus-three.svg new file mode 100644 index 0000000..cf9f56f --- /dev/null +++ b/src/assets/file-plus-three.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/file-question-three.svg b/src/assets/file-question-three.svg new file mode 100644 index 0000000..4cfb655 --- /dev/null +++ b/src/assets/file-question-three.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/file-search-one.svg b/src/assets/file-search-one.svg new file mode 100644 index 0000000..ecb9c72 --- /dev/null +++ b/src/assets/file-search-one.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/grid-one.svg b/src/assets/grid-one.svg new file mode 100644 index 0000000..7a9de73 --- /dev/null +++ b/src/assets/grid-one.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/home-line.svg b/src/assets/home-line.svg new file mode 100644 index 0000000..25624c8 --- /dev/null +++ b/src/assets/home-line.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/home-three.svg b/src/assets/home-three.svg new file mode 100644 index 0000000..4e3b619 --- /dev/null +++ b/src/assets/home-three.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/image-three.svg b/src/assets/image-three.svg new file mode 100644 index 0000000..412f853 --- /dev/null +++ b/src/assets/image-three.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/json/badWords.json b/src/assets/json/badWords.json new file mode 100644 index 0000000..b886761 --- /dev/null +++ b/src/assets/json/badWords.json @@ -0,0 +1,405 @@ +[ + "2g1c", + "2 girls 1 cup", + "acrotomophilia", + "alabama hot pocket", + "alaskan pipeline", + "anal", + "anilingus", + "anus", + "apeshit", + "arsehole", + "ass", + "asshole", + "assmunch", + "auto erotic", + "autoerotic", + "babeland", + "baby batter", + "baby juice", + "ball gag", + "ball gravy", + "ball kicking", + "ball licking", + "ball sack", + "ball sucking", + "bangbros", + "bangbus", + "bareback", + "barely legal", + "barenaked", + "bastard", + "bastardo", + "bastinado", + "bbw", + "bdsm", + "beaner", + "beaners", + "beaver cleaver", + "beaver lips", + "beastiality", + "bestiality", + "big black", + "big breasts", + "big knockers", + "big tits", + "bimbos", + "birdlock", + "bitch", + "bitches", + "black cock", + "blonde action", + "blonde on blonde action", + "blowjob", + "blow job", + "blow your load", + "blue waffle", + "blumpkin", + "bollocks", + "bondage", + "boner", + "boob", + "boobs", + "booty call", + "brown showers", + "brunette action", + "bukkake", + "bulldyke", + "bullet vibe", + "bullshit", + "bung hole", + "bunghole", + "busty", + "butt", + "buttcheeks", + "butthole", + "camel toe", + "camgirl", + "camslut", + "camwhore", + "carpet muncher", + "carpetmuncher", + "chocolate rosebuds", + "cialis", + "circlejerk", + "cleveland steamer", + "clit", + "clitoris", + "clover clamps", + "clusterfuck", + "cock", + "cocks", + "coprolagnia", + "coprophilia", + "cornhole", + "coon", + "coons", + "creampie", + "cum", + "cumming", + "cumshot", + "cumshots", + "cunnilingus", + "cunt", + "darkie", + "date rape", + "daterape", + "deep throat", + "deepthroat", + "dendrophilia", + "dick", + "dildo", + "dingleberry", + "dingleberries", + "dirty pillows", + "dirty sanchez", + "doggie style", + "doggiestyle", + "doggy style", + "doggystyle", + "dog style", + "dolcett", + "domination", + "dominatrix", + "dommes", + "donkey punch", + "double dong", + "double penetration", + "dp action", + "dry hump", + "dvda", + "eat my ass", + "ecchi", + "ejaculation", + "erotic", + "erotism", + "escort", + "eunuch", + "fag", + "faggot", + "fecal", + "felch", + "fellatio", + "feltch", + "female squirting", + "femdom", + "figging", + "fingerbang", + "fingering", + "fisting", + "foot fetish", + "footjob", + "frotting", + "fuck", + "fuck buttons", + "fuckin", + "fucking", + "fucktards", + "fudge packer", + "fudgepacker", + "futanari", + "gangbang", + "gang bang", + "gay sex", + "genitals", + "giant cock", + "girl on", + "girl on top", + "girls gone wild", + "goatcx", + "goatse", + "god damn", + "gokkun", + "golden shower", + "goodpoop", + "goo girl", + "goregasm", + "grope", + "group sex", + "g-spot", + "guro", + "hand job", + "handjob", + "hard core", + "hardcore", + "hentai", + "homoerotic", + "honkey", + "hooker", + "horny", + "hot carl", + "hot chick", + "how to kill", + "how to murder", + "huge fat", + "humping", + "incest", + "intercourse", + "jack off", + "jail bait", + "jailbait", + "jelly donut", + "jerk off", + "jigaboo", + "jiggaboo", + "jiggerboo", + "jizz", + "juggs", + "kike", + "kinbaku", + "kinkster", + "kinky", + "knobbing", + "leather restraint", + "leather straight jacket", + "lemon party", + "livesex", + "lolita", + "lovemaking", + "make me come", + "male squirting", + "masturbate", + "masturbating", + "masturbation", + "menage a trois", + "milf", + "missionary position", + "mong", + "motherfucker", + "mound of venus", + "mr hands", + "muff diver", + "muffdiving", + "nambla", + "nawashi", + "negro", + "neonazi", + "nigga", + "nigger", + "nig nog", + "nimphomania", + "nipple", + "nipples", + "nsfw", + "nsfw images", + "nude", + "nudity", + "nutten", + "nympho", + "nymphomania", + "octopussy", + "omorashi", + "one cup two girls", + "one guy one jar", + "orgasm", + "orgy", + "paedophile", + "paki", + "panties", + "panty", + "pedobear", + "pedophile", + "pegging", + "penis", + "phone sex", + "piece of shit", + "pikey", + "pissing", + "piss pig", + "pisspig", + "playboy", + "pleasure chest", + "pole smoker", + "ponyplay", + "poof", + "poon", + "poontang", + "punany", + "poop chute", + "poopchute", + "porn", + "porno", + "pornography", + "prince albert piercing", + "pthc", + "pubes", + "pussy", + "queaf", + "queef", + "quim", + "raghead", + "raging boner", + "rape", + "raping", + "rapist", + "rectum", + "reverse cowgirl", + "rimjob", + "rimming", + "rosy palm", + "rosy palm and her 5 sisters", + "rusty trombone", + "sadism", + "santorum", + "scat", + "schlong", + "scissoring", + "semen", + "sex", + "sexcam", + "sexo", + "sexy", + "sexual", + "sexually", + "sexuality", + "shaved beaver", + "shaved pussy", + "shemale", + "shibari", + "shit", + "shitblimp", + "shitty", + "shota", + "shrimping", + "skeet", + "slanteye", + "slut", + "s&m", + "smut", + "snatch", + "snowballing", + "sodomize", + "sodomy", + "spastic", + "spic", + "splooge", + "splooge moose", + "spooge", + "spread legs", + "spunk", + "strap on", + "strapon", + "strappado", + "strip club", + "style doggy", + "suck", + "sucks", + "suicide girls", + "sultry women", + "swastika", + "swinger", + "tainted love", + "taste my", + "tea bagging", + "threesome", + "throating", + "thumbzilla", + "tied up", + "tight white", + "tit", + "tits", + "titties", + "titty", + "tongue in a", + "topless", + "tosser", + "towelhead", + "tranny", + "tribadism", + "tub girl", + "tubgirl", + "tushy", + "twat", + "twink", + "twinkie", + "two girls one cup", + "undressing", + "upskirt", + "urethra play", + "urophilia", + "vagina", + "venus mound", + "viagra", + "vibrator", + "violet wand", + "vorarephilia", + "voyeur", + "voyeurweb", + "voyuer", + "vulva", + "wank", + "wetback", + "wet dream", + "white power", + "whore", + "worldsex", + "wrapping men", + "wrinkled starfish", + "xx", + "xxx", + "yaoi", + "yellow showers", + "yiffy", + "zoophilia", + "đŸ–•" +] \ No newline at end of file diff --git a/src/assets/json/common-passwords.json b/src/assets/json/common-passwords.json new file mode 100644 index 0000000..8d8c7be --- /dev/null +++ b/src/assets/json/common-passwords.json @@ -0,0 +1,10003 @@ +[ + "password", + "123456", + "12345678", + "1234", + "qwerty", + "12345", + "dragon", + "pussy", + "baseball", + "football", + "letmein", + "monkey", + "696969", + "abc123", + "mustang", + "michael", + "shadow", + "master", + "jennifer", + "111111", + "2000", + "jordan", + "superman", + "harley", + "1234567", + "fuckme", + "hunter", + "fuckyou", + "trustno1", + "ranger", + "buster", + "thomas", + "tigger", + "robert", + "soccer", + "fuck", + "batman", + "test", + "pass", + "killer", + "hockey", + "george", + "charlie", + "andrew", + "michelle", + "love", + "sunshine", + "jessica", + "asshole", + "6969", + "pepper", + "daniel", + "access", + "123456789", + "654321", + "joshua", + "maggie", + "starwars", + "silver", + "william", + "dallas", + "yankees", + "123123", + "ashley", + "666666", + "hello", + "amanda", + "orange", + "biteme", + "freedom", + "computer", + "sexy", + "thunder", + "nicole", + "ginger", + "heather", + "hammer", + "summer", + "corvette", + "taylor", + "fucker", + "austin", + "1111", + "merlin", + "matthew", + "121212", + "golfer", + "cheese", + "princess", + "martin", + "chelsea", + "patrick", + "richard", + "diamond", + "yellow", + "bigdog", + "secret", + "asdfgh", + "sparky", + "cowboy", + "camaro", + "anthony", + "matrix", + "falcon", + "iloveyou", + "bailey", + "guitar", + "jackson", + "purple", + "scooter", + "phoenix", + "aaaaaa", + "morgan", + "tigers", + "porsche", + "mickey", + "maverick", + "cookie", + "nascar", + "peanut", + "justin", + "131313", + "money", + "horny", + "samantha", + "panties", + "steelers", + "joseph", + "snoopy", + "boomer", + "whatever", + "iceman", + "smokey", + "gateway", + "dakota", + "cowboys", + "eagles", + "chicken", + "dick", + "black", + "zxcvbn", + "please", + "andrea", + "ferrari", + "knight", + "hardcore", + "melissa", + "compaq", + "coffee", + "booboo", + "bitch", + "johnny", + "bulldog", + "xxxxxx", + "welcome", + "james", + "player", + "ncc1701", + "wizard", + "scooby", + "charles", + "junior", + "internet", + "bigdick", + "mike", + "brandy", + "tennis", + "blowjob", + "banana", + "monster", + "spider", + "lakers", + "miller", + "rabbit", + "enter", + "mercedes", + "brandon", + "steven", + "fender", + "john", + "yamaha", + "diablo", + "chris", + "boston", + "tiger", + "marine", + "chicago", + "rangers", + "gandalf", + "winter", + "bigtits", + "barney", + "edward", + "raiders", + "porn", + "badboy", + "blowme", + "spanky", + "bigdaddy", + "johnson", + "chester", + "london", + "midnight", + "blue", + "fishing", + "000000", + "hannah", + "slayer", + "11111111", + "rachel", + "sexsex", + "redsox", + "thx1138", + "asdf", + "marlboro", + "panther", + "zxcvbnm", + "arsenal", + "oliver", + "qazwsx", + "mother", + "victoria", + "7777777", + "jasper", + "angel", + "david", + "winner", + "crystal", + "golden", + "butthead", + "viking", + "jack", + "iwantu", + "shannon", + "murphy", + "angels", + "prince", + "cameron", + "girls", + "madison", + "wilson", + "carlos", + "hooters", + "willie", + "startrek", + "captain", + "maddog", + "jasmine", + "butter", + "booger", + "angela", + "golf", + "lauren", + "rocket", + "tiffany", + "theman", + "dennis", + "liverpoo", + "flower", + "forever", + "green", + "jackie", + "muffin", + "turtle", + "sophie", + "danielle", + "redskins", + "toyota", + "jason", + "sierra", + "winston", + "debbie", + "giants", + "packers", + "newyork", + "jeremy", + "casper", + "bubba", + "112233", + "sandra", + "lovers", + "mountain", + "united", + "cooper", + "driver", + "tucker", + "helpme", + "fucking", + "pookie", + "lucky", + "maxwell", + "8675309", + "bear", + "suckit", + "gators", + "5150", + "222222", + "shithead", + "fuckoff", + "jaguar", + "monica", + "fred", + "happy", + "hotdog", + "tits", + "gemini", + "lover", + "xxxxxxxx", + "777777", + "canada", + "nathan", + "victor", + "florida", + "88888888", + "nicholas", + "rosebud", + "metallic", + "doctor", + "trouble", + "success", + "stupid", + "tomcat", + "warrior", + "peaches", + "apples", + "fish", + "qwertyui", + "magic", + "buddy", + "dolphins", + "rainbow", + "gunner", + "987654", + "freddy", + "alexis", + "braves", + "cock", + "2112", + "1212", + "cocacola", + "xavier", + "dolphin", + "testing", + "bond007", + "member", + "calvin", + "voodoo", + "7777", + "samson", + "alex", + "apollo", + "fire", + "tester", + "walter", + "beavis", + "voyager", + "peter", + "porno", + "bonnie", + "rush2112", + "beer", + "apple", + "scorpio", + "jonathan", + "skippy", + "sydney", + "scott", + "red123", + "power", + "gordon", + "travis", + "beaver", + "star", + "jackass", + "flyers", + "boobs", + "232323", + "zzzzzz", + "steve", + "rebecca", + "scorpion", + "doggie", + "legend", + "ou812", + "yankee", + "blazer", + "bill", + "runner", + "birdie", + "bitches", + "555555", + "parker", + "topgun", + "asdfasdf", + "heaven", + "viper", + "animal", + "2222", + "bigboy", + "4444", + "arthur", + "baby", + "private", + "godzilla", + "donald", + "williams", + "lifehack", + "phantom", + "dave", + "rock", + "august", + "sammy", + "cool", + "brian", + "platinum", + "jake", + "bronco", + "paul", + "mark", + "frank", + "heka6w2", + "copper", + "billy", + "cumshot", + "garfield", + "willow", + "cunt", + "little", + "carter", + "slut", + "albert", + "69696969", + "kitten", + "super", + "jordan23", + "eagle1", + "shelby", + "america", + "11111", + "jessie", + "house", + "free", + "123321", + "chevy", + "bullshit", + "white", + "broncos", + "horney", + "surfer", + "nissan", + "999999", + "saturn", + "airborne", + "elephant", + "marvin", + "shit", + "action", + "adidas", + "qwert", + "kevin", + "1313", + "explorer", + "walker", + "police", + "christin", + "december", + "benjamin", + "wolf", + "sweet", + "therock", + "king", + "online", + "dickhead", + "brooklyn", + "teresa", + "cricket", + "sharon", + "dexter", + "racing", + "penis", + "gregory", + "0000", + "teens", + "redwings", + "dreams", + "michigan", + "hentai", + "magnum", + "87654321", + "nothing", + "donkey", + "trinity", + "digital", + "333333", + "stella", + "cartman", + "guinness", + "123abc", + "speedy", + "buffalo", + "kitty", + "pimpin", + "eagle", + "einstein", + "kelly", + "nelson", + "nirvana", + "vampire", + "xxxx", + "playboy", + "louise", + "pumpkin", + "snowball", + "test123", + "girl", + "sucker", + "mexico", + "beatles", + "fantasy", + "ford", + "gibson", + "celtic", + "marcus", + "cherry", + "cassie", + "888888", + "natasha", + "sniper", + "chance", + "genesis", + "hotrod", + "reddog", + "alexande", + "college", + "jester", + "passw0rd", + "bigcock", + "smith", + "lasvegas", + "carmen", + "slipknot", + "3333", + "death", + "kimberly", + "1q2w3e", + "eclipse", + "1q2w3e4r", + "stanley", + "samuel", + "drummer", + "homer", + "montana", + "music", + "aaaa", + "spencer", + "jimmy", + "carolina", + "colorado", + "creative", + "hello1", + "rocky", + "goober", + "friday", + "bollocks", + "scotty", + "abcdef", + "bubbles", + "hawaii", + "fluffy", + "mine", + "stephen", + "horses", + "thumper", + "5555", + "pussies", + "darkness", + "asdfghjk", + "pamela", + "boobies", + "buddha", + "vanessa", + "sandman", + "naughty", + "douglas", + "honda", + "matt", + "azerty", + "6666", + "shorty", + "money1", + "beach", + "loveme", + "4321", + "simple", + "poohbear", + "444444", + "badass", + "destiny", + "sarah", + "denise", + "vikings", + "lizard", + "melanie", + "assman", + "sabrina", + "nintendo", + "water", + "good", + "howard", + "time", + "123qwe", + "november", + "xxxxx", + "october", + "leather", + "bastard", + "young", + "101010", + "extreme", + "hard", + "password1", + "vincent", + "pussy1", + "lacrosse", + "hotmail", + "spooky", + "amateur", + "alaska", + "badger", + "paradise", + "maryjane", + "poop", + "crazy", + "mozart", + "video", + "russell", + "vagina", + "spitfire", + "anderson", + "norman", + "eric", + "cherokee", + "cougar", + "barbara", + "long", + "420420", + "family", + "horse", + "enigma", + "allison", + "raider", + "brazil", + "blonde", + "jones", + "55555", + "dude", + "drowssap", + "jeff", + "school", + "marshall", + "lovely", + "1qaz2wsx", + "jeffrey", + "caroline", + "franklin", + "booty", + "molly", + "snickers", + "leslie", + "nipples", + "courtney", + "diesel", + "rocks", + "eminem", + "westside", + "suzuki", + "daddy", + "passion", + "hummer", + "ladies", + "zachary", + "frankie", + "elvis", + "reggie", + "alpha", + "suckme", + "simpson", + "patricia", + "147147", + "pirate", + "tommy", + "semperfi", + "jupiter", + "redrum", + "freeuser", + "wanker", + "stinky", + "ducati", + "paris", + "natalie", + "babygirl", + "bishop", + "windows", + "spirit", + "pantera", + "monday", + "patches", + "brutus", + "houston", + "smooth", + "penguin", + "marley", + "forest", + "cream", + "212121", + "flash", + "maximus", + "nipple", + "bobby", + "bradley", + "vision", + "pokemon", + "champion", + "fireman", + "indian", + "softball", + "picard", + "system", + "clinton", + "cobra", + "enjoy", + "lucky1", + "claire", + "claudia", + "boogie", + "timothy", + "marines", + "security", + "dirty", + "admin", + "wildcats", + "pimp", + "dancer", + "hardon", + "veronica", + "fucked", + "abcd1234", + "abcdefg", + "ironman", + "wolverin", + "remember", + "great", + "freepass", + "bigred", + "squirt", + "justice", + "francis", + "hobbes", + "kermit", + "pearljam", + "mercury", + "domino", + "9999", + "denver", + "brooke", + "rascal", + "hitman", + "mistress", + "simon", + "tony", + "bbbbbb", + "friend", + "peekaboo", + "naked", + "budlight", + "electric", + "sluts", + "stargate", + "saints", + "bondage", + "brittany", + "bigman", + "zombie", + "swimming", + "duke", + "qwerty1", + "babes", + "scotland", + "disney", + "rooster", + "brenda", + "mookie", + "swordfis", + "candy", + "duncan", + "olivia", + "hunting", + "blink182", + "alicia", + "8888", + "samsung", + "bubba1", + "whore", + "virginia", + "general", + "passport", + "aaaaaaaa", + "erotic", + "liberty", + "arizona", + "jesus", + "abcd", + "newport", + "skipper", + "rolltide", + "balls", + "happy1", + "galore", + "christ", + "weasel", + "242424", + "wombat", + "digger", + "classic", + "bulldogs", + "poopoo", + "accord", + "popcorn", + "turkey", + "jenny", + "amber", + "bunny", + "mouse", + "007007", + "titanic", + "liverpool", + "dreamer", + "everton", + "friends", + "chevelle", + "carrie", + "gabriel", + "psycho", + "nemesis", + "burton", + "pontiac", + "connor", + "eatme", + "lickme", + "roland", + "cumming", + "mitchell", + "ireland", + "lincoln", + "arnold", + "spiderma", + "patriots", + "goblue", + "devils", + "eugene", + "empire", + "asdfg", + "cardinal", + "brown", + "shaggy", + "froggy", + "qwer", + "kawasaki", + "kodiak", + "people", + "phpbb", + "light", + "54321", + "kramer", + "chopper", + "hooker", + "honey", + "whynot", + "lesbian", + "lisa", + "baxter", + "adam", + "snake", + "teen", + "ncc1701d", + "qqqqqq", + "airplane", + "britney", + "avalon", + "sandy", + "sugar", + "sublime", + "stewart", + "wildcat", + "raven", + "scarface", + "elizabet", + "123654", + "trucks", + "wolfpack", + "pervert", + "lawrence", + "raymond", + "redhead", + "american", + "alyssa", + "bambam", + "movie", + "woody", + "shaved", + "snowman", + "tiger1", + "chicks", + "raptor", + "1969", + "stingray", + "shooter", + "france", + "stars", + "madmax", + "kristen", + "sports", + "jerry", + "789456", + "garcia", + "simpsons", + "lights", + "ryan", + "looking", + "chronic", + "alison", + "hahaha", + "packard", + "hendrix", + "perfect", + "service", + "spring", + "srinivas", + "spike", + "katie", + "252525", + "oscar", + "brother", + "bigmac", + "suck", + "single", + "cannon", + "georgia", + "popeye", + "tattoo", + "texas", + "party", + "bullet", + "taurus", + "sailor", + "wolves", + "panthers", + "japan", + "strike", + "flowers", + "pussycat", + "chris1", + "loverboy", + "berlin", + "sticky", + "marina", + "tarheels", + "fisher", + "russia", + "connie", + "wolfgang", + "testtest", + "mature", + "bass", + "catch22", + "juice", + "michael1", + "nigger", + "159753", + "women", + "alpha1", + "trooper", + "hawkeye", + "head", + "freaky", + "dodgers", + "pakistan", + "machine", + "pyramid", + "vegeta", + "katana", + "moose", + "tinker", + "coyote", + "infinity", + "inside", + "pepsi", + "letmein1", + "bang", + "control", + "hercules", + "morris", + "james1", + "tickle", + "outlaw", + "browns", + "billybob", + "pickle", + "test1", + "michele", + "antonio", + "sucks", + "pavilion", + "changeme", + "caesar", + "prelude", + "tanner", + "adrian", + "darkside", + "bowling", + "wutang", + "sunset", + "robbie", + "alabama", + "danger", + "zeppelin", + "juan", + "rusty", + "pppppp", + "nick", + "2001", + "ping", + "darkstar", + "madonna", + "qwe123", + "bigone", + "casino", + "cheryl", + "charlie1", + "mmmmmm", + "integra", + "wrangler", + "apache", + "tweety", + "qwerty12", + "bobafett", + "simone", + "none", + "business", + "sterling", + "trevor", + "transam", + "dustin", + "harvey", + "england", + "2323", + "seattle", + "ssssss", + "rose", + "harry", + "openup", + "pandora", + "pussys", + "trucker", + "wallace", + "indigo", + "storm", + "malibu", + "weed", + "review", + "babydoll", + "doggy", + "dilbert", + "pegasus", + "joker", + "catfish", + "flipper", + "valerie", + "herman", + "fuckit", + "detroit", + "kenneth", + "cheyenne", + "bruins", + "stacey", + "smoke", + "joey", + "seven", + "marino", + "fetish", + "xfiles", + "wonder", + "stinger", + "pizza", + "babe", + "pretty", + "stealth", + "manutd", + "gracie", + "gundam", + "cessna", + "longhorn", + "presario", + "mnbvcxz", + "wicked", + "mustang1", + "victory", + "21122112", + "shelly", + "awesome", + "athena", + "q1w2e3r4", + "help", + "holiday", + "knicks", + "street", + "redneck", + "12341234", + "casey", + "gizmo", + "scully", + "dragon1", + "devildog", + "triumph", + "eddie", + "bluebird", + "shotgun", + "peewee", + "ronnie", + "angel1", + "daisy", + "special", + "metallica", + "madman", + "country", + "impala", + "lennon", + "roscoe", + "omega", + "access14", + "enterpri", + "miranda", + "search", + "smitty", + "blizzard", + "unicorn", + "tight", + "rick", + "ronald", + "asdf1234", + "harrison", + "trigger", + "truck", + "danny", + "home", + "winnie", + "beauty", + "thailand", + "1234567890", + "cadillac", + "castle", + "tyler", + "bobcat", + "buddy1", + "sunny", + "stones", + "asian", + "freddie", + "chuck", + "butt", + "loveyou", + "norton", + "hellfire", + "hotsex", + "indiana", + "short", + "panzer", + "lonewolf", + "trumpet", + "colors", + "blaster", + "12121212", + "fireball", + "logan", + "precious", + "aaron", + "elaine", + "jungle", + "atlanta", + "gold", + "corona", + "curtis", + "nikki", + "polaris", + "timber", + "theone", + "baller", + "chipper", + "orlando", + "island", + "skyline", + "dragons", + "dogs", + "benson", + "licker", + "goldie", + "engineer", + "kong", + "pencil", + "basketba", + "open", + "hornet", + "world", + "linda", + "barbie", + "chan", + "farmer", + "valentin", + "wetpussy", + "indians", + "larry", + "redman", + "foobar", + "travel", + "morpheus", + "bernie", + "target", + "141414", + "hotstuff", + "photos", + "laura", + "savage", + "holly", + "rocky1", + "fuck_inside", + "dollar", + "turbo", + "design", + "newton", + "hottie", + "moon", + "202020", + "blondes", + "4128", + "lestat", + "avatar", + "future", + "goforit", + "random", + "abgrtyu", + "jjjjjj", + "cancer", + "q1w2e3", + "smiley", + "goldberg", + "express", + "virgin", + "zipper", + "wrinkle1", + "stone", + "andy", + "babylon", + "dong", + "powers", + "consumer", + "dudley", + "monkey1", + "serenity", + "samurai", + "99999999", + "bigboobs", + "skeeter", + "lindsay", + "joejoe", + "master1", + "aaaaa", + "chocolat", + "christia", + "birthday", + "stephani", + "tang", + "1234qwer", + "alfred", + "ball", + "98765432", + "maria", + "sexual", + "maxima", + "77777777", + "sampson", + "buckeye", + "highland", + "kristin", + "seminole", + "reaper", + "bassman", + "nugget", + "lucifer", + "airforce", + "nasty", + "watson", + "warlock", + "2121", + "philip", + "always", + "dodge", + "chrissy", + "burger", + "bird", + "snatch", + "missy", + "pink", + "gang", + "maddie", + "holmes", + "huskers", + "piglet", + "photo", + "joanne", + "hamilton", + "dodger", + "paladin", + "christy", + "chubby", + "buckeyes", + "hamlet", + "abcdefgh", + "bigfoot", + "sunday", + "manson", + "goldfish", + "garden", + "deftones", + "icecream", + "blondie", + "spartan", + "julie", + "harold", + "charger", + "brandi", + "stormy", + "sherry", + "pleasure", + "juventus", + "rodney", + "galaxy", + "holland", + "escort", + "zxcvb", + "planet", + "jerome", + "wesley", + "blues", + "song", + "peace", + "david1", + "ncc1701e", + "1966", + "51505150", + "cavalier", + "gambit", + "karen", + "sidney", + "ripper", + "oicu812", + "jamie", + "sister", + "marie", + "martha", + "nylons", + "aardvark", + "nadine", + "minnie", + "whiskey", + "bing", + "plastic", + "anal", + "babylon5", + "chang", + "savannah", + "loser", + "racecar", + "insane", + "yankees1", + "mememe", + "hansolo", + "chiefs", + "fredfred", + "freak", + "frog", + "salmon", + "concrete", + "yvonne", + "zxcv", + "shamrock", + "atlantis", + "warren", + "wordpass", + "julian", + "mariah", + "rommel", + "1010", + "harris", + "predator", + "sylvia", + "massive", + "cats", + "sammy1", + "mister", + "stud", + "marathon", + "rubber", + "ding", + "trunks", + "desire", + "montreal", + "justme", + "faster", + "kathleen", + "irish", + "1999", + "bertha", + "jessica1", + "alpine", + "sammie", + "diamonds", + "tristan", + "00000", + "swinger", + "shan", + "stallion", + "pitbull", + "letmein2", + "roberto", + "ready", + "april", + "palmer", + "ming", + "shadow1", + "audrey", + "chong", + "clitoris", + "wang", + "shirley", + "fuckers", + "jackoff", + "bluesky", + "sundance", + "renegade", + "hollywoo", + "151515", + "bernard", + "wolfman", + "soldier", + "picture", + "pierre", + "ling", + "goddess", + "manager", + "nikita", + "sweety", + "titans", + "hang", + "fang", + "ficken", + "niners", + "bottom", + "bubble", + "hello123", + "ibanez", + "webster", + "sweetpea", + "stocking", + "323232", + "tornado", + "lindsey", + "content", + "bruce", + "buck", + "aragorn", + "griffin", + "chen", + "campbell", + "trojan", + "christop", + "newman", + "wayne", + "tina", + "rockstar", + "father", + "geronimo", + "pascal", + "crimson", + "brooks", + "hector", + "penny", + "anna", + "google", + "camera", + "chandler", + "fatcat", + "lovelove", + "cody", + "cunts", + "waters", + "stimpy", + "finger", + "cindy", + "wheels", + "viper1", + "latin", + "robin", + "greenday", + "987654321", + "creampie", + "brendan", + "hiphop", + "willy", + "snapper", + "funtime", + "duck", + "trombone", + "adult", + "cotton", + "cookies", + "kaiser", + "mulder", + "westham", + "latino", + "jeep", + "ravens", + "aurora", + "drizzt", + "madness", + "energy", + "kinky", + "314159", + "sophia", + "stefan", + "slick", + "rocker", + "55555555", + "freeman", + "french", + "mongoose", + "speed", + "dddddd", + "hong", + "henry", + "hungry", + "yang", + "catdog", + "cheng", + "ghost", + "gogogo", + "randy", + "tottenha", + "curious", + "butterfl", + "mission", + "january", + "singer", + "sherman", + "shark", + "techno", + "lancer", + "lalala", + "autumn", + "chichi", + "orion", + "trixie", + "clifford", + "delta", + "bobbob", + "bomber", + "holden", + "kang", + "kiss", + "1968", + "spunky", + "liquid", + "mary", + "beagle", + "granny", + "network", + "bond", + "kkkkkk", + "millie", + "1973", + "biggie", + "beetle", + "teacher", + "susan", + "toronto", + "anakin", + "genius", + "dream", + "cocks", + "dang", + "bush", + "karate", + "snakes", + "bangkok", + "callie", + "fuckyou2", + "pacific", + "daytona", + "kelsey", + "infantry", + "skywalke", + "foster", + "felix", + "sailing", + "raistlin", + "vanhalen", + "huang", + "herbert", + "jacob", + "blackie", + "tarzan", + "strider", + "sherlock", + "lang", + "gong", + "sang", + "dietcoke", + "ultimate", + "tree", + "shai", + "sprite", + "ting", + "artist", + "chai", + "chao", + "devil", + "python", + "ninja", + "misty", + "ytrewq", + "sweetie", + "superfly", + "456789", + "tian", + "jing", + "jesus1", + "freedom1", + "dian", + "drpepper", + "potter", + "chou", + "darren", + "hobbit", + "violet", + "yong", + "shen", + "phillip", + "maurice", + "gloria", + "nolimit", + "mylove", + "biscuit", + "yahoo", + "shasta", + "sex4me", + "smoker", + "smile", + "pebbles", + "pics", + "philly", + "tong", + "tintin", + "lesbians", + "marlin", + "cactus", + "frank1", + "tttttt", + "chun", + "danni", + "emerald", + "showme", + "pirates", + "lian", + "dogg", + "colleen", + "xiao", + "xian", + "tazman", + "tanker", + "patton", + "toshiba", + "richie", + "alberto", + "gotcha", + "graham", + "dillon", + "rang", + "emily", + "keng", + "jazz", + "bigguy", + "yuan", + "woman", + "tomtom", + "marion", + "greg", + "chaos", + "fossil", + "flight", + "racerx", + "tuan", + "creamy", + "boss", + "bobo", + "musicman", + "warcraft", + "window", + "blade", + "shuang", + "sheila", + "shun", + "lick", + "jian", + "microsoft", + "rong", + "allen", + "feng", + "getsome", + "sally", + "quality", + "kennedy", + "morrison", + "1977", + "beng", + "wwwwww", + "yoyoyo", + "zhang", + "seng", + "teddy", + "joanna", + "andreas", + "harder", + "luke", + "qazxsw", + "qian", + "cong", + "chuan", + "deng", + "nang", + "boeing", + "keeper", + "western", + "isabelle", + "1963", + "subaru", + "sheng", + "thuglife", + "teng", + "jiong", + "miao", + "martina", + "mang", + "maniac", + "pussie", + "tracey", + "a1b2c3", + "clayton", + "zhou", + "zhuang", + "xing", + "stonecol", + "snow", + "spyder", + "liang", + "jiang", + "memphis", + "regina", + "ceng", + "magic1", + "logitech", + "chuang", + "dark", + "million", + "blow", + "sesame", + "shao", + "poison", + "titty", + "terry", + "kuan", + "kuai", + "kyle", + "mian", + "guan", + "hamster", + "guai", + "ferret", + "florence", + "geng", + "duan", + "pang", + "maiden", + "quan", + "velvet", + "nong", + "neng", + "nookie", + "buttons", + "bian", + "bingo", + "biao", + "zhong", + "zeng", + "xiong", + "zhun", + "ying", + "zong", + "xuan", + "zang", + "0.0.000", + "suan", + "shei", + "shui", + "sharks", + "shang", + "shua", + "small", + "peng", + "pian", + "piao", + "liao", + "meng", + "miami", + "reng", + "guang", + "cang", + "change", + "ruan", + "diao", + "luan", + "lucas", + "qing", + "chui", + "chuo", + "cuan", + "nuan", + "ning", + "heng", + "huan", + "kansas", + "muscle", + "monroe", + "weng", + "whitney", + "1passwor", + "bluemoon", + "zhui", + "zhua", + "xiang", + "zheng", + "zhen", + "zhei", + "zhao", + "zhan", + "yomama", + "zhai", + "zhuo", + "zuan", + "tarheel", + "shou", + "shuo", + "tiao", + "lady", + "leonard", + "leng", + "kuang", + "jiao", + "13579", + "basket", + "qiao", + "qiong", + "qiang", + "chuai", + "nian", + "niao", + "niang", + "huai", + "22222222", + "bianca", + "zhuan", + "zhuai", + "shuan", + "shuai", + "stardust", + "jumper", + "margaret", + "archie", + "66666666", + "charlott", + "forget", + "qwertz", + "bones", + "history", + "milton", + "waterloo", + "2002", + "stuff", + "11223344", + "office", + "oldman", + "preston", + "trains", + "murray", + "vertigo", + "246810", + "black1", + "swallow", + "smiles", + "standard", + "alexandr", + "parrot", + "luther", + "user", + "nicolas", + "1976", + "surfing", + "pioneer", + "pete", + "masters", + "apple1", + "asdasd", + "auburn", + "hannibal", + "frontier", + "panama", + "lucy", + "buffy", + "brianna", + "welcome1", + "vette", + "blue22", + "shemale", + "111222", + "baggins", + "groovy", + "global", + "turner", + "181818", + "1979", + "blades", + "spanking", + "life", + "byteme", + "lobster", + "collins", + "dawg", + "hilton", + "japanese", + "1970", + "1964", + "2424", + "polo", + "markus", + "coco", + "deedee", + "mikey", + "1972", + "171717", + "1701", + "strip", + "jersey", + "green1", + "capital", + "sasha", + "sadie", + "putter", + "vader", + "seven7", + "lester", + "marcel", + "banshee", + "grendel", + "gilbert", + "dicks", + "dead", + "hidden", + "iloveu", + "1980", + "sound", + "ledzep", + "michel", + "147258", + "female", + "bugger", + "buffett", + "bryan", + "hell", + "kristina", + "molson", + "2020", + "wookie", + "sprint", + "thanks", + "jericho", + "102030", + "grace", + "fuckin", + "mandy", + "ranger1", + "trebor", + "deepthroat", + "bonehead", + "molly1", + "mirage", + "models", + "1984", + "2468", + "stuart", + "showtime", + "squirrel", + "pentium", + "mario", + "anime", + "gator", + "powder", + "twister", + "connect", + "neptune", + "bruno", + "butts", + "engine", + "eatshit", + "mustangs", + "woody1", + "shogun", + "septembe", + "pooh", + "jimbo", + "roger", + "annie", + "bacon", + "center", + "russian", + "sabine", + "damien", + "mollie", + "voyeur", + "2525", + "363636", + "leonardo", + "camel", + "chair", + "germany", + "giant", + "qqqq", + "nudist", + "bone", + "sleepy", + "tequila", + "megan", + "fighter", + "garrett", + "dominic", + "obiwan", + "makaveli", + "vacation", + "walnut", + "1974", + "ladybug", + "cantona", + "ccbill", + "satan", + "rusty1", + "passwor1", + "columbia", + "napoleon", + "dusty", + "kissme", + "motorola", + "william1", + "1967", + "zzzz", + "skater", + "smut", + "play", + "matthew1", + "robinson", + "valley", + "coolio", + "dagger", + "boner", + "bull", + "horndog", + "jason1", + "blake", + "penguins", + "rescue", + "griffey", + "8j4ye3uz", + "californ", + "champs", + "qwertyuiop", + "portland", + "queen", + "colt45", + "boat", + "xxxxxxx", + "xanadu", + "tacoma", + "mason", + "carpet", + "gggggg", + "safety", + "palace", + "italia", + "stevie", + "picturs", + "picasso", + "thongs", + "tempest", + "ricardo", + "roberts", + "asd123", + "hairy", + "foxtrot", + "gary", + "nimrod", + "hotboy", + "343434", + "1111111", + "asdfghjkl", + "goose", + "overlord", + "blood", + "wood", + "stranger", + "454545", + "shaolin", + "sooners", + "socrates", + "spiderman", + "peanuts", + "maxine", + "rogers", + "13131313", + "andrew1", + "filthy", + "donnie", + "ohyeah", + "africa", + "national", + "kenny", + "keith", + "monique", + "intrepid", + "jasmin", + "pickles", + "assass", + "fright", + "potato", + "darwin", + "hhhhhh", + "kingdom", + "weezer", + "424242", + "pepsi1", + "throat", + "romeo", + "gerard", + "looker", + "puppy", + "butch", + "monika", + "suzanne", + "sweets", + "temple", + "laurie", + "josh", + "megadeth", + "analsex", + "nymets", + "ddddddd", + "bigballs", + "support", + "stick", + "today", + "down", + "oakland", + "oooooo", + "qweasd", + "chucky", + "bridge", + "carrot", + "chargers", + "discover", + "dookie", + "condor", + "night", + "butler", + "hoover", + "horny1", + "isabella", + "sunrise", + "sinner", + "jojo", + "megapass", + "martini", + "assfuck", + "grateful", + "ffffff", + "abigail", + "esther", + "mushroom", + "janice", + "jamaica", + "wright", + "sims", + "space", + "there", + "timmy", + "7654321", + "77777", + "cccccc", + "gizmodo", + "roxanne", + "ralph", + "tractor", + "cristina", + "dance", + "mypass", + "hongkong", + "helena", + "1975", + "blue123", + "pissing", + "thomas1", + "redred", + "rich", + "basketball", + "attack", + "cash", + "satan666", + "drunk", + "dixie", + "dublin", + "bollox", + "kingkong", + "katrina", + "miles", + "1971", + "22222", + "272727", + "sexx", + "penelope", + "thompson", + "anything", + "bbbb", + "battle", + "grizzly", + "passat", + "porter", + "tracy", + "defiant", + "bowler", + "knickers", + "monitor", + "wisdom", + "wild", + "slappy", + "thor", + "letsgo", + "robert1", + "feet", + "rush", + "brownie", + "hudson", + "098765", + "playing", + "playtime", + "lightnin", + "melvin", + "atomic", + "bart", + "hawk", + "goku", + "glory", + "llllll", + "qwaszx", + "cosmos", + "bosco", + "knights", + "bentley", + "beast", + "slapshot", + "lewis", + "assword", + "frosty", + "gillian", + "sara", + "dumbass", + "mallard", + "dddd", + "deanna", + "elwood", + "wally", + "159357", + "titleist", + "angelo", + "aussie", + "guest", + "golfing", + "doobie", + "loveit", + "chloe", + "elliott", + "werewolf", + "vipers", + "janine", + "1965", + "blabla", + "surf", + "sucking", + "tardis", + "serena", + "shelley", + "thegame", + "legion", + "rebels", + "fernando", + "fast", + "gerald", + "sarah1", + "double", + "onelove", + "loulou", + "toto", + "crash", + "blackcat", + "0007", + "tacobell", + "soccer1", + "jedi", + "manuel", + "method", + "river", + "chase", + "ludwig", + "poopie", + "derrick", + "boob", + "breast", + "kittycat", + "isabel", + "belly", + "pikachu", + "thunder1", + "thankyou", + "jose", + "celeste", + "celtics", + "frances", + "frogger", + "scoobydo", + "sabbath", + "coltrane", + "budman", + "willis", + "jackal", + "bigger", + "zzzzz", + "silvia", + "sooner", + "licking", + "gopher", + "geheim", + "lonestar", + "primus", + "pooper", + "newpass", + "brasil", + "heather1", + "husker", + "element", + "moomoo", + "beefcake", + "zzzzzzzz", + "tammy", + "shitty", + "smokin", + "personal", + "jjjj", + "anthony1", + "anubis", + "backup", + "gorilla", + "fuckface", + "painter", + "lowrider", + "punkrock", + "traffic", + "claude", + "daniela", + "dale", + "delta1", + "nancy", + "boys", + "easy", + "kissing", + "kelley", + "wendy", + "theresa", + "amazon", + "alan", + "fatass", + "dodgeram", + "dingdong", + "malcolm", + "qqqqqqqq", + "breasts", + "boots", + "honda1", + "spidey", + "poker", + "temp", + "johnjohn", + "miguel", + "147852", + "archer", + "asshole1", + "dogdog", + "tricky", + "crusader", + "weather", + "syracuse", + "spankme", + "speaker", + "meridian", + "amadeus", + "back", + "harley1", + "falcons", + "dorothy", + "turkey50", + "kenwood", + "keyboard", + "ilovesex", + "1978", + "blackman", + "shazam", + "shalom", + "lickit", + "jimbob", + "richmond", + "roller", + "carson", + "check", + "fatman", + "funny", + "garbage", + "sandiego", + "loving", + "magnus", + "cooldude", + "clover", + "mobile", + "bell", + "payton", + "plumber", + "texas1", + "tool", + "topper", + "jenna", + "mariners", + "rebel", + "harmony", + "caliente", + "celica", + "fletcher", + "german", + "diana", + "oxford", + "osiris", + "orgasm", + "punkin", + "porsche9", + "tuesday", + "close", + "breeze", + "bossman", + "kangaroo", + "billie", + "latinas", + "judith", + "astros", + "scruffy", + "donna", + "qwertyu", + "davis", + "hearts", + "kathy", + "jammer", + "java", + "springer", + "rhonda", + "ricky", + "1122", + "goodtime", + "chelsea1", + "freckles", + "flyboy", + "doodle", + "city", + "nebraska", + "bootie", + "kicker", + "webmaster", + "vulcan", + "iverson", + "191919", + "blueeyes", + "stoner", + "321321", + "farside", + "rugby", + "director", + "pussy69", + "power1", + "bobbie", + "hershey", + "hermes", + "monopoly", + "west", + "birdman", + "blessed", + "blackjac", + "southern", + "peterpan", + "thumbs", + "lawyer", + "melinda", + "fingers", + "fuckyou1", + "rrrrrr", + "a1b2c3d4", + "coke", + "nicola", + "bohica", + "heart", + "elvis1", + "kids", + "blacky", + "stories", + "sentinel", + "snake1", + "phoebe", + "jesse", + "richard1", + "1234abcd", + "guardian", + "candyman", + "fisting", + "scarlet", + "dildo", + "pancho", + "mandingo", + "lucky7", + "condom", + "munchkin", + "billyboy", + "summer1", + "student", + "sword", + "skiing", + "sergio", + "site", + "sony", + "thong", + "rootbeer", + "assassin", + "cassidy", + "frederic", + "fffff", + "fitness", + "giovanni", + "scarlett", + "durango", + "postal", + "achilles", + "dawn", + "dylan", + "kisses", + "warriors", + "imagine", + "plymouth", + "topdog", + "asterix", + "hallo", + "cameltoe", + "fuckfuck", + "bridget", + "eeeeee", + "mouth", + "weird", + "will", + "sithlord", + "sommer", + "toby", + "theking", + "juliet", + "avenger", + "backdoor", + "goodbye", + "chevrole", + "faith", + "lorraine", + "trance", + "cosworth", + "brad", + "houses", + "homers", + "eternity", + "kingpin", + "verbatim", + "incubus", + "1961", + "blond", + "zaphod", + "shiloh", + "spurs", + "station", + "jennie", + "maynard", + "mighty", + "aliens", + "hank", + "charly", + "running", + "dogman", + "omega1", + "printer", + "aggies", + "chocolate", + "deadhead", + "hope", + "javier", + "bitch1", + "stone55", + "pineappl", + "thekid", + "lizzie", + "rockets", + "ashton", + "camels", + "formula", + "forrest", + "rosemary", + "oracle", + "rain", + "pussey", + "porkchop", + "abcde", + "clancy", + "nellie", + "mystic", + "inferno", + "blackdog", + "steve1", + "pauline", + "alexander", + "alice", + "alfa", + "grumpy", + "flames", + "scream", + "lonely", + "puffy", + "proxy", + "valhalla", + "unreal", + "cynthia", + "herbie", + "engage", + "yyyyyy", + "010101", + "solomon", + "pistol", + "melody", + "celeb", + "flying", + "gggg", + "santiago", + "scottie", + "oakley", + "portugal", + "a12345", + "newbie", + "mmmm", + "venus", + "1qazxsw2", + "beverly", + "zorro", + "work", + "writer", + "stripper", + "sebastia", + "spread", + "phil", + "tobias", + "links", + "members", + "metal", + "1221", + "andre", + "565656", + "funfun", + "trojans", + "again", + "cyber", + "hurrican", + "moneys", + "1x2zkg8w", + "zeus", + "thing", + "tomato", + "lion", + "atlantic", + "celine", + "usa123", + "trans", + "account", + "aaaaaaa", + "homerun", + "hyperion", + "kevin1", + "blacks", + "44444444", + "skittles", + "sean", + "hastings", + "fart", + "gangbang", + "fubar", + "sailboat", + "older", + "oilers", + "craig", + "conrad", + "church", + "damian", + "dean", + "broken", + "buster1", + "hithere", + "immortal", + "sticks", + "pilot", + "peters", + "lexmark", + "jerkoff", + "maryland", + "anders", + "cheers", + "possum", + "columbus", + "cutter", + "muppet", + "beautiful", + "stolen", + "swordfish", + "sport", + "sonic", + "peter1", + "jethro", + "rockon", + "asdfghj", + "pass123", + "paper", + "pornos", + "ncc1701a", + "bootys", + "buttman", + "bonjour", + "escape", + "1960", + "becky", + "bears", + "362436", + "spartans", + "tinman", + "threesom", + "lemons", + "maxmax", + "1414", + "bbbbb", + "camelot", + "chad", + "chewie", + "gogo", + "fusion", + "saint", + "dilligaf", + "nopass", + "myself", + "hustler", + "hunter1", + "whitey", + "beast1", + "yesyes", + "spank", + "smudge", + "pinkfloy", + "patriot", + "lespaul", + "annette", + "hammers", + "catalina", + "finish", + "formula1", + "sausage", + "scooter1", + "orioles", + "oscar1", + "over", + "colombia", + "cramps", + "natural", + "eating", + "exotic", + "iguana", + "bella", + "suckers", + "strong", + "sheena", + "start", + "slave", + "pearl", + "topcat", + "lancelot", + "angelica", + "magelan", + "racer", + "ramona", + "crunch", + "british", + "button", + "eileen", + "steph", + "456123", + "skinny", + "seeking", + "rockhard", + "chief", + "filter", + "first", + "freaks", + "sakura", + "pacman", + "poontang", + "dalton", + "newlife", + "homer1", + "klingon", + "watcher", + "walleye", + "tasha", + "tasty", + "sinatra", + "starship", + "steel", + "starbuck", + "poncho", + "amber1", + "gonzo", + "grover", + "catherin", + "carol", + "candle", + "firefly", + "goblin", + "scotch", + "diver", + "usmc", + "huskies", + "eleven", + "kentucky", + "kitkat", + "israel", + "beckham", + "bicycle", + "yourmom", + "studio", + "tara", + "33333333", + "shane", + "splash", + "jimmy1", + "reality", + "12344321", + "caitlin", + "focus", + "sapphire", + "mailman", + "raiders1", + "clark", + "ddddd", + "hopper", + "excalibu", + "more", + "wilbur", + "illini", + "imperial", + "phillips", + "lansing", + "maxx", + "gothic", + "golfball", + "carlton", + "camille", + "facial", + "front242", + "macdaddy", + "qwer1234", + "vectra", + "cowboys1", + "crazy1", + "dannyboy", + "jane", + "betty", + "benny", + "bennett", + "leader", + "martinez", + "aquarius", + "barkley", + "hayden", + "caught", + "franky", + "ffff", + "floyd", + "sassy", + "pppp", + "pppppppp", + "prodigy", + "clarence", + "noodle", + "eatpussy", + "vortex", + "wanking", + "beatrice", + "billy1", + "siemens", + "pedro", + "phillies", + "research", + "groups", + "carolyn", + "chevy1", + "cccc", + "fritz", + "gggggggg", + "doughboy", + "dracula", + "nurses", + "loco", + "madrid", + "lollipop", + "trout", + "utopia", + "chrono", + "cooler", + "conner", + "nevada", + "wibble", + "werner", + "summit", + "marco", + "marilyn", + "1225", + "babies", + "capone", + "fugazi", + "panda", + "mama", + "qazwsxed", + "puppies", + "triton", + "9876", + "command", + "nnnnnn", + "ernest", + "momoney", + "iforgot", + "wolfie", + "studly", + "shawn", + "renee", + "alien", + "hamburg", + "81fukkc", + "741852", + "catman", + "china", + "forgot", + "gagging", + "scott1", + "drew", + "oregon", + "qweqwe", + "train", + "crazybab", + "daniel1", + "cutlass", + "brothers", + "holes", + "heidi", + "mothers", + "music1", + "what", + "walrus", + "1957", + "bigtime", + "bike", + "xtreme", + "simba", + "ssss", + "rookie", + "angie", + "bathing", + "fresh", + "sanchez", + "rotten", + "maestro", + "luis", + "look", + "turbo1", + "99999", + "butthole", + "hhhh", + "elijah", + "monty", + "bender", + "yoda", + "shania", + "shock", + "phish", + "thecat", + "rightnow", + "reagan", + "baddog", + "asia", + "greatone", + "gateway1", + "randall", + "abstr", + "napster", + "brian1", + "bogart", + "high", + "hitler", + "emma", + "kill", + "weaver", + "wildfire", + "jackson1", + "isaiah", + "1981", + "belinda", + "beaner", + "yoyo", + "0.0.0.000", + "super1", + "select", + "snuggles", + "slutty", + "some", + "phoenix1", + "technics", + "toon", + "raven1", + "rayray", + "123789", + "1066", + "albion", + "greens", + "fashion", + "gesperrt", + "santana", + "paint", + "powell", + "credit", + "darling", + "mystery", + "bowser", + "bottle", + "brucelee", + "hehehe", + "kelly1", + "mojo", + "1998", + "bikini", + "woofwoof", + "yyyy", + "strap", + "sites", + "spears", + "theodore", + "julius", + "richards", + "amelia", + "central", + "f**k", + "nyjets", + "punisher", + "username", + "vanilla", + "twisted", + "bryant", + "brent", + "bunghole", + "here", + "elizabeth", + "erica", + "kimber", + "viagra", + "veritas", + "pony", + "pool", + "titts", + "labtec", + "lifetime", + "jenny1", + "masterbate", + "mayhem", + "redbull", + "govols", + "gremlin", + "505050", + "gmoney", + "rupert", + "rovers", + "diamond1", + "lorenzo", + "trident", + "abnormal", + "davidson", + "deskjet", + "cuddles", + "nice", + "bristol", + "karina", + "milano", + "vh5150", + "jarhead", + "1982", + "bigbird", + "bizkit", + "sixers", + "slider", + "star69", + "starfish", + "penetration", + "tommy1", + "john316", + "meghan", + "michaela", + "market", + "grant", + "caligula", + "carl", + "flicks", + "films", + "madden", + "railroad", + "cosmo", + "cthulhu", + "bradford", + "br0d3r", + "military", + "bearbear", + "swedish", + "spawn", + "patrick1", + "polly", + "these", + "todd", + "reds", + "anarchy", + "groove", + "franco", + "fuckher", + "oooo", + "tyrone", + "vegas", + "airbus", + "cobra1", + "christine", + "clips", + "delete", + "duster", + "kitty1", + "mouse1", + "monkeys", + "jazzman", + "1919", + "262626", + "swinging", + "stroke", + "stocks", + "sting", + "pippen", + "labrador", + "jordan1", + "justdoit", + "meatball", + "females", + "saturday", + "park", + "vector", + "cooter", + "defender", + "desert", + "demon", + "nike", + "bubbas", + "bonkers", + "english", + "kahuna", + "wildman", + "4121", + "sirius", + "static", + "piercing", + "terror", + "teenage", + "leelee", + "marissa", + "microsof", + "mechanic", + "robotech", + "rated", + "hailey", + "chaser", + "sanders", + "salsero", + "nuts", + "macross", + "quantum", + "rachael", + "tsunami", + "universe", + "daddy1", + "cruise", + "nguyen", + "newpass6", + "nudes", + "hellyeah", + "vernon", + "1959", + "zaq12wsx", + "striker", + "sixty", + "steele", + "spice", + "spectrum", + "smegma", + "thumb", + "jjjjjjjj", + "mellow", + "astrid", + "cancun", + "cartoon", + "sabres", + "samiam", + "pants", + "oranges", + "oklahoma", + "lust", + "coleman", + "denali", + "nude", + "noodles", + "buzz", + "brest", + "hooter", + "mmmmmmmm", + "warthog", + "bloody", + "blueblue", + "zappa", + "wolverine", + "sniffing", + "lance", + "jean", + "jjjjj", + "harper", + "calico", + "freee", + "rover", + "door", + "pooter", + "closeup", + "bonsai", + "evelyn", + "emily1", + "kathryn", + "keystone", + "iiii", + "1955", + "yzerman", + "theboss", + "tolkien", + "jill", + "megaman", + "rasta", + "bbbbbbbb", + "bean", + "handsome", + "hal9000", + "goofy", + "gringo", + "gofish", + "gizmo1", + "samsam", + "scuba", + "onlyme", + "tttttttt", + "corrado", + "clown", + "clapton", + "deborah", + "boris", + "bulls", + "vivian", + "jayhawk", + "bethany", + "wwww", + "sharky", + "seeker", + "ssssssss", + "somethin", + "pillow", + "thesims", + "lighter", + "lkjhgf", + "melissa1", + "marcius2", + "barry", + "guiness", + "gymnast", + "casey1", + "goalie", + "godsmack", + "doug", + "lolo", + "rangers1", + "poppy", + "abby", + "clemson", + "clipper", + "deeznuts", + "nobody", + "holly1", + "elliot", + "eeee", + "kingston", + "miriam", + "belle", + "yosemite", + "sucked", + "sex123", + "sexy69", + "pic's", + "tommyboy", + "lamont", + "meat", + "masterbating", + "marianne", + "marc", + "gretzky", + "happyday", + "frisco", + "scratch", + "orchid", + "orange1", + "manchest", + "quincy", + "unbelievable", + "aberdeen", + "dawson", + "nathalie", + "ne1469", + "boxing", + "hill", + "korn", + "intercourse", + "161616", + "1985", + "ziggy", + "supersta", + "stoney", + "senior", + "amature", + "barber", + "babyboy", + "bcfields", + "goliath", + "hack", + "hardrock", + "children", + "frodo", + "scout", + "scrappy", + "rosie", + "qazqaz", + "tracker", + "active", + "craving", + "commando", + "cohiba", + "deep", + "cyclone", + "dana", + "bubba69", + "katie1", + "mpegs", + "vsegda", + "jade", + "irish1", + "better", + "sexy1", + "sinclair", + "smelly", + "squerting", + "lions", + "jokers", + "jeanette", + "julia", + "jojojo", + "meathead", + "ashley1", + "groucho", + "cheetah", + "champ", + "firefox", + "gandalf1", + "packer", + "magnolia", + "love69", + "tyler1", + "typhoon", + "tundra", + "bobby1", + "kenworth", + "village", + "volley", + "beth", + "wolf359", + "0420", + "000007", + "swimmer", + "skydive", + "smokes", + "patty", + "peugeot", + "pompey", + "legolas", + "kristy", + "redhot", + "rodman", + "redalert", + "having", + "grapes", + "4runner", + "carrera", + "floppy", + "dollars", + "ou8122", + "quattro", + "adams", + "cloud9", + "davids", + "nofear", + "busty", + "homemade", + "mmmmm", + "whisper", + "vermont", + "webmaste", + "wives", + "insertion", + "jayjay", + "philips", + "phone", + "topher", + "tongue", + "temptress", + "midget", + "ripken", + "havefun", + "gretchen", + "canon", + "celebrity", + "five", + "getting", + "ghetto", + "direct", + "otto", + "ragnarok", + "trinidad", + "usnavy", + "conover", + "cruiser", + "dalshe", + "nicole1", + "buzzard", + "hottest", + "kingfish", + "misfit", + "moore", + "milfnew", + "warlord", + "wassup", + "bigsexy", + "blackhaw", + "zippy", + "shearer", + "tights", + "thursday", + "kungfu", + "labia", + "journey", + "meatloaf", + "marlene", + "rider", + "area51", + "batman1", + "bananas", + "636363", + "cancel", + "ggggg", + "paradox", + "mack", + "lynn", + "queens", + "adults", + "aikido", + "cigars", + "nova", + "hoosier", + "eeyore", + "moose1", + "warez", + "interacial", + "streaming", + "313131", + "pertinant", + "pool6123", + "mayday", + "rivers", + "revenge", + "animated", + "banker", + "baddest", + "gordon24", + "ccccc", + "fortune", + "fantasies", + "touching", + "aisan", + "deadman", + "homepage", + "ejaculation", + "whocares", + "iscool", + "jamesbon", + "1956", + "1pussy", + "womam", + "sweden", + "skidoo", + "spock", + "sssss", + "petra", + "pepper1", + "pinhead", + "micron", + "allsop", + "amsterda", + "army", + "aside", + "gunnar", + "666999", + "chip", + "foot", + "fowler", + "february", + "face", + "fletch", + "george1", + "sapper", + "science", + "sasha1", + "luckydog", + "lover1", + "magick", + "popopo", + "public", + "ultima", + "derek", + "cypress", + "booker", + "businessbabe", + "brandon1", + "edwards", + "experience", + "vulva", + "vvvv", + "jabroni", + "bigbear", + "yummy", + "010203", + "searay", + "secret1", + "showing", + "sinbad", + "sexxxx", + "soleil", + "software", + "piccolo", + "thirteen", + "leopard", + "legacy", + "jensen", + "justine", + "memorex", + "marisa", + "mathew", + "redwing", + "rasputin", + "134679", + "anfield", + "greenbay", + "gore", + "catcat", + "feather", + "scanner", + "pa55word", + "contortionist", + "danzig", + "daisy1", + "hores", + "erik", + "exodus", + "vinnie", + "iiiiii", + "zero", + "1001", + "subway", + "tank", + "second", + "snapple", + "sneakers", + "sonyfuck", + "picks", + "poodle", + "test1234", + "their", + "llll", + "junebug", + "june", + "marker", + "mellon", + "ronaldo", + "roadkill", + "amanda1", + "asdfjkl", + "beaches", + "greene", + "great1", + "cheerleaers", + "force", + "doitnow", + "ozzy", + "madeline", + "radio", + "tyson", + "christian", + "daphne", + "boxster", + "brighton", + "housewifes", + "emmanuel", + "emerson", + "kkkk", + "mnbvcx", + "moocow", + "vides", + "wagner", + "janet", + "1717", + "bigmoney", + "blonds", + "1000", + "storys", + "stereo", + "4545", + "420247", + "seductive", + "sexygirl", + "lesbean", + "live", + "justin1", + "124578", + "animals", + "balance", + "hansen", + "cabbage", + "canadian", + "gangbanged", + "dodge1", + "dimas", + "lori", + "loud", + "malaka", + "puss", + "probes", + "adriana", + "coolman", + "crawford", + "dante", + "nacked", + "hotpussy", + "erotica", + "kool", + "mirror", + "wearing", + "implants", + "intruder", + "bigass", + "zenith", + "woohoo", + "womans", + "tanya", + "tango", + "stacy", + "pisces", + "laguna", + "krystal", + "maxell", + "andyod22", + "barcelon", + "chainsaw", + "chickens", + "flash1", + "downtown", + "orgasms", + "magicman", + "profit", + "pusyy", + "pothead", + "coconut", + "chuckie", + "contact", + "clevelan", + "designer", + "builder", + "budweise", + "hotshot", + "horizon", + "hole", + "experienced", + "mondeo", + "wifes", + "1962", + "strange", + "stumpy", + "smiths", + "sparks", + "slacker", + "piper", + "pitchers", + "passwords", + "laptop", + "jeremiah", + "allmine", + "alliance", + "bbbbbbb", + "asscock", + "halflife", + "grandma", + "hayley", + "88888", + "cecilia", + "chacha", + "saratoga", + "sandy1", + "santos", + "doogie", + "number", + "positive", + "qwert40", + "transexual", + "crow", + "close-up", + "darrell", + "bonita", + "ib6ub9", + "volvo", + "jacob1", + "iiiii", + "beastie", + "sunnyday", + "stoned", + "sonics", + "starfire", + "snapon", + "pictuers", + "pepe", + "testing1", + "tiberius", + "lisalisa", + "lesbain", + "litle", + "retard", + "ripple", + "austin1", + "badgirl", + "golfgolf", + "flounder", + "garage", + "royals", + "dragoon", + "dickie", + "passwor", + "ocean", + "majestic", + "poppop", + "trailers", + "dammit", + "nokia", + "bobobo", + "br549", + "emmitt", + "knock", + "minime", + "mikemike", + "whitesox", + "1954", + "3232", + "353535", + "seamus", + "solo", + "sparkle", + "sluttey", + "pictere", + "titten", + "lback", + "1024", + "angelina", + "goodluck", + "charlton", + "fingerig", + "gallaries", + "goat", + "ruby", + "passme", + "oasis", + "lockerroom", + "logan1", + "rainman", + "twins", + "treasure", + "absolutely", + "club", + "custom", + "cyclops", + "nipper", + "bucket", + "homepage-", + "hhhhh", + "momsuck", + "indain", + "2345", + "beerbeer", + "bimmer", + "susanne", + "stunner", + "stevens", + "456456", + "shell", + "sheba", + "tootsie", + "tiny", + "testerer", + "reefer", + "really", + "1012", + "harcore", + "gollum", + "545454", + "chico", + "caveman", + "carole", + "fordf150", + "fishes", + "gaymen", + "saleen", + "doodoo", + "pa55w0rd", + "looney", + "presto", + "qqqqq", + "cigar", + "bogey", + "brewer", + "helloo", + "dutch", + "kamikaze", + "monte", + "wasser", + "vietnam", + "visa", + "japanees", + "0123", + "swords", + "slapper", + "peach", + "jump", + "marvel", + "masterbaiting", + "march", + "redwood", + "rolling", + "1005", + "ametuer", + "chiks", + "cathy", + "callaway", + "fucing", + "sadie1", + "panasoni", + "mamas", + "race", + "rambo", + "unknown", + "absolut", + "deacon", + "dallas1", + "housewife", + "kristi", + "keywest", + "kirsten", + "kipper", + "morning", + "wings", + "idiot", + "18436572", + "1515", + "beating", + "zxczxc", + "sullivan", + "303030", + "shaman", + "sparrow", + "terrapin", + "jeffery", + "masturbation", + "mick", + "redfish", + "1492", + "angus", + "barrett", + "goirish", + "hardcock", + "felicia", + "forfun", + "galary", + "freeporn", + "duchess", + "olivier", + "lotus", + "pornographic", + "ramses", + "purdue", + "traveler", + "crave", + "brando", + "enter1", + "killme", + "moneyman", + "welder", + "windsor", + "wifey", + "indon", + "yyyyy", + "stretch", + "taylor1", + "4417", + "shopping", + "picher", + "pickup", + "thumbnils", + "johnboy", + "jets", + "jess", + "maureen", + "anne", + "ameteur", + "amateurs", + "apollo13", + "hambone", + "goldwing", + "5050", + "charley", + "sally1", + "doghouse", + "padres", + "pounding", + "quest", + "truelove", + "underdog", + "trader", + "crack", + "climber", + "bolitas", + "bravo", + "hohoho", + "model", + "italian", + "beanie", + "beretta", + "wrestlin", + "stroker", + "tabitha", + "sherwood", + "sexyman", + "jewels", + "johannes", + "mets", + "marcos", + "rhino", + "bdsm", + "balloons", + "goodman", + "grils", + "happy123", + "flamingo", + "games", + "route66", + "devo", + "dino", + "outkast", + "paintbal", + "magpie", + "llllllll", + "twilight", + "critter", + "christie", + "cupcake", + "nickel", + "bullseye", + "krista", + "knickerless", + "mimi", + "murder", + "videoes", + "binladen", + "xerxes", + "slim", + "slinky", + "pinky", + "peterson", + "thanatos", + "meister", + "menace", + "ripley", + "retired", + "albatros", + "balloon", + "bank", + "goten", + "5551212", + "getsdown", + "donuts", + "divorce", + "nwo4life", + "lord", + "lost", + "underwear", + "tttt", + "comet", + "deer", + "damnit", + "dddddddd", + "deeznutz", + "nasty1", + "nonono", + "nina", + "enterprise", + "eeeee", + "misfit99", + "milkman", + "vvvvvv", + "isaac", + "1818", + "blueboy", + "beans", + "bigbutt", + "wyatt", + "tech", + "solution", + "poetry", + "toolman", + "laurel", + "juggalo", + "jetski", + "meredith", + "barefoot", + "50spanks", + "gobears", + "scandinavian", + "original", + "truman", + "cubbies", + "nitram", + "briana", + "ebony", + "kings", + "warner", + "bilbo", + "yumyum", + "zzzzzzz", + "stylus", + "321654", + "shannon1", + "server", + "secure", + "silly", + "squash", + "starman", + "steeler", + "staples", + "phrases", + "techniques", + "laser", + "135790", + "allan", + "barker", + "athens", + "cbr600", + "chemical", + "fester", + "gangsta", + "fucku2", + "freeze", + "game", + "salvador", + "droopy", + "objects", + "passwd", + "lllll", + "loaded", + "louis", + "manchester", + "losers", + "vedder", + "clit", + "chunky", + "darkman", + "damage", + "buckshot", + "buddah", + "boobed", + "henti", + "hillary", + "webber", + "winter1", + "ingrid", + "bigmike", + "beta", + "zidane", + "talon", + "slave1", + "pissoff", + "person", + "thegreat", + "living", + "lexus", + "matador", + "readers", + "riley", + "roberta", + "armani", + "ashlee", + "goldstar", + "5656", + "cards", + "fmale", + "ferris", + "fuking", + "gaston", + "fucku", + "ggggggg", + "sauron", + "diggler", + "pacers", + "looser", + "pounded", + "premier", + "pulled", + "town", + "trisha", + "triangle", + "cornell", + "collin", + "cosmic", + "deeper", + "depeche", + "norway", + "bright", + "helmet", + "kristine", + "kendall", + "mustard", + "misty1", + "watch", + "jagger", + "bertie", + "berger", + "word", + "3x7pxr", + "silver1", + "smoking", + "snowboar", + "sonny", + "paula", + "penetrating", + "photoes", + "lesbens", + "lambert", + "lindros", + "lillian", + "roadking", + "rockford", + "1357", + "143143", + "asasas", + "goodboy", + "898989", + "chicago1", + "card", + "ferrari1", + "galeries", + "godfathe", + "gawker", + "gargoyle", + "gangster", + "rubble", + "rrrr", + "onetime", + "pussyman", + "pooppoop", + "trapper", + "twenty", + "abraham", + "cinder", + "company", + "newcastl", + "boricua", + "bunny1", + "boxer", + "hotred", + "hockey1", + "hooper", + "edward1", + "evan", + "kris", + "misery", + "moscow", + "milk", + "mortgage", + "bigtit", + "show", + "snoopdog", + "three", + "lionel", + "leanne", + "joshua1", + "july", + "1230", + "assholes", + "cedric", + "fallen", + "farley", + "gene", + "frisky", + "sanity", + "script", + "divine", + "dharma", + "lucky13", + "property", + "tricia", + "akira", + "desiree", + "broadway", + "butterfly", + "hunt", + "hotbox", + "hootie", + "heat", + "howdy", + "earthlink", + "karma", + "kiteboy", + "motley", + "westwood", + "1988", + "bert", + "blackbir", + "biggles", + "wrench", + "working", + "wrestle", + "slippery", + "pheonix", + "penny1", + "pianoman", + "tomorrow", + "thedude", + "jenn", + "jonjon", + "jones1", + "mattie", + "memory", + "micheal", + "roadrunn", + "arrow", + "attitude", + "azzer", + "seahawks", + "diehard", + "dotcom", + "lola", + "tunafish", + "chivas", + "cinnamon", + "clouds", + "deluxe", + "northern", + "nuclear", + "north", + "boom", + "boobie", + "hurley", + "krishna", + "momomo", + "modles", + "volume", + "23232323", + "bluedog", + "wwwwwww", + "zerocool", + "yousuck", + "pluto", + "limewire", + "link", + "joung", + "marcia", + "awnyce", + "gonavy", + "haha", + "films+pic+galeries", + "fabian", + "francois", + "girsl", + "fuckthis", + "girfriend", + "rufus", + "drive", + "uncencored", + "a123456", + "airport", + "clay", + "chrisbln", + "combat", + "cygnus", + "cupoi", + "never", + "netscape", + "brett", + "hhhhhhhh", + "eagles1", + "elite", + "knockers", + "kendra", + "mommy", + "1958", + "tazmania", + "shonuf", + "piano", + "pharmacy", + "thedog", + "lips", + "jillian", + "jenkins", + "midway", + "arsenal1", + "anaconda", + "australi", + "gromit", + "gotohell", + "787878", + "66666", + "carmex2", + "camber", + "gator1", + "ginger1", + "fuzzy", + "seadoo", + "dorian", + "lovesex", + "rancid", + "uuuuuu", + "911911", + "nature", + "bulldog1", + "helen", + "health", + "heater", + "higgins", + "kirk", + "monalisa", + "mmmmmmm", + "whiteout", + "virtual", + "ventura", + "jamie1", + "japanes", + "james007", + "2727", + "2469", + "blam", + "bitchass", + "believe", + "zephyr", + "stiffy", + "sweet1", + "silent", + "southpar", + "spectre", + "tigger1", + "tekken", + "lenny", + "lakota", + "lionking", + "jjjjjjj", + "medical", + "megatron", + "1369", + "hawaiian", + "gymnastic", + "golfer1", + "gunners", + "7779311", + "515151", + "famous", + "glass", + "screen", + "rudy", + "royal", + "sanfran", + "drake", + "optimus", + "panther1", + "love1", + "mail", + "maggie1", + "pudding", + "venice", + "aaron1", + "delphi", + "niceass", + "bounce", + "busted", + "house1", + "killer1", + "miracle", + "momo", + "musashi", + "jammin", + "2003", + "234567", + "wp2003wp", + "submit", + "silence", + "sssssss", + "state", + "spikes", + "sleeper", + "passwort", + "toledo", + "kume", + "media", + "meme", + "medusa", + "mantis", + "remote", + "reading", + "reebok", + "1017", + "artemis", + "hampton", + "harry1", + "cafc91", + "fettish", + "friendly", + "oceans", + "oooooooo", + "mango", + "ppppp", + "trainer", + "troy", + "uuuu", + "909090", + "cross", + "death1", + "news", + "bullfrog", + "hokies", + "holyshit", + "eeeeeee", + "mitch", + "jasmine1", + "&", + "&", + "sergeant", + "spinner", + "leon", + "jockey", + "records", + "right", + "babyblue", + "hans", + "gooner", + "474747", + "cheeks", + "cars", + "candice", + "fight", + "glow", + "pass1234", + "parola", + "okokok", + "pablo", + "magical", + "major", + "ramsey", + "poseidon", + "989898", + "confused", + "circle", + "crusher", + "cubswin", + "nnnn", + "hollywood", + "erin", + "kotaku", + "milo", + "mittens", + "whatsup", + "vvvvv", + "iomega", + "insertions", + "bengals", + "bermuda", + "biit", + "yellow1", + "012345", + "spike1", + "south", + "sowhat", + "pitures", + "peacock", + "pecker", + "theend", + "juliette", + "jimmie", + "romance", + "augusta", + "hayabusa", + "hawkeyes", + "castro", + "florian", + "geoffrey", + "dolly", + "lulu", + "qaz123", + "usarmy", + "twinkle", + "cloud", + "chuckles", + "cold", + "hounddog", + "hover", + "hothot", + "europa", + "ernie", + "kenshin", + "kojak", + "mikey1", + "water1", + "196969", + "because", + "wraith", + "zebra", + "wwwww", + "33333", + "simon1", + "spider1", + "snuffy", + "philippe", + "thunderb", + "teddy1", + "lesley", + "marino13", + "maria1", + "redline", + "renault", + "aloha", + "antoine", + "handyman", + "cerberus", + "gamecock", + "gobucks", + "freesex", + "duffman", + "ooooo", + "papa", + "nuggets", + "magician", + "longbow", + "preacher", + "porno1", + "county", + "chrysler", + "contains", + "dalejr", + "darius", + "darlene", + "dell", + "navy", + "buffy1", + "hedgehog", + "hoosiers", + "honey1", + "hott", + "heyhey", + "europe", + "dutchess", + "everest", + "wareagle", + "ihateyou", + "sunflowe", + "3434", + "senators", + "shag", + "spoon", + "sonoma", + "stalker", + "poochie", + "terminal", + "terefon", + "laurence", + "maradona", + "maryann", + "marty", + "roman", + "1007", + "142536", + "alibaba", + "america1", + "bartman", + "astro", + "goth", + "century", + "chicken1", + "cheater", + "four", + "ghost1", + "passpass", + "oral", + "r2d2c3po", + "civic", + "cicero", + "myxworld", + "kkkkk", + "missouri", + "wishbone", + "infiniti", + "jameson", + "1a2b3c", + "1qwerty", + "wonderboy", + "skip", + "shojou", + "stanford", + "sparky1", + "smeghead", + "poiuy", + "titanium", + "torres", + "lantern", + "jelly", + "jeanne", + "meier", + "1213", + "bayern", + "basset", + "gsxr750", + "cattle", + "charlene", + "fishing1", + "fullmoon", + "gilles", + "dima", + "obelix", + "popo", + "prissy", + "ramrod", + "unique", + "absolute", + "bummer", + "hotone", + "dynasty", + "entry", + "konyor", + "missy1", + "moses", + "282828", + "yeah", + "xyz123", + "stop", + "426hemi", + "404040", + "seinfeld", + "simmons", + "pingpong", + "lazarus", + "matthews", + "marine1", + "manning", + "recovery", + "12345a", + "beamer", + "babyface", + "greece", + "gustav", + "7007", + "charity", + "camilla", + "ccccccc", + "faggot", + "foxy", + "frozen", + "gladiato", + "duckie", + "dogfood", + "paranoid", + "packers1", + "longjohn", + "radical", + "tuna", + "clarinet", + "claudio", + "circus", + "danny1", + "novell", + "nights", + "bonbon", + "kashmir", + "kiki", + "mortimer", + "modelsne", + "moondog", + "monaco", + "vladimir", + "insert", + "1953", + "zxc123", + "supreme", + "3131", + "sexxx", + "selena", + "softail", + "poipoi", + "pong", + "together", + "mars", + "martin1", + "rogue", + "alone", + "avalanch", + "audia4", + "55bgates", + "cccccccc", + "chick", + "came11", + "figaro", + "geneva", + "dogboy", + "dnsadm", + "dipshit", + "paradigm", + "othello", + "operator", + "officer", + "malone", + "post", + "rafael", + "valencia", + "tripod", + "choice", + "chopin", + "coucou", + "coach", + "cocksuck", + "common", + "creature", + "borussia", + "book", + "browning", + "heritage", + "hiziad", + "homerj", + "eight", + "earth", + "millions", + "mullet", + "whisky", + "jacques", + "store", + "4242", + "speedo", + "starcraf", + "skylar", + "spaceman", + "piggy", + "pierce", + "tiger2", + "legos", + "lala", + "jezebel", + "judy", + "joker1", + "mazda", + "barton", + "baker", + "727272", + "chester1", + "fishman", + "food", + "rrrrrrrr", + "sandwich", + "dundee", + "lumber", + "magazine", + "radar", + "ppppppp", + "tranny", + "aaliyah", + "admiral", + "comics", + "cleo", + "delight", + "buttfuck", + "homeboy", + "eternal", + "kilroy", + "kellie", + "khan", + "violin", + "wingman", + "walmart", + "bigblue", + "blaze", + "beemer", + "beowulf", + "bigfish", + "yyyyyyy", + "woodie", + "yeahbaby", + "0123456", + "tbone", + "style", + "syzygy", + "starter", + "lemon", + "linda1", + "merlot", + "mexican", + "11235813", + "anita", + "banner", + "bangbang", + "badman", + "barfly", + "grease", + "carla", + "charles1", + "ffffffff", + "screw", + "doberman", + "diane", + "dogshit", + "overkill", + "counter", + "coolguy", + "claymore", + "demons", + "demo", + "nomore", + "normal", + "brewster", + "hhhhhhh", + "hondas", + "iamgod", + "enterme", + "everett", + "electron", + "eastside", + "kayla", + "minimoni", + "mybaby", + "wildbill", + "wildcard", + "ipswich", + "200000", + "bearcat", + "zigzag", + "yyyyyyyy", + "xander", + "sweetnes", + "369369", + "skyler", + "skywalker", + "pigeon", + "peyton", + "tipper", + "lilly", + "asdf123", + "alphabet", + "asdzxc", + "babybaby", + "banane", + "barnes", + "guyver", + "graphics", + "grand", + "chinook", + "florida1", + "flexible", + "fuckinside", + "otis", + "ursitesux", + "tototo", + "trust", + "tower", + "adam12", + "christma", + "corey", + "chrome", + "buddie", + "bombers", + "bunker", + "hippie", + "keegan", + "misfits", + "vickie", + "292929", + "woofer", + "wwwwwwww", + "stubby", + "sheep", + "secrets", + "sparta", + "stang", + "spud", + "sporty", + "pinball", + "jorge", + "just4fun", + "johanna", + "maxxxx", + "rebecca1", + "gunther", + "fatima", + "fffffff", + "freeway", + "garion", + "score", + "rrrrr", + "sancho", + "outback", + "maggot", + "puddin", + "trial", + "adrienne", + "987456", + "colton", + "clyde", + "brain", + "brains", + "hoops", + "eleanor", + "dwayne", + "kirby", + "mydick", + "villa", + "19691969", + "bigcat", + "becker", + "shiner", + "silverad", + "spanish", + "templar", + "lamer", + "juicy", + "marsha", + "mike1", + "maximum", + "rhiannon", + "real", + "1223", + "10101010", + "arrows", + "andres", + "alucard", + "baldwin", + "baron", + "avenue", + "ashleigh", + "haggis", + "channel", + "cheech", + "safari", + "ross", + "dog123", + "orion1", + "paloma", + "qwerasdf", + "presiden", + "vegitto", + "trees", + "969696", + "adonis", + "colonel", + "cookie1", + "newyork1", + "brigitte", + "buddyboy", + "hellos", + "heineken", + "dwight", + "eraser", + "kerstin", + "motion", + "moritz", + "millwall", + "visual", + "jaybird", + "1983", + "beautifu", + "bitter", + "yvette", + "zodiac", + "steven1", + "sinister", + "slammer", + "smashing", + "slick1", + "sponge", + "teddybea", + "theater", + "this", + "ticklish", + "lipstick", + "jonny", + "massage", + "mann", + "reynolds", + "ring", + "1211", + "amazing", + "aptiva", + "applepie", + "bailey1", + "guitar1", + "chanel", + "canyon", + "gagged", + "fuckme1", + "rough", + "digital1", + "dinosaur", + "punk", + "98765", + "90210", + "clowns", + "cubs", + "daniels", + "deejay", + "nigga", + "naruto", + "boxcar", + "icehouse", + "hotties", + "electra", + "kent", + "widget", + "india", + "insanity", + "1986", + "2004", + "best", + "bluefish", + "bingo1", + "*****", + "stratus", + "strength", + "sultan", + "storm1", + "44444", + "4200", + "sentnece", + "season", + "sexyboy", + "sigma", + "smokie", + "spam", + "point", + "pippo", + "ticket", + "temppass", + "joel", + "manman", + "medicine", + "1022", + "anton", + "almond", + "bacchus", + "aztnm", + "axio", + "awful", + "bamboo", + "hakr", + "gregor", + "hahahaha", + "5678", + "casanova", + "caprice", + "camero1", + "fellow", + "fountain", + "dupont", + "dolphin1", + "dianne", + "paddle", + "magnet", + "qwert1", + "pyon", + "porsche1", + "tripper", + "vampires", + "coming", + "noway", + "burrito", + "bozo", + "highheel", + "hughes", + "hookem", + "eddie1", + "ellie", + "entropy", + "kkkkkkkk", + "kkkkkkk", + "illinois", + "jacobs", + "1945", + "1951", + "24680", + "21212121", + "100000", + "stonecold", + "taco", + "subzero", + "sharp", + "sexxxy", + "skolko", + "shanna", + "skyhawk", + "spurs1", + "sputnik", + "piazza", + "testpass", + "letter", + "lane", + "kurt", + "jiggaman", + "matilda", + "1224", + "harvard", + "hannah1", + "525252", + "4ever", + "carbon", + "chef", + "federico", + "ghosts", + "gina", + "scorpio1", + "rt6ytere", + "madison1", + "loki", + "raquel", + "promise", + "coolness", + "christina", + "coldbeer", + "citadel", + "brittney", + "highway", + "evil", + "monarch", + "morgan1", + "washingt", + "1997", + "bella1", + "berry", + "yaya", + "yolanda", + "superb", + "taxman", + "studman", + "stephanie", + "3636", + "sherri", + "sheriff", + "shepherd", + "poland", + "pizzas", + "tiffany1", + "toilet", + "latina", + "lassie", + "larry1", + "joseph1", + "mephisto", + "meagan", + "marian", + "reptile", + "rico", + "razor", + "1013", + "barron", + "hammer1", + "gypsy", + "grande", + "carroll", + "camper", + "chippy", + "cat123", + "call", + "chimera", + "fiesta", + "glock", + "glenn", + "domain", + "dieter", + "dragonba", + "onetwo", + "nygiants", + "odessa", + "password2", + "louie", + "quartz", + "prowler", + "prophet", + "towers", + "ultra", + "cocker", + "corleone", + "dakota1", + "cumm", + "nnnnnnn", + "natalia", + "boxers", + "hugo", + "heynow", + "hollow", + "iceberg", + "elvira", + "kittykat", + "kate", + "kitchen", + "wasabi", + "vikings1", + "impact", + "beerman", + "string", + "sleep", + "splinter", + "snoopy1", + "pipeline", + "pocket", + "legs", + "maple", + "mickey1", + "manuela", + "mermaid", + "micro", + "meowmeow", + "redbird", + "alisha", + "baura", + "battery", + "grass", + "chevys", + "chestnut", + "caravan", + "carina", + "charmed", + "fraser", + "frogman", + "diving", + "dogger", + "draven", + "drifter", + "oatmeal", + "paris1", + "longdong", + "quant4307s", + "rachel1", + "vegitta", + "cole", + "cobras", + "corsair", + "dadada", + "noelle", + "mylife", + "nine", + "bowwow", + "body", + "hotrats", + "eastwood", + "moonligh", + "modena", + "wave", + "illusion", + "iiiiiii", + "jayhawks", + "birgit", + "zone", + "sutton", + "susana", + "swingers", + "shocker", + "shrimp", + "sexgod", + "squall", + "stefanie", + "squeeze", + "soul", + "patrice", + "poiu", + "players", + "tigers1", + "toejam", + "tickler", + "line", + "julie1", + "jimbo1", + "jefferso", + "juanita", + "michael2", + "rodeo", + "robot", + "1023", + "annie1", + "bball", + "guess", + "happy2", + "charter", + "farm", + "flasher", + "falcon1", + "fiction", + "fastball", + "gadget", + "scrabble", + "diaper", + "dirtbike", + "dinner", + "oliver1", + "partner", + "paco", + "lucille", + "macman", + "poopy", + "popper", + "postman", + "ttttttt", + "ursula", + "acura", + "cowboy1", + "conan", + "daewoo", + "cyrus", + "customer", + "nation", + "nemrac58", + "nnnnn", + "nextel", + "bolton", + "bobdylan", + "hopeless", + "eureka", + "extra", + "kimmie", + "kcj9wx5n", + "killbill", + "musica", + "volkswag", + "wage", + "windmill", + "wert", + "vintage", + "iloveyou1", + "itsme", + "bessie", + "zippo", + "311311", + "starligh", + "smokey1", + "spot", + "snappy", + "soulmate", + "plasma", + "thelma", + "tonight", + "krusty", + "just4me", + "mcdonald", + "marius", + "rochelle", + "rebel1", + "1123", + "alfredo", + "aubrey", + "audi", + "chantal", + "fick", + "goaway", + "roses", + "sales", + "rusty2", + "dirt", + "dogbone", + "doofus", + "ooooooo", + "oblivion", + "mankind", + "luck", + "mahler", + "lllllll", + "pumper", + "puck", + "pulsar", + "valkyrie", + "tupac", + "compass", + "concorde", + "costello", + "cougars", + "delaware", + "niceguy", + "nocturne", + "bob123", + "boating", + "bronze", + "hopkins", + "herewego", + "hewlett", + "houhou", + "hubert", + "earnhard", + "eeeeeeee", + "keller", + "mingus", + "mobydick", + "venture", + "verizon", + "imation", + "1950", + "1948", + "1949", + "223344", + "bigbig", + "blossom", + "zack", + "wowwow", + "sissy", + "skinner", + "spiker", + "square", + "snooker", + "sluggo", + "player1", + "junk", + "jeannie", + "jsbach", + "jumbo", + "jewel", + "medic", + "robins", + "reddevil", + "reckless", + "123456a", + "1125", + "1031", + "beacon", + "astra", + "gumby", + "hammond", + "hassan", + "757575", + "585858", + "chillin", + "fuck1", + "sander", + "lowell", + "radiohea", + "upyours", + "trek", + "courage", + "coolcool", + "classics", + "choochoo", + "darryl", + "nikki1", + "nitro", + "bugs", + "boytoy", + "ellen", + "excite", + "kirsty", + "kane", + "wingnut", + "wireless", + "icu812", + "1master", + "beatle", + "bigblock", + "blanca", + "wolfen", + "summer99", + "sugar1", + "tartar", + "sexysexy", + "senna", + "sexman", + "sick", + "someone", + "soprano", + "pippin", + "platypus", + "pixies", + "telephon", + "land", + "laura1", + "laurent", + "rimmer", + "road", + "report", + "1020", + "12qwaszx", + "arturo", + "around", + "hamish", + "halifax", + "fishhead", + "forum", + "dododo", + "doit", + "outside", + "paramedi", + "lonesome", + "mandy1", + "twist", + "uuuuu", + "uranus", + "ttttt", + "butcher", + "bruce1", + "helper", + "hopeful", + "eduard", + "dusty1", + "kathy1", + "katherin", + "moonbeam", + "muscles", + "monster1", + "monkeybo", + "morton", + "windsurf", + "vvvvvvv", + "vivid", + "install", + "1947", + "187187", + "1941", + "1952", + "tatiana", + "susan1", + "31415926", + "sinned", + "sexxy", + "senator", + "sebastian", + "shadows", + "smoothie", + "snowflak", + "playstat", + "playa", + "playboy1", + "toaster", + "jerry1", + "marie1", + "mason1", + "merlin1", + "roger1", + "roadster", + "112358", + "1121", + "andrea1", + "bacardi", + "auto", + "hardware", + "hardy", + "789789", + "5555555", + "captain1", + "flores", + "fergus", + "sascha", + "rrrrrrr", + "dome", + "onion", + "nutter", + "lololo", + "qqqqqqq", + "quick", + "undertak", + "uuuuuuuu", + "uuuuuuu", + "criminal", + "cobain", + "cindy1", + "coors", + "dani", + "descent", + "nimbus", + "nomad", + "nanook", + "norwich", + "bomb", + "bombay", + "broker", + "hookup", + "kiwi", + "winners", + "jackpot", + "1a2b3c4d", + "1776", + "beardog", + "bighead", + "blast", + "bird33", + "0987", + "stress", + "shot", + "spooge", + "pelican", + "peepee", + "perry", + "pointer", + "titan", + "thedoors", + "jeremy1", + "annabell", + "altima", + "baba", + "hallie", + "hate", + "hardone", + "5454", + "candace", + "catwoman", + "flip", + "faithful", + "finance", + "farmboy", + "farscape", + "genesis1", + "salomon", + "destroy", + "papers", + "option", + "page", + "loser1", + "lopez", + "r2d2", + "pumpkins", + "training", + "chriss", + "cumcum", + "ninjas", + "ninja1", + "hung", + "erika", + "eduardo", + "killers", + "miller1", + "islander", + "jamesbond", + "intel", + "jarvis", + "19841984", + "2626", + "bizzare", + "blue12", + "biker", + "yoyoma", + "sushi", + "styles", + "shitface", + "series", + "shanti", + "spanker", + "steffi", + "smart", + "sphinx", + "please1", + "paulie", + "pistons", + "tiburon", + "limited", + "maxwell1", + "mdogg", + "rockies", + "armstron", + "alexia", + "arlene", + "alejandr", + "arctic", + "banger", + "audio", + "asimov", + "augustus", + "grandpa", + "753951", + "4you", + "chilly", + "care1839", + "chapman", + "flyfish", + "fantasia", + "freefall", + "santa", + "sandrine", + "oreo", + "ohshit", + "macbeth", + "madcat", + "loveya", + "mallory", + "rage", + "quentin", + "qwerqwer", + "project", + "ramirez", + "colnago", + "citizen", + "chocha", + "cobalt", + "crystal1", + "dabears", + "nevets", + "nineinch", + "broncos1", + "helene", + "huge", + "edgar", + "epsilon", + "easter", + "kestrel", + "moron", + "virgil", + "winston1", + "warrior1", + "iiiiiiii", + "iloveyou2", + "1616", + "beat", + "bettina", + "woowoo", + "zander", + "straight", + "shower", + "sloppy", + "specialk", + "tinkerbe", + "jellybea", + "reader", + "romero", + "redsox1", + "ride", + "1215", + "1112", + "annika", + "arcadia", + "answer", + "baggio", + "base", + "guido", + "555666", + "carmel", + "cayman", + "cbr900rr", + "chips", + "gabriell", + "gertrude", + "glennwei", + "roxy", + "sausages", + "disco", + "pass1", + "luna", + "lovebug", + "macmac", + "queenie", + "puffin", + "vanguard", + "trip", + "trinitro", + "airwolf", + "abbott", + "aaa111", + "cocaine", + "cisco", + "cottage", + "dayton", + "deadly", + "datsun", + "bricks", + "bumper", + "eldorado", + "kidrock", + "wizard1", + "whiskers", + "wind", + "wildwood", + "istheman", + "interest", + "italy", + "25802580", + "benoit", + "bigones", + "woodland", + "wolfpac", + "strawber", + "suicide", + "3030", + "sheba1", + "sixpack", + "peace1", + "physics", + "pearson", + "tigger2", + "toad", + "megan1", + "meow", + "ringo", + "roll", + "amsterdam", + "717171", + "686868", + "5424", + "catherine", + "canuck", + "football1", + "footjob", + "fulham", + "seagull", + "orgy", + "lobo", + "mancity", + "truth", + "trace", + "vancouve", + "vauxhall", + "acidburn", + "derf", + "myspace1", + "boozer", + "buttercu", + "howell", + "hola", + "easton", + "minemine", + "munch", + "jared", + "1dragon", + "biology", + "bestbuy", + "bigpoppa", + "blackout", + "blowfish", + "bmw325", + "bigbob", + "stream", + "talisman", + "tazz", + "sundevil", + "3333333", + "skate", + "shutup", + "shanghai", + "shop", + "spencer1", + "slowhand", + "polish", + "pinky1", + "tootie", + "thecrow", + "leroy", + "jonathon", + "jubilee", + "jingle", + "martine", + "matrix1", + "manowar", + "michaels", + "messiah", + "mclaren", + "resident", + "reilly", + "redbaron", + "rollins", + "romans", + "return", + "rivera", + "andromed", + "athlon", + "beach1", + "badgers", + "guitars", + "harald", + "harddick", + "gotribe", + "6996", + "7grout", + "5wr2i7h8", + "635241", + "chase1", + "carver", + "charlotte", + "fallout", + "fiddle", + "fredrick", + "fenris", + "francesc", + "fortuna", + "ferguson", + "fairlane", + "felipe", + "felix1", + "forward", + "gasman", + "frost", + "fucks", + "sahara", + "sassy1", + "dogpound", + "dogbert", + "divx1", + "manila", + "loretta", + "priest", + "pornporn", + "quasar", + "venom", + "987987", + "access1", + "clippers", + "daylight", + "decker", + "daman", + "data", + "dentist", + "crusty", + "nathan1", + "nnnnnnnn", + "bruno1", + "bucks", + "brodie", + "budapest", + "kittens", + "kerouac", + "mother1", + "waldo1", + "wedding", + "whistler", + "whatwhat", + "wanderer", + "idontkno", + "1942", + "1946", + "bigdawg", + "bigpimp", + "zaqwsx", + "414141", + "3000gt", + "434343", + "shoes", + "serpent", + "starr", + "smurf", + "pasword", + "tommie", + "thisisit", + "lake", + "john1", + "robotics", + "redeye", + "rebelz", + "1011", + "alatam", + "asses", + "asians", + "bama", + "banzai", + "harvest", + "gonzalez", + "hair", + "hanson", + "575757", + "5329", + "cascade", + "chinese", + "fatty", + "fender1", + "flower2", + "funky", + "sambo", + "drummer1", + "dogcat", + "dottie", + "oedipus", + "osama", + "macleod", + "prozac", + "private1", + "rampage", + "punch", + "presley", + "concord", + "cook", + "cinema", + "cornwall", + "cleaner", + "christopher", + "ciccio", + "corinne", + "clutch", + "corvet07", + "daemon", + "bruiser", + "boiler", + "hjkl", + "eyes", + "egghead", + "expert", + "ethan", + "kasper", + "mordor", + "wasted", + "jamess", + "iverson3", + "bluesman", + "zouzou", + "090909", + "1002", + "switch", + "stone1", + "4040", + "sisters", + "sexo", + "shawna", + "smith1", + "sperma", + "sneaky", + "polska", + "thewho", + "terminat", + "krypton", + "lawson", + "library", + "lekker", + "jules", + "johnson1", + "johann", + "justus", + "rockie", + "romano", + "aspire", + "bastards", + "goodie", + "cheese1", + "fenway", + "fishon", + "fishin", + "fuckoff1", + "girls1", + "sawyer", + "dolores", + "desmond", + "duane", + "doomsday", + "pornking", + "ramones", + "rabbits", + "transit", + "aaaaa1", + "clock", + "delilah", + "noel", + "boyz", + "bookworm", + "bongo", + "bunnies", + "brady", + "buceta", + "highbury", + "henry1", + "heels", + "eastern", + "krissy", + "mischief", + "mopar", + "ministry", + "vienna", + "weston", + "wildone", + "vodka", + "jayson", + "bigbooty", + "beavis1", + "betsy", + "xxxxxx1", + "yogibear", + "000001", + "0815", + "zulu", + "420000", + "september", + "sigmar", + "sprout", + "stalin", + "peggy", + "patch", + "lkjhgfds", + "lagnaf", + "rolex", + "redfox", + "referee", + "123123123", + "1231", + "angus1", + "ariana", + "ballin", + "attila", + "hall", + "greedy", + "grunt", + "747474", + "carpedie", + "cecile", + "caramel", + "foxylady", + "field", + "gatorade", + "gidget", + "futbol", + "frosch", + "saiyan", + "schmidt", + "drums", + "donner", + "doggy1", + "drum", + "doudou", + "pack", + "pain", + "nutmeg", + "quebec", + "valdepen", + "trash", + "triple", + "tosser", + "tuscl", + "track", + "comfort", + "choke", + "comein", + "cola", + "deputy", + "deadpool", + "bremen", + "borders", + "bronson", + "break", + "hotass", + "hotmail1", + "eskimo", + "eggman", + "koko", + "kieran", + "katrin", + "kordell1", + "komodo", + "mone", + "munich", + "vvvvvvvv", + "winger", + "jaeger", + "ivan", + "jackson5", + "2222222", + "bergkamp", + "bennie", + "bigben", + "zanzibar", + "worm", + "xxx123", + "sunny1", + "373737", + "services", + "sheridan", + "slater", + "slayer1", + "snoop", + "stacie", + "peachy", + "thecure", + "times", + "little1", + "jennaj", + "marquis", + "middle", + "rasta69", + "1114", + "aries", + "havana", + "gratis", + "calgary", + "checkers", + "flanker", + "salope", + "dirty1", + "draco", + "dogface", + "luv2epus", + "rainbow6", + "qwerty123", + "umpire", + "turnip", + "vbnm", + "tucson", + "troll", + "aileen", + "codered", + "commande", + "damon", + "nana", + "neon", + "nico", + "nightwin", + "neil", + "boomer1", + "bushido", + "hotmail0", + "horace", + "enternow", + "kaitlyn", + "keepout", + "karen1", + "mindy", + "mnbv", + "viewsoni", + "volcom", + "wizards", + "wine", + "1995", + "berkeley", + "bite", + "zach", + "woodstoc", + "tarpon", + "shinobi", + "starstar", + "phat", + "patience", + "patrol", + "toolbox", + "julien", + "johnny1", + "joebob", + "marble", + "riders", + "reflex", + "120676", + "1235", + "angelus", + "anthrax", + "atlas", + "hawks", + "grandam", + "harlem", + "hawaii50", + "gorgeous", + "655321", + "cabron", + "challeng", + "callisto", + "firewall", + "firefire", + "fischer", + "flyer", + "flower1", + "factory", + "federal", + "gambler", + "frodo1", + "funk", + "sand", + "sam123", + "scania", + "dingo", + "papito", + "passmast", + "olive", + "palermo", + "ou8123", + "lock", + "ranch", + "pride", + "randy1", + "twiggy", + "travis1", + "transfer", + "treetop", + "addict", + "admin1", + "963852", + "aceace", + "clarissa", + "cliff", + "cirrus", + "clifton", + "colin", + "bobdole", + "bonner", + "bogus", + "bonjovi", + "bootsy", + "boater", + "elway7", + "edison", + "kelvin", + "kenny1", + "moonshin", + "montag", + "moreno", + "wayne1", + "white1", + "jazzy", + "jakejake", + "1994", + "1991", + "2828", + "blunt", + "bluejays", + "beau", + "belmont", + "worthy", + "systems", + "sensei", + "southpark", + "stan", + "peeper", + "pharao", + "pigpen", + "tomahawk", + "teensex", + "leedsutd", + "larkin", + "jermaine", + "jeepster", + "jimjim", + "josephin", + "melons", + "marlon", + "matthias", + "marriage", + "robocop", + "1003", + "1027", + "antelope", + "azsxdc", + "gordo", + "hazard", + "granada", + "8989", + "7894", + "ceasar", + "cabernet", + "cheshire", + "california", + "chelle", + "candy1", + "fergie", + "fanny", + "fidelio", + "giorgio", + "fuckhead", + "ruth", + "sanford", + "diego", + "dominion", + "devon", + "panic", + "longer", + "mackie", + "qawsed", + "trucking", + "twelve", + "chloe1", + "coral", + "daddyo", + "nostromo", + "boyboy", + "booster", + "bucky", + "honolulu", + "esquire", + "dynamite", + "motor", + "mollydog", + "wilder", + "windows1", + "waffle", + "wallet", + "warning", + "virus", + "washburn", + "wealth", + "vincent1", + "jabber", + "jaguars", + "javelin", + "irishman", + "idefix", + "bigdog1", + "blue42", + "blanked", + "blue32", + "biteme1", + "bearcats", + "blaine", + "yessir", + "sylveste", + "team", + "stephan", + "sunfire", + "tbird", + "stryker", + "3ip76k2", + "sevens", + "sheldon", + "pilgrim", + "tenchi", + "titman", + "leeds", + "lithium", + "lander", + "linkin", + "landon", + "marijuan", + "mariner", + "markie", + "midnite", + "reddwarf", + "1129", + "123asd", + "12312312", + "allstar", + "albany", + "asdf12", + "antonia", + "aspen", + "hardball", + "goldfing", + "7734", + "49ers", + "carlo", + "chambers", + "cable", + "carnage", + "callum", + "carlos1", + "fitter", + "fandango", + "festival", + "flame", + "gofast", + "gamma", + "fucmy69", + "scrapper", + "dogwood", + "django", + "magneto", + "loose", + "premium", + "addison", + "9999999", + "abc1234", + "cromwell", + "newyear", + "nichole", + "bookie", + "burns", + "bounty", + "brown1", + "bologna", + "earl", + "entrance", + "elway", + "killjoy", + "kerry", + "keenan", + "kick", + "klondike", + "mini", + "mouser", + "mohammed", + "wayer", + "impreza", + "irene", + "insomnia", + "24682468", + "2580", + "24242424", + "billbill", + "bellaco", + "blessing", + "blues1", + "bedford", + "blanco", + "blunts", + "stinks", + "teaser", + "streets", + "sf49ers", + "shovel", + "solitude", + "spikey", + "sonia", + "pimpdadd", + "timeout", + "toffee", + "lefty", + "johndoe", + "johndeer", + "mega", + "manolo", + "mentor", + "margie", + "ratman", + "ridge", + "record", + "rhodes", + "robin1", + "1124", + "1210", + "1028", + "1226", + "another", + "babylove", + "barbados", + "harbor", + "gramma", + "646464", + "carpente", + "chaos1", + "fishbone", + "fireblad", + "glasgow", + "frogs", + "scissors", + "screamer", + "salem", + "scuba1", + "ducks", + "driven", + "doggies", + "dicky", + "donovan", + "obsidian", + "rams", + "progress", + "tottenham", + "aikman", + "comanche", + "corolla", + "clarke", + "conway", + "cumslut", + "cyborg", + "dancing", + "boston1", + "bong", + "houdini", + "helmut", + "elvisp", + "edge", + "keksa12", + "misha", + "monty1", + "monsters", + "wetter", + "watford", + "wiseguy", + "veronika", + "visitor", + "janelle", + "1989", + "1987", + "20202020", + "biatch", + "beezer", + "bigguns", + "blueball", + "bitchy", + "wyoming", + "yankees2", + "wrestler", + "stupid1", + "sealteam", + "sidekick", + "simple1", + "smackdow", + "sporting", + "spiral", + "smeller", + "sperm", + "plato", + "tophat", + "test2", + "theatre", + "thick", + "toomuch", + "leigh", + "jello", + "jewish", + "junkie", + "maxim", + "maxime", + "meadow", + "remingto", + "roofer", + "124038", + "1018", + "1269", + "1227", + "123457", + "arkansas", + "alberta", + "aramis", + "andersen", + "beaker", + "barcelona", + "baltimor", + "googoo", + "goochi", + "852456", + "4711", + "catcher", + "carman", + "champ1", + "chess", + "fortress", + "fishfish", + "firefigh", + "geezer", + "rsalinas", + "samuel1", + "saigon", + "scooby1", + "doors", + "dick1", + "devin", + "doom", + "dirk", + "doris", + "dontknow", + "load", + "magpies", + "manfred", + "raleigh", + "vader1", + "universa", + "tulips", + "defense", + "mygirl", + "burn", + "bowtie", + "bowman", + "holycow", + "heinrich", + "honeys", + "enforcer", + "katherine", + "minerva", + "wheeler", + "witch", + "waterboy", + "jaime", + "irving", + "1992", + "23skidoo", + "bimbo", + "blue11", + "birddog", + "woodman", + "womble", + "zildjian", + "030303", + "stinker", + "stoppedby", + "sexybabe", + "speakers", + "slugger", + "spotty", + "smoke1", + "polopolo", + "perfect1", + "things", + "torpedo", + "tender", + "thrasher", + "lakeside", + "lilith", + "jimmys", + "jerk", + "junior1", + "marsh", + "masamune", + "rice", + "root", + "1214", + "april1", + "allgood", + "bambi", + "grinch", + "767676", + "5252", + "cherries", + "chipmunk", + "cezer121", + "carnival", + "capecod", + "finder", + "flint", + "fearless", + "goats", + "funstuff", + "gideon", + "savior", + "seabee", + "sandro", + "schalke", + "salasana", + "disney1", + "duckman", + "options", + "pancake", + "pantera1", + "malice", + "lookin", + "love123", + "lloyd", + "qwert123", + "puppet", + "prayers", + "union", + "tracer", + "crap", + "creation", + "cwoui", + "nascar24", + "hookers", + "hollie", + "hewitt", + "estrella", + "erection", + "ernesto", + "ericsson", + "edthom", + "kaylee", + "kokoko", + "kokomo", + "kimball", + "morales", + "mooses", + "monk", + "walton", + "weekend", + "inter", + "internal", + "1michael", + "1993", + "19781978", + "25252525", + "worker", + "summers", + "surgery", + "shibby", + "shamus", + "skibum", + "sheepdog", + "sex69", + "spliff", + "slipper", + "spoons", + "spanner", + "snowbird", + "slow", + "toriamos", + "temp123", + "tennesse", + "lakers1", + "jomama", + "julio", + "mazdarx7", + "rosario", + "recon", + "riddle", + "room", + "revolver", + "1025", + "1101", + "barney1", + "babycake", + "baylor", + "gotham", + "gravity", + "hallowee", + "hancock", + "616161", + "515000", + "caca", + "cannabis", + "castor", + "chilli", + "fdsa", + "getout", + "fuck69", + "gators1", + "sail", + "sable", + "rumble", + "dolemite", + "dork", + "dickens", + "duffer", + "dodgers1", + "painting", + "onions", + "logger", + "lorena", + "lookout", + "magic32", + "port", + "poon", + "prime", + "twat", + "coventry", + "citroen", + "christmas", + "civicsi", + "cocksucker", + "coochie", + "compaq1", + "nancy1", + "buzzer", + "boulder", + "butkus", + "bungle", + "hogtied", + "honor", + "hero", + "hotgirls", + "hilary", + "heidi1", + "eggplant", + "mustang6", + "mortal", + "monkey12", + "wapapapa", + "wendy1", + "volleyba", + "vibrate", + "vicky", + "bledsoe", + "blink", + "birthday4", + "woof", + "xxxxx1", + "talk", + "stephen1", + "suburban", + "stock", + "tabatha", + "sheeba", + "start1", + "soccer10", + "something", + "starcraft", + "soccer12", + "peanut1", + "plastics", + "penthous", + "peterbil", + "tools", + "tetsuo", + "torino", + "tennis1", + "termite", + "ladder", + "last", + "lemmein", + "lakewood", + "jughead", + "melrose", + "megane", + "reginald", + "redone", + "request", + "angela1", + "alive", + "alissa", + "goodgirl", + "gonzo1", + "golden1", + "gotyoass", + "656565", + "626262", + "capricor", + "chains", + "calvin1", + "foolish", + "fallon", + "getmoney", + "godfather", + "gabber", + "gilligan", + "runaway", + "salami", + "dummy", + "dungeon", + "dudedude", + "dumb", + "dope", + "opus", + "paragon", + "oxygen", + "panhead", + "pasadena", + "opendoor", + "odyssey", + "magellan", + "lottie", + "printing", + "pressure", + "prince1", + "trustme", + "christa", + "court", + "davies", + "neville", + "nono", + "bread", + "buffet", + "hound", + "kajak", + "killkill", + "mona", + "moto", + "mildred", + "winner1", + "vixen", + "whiteboy", + "versace", + "winona", + "voyager1", + "instant", + "indy", + "jackjack", + "bigal", + "beech", + "biggun", + "blake1", + "blue99", + "big1", + "woods", + "synergy", + "success1", + "336699", + "sixty9", + "shark1", + "skin", + "simba1", + "sharpe", + "sebring", + "spongebo", + "spunk", + "springs", + "sliver", + "phialpha", + "password9", + "pizza1", + "plane", + "perkins", + "pookey", + "tickling", + "lexingky", + "lawman", + "joe123", + "jolly", + "mike123", + "romeo1", + "redheads", + "reserve", + "apple123", + "alanis", + "ariane", + "antony", + "backbone", + "aviation", + "band", + "hand", + "green123", + "haley", + "carlitos", + "byebye", + "cartman1", + "camden", + "chewy", + "camaross", + "favorite6", + "forumwp", + "franks", + "ginscoot", + "fruity", + "sabrina1", + "devil666", + "doughnut", + "pantie", + "oldone", + "paintball", + "lumina", + "rainbow1", + "prosper", + "total", + "true", + "umbrella", + "ajax", + "951753", + "achtung", + "abc12345", + "compact", + "color", + "corn", + "complete", + "christi", + "closer", + "corndog", + "deerhunt", + "darklord", + "dank", + "nimitz", + "brandy1", + "bowl", + "breanna", + "holidays", + "hetfield", + "holein1", + "hillbill", + "hugetits", + "east", + "evolutio", + "kenobi", + "whiplash", + "waldo", + "wg8e3wjf", + "wing", + "istanbul", + "invis", + "1996", + "benton", + "bigjohn", + "bluebell", + "beef", + "beater", + "benji", + "bluejay", + "xyzzy", + "wrestling", + "storage", + "superior", + "suckdick", + "taichi", + "stellar", + "stephane", + "shaker", + "skirt", + "seymour", + "semper", + "splurge", + "squeak", + "pearls", + "playball", + "pitch", + "phyllis", + "pooky", + "piss", + "tomas", + "titfuck", + "joemama", + "johnny5", + "marcello", + "marjorie", + "married", + "maxi", + "rhubarb", + "rockwell", + "ratboy", + "reload", + "rooney", + "redd", + "1029", + "1030", + "1220", + "anchor", + "bbking", + "baritone", + "gryphon", + "gone", + "57chevy", + "494949", + "celeron", + "fishy", + "gladiator", + "fucker1", + "roswell", + "dougie", + "downer", + "dicker", + "diva", + "domingo", + "donjuan", + "nympho", + "omar", + "praise", + "racers", + "trick", + "trauma", + "truck1", + "trample", + "acer", + "corwin", + "cricket1", + "clemente", + "climax", + "denmark", + "cuervo", + "notnow", + "nittany", + "neutron", + "native", + "bosco1", + "buffa", + "breaker", + "hello2", + "hydro", + "estelle", + "exchange", + "explore", + "kisskiss", + "kittys", + "kristian", + "montecar", + "modem", + "mississi", + "mooney", + "weiner", + "washington", + "20012001", + "bigdick1", + "bibi", + "benfica", + "yahoo1", + "striper", + "tabasco", + "supra", + "383838", + "456654", + "seneca", + "serious", + "shuttle", + "socks", + "stanton", + "penguin1", + "pathfind", + "testibil", + "thethe", + "listen", + "lightning", + "lighting", + "jeter2", + "marma", + "mark1", + "metoo", + "republic", + "rollin", + "redleg", + "redbone", + "redskin", + "rocco", + "1245", + "armand", + "anthony7", + "altoids", + "andrews", + "barley", + "away", + "asswipe", + "bauhaus", + "bbbbbb1", + "gohome", + "harrier", + "golfpro", + "goldeney", + "818181", + "6666666", + "5000", + "5rxypn", + "cameron1", + "calling", + "checker", + "calibra", + "fields", + "freefree", + "faith1", + "fist", + "fdm7ed", + "finally", + "giraffe", + "glasses", + "giggles", + "fringe", + "gate", + "georgie", + "scamper", + "rrpass1", + "screwyou", + "duffy", + "deville", + "dimples", + "pacino", + "ontario", + "passthie", + "oberon", + "quest1", + "postov1000", + "puppydog", + "puffer", + "raining", + "protect", + "qwerty7", + "trey", + "tribe", + "ulysses", + "tribal", + "adam25", + "a1234567", + "compton", + "collie", + "cleopatr", + "contract", + "davide", + "norris", + "namaste", + "myrtle", + "buffalo1", + "bonovox", + "buckley", + "bukkake", + "burning", + "burner", + "bordeaux", + "burly", + "hun999", + "emilie", + "elmo", + "enters", + "enrique", + "keisha", + "mohawk", + "willard", + "vgirl", + "whale", + "vince", + "jayden", + "jarrett", + "1812", + "1943", + "222333", + "bigjim", + "bigd", + "zoom", + "wordup", + "ziggy1", + "yahooo", + "workout", + "young1", + "written", + "xmas", + "zzzzzz1", + "surfer1", + "strife", + "sunlight", + "tasha1", + "skunk", + "shauna", + "seth", + "soft", + "sprinter", + "peaches1", + "planes", + "pinetree", + "plum", + "pimping", + "theforce", + "thedon", + "toocool", + "leeann", + "laddie", + "list", + "lkjh", + "lara", + "joke", + "jupiter1", + "mckenzie", + "matty", + "rene", + "redrose", + "1200", + "102938", + "annmarie", + "alexa", + "antares", + "austin31", + "ground", + "goose1", + "737373", + "78945612", + "789987", + "6464", + "calimero", + "caster", + "casper1", + "cement", + "chevrolet", + "chessie", + "caddy", + "chill", + "child", + "canucks", + "feeling", + "favorite", + "fellatio", + "f00tball", + "francine", + "gateway2", + "gigi", + "gamecube", + "giovanna", + "rugby1", + "scheisse", + "dshade", + "dudes", + "dixie1", + "owen", + "offshore", + "olympia", + "lucas1", + "macaroni", + "manga", + "pringles", + "puff", + "tribble", + "trouble1", + "ussy", + "core", + "clint", + "coolhand", + "colonial", + "colt", + "debra", + "darthvad", + "dealer", + "cygnusx1", + "natalie1", + "newark", + "husband", + "hiking", + "errors", + "eighteen", + "elcamino", + "emmett", + "emilia", + "koolaid", + "knight1", + "murphy1", + "volcano", + "idunno", + "2005", + "2233", + "block", + "benito", + "blueberr", + "biguns", + "yamahar1", + "zapper", + "zorro1", + "0911", + "3006", + "sixsix", + "shopper", + "siobhan", + "sextoy", + "stafford", + "snowboard", + "speedway", + "sounds", + "pokey", + "peabody", + "playboy2", + "titi", + "think", + "toast", + "toonarmy", + "lister", + "lambda", + "joecool", + "jonas", + "joyce", + "juniper", + "mercer", + "max123", + "manny", + "massimo", + "mariposa", + "met2002", + "reggae", + "ricky1", + "1236", + "1228", + "1016", + "all4one", + "arianna", + "baberuth", + "asgard", + "gonzales", + "484848", + "5683", + "6669", + "catnip", + "chiquita", + "charisma", + "capslock", + "cashmone", + "chat", + "figure", + "galant", + "frenchy", + "gizmodo1", + "girlies", + "gabby", + "garner", + "screwy", + "doubled", + "divers", + "dte4uw", + "done", + "dragonfl", + "maker", + "locks", + "rachelle", + "treble", + "twinkie", + "trailer", + "tropical", + "acid", + "crescent", + "cooking", + "cococo", + "cory", + "dabomb", + "daffy", + "dandfa", + "cyrano", + "nathanie", + "briggs", + "boners", + "helium", + "horton", + "hoffman", + "hellas", + "espresso", + "emperor", + "killa", + "kikimora", + "wanda", + "w4g8at", + "verona", + "ilikeit", + "iforget", + "1944", + "20002000", + "birthday1", + "beatles1", + "blue1", + "bigdicks", + "beethove", + "blacklab", + "blazers", + "benny1", + "woodwork", + "0069", + "0101", + "taffy", + "susie", + "survivor", + "swim", + "stokes", + "4567", + "shodan", + "spoiled", + "steffen", + "pissed", + "pavlov", + "pinnacle", + "place", + "petunia", + "terrell", + "thirty", + "toni", + "tito", + "teenie", + "lemonade", + "lily", + "lillie", + "lalakers", + "lebowski", + "lalalala", + "ladyboy", + "jeeper", + "joyjoy", + "mercury1", + "mantle", + "mannn", + "rocknrol", + "riversid", + "reeves", + "123aaa", + "11112222", + "121314", + "1021", + "1004", + "1120", + "allen1", + "ambers", + "amstel", + "ambrose", + "alice1", + "alleycat", + "allegro", + "ambrosia", + "alley", + "australia", + "hatred", + "gspot", + "graves", + "goodsex", + "hattrick", + "harpoon", + "878787", + "8inches", + "4wwvte", + "cassandr", + "charlie123", + "case", + "chavez", + "fighting", + "gabriela", + "gatsby", + "fudge", + "gerry", + "generic", + "gareth", + "fuckme2", + "samm", + "sage", + "seadog", + "satchmo", + "scxakv", + "santafe", + "dipper", + "dingle", + "dizzy", + "outoutout", + "madmad", + "london1", + "qbg26i", + "pussy123", + "randolph", + "vaughn", + "tzpvaw", + "vamp", + "comedy", + "comp", + "cowgirl", + "coldplay", + "dawgs", + "delaney", + "nt5d27", + "novifarm", + "needles", + "notredam", + "newness", + "mykids", + "bryan1", + "bouncer", + "hihihi", + "honeybee", + "iceman1", + "herring", + "horn", + "hook", + "hotlips", + "dynamo", + "klaus", + "kittie", + "kappa", + "kahlua", + "muffy", + "mizzou", + "mohamed", + "musical", + "wannabe", + "wednesda", + "whatup", + "weller", + "waterfal", + "willy1", + "invest", + "blanche", + "bear1", + "billabon", + "youknow", + "zelda", + "yyyyyy1", + "zachary1", + "01234567", + "070462", + "zurich", + "superstar", + "storms", + "tail", + "stiletto", + "strat", + "427900", + "sigmachi", + "shelter", + "shells", + "sexy123", + "smile1", + "sophie1", + "stefano", + "stayout", + "somerset", + "smithers", + "playmate", + "pinkfloyd", + "phish1", + "payday", + "thebear", + "telefon", + "laetitia", + "kswbdu", + "larson", + "jetta", + "jerky", + "melina", + "metro", + "revoluti", + "retire", + "respect", + "1216", + "1201", + "1204", + "1222", + "1115", + "archange", + "barry1", + "handball", + "676767", + "chandra", + "chewbacc", + "flesh", + "furball", + "gocubs", + "fruit", + "fullback", + "gman", + "gentle", + "dunbar", + "dewalt", + "dominiqu", + "diver1", + "dhip6a", + "olemiss", + "ollie", + "mandrake", + "mangos", + "pretzel", + "pusssy", + "tripleh", + "valdez", + "vagabond", + "clean", + "comment", + "crew", + "clovis", + "deaths", + "dandan", + "csfbr5yy", + "deadspin", + "darrel", + "ninguna", + "noah", + "ncc74656", + "bootsie", + "bp2002", + "bourbon", + "brennan", + "bumble", + "books", + "hose", + "heyyou", + "houston1", + "hemlock", + "hippo", + "hornets", + "hurricane", + "horseman", + "hogan", + "excess", + "extensa", + "muffin1", + "virginie", + "werdna", + "idontknow", + "info", + "iron", + "jack1", + "1bitch", + "151nxjmt", + "bendover", + "bmwbmw", + "bills", + "zaq123", + "wxcvbn", + "surprise", + "supernov", + "tahoe", + "talbot", + "simona", + "shakur", + "sexyone", + "seviyi", + "sonja", + "smart1", + "speed1", + "pepito", + "phantom1", + "playoffs", + "terry1", + "terrier", + "laser1", + "lite", + "lancia", + "johngalt", + "jenjen", + "jolene", + "midori", + "message", + "maserati", + "matteo", + "mental", + "miami1", + "riffraff", + "ronald1", + "reason", + "rhythm", + "1218", + "1026", + "123987", + "1015", + "1103", + "armada", + "architec", + "austria", + "gotmilk", + "hawkins", + "gray", + "camila", + "camp", + "cambridg", + "charge", + "camero", + "flex", + "foreplay", + "getoff", + "glacier", + "glotest", + "froggie", + "gerbil", + "rugger", + "sanity72", + "salesman", + "donna1", + "dreaming", + "deutsch", + "orchard", + "oyster", + "palmtree", + "ophelia", + "pajero", + "m5wkqf", + "magenta", + "luckyone", + "treefrog", + "vantage", + "usmarine", + "tyvugq", + "uptown", + "abacab", + "aaaaaa1", + "advance", + "chuck1", + "delmar", + "darkange", + "cyclones", + "nate", + "navajo", + "nope", + "border", + "bubba123", + "building", + "iawgk2", + "hrfzlz", + "dylan1", + "enrico", + "encore", + "emilio", + "eclipse1", + "killian", + "kayleigh", + "mutant", + "mizuno", + "mustang2", + "video1", + "viewer", + "weed420", + "whales", + "jaguar1", + "insight", + "1990", + "159159", + "1love", + "bliss", + "bears1", + "bigtruck", + "binder", + "bigboss", + "blitz", + "xqgann", + "yeahyeah", + "zeke", + "zardoz", + "stickman", + "table", + "3825", + "signal", + "sentra", + "side", + "shiva", + "skipper1", + "singapor", + "southpaw", + "sonora", + "squid", + "slamdunk", + "slimjim", + "placid", + "photon", + "placebo", + "pearl1", + "test12", + "therock1", + "tiger123", + "leinad", + "legman", + "jeepers", + "joeblow", + "mccarthy", + "mike23", + "redcar", + "rhinos", + "rjw7x4", + "1102", + "13576479", + "112211", + "alcohol", + "gwju3g", + "greywolf", + "7bgiqk", + "7878", + "535353", + "4snz9g", + "candyass", + "cccccc1", + "carola", + "catfight", + "cali", + "fister", + "fosters", + "finland", + "frankie1", + "gizzmo", + "fuller", + "royalty", + "rugrat", + "sandie", + "rudolf", + "dooley", + "dive", + "doreen", + "dodo", + "drop", + "oemdlg", + "out3xf", + "paddy", + "opennow", + "puppy1", + "qazwsxedc", + "pregnant", + "quinn", + "ramjet", + "under", + "uncle", + "abraxas", + "corner", + "creed", + "cocoa", + "crown", + "cows", + "cn42qj", + "dancer1", + "death666", + "damned", + "nudity", + "negative", + "nimda2k", + "buick", + "bobb", + "braves1", + "brook", + "henrik", + "higher", + "hooligan", + "dust", + "everlast", + "karachi", + "mortis", + "mulligan", + "monies", + "motocros", + "wally1", + "weapon", + "waterman", + "view", + "willie1", + "vicki", + "inspiron", + "1test", + "2929", + "bigblack", + "xytfu7", + "yackwin", + "zaq1xsw2", + "yy5rbfsc", + "100100", + "0660", + "tahiti", + "takehana", + "talks", + "332211", + "3535", + "sedona", + "seawolf", + "skydiver", + "shine", + "spleen", + "slash", + "spjfet", + "special1", + "spooner", + "slimshad", + "sopranos", + "spock1", + "penis1", + "patches1", + "terri", + "thierry", + "thething", + "toohot", + "large", + "limpone", + "johnnie", + "mash4077", + "matchbox", + "masterp", + "maxdog", + "ribbit", + "reed", + "rita", + "rockin", + "redhat", + "rising", + "1113", + "14789632", + "1331", + "allday", + "aladin", + "andrey", + "amethyst", + "ariel", + "anytime", + "baseball1", + "athome", + "basil", + "goofy1", + "greenman", + "gustavo", + "goofball", + "ha8fyp", + "goodday", + "778899", + "charon", + "chappy", + "castillo", + "caracas", + "cardiff", + "capitals", + "canada1", + "cajun", + "catter", + "freddy1", + "favorite2", + "frazier", + "forme", + "follow", + "forsaken", + "feelgood", + "gavin", + "gfxqx686", + "garlic", + "sarge", + "saskia", + "sanjose", + "russ", + "salsa", + "dilbert1", + "dukeduke", + "downhill", + "longhair", + "loop", + "locutus", + "lockdown", + "malachi", + "mamacita", + "lolipop", + "rainyday", + "pumpkin1", + "punker", + "prospect", + "rambo1", + "rainbows", + "quake", + "twin", + "trinity1", + "trooper1", + "aimee", + "citation", + "coolcat", + "crappy", + "default", + "dental", + "deniro", + "d9ungl", + "daddys", + "napoli", + "nautica", + "nermal", + "bukowski", + "brick", + "bubbles1", + "bogota", + "board", + "branch", + "breath", + "buds", + "hulk", + "humphrey", + "hitachi", + "evans", + "ender", + "export", + "kikiki", + "kcchiefs", + "kram", + "morticia", + "montrose", + "mongo", + "waqw3p", + "wizzard", + "visited", + "whdbtp", + "whkzyc", + "image", + "154ugeiu", + "1fuck", + "binky", + "blind", + "bigred1", + "blubber", + "benz", + "becky1", + "year2005", + "wonderfu", + "wooden", + "xrated", + "0001", + "tampabay", + "survey", + "tammy1", + "stuffer", + "3mpz4r", + "3000", + "3some", + "selina", + "sierra1", + "shampoo", + "silk", + "shyshy", + "slapnuts", + "standby", + "spartan1", + "sprocket", + "sometime", + "stanley1", + "poker1", + "plus", + "thought", + "theshit", + "torture", + "thinking", + "lavalamp", + "light1", + "laserjet", + "jediknig", + "jjjjj1", + "jocelyn", + "mazda626", + "menthol", + "maximo", + "margaux", + "medic1", + "release", + "richter", + "rhino1", + "roach", + "renate", + "repair", + "reveal", + "1209", + "1234321", + "amigos", + "apricot", + "alexandra", + "asdfgh1", + "hairball", + "hatter", + "graduate", + "grimace", + "7xm5rq", + "6789", + "cartoons", + "capcom", + "cheesy", + "cashflow", + "carrots", + "camping", + "fanatic", + "fool", + "format", + "fleming", + "girlie", + "glover", + "gilmore", + "gardner", + "safeway", + "ruthie", + "dogfart", + "dondon", + "diapers", + "outsider", + "odin", + "opiate", + "lollol", + "love12", + "loomis", + "mallrats", + "prague", + "primetime21", + "pugsley", + "program", + "r29hqq", + "touch", + "valleywa", + "airman", + "abcdefg1", + "darkone", + "cummer", + "dempsey", + "damn", + "nadia", + "natedogg", + "nineball", + "ndeyl5", + "natchez", + "newone", + "normandy", + "nicetits", + "buddy123", + "buddys", + "homely", + "husky", + "iceland", + "hr3ytm", + "highlife", + "holla", + "earthlin", + "exeter", + "eatmenow", + "kimkim", + "karine", + "k2trix", + "kernel", + "kirkland", + "money123", + "moonman", + "miles1", + "mufasa", + "mousey", + "wilma", + "wilhelm", + "whites", + "warhamme", + "instinct", + "jackass1", + "2277", + "20spanks", + "blobby", + "blair", + "blinky", + "bikers", + "blackjack", + "becca", + "blue23", + "xman", + "wyvern", + "085tzzqi", + "zxzxzx", + "zsmj2v", + "suede", + "t26gn4", + "sugars", + "sylvie", + "tantra", + "swoosh", + "swiss", + "4226", + "4271", + "321123", + "383pdjvl", + "shoe", + "shane1", + "shelby1", + "spades", + "spain", + "smother", + "soup", + "sparhawk", + "pisser", + "photo1", + "pebble", + "phones", + "peavey", + "picnic", + "pavement", + "terra", + "thistle", + "tokyo", + "therapy", + "lives", + "linden", + "kronos", + "lilbit", + "linux", + "johnston", + "material", + "melanie1", + "marbles", + "redlight", + "reno", + "recall", + "1208", + "1138", + "1008", + "alchemy", + "aolsucks", + "alexalex", + "atticus", + "auditt", + "ballet", + "b929ezzh", + "goodyear", + "hanna", + "griffith", + "gubber", + "863abgsg", + "7474", + "797979", + "464646", + "543210", + "4zqauf", + "4949", + "ch5nmk", + "carlito", + "chewey", + "carebear", + "caleb", + "checkmat", + "cheddar", + "chachi", + "fever", + "forgetit", + "fine", + "forlife", + "giants1", + "gates", + "getit", + "gamble", + "gerhard", + "galileo", + "g3ujwg", + "ganja", + "rufus1", + "rushmore", + "scouts", + "discus", + "dudeman", + "olympus", + "oscars", + "osprey", + "madcow", + "locust", + "loyola", + "mammoth", + "proton", + "rabbit1", + "question", + "ptfe3xxp", + "pwxd5x", + "purple1", + "punkass", + "prophecy", + "uyxnyd", + "tyson1", + "aircraft", + "access99", + "abcabc", + "cocktail", + "colts", + "civilwar", + "cleveland", + "claudia1", + "contour", + "clement", + "dddddd1", + "cypher", + "denied", + "dapzu455", + "dagmar", + "daisydog", + "name", + "noles", + "butters", + "buford", + "hoochie", + "hotel", + "hoser", + "eddy", + "ellis", + "eldiablo", + "kingrich", + "mudvayne", + "motown", + "mp8o6d", + "wife", + "vipergts", + "italiano", + "innocent", + "2055", + "2211", + "beavers", + "bloke", + "blade1", + "yamato", + "zooropa", + "yqlgr667", + "050505", + "zxcvbnm1", + "zw6syj", + "suckcock", + "tango1", + "swing", + "stern", + "stephens", + "swampy", + "susanna", + "tammie", + "445566", + "333666", + "380zliki", + "sexpot", + "sexylady", + "sixtynin", + "sickboy", + "spiffy", + "sleeping", + "skylark", + "sparkles", + "slam", + "pintail", + "phreak", + "places", + "teller", + "timtim", + "tires", + "thighs", + "left", + "latex", + "llamas", + "letsdoit", + "lkjhg", + "landmark", + "letters", + "lizzard", + "marlins", + "marauder", + "metal1", + "manu", + "register", + "righton", + "1127", + "alain", + "alcat", + "amigo", + "basebal1", + "azertyui", + "attract", + "azrael", + "hamper", + "gotenks", + "golfgti", + "gutter", + "hawkwind", + "h2slca", + "harman", + "grace1", + "6chid8", + "789654", + "canine", + "casio", + "cazzo", + "chamber", + "cbr900", + "cabrio", + "calypso", + "capetown", + "feline", + "flathead", + "fisherma", + "flipmode", + "fungus", + "goal", + "g9zns4", + "full", + "giggle", + "gabriel1", + "fuck123", + "saffron", + "dogmeat", + "dreamcas", + "dirtydog", + "dunlop", + "douche", + "dresden", + "dickdick", + "destiny1", + "pappy", + "oaktree", + "lydia", + "luft4", + "puta", + "prayer", + "ramada", + "trumpet1", + "vcradq", + "tulip", + "tracy71", + "tycoon", + "aaaaaaa1", + "conquest", + "click", + "chitown", + "corps", + "creepers", + "constant", + "couples", + "code", + "cornhole", + "danman", + "dada", + "density", + "d9ebk7", + "cummins", + "darth", + "cute", + "nash", + "nirvana1", + "nixon", + "norbert", + "nestle", + "brenda1", + "bonanza", + "bundy", + "buddies", + "hotspur", + "heavy", + "horror", + "hufmqw", + "electro", + "erasure", + "enough", + "elisabet", + "etvww4", + "ewyuza", + "eric1", + "kinder", + "kenken", + "kismet", + "klaatu", + "musician", + "milamber", + "willi", + "waiting", + "isacs155", + "igor", + "1million", + "1letmein", + "x35v8l", + "yogi", + "ywvxpz", + "xngwoj", + "zippy1", + "020202", + "****", + "stonewal", + "sweeney", + "story", + "sentry", + "sexsexsex", + "spence", + "sonysony", + "smirnoff", + "star12", + "solace", + "sledge", + "states", + "snyder", + "star1", + "paxton", + "pentagon", + "pkxe62", + "pilot1", + "pommes", + "paulpaul", + "plants", + "tical", + "tictac", + "toes", + "lighthou", + "lemans", + "kubrick", + "letmein22", + "letmesee", + "jys6wz", + "jonesy", + "jjjjjj1", + "jigga", + "joelle", + "mate", + "merchant", + "redstorm", + "riley1", + "rosa", + "relief", + "14141414", + "1126", + "allison1", + "badboy1", + "asthma", + "auggie", + "basement", + "hartley", + "hartford", + "hardwood", + "gumbo", + "616913", + "57np39", + "56qhxs", + "4mnveh", + "cake", + "forbes", + "fatluvr69", + "fqkw5m", + "fidelity", + "feathers", + "fresno", + "godiva", + "gecko", + "gladys", + "gibson1", + "gogators", + "fridge", + "general1", + "saxman", + "rowing", + "sammys", + "scotts", + "scout1", + "sasasa", + "samoht", + "dragon69", + "ducky", + "dragonball", + "driller", + "p3wqaw", + "nurse", + "papillon", + "oneone", + "openit", + "optimist", + "longshot", + "portia", + "rapier", + "pussy2", + "ralphie", + "tuxedo", + "ulrike", + "undertow", + "trenton", + "copenhag", + "come", + "delldell", + "culinary", + "deltas", + "mytime", + "nicky", + "nickie", + "noname", + "noles1", + "bucker", + "bopper", + "bullock", + "burnout", + "bryce", + "hedges", + "ibilltes", + "hihje863", + "hitter", + "ekim", + "espana", + "eatme69", + "elpaso", + "envelope", + "express1", + "eeeeee1", + "eatme1", + "karaoke", + "kara", + "mustang5", + "misses", + "wellingt", + "willem", + "waterski", + "webcam", + "jasons", + "infinite", + "iloveyou!", + "jakarta", + "belair", + "bigdad", + "beerme", + "yoshi", + "yinyang", + "zimmer", + "x24ik3", + "063dyjuy", + "0000007", + "ztmfcq", + "stopit", + "stooges", + "survival", + "stockton", + "symow8", + "strato", + "2hot4u", + "ship", + "simons", + "skins", + "shakes", + "sex1", + "shield", + "snacks", + "softtail", + "slimed123", + "pizzaman", + "pipe", + "pitt", + "pathetic", + "pinto", + "tigercat", + "tonton", + "lager", + "lizzy", + "juju", + "john123", + "jennings", + "josiah", + "jesse1", + "jordon", + "jingles", + "martian", + "mario1", + "rootedit", + "rochard", + "redwine", + "requiem", + "riverrat", + "rats", + "1117", + "1014", + "1205", + "althea", + "allie", + "amor", + "amiga", + "alpina", + "alert", + "atreides", + "banana1", + "bahamut", + "hart", + "golfman", + "happines", + "7uftyx", + "5432", + "5353", + "5151", + "4747", + "byron", + "chatham", + "chadwick", + "cherie", + "foxfire", + "ffvdj474", + "freaked", + "foreskin", + "gayboy", + "gggggg1", + "glenda", + "gameover", + "glitter", + "funny1", + "scoobydoo", + "scroll", + "rudolph", + "saddle", + "saxophon", + "dingbat", + "digimon", + "omicron", + "parsons", + "ohio", + "panda1", + "loloxx", + "macintos", + "lululu", + "lollypop", + "racer1", + "queen1", + "qwertzui", + "prick", + "upnfmc", + "tyrant", + "trout1", + "9skw5g", + "aceman", + "adelaide", + "acls2h", + "aaabbb", + "acapulco", + "aggie", + "comcast", + "craft", + "crissy", + "cloudy", + "cq2kph", + "custer", + "d6o8pm", + "cybersex", + "davecole", + "darian", + "crumbs", + "daisey", + "davedave", + "dasani", + "needle", + "mzepab", + "myporn", + "narnia", + "nineteen", + "booger1", + "bravo1", + "budgie", + "btnjey", + "highlander", + "hotel6", + "humbug", + "edwin", + "ewtosi", + "kristin1", + "kobe", + "knuckles", + "keith1", + "katarina", + "muff", + "muschi", + "montana1", + "wingchun", + "wiggle", + "whatthe", + "walking", + "watching", + "vette1", + "vols", + "virago", + "intj3a", + "ishmael", + "intern", + "jachin", + "illmatic", + "199999", + "2010", + "beck", + "blender", + "bigpenis", + "bengal", + "blue1234", + "your", + "zaqxsw", + "xray", + "xxxxxxx1", + "zebras", + "yanks", + "worlds", + "tadpole", + "stripes", + "svetlana", + "3737", + "4343", + "3728", + "4444444", + "368ejhih", + "solar", + "sonne", + "smalls", + "sniffer", + "sonata", + "squirts", + "pitcher", + "playstation", + "pktmxr", + "pescator", + "points", + "texaco", + "lesbos", + "lilian", + "l8v53x", + "jo9k2jw2", + "jimbeam", + "josie", + "jimi", + "jupiter2", + "jurassic", + "marines1", + "maya", + "rocket1", + "ringer", + "14725836", + "12345679", + "1219", + "123098", + "1233", + "alessand", + "althor", + "angelika", + "arch", + "armando", + "alpha123", + "basher", + "barefeet", + "balboa", + "bbbbb1", + "banks", + "badabing", + "harriet", + "gopack", + "golfnut", + "gsxr1000", + "gregory1", + "766rglqy", + "8520", + "753159", + "8dihc6", + "69camaro", + "666777", + "cheeba", + "chino", + "calendar", + "cheeky", + "camel1", + "fishcake", + "falling", + "flubber", + "giuseppe", + "gianni", + "gloves", + "gnasher23", + "frisbee", + "fuzzy1", + "fuzzball", + "sauce", + "save13tx", + "schatz", + "russell1", + "sandra1", + "scrotum", + "scumbag", + "sabre", + "samdog", + "dripping", + "dragon12", + "dragster", + "paige", + "orwell", + "mainland", + "lunatic", + "lonnie", + "lotion", + "maine", + "maddux", + "qn632o", + "poophead", + "rapper", + "porn4life", + "producer", + "rapunzel", + "tracks", + "velocity", + "vanessa1", + "ulrich", + "trueblue", + "vampire1", + "abacus", + "902100", + "crispy", + "corky", + "crane", + "chooch", + "d6wnro", + "cutie", + "deal", + "dabulls", + "dehpye", + "navyseal", + "njqcw4", + "nownow", + "nigger1", + "nightowl", + "nonenone", + "nightmar", + "bustle", + "buddy2", + "boingo", + "bugman", + "bulletin", + "bosshog", + "bowie", + "hybrid", + "hillside", + "hilltop", + "hotlegs", + "honesty", + "hzze929b", + "hhhhh1", + "hellohel", + "eloise", + "evilone", + "edgewise", + "e5pftu", + "eded", + "embalmer", + "excalibur", + "elefant", + "kenzie", + "karl", + "karin", + "killah", + "kleenex", + "mouses", + "mounta1n", + "motors", + "mutley", + "muffdive", + "vivitron", + "winfield", + "wednesday", + "w00t88", + "iloveit", + "jarjar", + "incest", + "indycar", + "17171717", + "1664", + "17011701", + "222777", + "2663", + "beelch", + "benben", + "yitbos", + "yyyyy1", + "yasmin", + "zapata", + "zzzzz1", + "stooge", + "tangerin", + "taztaz", + "stewart1", + "summer69", + "sweetness", + "system1", + "surveyor", + "stirling", + "3qvqod", + "3way", + "456321", + "sizzle", + "simhrq", + "shrink", + "shawnee", + "someday", + "sparty", + "ssptx452", + "sphere", + "spark", + "slammed", + "sober", + "persian", + "peppers", + "ploppy", + "pn5jvw", + "poobear", + "pianos", + "plaster", + "testme", + "tiff", + "thriller", + "larissa", + "lennox", + "jewell", + "master12", + "messier", + "rockey", + "1229", + "1217", + "1478", + "1009", + "anastasi", + "almighty", + "amonra", + "aragon", + "argentin", + "albino", + "azazel", + "grinder", + "6uldv8", + "83y6pv", + "8888888", + "4tlved", + "515051", + "carsten", + "changes", + "flanders", + "flyers88", + "ffffff1", + "firehawk", + "foreman", + "firedog", + "flashman", + "ggggg1", + "gerber", + "godspeed", + "galway", + "giveitup", + "funtimes", + "gohan", + "giveme", + "geryfe", + "frenchie", + "sayang", + "rudeboy", + "savanna", + "sandals", + "devine", + "dougal", + "drag0n", + "dga9la", + "disaster", + "desktop", + "only", + "onlyone", + "otter", + "pandas", + "mafia", + "lombard", + "luckys", + "lovejoy", + "lovelife", + "manders", + "product", + "qqh92r", + "qcmfd454", + "pork", + "radar1", + "punani", + "ptbdhw", + "turtles", + "undertaker", + "trs8f7", + "tramp", + "ugejvp", + "abba", + "911turbo", + "acdc", + "abcd123", + "clever", + "corina", + "cristian", + "create", + "crash1", + "colony", + "crosby", + "delboy", + "daniele", + "davinci", + "daughter", + "notebook", + "niki", + "nitrox", + "borabora", + "bonzai", + "budd", + "brisbane", + "hotter", + "heeled", + "heroes", + "hooyah", + "hotgirl", + "i62gbq", + "horse1", + "hills", + "hpk2qc", + "epvjb6", + "echo", + "korean", + "kristie", + "mnbvc", + "mohammad", + "mind", + "mommy1", + "munster", + "wade", + "wiccan", + "wanted", + "jacket", + "2369", + "bettyboo", + "blondy", + "bismark", + "beanbag", + "bjhgfi", + "blackice", + "yvtte545", + "ynot", + "yess", + "zlzfrh", + "wolvie", + "007bond", + "******", + "tailgate", + "tanya1", + "sxhq65", + "stinky1", + "3234412", + "3ki42x", + "seville", + "shimmer", + "sheryl", + "sienna", + "shitshit", + "skillet", + "seaman", + "sooners1", + "solaris", + "smartass", + "pastor", + "pasta", + "pedros", + "pennywis", + "pfloyd", + "tobydog", + "thetruth", + "lethal", + "letme1n", + "leland", + "jenifer", + "mario66", + "micky", + "rocky2", + "rewq", + "ripped", + "reindeer", + "1128", + "1207", + "1104", + "1432", + "aprilia", + "allstate", + "alyson", + "bagels", + "basic", + "baggies", + "barb", + "barrage", + "greatest", + "gomez", + "guru", + "guard", + "72d5tn", + "606060", + "4wcqjn", + "caldwell", + "chance1", + "catalog", + "faust", + "film", + "flange", + "fran", + "fartman", + "geil", + "gbhcf2", + "fussball", + "glen", + "fuaqz4", + "gameboy", + "garnet", + "geneviev", + "rotary", + "seahawk", + "russel", + "saab", + "seal", + "samadams", + "devlt4", + "ditto", + "drevil", + "drinker", + "deuce", + "dipstick", + "donut", + "octopus", + "ottawa", + "losangel", + "loverman", + "porky", + "q9umoz", + "rapture", + "pump", + "pussy4me", + "university", + "triplex", + "ue8fpw", + "trent", + "trophy", + "turbos", + "troubles", + "agent", + "aaa340", + "churchil", + "crazyman", + "consult", + "creepy", + "craven", + "class", + "cutiepie", + "ddddd1", + "dejavu", + "cuxldv", + "nettie", + "nbvibt", + "nikon", + "niko", + "norwood", + "nascar1", + "nolan", + "bubba2", + "boobear", + "boogers", + "buff", + "bullwink", + "bully", + "bulldawg", + "horsemen", + "escalade", + "editor", + "eagle2", + "dynamic", + "ella", + "efyreg", + "edition", + "kidney", + "minnesot", + "mogwai", + "morrow", + "msnxbi", + "moonlight", + "mwq6qlzo", + "wars", + "werder", + "verygood", + "voodoo1", + "wheel", + "iiiiii1", + "159951", + "1624", + "1911a1", + "2244", + "bellagio", + "bedlam", + "belkin", + "bill1", + "woodrow", + "xirt2k", + "worship", + "??????", + "tanaka", + "swift", + "susieq", + "sundown", + "sukebe", + "tales", + "swifty", + "2fast4u", + "senate", + "sexe", + "sickness", + "shroom", + "shaun", + "seaweed", + "skeeter1", + "status", + "snicker", + "sorrow", + "spanky1", + "spook", + "patti", + "phaedrus", + "pilots", + "pinch", + "peddler", + "theo", + "thumper1", + "tessie", + "tiger7", + "tmjxn151", + "thematri", + "l2g7k3", + "letmeinn", + "lazy", + "jeffjeff", + "joan", + "johnmish", + "mantra", + "mariana", + "mike69", + "marshal", + "mart", + "mazda6", + "riptide", + "robots", + "rental", + "1107", + "1130", + "142857", + "11001001", + "1134", + "armored", + "alvin", + "alec", + "allnight", + "alright", + "amatuers", + "bartok", + "attorney", + "astral", + "baboon", + "bahamas", + "balls1", + "bassoon", + "hcleeb", + "happyman", + "granite", + "graywolf", + "golf1", + "gomets", + "8vjzus", + "7890", + "789123", + "8uiazp", + "5757", + "474jdvff", + "551scasi", + "50cent", + "camaro1", + "cherry1", + "chemist", + "final", + "firenze", + "fishtank", + "farrell", + "freewill", + "glendale", + "frogfrog", + "gerhardt", + "ganesh", + "same", + "scirocco", + "devilman", + "doodles", + "dinger", + "okinawa", + "olympic", + "nursing", + "orpheus", + "ohmygod", + "paisley", + "pallmall", + "null", + "lounge", + "lunchbox", + "manhatta", + "mahalo", + "mandarin", + "qwqwqw", + "qguvyt", + "pxx3eftp", + "president", + "rambler", + "puzzle", + "poppy1", + "turk182", + "trotter", + "vdlxuc", + "trish", + "tugboat", + "valiant", + "tracie", + "uwrl7c", + "chris123", + "coaster", + "cmfnpu", + "decimal", + "debbie1", + "dandy", + "daedalus", + "dede", + "natasha1", + "nissan1", + "nancy123", + "nevermin", + "napalm", + "newcastle", + "boats", + "branden", + "britt", + "bonghit", + "hester", + "ibxnsm", + "hhhhhh1", + "holger", + "durham", + "edmonton", + "erwin", + "equinox", + "dvader", + "kimmy", + "knulla", + "mustafa", + "monsoon", + "mistral", + "morgana", + "monica1", + "mojave", + "month", + "monterey", + "mrbill", + "vkaxcs", + "victor1", + "wacker", + "wendell", + "violator", + "vfdhif", + "wilson1", + "wavpzt", + "verena", + "wildstar", + "winter99", + "iqzzt580", + "jarrod", + "imback", + "1914", + "19741974", + "1monkey", + "1q2w3e4r5t", + "2500", + "2255", + "blank", + "bigshow", + "bigbucks", + "blackcoc", + "zoomer", + "wtcacq", + "wobble", + "xmen", + "xjznq5", + "yesterda", + "yhwnqc", + "zzzxxx", + "streak", + "393939", + "2fchbg", + "skinhead", + "skilled", + "shakira", + "shaft", + "shadow12", + "seaside", + "sigrid", + "sinful", + "silicon", + "smk7366", + "snapshot", + "sniper1", + "soccer11", + "staff", + "slap", + "smutty", + "peepers", + "pleasant", + "plokij", + "pdiddy", + "pimpdaddy", + "thrust", + "terran", + "topaz", + "today1", + "lionhear", + "littlema", + "lauren1", + "lincoln1", + "lgnu9d", + "laughing", + "juneau", + "methos", + "medina", + "merlyn", + "rogue1", + "romulus", + "redshift", + "1202", + "1469", + "12locked", + "arizona1", + "alfarome", + "al9agd", + "aol123", + "altec", + "apollo1", + "arse", + "baker1", + "bbb747", + "bach", + "axeman", + "astro1", + "hawthorn", + "goodfell", + "hawks1", + "gstring", + "hannes", + "8543852", + "868686", + "4ng62t", + "554uzpad", + "5401", + "567890", + "5232", + "catfood", + "frame", + "flow", + "fire1", + "flipflop", + "fffff1", + "fozzie", + "fluff", + "garrison", + "fzappa", + "furious", + "round", + "rustydog", + "sandberg", + "scarab", + "satin", + "ruger", + "samsung1", + "destin", + "diablo2", + "dreamer1", + "detectiv", + "dominick", + "doqvq3", + "drywall", + "paladin1", + "papabear", + "offroad", + "panasonic", + "nyyankee", + "luetdi", + "qcfmtz", + "pyf8ah", + "puddles", + "privacy", + "rainer", + "pussyeat", + "ralph1", + "princeto", + "trivia", + "trewq", + "tri5a3", + "advent", + "9898", + "agyvorc", + "clarkie", + "coach1", + "courier", + "contest", + "christo", + "corinna", + "chowder", + "concept", + "climbing", + "cyzkhw", + "davidb", + "dad2ownu", + "days", + "daredevi", + "de7mdf", + "nose", + "necklace", + "nazgul", + "booboo1", + "broad", + "bonzo", + "brenna", + "boot", + "butch1", + "huskers1", + "hgfdsa", + "hornyman", + "elmer", + "elektra", + "england1", + "elodie", + "kermit1", + "knife", + "kaboom", + "minute", + "modern", + "motherfucker", + "morten", + "mocha", + "monday1", + "morgoth", + "ward", + "weewee", + "weenie", + "walters", + "vorlon", + "website", + "wahoo", + "ilovegod", + "insider", + "jayman", + "1911", + "1dallas", + "1900", + "1ranger", + "201jedlz", + "2501", + "1qaz", + "bertram", + "bignuts", + "bigbad", + "beebee", + "billows", + "belize", + "bebe", + "wvj5np", + "wu4etd", + "yamaha1", + "wrinkle5", + "zebra1", + "yankee1", + "zoomzoom", + "09876543", + "0311", + "?????", + "stjabn", + "tainted", + "3tmnej", + "shoot", + "skooter", + "skelter", + "sixteen", + "starlite", + "smack", + "spice1", + "stacey1", + "smithy", + "perrin", + "pollux", + "peternorth", + "pixie", + "paulina", + "piston", + "pick", + "poets", + "pine", + "toons", + "tooth", + "topspin", + "kugm7b", + "legends", + "jeepjeep", + "juliana", + "joystick", + "junkmail", + "jojojojo", + "jonboy", + "judge", + "midland", + "meteor", + "mccabe", + "matter", + "mayfair", + "meeting", + "merrill", + "raul", + "riches", + "reznor", + "rockrock", + "reboot", + "reject", + "robyn", + "renee1", + "roadway", + "rasta220", + "1411", + "1478963", + "1019", + "archery", + "allman", + "andyandy", + "barks", + "bagpuss", + "auckland", + "gooseman", + "hazmat", + "gucci", + "guns", + "grammy", + "happydog", + "greek", + "7kbe9d", + "7676", + "6bjvpe", + "5lyedn", + "5858", + "5291", + "charlie2", + "chas", + "c7lrwu", + "candys", + "chateau", + "ccccc1", + "cardinals", + "fear", + "fihdfv", + "fortune12", + "gocats", + "gaelic", + "fwsadn", + "godboy", + "gldmeo", + "fx3tuo", + "fubar1", + "garland", + "generals", + "gforce", + "rxmtkp", + "rulz", + "sairam", + "dunhill", + "division", + "dogggg", + "detect", + "details", + "doll", + "drinks", + "ozlq6qwm", + "ov3ajy", + "lockout", + "makayla", + "macgyver", + "mallorca", + "loves", + "prima", + "pvjegu", + "qhxbij", + "raphael", + "prelude1", + "totoro", + "tusymo", + "trousers", + "tunnel", + "valeria", + "tulane", + "turtle1", + "tracy1", + "aerosmit", + "abbey1", + "address", + "clticic", + "clueless", + "cooper1", + "comets", + "collect", + "corbin", + "delpiero", + "derick", + "cyprus", + "dante1", + "dave1", + "nounours", + "neal", + "nexus6", + "nero", + "nogard", + "norfolk", + "brent1", + "booyah", + "bootleg", + "buckaroo", + "bulls23", + "bulls1", + "booper", + "heretic", + "icecube", + "hellno", + "hounds", + "honeydew", + "hooters1", + "hoes", + "howie", + "hevnm4", + "hugohugo", + "eighty", + "epson", + "evangeli", + "eeeee1", + "eyphed", + "tiwaribachjayega" +] \ No newline at end of file diff --git a/src/assets/json/email-tlds.json b/src/assets/json/email-tlds.json new file mode 100644 index 0000000..73b5df1 --- /dev/null +++ b/src/assets/json/email-tlds.json @@ -0,0 +1,1481 @@ +[ + "aaa", + "aarp", + "abarth", + "abb", + "abbott", + "abbvie", + "abc", + "able", + "abogado", + "abudhabi", + "ac", + "academy", + "accenture", + "accountant", + "accountants", + "aco", + "actor", + "ad", + "ads", + "adult", + "ae", + "aeg", + "aero", + "aetna", + "af", + "afl", + "africa", + "ag", + "agakhan", + "agency", + "ai", + "aig", + "airbus", + "airforce", + "airtel", + "akdn", + "al", + "alfaromeo", + "alibaba", + "alipay", + "allfinanz", + "allstate", + "ally", + "alsace", + "alstom", + "am", + "amazon", + "americanexpress", + "americanfamily", + "amex", + "amfam", + "amica", + "amsterdam", + "analytics", + "android", + "anquan", + "anz", + "ao", + "aol", + "apartments", + "app", + "apple", + "aq", + "aquarelle", + "ar", + "arab", + "aramco", + "archi", + "army", + "arpa", + "art", + "arte", + "as", + "asda", + "asia", + "associates", + "at", + "athleta", + "attorney", + "au", + "auction", + "audi", + "audible", + "audio", + "auspost", + "author", + "auto", + "autos", + "avianca", + "aw", + "aws", + "ax", + "axa", + "az", + "azure", + "ba", + "baby", + "baidu", + "banamex", + "bananarepublic", + "band", + "bank", + "bar", + "barcelona", + "barclaycard", + "barclays", + "barefoot", + "bargains", + "baseball", + "basketball", + "bauhaus", + "bayern", + "bb", + "bbc", + "bbt", + "bbva", + "bcg", + "bcn", + "bd", + "be", + "beats", + "beauty", + "beer", + "bentley", + "berlin", + "best", + "bestbuy", + "bet", + "bf", + "bg", + "bh", + "bharti", + "bi", + "bible", + "bid", + "bike", + "bing", + "bingo", + "bio", + "biz", + "bj", + "black", + "blackfriday", + "blockbuster", + "blog", + "bloomberg", + "blue", + "bm", + "bms", + "bmw", + "bn", + "bnpparibas", + "bo", + "boats", + "boehringer", + "bofa", + "bom", + "bond", + "boo", + "book", + "booking", + "bosch", + "bostik", + "boston", + "bot", + "boutique", + "box", + "br", + "bradesco", + "bridgestone", + "broadway", + "broker", + "brother", + "brussels", + "bs", + "bt", + "build", + "builders", + "business", + "buy", + "buzz", + "bv", + "bw", + "by", + "bz", + "bzh", + "ca", + "cab", + "cafe", + "cal", + "call", + "calvinklein", + "cam", + "camera", + "camp", + "canon", + "capetown", + "capital", + "capitalone", + "car", + "caravan", + "cards", + "care", + "career", + "careers", + "cars", + "casa", + "case", + "cash", + "casino", + "cat", + "catering", + "catholic", + "cba", + "cbn", + "cbre", + "cbs", + "cc", + "cd", + "center", + "ceo", + "cern", + "cf", + "cfa", + "cfd", + "cg", + "ch", + "chanel", + "channel", + "charity", + "chase", + "chat", + "cheap", + "chintai", + "christmas", + "chrome", + "church", + "ci", + "cipriani", + "circle", + "cisco", + "citadel", + "citi", + "citic", + "city", + "cityeats", + "ck", + "cl", + "claims", + "cleaning", + "click", + "clinic", + "clinique", + "clothing", + "cloud", + "club", + "clubmed", + "cm", + "cn", + "co", + "coach", + "codes", + "coffee", + "college", + "cologne", + "com", + "comcast", + "commbank", + "community", + "company", + "compare", + "computer", + "comsec", + "condos", + "construction", + "consulting", + "contact", + "contractors", + "cooking", + "cookingchannel", + "cool", + "coop", + "corsica", + "country", + "coupon", + "coupons", + "courses", + "cpa", + "cr", + "credit", + "creditcard", + "creditunion", + "cricket", + "crown", + "crs", + "cruise", + "cruises", + "cu", + "cuisinella", + "cv", + "cw", + "cx", + "cy", + "cymru", + "cyou", + "cz", + "dabur", + "dad", + "dance", + "data", + "date", + "dating", + "datsun", + "day", + "dclk", + "dds", + "de", + "deal", + "dealer", + "deals", + "degree", + "delivery", + "dell", + "deloitte", + "delta", + "democrat", + "dental", + "dentist", + "desi", + "design", + "dev", + "dhl", + "diamonds", + "diet", + "digital", + "direct", + "directory", + "discount", + "discover", + "dish", + "diy", + "dj", + "dk", + "dm", + "dnp", + "do", + "docs", + "doctor", + "dog", + "domains", + "dot", + "download", + "drive", + "dtv", + "dubai", + "dunlop", + "dupont", + "durban", + "dvag", + "dvr", + "dz", + "earth", + "eat", + "ec", + "eco", + "edeka", + "edu", + "education", + "ee", + "eg", + "email", + "emerck", + "energy", + "engineer", + "engineering", + "enterprises", + "epson", + "equipment", + "er", + "ericsson", + "erni", + "es", + "esq", + "estate", + "et", + "etisalat", + "eu", + "eurovision", + "eus", + "events", + "exchange", + "expert", + "exposed", + "express", + "extraspace", + "fage", + "fail", + "fairwinds", + "faith", + "family", + "fan", + "fans", + "farm", + "farmers", + "fashion", + "fast", + "fedex", + "feedback", + "ferrari", + "ferrero", + "fi", + "fiat", + "fidelity", + "fido", + "film", + "final", + "finance", + "financial", + "fire", + "firestone", + "firmdale", + "fish", + "fishing", + "fit", + "fitness", + "fj", + "fk", + "flickr", + "flights", + "flir", + "florist", + "flowers", + "fly", + "fm", + "fo", + "foo", + "food", + "foodnetwork", + "football", + "ford", + "forex", + "forsale", + "forum", + "foundation", + "fox", + "fr", + "free", + "fresenius", + "frl", + "frogans", + "frontdoor", + "frontier", + "ftr", + "fujitsu", + "fun", + "fund", + "furniture", + "futbol", + "fyi", + "ga", + "gal", + "gallery", + "gallo", + "gallup", + "game", + "games", + "gap", + "garden", + "gay", + "gb", + "gbiz", + "gd", + "gdn", + "ge", + "gea", + "gent", + "genting", + "george", + "gf", + "gg", + "ggee", + "gh", + "gi", + "gift", + "gifts", + "gives", + "giving", + "gl", + "glass", + "gle", + "global", + "globo", + "gm", + "gmail", + "gmbh", + "gmo", + "gmx", + "gn", + "godaddy", + "gold", + "goldpoint", + "golf", + "goo", + "goodyear", + "goog", + "google", + "gop", + "got", + "gov", + "gp", + "gq", + "gr", + "grainger", + "graphics", + "gratis", + "green", + "gripe", + "grocery", + "group", + "gs", + "gt", + "gu", + "guardian", + "gucci", + "guge", + "guide", + "guitars", + "guru", + "gw", + "gy", + "hair", + "hamburg", + "hangout", + "haus", + "hbo", + "hdfc", + "hdfcbank", + "health", + "healthcare", + "help", + "helsinki", + "here", + "hermes", + "hgtv", + "hiphop", + "hisamitsu", + "hitachi", + "hiv", + "hk", + "hkt", + "hm", + "hn", + "hockey", + "holdings", + "holiday", + "homedepot", + "homegoods", + "homes", + "homesense", + "honda", + "horse", + "hospital", + "host", + "hosting", + "hot", + "hoteles", + "hotels", + "hotmail", + "house", + "how", + "hr", + "hsbc", + "ht", + "hu", + "hughes", + "hyatt", + "hyundai", + "ibm", + "icbc", + "ice", + "icu", + "id", + "ie", + "ieee", + "ifm", + "ikano", + "il", + "im", + "imamat", + "imdb", + "immo", + "immobilien", + "in", + "inc", + "industries", + "infiniti", + "info", + "ing", + "ink", + "institute", + "insurance", + "insure", + "int", + "international", + "intuit", + "investments", + "io", + "ipiranga", + "iq", + "ir", + "irish", + "is", + "ismaili", + "ist", + "istanbul", + "it", + "itau", + "itv", + "jaguar", + "java", + "jcb", + "je", + "jeep", + "jetzt", + "jewelry", + "jio", + "jll", + "jm", + "jmp", + "jnj", + "jo", + "jobs", + "joburg", + "jot", + "joy", + "jp", + "jpmorgan", + "jprs", + "juegos", + "juniper", + "kaufen", + "kddi", + "ke", + "kerryhotels", + "kerrylogistics", + "kerryproperties", + "kfh", + "kg", + "kh", + "ki", + "kia", + "kids", + "kim", + "kinder", + "kindle", + "kitchen", + "kiwi", + "km", + "kn", + "koeln", + "komatsu", + "kosher", + "kp", + "kpmg", + "kpn", + "kr", + "krd", + "kred", + "kuokgroup", + "kw", + "ky", + "kyoto", + "kz", + "la", + "lacaixa", + "lamborghini", + "lamer", + "lancaster", + "lancia", + "land", + "landrover", + "lanxess", + "lasalle", + "lat", + "latino", + "latrobe", + "law", + "lawyer", + "lb", + "lc", + "lds", + "lease", + "leclerc", + "lefrak", + "legal", + "lego", + "lexus", + "lgbt", + "li", + "lidl", + "life", + "lifeinsurance", + "lifestyle", + "lighting", + "like", + "lilly", + "limited", + "limo", + "lincoln", + "link", + "lipsy", + "live", + "living", + "lk", + "llc", + "llp", + "loan", + "loans", + "locker", + "locus", + "lol", + "london", + "lotte", + "lotto", + "love", + "lpl", + "lplfinancial", + "lr", + "ls", + "lt", + "ltd", + "ltda", + "lu", + "lundbeck", + "luxe", + "luxury", + "lv", + "ly", + "ma", + "madrid", + "maif", + "maison", + "makeup", + "man", + "management", + "mango", + "map", + "market", + "marketing", + "markets", + "marriott", + "marshalls", + "maserati", + "mattel", + "mba", + "mc", + "mckinsey", + "md", + "me", + "med", + "media", + "meet", + "melbourne", + "meme", + "memorial", + "men", + "menu", + "merckmsd", + "mg", + "mh", + "miami", + "microsoft", + "mil", + "mini", + "mint", + "mit", + "mitsubishi", + "mk", + "ml", + "mlb", + "mls", + "mm", + "mma", + "mn", + "mo", + "mobi", + "mobile", + "moda", + "moe", + "moi", + "mom", + "monash", + "money", + "monster", + "mormon", + "mortgage", + "moscow", + "moto", + "motorcycles", + "mov", + "movie", + "mp", + "mq", + "mr", + "ms", + "msd", + "mt", + "mtn", + "mtr", + "mu", + "museum", + "music", + "mutual", + "mv", + "mw", + "mx", + "my", + "mz", + "na", + "nab", + "nagoya", + "name", + "natura", + "navy", + "nba", + "nc", + "ne", + "nec", + "net", + "netbank", + "netflix", + "network", + "neustar", + "new", + "news", + "next", + "nextdirect", + "nexus", + "nf", + "nfl", + "ng", + "ngo", + "nhk", + "ni", + "nico", + "nike", + "nikon", + "ninja", + "nissan", + "nissay", + "nl", + "no", + "nokia", + "northwesternmutual", + "norton", + "now", + "nowruz", + "nowtv", + "np", + "nr", + "nra", + "nrw", + "ntt", + "nu", + "nyc", + "nz", + "obi", + "observer", + "office", + "okinawa", + "olayan", + "olayangroup", + "oldnavy", + "ollo", + "om", + "omega", + "one", + "ong", + "onl", + "online", + "ooo", + "open", + "oracle", + "orange", + "org", + "organic", + "origins", + "osaka", + "otsuka", + "ott", + "ovh", + "pa", + "page", + "panasonic", + "paris", + "pars", + "partners", + "parts", + "party", + "passagens", + "pay", + "pccw", + "pe", + "pet", + "pf", + "pfizer", + "pg", + "ph", + "pharmacy", + "phd", + "philips", + "phone", + "photo", + "photography", + "photos", + "physio", + "pics", + "pictet", + "pictures", + "pid", + "pin", + "ping", + "pink", + "pioneer", + "pizza", + "pk", + "pl", + "place", + "play", + "playstation", + "plumbing", + "plus", + "pm", + "pn", + "pnc", + "pohl", + "poker", + "politie", + "porn", + "post", + "pr", + "pramerica", + "praxi", + "press", + "prime", + "pro", + "prod", + "productions", + "prof", + "progressive", + "promo", + "properties", + "property", + "protection", + "pru", + "prudential", + "ps", + "pt", + "pub", + "pw", + "pwc", + "py", + "qa", + "qpon", + "quebec", + "quest", + "racing", + "radio", + "re", + "read", + "realestate", + "realtor", + "realty", + "recipes", + "red", + "redstone", + "redumbrella", + "rehab", + "reise", + "reisen", + "reit", + "reliance", + "ren", + "rent", + "rentals", + "repair", + "report", + "republican", + "rest", + "restaurant", + "review", + "reviews", + "rexroth", + "rich", + "richardli", + "ricoh", + "ril", + "rio", + "rip", + "ro", + "rocher", + "rocks", + "rodeo", + "rogers", + "room", + "rs", + "rsvp", + "ru", + "rugby", + "ruhr", + "run", + "rw", + "rwe", + "ryukyu", + "sa", + "saarland", + "safe", + "safety", + "sakura", + "sale", + "salon", + "samsclub", + "samsung", + "sandvik", + "sandvikcoromant", + "sanofi", + "sap", + "sarl", + "sas", + "save", + "saxo", + "sb", + "sbi", + "sbs", + "sc", + "sca", + "scb", + "schaeffler", + "schmidt", + "scholarships", + "school", + "schule", + "schwarz", + "science", + "scot", + "sd", + "se", + "search", + "seat", + "secure", + "security", + "seek", + "select", + "sener", + "services", + "seven", + "sew", + "sex", + "sexy", + "sfr", + "sg", + "sh", + "shangrila", + "sharp", + "shaw", + "shell", + "shia", + "shiksha", + "shoes", + "shop", + "shopping", + "shouji", + "show", + "showtime", + "si", + "silk", + "sina", + "singles", + "site", + "sj", + "sk", + "ski", + "skin", + "sky", + "skype", + "sl", + "sling", + "sm", + "smart", + "smile", + "sn", + "sncf", + "so", + "soccer", + "social", + "softbank", + "software", + "sohu", + "solar", + "solutions", + "song", + "sony", + "soy", + "spa", + "space", + "sport", + "spot", + "sr", + "srl", + "ss", + "st", + "stada", + "staples", + "star", + "statebank", + "statefarm", + "stc", + "stcgroup", + "stockholm", + "storage", + "store", + "stream", + "studio", + "study", + "style", + "su", + "sucks", + "supplies", + "supply", + "support", + "surf", + "surgery", + "suzuki", + "sv", + "swatch", + "swiss", + "sx", + "sy", + "sydney", + "systems", + "sz", + "tab", + "taipei", + "talk", + "taobao", + "target", + "tatamotors", + "tatar", + "tattoo", + "tax", + "taxi", + "tc", + "tci", + "td", + "tdk", + "team", + "tech", + "technology", + "tel", + "temasek", + "tennis", + "teva", + "tf", + "tg", + "th", + "thd", + "theater", + "theatre", + "tiaa", + "tickets", + "tienda", + "tiffany", + "tips", + "tires", + "tirol", + "tj", + "tjmaxx", + "tjx", + "tk", + "tkmaxx", + "tl", + "tm", + "tmall", + "tn", + "to", + "today", + "tokyo", + "tools", + "top", + "toray", + "toshiba", + "total", + "tours", + "town", + "toyota", + "toys", + "tr", + "trade", + "trading", + "training", + "travel", + "travelchannel", + "travelers", + "travelersinsurance", + "trust", + "trv", + "tt", + "tube", + "tui", + "tunes", + "tushu", + "tv", + "tvs", + "tw", + "tz", + "ua", + "ubank", + "ubs", + "ug", + "uk", + "unicom", + "university", + "uno", + "uol", + "ups", + "us", + "uy", + "uz", + "va", + "vacations", + "vana", + "vanguard", + "vc", + "ve", + "vegas", + "ventures", + "verisign", + "versicherung", + "vet", + "vg", + "vi", + "viajes", + "video", + "vig", + "viking", + "villas", + "vin", + "vip", + "virgin", + "visa", + "vision", + "viva", + "vivo", + "vlaanderen", + "vn", + "vodka", + "volkswagen", + "volvo", + "vote", + "voting", + "voto", + "voyage", + "vu", + "vuelos", + "wales", + "walmart", + "walter", + "wang", + "wanggou", + "watch", + "watches", + "weather", + "weatherchannel", + "webcam", + "weber", + "website", + "wed", + "wedding", + "weibo", + "weir", + "wf", + "whoswho", + "wien", + "wiki", + "williamhill", + "win", + "windows", + "wine", + "winners", + "wme", + "wolterskluwer", + "woodside", + "work", + "works", + "world", + "wow", + "ws", + "wtc", + "wtf", + "xbox", + "xerox", + "xfinity", + "xihuan", + "xin", + "xn--11b4c3d", + "xn--1ck2e1b", + "xn--1qqw23a", + "xn--2scrj9c", + "xn--30rr7y", + "xn--3bst00m", + "xn--3ds443g", + "xn--3e0b707e", + "xn--3hcrj9c", + "xn--3pxu8k", + "xn--42c2d9a", + "xn--45br5cyl", + "xn--45brj9c", + "xn--45q11c", + "xn--4dbrk0ce", + "xn--4gbrim", + "xn--54b7fta0cc", + "xn--55qw42g", + "xn--55qx5d", + "xn--5su34j936bgsg", + "xn--5tzm5g", + "xn--6frz82g", + "xn--6qq986b3xl", + "xn--80adxhks", + "xn--80ao21a", + "xn--80aqecdr1a", + "xn--80asehdb", + "xn--80aswg", + "xn--8y0a063a", + "xn--90a3ac", + "xn--90ae", + "xn--90ais", + "xn--9dbq2a", + "xn--9et52u", + "xn--9krt00a", + "xn--b4w605ferd", + "xn--bck1b9a5dre4c", + "xn--c1avg", + "xn--c2br7g", + "xn--cck2b3b", + "xn--cckwcxetd", + "xn--cg4bki", + "xn--clchc0ea0b2g2a9gcd", + "xn--czr694b", + "xn--czrs0t", + "xn--czru2d", + "xn--d1acj3b", + "xn--d1alf", + "xn--e1a4c", + "xn--eckvdtc9d", + "xn--efvy88h", + "xn--fct429k", + "xn--fhbei", + "xn--fiq228c5hs", + "xn--fiq64b", + "xn--fiqs8s", + "xn--fiqz9s", + "xn--fjq720a", + "xn--flw351e", + "xn--fpcrj9c3d", + "xn--fzc2c9e2c", + "xn--fzys8d69uvgm", + "xn--g2xx48c", + "xn--gckr3f0f", + "xn--gecrj9c", + "xn--gk3at1e", + "xn--h2breg3eve", + "xn--h2brj9c", + "xn--h2brj9c8c", + "xn--hxt814e", + "xn--i1b6b1a6a2e", + "xn--imr513n", + "xn--io0a7i", + "xn--j1aef", + "xn--j1amh", + "xn--j6w193g", + "xn--jlq480n2rg", + "xn--jvr189m", + "xn--kcrx77d1x4a", + "xn--kprw13d", + "xn--kpry57d", + "xn--kput3i", + "xn--l1acc", + "xn--lgbbat1ad8j", + "xn--mgb9awbf", + "xn--mgba3a3ejt", + "xn--mgba3a4f16a", + "xn--mgba7c0bbn0a", + "xn--mgbaakc7dvf", + "xn--mgbaam7a8h", + "xn--mgbab2bd", + "xn--mgbah1a3hjkrd", + "xn--mgbai9azgqp6j", + "xn--mgbayh7gpa", + "xn--mgbbh1a", + "xn--mgbbh1a71e", + "xn--mgbc0a9azcg", + "xn--mgbca7dzdo", + "xn--mgbcpq6gpa1a", + "xn--mgberp4a5d4ar", + "xn--mgbgu82a", + "xn--mgbi4ecexp", + "xn--mgbpl2fh", + "xn--mgbt3dhd", + "xn--mgbtx2b", + "xn--mgbx4cd0ab", + "xn--mix891f", + "xn--mk1bu44c", + "xn--mxtq1m", + "xn--ngbc5azd", + "xn--ngbe9e0a", + "xn--ngbrx", + "xn--node", + "xn--nqv7f", + "xn--nqv7fs00ema", + "xn--nyqy26a", + "xn--o3cw4h", + "xn--ogbpf8fl", + "xn--otu796d", + "xn--p1acf", + "xn--p1ai", + "xn--pgbs0dh", + "xn--pssy2u", + "xn--q7ce6a", + "xn--q9jyb4c", + "xn--qcka1pmc", + "xn--qxa6a", + "xn--qxam", + "xn--rhqv96g", + "xn--rovu88b", + "xn--rvc1e0am3e", + "xn--s9brj9c", + "xn--ses554g", + "xn--t60b56a", + "xn--tckwe", + "xn--tiq49xqyj", + "xn--unup4y", + "xn--vermgensberater-ctb", + "xn--vermgensberatung-pwb", + "xn--vhquv", + "xn--vuq861b", + "xn--w4r85el8fhu5dnra", + "xn--w4rs40l", + "xn--wgbh1c", + "xn--wgbl6a", + "xn--xhq521b", + "xn--xkc2al3hye2a", + "xn--xkc2dl3a5ee0h", + "xn--y9a3aq", + "xn--yfro4i67o", + "xn--ygbi2ammx", + "xn--zfr164b", + "xxx", + "xyz", + "yachts", + "yahoo", + "yamaxun", + "yandex", + "ye", + "yodobashi", + "yoga", + "yokohama", + "you", + "youtube", + "yt", + "yun", + "za", + "zappos", + "zara", + "zero", + "zip", + "zm", + "zone", + "zuerich", + "zw" +] \ No newline at end of file diff --git a/src/assets/logo.svg b/src/assets/logo.svg new file mode 100644 index 0000000..efa38e5 --- /dev/null +++ b/src/assets/logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/assets/logout.svg b/src/assets/logout.svg new file mode 100644 index 0000000..2bf0613 --- /dev/null +++ b/src/assets/logout.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/mail-one.svg b/src/assets/mail-one.svg new file mode 100644 index 0000000..bf24bd8 --- /dev/null +++ b/src/assets/mail-one.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/minus.svg b/src/assets/minus.svg new file mode 100644 index 0000000..d4a3cad --- /dev/null +++ b/src/assets/minus.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/pencil.svg b/src/assets/pencil.svg new file mode 100644 index 0000000..4c012a3 --- /dev/null +++ b/src/assets/pencil.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/plus.svg b/src/assets/plus.svg new file mode 100644 index 0000000..48cb7d3 --- /dev/null +++ b/src/assets/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/settings-two.svg b/src/assets/settings-two.svg new file mode 100644 index 0000000..e0096b0 --- /dev/null +++ b/src/assets/settings-two.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/share-one.svg b/src/assets/share-one.svg new file mode 100644 index 0000000..28462b1 --- /dev/null +++ b/src/assets/share-one.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/star.svg b/src/assets/star.svg new file mode 100644 index 0000000..c202887 --- /dev/null +++ b/src/assets/star.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/trash-two.svg b/src/assets/trash-two.svg new file mode 100644 index 0000000..d94f1c6 --- /dev/null +++ b/src/assets/trash-two.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/user-circle.svg b/src/assets/user-circle.svg new file mode 100644 index 0000000..3ccbe55 --- /dev/null +++ b/src/assets/user-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/user-square.svg b/src/assets/user-square.svg new file mode 100644 index 0000000..77d8edb --- /dev/null +++ b/src/assets/user-square.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/users-one.svg b/src/assets/users-one.svg new file mode 100644 index 0000000..d0f3bfe --- /dev/null +++ b/src/assets/users-one.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/authContext.jsx b/src/authContext.jsx new file mode 100644 index 0000000..6ae9b33 --- /dev/null +++ b/src/authContext.jsx @@ -0,0 +1,177 @@ +import React, { useReducer, useState } from "react"; +import MkdSDK from "@/utils/MkdSDK"; + +export const AuthContext = React.createContext({ state: {} }); + +const initialState = { + isAuthenticated: false, + user: null, + token: null, + role: null, + originalRole: null, + sessionExpired: false, + allowCheckVerification: false, +}; + +const reducer = (state, action) => { + switch (action.type) { + case "LOGIN": + localStorage.setItem("user", Number(action.payload.user_id)); + localStorage.setItem("token", action.payload.token ?? action.payload.access_token); + localStorage.setItem("role", action.payload.role); + localStorage.setItem("originalRole", ((action.payload.originalRole === undefined) || action.payload.originalRole === "undefined") ? "customer" : action.payload.originalRole); + return { + ...state, + isAuthenticated: true, + user: Number(localStorage.getItem("user")), + token: localStorage.getItem("token"), + role: localStorage.getItem("role"), + originalRole: localStorage.getItem("originalRole"), + }; + case "LOGOUT": + localStorage.removeItem("user"); + localStorage.removeItem("token"); + return { + ...state, + isAuthenticated: false, + user: null, + sessionExpired: false, + role: null, + originalRole: null, + }; + case "SESSION_EXPIRED": + return { + ...state, + sessionExpired: true, + }; + case "SWITCH_TO_HOST": + localStorage.setItem("role", "host"); + return { + ...state, + role: "host", + }; + case "SWITCH_TO_CUSTOMER": + localStorage.setItem("role", "customer"); + return { + ...state, + role: "customer", + }; + case "SWITCH_TO_ADMIN": + localStorage.setItem("role", "admin"); + return { + ...state, + role: "admin", + }; + case "ALLOW_CHECK_VERIFICATION": + return { + ...state, + allowCheckVerification: true, + }; + case "DISALLOW_CHECK_VERIFICATION": + return { + ...state, + allowCheckVerification: false, + }; + default: + return state; + } +}; + +let sdk = new MkdSDK(); + +export const tokenExpireError = (dispatch, errorMessage) => { + /** + * either this or we pass the role as a parameter + */ + const role = localStorage.getItem("role"); + if (errorMessage === "TOKEN_EXPIRED") { + dispatch({ type: "SESSION_EXPIRED" }); + } +}; + +const AuthProvider = ({ children }) => { + const [state, dispatch] = useReducer(reducer, initialState); + const [loading, setLoading] = useState(true); + + React.useEffect(() => { + const user = localStorage.getItem("user"); + const token = localStorage.getItem("token"); + const role = localStorage.getItem("role"); + const originalRole = localStorage.getItem("originalRole"); + + if (!token) { + setLoading(false); + return; + } + (async function () { + setLoading(true); + try { + await sdk.check(originalRole); + dispatch({ + type: "LOGIN", + payload: { + user_id: user, + token, + role: role, + originalRole: originalRole, + }, + }); + } catch (error) { + if (role) { + dispatch({ + type: "LOGOUT", + }); + window.location.href = "/" + role + "/login"; + } else { + dispatch({ + type: "LOGOUT", + }); + window.location.href = "/"; + } + } + setLoading(false); + })(); + }, []); + + if (loading) return
+
+ + + + + +
+
; + + return ( + + {children} + + ); +}; + +export default AuthProvider; diff --git a/src/components/AddButton.jsx b/src/components/AddButton.jsx new file mode 100644 index 0000000..839ed99 --- /dev/null +++ b/src/components/AddButton.jsx @@ -0,0 +1,18 @@ +import { PlusCircleIcon } from "@heroicons/react/24/outline"; +import React from "react"; +import { NavLink } from "react-router-dom"; +const AddButton = ({ link, text }) => { + return ( + <> + + + {text ? text : ""} + + + ); +}; + +export default AddButton; diff --git a/src/components/AdminHeader.jsx b/src/components/AdminHeader.jsx new file mode 100644 index 0000000..c50f44a --- /dev/null +++ b/src/components/AdminHeader.jsx @@ -0,0 +1,207 @@ +import React from "react"; +import { NavLink, useNavigate } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { useEffect } from "react"; +import MkdSDK from "@/utils/MkdSDK"; +import { NOTIFICATION_STATUS } from "@/utils/constants"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import { useContext } from "react"; +import adminNavigationItems from "@/utils/adminNavigationItems"; +import { ChevronDownIcon } from "@heroicons/react/24/solid"; +import { ArrowLeftOnRectangleIcon, ArrowsRightLeftIcon } from "@heroicons/react/24/outline"; +import LogoIcon from "./Icons/LogoIcon"; + +export const AdminHeader = () => { + const { state, dispatch: globalDispatch } = React.useContext(GlobalContext); + const { dispatch, state: authState } = useContext(AuthContext); + const navigate = useNavigate(); + + async function fetchNotificationCount() { + const sdk = new MkdSDK(); + sdk.setTable("notification"); + + try { + const result = await sdk.callRestAPI({ payload: { status: NOTIFICATION_STATUS.NOT_ADDRESSED } }, "GETALL"); + const g = result?.list?.filter((not) => Number(not?.status) == 0) + + globalDispatch({ type: "SET_NOTIFICATION_COUNT", payload: g.length }); + } catch (err) { + showToast(globalDispatch, err.message); + tokenExpireError(dispatch, err.message); + } + } + + function switchToHost() { + dispatch({ type: "SWITCH_TO_HOST" }); + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Success", + message: `You are now signed in as a host`, + btn: "Ok got it", + }, + }); + navigate("/"); + } + + function switchToCustomer() { + dispatch({ type: "SWITCH_TO_CUSTOMER" }); + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Success", + message: `You are now signed in as a customer`, + btn: "Ok got it", + }, + }); + navigate("/"); + } + + useEffect(() => { + let interval = setInterval(() => { + fetchNotificationCount(); + }, 10000); + return () => clearInterval(interval); + }, []); + + return ( + <> +
+
+
+
+ +
+
+ +
+ +
+
+
+ + ); +}; + +export default AdminHeader; diff --git a/src/components/Billing/AddCardMethodModal.jsx b/src/components/Billing/AddCardMethodModal.jsx new file mode 100644 index 0000000..74f5055 --- /dev/null +++ b/src/components/Billing/AddCardMethodModal.jsx @@ -0,0 +1,59 @@ +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { Dialog, Transition } from "@headlessui/react"; +import { CardCvcElement, CardExpiryElement, CardNumberElement, useElements, useStripe } from "@stripe/react-stripe-js"; +import React, { Fragment, useState } from "react"; +import { useContext } from "react"; +import { LoadingButton } from "../frontend"; + +export default function AddCardMethodModal({ modalOpen, closeModal, onSuccess }) { + const [loading, setLoading] = useState(false); + const stripe = useStripe(); + const elements = useElements(); + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const sdk = new MkdSDK(); + const [ctrl] = useState(new AbortController()); + + const addNewCard = async (e) => { + setLoading(true); + e.preventDefault(); + // create stripe token + try { + const cardNum = elements.getElement("cardNumber"); + const result = await stripe.createToken(cardNum).then(async (r) => { + if (r.error) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + message: r.error?.message ? r.error?.message : r?.trace?.raw?.message, + }, + }); + } else { + await sdk.createCustomerStripeCard({ sourceToken: r ? r.token.id : result.token.id }, ctrl.signal); + closeModal(); + onSuccess(); + } + } + ); + } catch (error) { + if (error.name == "AbortError") { + setLoading(false); + return; + } + console.log(error) + globalDispatch({ + type: "SHOW_ERROR", + payload: { + message: error?.message ? error?.message : "Declined", + }, + }); + } + setLoading(false); + }; + + return ( +
+ {/* Add Modal UI here to allow card to be created */} +
+ ); +} diff --git a/src/components/Billing/DeleteCardMethodModal.jsx b/src/components/Billing/DeleteCardMethodModal.jsx new file mode 100644 index 0000000..20bab45 --- /dev/null +++ b/src/components/Billing/DeleteCardMethodModal.jsx @@ -0,0 +1,113 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment, useContext, useState } from "react"; +import { LoadingButton } from "../frontend"; + +export default function DeleteCardMethodModal({ modalOpen, closeModal, onSuccess, card }) { + const [loading, setLoading] = useState(false); + const { dispatch } = useContext(AuthContext); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const sdk = new MkdSDK(); + const [ctrl] = useState(new AbortController()); + + async function onSubmit(e) { + setLoading(true); + e.preventDefault(); + try { + await sdk.deleteCustomerStripeCard(card.id, ctrl.signal); + closeModal(); + onSuccess(); + } catch (err) { + if (err.name == "AbortError") { + setLoading(false); + return; + } + tokenExpireError(dispatch, err.message); + globalDispatch({ type: "SHOW_ERROR", payload: { heading: "Card Deletion Failed", message: err.message } }); + } + setLoading(false); + } + + return ( + + + +
+ + +
+
+ + +
+ + Delete Payment Method + + +
+

You are about to remove card ending with {card.last4}

+
+ + + Yes, remove + +
+
+
+
+
+
+
+ ); +} diff --git a/src/components/Button.jsx b/src/components/Button.jsx new file mode 100644 index 0000000..586568d --- /dev/null +++ b/src/components/Button.jsx @@ -0,0 +1,10 @@ +import React from 'react' + + +const Button = ({ text, setResetClicked }) => { + return () +} + +export default Button \ No newline at end of file diff --git a/src/components/CaretDownIcon.jsx b/src/components/CaretDownIcon.jsx new file mode 100644 index 0000000..0fe203c --- /dev/null +++ b/src/components/CaretDownIcon.jsx @@ -0,0 +1,18 @@ +import React from "react"; + +export default function CaretDownIcon() { + return ( + + + + ); +} diff --git a/src/components/CaretUpIcon.jsx b/src/components/CaretUpIcon.jsx new file mode 100644 index 0000000..9e3f66e --- /dev/null +++ b/src/components/CaretUpIcon.jsx @@ -0,0 +1,18 @@ +import React from "react"; + +export default function CaretUpIcon() { + return ( + + + + ); +} diff --git a/src/components/ConfirmationModal.jsx b/src/components/ConfirmationModal.jsx new file mode 100644 index 0000000..2222e6f --- /dev/null +++ b/src/components/ConfirmationModal.jsx @@ -0,0 +1,37 @@ +import { GlobalContext } from "@/globalContext"; +import React from "react"; +import { useContext } from "react"; +import GreenCheckIcon from "./frontend/icons/GreenCheckIcon"; + +export default function ConfirmationModal() { + const { state, dispatch } = useContext(GlobalContext); + + if (!state.confirmation) return null; + + return ( +
+
e.stopPropagation()} + > +

+ + {state.confirmationHeading} +

+

{state.confirmationMsg}

+ +
+
+ ); +} diff --git a/src/components/CounterV2.jsx b/src/components/CounterV2.jsx new file mode 100644 index 0000000..f590e0a --- /dev/null +++ b/src/components/CounterV2.jsx @@ -0,0 +1,30 @@ +import React from "react"; +import { useController } from "react-hook-form"; + +export default function CounterV2({ setValue, name, maxCount, minCount, control }) { + const { field, fieldState, formState } = useController({ control, name }); + + return ( +
+ + {field.value} + +
+ ); +} diff --git a/src/components/CustomComboBox.jsx b/src/components/CustomComboBox.jsx new file mode 100644 index 0000000..e619e55 --- /dev/null +++ b/src/components/CustomComboBox.jsx @@ -0,0 +1,63 @@ +import { Combobox, Transition } from "@headlessui/react"; +import React, { Fragment } from "react"; +import { useController } from "react-hook-form"; + +export default function CustomComboBox({ control, name, setValue, containerClassName, valueField, labelField, items, ...restProps }) { + const { field, fieldState, formState } = useController({ control, name }); + + const filteredItems = + field.value === "" + ? items + : items + .filter((item) => item[labelField].toLowerCase().replace(/\s+/g, "").includes(field.value.toLowerCase().replace(/\s+/g, ""))) + .sort((a, b) => { + if (a[labelField].toLowerCase().indexOf(field.value.toLowerCase()) > b[labelField].toLowerCase().indexOf(field.value.toLowerCase())) { + return 1; + } else if (a[labelField].toLowerCase().indexOf(field.value.toLowerCase()) < b[labelField].toLowerCase().indexOf(field.value.toLowerCase())) { + return -1; + } else { + if (a[labelField] > b[labelField]) return 1; + else return -1; + } + }); + + return ( + + + + 0 ? "py-2 shadow-lg ring-1" : "" + }`} + > + {filteredItems.map((item, idx) => ( + + {item[labelField]} + + ))} + + + + ); +} diff --git a/src/components/CustomComboBoxV2.jsx b/src/components/CustomComboBoxV2.jsx new file mode 100644 index 0000000..e42f7fa --- /dev/null +++ b/src/components/CustomComboBoxV2.jsx @@ -0,0 +1,75 @@ +import { Combobox, Transition } from "@headlessui/react"; +import React, { Fragment, useEffect, useState } from "react"; +import { useController } from "react-hook-form"; + +export default function CustomComboBoxV2({ control, name, setValue, className, valueField, labelField, getItems, ...restProps }) { + const { field, fieldState, formState } = useController({ control, name }); + const [items, setItems] = useState([]); + const [query, setQuery] = useState(""); + const [selected, setSelected] = useState({}); + + useEffect(() => { + getItems("", setItems, field.value); + }, [field.value]); + + useEffect(() => { + if (selected[labelField]) { + setQuery(selected[labelField]); + } + }, [selected[valueField]]); + + useEffect(() => { + setSelected(items.find((item) => item[valueField] == field.value) ?? {}); + }, [field.value, items.length]); + + return ( + + { + setQuery(e.target.value); + if (e.target.value.trim() == "") { + setValue(""); + } + getItems(e.target.value, setItems, field.value); + }} + value={query} + onBlur={field.onBlur} + ref={field.ref} + name={field.name} + autoComplete="off" + /> + + 0 ? "py-2 shadow-lg ring-1" : "" + }`} + > + {items.map((item, idx) => ( + + {item[labelField]} + + ))} + + + + ); +} diff --git a/src/components/CustomLocationAutoCompleteV2.jsx b/src/components/CustomLocationAutoCompleteV2.jsx new file mode 100644 index 0000000..2b3eff9 --- /dev/null +++ b/src/components/CustomLocationAutoCompleteV2.jsx @@ -0,0 +1,105 @@ +import { Combobox, Transition } from "@headlessui/react"; +import React, { Fragment } from "react"; +import usePlacesService from "react-google-autocomplete/lib/usePlacesAutocompleteService"; +import { useController } from "react-hook-form"; +import LocationIcon from "./frontend/icons/LocationIcon"; + +export default function CustomLocationAutoCompleteV2({ type, control, name, setValue, onClear, className, containerClassName, hideIcons, suggestionType, ...restProps }) { + const { field } = useController({ control, name }); + + const { placePredictions, getPlacePredictions, isPlacePredictionsLoading } = usePlacesService({ + apiKey: import.meta.env.VITE_GOOGLE_API_KEY, + options: { types: suggestionType ?? ["(region)"] }, + debounce: 200, + }); + + return ( + + {!hideIcons && } + + { + field.onChange(evt); + getPlacePredictions({ input: evt.target.value }); + }} + /> + {!hideIcons && field.value && ( + + )} + + {isPlacePredictionsLoading ? ( +
+ + + + + +
+ ) : ( + 0 ? "py-2 shadow-lg ring-1" : "" + } absolute left-0 right-0 top-full z-50 mt-2 w-full origin-top cursor-pointer divide-y divide-gray-100 rounded-xl bg-white ring-black ring-opacity-5 focus:outline-none`} + > + {placePredictions.map((place, idx) => ( + setValue(place?.structured_formatting.main_text + ', ' + place.structured_formatting?.secondary_text)} + > + {`${place.structured_formatting.main_text} ${place.structured_formatting?.secondary_text ? "," : ""} ${place.structured_formatting?.secondary_text ? place.structured_formatting?.secondary_text : ""}`} + + ))} + + )} +
+
+ ); +} diff --git a/src/components/CustomSelectV2.jsx b/src/components/CustomSelectV2.jsx new file mode 100644 index 0000000..927758f --- /dev/null +++ b/src/components/CustomSelectV2.jsx @@ -0,0 +1,59 @@ +import { Fragment, useEffect, useState } from "react"; +import { Listbox, Transition } from "@headlessui/react"; +import { useController } from "react-hook-form"; +import { ChevronUpDownIcon } from "@heroicons/react/24/solid"; + +export default function CustomSelectV2({ control, name, containerClassName, items, labelField, valueField, placeholder, shouldUnregister, ...restProps }) { + const { field, fieldState } = useController({ control, name, shouldUnregister: shouldUnregister ?? true }); + const [dropdownOpen, setDropdownOpen] = useState(false); + const selected = items.find((item) => item[valueField] === (typeof field.value !== "number" ? field.value : +field.value)); + const defaultImage = items.find((item) => item["type"] === "1"); + + return ( +
+ + + {selected ? selected[labelField] : defaultImage === undefined ? placeholder : defaultImage["name"]} + + + + setDropdownOpen(true)} + afterLeave={() => setDropdownOpen(false)} + > + + {items.map((item, idx) => ( + + {item[labelField]} + + ))} + + + +
+ ); +} diff --git a/src/components/CustomStaticLocationAutoCompleteV2.jsx b/src/components/CustomStaticLocationAutoCompleteV2.jsx new file mode 100644 index 0000000..0712fdd --- /dev/null +++ b/src/components/CustomStaticLocationAutoCompleteV2.jsx @@ -0,0 +1,109 @@ +import { Combobox, Transition } from "@headlessui/react"; +import React, { Fragment, useContext, useState } from "react"; +import usePlacesService from "react-google-autocomplete/lib/usePlacesAutocompleteService"; +import { useController } from "react-hook-form"; +import LocationIcon from "./frontend/icons/LocationIcon"; +import { GlobalContext } from "@/globalContext"; + +export default function CustomStaticLocationAutoCompleteV2({ type, control, name, setValue, onClear, className, containerClassName, hideIcons, suggestionType, ...restProps }) { + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const [location, setLocation] = useState(globalState.location); + + + const { placePredictions, getPlacePredictions, isPlacePredictionsLoading } = usePlacesService({ + apiKey: import.meta.env.VITE_GOOGLE_API_KEY, + options: { types: suggestionType ?? ["(region)"] }, + debounce: 200, + }); + return ( + + {!hideIcons && } + + { + setLocation(evt.target.value) + getPlacePredictions({ input: evt.target.value }); + }} + /> + {!hideIcons && globalState.location && ( + + )} + + {isPlacePredictionsLoading ? ( +
+ + + + + +
+ ) : ( + 0 ? "py-2 shadow-lg ring-1" : "" + } absolute left-0 right-0 top-full z-50 mt-2 w-full origin-top cursor-pointer divide-y divide-gray-100 rounded-xl bg-white ring-black ring-opacity-5 focus:outline-none`} + > + {placePredictions.map((place, idx) => ( + + setValue(place?.structured_formatting.main_text + ', ' + place.structured_formatting?.secondary_text) + } + > + {`${place.structured_formatting.main_text} ${place.structured_formatting?.secondary_text ? "," : ""} ${place.structured_formatting?.secondary_text ? place.structured_formatting?.secondary_text : ""}`} + + ))} + + )} +
+
+ ); +} diff --git a/src/components/CustomerGettingStartedTour.jsx b/src/components/CustomerGettingStartedTour.jsx new file mode 100644 index 0000000..c72e1cf --- /dev/null +++ b/src/components/CustomerGettingStartedTour.jsx @@ -0,0 +1,130 @@ +import { AuthContext } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment, useContext, useEffect, useState } from "react"; +import { useLocation, useNavigate } from "react-router"; +import { Link } from "react-router-dom"; +import { useTour } from "@reactour/tour"; + +export default function CustomerGettingStartedTour() { + const navigate = useNavigate(); + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const { dispatch } = useContext(AuthContext); + const [modalOpen, setModalOpen] = useState(true); + const [gettingStarted, setGettingStarted] = useState(); + const { pathname } = useLocation(); + const sdk = new MkdSDK(); + + const { setIsOpen } = useTour() + + async function markAsNotFirstTimeUser() { + try { + await sdk.callRawAPI("/v2/api/custom/ergo/edit-self", { profile: { getting_started: 1 } }, "POST"); + globalDispatch({ + type: "SET_USER_DATA", + payload: { + ...globalState.user, + getting_started: 1, + }, + }); + } catch (err) { + tokenExpireError(dispatch, err.message); + console.log("err", err); + } + } + + if (!globalState.user.id) return null; + + const fetchUser = async () => { + const result = await sdk.callRawAPI("/rest/profile/GETALL", { + "payload": { + "user_id": Number(globalState.user.id) + }, + "selectStr": "*" + }, + "POST"); + setGettingStarted(result.list[0]?.getting_started) + } + + fetchUser() + + + return ( + <> + + setModalOpen(false)} + > + +
+ + +
+
+ + + + First time login? + +
+

Would you like a tour of the site?

+
+ +
+ + +
+
+
+
+
+
+
+ + ); +} diff --git a/src/components/CustomerHeader.jsx b/src/components/CustomerHeader.jsx new file mode 100644 index 0000000..e49583c --- /dev/null +++ b/src/components/CustomerHeader.jsx @@ -0,0 +1,180 @@ +import { GlobalContext, showToast } from "@/globalContext"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import MkdSDK from "@/utils/MkdSDK"; +import { sleep } from "@/utils/utils"; +import React, { useEffect, useState } from "react"; +import { useLocation, useNavigate } from "react-router"; +import { Link } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "../authContext"; +import LogoIcon from "./frontend/icons/LogoIcon"; +import NavMenu from "./frontend/NavMenu"; +import StaticSearchBar from "./frontend/StaticSearchBar"; + +const getNavBarVariant = (path) => { + if (path.startsWith("/account") || path.startsWith("/property") || path.startsWith("/help")) { + return "light"; + } + switch (path) { + case "/contact-us": + case "/faq": + return "white"; + case "/search": + case "/explore": + case "/favorites": + case "/become-a-host": + case "/reset-password": + return "light"; + default: + return "transparent"; + } +}; + +export const CustomerHeader = () => { + const { state: authState, dispatch } = React.useContext(AuthContext); + const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext); + const navigate = useNavigate(); + const { pathname } = useLocation(); + const [variant, setVariant] = useState(getNavBarVariant(pathname)); + + async function fetchProfile() { + const sdk = new MkdSDK(); + try { + const result = await sdk.getProfileCustom(); + globalDispatch({ type: "SET_USER_DATA", payload: result }); + } catch (err) { + showToast(globalDispatch, err.message, 4000, "ERROR"); + tokenExpireError(dispatch, err.message); + } + } + + useEffect(() => { + const onScroll = () => { + if (pathname == "/") { + if (window.scrollY > 10) { + setVariant("white"); + } else { + setVariant("transparent"); + } + } + }; + window.addEventListener("scroll", onScroll); + fetchProfile(); + return () => { + window.removeEventListener("scroll", onScroll); + }; + }, [pathname]); + + useEffect(() => { + setVariant(getNavBarVariant(pathname)); + }, [pathname]); + + const joinAsHostAdmin = async () => { + let newLogin = { ...authState, role: "host" }; + globalDispatch({ type: "START_LOADING" }); + await sleep(500); + globalDispatch({ type: "STOP_LOADING" }); + navigate("/"); + dispatch({ type: "LOGOUT" }); + dispatch({ type: "LOGIN", payload: newLogin }); + showToast(globalDispatch, "Joined as Host", 2000); + }; + + async function becomeAHost() { + // check if all fields are ready to go + if (!(globalState.verificationType && globalState.dob && globalState.city && globalState.country && globalState.about)) { + navigate("/become-a-host"); + return; + } + try { + await callCustomAPI( + "edit-self", + "post", + { + user: { role: "host" }, + }, + "", + ); + dispatch({ type: "SWITCH_TO_HOST" }); + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Success", + message: `You are now signed in as a host`, + btn: "Ok got it", + }, + }); + } catch (err) { } + } + + function switchToHost() { + dispatch({ type: "SWITCH_TO_HOST" }); + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Success", + message: `You are now signed in as a host`, + btn: "Ok got it", + }, + }); + navigate("/"); + } + + if (pathname.includes("/login") || pathname.includes("/signup")) return null; + + return ( + <> +
+ + + +
+ + + + +
+ + +
+ + ); +}; + +export default CustomerHeader; diff --git a/src/components/DatePickerV3.jsx b/src/components/DatePickerV3.jsx new file mode 100644 index 0000000..aec22a9 --- /dev/null +++ b/src/components/DatePickerV3.jsx @@ -0,0 +1,82 @@ +import { formatDate } from "@/utils/date-time-utils"; +import { Popover, Transition } from "@headlessui/react"; +import React, { Fragment, useEffect } from "react"; +import { Calendar } from "react-calendar"; +import { useController } from "react-hook-form"; +import { useSearchParams } from "react-router-dom"; +import CalendarIcon from "./frontend/icons/CalendarIcon"; +import NextIcon from "./frontend/icons/NextIcon"; +import PrevIcon from "./frontend/icons/PrevIcon"; + +export default function DatePickerV3({ control, name, placeholder, labelClassName, reset, min }) { + const { field, fieldState, formState } = useController({ control, name }); + const [searchParams] = useSearchParams(); + + return ( +
+ + +
+ {!fieldState.isDirty ? ( + + ) : ( + { + e.stopPropagation(); + reset(); + searchParams.delete(name); + }} + > + ✕ + + )} + {fieldState.isDirty || searchParams.get(name) ? formatDate(field.value) : placeholder} +
+
+ + + {({ close }) => ( +
+ { + field.onChange(val); + close(); + }} + value={field.value} + className={`calendar date-picker`} + nextLabel={} + prevLabel={} + next2Label={ +
e.stopPropagation()} + >
+ } + prev2Label={ +
e.stopPropagation()} + >
+ } + maxDetail="month" + minDate={min} + /> +
+ )} +
+
+
+
+ ); +} diff --git a/src/components/Details.jsx b/src/components/Details.jsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/ErrorModal.jsx b/src/components/ErrorModal.jsx new file mode 100644 index 0000000..104803c --- /dev/null +++ b/src/components/ErrorModal.jsx @@ -0,0 +1,74 @@ +import { GlobalContext } from "@/globalContext"; +import { Dialog, Transition } from "@headlessui/react"; +import { XMarkIcon } from "@heroicons/react/24/outline"; +import { ExclamationTriangleIcon } from "@heroicons/react/24/solid"; +import React, { Fragment } from "react"; +import { useContext } from "react"; + +export default function ErrorModal() { + const { state, dispatch } = useContext(GlobalContext); + + if (!state.error) return null; + + return ( + + dispatch({ type: "CLOSE_ERROR" })} + > + +
+ + +
+
+ + +
+ +
+
+ +
+ + {state.errorHeading} + + +

{state.errorMsg}

+
+
+
+
+
+
+ ); +} diff --git a/src/components/Faq.jsx b/src/components/Faq.jsx new file mode 100644 index 0000000..5c48ffa --- /dev/null +++ b/src/components/Faq.jsx @@ -0,0 +1,83 @@ +import React, { useState, useContext } from "react"; +import { GlobalContext } from "@/globalContext"; +import { useNavigate } from "react-router-dom"; +import Icon from "./Icons"; +import "suneditor/dist/css/suneditor.min.css"; + +const Faq = ({ data }) => { + const [showAnswer, setShowAnswer] = useState(false); + const { dispatch } = useContext(GlobalContext); + const navigate = useNavigate(); + + return ( +
+
+
+
+ + +
+
+
+
+
+
+

{data.question}

+
+ {showAnswer && ( +

+ )} +
+
+ {showAnswer ? ( + setShowAnswer(!showAnswer)} + /> + ) : ( + setShowAnswer(!showAnswer)} + /> + )} +
+
+
+
+ ); +}; + +export default Faq; diff --git a/src/components/FilterCheckBoxesV2.jsx b/src/components/FilterCheckBoxesV2.jsx new file mode 100644 index 0000000..0d2c1d7 --- /dev/null +++ b/src/components/FilterCheckBoxesV2.jsx @@ -0,0 +1,116 @@ +import { Disclosure, Transition } from "@headlessui/react"; +import React, { Fragment } from "react"; +import { useController } from "react-hook-form"; +import StarIcon from "./frontend/icons/StarIcon"; + +export default function FilterCheckBoxesV2({ name, control, setValue, reset, title, labelField, valueField, options }) { + const { field, fieldState, formState } = useController({ control, name }); + + return ( +
+ +
+

+ {title} + +

+ + {" "} + + + + +
+ + + + {options.map((op, idx) => ( +
+ { + if (name === "capacity") { + field.onChange(op[valueField]); + } else { + const exists = field.value.includes(op[valueField]); + if (exists) { + field.onChange(field.value.filter((item) => item !== op[valueField])); + } else { + field.onChange([...field.value, op[valueField]]); + } + } + }} + // onChange={() => { + // // remove if in array else add + // const exists = field.value.includes(op[valueField]); + // if (exists && op[name] !== "capacity") { + // field.onChange(field.value.filter((item) => item != op[valueField])); + // return; + // } + // field.onChange([...field.value, op[valueField]]); + // }} + onBlur={field.onBlur} + /> + + {name !== "capacity" ? + + : + + {op[labelField]}{" "} + + } +
+ ))} +
+
+
+
+ ); +} diff --git a/src/components/History.jsx b/src/components/History.jsx new file mode 100644 index 0000000..fc7e105 --- /dev/null +++ b/src/components/History.jsx @@ -0,0 +1,192 @@ +import React, { Fragment } from "react"; +import MkdSDK from "@/utils/MkdSDK"; +import PaginationBar from "./PaginationBar"; +import PaginationHeader from "./PaginationHeader"; +import { Menu, Transition } from "@headlessui/react"; +import Icon from "./Icons"; +import { useNavigate } from "react-router-dom"; +import { secondsToHour } from "@/utils/utils"; +import moment from "moment"; +import { ID_PREFIX } from "@/utils/constants"; + +const History = ({ id, table }) => { + const navigate = useNavigate(); + const [query, setQuery] = React.useState(""); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + + const statusMapping = [ + { key: "0", value: "Pending" }, + { key: "1", value: "Upcoming" }, + { key: "2", value: "Ongoing" }, + { key: "3", value: "Complete" }, + { key: "4", value: "Declined" }, + { key: "5", value: "Cancelled" } + ]; + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum) { + try { + let sdk = new MkdSDK(); + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/booking/PAGINATE", + { + where: [ + table ? `${table === "customer" ? `customer.id = ${id}` : "1"} AND ${table === "host" ? `ergo_user.id = ${id}` : "1"} AND ${table == "property" ? `ergo_property.id = ${id}` : "1"}` : 1 + ], + page: pageNum, + limit: limitNum + }, + "POST" + ); + + const { list, total, limit, num_pages, page } = result; + + setCurrentTableData(list); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + + React.useEffect(() => { + (async function () { + await getData(1, pageSize); + })(); + }, []); + + return ( + <> + +
+ {data.map((data) => ( +
+
{ID_PREFIX.BOOKINGS + data.id}
+ property_image +
+

{data.property_name}

+

{data.space_category}

+

{statusMapping.find((status) => status.key == data.status)?.value}

+
+
+

Host

+

+ {data.host_last_name}, {data.host_first_name}{" "} +

+

Customer

+

+ {data.customer_last_name}, {data.customer_first_name}{" "} +

+
+
+

Date

+

{moment(data.booking_start_time).format("MM/DD/YY")}

+

Duration

+

{secondsToHour(data.duration)}

+
+
+

Rate

+

${data?.rate?.toFixed(2)}

+

Tax

+

${data?.tax?.toFixed(2)}

+
+
+

Total

+

${data?.total?.toFixed(2)}

+

Commission

+

${data?.commission?.toFixed(2)}

+
+ +
+ + + +
+ + +
+ + {({ active }) => ( + + )} + +
+
+
+
+
+ ))} +
+ + + ); +}; + +export default History; diff --git a/src/components/HostAddAddonsModal.jsx b/src/components/HostAddAddonsModal.jsx new file mode 100644 index 0000000..e985c8e --- /dev/null +++ b/src/components/HostAddAddonsModal.jsx @@ -0,0 +1,140 @@ +import { GlobalContext, showToast } from "@/globalContext"; +import React from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { useContext } from "react"; +import GreenCheckIcon from "./frontend/icons/GreenCheckIcon"; +import { useNavigate } from "react-router"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { LoadingButton } from "./frontend"; + +export default function HostAddAddonsModal({setAddOnModal, getData}) { + let sdk = new MkdSDK(); + const { state, dispatch: globalDispatch } = React.useContext(GlobalContext); + const [loading, setLoading] = React.useState(); + const schema = yup + .object({ + name: yup.string().required("Name is required"), + cost: yup.number().required().typeError("Cost must be a number"), + }) + .required(); + + const { dispatch } = React.useContext(AuthContext); + + const navigate = useNavigate(); + + const { + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const onSubmit = async (data) => { + setLoading(true) + try { + sdk.setTable("add_on"); + + const result = await sdk.callRestAPI( + { + name: data.name, + cost: data.cost, + creator_id: Number(localStorage.getItem("user")), + space_id: data.space_id || null, + }, + "POST", + ); + if (!result.error) { + getData(); + showToast(globalDispatch, "Added"); + setLoading(false) + setAddOnModal(false) + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + setLoading(false) + console.log("Error", error); + setError("name", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + return ( +
+
e.stopPropagation()} + > +
+ +
+ + +

{errors.name?.message}

+
+ +
+ + +

{errors.cost?.message}

+
+ +
+ + + Save + +
+
+
+
+ ); +} diff --git a/src/components/HostAddAmenityModal.jsx b/src/components/HostAddAmenityModal.jsx new file mode 100644 index 0000000..ba93655 --- /dev/null +++ b/src/components/HostAddAmenityModal.jsx @@ -0,0 +1,120 @@ +import { GlobalContext, showToast } from "@/globalContext"; +import React from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { useContext } from "react"; +import GreenCheckIcon from "./frontend/icons/GreenCheckIcon"; +import { useNavigate } from "react-router"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { LoadingButton } from "./frontend"; + +export default function HostAddAmenityModal({setAmenityModal,getData}) { + let sdk = new MkdSDK(); + const { state, dispatch: globalDispatch } = React.useContext(GlobalContext); + const [loading, setLoading] = React.useState(); + const schema = yup + .object({ + name: yup.string().required("Name is required"), + }) + .required(); + + const { dispatch } = React.useContext(AuthContext); + + const { + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const onSubmit = async (data) => { + setLoading(true) + try { + sdk.setTable("amenity"); + + const result = await sdk.callRestAPI( + { + name: data.name, + creator_id: Number(localStorage.getItem("user")), + space_id: data.space_id || null, + }, + "POST", + ); + if (!result.error) { + getData(); + showToast(globalDispatch, "Amenity Added"); + setLoading(false) + setAmenityModal(false) + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + setLoading(false) + console.log("Error", error); + setError("name", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + return ( +
+
e.stopPropagation()} + > +
+ +
+ + +

{errors.name?.message}

+
+ +
+ + + Save + +
+
+
+
+ ); +} diff --git a/src/components/HostGettingStartedTour.jsx b/src/components/HostGettingStartedTour.jsx new file mode 100644 index 0000000..96ba196 --- /dev/null +++ b/src/components/HostGettingStartedTour.jsx @@ -0,0 +1,131 @@ +import React, { Fragment, useContext, useEffect, useState } from "react"; +import { AuthContext } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { Dialog, Transition } from "@headlessui/react"; +import { useLocation, useNavigate } from "react-router"; +import { useTour } from "@reactour/tour"; + +export default function HostGettingStartedTour() { + const navigate = useNavigate(); + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const { dispatch } = useContext(AuthContext); + const [modalOpen, setModalOpen] = useState(true); + const [gettingStarted, setGettingStarted] = useState(); + const sdk = new MkdSDK(); + + const { setIsOpen } = useTour() + + async function markAsNotFirstTimeUser() { + try { + await sdk.callRawAPI("/v2/api/custom/ergo/edit-self", { profile: { getting_started: 1 } }, "POST"); + globalDispatch({ + type: "SET_USER_DATA", + payload: { + ...globalState.user, + getting_started: 1, + }, + }); + } catch (err) { + tokenExpireError(dispatch, err.message); + console.log("err", err); + } + } + + if (!globalState.user.id) return null; + + const fetchUser = async () => { + const result = await sdk.callRawAPI("/rest/profile/GETALL", { + "payload": { + "user_id": Number(globalState.user.id) + }, + "selectStr": "*" + }, + "POST"); + setGettingStarted(result.list[0]?.getting_started) + } + + fetchUser() + + const setTour = () => { + setModalOpen(false); + globalDispatch({ type: "START_TOUR" }); + setIsOpen(true) + } + + return ( + <> + + setModalOpen(false)} + > + +
+ + +
+
+ + + + First time login? + +
+

Would you like a tour of the site?

+
+ +
+ + +
+
+
+
+
+
+
+ + ); +} diff --git a/src/components/HostHeader.jsx b/src/components/HostHeader.jsx new file mode 100644 index 0000000..fd98c1e --- /dev/null +++ b/src/components/HostHeader.jsx @@ -0,0 +1,149 @@ +import React, { useEffect, useState, useContext } from "react"; +import { useLocation, useNavigate } from "react-router"; +import { Link } from "react-router-dom"; +import LogoIcon from "./frontend/icons/LogoIcon"; +import ReactTestUtils from "react-dom/test-utils"; +import StaticSearchBar from "./frontend/StaticSearchBar"; +import NavMenu from "./frontend/NavMenu"; +import { GlobalContext, showToast } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { AuthContext, tokenExpireError } from "@/authContext"; + +const getNavBarVariant = (path) => { + if (path.startsWith("/account") || path.startsWith("/property") || path.startsWith("/spaces") || path.startsWith("/help")) { + return "light"; + } + switch (path) { + case "/contact-us": + case "/faq": + return "white"; + case "/search": + case "/explore": + case "/favorites": + case "/reset-password": + return "light"; + default: + return "transparent"; + } +}; + +export const HostHeader = () => { + const { pathname } = useLocation(); + const [variant, setVariant] = useState(getNavBarVariant(pathname)); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const { dispatch } = useContext(AuthContext); +const navigate = useNavigate(); + async function fetchProfile() { + const sdk = new MkdSDK(); + try { + const result = await sdk.getProfileCustom(); + globalDispatch({ type: "SET_USER_DATA", payload: result }); + } catch (err) { + showToast(globalDispatch, err.message, 4000, "ERROR"); + tokenExpireError(dispatch, err.message); + } + } + + useEffect(() => { + const onScroll = () => { + if (pathname == "/") { + if (window.scrollY > 10) { + setVariant("white"); + } else { + setVariant("transparent"); + } + } + }; + window.addEventListener("scroll", onScroll); + + fetchProfile(); + + return () => { + window.removeEventListener("scroll", onScroll); + }; + }, [pathname]); + + useEffect(() => { + setVariant(getNavBarVariant(pathname)); + }, [pathname]); + + const saveAsDraft = () => { + console.log("clicked"); + const saveDraftBtn = document.getElementById("save-as-draft"); + if (saveDraftBtn) { + ReactTestUtils.Simulate.click(saveDraftBtn); + } + }; + + if (pathname.includes("/login") || pathname.includes("/signup")) return null; + + return ( +
+ + + +
+ + + + +
+ + + +
+ ); +}; + +export default HostHeader; diff --git a/src/components/Icons/ArrowSvg.jsx b/src/components/Icons/ArrowSvg.jsx new file mode 100644 index 0000000..88cf4b4 --- /dev/null +++ b/src/components/Icons/ArrowSvg.jsx @@ -0,0 +1,24 @@ +import React from "react"; +import { ReactComponent as ArrowNarrowLeft } from "../../assets/arrow-narrow-left.svg"; +import { ReactComponent as ArrowNarrowRight } from "../../assets/arrow-narrow-right.svg"; + + +const ArrowSvg = ({ className = "", id, onClick, onKeyUp, variant }) => { + + switch (variant) { + case "narrow-right": return + case "narrow-left": return + } +} + +export default ArrowSvg \ No newline at end of file diff --git a/src/components/Icons/BankNoteSvg.jsx b/src/components/Icons/BankNoteSvg.jsx new file mode 100644 index 0000000..389c656 --- /dev/null +++ b/src/components/Icons/BankNoteSvg.jsx @@ -0,0 +1,16 @@ +import React from "react"; +import { ReactComponent as BankNoteOne } from "../../assets/bank-note-one.svg"; + +const BankNoteSvg = ({ className = "", id, onClick, onKeyUp, variant }) => { + + switch (variant) { + case "one": return + } +} + +export default BankNoteSvg \ No newline at end of file diff --git a/src/components/Icons/BuildingSvg.jsx b/src/components/Icons/BuildingSvg.jsx new file mode 100644 index 0000000..9689fc2 --- /dev/null +++ b/src/components/Icons/BuildingSvg.jsx @@ -0,0 +1,16 @@ +import React from "react"; +import { ReactComponent as BuildingOne } from "../../assets/building-one.svg"; + +const BuildingSvg = ({ className = "", id, onClick, onKeyUp, variant }) => { + + switch (variant) { + case "one": return + } +} + +export default BuildingSvg \ No newline at end of file diff --git a/src/components/Icons/Calender.jsx b/src/components/Icons/Calender.jsx new file mode 100644 index 0000000..904497f --- /dev/null +++ b/src/components/Icons/Calender.jsx @@ -0,0 +1,16 @@ +import React from "react"; +import { ReactComponent as Calender } from "../../assets/calender.svg"; + +const CalenderSvg = ({ className = "", id, onClick, onKeyUp, variant }) => { + + switch (variant) { + default: return + } +} + +export default CalenderSvg \ No newline at end of file diff --git a/src/components/Icons/ChevronSvg.jsx b/src/components/Icons/ChevronSvg.jsx new file mode 100644 index 0000000..fb37844 --- /dev/null +++ b/src/components/Icons/ChevronSvg.jsx @@ -0,0 +1,16 @@ +import React from "react"; +import { ReactComponent as ChevronDown } from "../../assets/chevron-down.svg"; + +const CalenderSvg = ({ className = "", id, onClick, onKeyUp, variant }) => { + + switch (variant) { + case 'down': return + } +} + +export default CalenderSvg \ No newline at end of file diff --git a/src/components/Icons/DotsSvg.jsx b/src/components/Icons/DotsSvg.jsx new file mode 100644 index 0000000..5c270ce --- /dev/null +++ b/src/components/Icons/DotsSvg.jsx @@ -0,0 +1,13 @@ +import React from "react"; +import { ReactComponent as Dots } from "../../assets/dots.svg"; + +const DotsSvg = ({ className = "", id, onClick, onKeyUp }) => ( + +); + +export default DotsSvg \ No newline at end of file diff --git a/src/components/Icons/FileSvg.jsx b/src/components/Icons/FileSvg.jsx new file mode 100644 index 0000000..edf615e --- /dev/null +++ b/src/components/Icons/FileSvg.jsx @@ -0,0 +1,38 @@ +import React from "react"; +import { ReactComponent as FileCheckThree } from "../../assets/file-check-three.svg"; +import { ReactComponent as FilePlusThree } from "../../assets/file-plus-three.svg"; +import { ReactComponent as FileQuestionThree } from "../../assets/file-question-three.svg"; +import { ReactComponent as FileSearchOne } from "../../assets/file-search-one.svg"; + + +const BuildingSvg = ({ className = "", id, onClick, onKeyUp, variant }) => { + + switch (variant) { + case "check-three": return + case "plus-three": return + case "question-three": return + case "search-one": return + } +} + +export default BuildingSvg \ No newline at end of file diff --git a/src/components/Icons/GridSvg.jsx b/src/components/Icons/GridSvg.jsx new file mode 100644 index 0000000..b71edcd --- /dev/null +++ b/src/components/Icons/GridSvg.jsx @@ -0,0 +1,16 @@ +import React from "react"; +import { ReactComponent as GridOne } from "../../assets/grid-one.svg"; + +const GridSvg = ({ className = "", id, onClick, onKeyUp, variant }) => { + + switch (variant) { + case "one": return + } +} + +export default GridSvg \ No newline at end of file diff --git a/src/components/Icons/HomeSvg.jsx b/src/components/Icons/HomeSvg.jsx new file mode 100644 index 0000000..d7a7bfd --- /dev/null +++ b/src/components/Icons/HomeSvg.jsx @@ -0,0 +1,23 @@ +import React from "react"; +import { ReactComponent as HomeThree } from "../../assets/home-three.svg"; +import { ReactComponent as HomeLine } from "../../assets/home-line.svg"; + +const HomeSvg = ({ className = "", id, onClick, onKeyUp, variant }) => { + + switch (variant) { + case "three": return + case "line": return + } +} + +export default HomeSvg \ No newline at end of file diff --git a/src/components/Icons/HouseIcon.jsx b/src/components/Icons/HouseIcon.jsx new file mode 100644 index 0000000..8e56e3a --- /dev/null +++ b/src/components/Icons/HouseIcon.jsx @@ -0,0 +1,21 @@ +import React from "react"; + +const HouseIcon = () => ( + + + +); + +export default HouseIcon; diff --git a/src/components/Icons/ImageSvg.jsx b/src/components/Icons/ImageSvg.jsx new file mode 100644 index 0000000..7ece79e --- /dev/null +++ b/src/components/Icons/ImageSvg.jsx @@ -0,0 +1,16 @@ +import React from "react"; +import { ReactComponent as ImageThree } from "../../assets/image-three.svg"; + +const ImageSvg = ({ className = "", id, onClick, onKeyUp, variant }) => { + + switch (variant) { + case "three": return + } +} + +export default ImageSvg \ No newline at end of file diff --git a/src/components/Icons/LogoIcon.jsx b/src/components/Icons/LogoIcon.jsx new file mode 100644 index 0000000..055a6e1 --- /dev/null +++ b/src/components/Icons/LogoIcon.jsx @@ -0,0 +1,16 @@ +export default function LogoIcon({ fill }) { + return ( + + + + + + + ); +} diff --git a/src/components/Icons/LogoSvg.jsx b/src/components/Icons/LogoSvg.jsx new file mode 100644 index 0000000..0272e0d --- /dev/null +++ b/src/components/Icons/LogoSvg.jsx @@ -0,0 +1,13 @@ +import React from "react"; +import { ReactComponent as Logo } from "../../assets/logo.svg"; + +const LogoSvg = ({ className = "",fill, id, onClick, onKeyUp }) => ( + +); + +export default LogoSvg \ No newline at end of file diff --git a/src/components/Icons/LogoutSvg.jsx b/src/components/Icons/LogoutSvg.jsx new file mode 100644 index 0000000..2c76fb3 --- /dev/null +++ b/src/components/Icons/LogoutSvg.jsx @@ -0,0 +1,13 @@ +import React from "react"; +import { ReactComponent as Logout } from "../../assets/logout.svg"; + +const LogoutSvg = ({ className = "", id, onClick, onKeyUp }) => ( + +); + +export default LogoutSvg \ No newline at end of file diff --git a/src/components/Icons/MailSvg.jsx b/src/components/Icons/MailSvg.jsx new file mode 100644 index 0000000..c0df920 --- /dev/null +++ b/src/components/Icons/MailSvg.jsx @@ -0,0 +1,16 @@ +import React from "react"; +import { ReactComponent as MailOne } from "../../assets/mail-one.svg"; + +const SettingsSvg = ({ className = "", id, onClick, onKeyUp, variant }) => { + + switch (variant) { + default : return + } +} + +export default SettingsSvg \ No newline at end of file diff --git a/src/components/Icons/MinusSvg.jsx b/src/components/Icons/MinusSvg.jsx new file mode 100644 index 0000000..a56e2d0 --- /dev/null +++ b/src/components/Icons/MinusSvg.jsx @@ -0,0 +1,13 @@ +import React from "react"; +import { ReactComponent as Minus } from "../../assets/minus.svg"; + +const MinusSvg = ({ className = "", id, onClick, onKeyUp }) => ( + +); + +export default MinusSvg \ No newline at end of file diff --git a/src/components/Icons/PencilSvg.jsx b/src/components/Icons/PencilSvg.jsx new file mode 100644 index 0000000..a1c4e58 --- /dev/null +++ b/src/components/Icons/PencilSvg.jsx @@ -0,0 +1,13 @@ +import React from "react"; +import { ReactComponent as Pencil } from "../../assets/pencil.svg"; + +const PencilSvg = ({ className = "", id, onClick, onKeyUp }) => ( + +); + +export default PencilSvg \ No newline at end of file diff --git a/src/components/Icons/PlusSvg.jsx b/src/components/Icons/PlusSvg.jsx new file mode 100644 index 0000000..eb5fb01 --- /dev/null +++ b/src/components/Icons/PlusSvg.jsx @@ -0,0 +1,13 @@ +import React from "react"; +import { ReactComponent as Plus } from "../../assets/plus.svg"; + +const PlusSvg = ({ className = "", id, onClick, onKeyUp }) => ( + +); + +export default PlusSvg \ No newline at end of file diff --git a/src/components/Icons/ReceiptSvg.jsx b/src/components/Icons/ReceiptSvg.jsx new file mode 100644 index 0000000..bcee91c --- /dev/null +++ b/src/components/Icons/ReceiptSvg.jsx @@ -0,0 +1,16 @@ +import React from "react"; +import { ReactComponent as BookingReceipt } from "../../assets/booking-receipt.svg"; + +const ReceiptSvg = ({ className = "", id, onClick, onKeyUp, variant }) => { + + switch (variant) { + case "booking": return + } +} + +export default ReceiptSvg \ No newline at end of file diff --git a/src/components/Icons/SettingsSvg.jsx b/src/components/Icons/SettingsSvg.jsx new file mode 100644 index 0000000..8f55c1b --- /dev/null +++ b/src/components/Icons/SettingsSvg.jsx @@ -0,0 +1,16 @@ +import React from "react"; +import { ReactComponent as SettingsTwo } from "../../assets/settings-two.svg"; + +const SettingsSvg = ({ className = "", id, onClick, onKeyUp, variant }) => { + + switch (variant) { + case "two": return + } +} + +export default SettingsSvg \ No newline at end of file diff --git a/src/components/Icons/ShareSvg.jsx b/src/components/Icons/ShareSvg.jsx new file mode 100644 index 0000000..aa63424 --- /dev/null +++ b/src/components/Icons/ShareSvg.jsx @@ -0,0 +1,15 @@ +import React from "react"; +import { ReactComponent as ShareOne } from "../../assets/share-one.svg"; + +const ShareSvg = ({ className = "", id, onClick, onKeyUp, variant }) => { + switch (variant) { + case "one": return + } +} + +export default ShareSvg \ No newline at end of file diff --git a/src/components/Icons/StarSvg.jsx b/src/components/Icons/StarSvg.jsx new file mode 100644 index 0000000..838f010 --- /dev/null +++ b/src/components/Icons/StarSvg.jsx @@ -0,0 +1,13 @@ +import React from "react"; +import { ReactComponent as Star } from "../../assets/star.svg"; + +const StarSvg = ({ className = "", id, onClick, onKeyUp }) => ( + +); + +export default StarSvg; diff --git a/src/components/Icons/TrashSvg.jsx b/src/components/Icons/TrashSvg.jsx new file mode 100644 index 0000000..92b3073 --- /dev/null +++ b/src/components/Icons/TrashSvg.jsx @@ -0,0 +1,16 @@ +import React from "react"; +import { ReactComponent as TrashTwo } from "../../assets/trash-two.svg"; + +const TrashSvg = ({ className = "", id, onClick, onKeyUp, variant }) => { + + switch (variant) { + case "two": return + } +} + +export default TrashSvg \ No newline at end of file diff --git a/src/components/Icons/UserSvg.jsx b/src/components/Icons/UserSvg.jsx new file mode 100644 index 0000000..48396d1 --- /dev/null +++ b/src/components/Icons/UserSvg.jsx @@ -0,0 +1,23 @@ +import React from "react"; +import { ReactComponent as UserSquare } from "../../assets/user-square.svg"; +import { ReactComponent as UserCircle } from "../../assets/user-circle.svg"; + +const UserSvg = ({ className = "", id, onClick, onKeyUp, variant }) => { + + switch (variant) { + case "square": return + case "circle": return + } +} + +export default UserSvg \ No newline at end of file diff --git a/src/components/Icons/UsersSvg.jsx b/src/components/Icons/UsersSvg.jsx new file mode 100644 index 0000000..6e178b1 --- /dev/null +++ b/src/components/Icons/UsersSvg.jsx @@ -0,0 +1,16 @@ +import React from "react"; +import { ReactComponent as UsersOne } from "../../assets/users-one.svg"; + +const Users = ({ className = "", id, onClick, onKeyUp, variant }) => { + + switch (variant) { + case "one": return + } +} + +export default Users \ No newline at end of file diff --git a/src/components/Icons/index.jsx b/src/components/Icons/index.jsx new file mode 100644 index 0000000..0f04079 --- /dev/null +++ b/src/components/Icons/index.jsx @@ -0,0 +1,75 @@ +import React, { useEffect, useState } from "react"; +import LogoSvg from "./LogoSvg" +import LogoutSvg from "./LogoutSvg" +import ShareSvg from "./ShareSvg" +import GridSvg from "./GridSvg" +import UserSvg from "./UserSvg" +import ImageSvg from "./ImageSvg" +import BankNoteSvg from "./BankNoteSvg" +import BuildingSvg from "./BuildingSvg" +import UsersSvg from "./UsersSvg" +import HomeSvg from "./HomeSvg" +import FileSvg from "./FileSvg" +import CalenderSvg from "./Calender"; +import ReceiptSvg from "./ReceiptSvg"; +import MailSvg from "./MailSvg" +import SettingsSvg from "./SettingsSvg" +import ArrowSvg from "./ArrowSvg" +import ChevronSvg from "./ChevronSvg" +import TrashSvg from "./TrashSvg" +import PencilSvg from "./PencilSvg" +import PlusSvg from "./PlusSvg" +import MinusSvg from "./MinusSvg" +import DotsSvg from "./DotsSvg" +import StarSvg from "./StarSvg" + + +const getIcon = (type, className, id, fill, onClick, onKeyUp, variant) => { + const icons = { + logo: , + logout: , + pencil: , + share: , + grid: , + user: , + image: , + banknote: , + building: , + users: , + home: , + file: , + calender: , + receipt: , + mail: , + settings: , + arrow: , + chevron: , + trash: , + plus: , + minus: , + dots: , + star: + } + + return icons[type] || null; +} + + + +const Icon = ({ className, id, fill = '', onClick, onKeyUp, type, variant }) => { + const [icon, setIcon] = useState(null); + + useEffect(() => { + if (type) { + // Remove all white space from the string, with the regex + const iconType = type.toLocaleLowerCase().replace(/\s+/g, ''); + + // set the icon based on icon type change, useful for conditional icon renderings + setIcon(getIcon(iconType, className, id, fill, onClick, onKeyUp, variant)); + } + }, [type, className]); + + return icon; +}; + +export default Icon \ No newline at end of file diff --git a/src/components/LoadingSpinner.jsx b/src/components/LoadingSpinner.jsx new file mode 100644 index 0000000..b458eff --- /dev/null +++ b/src/components/LoadingSpinner.jsx @@ -0,0 +1,40 @@ +import { GlobalContext } from "@/globalContext"; +import React from "react"; +import { useContext } from "react"; + +export default function LoadingSpinner() { + const { state } = useContext(GlobalContext); + + if (!state.loading) return null; + + return ( +
+
+ + + + + +
+
+ ); +} diff --git a/src/components/Modal.jsx b/src/components/Modal.jsx new file mode 100644 index 0000000..5c169ad --- /dev/null +++ b/src/components/Modal.jsx @@ -0,0 +1,205 @@ +import React from "react"; +import { GlobalContext, showToast } from "@/globalContext"; +import { AuthContext } from "../authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate } from "react-router-dom"; +import moment from "moment"; + +let sdk = new MkdSDK(); + +export default function Modal({ showModal, modalShowTitle, modalShowMessage, type, modalBtnText, itemId, itemId2, table1, table2, backTo }) { + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const { dispatch } = React.useContext(AuthContext); + const navigate = useNavigate(); + + return ( + <> + {showModal ? ( + <> +
+
+ {/*content*/} +
+ {/*header*/} +
+

{modalShowTitle}

+ +
+ {/*body*/} +
+

{modalShowMessage}

+
+ {/*footer*/} +
+ + +
+
+
+
+
+ + ) : null} + + ); +} diff --git a/src/components/NotVerifiedModal.jsx b/src/components/NotVerifiedModal.jsx new file mode 100644 index 0000000..28f6728 --- /dev/null +++ b/src/components/NotVerifiedModal.jsx @@ -0,0 +1,85 @@ +import { AuthContext } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment } from "react"; +import { useContext } from "react"; +import { useLocation, useNavigate } from "react-router"; + +export default function NotVerifiedModal() { + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const { state: authState } = useContext(AuthContext); + const { pathname } = useLocation(); + const navigate = useNavigate(); + + return ( + + globalDispatch({ type: "CLOSE_NOT_VERIFIED_MODAL" })} + > + +
+ + +
+
+ + + + User not verified + +
+

Please verify your account to proceed with booking

+
+ +
+ + +
+
+
+
+
+
+
+ ); +} diff --git a/src/components/PaginationBar.jsx b/src/components/PaginationBar.jsx new file mode 100644 index 0000000..24969c9 --- /dev/null +++ b/src/components/PaginationBar.jsx @@ -0,0 +1,50 @@ +import React from "react"; +import Icon from "./Icons"; +const PaginationBar = ({ currentPage, pageSize, canPreviousPage, canNextPage, previousPage, nextPage, totalNumber, className }) => { + return ( + <> +
+
+ + Showing{" "} + + {totalNumber < 1 ? 0 : currentPage > 1 ? (currentPage - 1) * pageSize + 1 : currentPage} - {currentPage * pageSize < totalNumber ? currentPage * pageSize : totalNumber} of {totalNumber} + {" "} + +
+ {/* */} +
+ + + +
+
+ + ); +}; + +export default PaginationBar; diff --git a/src/components/PaginationHeader.jsx b/src/components/PaginationHeader.jsx new file mode 100644 index 0000000..592b5ad --- /dev/null +++ b/src/components/PaginationHeader.jsx @@ -0,0 +1,40 @@ +import React from "react"; + +const PaginationBar = ({ currentPage, pageSize, updatePageSize, totalNumber, noBorder }) => { + return ( + <> +
+
+

+ Showing{" "} + + {totalNumber < 1 ? 0 : currentPage > 1 ? (currentPage - 1) * pageSize + 1 : currentPage}-{currentPage * pageSize < totalNumber ? currentPage * pageSize : totalNumber} of {totalNumber} + {" "} +

+
+ {/* */} +
+ Results per page: + +
+
+ + ); +}; + +export default PaginationBar; diff --git a/src/components/Payment.jsx b/src/components/Payment.jsx new file mode 100644 index 0000000..d628d37 --- /dev/null +++ b/src/components/Payment.jsx @@ -0,0 +1,181 @@ +import React, { Fragment } from "react"; +import MkdSDK from "@/utils/MkdSDK"; +import PaginationBar from "./PaginationBar"; +import PaginationHeader from "./PaginationHeader"; +import { Menu, Transition } from "@headlessui/react"; +import Icon from "./Icons"; +import { useNavigate } from "react-router-dom"; +import { secondsToHour } from "@/utils/utils"; +import moment from "moment"; +import { ID_PREFIX } from "@/utils/constants"; + +const Payment = ({ id, table }) => { + const navigate = useNavigate(); + const [query, setQuery] = React.useState(""); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + + const payoutMapping = [ + { key: "0", value: "Pending" }, + { key: "1", value: "Initiated" }, + { key: "2", value: "Paid" }, + { key: "3", value: "Cancelled" } + ]; + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum) { + try { + let sdk = new MkdSDK(); + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/payout/PAGINATE", + { + where: [table ? `${table === "host" ? `ergo_user.id = ${id}` : "1"}` : 1], + page: pageNum, + limit: limitNum + }, + "POST" + ); + + const { list, total, limit, num_pages, page } = result; + + setCurrentTableData(list); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + + React.useEffect(() => { + (async function () { + await getData(1, pageSize); + })(); + }, []); + + return ( + <> + +
+ {data.map((data, index) => ( +
+
{ID_PREFIX.PAYOUT + data.id}
+
+

Host

+

+ {data.host_last_name}, {data.host_first_name}{" "} +

+

Customer

+

+ {data.customer_last_name}, {data.customer_first_name}{" "} +

+
+
+

Booking Date

+

{data.create_at}

+

Order Number

+

{data.id}

+
+
+

Total

+

${data?.total?.toFixed(2)}

+

Tax

+

${data?.tax?.toFixed(2)}

+
+
+

Commission

+

${data?.commission?.toFixed(2)}

+

Payout Date

+

{data?.initiated_at ? moment(data.initiated_at).add(7, "days").format("MM/DD/YY") : ""}

+
+
+

{payoutMapping.find((status) => status.key == data.status)?.value}

+
+ +
+ + + +
+ + +
+ + {({ active }) => ( + + )} + +
+
+
+
+
+ ))} +
+ + + ); +}; + +export default Payment; diff --git a/src/components/Profile/DeleteAccountModal.jsx b/src/components/Profile/DeleteAccountModal.jsx new file mode 100644 index 0000000..00abf61 --- /dev/null +++ b/src/components/Profile/DeleteAccountModal.jsx @@ -0,0 +1,102 @@ +import { AuthContext } from "@/authContext"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment, useState } from "react"; +import { useContext } from "react"; +import { useNavigate } from "react-router"; +import { LoadingButton } from "@/components/frontend"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import { GlobalContext } from "@/globalContext"; + +export default function DeleteAccountModal({ modalOpen, closeModal }) { + const { dispatch: authDispatch, state: authState } = useContext(AuthContext); + const { state: globalState, dispatch: globalDispatch } = useContext(GlobalContext); + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + + async function requestAccountDelete() { + setLoading(true); + try { + await callCustomAPI("confirm-delete-email", "post", { user_id: globalState.user.id, email: globalState.user.email, role: authState.role }, ""); + navigate("/account/delete/check"); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + setLoading(false); + } + + return ( + + + +
+ + +
+
+ + + + Are you sure you want to delete your account? + +
+

You will receive an email to confirm this action

+
+ +
+ + + Proceed + +
+
+
+
+
+
+
+ ); +} diff --git a/src/components/Profile/EditAboutModal.jsx b/src/components/Profile/EditAboutModal.jsx new file mode 100644 index 0000000..c4e4330 --- /dev/null +++ b/src/components/Profile/EditAboutModal.jsx @@ -0,0 +1,142 @@ +import { LoadingButton } from "@/components/frontend"; +import { GlobalContext } from "@/globalContext"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment } from "react"; +import { useState } from "react"; +import { useContext } from "react"; +import { useForm } from "react-hook-form"; + +export default function EditAboutModal({ modalOpen, closeModal }) { + const { state: globalState, dispatch: globalDispatch } = useContext(GlobalContext); + const { handleSubmit, register } = useForm({ defaultValues: { about: globalState.user.about } }); + + const [loading, setLoading] = useState(false); + + async function onSubmit(data) { + console.log("submitting", data); + setLoading(true); + try { + await callCustomAPI( + "edit-self", + "post", + { + profile: data, + }, + "", + ); + closeModal(); + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Success", + message: "About change successful", + btn: "Ok got it", + }, + }); + globalDispatch({ + type: "SET_USER_DATA", + payload: { + ...globalState.user, + about: data.about, + }, + }); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + setLoading(false); + } + + return ( + <> + + + +
+ + +
+
+ + +
+ + Edit About + + {" "} +
+
+ +
+ + + Update + +
+
+
+
+
+
+
+ + ); +} diff --git a/src/components/Profile/EditLocationModal.jsx b/src/components/Profile/EditLocationModal.jsx new file mode 100644 index 0000000..5c0ebb6 --- /dev/null +++ b/src/components/Profile/EditLocationModal.jsx @@ -0,0 +1,180 @@ +import { LoadingButton } from "@/components/frontend"; +import { GlobalContext } from "@/globalContext"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment } from "react"; +import { useState } from "react"; +import { useContext } from "react"; +import { useForm } from "react-hook-form"; +import CustomLocationAutoCompleteV2 from "../CustomLocationAutoCompleteV2"; +import StickyCustomLocationAutoComplete from "../StickyCustomLocationAutoComplete"; + +export default function EditLocationModal({ modalOpen, closeModal }) { + const { state: globalState, dispatch: globalDispatch } = useContext(GlobalContext); + const { handleSubmit, register, setValue, control, formState: { errors } } = useForm({ defaultValues: { city: globalState.user.city, country: globalState.user.country } }); + const [loading, setLoading] = useState(false); + + async function onSubmit(data) { + console.log("submitting", data); + // const parts = data.city.split(", "); + // data.city = parts[0] + // data.country = parts[1] + setLoading(true); + try { + await callCustomAPI( + "edit-self", + "post", + { + profile: data, + }, + "", + ); + closeModal(); + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Success", + message: "Location changed successful", + btn: "Ok got it", + }, + }); + globalDispatch({ + type: "SET_USER_DATA", + payload: { + ...globalState.user, + city: data.city, + country: data.country, + }, + }); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + setLoading(false); + } + + return ( + <> + + + +
+ + +
+
+ + +
+ + Edit Location + + {" "} +
+
+
+ + setValue("city", val)} + name="city" + className={`w-full z-20 rounded relative border py-2 px-3 leading-tight text-gray-700 ${errors.city?.message ? "border-red-500 focus:outline-red-500" : "focus-within:outline-primary"}`} + placeholder="" + hideIcons + suggestionType={["(cities)"]} + /> + {/* */} +
+ {/*
+ + +
*/} +
+ + + Update + +
+
+
+
+
+
+
+ + ); +} diff --git a/src/components/Profile/EditPasswordModal.jsx b/src/components/Profile/EditPasswordModal.jsx new file mode 100644 index 0000000..7ef3e09 --- /dev/null +++ b/src/components/Profile/EditPasswordModal.jsx @@ -0,0 +1,305 @@ +import { LoadingButton } from "@/components/frontend"; +import { GlobalContext } from "@/globalContext"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import { Dialog, Transition } from "@headlessui/react"; +import * as yup from "yup"; +import { yupResolver } from "@hookform/resolvers/yup"; +import React, { Fragment } from "react"; +import { useState } from "react"; +import { useContext } from "react"; +import { useForm } from "react-hook-form"; +import commonPasswords from "@/assets/json/common-passwords.json"; +import moment from "moment"; + +export default function EditPasswordModal({ modalOpen, closeModal }) { + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const [showOldPassword, setShowOldPassword] = useState(false); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + + const schema = yup.object({ + current_password: yup.string().required("This field is required"), + password: yup + .string() + .required("Password is required") + .min(10, "Password must be at least 10 characters long") + .matches(/^(?=.*[0-9])/, "Password must contain at least one digit(0-9)") + .matches(/^(?=.*[a-z])/, "Password must contain at least one lowercase letter") + .matches(/^(?=.*[A-Z])/, "Password must contain at least one uppercase letter") + .matches(/^(?=.*[!@#\$%\^&\*])/, "Password must contain at least one symbol") + .test("is-not-dictionary", "Password must not contain a common word", (val) => { + return commonPasswords.every((pass) => !val.includes(pass)); + }) + .test("does-not-contain-user-info", "Password must not contain your name or date of birth", (val) => { + const d = moment(globalState.user.dob); + return [ + globalState.user.first_name, + globalState.user.last_name, + d.format("yyyyMMDD"), + d.format("DDMMyyyy"), + d.format("MMDDyyyy"), + d.format("YYMMDD"), + d.format("MMDDYY"), + d.format("DDMMYY"), + ].every((field) => field.trim() == "" || !val.toLowerCase().includes(field.toLowerCase())); + }), + confirm_password: yup + .string() + .oneOf([yup.ref("password"), null], "Passwords don't match") + .required("This field is required"), + }); + + const { + handleSubmit, + register, + reset, + trigger, + formState: { errors, dirtyFields }, + } = useForm({ defaultValues: { current_password: "", password: "", confirm_password: "" }, resolver: yupResolver(schema), criteriaMode: "all" }); + const [loading, setLoading] = useState(false); + + async function onSubmit(data) { + console.log("submitting", data); + setLoading(true); + try { + await callCustomAPI( + "edit-self", + "post", + { + user: { password: data.password, oldPassword: data.current_password }, + }, + "", + ); + closeModal(); + reset(); + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Success", + message: "Password change successful", + btn: "Ok got it", + }, + }); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + setLoading(false); + } + + function getPasswordErrors() { + var arr = []; + if (Array.isArray(errors.password?.types.matches)) { + arr = [...errors.password.types.matches]; + } + if (typeof errors.password?.types.matches === "string") { + arr.push(errors.password.types.matches); + } + if (errors.password?.types.min) { + arr.push(errors.password.types.min); + } + if (errors.password?.types["does-not-contain-user-info"]) { + arr.push(errors.password?.types["does-not-contain-user-info"]); + } + if (errors.password?.types["is-not-dictionary"]) { + arr.push(errors.password?.types["is-not-dictionary"]); + } + return arr; + } + const passwordErrors = getPasswordErrors(); + + return ( + <> + + + +
+ + +
+
+ + +
+ + Change Password + + {" "} +
+
+ +

In order to set new password provide the current one:

+ +
+
+ {" "} + +
+
+
+
+
+ { + trigger("password"); + }, + })} + className="flex-grow border-0 p-2 px-4 focus:outline-none active:outline-none " + placeholder="Type new password" + autoComplete="new-password" + />{" "} + +
+ {dirtyFields.password && ( +
+ {passwordErrors.map((msg) => ( +

{msg}

+ ))} +
+ )} +
+
+
+ {" "} + +
+ {Object.entries(errors).length > 0 && dirtyFields.password && !errors.password ? ( +

{Object.values(errors)[0].message}

+ ) : null} +
+
+ + + Update + +
+
+
+
+
+
+
+ + ); +} diff --git a/src/components/Profile/EditProfileModal.jsx b/src/components/Profile/EditProfileModal.jsx new file mode 100644 index 0000000..b7f1692 --- /dev/null +++ b/src/components/Profile/EditProfileModal.jsx @@ -0,0 +1,166 @@ +import { LoadingButton } from "@/components/frontend"; +import { GlobalContext } from "@/globalContext"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment } from "react"; +import { useState } from "react"; +import { useContext } from "react"; +import { useForm } from "react-hook-form"; + +export default function EditProfileModal({ modalOpen, closeModal }) { + const { state: globalState, dispatch: globalDispatch } = useContext(GlobalContext); + const { handleSubmit, register } = useForm({ defaultValues: { first_name: globalState.user.first_name, last_name: globalState.user.last_name } }); + const [loading, setLoading] = useState(false); + + async function onSubmit(data) { + console.log("submitting", data); + setLoading(true); + try { + await callCustomAPI( + "edit-self", + "post", + { + user: data, + }, + "", + ); + closeModal(); + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Success", + message: "Name change successful", + btn: "Ok got it", + }, + }); + globalDispatch({ + type: "SET_USER_DATA", + payload: { + ...globalState.user, + first_name: data.first_name, + last_name: data.last_name, + }, + }); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + setLoading(false); + } + + return ( + <> + + + +
+ + +
+
+ + +
+ + Edit Profile + + {" "} +
+
+
+ + +
+
+ + +
+
+ + + Update + +
+
+
+
+
+
+
+ + ); +} diff --git a/src/components/Profile/EnableEmailDialog.jsx b/src/components/Profile/EnableEmailDialog.jsx new file mode 100644 index 0000000..4188bfd --- /dev/null +++ b/src/components/Profile/EnableEmailDialog.jsx @@ -0,0 +1,133 @@ +import { LoadingButton } from "@/components/frontend"; +import { GlobalContext } from "@/globalContext"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import { parseJsonSafely, sleep } from "@/utils/utils"; +import { Dialog, Transition } from "@headlessui/react"; +import { useState } from "react"; +import { useContext } from "react"; +import { Fragment } from "react"; + +export default function EnableEmailDialog({ isOpen, closeModal }) { + const { state: globalState, dispatch: globalDispatch } = useContext(GlobalContext); + const isEnabled = parseJsonSafely(globalState.user.settings, {}).email_on_booking_declined == true; + const [loading, setLoading] = useState(false); + + async function toggleEmailPreference() { + setLoading(true); + let newSettings; + if (!isEnabled) { + newSettings = { + ...parseJsonSafely(globalState.user.settings, {}), + email_on_space_image_declined: true, + email_on_booking_declined: true, + email_on_profile_photo_declined: true, + email_on_new_chat_message: true, + email_on_space_booked: true, + email_on_booking_cancelled: true, + email_on_booking_accepted: true, + }; + } else { + newSettings = { + ...parseJsonSafely(globalState.user.settings, {}), + email_on_space_image_declined: false, + email_on_booking_declined: false, + email_on_profile_photo_declined: false, + email_on_new_chat_message: false, + email_on_space_booked: false, + email_on_booking_cancelled: false, + email_on_booking_accepted: false, + }; + } + try { + await callCustomAPI( + "edit-self", + "post", + { + profile: { settings: JSON.stringify(newSettings) }, + }, + "", + ); + closeModal(); + await sleep(200); + globalDispatch({ type: "SET_USER_DATA", payload: { ...globalState.user, settings: JSON.stringify(newSettings) } }); + } catch (err) { + globalDispatch({ type: "SHOW_ERROR", payload: { heading: "Operation Failed", message: err.message } }); + } + setLoading(false); + } + + return ( + <> + {isOpen &&
} + + + +
+ + +
+
+ + + + {isEnabled ? "Turn Off Email Notifications" : "Enable email notifications?"} + +
+

+ {!isEnabled ? "Enable email notifications on site activity such as booking when booking is declined by host" : "Disabling email notifications on site activity"} +

+
+ +
+ + + Proceed + +
+
+
+
+
+
+
+ + ); +} diff --git a/src/components/Profile/TwoFaDialog.jsx b/src/components/Profile/TwoFaDialog.jsx new file mode 100644 index 0000000..24784f0 --- /dev/null +++ b/src/components/Profile/TwoFaDialog.jsx @@ -0,0 +1,82 @@ +import { LoadingButton } from "@/components/frontend"; +import { Dialog, Transition } from "@headlessui/react"; +import { Fragment } from "react"; + +export default function TwoFaDialog({ isOpen, closeModal, isEnabled, onProceed, loading }) { + return ( + <> + {isOpen &&
} + + + +
+ + +
+
+ + + + {isEnabled ? "Turn Off 2-Step Verification?" : "Protect your account with 2-Step Verification"} + +
+

+ {!isEnabled + ? "Prevent hackers from accessing your account with an additional layer of security. When you sign in, 2-Step Verification helps make sure that your personal information stays private, safe and secure." + : "Turning off 2-Step Verification will remove the extra security on your account, and you’ll only use your password to sign in."} +

+
+ +
+ + + Proceed + +
+
+
+
+
+
+
+ + ); +} diff --git a/src/components/PropertySpaceFiltersModal.jsx b/src/components/PropertySpaceFiltersModal.jsx new file mode 100644 index 0000000..98fbc1a --- /dev/null +++ b/src/components/PropertySpaceFiltersModal.jsx @@ -0,0 +1,233 @@ +import { GlobalContext } from "@/globalContext"; +import { isValidDate, parseSearchParams } from "@/utils/utils"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment, useContext } from "react"; +import { useForm } from "react-hook-form"; +import { useSearchParams } from "react-router-dom"; +import CustomLocationAutoCompleteV2 from "./CustomLocationAutoCompleteV2"; +import DatePickerV3 from "./DatePickerV3"; + +const prices = [ + { + label: "All Prices", + value: "", + }, + { + label: "$0 - $30", + value: "$0 - $30", + }, + { + label: "$31 - $60", + value: "$31 - $60", + }, + { + label: "$60 - $90", + value: "$60 - $90", + }, + { + label: "$90 - $120", + value: "$90 - $120", + }, + { + label: "$120 - $150", + value: "$120 - $150", + }, + { + label: "$150 - $180", + value: "$150 - $180", + }, +]; + +export default function PropertySpaceFiltersModal({ modalOpen, closeModal }) { + const [searchParams, setSearchParams] = useSearchParams(); + const { state: globalState } = useContext(GlobalContext); + const { handleSubmit, register, watch, reset, setValue, control, formState, resetField } = useForm({ + defaultValues: (() => { + const params = parseSearchParams(searchParams); + return { + location: params.location ?? "", + from: isValidDate(params.from ?? "") ? new Date(params.from) : new Date(), + to: isValidDate(params.to ?? "") ? new Date(params.to) : new Date(), + space_name: params.space_name ?? "", + category: params.category ?? "", + price_range: params.price_range ?? "", + direction: "DESC", + }; + })(), + }); + + const { dirtyFields } = formState; + + const fromDate = watch("from"); + + const onSubmit = async (data) => { + console.log("submitting ", data); + searchParams.set("category", data.category); + searchParams.set("price_range", data.price_range); + searchParams.set("space_name", data.space_name); + searchParams.set("location", data.location); + searchParams.set("from", dirtyFields?.from ? data.from.toISOString() : ""); + searchParams.set("to", dirtyFields?.to ? data.to.toISOString() : ""); + setSearchParams(searchParams); + closeModal(); + }; + + return ( + + + +
+ + +
+
+ + +
+
+ + Filters + + +
+ {" "} +
+
+
+ + + setValue("location", val)} + name="location" + className={`w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none`} + placeholder="Location" + hideIcons + /> +
+
+ resetField("from", { keepDirty: false, keepTouched: false })} + setValue={(val) => setValue("from", val, { shouldDirty: true })} + control={control} + name="from" + labelClassName="justify-between flex-grow flex-row-reverse" + placeholder="From" + min={new Date()} + /> +
+
+ resetField("to", { keepDirty: false, keepTouched: false })} + setValue={(val) => setValue("to", val, { shouldDirty: true })} + control={control} + name="to" + labelClassName="justify-between flex-grow flex-row-reverse" + placeholder="To" + min={fromDate} + /> +
+
+ +
+ +
+
+
+
+
+
+ ); +} diff --git a/src/components/PublicHeader.jsx b/src/components/PublicHeader.jsx new file mode 100644 index 0000000..b0704b8 --- /dev/null +++ b/src/components/PublicHeader.jsx @@ -0,0 +1,104 @@ +import { isNotInViewport } from "@/utils/utils"; +import React, { useEffect, useState } from "react"; +import { useLocation, useNavigate } from "react-router"; +import { Link } from "react-router-dom"; +import LogoIcon from "./frontend/icons/LogoIcon"; +import StaticSearchBar from "./frontend/StaticSearchBar"; + +const getNavBarVariant = (path) => { + if (path.startsWith("/account") || path.startsWith("/property") || path.startsWith("/help")) { + return "light"; + } + switch (path) { + case "/contact-us": + case "/faq": + return "white"; + case "/search": + case "/explore": + case "/favorites": + case "/reset-password": + case "/check-verification": + return "light"; + default: + return "transparent"; + } +}; + +export const PublicHeader = () => { + const navigate = useNavigate(); + const { pathname } = useLocation(); + const [variant, setVariant] = useState(getNavBarVariant(pathname)); + const [showStaticBar, setShowStaticBar] = useState(false); + + useEffect(() => { + const onScroll = () => { + if (pathname == "/") { + if (window.scrollY > 10) { + setVariant("white"); + } else { + setVariant("transparent"); + } + } + setShowStaticBar(isNotInViewport("search-bar")); + }; + window.addEventListener("scroll", onScroll); + setShowStaticBar(isNotInViewport("search-bar")); + + return () => { + window.removeEventListener("scroll", onScroll); + }; + }, [pathname]); + + useEffect(() => { + setVariant(getNavBarVariant(pathname)); + }, [pathname]); + + if (pathname.includes("/login") || pathname.includes("/signup")) return null; + + return ( +
+ +
{showStaticBar && (pathname == "/search" || pathname == "/") && }
+ + + +
+ {showStaticBar && (pathname == "/search" || pathname == "/") && } +
+
+ ); +}; + +export default PublicHeader; diff --git a/src/components/ReviewPopUp.jsx b/src/components/ReviewPopUp.jsx new file mode 100644 index 0000000..7911178 --- /dev/null +++ b/src/components/ReviewPopUp.jsx @@ -0,0 +1,258 @@ +import React from "react"; +import { GlobalContext, showToast } from "@/globalContext"; +import { Link } from "react-router-dom"; +import MkdSDK from "@/utils/MkdSDK"; +import Icon from "./Icons"; +import LoadingButton from "@/components/frontend/LoadingButton"; + +export default function ReviewPopUp({ showReview, review }) { + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const stars = Array(5).fill(0); + const [hashtags, setHashtags] = React.useState([]); + const [reason, setReason] = React.useState(""); + const [loading, setLoading] = React.useState(false); + const [acceptLoading, setAcceptLoading] = React.useState(false); + + let sdk = new MkdSDK(); + + async function acceptReview(id) { + sdk.setTable("review"); + setAcceptLoading(true); + try { + const result = await sdk.callRestAPI({ id, status: 1 }, "PUT"); + if (!result.error) { + showToast(globalDispatch, "Review accepted", 4000, "Success") + globalDispatch({ + type: "SHOW_REVIEW", + payload: { + showReview: false, + review: "", + }, + }) + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + setAcceptLoading(false) + } + + async function declineReview(review) { + if (reason.length < 1) { + showToast(globalDispatch, "Please add a reason for declining review", 4000, "ERROR") + } else { + sdk.setTable("review"); + setLoading(true); + try { + const result = await sdk.callRestAPI({ "id": review.id, status: 2 }, "PUT"); + if (!result.error) { + showToast(globalDispatch, "Review declined", 4000, "Error") + globalDispatch({ + type: "SHOW_REVIEW", + payload: { + showReview: false, + review: "", + }, + }) + sendEmailAlert(review.given_by === "host" ? review.host_id : review.customer_id, review?.property_name ?? "property_name") + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + setLoading(false); + } + + } + + async function sendEmailAlert(to, property_name) { + try { + // get receiver preferences + const result = await sdk.callRawAPI("/v2/api/custom/ergo/get-user", { id: to }, "POST"); + if (!result.error) { + const tmpl = await sdk.getEmailTemplate("customer-review-declined"); + const body = tmpl.html + ?.replace(new RegExp("{{{reason}}}", "g"), reason) + .replace(new RegExp("{{{property_name}}}"), property_name) + + // send email + await sdk.sendEmail(result.email, tmpl.subject, body); + } + + } catch (err) { + console.log("ERROR", err); + } + } + + + + async function getHashtags() { + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/review/get-hashtag", + { + where: [`review_id=${review?.id}`], + }, + "POST", + ); + if (!result.error && result?.list) { + setHashtags(result.list); + } + } catch (error) { } + } + + React.useEffect(() => { + getHashtags(); + }, [review?.id]); + + return ( + <> + {showReview ? ( + <> +
+
+ {/*content*/} +
+ {/*header*/} +
+

Review details

+ +
+
+ {/*body*/} +
+ {review?.type === "received" &&

Reviewed By: {review?.given_by}

} +

Review posted on: {review?.create_at}

+

Space name: {review?.category}

+

+ Booking: #{review?.booking_id} + + globalDispatch({ + type: "SHOW_REVIEW", + payload: { + showReview: false, + review: "", + }, + }) + } + > + (view details) + +

+ +

Rating

+

+ {stars.map((_, index) => ( + index ? "stroke-[#FEC84B] fill-[#FEC84B]" : "stroke-[#98A2B3]"} + /> + ))} +

+ +

Hashtags

+
+ {hashtags.map((hashtag) => ( + + {hashtag.name} + + ))} +
+ +

Comment

+

{review?.comment}

+
+ + setReason(e.target.value)} required className=" border mt-2 rounded-lg mt-1 p-2 max-w-xs h-[200px]" /> +
+
+
+ {/* */} + { + acceptReview(review?.id); + }} + > + Accept + + { + declineReview(review); + }} + > + Decline + +
+ {/*footer*/} +
+ +
+
+
+
+
+ + ) : null} + + ); +} diff --git a/src/components/SessionExpiredModal.jsx b/src/components/SessionExpiredModal.jsx new file mode 100644 index 0000000..d9df7de --- /dev/null +++ b/src/components/SessionExpiredModal.jsx @@ -0,0 +1,99 @@ +import { AuthContext } from "@/authContext"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment } from "react"; +import { useEffect } from "react"; +import { useContext } from "react"; +import { useLocation, useNavigate } from "react-router"; + +export default function SessionExpiredModal() { + const { state, dispatch } = useContext(AuthContext); + const { state: globalState, dispatch: globalDispatch } = useContext(AuthContext); + const { pathname } = useLocation(); + const navigate = useNavigate(); + + useEffect(() => { + let modalTimeout; + if (state.sessionExpired) { + modalTimeout = setTimeout(() => { + dispatch({ type: "LOGOUT" }); + if (["superadmin", "admin"].includes(localStorage.getItem("role"))) { + location.href = `/login?redirect_uri=${pathname}`; + } else { + location.href = `/login?redirect_uri=${pathname}`; + } + }, 8000); + } + return () => clearTimeout(modalTimeout); + }, [state.sessionExpired]); + + if (!state.sessionExpired) return null; + + return ( +
+ + { }} + > + +
+ + +
+
+ + + + Session Expired + +
+

Your current login session has expired. Redirecting to login page shortly

+
+ +
+ +
+
+
+
+
+
+
+
+ ); +} diff --git a/src/components/SmartSearch.jsx b/src/components/SmartSearch.jsx new file mode 100644 index 0000000..4f805c7 --- /dev/null +++ b/src/components/SmartSearch.jsx @@ -0,0 +1,89 @@ +import React, { Fragment, useState } from "react"; +import { Combobox, Transition } from "@headlessui/react"; +import { debounce } from "@/utils/utils"; + +const SmartSearch = ({ selectedData, setSelectedData, data, field, field2, errorField, getData, setError, type, multiple = false }) => { + const [query, setQuery] = useState(""); + + return ( + { + setSelectedData(item); + setError(errorField, { + type: "manual", + message: null + }); + }} + disabled={type ? true : false} + multiple={multiple} + > +
+
+ + !field2 + ? multiple + ? item.map((it) => it[field]).join(",") + : item[field] + : item !== undefined && item[field] !== "" + ? multiple + ? item.map((it) => `${item[field]} - ${item[field2]}`) + : `${item[field]} - ${item[field2]}` + : "" + } + onChange={(event) => { + setQuery(event.target.value); + let searchValue = event.target.value; + if (multiple) { + let splitResult = searchValue.split(","); + let index = splitResult.length > 1 ? splitResult.length - 1 : 0; + searchValue = splitResult[index]; + } + debounce(() => getData(1, 10, { [field]: searchValue.trim() })); + if (event.target.value === "") { + const emptyParam = { [field]: "" }; + if (field2) { + emptyParam[field2] = ""; + } + setSelectedData(multiple ? [] : { ...emptyParam }); + } + }} + /> +
+ setQuery("")} + > + + {data && data.length === 0 && query !== "" ? ( +
Nothing found.
+ ) : ( + data && + data.map((item) => ( + `relative normal-case cursor-default select-none py-2 pl-10 pr-4 ${active ? "bg-teal-600 text-white" : "text-gray-900"}`} + value={item} + > + {({ selected }) => ( + <> + {!field2 ? item[field] : `${item[field]} - ${item[field2]}`} + + )} + + )) + )} +
+
+
+
+ ); +}; + +export default SmartSearch; diff --git a/src/components/SmartSearchV2.jsx b/src/components/SmartSearchV2.jsx new file mode 100644 index 0000000..63406d8 --- /dev/null +++ b/src/components/SmartSearchV2.jsx @@ -0,0 +1,73 @@ +import { Combobox, Transition } from "@headlessui/react"; +import React, { Fragment, useState } from "react"; + +export default function SmartSearchV2({ data, fieldToDisplay, setSelected, selected, placeholder }) { + const [query, setQuery] = useState(""); + const filteredData = + query === "" + ? data + : data + .filter((cat) => cat[fieldToDisplay].toLowerCase().replace(/\s+/g, "").includes(query.toLowerCase().replace(/\s+/g, ""))) + .sort((a, b) => { + if (a[fieldToDisplay].toLowerCase().indexOf(query.toLowerCase()) > b[fieldToDisplay].toLowerCase().indexOf(query.toLowerCase())) { + return 1; + } else if (a[fieldToDisplay].toLowerCase().indexOf(query.toLowerCase()) < b[fieldToDisplay].toLowerCase().indexOf(query.toLowerCase())) { + return -1; + } else { + if (a[fieldToDisplay] > b[fieldToDisplay]) return 1; + else return -1; + } + }); + + return ( + <> + {" "} +
+ +
+
+ cat[fieldToDisplay]} + onChange={(event) => setQuery(event.target.value)} + placeholder={placeholder ?? "Type to search.."} + /> +
+ setQuery("")} + > + + {filteredData.length === 0 && query !== "" ? ( +
Other
+ ) : ( + filteredData + .filter((cat) => cat[fieldToDisplay] != "") + .map((cat) => ( + `relative cursor-default select-none py-2 px-4 ${active ? "bg-teal-600 text-white" : "text-gray-900"}`} + value={cat} + > + {({ selected, active }) => ( + <> + {cat[fieldToDisplay]} + + )} + + )) + )} +
+
+
+
+
+ + ); +} diff --git a/src/components/SnackBar.jsx b/src/components/SnackBar.jsx new file mode 100644 index 0000000..75388e1 --- /dev/null +++ b/src/components/SnackBar.jsx @@ -0,0 +1,44 @@ +import React from "react"; +import { GlobalContext } from "@/globalContext"; +const SnackBar = () => { + const { state, dispatch } = React.useContext(GlobalContext); + const show = state.globalMessage.length > 0; + + return show ? ( + + ) : null; +}; + +export default SnackBar; diff --git a/src/components/StickyCustomLocationAutoComplete.jsx b/src/components/StickyCustomLocationAutoComplete.jsx new file mode 100644 index 0000000..78c7b1d --- /dev/null +++ b/src/components/StickyCustomLocationAutoComplete.jsx @@ -0,0 +1,105 @@ +import { Combobox, Transition } from "@headlessui/react"; +import React, { Fragment } from "react"; +import usePlacesService from "react-google-autocomplete/lib/usePlacesAutocompleteService"; +import { useController } from "react-hook-form"; +import LocationIcon from "./frontend/icons/LocationIcon"; + +export default function StickyCustomLocationAutoComplete({ control, name, setValue, onClear, className, containerClassName, hideIcons, suggestionType, ...restProps }) { + const { field } = useController({ control, name }); + + const { placePredictions, getPlacePredictions, isPlacePredictionsLoading } = usePlacesService({ + apiKey: import.meta.env.VITE_GOOGLE_API_KEY, + options: { types: suggestionType ?? ["(region)"] }, + debounce: 200, + }); + + return ( + + {!hideIcons && } + + { + field.onChange(evt); + getPlacePredictions({ input: evt.target.value }); + }} + /> + {!hideIcons && field.value && ( + + )} + + {isPlacePredictionsLoading ? ( +
+ + + + + +
+ ) : ( + 0 ? "py-2 shadow-lg ring-1 sticky" : "" + } absolute left-0 right-0 top-full z-50 mt-2 w-full origin-top cursor-pointer divide-y divide-gray-100 rounded-xl bg-white ring-black ring-opacity-5 focus:outline-none`} + > + {placePredictions.map((place, idx) => ( + setValue(place?.structured_formatting.main_text + ', ' + place.structured_formatting?.secondary_text)} + > + {`${place.structured_formatting.main_text} ${place.structured_formatting?.secondary_text ? "," : ""} ${place.structured_formatting?.secondary_text ? place.structured_formatting?.secondary_text : ""}`} + + ))} + + )} +
+
+ ); +} diff --git a/src/components/SwitchBulkMode.jsx b/src/components/SwitchBulkMode.jsx new file mode 100644 index 0000000..395a77f --- /dev/null +++ b/src/components/SwitchBulkMode.jsx @@ -0,0 +1,22 @@ +import { useState } from "react"; +import { Switch } from "@headlessui/react"; + +export default function SwitchBulkMode({ enabled, setEnabled }) { + return ( +
+ + Use setting + +
+ ); +} diff --git a/src/components/Table.jsx b/src/components/Table.jsx new file mode 100644 index 0000000..746b124 --- /dev/null +++ b/src/components/Table.jsx @@ -0,0 +1,452 @@ +import React, { useContext } from "react"; +import { useNavigate } from "react-router-dom"; +import { GlobalContext } from "@/globalContext"; +import moment from "moment"; +import { IMAGE_STATUS } from "@/utils/constants"; + +const Table = ({ + columns, + rows, + actions, + profile, + tableType, + type, + table1, + table2, + deleteMessage, + deleteTitle, + showDelete = true, + onSort, + id, + rejectImage, + approveImage, + setActivePicture, + openPictureModal, + baasDelete, + emailActions, +}) => { + const { dispatch: globalDispatch } = useContext(GlobalContext); + const navigate = useNavigate(); + + return ( + + + + {columns.map((column, index) => ( + + ))} + + + + {rows?.length > 0 && rows.map((row, i) => { + return ( + + {columns.map((cell, index) => { + if (cell.accessor.split(",").length > 1) { + return ( + + ); + } + if (cell.accessor === "" && emailActions) { + return ( + + ); + } + if (cell.accessor === "" && profile && cell.viewProperty) { + return ( + + ); + } + + if (cell.accessor === "" && actions) { + return ( + + ); + } + if (cell.accessor === "" && profile) { + return ( + + ); + } + + if (cell.accessor == "image" || cell.accessor == "photo_url") { + return ( + + ); + } + if (cell.accessor == "icon") { + return ( + + ); + } + if (cell.statusMapping) { + return ( + + ); + } + if (cell.mapping) { + return ( + + ); + } + + if (cell.accessor == "dob") { + return ( + + ); + } + if (cell.accessor?.includes("email")) { + return ( + + ); + } + if (cell.accessor == "num_properties") { + return ( + + ); + } + if (cell.accessor.includes("payout") || cell.amountField) { + return ( + + ); + } + if (cell.formatDate) { + return ( + + ); + } + + if (cell.nested) { + return ( + + ); + } + + if (cell.idPrefix) { + return ( + + ); + } + + if (cell.format) { + return ( + + ); + } + if (cell.accessor === "cost") { + return ( + + ); + } + + return ( + + ); + })} + + ); + })} + +
onSort(column.accessor)} + > + {column.header} + {column.isSorted} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
+ {cell.accessor.split(",").map((accessor, i) => ( + + {row[accessor.trim()]} + + ))} + + + {showDelete && ( + + )} + + + + {showDelete && ( + + )} + + + + {row?.is_photo_approved == IMAGE_STATUS.IN_REVIEW ? ( + <> + + + + ) : row?.is_photo_approved === IMAGE_STATUS.APPROVED ? ( + + ) : ( + + )} + + + + {showDelete && ( + + )} + + image + + icon + + + {" "} + {cell.statusMapping[row[cell.accessor]]} + + + {cell.mapping[row[cell.accessor]] ?? "N/A"} + + {row[cell.accessor] ? moment(row[cell.accessor]).format("MM/DD/YY") : ""} + + {row[cell.accessor]} + + + {/* {row[cell.accessor]} */} + + ${(row[cell.accessor] ? row[cell.accessor] : 0).toFixed(2)} + + {new Date(row[cell.accessor]).toUTCString()} + + {row[cell.nested][cell.accessor]} + + {cell.idPrefix + row[cell.accessor]} + + {cell.format(row[cell.accessor])} + + ${row[cell.accessor]} + + {row[cell.accessor]} + +
+ ); +}; + +export default Table; diff --git a/src/components/TopHeader.jsx b/src/components/TopHeader.jsx new file mode 100644 index 0000000..ac2926a --- /dev/null +++ b/src/components/TopHeader.jsx @@ -0,0 +1,52 @@ +import React from "react"; +import { GlobalContext } from "@/globalContext"; +const TopHeader = () => { + const { state, dispatch } = React.useContext(GlobalContext); + let { isOpen } = state; + let toggleOpen = (open) => + dispatch({ + type: "OPEN_SIDEBAR", + payload: { isOpen: open }, + }); + return ( +
+ toggleOpen(!isOpen)}> + {!isOpen ? ( + + ) : ( + + )} + +
+ ); +}; + +export default TopHeader; diff --git a/src/components/frontend/AddOnCounter.jsx b/src/components/frontend/AddOnCounter.jsx new file mode 100644 index 0000000..d045d53 --- /dev/null +++ b/src/components/frontend/AddOnCounter.jsx @@ -0,0 +1,51 @@ +import React from "react"; +import { useState } from "react"; + +const AddOnCounter = ({ data, register, singleName }) => { + const [counter, setCounter] = useState(1); + console.log(singleName) + + + return ( +
+
+ + +
+ +
+ {data.showCounter && ( +
+ + {counter} + +
+ )} +

${data.cost * counter}

+
+
+ ); +}; + +export default AddOnCounter; diff --git a/src/components/frontend/AddonCounterV2.jsx b/src/components/frontend/AddonCounterV2.jsx new file mode 100644 index 0000000..8492c63 --- /dev/null +++ b/src/components/frontend/AddonCounterV2.jsx @@ -0,0 +1,44 @@ +import React from "react"; +import { useState } from "react"; + +export default function AddonCounterV2({ data, register, name }) { + const [counter, setCounter] = useState(1); + return ( +
+
+ + +
+
+ {data.showCounter && ( +
+ + {counter} + +
+ )} +

${data.cost * counter}

+
+
+ ); +} diff --git a/src/components/frontend/AllReviewsModal.jsx b/src/components/frontend/AllReviewsModal.jsx new file mode 100644 index 0000000..c3916c4 --- /dev/null +++ b/src/components/frontend/AllReviewsModal.jsx @@ -0,0 +1,84 @@ +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment } from "react"; +import CustomSelect from "./CustomSelect"; +import ReviewCard from "./ReviewCard"; + +export default function AllReviewsModal({ modalOpen, closeModal, reviews, onDirectionChange }) { + return ( + + + +
+ + +
+
+ + +
+ + All Reviews ({reviews.length}) + +
+ + +
+
+
+
+ {reviews.map((rw) => ( + + ))} +
+
+
+
+
+
+
+ ); +} diff --git a/src/components/frontend/AvailabilityTemplate.jsx b/src/components/frontend/AvailabilityTemplate.jsx new file mode 100644 index 0000000..60a4557 --- /dev/null +++ b/src/components/frontend/AvailabilityTemplate.jsx @@ -0,0 +1,117 @@ +import React, { useState } from "react"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import { parseJsonSafely } from "@/utils/utils"; +import PencilIcon from "./icons/PencilIcon"; +import TrashIcon from "./icons/TrashIcon"; +import { formatAMPM, daysMapping } from "@/utils/date-time-utils"; +import { useContext } from "react"; +import { GlobalContext } from "@/globalContext"; +import ThreeDotsMenu from "./ThreeDotsMenu"; +import EditTemplateModal from "@/pages/Host/Spaces/Add/EditTemplateModal"; + +const AvailabilityTemplate = ({ data, forceRender, selectedTemplate, setSelectedTemplate }) => { + const [editPopup, setEditPopup] = useState(false); + const { dispatch: globalDispatch } = useContext(GlobalContext); + + const parsedSlots = parseJsonSafely(data.slots, []); + + async function deleteTemplate(id) { + globalDispatch({ type: "START_LOADING" }); + try { + await callCustomAPI("host/schedule-slot/template", "delete", { id }, ""); + if (forceRender) forceRender(new Date()); + } catch (err) { + globalDispatch({ type: "STOP_LOADING" }); + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + return ( +
+
+
+
+

{data.template_name}

+

+ ( + {daysMapping + .filter((day) => data[day] == 1) + .map((day, i, arr) => { + return day + (i == arr.length - 1 ? "" : ", "); + })} + ) +

+
+
+ , + onClick: () => setEditPopup(true), + }, + { + label: "Delete", + icon: , + onClick: () => deleteTemplate(data.id), + }, + ]} + menuClassName="right-[unset] left-0 origin-top-left" + /> +
+
+
+ {Array.isArray(parsedSlots) && + parsedSlots.slice(0, 2).map((slot, idx) => ( +
+

Slot {idx + 1}:

+

+ {formatAMPM(slot.start)} - {formatAMPM(slot.end)} +

+
+ ))} +
+
+
+
+ + +
+ + { + if (forceRender) forceRender(); + }} + modalOpen={editPopup} + closeModal={() => setEditPopup(false)} + /> +
+ ); +}; + +export default AvailabilityTemplate; diff --git a/src/components/frontend/BottomNav.jsx b/src/components/frontend/BottomNav.jsx new file mode 100644 index 0000000..9282d32 --- /dev/null +++ b/src/components/frontend/BottomNav.jsx @@ -0,0 +1,79 @@ +import { AuthContext } from "@/authContext"; +import { isNotInViewport } from "@/utils/utils"; +import React, { useEffect, useState } from "react"; +import { useContext } from "react"; +import { useLocation, useNavigate } from "react-router"; +import { NavLink } from "react-router-dom"; +import Icon from "../Icons"; +import HeartIcon from "./icons/HeartIcon"; +import LogoutIcon from "./icons/LogoutIcon"; +import SearchIcon from "./icons/SearchIcon"; + +export default function BottomNav({ scrollDir, showAccount }) { + const { pathname } = useLocation(); + const navigate = useNavigate(); + const { dispatch } = useContext(AuthContext); + + const [showStaticBar, setShowStaticBar] = useState(false); + + useEffect(() => { + const onScroll = () => { + setShowStaticBar(isNotInViewport("search-bar")); + }; + window.addEventListener("scroll", onScroll); + setShowStaticBar(isNotInViewport("search-bar")); + + return () => { + window.removeEventListener("scroll", onScroll); + }; + }, [pathname]); + + function logout() { + dispatch({ type: "LOGOUT" }); + navigate("/"); + } + + const whiteList = ["/search", "/"]; + + if (!whiteList.some((path) => pathname == path)) return null; + + return ( +
+
+ + + Explore + + + + Favorites + + + + {showAccount ? "Account" : "Login"} + + +
+
+ ); +} diff --git a/src/components/frontend/Counter.jsx b/src/components/frontend/Counter.jsx new file mode 100644 index 0000000..df52ae4 --- /dev/null +++ b/src/components/frontend/Counter.jsx @@ -0,0 +1,51 @@ +import React from "react"; +import { useEffect } from "react"; +import { useState } from "react"; + +const Counter = ({ register, setValue, name, initialValue, maxCount, minCount }) => { + const [counter, setCounter] = useState(Number(initialValue) || 0); + + useEffect(() => { + setValue(name, counter); + }, [counter]); + + useEffect(() => { + setCounter(initialValue || 0); + }, [initialValue]); + + return ( +
+ + {counter} + + +
+ ); +}; + +export default Counter; diff --git a/src/components/frontend/CustomLocationAutoComplete.jsx b/src/components/frontend/CustomLocationAutoComplete.jsx new file mode 100644 index 0000000..e275a78 --- /dev/null +++ b/src/components/frontend/CustomLocationAutoComplete.jsx @@ -0,0 +1,140 @@ +import { Menu, Transition } from "@headlessui/react"; +import React, { Fragment } from "react"; +import { useState } from "react"; +import usePlacesService from "react-google-autocomplete/lib/usePlacesAutocompleteService"; +import LocationIcon from "./icons/LocationIcon"; + +function truncateString(str, limit) { + if (str.length > limit) return str.slice(0, limit) + "..."; + return str; +} + +export default function CustomLocationAutoComplete({ location, setLocation, className, truncateNum, onChange, onClear, hideIcon, detailMode, ...restProps }) { + const [predictionsOpen, setPredictionsOpen] = useState(false); + // const [predictions, setPredictions] = useState([]); + + const { placesService, placePredictions, getPlacePredictions, isPlacePredictionsLoading } = usePlacesService({ + apiKey: import.meta.env.VITE_GOOGLE_API_KEY, + }); + + // useEffect(() => { + // fetch place details for the first element in placePredictions array + // if (placePredictions.length) + // placesService?.getDetails( + // { + // placeId: placePredictions[0].place_id, + // }, + // (placeDetails) => setPredictions(placeDetails), + // ); + // }, [placePredictions]); + + return ( + + {!hideIcon && } + + { + getPlacePredictions({ input: evt.target.value }); + setLocation(evt.target.value); + if (onChange) { + onChange(evt.target.value); + } + }} + onFocus={() => setPredictionsOpen(true)} + onBlur={() => setPredictionsOpen(false)} + value={location} + /> + {location && ( + + )} + + + 0 ? "py-2 shadow-lg ring-1" : "" + } z-50 absolute left-0 right-0 top-full mt-2 w-full origin-top divide-y divide-gray-100 rounded-xl bg-white ring-black ring-opacity-5 focus:outline-none`} + > + {!detailMode && + placePredictions.map((place, idx) => ( +
+ + {({ active }) => ( + + )} + +
+ ))} + {detailMode && + placePredictions.map((place, idx) => ( +
+ + {({ active }) => ( + + )} + +
+ ))} +
+
+
+ ); +} diff --git a/src/components/frontend/CustomSelect.jsx b/src/components/frontend/CustomSelect.jsx new file mode 100644 index 0000000..362e553 --- /dev/null +++ b/src/components/frontend/CustomSelect.jsx @@ -0,0 +1,121 @@ +import { Fragment, useState } from "react"; +import { Listbox, Transition } from "@headlessui/react"; +import NextIcon from "./icons/NextIcon"; +import { useEffect } from "react"; + +export default function CustomSelect({ + options, + accessor, + name, + register, + setValue, + formMode, + valueAccessor, + defaultValue, + className, + optionsClassName, + defaultOptionClassName, + onChange, + initialEditValue, + buttonClassName, + listOptionClassName, + noSelectedHighlight, + hideIcon, +}) { + const [selected, setSelected] = useState(defaultValue ?? options[0]); + + useEffect(() => { + if (formMode) { + if (selected == defaultValue) { + setValue(name, ""); + } else { + setValue(name, valueAccessor ? selected[valueAccessor] : selected); + } + } + }, [selected]); + + useEffect(() => { + if (formMode && defaultValue) { + setValue(name, ""); + } + }, []); + + useEffect(() => { + if (initialEditValue) { + setSelected(initialEditValue); + } + }, [JSON.stringify(initialEditValue)]); + + return ( +
+ { + setSelected(v); + if (onChange) { + onChange(valueAccessor ? v[valueAccessor] : v); + } + }} + > + {formMode && ( + + )} + +
+ + {accessor ? selected[accessor] : selected} + + + {" "} + + + + {defaultValue && ( + `relative cursor-default select-none py-2 pr-4 ${active ? "bg-amber-100 text-amber-900" : "text-gray-900"} ${listOptionClassName ?? "pl-10"}`} + value={defaultValue} + > + {({ selected }) => ( + <> + {accessor ? defaultValue[accessor] : defaultValue} + {selected && !noSelectedHighlight ? : null} + + )} + + )} + {options.map((option, idx) => ( + `relative cursor-default select-none py-2 ${listOptionClassName ?? "pl-10"} pr-4 ${active ? "bg-amber-100 text-amber-900" : "text-gray-900"}`} + value={option} + > + {({ selected }) => ( + <> + {accessor ? option[accessor] : option} + {selected && !noSelectedHighlight ? : null} + + )} + + ))} + + +
+
+
+ ); +} diff --git a/src/components/frontend/DatePicker.jsx b/src/components/frontend/DatePicker.jsx new file mode 100644 index 0000000..897f5bf --- /dev/null +++ b/src/components/frontend/DatePicker.jsx @@ -0,0 +1,97 @@ +import { formatDate, isSameDay } from "@/utils/date-time-utils"; +import { Popover, Transition } from "@headlessui/react"; +import React, { Fragment, useEffect, useState } from "react"; +import { Calendar } from "react-calendar"; +import CalendarIcon from "./icons/CalendarIcon"; +import NextIcon from "./icons/NextIcon"; +import PrevIcon from "./icons/PrevIcon"; +import { useController } from "react-hook-form"; + +const DatePicker = ({ initialDate, searchDate, control, setSearchDate, className, placeHolder, min, max, onChange, onReset, labelClassName, xClassName, panelClassName, hideIcon }) => { + let isInitial = isSameDay(searchDate, initialDate); + const { field, fieldState } = useController({ control, name }); + const [date, setDate] = useState(new Date()); + + useEffect(() => { + if (!isNaN(new Date(field.value))) setDate(new Date(field.value)); + }, [field.value]); + return ( +
+ + {({ open }) => ( + <> + +
+ {/* {!hideIcon ? : null} */} + {isInitial ? ( + + ) : ( + + )} + {!isInitial ? formatDate(searchDate) : placeHolder ?? "Select date"} +
+
+ + +
+ { + setSearchDate(v); + + if (onChange) { + onChange(v); + } + }} + value={date} + className={`calendar date-picker`} + defaultValue={initialDate} + nextLabel={} + prevLabel={} + next2Label={ +
e.stopPropagation()} + >
+ } + prev2Label={ +
e.stopPropagation()} + >
+ } + minDate={min} + maxDate={max} + maxDetail="month" + /> +
+
+
+ + )} +
+
+ ); +}; + +export default DatePicker; diff --git a/src/components/frontend/DatePickerV2.jsx b/src/components/frontend/DatePickerV2.jsx new file mode 100644 index 0000000..174788a --- /dev/null +++ b/src/components/frontend/DatePickerV2.jsx @@ -0,0 +1,76 @@ +import { Popover, Transition } from "@headlessui/react"; +import moment from "moment"; +import React, { Fragment, useState } from "react"; +import { useEffect } from "react"; +import { Calendar } from "react-calendar"; +import { useController } from "react-hook-form"; +import CalendarIcon from "./icons/CalendarIcon"; +import NextIcon from "./icons/NextIcon"; +import PrevIcon from "./icons/PrevIcon"; + +export default function DatePickerV2({ control, name, min, type, max, setValue, classNameCustomized }) { + const { field, fieldState } = useController({ control, name }); + const [date, setDate] = useState(new Date()); + const [showCalender, setShowCalender] = useState(false); + + useEffect(() => { + if (!isNaN(new Date(field.value))) setDate(new Date(field.value)); + }, [field.value]); + + return ( +
+
+ +
+ D.O.B + +
+
+ {showCalender && +
+ { + setValue(moment(v).format("yyyy-MM-DD")); + setShowCalender(false); + }} + value={date} + className={`calendar date-picker`} + nextLabel={} + prevLabel={} + next2Label={ +
e.stopPropagation()} + >
+ } + prev2Label={ +
e.stopPropagation()} + >
+ } + minDate={min} + maxDate={max} + maxDetail="month" + /> +
+ } + +
+ ); +} diff --git a/src/components/frontend/DateTimePicker.jsx b/src/components/frontend/DateTimePicker.jsx new file mode 100644 index 0000000..33b5221 --- /dev/null +++ b/src/components/frontend/DateTimePicker.jsx @@ -0,0 +1,211 @@ +import { fullMonthsMapping, hourlySlots, monthsMapping, daysMapping } from "@/utils/date-time-utils"; +import { formatScheduleDate, parseJsonSafely } from "@/utils/utils"; +import moment from "moment"; +import React, { useState } from "react"; +import { Calendar } from "react-calendar"; +import CalendarIcon from "./icons/CalendarIcon"; +import NextIcon from "./icons/NextIcon"; +import PrevIcon from "./icons/PrevIcon"; + +const DateTimePicker = ({ defaultDate, register, fieldNames, setValue, showCalendar, setShowCalendar, toDefault, fromDefault, bookedSlots, scheduleTemplate, defaultMessage }) => { + const [selectedDate, setSelectedDate] = useState(defaultDate ?? new Date()); + const [from, setFrom] = useState(fromDefault ?? ""); + const [to, setTo] = useState(toDefault ?? ""); + + const onApply = () => { + setValue("from", from); + setValue("to", to); + setValue("selectedDate", selectedDate); + setShowCalendar(false); + }; + + return ( +
setShowCalendar((prev) => !prev)} + > + {fieldNames.map((field, idx) => ( + + ))} + + +
+
+
+ { + setSelectedDate(newDate); + setFrom(""); + setTo(""); + }} + value={selectedDate} + className={`custom-calendar`} + nextLabel={} + prevLabel={} + next2Label={<>} + prev2Label={<>} + tileDisabled={({ date }) => { + let customSlots = []; + try { + if (scheduleTemplate?.custom_slots && (Object.keys(scheduleTemplate?.custom_slots))?.length > 0) { + customSlots = JSON.parse(scheduleTemplate?.custom_slots || "[]"); + } + } catch (e) { + console.error("Invalid JSON in custom_slots", e); + } + if (customSlots.length > 0 && customSlots[(formatScheduleDate(date)).toString()]?.length === 0) { + return true; + } + if (scheduleTemplate?.id && scheduleTemplate[daysMapping[date.getDay()]] != 1) { + return true; + } + }} + minDate={new Date()} + maxDetail="month" + /> +

Pacific Time - US & Canada

+
+

From - {from}

+

Until - {to}

+
+
+
+

+ {daysMapping[selectedDate.getDay()]} , {fullMonthsMapping[selectedDate.getMonth()]} {selectedDate.getDate()} +

+
+ {hourlySlots.map((tm, idx) => { + var formattedDate = moment(selectedDate).format("MM/DD/YY"); + var fromTime = new Date(formattedDate + " " + from); + var toTime = new Date(formattedDate + " " + to); + var slotTime = new Date(formattedDate + " " + tm); + var slotTimeOnly = new Date("01/01/2001" + " " + tm); + var json = scheduleTemplate.custom_slots ?? "[]"; + var custom_slots_obj = parseJsonSafely(json, {}); + var custom_slots = custom_slots_obj[formattedDate] ?? []; + custom_slots = custom_slots.map((slot) => ({ fromTime: new Date(slot.start), toTime: new Date(slot.end) })); + var template_slots = Array.isArray(scheduleTemplate.slots) ? scheduleTemplate.slots.map((slot) => ({ fromTime: new Date(slot.start), toTime: new Date(slot.end) })) : []; + + return ( + + ); + })} +
+
+ +
+
+

From - {from}

+

Until - {to}

+
+
+
+ + } + + + ); +}; + +export default DateTimePicker; \ No newline at end of file diff --git a/src/components/frontend/DraftProgress.jsx b/src/components/frontend/DraftProgress.jsx new file mode 100644 index 0000000..1831236 --- /dev/null +++ b/src/components/frontend/DraftProgress.jsx @@ -0,0 +1,46 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import { DRAFT_STATUS } from "@/utils/constants"; + +const DraftProgress = ({ data, scheduleTemplate }) => { + return ( +
+
+
= DRAFT_STATUS.IMAGES ? "login-btn-gradient" : ""} h-full flex-grow`}>
+
DRAFT_STATUS.SCHEDULING ? "login-btn-gradient" : ""} h-full flex-grow`}>
+
+
+ = DRAFT_STATUS.PROPERTY_SPACE ? "property-space?mode=edit" : "property-space?mode=create"}`} + className={`draft-stage ${data.draft_status >= DRAFT_STATUS.PROPERTY_SPACE ? "complete" : ""}`} + state={data} + > + 1 + +

About location

+
+
+ = DRAFT_STATUS.IMAGES ? "images?mode=edit" : "images?mode=create"}`} + className={`draft-stage ${data.draft_status >= DRAFT_STATUS.IMAGES ? "complete" : ""}`} + state={data} + > + 2 + +

Images, Addons, FAQs etc

+
+
+ DRAFT_STATUS.SCHEDULING ? "complete" : ""}`} + state={scheduleTemplate} + > + 3 + +

Templates & Scheduling

+
+
+ ); +}; + +export default DraftProgress; diff --git a/src/components/frontend/FaqAccordion.jsx b/src/components/frontend/FaqAccordion.jsx new file mode 100644 index 0000000..dc52775 --- /dev/null +++ b/src/components/frontend/FaqAccordion.jsx @@ -0,0 +1,40 @@ +import React, { useState } from "react"; + +const FaqAccordion = ({ data }) => { + const [open, setOpen] = useState(false); + + return ( +
+
+ +
+

+
+ ); +}; + +export default FaqAccordion; diff --git a/src/components/frontend/FaqTile.jsx b/src/components/frontend/FaqTile.jsx new file mode 100644 index 0000000..89d7aff --- /dev/null +++ b/src/components/frontend/FaqTile.jsx @@ -0,0 +1,37 @@ +import { Transition } from "@headlessui/react"; +import React, { Fragment, useState } from "react"; + +const FaqTile = ({ data }) => { + const [open, setOpen] = useState(false); + + return ( +
+
setOpen((prev) => !prev)} + > +
+

{data.question}

+ +
+
+ +

+
+
+ ); +}; + +export default FaqTile; diff --git a/src/components/frontend/FavoriteButton.jsx b/src/components/frontend/FavoriteButton.jsx new file mode 100644 index 0000000..a4d0e10 --- /dev/null +++ b/src/components/frontend/FavoriteButton.jsx @@ -0,0 +1,207 @@ +import { AuthContext } from "@/authContext"; +import HeartIcon from "@/components/frontend/icons/HeartIcon"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import React, { useContext, useState } from "react"; +import useDelayUnmount from "@/hooks/useDelayUnmount"; +import { Tooltip } from "react-tooltip"; + +function FavoriteButton({ space_id, user_property_spaces_id, reRender, withLoader, className, buttonClassName, stroke, favColor }) { + const [unfavorite, setUnfavorite] = useState(false); + const showUnfavorite = useDelayUnmount(unfavorite, 100); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const { state: authState } = useContext(AuthContext); + const sdk = new MkdSDK(); + async function favorite() { + if (withLoader) { + globalDispatch({ type: "START_LOADING" }); + } + sdk.setTable("user_property_spaces"); + try { + await sdk.callRestAPI({ property_spaces_id: space_id, user_id: authState.user }, "POST"); + if (reRender) { + reRender(new Date()); + } + globalDispatch({ type: "STOP_LOADING" }); + } catch (err) { + globalDispatch({ type: "STOP_LOADING" }); + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function unFavorite() { + if (withLoader) { + globalDispatch({ type: "START_LOADING" }); + } + sdk.setTable("user_property_spaces"); + try { + await sdk.callRestAPI({ id: user_property_spaces_id }, "DELETE"); + if (reRender) { + reRender(new Date()); + } + globalDispatch({ type: "STOP_LOADING" }); + setUnfavorite(false); + } catch (err) { + globalDispatch({ type: "STOP_LOADING" }); + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation Failed", + message: err.message, + }, + }); + } + } + + async function toggleFavorite() { + if (user_property_spaces_id) { + setUnfavorite(true); + } else { + favorite(); + } + } + + return ( +
+ + {showUnfavorite && ( +
{ + e.preventDefault(); + e.stopPropagation(); + setUnfavorite(false); + { + showUnfavorite && ( +
{ + e.preventDefault(); + e.stopPropagation(); + setUnfavorite(false); + }} + > +
e.stopPropagation()} + > +
+

Are you sure?

+ +
+

Are you sure you want to remove this space from your favorites?

+
+ + +
+
+
+ ); + } + }} + > +
e.stopPropagation()} + > +
+

Are you sure?

+ +
+

Are you sure you want to remove this space from your favorites?

+
+ + +
+
+
+ )} + {/* */} +
+ ); +} + +export default FavoriteButton; diff --git a/src/components/frontend/FilterCheckBoxes.jsx b/src/components/frontend/FilterCheckBoxes.jsx new file mode 100644 index 0000000..a9c2d40 --- /dev/null +++ b/src/components/frontend/FilterCheckBoxes.jsx @@ -0,0 +1,88 @@ +import React, { useEffect, useState } from "react"; +import { useSearchParams } from "react-router-dom"; +import StarIcon from "./icons/StarIcon"; + +const FilterCheckBoxes = ({ name, options, searchField, query, optionFieldName, filterPopup }) => { + const [searchParams, setSearchParams] = useSearchParams(); + const [open, setOpen] = useState(true); + + const uncheckAll = () => { + searchParams.set(searchField, ""); + setSearchParams(searchParams); + }; + + const updateSearchQuery = (e) => { + e.preventDefault(); + var prev = searchParams.get(searchField); + prev = prev?.split(",") || []; + if (!prev.includes(e.target.name)) { + prev.push(e.target.name); + } else { + prev.splice(prev.indexOf(e.target.name), 1); + } + searchParams.set(searchField, prev.join(",")); + setSearchParams(searchParams); + }; + + useEffect(() => { + if (filterPopup) { + setOpen(true); + } + }, [filterPopup]); + + return ( +
+
+

+ {name} + +

+ +
+
+ {options.map((op) => ( +
+ {}} + /> + +
+ ))} +
+
+ ); +}; + +export default FilterCheckBoxes; diff --git a/src/components/frontend/Footer.jsx b/src/components/frontend/Footer.jsx new file mode 100644 index 0000000..6a7c465 --- /dev/null +++ b/src/components/frontend/Footer.jsx @@ -0,0 +1,122 @@ +import { AuthContext } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import React from "react"; +import { useMemo } from "react"; +import { useContext } from "react"; +import { Link, useLocation, useNavigate } from "react-router-dom"; +import LogoIcon from "./icons/LogoIcon"; + +const Footer = () => { + const { state: authState, dispatch: authDispatch } = useContext(AuthContext); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const navigate = useNavigate(); + + const { pathname } = useLocation(); + + const blackList = useMemo(() => ["/admin", "/login", "/account/messages", "/signup"], []); + + function switchToHost() { + authDispatch({ type: "SWITCH_TO_HOST" }); + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Success", + message: `You are now signed in as a host`, + btn: "Ok got it", + }, + }); + } + + function switchToCustomer() { + authDispatch({ type: "SWITCH_TO_CUSTOMER" }); + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Success", + message: `You are now signed in as a customer`, + btn: "Ok got it", + }, + }); + } + + function switchToHostOrCustomer() { + if (authState.role == "host") { + switchToCustomer(); + } else { + switchToHost(); + } + navigate("/"); + } + + if (blackList.some((path) => pathname.startsWith(path))) return null; + + return ( +
+
+
+
+ + + +
+ {(authState.role == "host" || authState.role == "customer") && ( + <> + {authState.originalRole != "customer" ? ( + + ) : ( + + Host Your Space + + )} + + )} +
+
+ + FAQs + + + Contact us + +
+
+
+
+ ergo © All rights reserved +
+
+ + Terms and conditions + + + Privacy and policy + +
+
+
+
+
+ ); +}; + +export default Footer; diff --git a/src/components/frontend/HostCard.jsx b/src/components/frontend/HostCard.jsx new file mode 100644 index 0000000..1804e80 --- /dev/null +++ b/src/components/frontend/HostCard.jsx @@ -0,0 +1,60 @@ + +import { IMAGE_STATUS } from "@/utils/constants"; +import React, { useEffect } from "react"; +import Skeleton from "react-loading-skeleton"; +import StarIcon from "./icons/StarIcon"; +import { useState } from "react"; +import MkdSDK from "@/utils/MkdSDK"; + +const HostCard = ({ data }) => { + let sdk = new MkdSDK(); + + const [user, setUser] = useState() + + const fetchUser = async () => { + sdk.setTable("user") + const users = await sdk.getAllUsers() + let host_user = users?.find(user => user.id == data.id) + setUser(host_user) + } + + useEffect(() => { + if (localStorage.getItem("token")) { + fetchUser() + } + }, []) + + return ( +
+ {data.first_name} +
+

+ {data.first_name || } {data.last_name} +

+
+

{data?.city && data?.city}

+

{data?.country && ", " + data?.country}

+
+
+

+ + + {(Number(data.avg_host_rating) || 0).toFixed(1)} + {(typeof data?.rating_count === "number" && data?.rating_count > 0) && + + {" "}({data.rating_count}) + + } + +

+
+
+
+ ); +}; + +export default HostCard; diff --git a/src/components/frontend/HostCardSlider.jsx b/src/components/frontend/HostCardSlider.jsx new file mode 100644 index 0000000..248ae18 --- /dev/null +++ b/src/components/frontend/HostCardSlider.jsx @@ -0,0 +1,72 @@ +import React, { useRef } from "react"; +import { Swiper, SwiperSlide } from "swiper/react"; +import "swiper/css"; + +import { Mousewheel } from "swiper"; +import HostCard from "./HostCard"; +import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline"; +import { useNavigate } from "react-router"; + +export default function HostCardSlider({ hosts }) { + const scrollTable = useRef(null); + const navigate = useNavigate() + + const moveTable = (ref) => { + ref.scrollLeft += 160; + }; + const moveTableBack = (ref) => { + ref.scrollLeft += -160; + }; + + return ( + <> + {hosts.length == 0 && ( +

+ No Hosts found +

+ )} + + +
+ {hosts.length > 0 && hosts.map((host, idx) => ( +
+ +
+ ))} +
+ {/* !["/"].includes(location.pathname) && */} + {(hosts.length > 3 && window.innerWidth > 800) && +
+
+ moveTableBack(scrollTable.current) + } + > + +
+
moveTable(scrollTable.current)}> + +
+
+ } + + + ); +} diff --git a/src/components/frontend/LoadingButton.jsx b/src/components/frontend/LoadingButton.jsx new file mode 100644 index 0000000..78fd9cd --- /dev/null +++ b/src/components/frontend/LoadingButton.jsx @@ -0,0 +1,44 @@ +import React from "react"; + +export default function LoadingButton({ loading, loadingEl, children, ...restProps }) { + return ( + + ); +} diff --git a/src/components/frontend/LogoutModal.jsx b/src/components/frontend/LogoutModal.jsx new file mode 100644 index 0000000..b083b4f --- /dev/null +++ b/src/components/frontend/LogoutModal.jsx @@ -0,0 +1,102 @@ +import { AuthContext } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment, useState } from "react"; +import { useContext } from "react"; +import { useNavigate } from "react-router"; +import LoadingButton from "./LoadingButton"; + +export default function LogoutModal({ modalOpen, closeModal }) { + const { dispatch: authDispatch } = useContext(AuthContext); + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + + async function logout() { + setLoading(true); + const sdk = new MkdSDK(); + try { + await sdk.logout(); + authDispatch({ type: "LOGOUT" }); + navigate("/"); + closeModal(); + } catch (err) { + // still logout if the token is already expired + if (err.message == "TOKEN_EXPIRED") { + authDispatch({ type: "LOGOUT" }); + navigate("/"); + closeModal(); + } + } + setLoading(false); + } + + return ( + + + +
+ + +
+
+ + + + Are you sure + +
+

Are you sure you want to sign out?

+
+ +
+ + + Proceed + +
+
+
+
+
+
+
+ ); +} diff --git a/src/components/frontend/NavBarSlider.jsx b/src/components/frontend/NavBarSlider.jsx new file mode 100644 index 0000000..a1b1ebd --- /dev/null +++ b/src/components/frontend/NavBarSlider.jsx @@ -0,0 +1,68 @@ +import React, { useState } from "react"; +import { Swiper, SwiperSlide } from "swiper/react"; +import "swiper/css"; +import { NavLink } from "react-router-dom"; +import { Mousewheel } from "swiper"; +import { useContext } from "react"; +import { AuthContext } from "@/authContext"; + +export default function NavBarSlider() { + const [swiper, setSwiper] = useState(null); + const { state } = useContext(AuthContext); + const role = state.role; + + const customerNavItems = [ + { name: "My Bookings", route: "/account/my-bookings" }, + { name: "Messages", route: "/account/messages" }, + { name: "Reviews", route: "/account/reviews" }, + { name: "Profile", route: "/account/profile" }, + { name: "Payment", route: "/account/payments" }, + { name: "Billing", route: "/account/billing" }, + ]; + const hostNavItems = [ + { name: "My Bookings", route: "/account/my-bookings" }, + { name: "Messages", route: "/account/messages" }, + { name: "Reviews", route: "/account/reviews" }, + { name: "My Spaces", route: "/account/my-spaces" }, + { name: "My Addons", route: "/account/my-addons" }, + { name: "My Amenities", route: "/account/my-amenities" }, + { name: "Profile", route: "/account/profile" }, + { name: "Payment", route: "/account/payments" }, + { name: "Billing", route: "/account/billing" }, + ]; + + return ( +
+ + {(role == "host" ? hostNavItems : customerNavItems).map((items, i) => ( + + swiper.slideTo(i)} + > + {items.name} + + + ))} +
+
+
+ ); +} diff --git a/src/components/frontend/NavMenu.jsx b/src/components/frontend/NavMenu.jsx new file mode 100644 index 0000000..2bff941 --- /dev/null +++ b/src/components/frontend/NavMenu.jsx @@ -0,0 +1,331 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import { ID_VERIFICATION_STATUSES, IMAGE_STATUS } from "@/utils/constants"; +import { Menu, Transition } from "@headlessui/react"; +import React, { Fragment, useEffect, useState } from "react"; +import { useRef } from "react"; +import { useContext } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import Icon from "../Icons"; +import LogoutModal from "./LogoutModal"; +import MkdSDK from "@/utils/MkdSDK"; +import { ChatBubbleBottomCenterIcon } from "@heroicons/react/24/outline"; +import { useTour } from "@reactour/tour"; + +const sdk = new MkdSDK(); + +export default function NavMenu({ variant }) { + const { state: globalState, dispatch: globalDispatch } = useContext(GlobalContext); + const { state: authState, dispatch: authDispatch } = useContext(AuthContext); + const [unreadCount, setUnreadCount] = useState(globalState.unreadMessages); + const [height, setHeight] = useState(window.innerHeight); + const navigate = useNavigate(); + const menuRef = useRef(null); + + function switchToHost() { + authDispatch({ type: "SWITCH_TO_HOST" }); + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Success", + message: `You are now signed in as a host`, + btn: "Ok got it", + }, + }); + navigate("/"); + } + + function switchToAdmin() { + authDispatch({ type: "SWITCH_TO_ADMIN" }); + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Success", + message: `You are now signed in as an admin`, + btn: "Ok got it", + }, + }); + navigate("/admin/dashboard"); + } + + function switchToCustomer() { + authDispatch({ type: "SWITCH_TO_CUSTOMER" }); + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Success", + message: `You are now signed in as a customer`, + btn: "Ok got it", + }, + }); + navigate("/"); + } + async function fetchUnreadMessagesCount() { + try { + const result = await sdk.getMyRoom(); + if (Array.isArray(result.messages)) { + globalDispatch({ + type: "SET_UNREAD_MESSAGES_COUNT", + payload: result.messages.filter((msg) => { + const messageSenderId = JSON.parse(msg.chat).user_id; + return Number(messageSenderId) != Number(authState.user); + }).length, + }); + } + + setUnreadCount(result.messages.filter((msg) => { + const messageSenderId = JSON.parse(msg.chat).user_id; + return Number(messageSenderId) != Number(authState.user); + }).length) + } catch (err) { + tokenExpireError(authDispatch, err.message); + } + } + + useEffect(() => { + fetchUnreadMessagesCount(); + }, []); + + + useEffect(() => { + const handleResize = () => { + setHeight(window.innerHeight); + }; + + window.addEventListener('resize', handleResize); + + // Cleanup event listener on component unmount + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + const [logoutModal, setLogoutModal] = useState(false); + + function getVerifiedColor(status) { + switch (status) { + case ID_VERIFICATION_STATUSES.PENDING: + return ""; + case ID_VERIFICATION_STATUSES.VERIFIED: + return "text-green-600"; + case ID_VERIFICATION_STATUSES.REJECTED: + return "text-red-600"; + default: + return "text-red-600"; + } + } + + const { setIsOpen } = useTour() + + const verificationStatuses = ["Pending Verification", "Verified", "Not Verified"]; + + return ( + <> +
+ +
+ + + +
+ + 560) && "max-h-[500px]"} ${(height < 560) && "max-h-[400px]"} overflow-y-auto right-0 mt-2 w-80 max-w-screen-sm origin-top-right rounded-3xl border bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none`}> +
+ +

+ {globalState.user.first_name} {globalState.user.last_name} +

+

You are signed in as {authState.role}

+ {verificationStatuses[globalState.user.verificationStatus] ?? "Not verified"} +
+ + <> +
+ +
+
+ + Messages{" "} + {globalState.unreadMessages > 0 && ( + {globalState.unreadMessages} + )} + +
+ +
+
+ + <> + + My bookings + + + Messages{" "} + {globalState.unreadMessages > 0 && ( + {globalState.unreadMessages} + )} + + + + Reviews + + {authState.role == "host" && ( + + My Spaces + + )} + + Profile + + + + Payment + + + Billing + + + +
+
+ + + + + + FAQs + + + + + Favorites + + + + {authState.role == "customer" ? ( + <> + {authState.originalRole != "customer" ? ( + + ) : ( + + Become a host + + )} + + ) : ( + + )} + + {["superadmin", "admin"].includes(authState.originalRole) && ( + + + + )} +
+
+ + + +
+
+
+
+
+ setLogoutModal(false)} + /> + + ); +} diff --git a/src/components/frontend/PropertyEditImageSlider.jsx b/src/components/frontend/PropertyEditImageSlider.jsx new file mode 100644 index 0000000..3d84041 --- /dev/null +++ b/src/components/frontend/PropertyEditImageSlider.jsx @@ -0,0 +1,99 @@ +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment, useState } from "react"; +import Swiper from "swiper"; +import { SwiperSlide, Swiper as SwiperComponent } from "swiper/react"; +import { Navigation, Pagination, A11y } from "swiper"; + +export default function PropertyEditImageSlider({ modalOpen, closeModal, spaceImages }) { + const [currentImageSlide, setCurrentImageSlide] = useState(0); + return ( + + + +
+ + +
+
+ + +
+
+

+ Images {currentImageSlide + 1} of {spaceImages.length} +

+ +
+
+ ``, + }} + className="property-swiper-slid" + > + {spaceImages.map((img, i) => ( + + {({ isActive }) => { + if (isActive) setCurrentImageSlide(i); + return ( + + ); + }} + + ))} + +
+
+
+
+
+
+
+ ); +} diff --git a/src/components/frontend/PropertyImageSlider.jsx b/src/components/frontend/PropertyImageSlider.jsx new file mode 100644 index 0000000..8e96b48 --- /dev/null +++ b/src/components/frontend/PropertyImageSlider.jsx @@ -0,0 +1,106 @@ +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment, useState } from "react"; +import Swiper from "swiper"; +import { SwiperSlide, Swiper as SwiperComponent } from "swiper/react"; +import { Navigation, Pagination, A11y } from "swiper"; + +export default function PropertyImageSlider({ modalOpen, closeModal, spaceImages }) { + const [currentImageSlide, setCurrentImageSlide] = useState(0); + + return ( + + + +
+ + +
+
+ + +
+
+ {spaceImages.length > 0 ? +

+ Images {currentImageSlide + 1} of {spaceImages.length} +

+ : +

+ No approved images to preview +

+ } + +
+
+ ``, + }} + className="property-swiper-slid" + > + {spaceImages.map((img, i) => ( + + {({ isActive }) => { + if (isActive) setCurrentImageSlide(i); + return ( + + ); + }} + + ))} + +
+
+
+
+
+
+
+ ); +} diff --git a/src/components/frontend/PropertyImageSliderAdd.jsx b/src/components/frontend/PropertyImageSliderAdd.jsx new file mode 100644 index 0000000..5a62374 --- /dev/null +++ b/src/components/frontend/PropertyImageSliderAdd.jsx @@ -0,0 +1,118 @@ +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment, useState } from "react"; +import Swiper from "swiper"; +import { SwiperSlide, Swiper as SwiperComponent } from "swiper/react"; +import { Navigation, Pagination, A11y } from "swiper"; + +export default function PropertyImageSliderAdd({ modalOpen, closeModal, spaceImages }) { + const [currentImageSlide, setCurrentImageSlide] = useState(0); + + return ( + + + +
+ + +
+
+ + + {/*
+
+ {spaceImages.length > 0 ? +

+ Images {currentImageSlide + 1} of {spaceImages.length} +

+ : +

+ No approved images to preview +

+ } + +
*/} +
+
+

+ Images {currentImageSlide + 1} of {spaceImages?.filter((v) => (v != null && v != "")).length} +

+ +
+
+ ``, + }} + className="property-swiper-slid" + > + {spaceImages?.filter((v) => (v != null && v != "")).map((img, i) => ( + + {({ isActive }) => { + if (isActive) setCurrentImageSlide(i); + return ( + + ); + }} + + ))} + +
+
+
+
+
+
+
+ ); +} diff --git a/src/components/frontend/PropertySpaceCard.jsx b/src/components/frontend/PropertySpaceCard.jsx new file mode 100644 index 0000000..bedd641 --- /dev/null +++ b/src/components/frontend/PropertySpaceCard.jsx @@ -0,0 +1,73 @@ +import React, { useState } from "react"; +import Skeleton, { SkeletonTheme } from "react-loading-skeleton"; +import { Link } from "react-router-dom"; +import MkdSDK from "@/utils/MkdSDK"; +import PersonIcon from "./icons/PersonIcon"; +import StarIcon from "./icons/StarIcon"; +import FavoriteButton from "./FavoriteButton"; + +let sdk = new MkdSDK(); + +const PropertySpaceCard = ({ data, forceRender, isFav }) => { + const [imageLoaded, setImageLoaded] = useState(false); + return ( + + + {data.name} setImageLoaded(true)} + /> + {imageLoaded ? ( +
+ + {data.category || } +
+ ) : ( + + )} + {/* Need to move this up because of br caused by skeleton */} +
+

{data.name || }

+

{data.city ? data.city + ", " + data.country : }

+
+

+ {data.rate ? "from:" : }{" "} + {data.rate ? ( + <> + ${data.rate} / hour + + ) : ( + <> + + + + )} +

+
+ {data.max_capacity ? : } + + {data.max_capacity || } +
+

+ {data.max_capacity ? : } + {data.rate ? {(Number(data.average_space_rating) || 0).toFixed(1)} : } +

+
+
+ +
+ ); +}; + +export default PropertySpaceCard; diff --git a/src/components/frontend/PropertySpaceMapImage.jsx b/src/components/frontend/PropertySpaceMapImage.jsx new file mode 100644 index 0000000..849f5ec --- /dev/null +++ b/src/components/frontend/PropertySpaceMapImage.jsx @@ -0,0 +1,52 @@ +import { Dialog, Transition } from "@headlessui/react"; +import { Fragment } from "react"; + +export default function PropertySpaceMapImage({ modalOpen, modalImage, closeModal }) { + return ( + <> + + + +
+ + +
+
+ + + +
+
+
+
+ + ); +} diff --git a/src/components/frontend/PropertySpaceTile.jsx b/src/components/frontend/PropertySpaceTile.jsx new file mode 100644 index 0000000..575fe69 --- /dev/null +++ b/src/components/frontend/PropertySpaceTile.jsx @@ -0,0 +1,99 @@ + +import { StarIcon } from "@heroicons/react/24/solid"; +import React, { useState } from "react"; +import { Link } from "react-router-dom"; +import FavoriteButton from "./FavoriteButton"; +import PersonIcon from "./icons/PersonIcon"; + +import PropertySpaceMapImage from "./PropertySpaceMapImage"; + +const PropertySpaceTile = ({ data, forceRender }) => { + const [showMap, setShowMap] = useState(false); + var amenities = (data.amenities ?? "").split(","); + amenities = Array.from(new Set(amenities)); + + return ( + <> + +
+ + {data.category || "N/A"} +
+
+
+

{data.name}

+

{data.city}

+

{data.country}

+
+

+ from: ${data.rate}/hour +

+
+ + {data.max_capacity} +
+
+
+
+
+

+ + + {(Number(data.average_space_rating) || 0).toFixed(1)} + {Number(data.space_rating_count) > 0 && + ({data.space_rating_count}) + } + +

+ +
+ +
+ {amenities.slice(0, 3).map((am, idx) => ( + + {am} + + ))} + {amenities.length > 3 ? +{amenities.length - 3} more : null} +
+
+
+ + setShowMap(false)} + /> + + ); +}; + +export default PropertySpaceTile; diff --git a/src/components/frontend/ReviewCard.jsx b/src/components/frontend/ReviewCard.jsx new file mode 100644 index 0000000..b5373c0 --- /dev/null +++ b/src/components/frontend/ReviewCard.jsx @@ -0,0 +1,45 @@ +import { IMAGE_STATUS } from "@/utils/constants"; +import moment from "moment"; +import React from "react"; +import StarIcon from "./icons/StarIcon"; + +const ReviewCard = ({ data }) => { + const role = localStorage.getItem("role") ?? "customer"; + return ( +
+ +
+
+

Posted by - 

+

+ {data?.customer_last_name}{" "}{data?.customer_first_name} +

+
+
+

{moment(data.post_date).format("MM/DD/YY")}

+

+ + {(Number(data.space_rating) || 0).toFixed(1)} +

+
+

{data.comment}

+
+ {data.hashtags != null && + data.hashtags.split(",").map((tag, i) => ( + + {tag} + + ))} +
+
+
+ ); +}; + +export default ReviewCard; diff --git a/src/components/frontend/SearchAutoComplete.jsx b/src/components/frontend/SearchAutoComplete.jsx new file mode 100644 index 0000000..1aae1ae --- /dev/null +++ b/src/components/frontend/SearchAutoComplete.jsx @@ -0,0 +1,108 @@ +import { Fragment, useContext, useEffect, useState } from "react"; +import { Combobox, Transition } from "@headlessui/react"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import { GlobalContext } from "@/globalContext"; + +const SearchAutoComplete = ({ selected, setSelected, className, optionsClassName }) => { + const [categories, setCategories] = useState([]); + const [query, setQuery] = useState(""); + const { dispatch: globalDispatch } = useContext(GlobalContext); + + const filteredCategories = + query === "" + ? categories + : categories + .filter((cat) => cat.category.toLowerCase().replace(/\s+/g, "").includes(query.toLowerCase().replace(/\s+/g, ""))) + .sort((a, b) => { + if (a.category.toLowerCase().indexOf(query.toLowerCase()) > b.category.toLowerCase().indexOf(query.toLowerCase())) { + return 1; + } else if (a.category.toLowerCase().indexOf(query.toLowerCase()) < b.category.toLowerCase().indexOf(query.toLowerCase())) { + return -1; + } else { + if (a.category > b.category) return 1; + else return -1; + } + }); + + async function fetchCategories() { + const where = [1]; + try { + const result = await callCustomAPI("spaces", "get", { page: 1, limit: 1000 }, ""); + if (Array.isArray(result.list)) { + setCategories(result.list); + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + useEffect(() => { + fetchCategories(); + }, []); + + return ( +
+ +
+
+ cat.category} + onChange={(event) => setQuery(event.target.value)} + placeholder="Search by category" + value={query} + autoComplete="off" + /> +
+ setQuery("")} + > + + {filteredCategories.length === 0 && query !== "" ? ( +
Other
+ ) : ( + filteredCategories + .filter((cat) => cat.category != "") + .map((cat) => ( + `relative cursor-default select-none py-3 px-4 ${active ? "bg-teal-600 text-white" : "text-gray-900"}`} + value={cat} + > + {({ selected, active }) => ( + <> + {cat.category} + + )} + + )) + )} +
+
+
+
+
+ ); +}; + +export default SearchAutoComplete; diff --git a/src/components/frontend/StaticSearchBar.jsx b/src/components/frontend/StaticSearchBar.jsx new file mode 100644 index 0000000..7e979b0 --- /dev/null +++ b/src/components/frontend/StaticSearchBar.jsx @@ -0,0 +1,118 @@ +import React, { useContext, useState } from "react"; +import { useEffect } from "react"; +import { useLocation, useMatch, useNavigate } from "react-router"; +import { createSearchParams, useSearchParams } from "react-router-dom"; +import SearchIcon from "./icons/SearchIcon"; +import ReactTestUtils from "react-dom/test-utils"; +import { isNotInViewport, sleep } from "@/utils/utils"; +import { useForm } from "react-hook-form"; +import CustomLocationAutoCompleteV2 from "../CustomLocationAutoCompleteV2"; +import CustomComboBox from "../CustomComboBox"; +import { GlobalContext } from "@/globalContext"; +import { MagnifyingGlassIcon } from "@heroicons/react/24/solid"; +import CustomStaticLocationAutoCompleteV2 from "../CustomStaticLocationAutoCompleteV2"; + +const StaticSearchBar = ({ className }) => { + const navigate = useNavigate(); + const inSearchPage = useMatch("/search"); + const [searchParams, setSearchParams] = useSearchParams(); + const [showStaticBar, setShowStaticBar] = useState(isNotInViewport("search-bar")); + const { pathname } = useLocation(); + const { state: globalState, dispatch } = useContext(GlobalContext); + + + + const categories = globalState.spaceCategories; + + const { handleSubmit, control, setValue } = useForm({ defaultValues: { category: "", location: globalState.location } }); + + useEffect(() => { + const onScroll = () => { + setShowStaticBar(isNotInViewport("search-bar")); + }; + window.addEventListener("scroll", onScroll); + + return () => { + window.removeEventListener("scroll", onScroll); + }; + }, []); + + useEffect(() => { + setShowStaticBar(false); + }, [pathname]); + + const onSubmit = async (data) => { + const searchBar = document.getElementById("search-bar"); + + if (inSearchPage && searchBar) { + // submit search form + if (data.category) searchParams.set("category", selected.category); + if (globalState.location) searchParams.set("location", location); + setSearchParams(searchParams); + await sleep(500); + ReactTestUtils.Simulate.submit(searchBar); + return; + } + navigate({ + pathname: "/search", + search: createSearchParams({ + location: globalState.location ?? "", + category: data.category ?? "", + }).toString(), + }); + }; + + if (!showStaticBar || !["/search", "/"].includes(pathname)) return null; + + return ( +
+
+ ); +}; +export default StaticSearchBar; diff --git a/src/components/frontend/ThreeDotsMenu.jsx b/src/components/frontend/ThreeDotsMenu.jsx new file mode 100644 index 0000000..ebd0d87 --- /dev/null +++ b/src/components/frontend/ThreeDotsMenu.jsx @@ -0,0 +1,57 @@ +import { Menu, Transition } from "@headlessui/react"; +import React from "react"; +import { Fragment } from "react"; +import Icon from "../Icons"; + +const ThreeDotsMenu = ({ items, direction, disabled, menuClassName, hidden }) => { + const filteredItems = items.filter((item) => !item.notShow); + return ( + e.stopPropagation()} + > +
+ + + +
+ + +
0 ? "py-1" : ""}> + {filteredItems.map((item, idx) => ( + + {({ active }) => ( + + )} + + ))} +
+
+
+
+ ); +}; + +export default ThreeDotsMenu; diff --git a/src/components/frontend/icons/AddIcon.jsx b/src/components/frontend/icons/AddIcon.jsx new file mode 100644 index 0000000..a6e5b13 --- /dev/null +++ b/src/components/frontend/icons/AddIcon.jsx @@ -0,0 +1,23 @@ +import React from "react"; + +const AddIcon = () => { + return ( + + + + ); +}; + +export default AddIcon; diff --git a/src/components/frontend/icons/CalendarIcon.jsx b/src/components/frontend/icons/CalendarIcon.jsx new file mode 100644 index 0000000..d322153 --- /dev/null +++ b/src/components/frontend/icons/CalendarIcon.jsx @@ -0,0 +1,21 @@ +import React from "react"; + +const CalendarIcon = () => ( + + + +); + +export default CalendarIcon; diff --git a/src/components/frontend/icons/CircleCheckIcon.jsx b/src/components/frontend/icons/CircleCheckIcon.jsx new file mode 100644 index 0000000..4ca709c --- /dev/null +++ b/src/components/frontend/icons/CircleCheckIcon.jsx @@ -0,0 +1,23 @@ +import React from "react"; + +const CircleCheckIcon = () => { + return ( + + + + ); +}; + +export default CircleCheckIcon; diff --git a/src/components/frontend/icons/CopyIcon.jsx b/src/components/frontend/icons/CopyIcon.jsx new file mode 100644 index 0000000..6b15c0b --- /dev/null +++ b/src/components/frontend/icons/CopyIcon.jsx @@ -0,0 +1,20 @@ +import React from "react"; + +const CopyIcon = () => ( + + + +); +export default CopyIcon; diff --git a/src/components/frontend/icons/CustomizedIcon.jsx b/src/components/frontend/icons/CustomizedIcon.jsx new file mode 100644 index 0000000..cfba148 --- /dev/null +++ b/src/components/frontend/icons/CustomizedIcon.jsx @@ -0,0 +1,37 @@ +import React from "react"; + +const CustomizedIcon = () => ( + + + + + + + + + +); + +export default CustomizedIcon; diff --git a/src/components/frontend/icons/DateTimeIcon.jsx b/src/components/frontend/icons/DateTimeIcon.jsx new file mode 100644 index 0000000..9d32853 --- /dev/null +++ b/src/components/frontend/icons/DateTimeIcon.jsx @@ -0,0 +1,23 @@ +import React from "react"; + +const DateTimeIcon = () => { + return ( + + + + ); +}; + +export default DateTimeIcon; diff --git a/src/components/frontend/icons/DownloadIcon.jsx b/src/components/frontend/icons/DownloadIcon.jsx new file mode 100644 index 0000000..1b7da1c --- /dev/null +++ b/src/components/frontend/icons/DownloadIcon.jsx @@ -0,0 +1,20 @@ +import React from "react"; + +const DownloadIcon = () => ( + + + +); +export default DownloadIcon; diff --git a/src/components/frontend/icons/FilterIcon.jsx b/src/components/frontend/icons/FilterIcon.jsx new file mode 100644 index 0000000..14619b7 --- /dev/null +++ b/src/components/frontend/icons/FilterIcon.jsx @@ -0,0 +1,21 @@ +import React from "react"; + +export default function FilterIcon() { + return ( + + + + ); +} diff --git a/src/components/frontend/icons/FlexibleIcon.jsx b/src/components/frontend/icons/FlexibleIcon.jsx new file mode 100644 index 0000000..0c17624 --- /dev/null +++ b/src/components/frontend/icons/FlexibleIcon.jsx @@ -0,0 +1,19 @@ +import React from "react"; + +const FlexibleIcon = () => ( + + + +); +export default FlexibleIcon; diff --git a/src/components/frontend/icons/GreenCheckIcon.jsx b/src/components/frontend/icons/GreenCheckIcon.jsx new file mode 100644 index 0000000..15b1323 --- /dev/null +++ b/src/components/frontend/icons/GreenCheckIcon.jsx @@ -0,0 +1,37 @@ +import React from "react"; + +const GreenCheckIcon = ({ size }) => { + return ( + + + + + + + + + + ); +}; + +export default GreenCheckIcon; diff --git a/src/components/frontend/icons/Hamburger.jsx b/src/components/frontend/icons/Hamburger.jsx new file mode 100644 index 0000000..5b761ba --- /dev/null +++ b/src/components/frontend/icons/Hamburger.jsx @@ -0,0 +1,21 @@ +import React from "react"; + +const Hamburger = ({ stroke }) => ( + + + +); + +export default Hamburger; diff --git a/src/components/frontend/icons/HeartIcon.jsx b/src/components/frontend/icons/HeartIcon.jsx new file mode 100644 index 0000000..f086717 --- /dev/null +++ b/src/components/frontend/icons/HeartIcon.jsx @@ -0,0 +1,25 @@ +import React from "react"; + +const HeartIcon = ({ isFav, stroke, favColor }) => { + return ( + + + + ); +}; + +export default HeartIcon; diff --git a/src/components/frontend/icons/LocationIcon.jsx b/src/components/frontend/icons/LocationIcon.jsx new file mode 100644 index 0000000..6097d11 --- /dev/null +++ b/src/components/frontend/icons/LocationIcon.jsx @@ -0,0 +1,28 @@ +import React from "react"; + +const LocationIcon = () => ( + + + + +); + +export default LocationIcon; diff --git a/src/components/frontend/icons/LogoIcon.jsx b/src/components/frontend/icons/LogoIcon.jsx new file mode 100644 index 0000000..2fc66a4 --- /dev/null +++ b/src/components/frontend/icons/LogoIcon.jsx @@ -0,0 +1,18 @@ +import React from "react"; + +const LogoIcon = ({ fill }) => ( + + + +); + +export default LogoIcon; diff --git a/src/components/frontend/icons/LogoutIcon.jsx b/src/components/frontend/icons/LogoutIcon.jsx new file mode 100644 index 0000000..ce55b47 --- /dev/null +++ b/src/components/frontend/icons/LogoutIcon.jsx @@ -0,0 +1,34 @@ +import React from "react"; + +export default function LogoutIcon() { + return ( + + + + + + + + + + + + + + + ); +} diff --git a/src/components/frontend/icons/NextIcon.jsx b/src/components/frontend/icons/NextIcon.jsx new file mode 100644 index 0000000..797b115 --- /dev/null +++ b/src/components/frontend/icons/NextIcon.jsx @@ -0,0 +1,24 @@ +import React from "react"; + +const NextIcon = () => { + return ( + + + + ); +}; + +export default NextIcon; diff --git a/src/components/frontend/icons/NotVerifiedIcon.jsx b/src/components/frontend/icons/NotVerifiedIcon.jsx new file mode 100644 index 0000000..937b6a9 --- /dev/null +++ b/src/components/frontend/icons/NotVerifiedIcon.jsx @@ -0,0 +1,20 @@ +import React from "react"; + +const NotVerifiedIcon = ({ stroke }) => ( + + + +); +export default NotVerifiedIcon; diff --git a/src/components/frontend/icons/NoteIcon.jsx b/src/components/frontend/icons/NoteIcon.jsx new file mode 100644 index 0000000..16b341d --- /dev/null +++ b/src/components/frontend/icons/NoteIcon.jsx @@ -0,0 +1,20 @@ +import React from "react"; + +const NoteIcon = ({ width }) => ( + + + +); +export default NoteIcon; diff --git a/src/components/frontend/icons/PencilIcon.jsx b/src/components/frontend/icons/PencilIcon.jsx new file mode 100644 index 0000000..b806e2e --- /dev/null +++ b/src/components/frontend/icons/PencilIcon.jsx @@ -0,0 +1,23 @@ +import React from "react"; + +const PencilIcon = ({ stroke }) => { + return ( + + + + ); +}; + +export default PencilIcon; diff --git a/src/components/frontend/icons/PeopleIcon.jsx b/src/components/frontend/icons/PeopleIcon.jsx new file mode 100644 index 0000000..0cd9a71 --- /dev/null +++ b/src/components/frontend/icons/PeopleIcon.jsx @@ -0,0 +1,21 @@ +import React from "react"; + +const PeopleIcon = () => ( + + + +); + +export default PeopleIcon; diff --git a/src/components/frontend/icons/PersonIcon.jsx b/src/components/frontend/icons/PersonIcon.jsx new file mode 100644 index 0000000..ac420d6 --- /dev/null +++ b/src/components/frontend/icons/PersonIcon.jsx @@ -0,0 +1,23 @@ +import React from "react"; + +const PersonIcon = () => { + return ( + + + + ); +}; + +export default PersonIcon; diff --git a/src/components/frontend/icons/PictureIcon.jsx b/src/components/frontend/icons/PictureIcon.jsx new file mode 100644 index 0000000..4a35328 --- /dev/null +++ b/src/components/frontend/icons/PictureIcon.jsx @@ -0,0 +1,18 @@ +import React from "react"; + +const PictureIcon = () => ( + + + +); + +export default PictureIcon; diff --git a/src/components/frontend/icons/PrevIcon.jsx b/src/components/frontend/icons/PrevIcon.jsx new file mode 100644 index 0000000..2428263 --- /dev/null +++ b/src/components/frontend/icons/PrevIcon.jsx @@ -0,0 +1,22 @@ +import React from "react"; + +const PrevIcon = () => ( + + + +); + +export default PrevIcon; diff --git a/src/components/frontend/icons/RecurringIcon.jsx b/src/components/frontend/icons/RecurringIcon.jsx new file mode 100644 index 0000000..db10161 --- /dev/null +++ b/src/components/frontend/icons/RecurringIcon.jsx @@ -0,0 +1,21 @@ +import React from "react"; + +const RecurringIcon = () => ( + + + +); + +export default RecurringIcon; diff --git a/src/components/frontend/icons/ResetIcon.jsx b/src/components/frontend/icons/ResetIcon.jsx new file mode 100644 index 0000000..a71eccf --- /dev/null +++ b/src/components/frontend/icons/ResetIcon.jsx @@ -0,0 +1,21 @@ +import React from "react"; + +const ResetIcon = () => ( + + + +); + +export default ResetIcon; diff --git a/src/components/frontend/icons/SearchIcon.jsx b/src/components/frontend/icons/SearchIcon.jsx new file mode 100644 index 0000000..4978389 --- /dev/null +++ b/src/components/frontend/icons/SearchIcon.jsx @@ -0,0 +1,21 @@ +import React from "react"; + +const SearchIcon = ({ stroke, className }) => ( + + + +); +export default SearchIcon; diff --git a/src/components/frontend/icons/SecurityIcon.jsx b/src/components/frontend/icons/SecurityIcon.jsx new file mode 100644 index 0000000..7bb5e9b --- /dev/null +++ b/src/components/frontend/icons/SecurityIcon.jsx @@ -0,0 +1,36 @@ +import React from "react"; + +const SecurityIcon = () => ( + + + + + + + + + +); +export default SecurityIcon; diff --git a/src/components/frontend/icons/SmileIcon.jsx b/src/components/frontend/icons/SmileIcon.jsx new file mode 100644 index 0000000..d8116ac --- /dev/null +++ b/src/components/frontend/icons/SmileIcon.jsx @@ -0,0 +1,17 @@ +import React from "react"; + +const SmileIcon = () => ( + + + +); +export default SmileIcon; diff --git a/src/components/frontend/icons/StarIcon.jsx b/src/components/frontend/icons/StarIcon.jsx new file mode 100644 index 0000000..3d523e1 --- /dev/null +++ b/src/components/frontend/icons/StarIcon.jsx @@ -0,0 +1,34 @@ +import React from "react"; + +const StarIcon = () => ( + + + + + + + + + +); + +export default StarIcon; diff --git a/src/components/frontend/icons/TrashIcon.jsx b/src/components/frontend/icons/TrashIcon.jsx new file mode 100644 index 0000000..627b825 --- /dev/null +++ b/src/components/frontend/icons/TrashIcon.jsx @@ -0,0 +1,21 @@ +import React from "react"; + +const TrashIcon = () => ( + + + +); + +export default TrashIcon; diff --git a/src/components/frontend/icons/TrustedIcon.jsx b/src/components/frontend/icons/TrustedIcon.jsx new file mode 100644 index 0000000..7dc053b --- /dev/null +++ b/src/components/frontend/icons/TrustedIcon.jsx @@ -0,0 +1,19 @@ +import React from "react"; + +const TrustedIcon = ({ stroke }) => ( + + + +); +export default TrustedIcon; diff --git a/src/components/frontend/icons/WelcomeIcon.jsx b/src/components/frontend/icons/WelcomeIcon.jsx new file mode 100644 index 0000000..61fb622 --- /dev/null +++ b/src/components/frontend/icons/WelcomeIcon.jsx @@ -0,0 +1,37 @@ +import React from "react"; + +const WelcomeIcon = () => ( + + + + + + + + + +); + +export default WelcomeIcon; diff --git a/src/components/frontend/icons/WhitePlusIcon.jsx b/src/components/frontend/icons/WhitePlusIcon.jsx new file mode 100644 index 0000000..cfd9189 --- /dev/null +++ b/src/components/frontend/icons/WhitePlusIcon.jsx @@ -0,0 +1,17 @@ +import React from "react"; + +const WhitePlusIcon = () => ( + + + +); +export default WhitePlusIcon; diff --git a/src/components/frontend/index.jsx b/src/components/frontend/index.jsx new file mode 100644 index 0000000..3a4b55e --- /dev/null +++ b/src/components/frontend/index.jsx @@ -0,0 +1,5 @@ +import LoadingButton from "./LoadingButton"; +import AddOnCounter from "./AddOnCounter"; +import FavoriteButton from "./FavoriteButton"; + +export { LoadingButton, AddOnCounter, FavoriteButton }; diff --git a/src/favicon.svg b/src/favicon.svg new file mode 100644 index 0000000..0e61ef5 --- /dev/null +++ b/src/favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/globalContext.jsx b/src/globalContext.jsx new file mode 100644 index 0000000..ec3a9e6 --- /dev/null +++ b/src/globalContext.jsx @@ -0,0 +1,261 @@ +import React, { useReducer } from "react"; +export const GlobalContext = React.createContext({}); + +const initialState = { + globalMessage: "", + globalMessageType: "", + isOpen: true, + show: false, + path: "", + location: "", + saveChanges: false, + deleted: false, + loading: false, + error: false, + errorHeading: "", + errorMsg: "", + confirmation: false, + confirmationHeading: "", + confirmationMsg: "", + confirmationCloseFn: undefined, + adminNotificationCount: 0, + unreadMessages: 0, + isLocationSet: false, + userLocationData: {}, + user: {}, + spaceCategories: [], + notVerifiedModal: false, + menuIconOpen: false, + addPaymentMethodModal: false, + addPayoutMethodModal: false, + tourOpen: false, +}; + +const reducer = (state, action) => { + switch (action.type) { + case "SNACKBAR": + return { + ...state, + globalMessage: action.payload.message, + globalMessageType: action.payload.type, + }; + case "SETPATH": + return { + ...state, + path: action.payload.path, + }; + case "SETLOCATION": + let data = action.payload.location; + if (action.payload.location.includes("undefined")) { + const parts = action.payload.location.split(","); + const result = parts[0].trim(); + data = result; + } + return { + ...state, + location: data, + }; + case "OPEN_SIDEBAR": + return { + ...state, + isOpen: action.payload.isOpen, + }; + case "SHOWMODAL": + return { + ...state, + showModal: action.payload.showModal, + modalShowMessage: action.payload.modalShowMessage, + modalBtnText: action.payload.modalBtnText, + modalShowTitle: action.payload.modalShowTitle, + type: action.payload.type, + itemId: action.payload.itemId, + itemId2: action.payload.itemId2, + table1: action.payload.table1, + table2: action.payload.table2, + backTo: action.payload.backTo, + }; + + case "SAVE_CHANGES": + return { + ...state, + saveChanges: action.payload.saveChanges, + }; + + case "DELETED": + return { + ...state, + deleted: action.payload.deleted, + }; + + case "SHOW_REVIEW": + return { + ...state, + review: action.payload.review, + showReview: action.payload.showReview, + }; + case "START_LOADING": + return { + ...state, + loading: true, + }; + case "STOP_LOADING": + return { + ...state, + loading: false, + }; + + case "SHOW_ERROR": + if (action.payload.message == "TOKEN_EXPIRED") { + const role = localStorage.getItem("role") ?? "customer"; + localStorage.clear(); + location.href = "/" + role + "/login"; + return state; + } + return { + ...state, + error: true, + errorHeading: action.payload.heading, + errorMsg: action.payload.message, + }; + case "CLOSE_ERROR": + return { + ...state, + error: false, + errorHeading: "", + errorMsg: "", + }; + + case "SHOW_CONFIRMATION": + return { + ...state, + confirmation: true, + confirmationHeading: action.payload.heading, + confirmationMsg: action.payload.message, + confirmationBtn: action.payload.btn, + confirmationCloseFn: action.payload.onClose, + }; + case "CLOSE_CONFIRMATION": + return { + ...state, + confirmation: false, + confirmationHeading: "", + confirmationMsg: "", + confirmationBtn: "", + confirmationCloseFn: undefined, + }; + case "SET_NOTIFICATION_COUNT": + return { + ...state, + adminNotificationCount: action.payload, + }; + case "SET_USER_CURRENT_LOCATION": + return { + ...state, + isLocationSet: true, + userLocationData: action.payload, + }; + case "SET_UNREAD_MESSAGES_COUNT": + return { + ...state, + unreadMessages: action.payload, + }; + case "SET_USER_DATA": + return { + ...state, + user: action.payload, + }; + case "CLEAR_USER_DATA": + return { + ...state, + user: {}, + }; + case "SET_SPACE_CATEGORIES": + return { + ...state, + spaceCategories: action.payload, + }; + case "OPEN_NOT_VERIFIED_MODAL": + return { + ...state, + notVerifiedModal: true, + }; + case "CLOSE_NOT_VERIFIED_MODAL": + return { + ...state, + notVerifiedModal: false, + }; + case "OPEN_MENU_ICON": + return { + ...state, + menuIconOpen: true, + }; + case "CLOSE_MENU_ICON": + return { + ...state, + menuIconOpen: false, + }; + case "OPEN_ADD_PAYMENT_METHOD": + return { + ...state, + addPaymentMethodModal: true, + }; + case "CLOSE_ADD_PAYMENT_METHOD": + return { + ...state, + addPaymentMethodModal: false, + }; + case "START_TOUR": + return { + ...state, + tourOpen: true, + }; + case "END_TOUR": + return { + ...state, + tourOpen: false, + }; + default: + return state; + } +}; + +export const showToast = (dispatch, message, timeout = 3000, type) => { + dispatch({ + type: "SNACKBAR", + payload: { + type, + message, + }, + }); + + setTimeout(() => { + dispatch({ + type: "SNACKBAR", + payload: { + type: "", + message: "", + }, + }); + }, timeout); +}; + +const GlobalProvider = ({ children }) => { + const [state, dispatch] = useReducer(reducer, initialState); + + // React.useEffect(() => { + + // }, []); + + return ( + + {children} + + ); +}; + +export default GlobalProvider; diff --git a/src/hooks/api/UsePropImage.jsx b/src/hooks/api/UsePropImage.jsx new file mode 100644 index 0000000..e075913 --- /dev/null +++ b/src/hooks/api/UsePropImage.jsx @@ -0,0 +1,44 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { IMAGE_STATUS } from "@/utils/constants"; +import { useContext, useState } from "react"; +import { useEffect } from "react"; + +const ctrl = new AbortController(); + +export default function usePropertySpaceImages(property_space_id, allowUnApproved, setFetching) { + const [spaceImages, setSpaceImages] = useState([]); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const { dispatch } = useContext(AuthContext); + + async function fetchPropertySpaceImages() { + setFetching(true) + const where = [`property_spaces_id = ${property_space_id} AND ${allowUnApproved ? `is_approved = ${IMAGE_STATUS.APPROVED}` : `is_approved = ${IMAGE_STATUS.IN_REVIEW}`} AND ergo_property_spaces_images.deleted_at IS NULL`]; + try { + const sdk = new MkdSDK(); + const result = await sdk.callRawAPI("/v2/api/custom/ergo/property-space-images/PAGINATE", { page: 1, limit: 7, where }, "POST", ctrl.signal); + if (Array.isArray(result.list)) { + setSpaceImages(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + setFetching(false) + } + + useEffect(() => { + if (isNaN(property_space_id)) return; + fetchPropertySpaceImages(); + }, [property_space_id]); + + return spaceImages; +} diff --git a/src/hooks/api/index.jsx b/src/hooks/api/index.jsx new file mode 100644 index 0000000..95fbe38 --- /dev/null +++ b/src/hooks/api/index.jsx @@ -0,0 +1,11 @@ +import useCards from "./useCards"; +import usePropertySpace from "./usePropertySpace"; +import usePropertySpaceImages from "./usePropertySpaceImages"; +import usePropertyAddons from "./usePropertyAddons"; +import usePublicUserData from "./usePublicUserData"; +import useTaxAndCommission from "./useTaxAndCommission"; +import usePropertySpaceAmenities from "./usePropertySpaceAmenities"; +import usePropertySpaceFaqs from "./usePropertySpaceFaqs"; +import usePropertySpaceReviews from "./usePropertySpaceReviews"; + +export { useCards, usePropertySpace, usePropertyAddons, usePropertySpaceImages, usePublicUserData, useTaxAndCommission, usePropertySpaceAmenities, usePropertySpaceFaqs, usePropertySpaceReviews }; diff --git a/src/hooks/api/useAddonCategories.jsx b/src/hooks/api/useAddonCategories.jsx new file mode 100644 index 0000000..63a8749 --- /dev/null +++ b/src/hooks/api/useAddonCategories.jsx @@ -0,0 +1,42 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useContext, useEffect, useState } from "react"; + +const ctrl = new AbortController(); +export default function useAddonCategories(space_id, is_others) { + const [addons, setAddons] = useState([]); + const { dispatch: authDispatch } = useContext(AuthContext); + const { dispatch: globalDispatch } = useContext(GlobalContext); + + async function fetchAddons() { + let sdk = new MkdSDK(); + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/add_on/PAGINATE", + { page: 1, limit: 1000, where: ["deleted_at IS NULL", `${is_others ? space_id && `space_id != ${space_id}` : space_id ? `space_id = ${space_id}` : "1"} OR ${`creator_id = ${Number(localStorage.getItem("user"))}`}`] }, + "POST", + ctrl.signal, + ); + if (!result.error) { + setAddons(result.list); + } + } catch (err) { + tokenExpireError(authDispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + useEffect(() => { + fetchAddons(); + }, [space_id]); + + return addons; +} diff --git a/src/hooks/api/useAmenityCategories.jsx b/src/hooks/api/useAmenityCategories.jsx new file mode 100644 index 0000000..dccd4cd --- /dev/null +++ b/src/hooks/api/useAmenityCategories.jsx @@ -0,0 +1,42 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useContext, useEffect, useState } from "react"; + +const ctrl = new AbortController(); +export default function useAmenityCategories(space_id, is_others) { + const [amenities, setAmenities] = useState([]); + const { dispatch: authDispatch } = useContext(AuthContext); + const { dispatch: globalDispatch } = useContext(GlobalContext); + + async function fetchAmenities() { + let sdk = new MkdSDK(); + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/amenity/PAGINATE", + { page: 1, limit: 1000, where: ["deleted_at IS NULL", `${is_others ? space_id && `space_id != ${space_id}` : space_id ? `space_id = ${space_id}` : "1"} OR ${`creator_id = ${Number(localStorage.getItem("user"))}`}`] }, + "POST", + ctrl.signal, + ); + + if (!result.error) { + setAmenities(result.list); + } + } catch (err) { + tokenExpireError(authDispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + useEffect(() => { + fetchAmenities(); + }, [space_id]); + return amenities; +} diff --git a/src/hooks/api/useCancellation.jsx b/src/hooks/api/useCancellation.jsx new file mode 100644 index 0000000..c88468d --- /dev/null +++ b/src/hooks/api/useCancellation.jsx @@ -0,0 +1,27 @@ +import { callCustomAPI } from "@/utils/callCustomAPI"; +import MkdSDK from "@/utils/MkdSDK"; +import { useEffect } from "react"; +import { useState } from "react"; + +const sdk = new MkdSDK(); + +export default function useCancellation() { + const [content, setContent] = useState(""); + + async function fetchCancellationPolicy() { + const sdk = new MkdSDK(); + sdk.setTable("cms"); + const result = await callCustomAPI("cms", "post", { payload: { content_key: "cancellation_policy" }, limit: 1000, page: 1 }, "PAGINATE"); + + if (Array.isArray(result.list) && result.list.length > 0) { + setContent(result.list.find((stg) => stg.content_key == "cancellation_policy")?.content_value); + return + } + } + + useEffect(() => { + fetchCancellationPolicy(); + }, []); + + return content; +} diff --git a/src/hooks/api/useCards.jsx b/src/hooks/api/useCards.jsx new file mode 100644 index 0000000..9eef5c8 --- /dev/null +++ b/src/hooks/api/useCards.jsx @@ -0,0 +1,88 @@ +import { AuthContext } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import React, { useState, useEffect, useContext } from "react"; + +export default function useCards({ loader, onCardDelete }) { + const [cards, setCards] = useState([]); + const [defaultCard, setDefaultCard] = useState({}); + const sdk = new MkdSDK(); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const { state: authState } = useContext(AuthContext); + + async function fetchCards() { + if (loader) { + globalDispatch({ type: "START_LOADING" }); + } + try { + const result = await sdk.getCustomerStripeCards(); + if (Array.isArray(result.data?.data)) { + setCards(result.data.data); + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Unable to get payment methods", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + async function changeDefaultCard(cardId) { + if (loader) { + globalDispatch({ type: "START_LOADING" }); + } + try { + await sdk.setStripeCustomerDefaultCard(cardId); + fetchCards(); + } catch (err) { + // globalDispatch({ + // type: "SHOW_ERROR", + // payload: { + // heading: "Operation failed", + // message: err.message, + // }, + // }); + globalDispatch({ type: "STOP_LOADING" }); + } + } + + async function deleteCard(cardId) { + if (loader) { + globalDispatch({ type: "START_LOADING" }); + } + + try { + await sdk.deleteCustomerStripeCard(cardId); + if (onCardDelete) { + onCardDelete(); + } + fetchCards(); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + globalDispatch({ type: "STOP_LOADING" }); + } + } + + useEffect(() => { + fetchCards(); + }, []); + + useEffect(() => { + if (cards.length > 0) { + var found = cards.find((card) => card.id == card.customer.default_source); + setDefaultCard(found || {}); + } + }, [cards]); + + return { cards, defaultCard, changeDefaultCard, fetchCards, deleteCard }; +} diff --git a/src/hooks/api/usePropertyAddons.jsx b/src/hooks/api/usePropertyAddons.jsx new file mode 100644 index 0000000..f46dd56 --- /dev/null +++ b/src/hooks/api/usePropertyAddons.jsx @@ -0,0 +1,42 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useContext } from "react"; +import { useEffect } from "react"; +import { useState } from "react"; + +const ctrl = new AbortController(); +export default function usePropertyAddons(property_id, editAddons) { + const [addons, setAddons] = useState([]); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const { dispatch } = useContext(AuthContext); + + async function fetchPropertyAddons() { + const sdk = new MkdSDK(); + const where = [`ergo_property.id = ${property_id} AND ergo_property_add_on.deleted_at IS NULL`]; + try { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/property-addons/PAGINATE", { page: 1, limit: 10000, where }, "POST", ctrl.signal); + if (Array.isArray(result.list)) { + setAddons(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + useEffect(() => { + if (property_id) { + fetchPropertyAddons(); + } + }, [property_id, editAddons]); + + return addons; +} diff --git a/src/hooks/api/usePropertySpace.jsx b/src/hooks/api/usePropertySpace.jsx new file mode 100644 index 0000000..813f3d2 --- /dev/null +++ b/src/hooks/api/usePropertySpace.jsx @@ -0,0 +1,53 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useContext } from "react"; +import { useEffect } from "react"; +import { useState } from "react"; + +const sdk = new MkdSDK(); +const ctrl = new AbortController(); + +export default function usePropertySpace(property_space_id, reRender) { + const [propertySpace, setPropertySpace] = useState({}); + const [notFound, setNotFound] = useState(null); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const { dispatch } = useContext(AuthContext); + + async function fetchPropertySpace() { + const user_id = localStorage.getItem("user"); + const where = [`ergo_property_spaces.id = ${property_space_id} AND ergo_property_spaces.deleted_at IS NULL`]; + try { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/popular/PAGINATE", { page: 1, limit: 1, user_id: Number(user_id), where, all: true }, "POST", ctrl.signal); + if (Array.isArray(result.list) && result.list.length > 0) { + setPropertySpace(result.list[0]); + } else { + // space not found + setNotFound(true); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + useEffect(() => { + if (isNaN(Number(property_space_id))) return; + fetchPropertySpace(); + }, [property_space_id]); + + useEffect(() => { + if (!reRender) return; + fetchPropertySpace(); + }, [reRender]); + + return { propertySpace, notFound }; +} diff --git a/src/hooks/api/usePropertySpaceAmenities.jsx b/src/hooks/api/usePropertySpaceAmenities.jsx new file mode 100644 index 0000000..640fe6d --- /dev/null +++ b/src/hooks/api/usePropertySpaceAmenities.jsx @@ -0,0 +1,33 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useContext } from "react"; +import { useEffect, useState } from "react"; + +const ctrl = new AbortController(); +export default function usePropertySpaceAmenities(property_space_id, editAmenities) { + const [amenities, setAmenities] = useState([]); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const { dispatch } = useContext(AuthContext); + + async function fetchPropertySpaceAmenities() { + const sdk = new MkdSDK(); + const where = [`ergo_property_spaces.id = ${property_space_id}`]; + try { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/property-spaces-amenitites/PAGINATE", { page: 1, limit: 1000, where }, "POST", ctrl.signal); + if (Array.isArray(result.list)) { + setAmenities(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + } + } + + useEffect(() => { + if (isNaN(property_space_id)) return; + fetchPropertySpaceAmenities(); + }, [property_space_id, editAmenities]); + + return amenities; +} diff --git a/src/hooks/api/usePropertySpaceFaqs.jsx b/src/hooks/api/usePropertySpaceFaqs.jsx new file mode 100644 index 0000000..1f28997 --- /dev/null +++ b/src/hooks/api/usePropertySpaceFaqs.jsx @@ -0,0 +1,44 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useContext } from "react"; +import { useEffect } from "react"; +import { useState } from "react"; +const ctrl = new AbortController(); + +export default function usePropertySpaceFaqs(property_space_id) { + const [faqs, setFaqs] = useState([]); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const { dispatch } = useContext(AuthContext); + + async function fetchPropertySpaceFaqs() { + try { + const sdk = new MkdSDK(); + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/property_space_faq/PAGINATE", + { page: 1, limit: 10, where: [`property_space_id = ${property_space_id} AND ergo_property_space_faq.deleted_at IS NULL`] }, + "POST", + ctrl.signal, + ); + if (Array.isArray(result.list)) { + setFaqs(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + useEffect(() => { + if (isNaN(property_space_id)) return; + fetchPropertySpaceFaqs(); + }, [property_space_id]); + return faqs; +} diff --git a/src/hooks/api/usePropertySpaceImages.jsx b/src/hooks/api/usePropertySpaceImages.jsx new file mode 100644 index 0000000..e075913 --- /dev/null +++ b/src/hooks/api/usePropertySpaceImages.jsx @@ -0,0 +1,44 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { IMAGE_STATUS } from "@/utils/constants"; +import { useContext, useState } from "react"; +import { useEffect } from "react"; + +const ctrl = new AbortController(); + +export default function usePropertySpaceImages(property_space_id, allowUnApproved, setFetching) { + const [spaceImages, setSpaceImages] = useState([]); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const { dispatch } = useContext(AuthContext); + + async function fetchPropertySpaceImages() { + setFetching(true) + const where = [`property_spaces_id = ${property_space_id} AND ${allowUnApproved ? `is_approved = ${IMAGE_STATUS.APPROVED}` : `is_approved = ${IMAGE_STATUS.IN_REVIEW}`} AND ergo_property_spaces_images.deleted_at IS NULL`]; + try { + const sdk = new MkdSDK(); + const result = await sdk.callRawAPI("/v2/api/custom/ergo/property-space-images/PAGINATE", { page: 1, limit: 7, where }, "POST", ctrl.signal); + if (Array.isArray(result.list)) { + setSpaceImages(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + setFetching(false) + } + + useEffect(() => { + if (isNaN(property_space_id)) return; + fetchPropertySpaceImages(); + }, [property_space_id]); + + return spaceImages; +} diff --git a/src/hooks/api/usePropertySpaceImagesV2.jsx b/src/hooks/api/usePropertySpaceImagesV2.jsx new file mode 100644 index 0000000..624dd33 --- /dev/null +++ b/src/hooks/api/usePropertySpaceImagesV2.jsx @@ -0,0 +1,42 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { IMAGE_STATUS } from "@/utils/constants"; +import { useContext, useState } from "react"; +import { useEffect } from "react"; + +const ctrl = new AbortController(); + +export default function usePropertySpaceImagesV2(property_space_id, allowUnApproved) { + const [spaceImages, setSpaceImages] = useState([]); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const { dispatch } = useContext(AuthContext); + + async function fetchPropertySpaceImages() { + const where = [`property_spaces_id = ${property_space_id} AND ergo_property_spaces_images.deleted_at IS NULL`]; + try { + const sdk = new MkdSDK(); + const result = await sdk.callRawAPI("/v2/api/custom/ergo/property-space-images/PAGINATE", { page: 1, limit: 7, where }, "POST", ctrl.signal); + if (Array.isArray(result.list)) { + setSpaceImages(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + useEffect(() => { + if (isNaN(property_space_id)) return; + fetchPropertySpaceImages(); + }, [property_space_id]); + + return spaceImages; +} diff --git a/src/hooks/api/usePropertySpaceReviews.jsx b/src/hooks/api/usePropertySpaceReviews.jsx new file mode 100644 index 0000000..366d15b --- /dev/null +++ b/src/hooks/api/usePropertySpaceReviews.jsx @@ -0,0 +1,41 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useEffect, useState } from "react"; +import { useContext } from "react"; +const ctrl = new AbortController(); + +export default function usePropertySpaceReviews(property_space_id) { + const [reviews, setReviews] = useState([]); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const { state: authState } = useContext(AuthContext); + const { dispatch } = useContext(AuthContext); + + async function fetchReviews() { + const where = [`ergo_review.property_spaces_id = ${property_space_id} AND ergo_review.status = 1 AND ergo_review.given_by = 'customer'`]; + try { + const sdk = new MkdSDK(); + const result = await sdk.callRawAPI("/v2/api/custom/ergo/review-hashtag/PAGINATE", { page: 1, limit: 1000, where, user: authState.role }, "POST", ctrl.signal); + if (Array.isArray(result.list)) { + setReviews(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + useEffect(() => { + if (isNaN(property_space_id)) return; + fetchReviews(); + }, [property_space_id]); + + return reviews; +} diff --git a/src/hooks/api/usePublicUserData.jsx b/src/hooks/api/usePublicUserData.jsx new file mode 100644 index 0000000..2ffe0a1 --- /dev/null +++ b/src/hooks/api/usePublicUserData.jsx @@ -0,0 +1,37 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import { IMAGE_STATUS } from "@/utils/constants"; +import { useContext, useState } from "react"; +import { useEffect } from "react"; +const ctrl = new AbortController(); +const sdk = new MkdSDK(); +export default function usePublicUserData(user_id) { + const [user, setUser] = useState({}); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const { dispatch } = useContext(AuthContext); + + async function fetchUserData() { + try { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/get-user", { id: user_id }, "POST", ctrl.signal); + setUser({ ...result, photo: result.is_photo_approved == IMAGE_STATUS.APPROVED ? result.photo : null }); + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + useEffect(() => { + if (isNaN(user_id)) return; + fetchUserData(); + }, [user_id]); + return user; +} diff --git a/src/hooks/api/useRuleTemplates.jsx b/src/hooks/api/useRuleTemplates.jsx new file mode 100644 index 0000000..f566608 --- /dev/null +++ b/src/hooks/api/useRuleTemplates.jsx @@ -0,0 +1,42 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import TreeSDK from "@/utils/TreeSDK"; +import { useContext, useEffect, useState } from "react"; + +const ctrl = new AbortController(); +export default function useRuleTemplates(host_id) { + const [rules, setRules] = useState([]); + const { dispatch: authDispatch } = useContext(AuthContext); + const { dispatch: globalDispatch } = useContext(GlobalContext); + + async function fetchRules() { + let treeSdk = new TreeSDK(); + try { + let filter = ["deleted_at,is"]; + if (host_id) { + filter.push(`host_id,eq,${host_id}`); + } + const result = await treeSdk.getList("property_space_rule_template", { filter, join: [] }); + if (!result.error) { + setRules(result.list); + } + } catch (err) { + tokenExpireError(authDispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + useEffect(() => { + fetchRules(); + }, [host_id]); + + return rules; +} diff --git a/src/hooks/api/useSchedulingData.jsx b/src/hooks/api/useSchedulingData.jsx new file mode 100644 index 0000000..cefe3e9 --- /dev/null +++ b/src/hooks/api/useSchedulingData.jsx @@ -0,0 +1,82 @@ +import { GlobalContext } from "@/globalContext"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import React from "react"; +import { useContext } from "react"; +import { useEffect } from "react"; +import { useState } from "react"; + +export default function useSchedulingData({ property_space_id }) { + const [bookedSlots, setBookedSlots] = useState([]); + const [scheduleTemplate, setScheduleTemplate] = useState({}); + const { dispatch: globalDispatch } = useContext(GlobalContext); + + async function fetchBookedSlots(id) { + try { + const result = await callCustomAPI("customer/schedule", "post", { property_spaces_id: id }, "", null, "v3"); + if (Array.isArray(result.list)) { + setBookedSlots(result.list); + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function fetchScheduleTemplate(id) { + try { + const result = await callCustomAPI( + "property_spaces_schedule_template", + "post", + { + page: 1, + limit: 1, + where: [`property_spaces_id = ${id}`], + }, + "PAGINATE", + ); + if (Array.isArray(result.list) && result.list.length > 0) { + setScheduleTemplate({ custom_slots: result.list[0].custom_slots }); + } + if (result.list[0]?.schedule_template_id) { + const templateResult = await callCustomAPI( + "schedule_template", + "post", + { + page: 1, + limit: 1, + where: [`id = ${result.list[0].schedule_template_id}`], + }, + "PAGINATE", + ); + if (Array.isArray(templateResult.list) && (templateResult.list[0] ?? {})) { + setScheduleTemplate((prev) => { + let updated = { ...prev, ...templateResult.list[0] }; + return updated; + }); + } + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + useEffect(() => { + if (property_space_id) { + fetchBookedSlots(property_space_id); + fetchScheduleTemplate(property_space_id); + } + }, [property_space_id]); + + return { bookedSlots, scheduleTemplate }; +} diff --git a/src/hooks/api/useSpaceCategories.jsx b/src/hooks/api/useSpaceCategories.jsx new file mode 100644 index 0000000..7197643 --- /dev/null +++ b/src/hooks/api/useSpaceCategories.jsx @@ -0,0 +1,37 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useContext, useEffect, useState } from "react"; + +export default function useSpaceCategories() { + const [categories, setCategories] = useState([]); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const { dispatch } = useContext(AuthContext); + async function fetchCategories() { + const sdk = new MkdSDK(); + const ctrl = new AbortController(); + try { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/spaces/PAGINATE", { page: 1, limit: 1000, where: ["deleted_at IS NULL"] }, "POST", ctrl.signal); + if (Array.isArray(result.list)) { + setCategories(result.list); + globalDispatch({ type: "SET_SPACE_CATEGORIES", payload: result.list }); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + useEffect(() => { + fetchCategories(); + }, []); + + return categories; +} diff --git a/src/hooks/api/useTaxAndCommission.jsx b/src/hooks/api/useTaxAndCommission.jsx new file mode 100644 index 0000000..ec4aad8 --- /dev/null +++ b/src/hooks/api/useTaxAndCommission.jsx @@ -0,0 +1,38 @@ +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import React from "react"; +import { useContext } from "react"; +import { useEffect } from "react"; +import { useState } from "react"; + +export default function useTaxAndCommission() { + const [tax, setTax] = useState(null); + const [commission, setCommission] = useState(null); + const sdk = new MkdSDK(); + const { dispatch: globalDispatch } = useContext(GlobalContext); + + async function fetchSettings() { + sdk.setTable("settings"); + try { + const result = await sdk.callRestAPI({ page: 1, limit: 2, payload: {} }, "PAGINATE"); + if (Array.isArray(result.list) && result.list.length > 0) { + setTax(result.list.find((x) => x.key_name == "tax")?.key_value); + setCommission(result.list.find((x) => x.key_name == "commission")?.key_value); + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Unable to determine tax amount", + message: err.message, + }, + }); + } + } + + useEffect(() => { + fetchSettings(); + }, []); + + return { tax, commission }; +} diff --git a/src/hooks/api/useUserCurrentLocation.jsx b/src/hooks/api/useUserCurrentLocation.jsx new file mode 100644 index 0000000..58438f7 --- /dev/null +++ b/src/hooks/api/useUserCurrentLocation.jsx @@ -0,0 +1,45 @@ +import { GlobalContext } from "@/globalContext"; +import { parseJsonSafely } from "@/utils/utils"; +import { useContext } from "react"; +import { useEffect, useState } from "react"; + +export default function useUserCurrentLocation() { + const [loc, setLoc] = useState({}); + const { state, dispatch } = useContext(GlobalContext); + + async function fetchIpData() { + if (state.isLocationSet) { + setLoc(state.userLocationData || {}); + return; + } + + const localLoc = parseJsonSafely(localStorage.getItem("location"), {}); + if (localLoc?.city || localLoc?.country) { + setLoc(localLoc); + return; + } + try { + const res = await fetch("https://api.ipregistry.co/?key=tryout"); + const json = await res.json(); + + setLoc({ city: json.location?.city, country: json.location?.country?.name, done: true, latitude: json.location?.latitude, longitude: json.location?.longitude }); + localStorage.setItem( + "location", + JSON.stringify({ city: json.location?.city, country: json.location?.country?.name, done: true, latitude: json.location?.latitude, longitude: json.location?.longitude }), + ); + // store globally + dispatch({ + type: "SET_USER_CURRENT_LOCATION", + payload: { city: json.location?.city, country: json.location?.country?.name, done: true, latitude: json.location?.latitude, longitude: json.location?.longitude }, + }); + } catch (err) { + setLoc({ done: true }); + } + } + + useEffect(() => { + fetchIpData(); + }, []); + + return loc; +} diff --git a/src/hooks/useDelayUnmount.jsx b/src/hooks/useDelayUnmount.jsx new file mode 100644 index 0000000..22cc253 --- /dev/null +++ b/src/hooks/useDelayUnmount.jsx @@ -0,0 +1,20 @@ +import React from "react"; +import { useState } from "react"; +import { useEffect } from "react"; + +const useDelayUnmount = (isMounted, delayTime) => { + const [shouldRender, setShouldRender] = useState(false); + + useEffect(() => { + let timeoutId; + if (isMounted && !shouldRender) { + setShouldRender(true); + } else if (!isMounted && shouldRender) { + timeoutId = setTimeout(() => setShouldRender(false), delayTime); + } + return () => clearTimeout(timeoutId); + }, [isMounted, delayTime, shouldRender]); + return shouldRender; +}; + +export default useDelayUnmount; diff --git a/src/hooks/useImageHost.jsx b/src/hooks/useImageHost.jsx new file mode 100644 index 0000000..e69de29 diff --git a/src/hooks/useInterval.jsx b/src/hooks/useInterval.jsx new file mode 100644 index 0000000..c7e6756 --- /dev/null +++ b/src/hooks/useInterval.jsx @@ -0,0 +1,22 @@ +import React, { useState, useEffect, useRef } from "react"; + +export const useInterval = (callback, delay) => { + const savedCallback = useRef(); + + useEffect(() => { + savedCallback.current = callback; + }, [callback]); + + useEffect(() => { + function tick() { + savedCallback.current(); + } + if (delay !== null) { + const id = setInterval(tick, delay); + return () => { + console.log("clearing interval"); + clearInterval(id); + }; + } + }, [delay]); +}; diff --git a/src/hooks/useScrollDirection.jsx b/src/hooks/useScrollDirection.jsx new file mode 100644 index 0000000..18fb7a5 --- /dev/null +++ b/src/hooks/useScrollDirection.jsx @@ -0,0 +1,27 @@ +import { useCallback, useEffect, useState } from "react"; + +export default function useScrollDirection() { + const [y, setY] = useState(document.scrollingElement.scrollHeight); + const [scrollDirection, setScrollDirection] = useState("NONE"); + + const handleNavigation = useCallback( + (e) => { + if (y > window.scrollY) { + setScrollDirection("UP"); + } else if (y < window.scrollY) { + setScrollDirection("DOWN"); + } + setY(window.scrollY); + }, + [y], + ); + + useEffect(() => { + window.addEventListener("scroll", handleNavigation); + + return () => { + window.removeEventListener("scroll", handleNavigation); + }; + }, [handleNavigation]); + return scrollDirection; +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..87db883 --- /dev/null +++ b/src/index.css @@ -0,0 +1,2208 @@ +@import url("https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;1,300;1,400&display=swap"); + +*, +*::before, +*::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --navbar-height: 160px; + --messages-page-height: calc(100vh - var(--navbar-height)); + --property-card-img-height: 250px; + --property-card-width: 300px; + --category-slider-width: 120px; + --account-menu-item-width: 120px; +} + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-family: medium-content-sans-serif-font, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; + font-family: "General Sans"; + font-family: "Noto Sans", sans-serif; +} + +html { + overflow-x: hidden; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; +} + +input, +textarea, +button, +select, +a { + -webkit-tap-highlight-color: transparent; +} + +.hidden-scrollbar { + overflow: -moz-scrollbars-none; /* For older Firefox versions */ + scrollbar-width: none; /* For Firefox */ + -ms-overflow-style: none; /* For Internet Explorer and Edge */ +} + +.hidden-scrollbar::-webkit-scrollbar { + display: none; /* For WebKit browsers */ +} + +.sidebar-holder { + /* width: 100%; + min-width: 240px; + max-width: 240px; + position: relative; + background: #ffff; + color: #475467; + z-index: 2; + transition: all 0.3s; + min-height: 100vh; + max-height: 100vh; + transition: 0.2s; */ +} + +/* Hide scrollbar for Chrome, Safari and Opera */ +.sidebar-holder::-webkit-scrollbar { + display: none; +} + +/* Hide scrollbar for IE, Edge and Firefox */ +.sidebar-holder { + -ms-overflow-style: none; + /* IE and Edge */ + scrollbar-width: none; + /* Firefox */ +} + +.open-nav { + min-width: 0px !important; + max-width: 0px !important; + width: 0 !important; + transition: 0.2s; + opacity: 0; +} + +.sidebar-list ul li a { + padding: 10px; + display: flex; + width: 100%; + margin: 2px 0; + font-size: 15px; + font-weight: 600; + transition: 0.2s ease-in; + text-transform: capitalize; +} + +.sidebar-list .sidebar-item { + padding: 10px; + display: flex; + width: 100%; + margin: 2px 0; + font-size: 15px; + font-weight: 600; + transition: 0.2s ease-in; + text-transform: capitalize; + cursor: pointer; +} + +.page-header { + width: 100%; + padding: 20px; + background: white; +} + +.page-header span { + cursor: pointer; + display: block; + width: fit-content; + font-size: 20px; +} + +.center-svg { + aspect-ratio: 1/1; + align-items: center; + justify-content: center; + line-height: 1.2em !important; +} + +.uppy-Dashboard-inner { + width: 100% !important; +} + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@media screen and (max-width: 767px) { + .sidebar-holder { + width: 100%; + min-width: 200px; + max-width: 200px; + position: fixed; + top: 0; + left: 0; + } + + .page-header span { + margin-left: auto; + } +} + +/* FRONTEND STYLES */ + +.customer-section { + /* font-family: General Sans sans-serif !important; */ +} + +.my-text-gradient { + background: -webkit-linear-gradient(230.69deg, #33d4b7 9.11%, #0d9895 69.45%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.hr { + position: relative; + color: #667085; + font-weight: 400; + font-size: 14px; + line-height: 20px; +} + +.hr::before, +.hr::after { + content: ""; + position: absolute; + height: 1.5px; + background-color: #eee; + width: 170px; + top: 50%; +} + +.hr::after { + right: 140%; +} + +.hr::before { + left: 140%; +} + +.customer-section { + width: 100% !important; +} + +.google-btn { + border: 2px solid #eee !important; + box-shadow: none !important; + color: black !important; +} + +.bg-login { + background-color: #f0f5f3; +} + +.login-btn-gradient { + background: linear-gradient(230.69deg, #33d4b7 9.11%, #0d9895 69.45%); +} + +.login-btn-gradient.loading { + pointer-events: none; +} + +.login-btn-gradient.loading::before { + position: absolute; + content: ""; + inset: 0; + background-color: rgba(225, 225, 225, 0.3); +} + +.login-btn-gradient:disabled { + background: #d0d5dd; +} + +.my-background-image { + background-size: cover !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-blend-mode: multiply !important; +} + +.fadeIn { + animation: fadeIn 800ms ease-in; +} + +.fadeOut { + animation: fadeOut 800ms ease-in; +} + +@keyframes fadeIn { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +@keyframes fadeOut { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + } +} + +.property-grid { + display: grid; + grid-template-columns: repeat(2, 250px); + column-gap: 21px; + row-gap: 40px; + padding-left: 1rem; + max-width: 100%; +} + +.property-space-card .aspect { + aspect-ratio: 292/227 !important; + min-height: 194px; +} + +.browse-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + column-gap: 19px; + row-gap: 20px; + padding-inline: 1rem; +} + +.react-tooltip { + text-transform: none !important; + background: black !important; + border-radius: 0.6em !important; + font-size: 11px !important; + padding: 4px 9px !important; +} + +.snap-scroll { + /* scroll-snap-type: inline mandatory; */ + overflow-x: auto; + overscroll-behavior-inline: contain; +} + +.snap-scroll::-webkit-scrollbar { + height: 0px !important; +} + +.snap-scroll button { + scroll-snap-align: start; +} + +/* custom checkbox */ +.checkbox-container { + display: flex; + align-items: center; +} + +.checkbox-container label { + cursor: pointer; + display: flex; + align-items: center; +} + +.checkbox-container input[type="checkbox"] { + cursor: pointer; + opacity: 0; + position: absolute; +} + +.checkbox-container label::before { + content: ""; + width: 20px; + height: 20px; + border-radius: 4px; + margin-right: 10.5px; + /* border: 1px solid theme(colors.primary-dark); */ +} + +.checkbox-container input[type="checkbox"]:disabled+label, +.checkbox-container input[type="checkbox"]:disabled { + color: #aaa; + cursor: default; +} + +.checkbox-container input[type="checkbox"]:checked+label::before { + content: "\002714"; + /* background-color: theme(colors.primary-dark); */ + display: flex; + justify-content: center; + align-items: center; + -webkit-text-fill-color: #fff !important; + -webkit-opacity: 1; + color: #fff !important; +} + +.checkbox-container input[type="checkbox"]:disabled+label::before { + background-color: #ccc; + border-color: #999; +} + +.remove-arrow::-webkit-outer-spin-button, +.remove-arrow::-webkit-inner-spin-button { + appearance: unset; + -webkit-appearance: none; + margin: 0; +} + +/* Firefox */ +.remove-arrow[type="number"] { + appearance: unset; + -moz-appearance: textfield; +} + +.date-placeholder::placeholder { + background-image: url("/date-placeholder.png"); + background-repeat: no-repeat; + background-position: left; + background-size: contain; +} + +.date-placeholder, +.people-placeholder { + text-indent: 26px; +} + +.people-placeholder::placeholder { + background-image: url("/people-placeholder.png"); + background-repeat: no-repeat; + background-position: left; + background-size: contain; +} + +.addons-grid { + display: grid; + /* grid-template-columns: repeat(1, 200px); */ + column-gap: 50px; + row-gap: 12px; +} + +.amenities-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + row-gap: 8px; +} + +.list-disk-important { + list-style-type: disc !important; + list-style-position: inside; +} + +.popup-container { + position: fixed; + background-color: rgba(0, 0, 0, 0.6); + top: 0; + right: 0; + left: 0; + height: 100vh; + z-index: 54; +} + +.review-scroll::-webkit-scrollbar { + width: 10px; +} + +.custom-calendar-scroll::-webkit-scrollbar { + width: 5px; +} + +/* Track */ +.review-scroll::-webkit-scrollbar-track { + background: #fff; +} + +/* Handle */ +.review-scroll::-webkit-scrollbar-thumb { + background: #eaecf0; + border-radius: 10px; +} + +/* Handle on hover */ +.review-scroll::-webkit-scrollbar-thumb:hover { + background: #d0d5dd; +} + +.tiny-scroll::-webkit-scrollbar { + width: 5px; +} + +/* Track */ +.tiny-scroll::-webkit-scrollbar-track { + background: #fff; +} + +/* Handle */ +.tiny-scroll::-webkit-scrollbar-thumb { + background: #eaecf0; + border-radius: 10px; +} + +/* Handle on hover */ +.tiny-scroll::-webkit-scrollbar-thumb:hover { + background: #d0d5dd; +} + +.property-swiper-slide { + height: 100%; +} + +.property-swiper-slide .swiper-pagination { + display: none; +} + +.property-swiper-slide .swiper-button-next, +.property-swiper-slide .swiper-button-prev { + display: none; +} + +.remove-select { + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -o-user-select: none; + user-select: none; +} + +.property-swiper-image { + object-fit: cover; + + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -o-user-select: none; + user-select: none; +} + +.swiper-pagination { + text-align: unset !important; +} + +.swiper-button-next, +.swiper-button-prev { + top: 50% !important; + background-color: #908d8c; + width: 56px !important; + height: 56px !important; + border-radius: 50% !important; + color: white !important; +} + +.swiper-button-next::after, +.swiper-button-prev::after { + font-size: 20px !important; + font-weight: 700 !important; +} + +.swiper-button-next::before, +.swiper-button-prev::before { + content: " "; + position: absolute; + background-color: transparent; + border: 2px solid white; + width: 33px; + height: 33px; + border-radius: 50%; +} + +.pagination-image { + height: 75px !important; + width: 120px !important; + display: none !important; + border-radius: 4px !important; + margin-right: 24px !important; + opacity: 1 !important; + + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -o-user-select: none; + user-select: none; +} + +.pagination-image.swiper-pagination-bullet-active { + box-shadow: 0 0 0 5px #0d9895; +} + +.my-shadow2 { + box-shadow: 0px 8px 24px rgb(29 17 96 / 15%); +} + +.my-shadow { + box-shadow: 0px 8px 24px rgb(29 17 96 / 15%); + border-radius: 10px; + overflow: hidden; +} + +.property-swiper-slide .swiper-pagination-horizontal { + max-width: 100%; + overflow-x: auto !important; + white-space: nowrap !important; + overflow-y: visible; + text-align: start !important; + padding: 1rem 0; +} + +.full-circle { + border-radius: 50%; + aspect-ratio: 1 /1; +} + +.property-swiper-slide .swiper-pagination-horizontal::-webkit-scrollbar { + width: 5px !important; +} + +/* Track */ +.property-swiper-slide .swiper-pagination-horizontal::-webkit-scrollbar-track { + background: #fff; +} + +/* Handle */ +.property-swiper-slide .swiper-pagination-horizontal::-webkit-scrollbar-thumb { + background: #eaecf0; + border-radius: 10px; +} + +/* Handle on hover */ +.property-swiper-slide .swiper-pagination-horizontal::-webkit-scrollbar-thumb:hover { + background: #d0d5dd; +} + +.sticky-price-summary { + box-shadow: 0px -4px 8px -2px rgba(16, 24, 40, 0.05), 0px -4px 4px -2px rgba(16, 24, 40, 0.04); +} + +.static-search-bar { + box-shadow: 0px 8px 24px rgb(29 17 96 / 15%); + background-color: white; + /* padding: 8px; */ + border-radius: 100vw; +} + +.animated-nav { + position: relative; + display: flex; +} + +.animated-nav a { + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 24px; + letter-spacing: 0.02em; + color: #475467; + width: 105px; + transition: 300ms; + text-align: center; +} + +.animated-nav a.active { + position: relative; + color: #101828; +} + +.bottom-nav a.active { + font-weight: 600; +} + +.animated-nav .slide { + width: 105px; + border-radius: 2px; + height: 2px; + position: absolute; + left: 0; + top: 100%; + transition: 300ms; + opacity: 0; + background-color: #101828; + /* z-index: -1; */ + pointer-events: none; +} + +.animated-nav .slide.white { + background-color: white !important; +} + +.animated-nav a:nth-child(1).active~.slide, +.animated-nav button:nth-child(1).active~.slide { + left: 0; + opacity: 1; +} + +.animated-nav a:nth-child(2).active~.slide, +.animated-nav button:nth-child(2).active~.slide { + left: 105px; + opacity: 1; +} + +.animated-nav a:nth-child(3).active~.slide, +.animated-nav button:nth-child(3).active~.slide { + left: calc(2 * 105px); + opacity: 1; +} + +.animated-nav a:nth-child(4).active~.slide, +.animated-nav button:nth-child(4).active~.slide { + left: calc(3 * 105px); + opacity: 1; +} + +.animated-nav a:nth-child(5).active~.slide, +.animated-nav button:nth-child(5).active~.slide { + left: calc(4 * 105px); + opacity: 1; +} + +.animated-nav a:nth-child(6).active~.slide, +.animated-nav button:nth-child(6).active~.slide { + left: calc(5 * 105px); + opacity: 1; +} + +.animated-nav a:nth-child(7).active~.slide, +.animated-nav button:nth-child(7).active~.slide { + left: calc(6 * 105px); + opacity: 1; +} + +.animated-nav a:nth-child(8).active~.slide, +.animated-nav button:nth-child(8).active~.slide { + left: calc(7 * 105px); + opacity: 1; +} + +.animated-nav a:nth-child(9).active~.slide, +.animated-nav button:nth-child(9).active~.slide { + left: calc(8 * 105px); + opacity: 1; +} + +/* ACCOUNT PAGES HEADER */ +.account-header { + position: relative; + display: flex; +} + +.account-header a { + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 24px; + letter-spacing: 0.02em; + color: #475467; + width: 120px; + transition: 300ms; + text-align: center; + white-space: nowrap; +} + +.account-header a.active { + position: relative; + color: #101828; +} + +.account-header .slide { + width: 120px; + border-radius: 2px; + height: 2px; + position: absolute; + left: 0; + top: 100%; + transition: 300ms; + opacity: 0; + background-color: #101828; + /* z-index: -1; */ + pointer-events: none; +} + +.account-header .slide.white { + background-color: white !important; +} + +.account-header a:nth-child(1).active~.slide, +.account-header button:nth-child(1).active~.slide { + left: 0; + opacity: 1; +} + +.account-header a:nth-child(2).active~.slide, +.account-header button:nth-child(2).active~.slide { + left: 120px; + opacity: 1; +} + +.account-header a:nth-child(3).active~.slide, +.account-header button:nth-child(3).active~.slide { + left: calc(2 * 120px); + opacity: 1; +} + +.account-header a:nth-child(4).active~.slide, +.account-header button:nth-child(4).active~.slide { + left: calc(3 * 120px); + opacity: 1; +} + +.account-header a:nth-child(5).active~.slide, +.account-header button:nth-child(5).active~.slide { + left: calc(4 * 120px); + opacity: 1; +} + +.account-header a:nth-child(6).active~.slide, +.account-header button:nth-child(6).active~.slide { + left: calc(5 * 120px); + opacity: 1; +} + +.account-header a:nth-child(7).active~.slide, +.account-header button:nth-child(7).active~.slide { + left: calc(6 * 120px); + opacity: 1; +} + +.account-header a:nth-child(8).active~.slide, +.account-header button:nth-child(8).active~.slide { + left: calc(7 * 120px); + opacity: 1; +} + +.account-header a:nth-child(9).active~.slide, +.account-header button:nth-child(9).active~.slide { + left: calc(8 * 120px); + opacity: 1; +} + +.pop-in { + animation: pop-in 300ms; +} + +.pop-out { + animation: pop-out 500ms; +} + +.gallery-in { + animation: gallery-in 400ms ease-in-out; +} + +.gallery-out { + animation: gallery-out 200ms reverse; +} + +@keyframes gallery-in { + 0% { + opacity: 0; + transform: translateY(0px); + } + + 50% { + opacity: 0.2; + } + + 100% { + opacity: 1; + transform: translateY(300px); + } +} + +@keyframes gallery-in { + 0% { + opacity: 0; + transform: translateY(300px); + } + + 50% { + opacity: 0.2; + } + + 100% { + opacity: 1; + transform: translateY(0px); + } +} + +@keyframes pop-in { + 0% { + opacity: 0; + transform: translateY(-100px); + } + + 100% { + opacity: 1; + transform: translateY(0px); + } +} + +@keyframes pop-out { + 0% { + opacity: 1; + transform: translateY(0px); + } + + 100% { + opacity: 0; + transform: translateY(-5px); + } +} + +.select-rating-container input { + display: none; +} + +.select-rating-container label { + cursor: pointer; +} + +.radio-container input { + display: none; +} + +.radio-container label { + display: flex; + align-items: center; + gap: 0.6rem; +} + +.radio-container span { + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid #1d2939; + position: relative; +} + +.radio-container span::before { + content: ""; + position: absolute; + inset: 10px; + background-color: #1d2939; + border-radius: 50%; + top: 50%; + left: 50%; + transition: 300ms; +} + +.radio-container input[type="radio"]:checked+span::before { + inset: 2px; +} + +.bg-my-gradient { + background: linear-gradient(230.69deg, #33d4b7 9.11%, #0d9895 69.45%); +} + +.chat-active { + position: relative; +} + +.chat-active::before { + position: absolute; + content: ""; + width: 4px; + top: 0; + bottom: 0; + left: 0; + background: linear-gradient(230.69deg, #33d4b7 9.11%, #0d9895 69.45%); +} + +.scale-in-ver-center { + -webkit-animation: scale-in-ver-center 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; + animation: scale-in-ver-center 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; +} + +/* .image-not-approved { + border: 2px solid red; + position: relative; +} + +.image-not-approved::before { + content: "-"; + position: absolute; + top: 10px; + right: 10px; + width: 100px; + height: 100px; + background-color: red; + z-index: 23; + border: 2px solid limegreen; +} */ + +@-webkit-keyframes scale-in-ver-center { + 0% { + -webkit-transform: scaleY(0); + transform: scaleY(0); + opacity: 1; + } + + 100% { + -webkit-transform: scaleY(1); + transform: scaleY(1); + opacity: 1; + } +} + +@keyframes scale-in-ver-center { + 0% { + -webkit-transform: scaleY(0); + transform: scaleY(0); + opacity: 1; + } + + 100% { + -webkit-transform: scaleY(1); + transform: scaleY(1); + opacity: 1; + } +} + +.header-transparent { + color: white; + background-color: transparent; +} + +.header-light { + background-color: black; + color: white; +} + +.header-white { + color: black; + background-color: white; +} + +.my-border-white { + border-color: #00261c; +} + +.my-border-transparent { + border-color: white; +} + +.my-border-light { + /* border-color: #00261c; */ + border-color: white; +} + +.my-stroke-white { + stroke: #00261c; +} + +.my-stroke-transparent { + stroke: white; +} + +.my-stroke-light { + /* stroke: #00261c; */ + stroke: white; +} + +.template-grid { + display: grid; + grid-template-columns: 340px 170px 170px; +} + +.react-calendar { + font-family: "General Sans" sans-serif !important; + color: #344054; + border: 1px solid #eaecf0 !important; + width: 300px; +} + +.react-calendar abbr:where([title]) { + text-decoration: none; + text-transform: none; +} + +.react-calendar__tile { + height: 40px; + font-size: 14px !important; +} + +.react-calendar__tile:enabled:hover { + background: none !important; +} + +.react-calendar__month-view__days__day--weekend { + color: #344054 !important; +} + +.react-calendar__tile--now { + background: none !important; + position: relative; +} + +.react-calendar__tile--now::after { + position: absolute; + bottom: 5px; + content: ""; + background-color: #00261c; + width: 5px; + height: 5px; + left: calc(50% - 2.5px); + border-radius: 50%; +} + +.react-calendar__tile--active { + background: linear-gradient(230.69deg, #33d4b7 9.11%, #0d9895 69.45%) !important; + border-radius: 50%; + color: white !important; +} + +.react-calendar__tile--active.react-calendar__tile--now::after { + background-color: white; +} + +.react-calendar__tile:enabled:hover.react-calendar__tile--active:hover { + background: linear-gradient(230.69deg, #33d4b7 9.11%, #0d9895 69.45%) !important; +} + +.react-calendar__navigation button:enabled:hover, +.react-calendar__navigation button:enabled:focus { + background: none !important; +} + +.react-calendar__month-view__days__day--neighboringMonth { + color: #98a2b3 !important; +} + +.nav-dropdown-btn { + position: relative; +} + +.nav-dropdown-content { + display: none; + position: absolute; + top: 100%; + left: -150px; + color: #00261c; + width: 180px; + padding-top: 10px; +} + +.nav-dropdown-content.mobile { + left: -180px; + top: 150%; + display: block; +} + +.nav-dropdown-content>div { + background-color: white; + border-radius: 15px; + overflow: hidden; + border: 2px solid #0d9895; +} + +.animated-nav-vert { + position: relative; +} + +.animated-nav-vert .slide { + width: 100%; + height: 30px; + position: absolute; + left: 0; + top: 0; + transition: 200ms; + opacity: 0; + background: linear-gradient(230.69deg, #33d4b7 9.11%, #0d9895 69.45%); + /* z-index: -1; */ + pointer-events: none; +} + +.nav-dropdown-content a, +.nav-dropdown-content button { + display: flex; + width: 100%; + align-items: center; + justify-content: center; + height: 30px; + text-align: center; + width: 100%; + color: black; + transition: 100ms; + position: relative; + z-index: 1; +} + +.nav-dropdown-btn:hover .nav-dropdown-content { + display: block; +} + +.animated-nav-vert a:hover, +.animated-nav-vert button:hover { + color: white; +} + +.animated-nav-vert a:nth-child(2):hover~.slide { + top: 30px; + opacity: 1; +} + +.animated-nav-vert a:nth-child(1):hover~.slide { + top: 0; + opacity: 1; +} + +.animated-nav-vert a:nth-child(3):hover~.slide { + top: calc(2 * 30px); + opacity: 1; +} + +.animated-nav-vert a:nth-child(4):hover~.slide { + top: calc(3 * 30px); + opacity: 1; +} + +.animated-nav-vert button:nth-child(5):hover~.slide { + top: calc(4 * 30px); + opacity: 1; +} + +.custom-calendar { + border: 0px !important; + width: 290px !important; +} + +.custom-calendar .react-calendar_tile { + font-size: 12px !important; + padding: 5px !important; + line-height: 1 !important; +} + +.custom-calendar .react-calendar__navigation { + margin-bottom: 0; +} + +.custom-calendar .react-calendar__tile:disabled { + /* position: relative; */ + background: transparent !important; + opacity: 0.4; + text-decoration: line-through; +} + +.custom-calendar .react-calendar__tile:disabled.react-calendar__tile--active { + background: transparent !important; + color: rgba(0, 0, 0, 0.6) !important; +} + +.date-picker .react-calendar__tile:disabled { + /* position: relative; */ + background: transparent !important; + opacity: 0.4; + text-decoration: line-through; +} + +.to-chat { + position: relative; +} + +.to-chat::before { + content: ""; + position: absolute; + bottom: 0; + right: 0; + width: 10px; + height: 10px; + border: 0px solid #f2f4f7; + background-color: white; + border-right-width: 1px; + border-bottom-width: 1px; + z-index: 2; +} + +.from-chat { + position: relative; +} + +.from-chat::before { + content: ""; + position: absolute; + top: 0; + left: 0; + background-color: #15212a; + width: 10px; + height: 10px; +} + +.reduce-z-index { + z-index: -1; +} + +.property-space-card:hover>div:first-child { + transition: 200ms; +} + +.animate-filter { + animation: slideDown 200ms ease-in-out; +} + +.slideUp { + animation: slideUp 200ms ease-in-out; +} + +.emoji-picker { + bottom: -100%; + left: -400px; + z-index: 3; + animation: slideUp 400ms ease-in-out; +} + +.scheduling-calendar { + width: 100% !important; + border: none !important; +} + +.scheduling-calendar .react-calendar__tile abbr { + display: none; +} + +.scheduling-calendar .react-calendar__navigation { + display: grid !important; + grid-template-columns: 205px 40px 40px; + gap: 12px; + align-items: center; + padding-inline: 5px; +} + +.scheduling-calendar .react-calendar__navigation__prev2-button { + display: none; +} + +.scheduling-calendar .react-calendar__navigation__next2-button { + display: flex; +} + +.scheduling-calendar .react-calendar__navigation__label { + grid-column: 1/2; +} + +.scheduling-calendar .react-calendar__navigation__prev-button { + grid-column: 2/3; + border: 1px solid #d0d5dd; + grid-row: 1; + padding: 10px 0; +} + +.scheduling-calendar .react-calendar__navigation__next-button { + grid-column: 3/4; +} + +.use-template { + width: 150px; + padding: 8px 0; + display: inline-flex; + justify-content: center; + background: linear-gradient(230.69deg, #33d4b7 9.11%, #0d9895 69.45%) !important; + color: white; +} + +.react-calendar__navigation button.use-template:enabled:hover { + background: linear-gradient(230.69deg, #33d4b7 9.11%, #0d9895 69.45%) !important; +} + +.react-calendar__navigation button.use-template:enabled:focus { + background: linear-gradient(230.69deg, #33d4b7 9.11%, #0d9895 69.45%) !important; +} + +.scheduling-calendar .react-calendar__tile { + padding: 15px 8px !important; + font-size: 14px !important; + height: unset; +} + +.scheduling-calendar .react-calendar__viewContainer { + margin-top: 3rem; +} + +.scheduling-calendar .react-calendar__month-view__weekdays { + display: grid !important; + grid-template-columns: repeat(7, minmax(170px, 1fr)); + border-bottom-width: 1px; +} + +.scheduling-calendar .react-calendar__month-view__days { + display: grid !important; + grid-template-columns: repeat(7, minmax(170px, 1fr)); +} + +.scheduling-calendar .react-calendar__month-view__weekdays__weekday { + justify-self: flex-start; + color: #475467; + font-size: 16px; + font-weight: normal !important; +} + +.scheduling-calendar .react-calendar__tile { + padding: 0px !important; + color: #667085 !important; +} + +.scheduling-calendar .react-calendar__month-view__days__day--neighboringMonth { + color: #98a2b3 !important; +} + +.scheduling-calendar .react-calendar__tile--active { + background: #f0f5f3 !important; + border-radius: unset !important; + border: 2px solid #0d9895; + color: #667085 !important; + overflow: visible !important; +} + +.scheduling-calendar .react-calendar__tile:enabled:hover { + background: unset !important; +} + +.scheduling-calendar .react-calendar__tile:enabled:hover.react-calendar__tile--active:hover { + background: #f0f5f3 !important; +} + +.scheduling-calendar .react-calendar__tile--now::after { + display: none !important; +} + +.react-calendar__viewContainer { + overflow: auto; + scroll-margin-right: 1rem; +} + +.react-calendar__viewContainer::-webkit-scrollbar { + height: 0px !important; +} + +.my-z-index { + z-index: 1000; +} + +.schedule-options { + text-align: start !important; + align-items: flex-start !important; +} + +.between-slots { + background: linear-gradient(230.69deg, rgba(51, 212, 183, 0.05) 9.11%, rgba(13, 152, 149, 0.05) 69.45%) !important; +} + +.absolute-middle { + height: 10px; + top: calc(50% - 5px); +} + +.draft-stage { + border-radius: 50%; + width: 30px; + height: 30px; + box-shadow: 0 0 0 5px white, 0 0 0 7px #ccc; + display: flex; + justify-content: center; + align-items: center; + color: white; + background-color: #ccc; +} + +.draft-stage.complete { + box-shadow: 0 0 0 5px white, 0 0 0 7px #0d9895; + background-color: #0d9895; +} + +.draft-stage~.absolute { + top: calc(100% + 1rem); + width: 180px; +} + +.nav-item-dropdown { + height: 0px; + overflow: hidden; + opacity: 0; + transition: 400ms; +} + +.open .nav-item-dropdown { + height: unset; + opacity: 1; +} + +.open .caret-up { + display: none; +} + +.caret-down { + display: none; +} + +.open .caret-down { + display: inline; +} + +.super-nav { + transition: 400ms; + height: 50px; +} + +.super-nav .sidebar-item { + background-color: white; + color: #475467; +} + +.super-nav.highlight .sidebar-item, +.sidebar-item:hover { + background-color: #475467; + color: white; +} + +.super-nav.open { + height: 150px; +} + +.super-nav.open.large { + height: 200px; +} + +.super-nav.open.larger { + height: 660px; +} + +.super-nav.open.small { + height: 100px; +} + +.sun-editor-editable, +.sun-editor-editable *::placeholder { + font-size: 14px !important; + font-family: "Noto Sans", sans-serif !important; +} + +.infinite-scroll-component { + overflow: hidden !important; +} + +.hover-show-edit { + position: relative; +} + +.hover-show-edit .edit-btn { + display: none; + position: absolute; +} + +.hover-show-edit input, +.hover-show-edit .save-btn { + display: none; +} + +.hover-show-edit.edit-mode input, +.hover-show-edit.edit-mode .save-btn { + display: inline; +} + +.hover-show-edit.edit-mode span, +.hover-show-edit.edit-mode .edit-btn { + display: none !important; +} + +.hover-show-edit:hover .edit-btn { + display: inline; +} + +.options-btn { + display: none; +} + +.schedule-day:hover .options-btn { + display: flex; +} + +.sun-editor { + border-radius: 5px; + border-style: solid; +} + +.slide-down { + animation: dropDown 300ms; +} + +.slide-up { + animation: dropUp 300ms; +} + +.show-on-parent-hover { + display: none; +} + +*:has(> .show-on-parent-hover):hover .show-on-parent-hover { + display: inline; +} + +.space-tile { + aspect-ratio: 262/167; +} + +button.login-btn-gradient { + transition: opacity 400ms; + position: relative; + background: unset; + z-index: 4; + overflow: hidden; +} + +button.login-btn-gradient:disabled { + cursor: auto; +} + +button.login-btn-gradient:not(:disabled):after { + content: ""; + position: absolute; + z-index: -1; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.8; + background: linear-gradient(230.69deg, hsl(169, 65%, 42%) 9.11%, hsl(179, 84%, 22%) 69.45%); + transition: opacity 200ms linear; +} + +button.login-btn-gradient:not(:disabled):hover::after, +button.login-btn-gradient:not(:disabled):focus::after { + opacity: 0.9; +} + +button.login-btn-gradient:active::after { + opacity: 1; +} + +.react-calendar__navigation__prev-button:disabled, +.react-calendar__navigation__prev2-button:disabled { + background-color: transparent !important; +} + +.react-calendar__navigation__prev-button:disabled .prev-icon path { + opacity: 0.4; +} + +.booking-card-grid { + display: grid; + grid-template-columns: 3fr 2fr 1fr; +} + +.react-calendar__tile:disabled:has(> .schedule-day) { + opacity: 0.6; +} + +@keyframes dropUp { + 0% { + max-height: 200px; + } + + 100% { + max-height: 70px; + } +} + +@keyframes dropDown { + 0% { + max-height: 70px; + } + + 100% { + max-height: 200px; + } +} + +@media (min-width: 1200px) { + .ov-visible { + overflow: visible !important; + } + + .account-header { + min-width: fit-content; + } + + .property-swiper-slide .swiper-pagination { + display: block; + } + + .property-swiper-slide .swiper-button-next, + .property-swiper-slide .swiper-button-prev { + display: flex; + } +} + +@media (max-width: 600px) { + .account-header a { + padding-inline: 5rem !important; + } + + .popup-mobile { + position: fixed; + background-color: rgba(0, 0, 0, 0.6); + top: 0; + right: 0; + left: 0; + height: 100vh; + z-index: 54; + display: flex; + align-items: center; + justify-content: center; + padding-top: 5rem; + } + + .popup-mobile-2 { + position: absolute; + /* background-color: rgba(0, 0, 0, 0.3); */ + top: 0; + right: 0; + left: 0; + height: 100%; + z-index: 54; + display: flex; + padding: 1rem 0; + } +} + +@keyframes slideDown { + 0% { + transform: translateY(-30px); + opacity: 0; + } + + 100% { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes slideUp { + 0% { + transform: translateY(30px); + opacity: 0; + } + + 100% { + transform: translateY(0); + opacity: 1; + } +} + +.pill { + border-radius: 100vw; +} + +.horizontal-scroll-categories { + display: grid; + grid-auto-flow: column; + grid-auto-columns: var(--category-slider-width); + overflow-x: auto; + overscroll-behavior-inline: contain; + scroll-snap-type: inline mandatory; + position: relative; +} + +.horizontal-scroll-categories>* { + scroll-snap-align: start; +} + +.horizontal-scroll-categories::-webkit-scrollbar { + display: none; +} + +.mover { + display: block; + width: 105px; + border-radius: 2px; + height: 2px; + position: absolute; + left: 0; + bottom: 0; + transition: 300ms; + opacity: 0; + background-color: white; +} + +.horizontal-scroll-categories button:nth-child(1).active~.mover { + left: 0; + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(2).active~.mover { + left: var(--category-slider-width); + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(3).active~.mover { + left: calc(var(--category-slider-width) * 2); + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(4).active~.mover { + left: calc(var(--category-slider-width) * 3); + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(5).active~.mover { + left: calc(var(--category-slider-width) * 4); + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(6).active~.mover { + left: calc(var(--category-slider-width) * 5); + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(7).active~.mover { + left: calc(var(--category-slider-width) * 6); + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(8).active~.mover { + left: calc(var(--category-slider-width) * 7); + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(9).active~.mover { + left: calc(var(--category-slider-width) * 8); + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(10).active~.mover { + left: calc(var(--category-slider-width) * 9); + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(11).active~.mover { + left: calc(var(--category-slider-width) * 10); + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(12).active~.mover { + left: calc(var(--category-slider-width) * 11); + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(13).active~.mover { + left: calc(var(--category-slider-width) * 12); + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(14).active~.mover { + left: calc(var(--category-slider-width) * 13); + opacity: 1; +} + +.navbar-slider .mover { + background-color: black; + width: var(--account-menu-item-width); +} + +.navbar-slider .swiper-wrapper:has(.slider-menu:nth-child(1) a.active)~.mover { + left: 0; + opacity: 1; +} + +.navbar-slider .swiper-wrapper:has(.slider-menu:nth-child(2) a.active)~.mover { + left: calc(var(--account-menu-item-width) * 1); + opacity: 1; +} + +.navbar-slider .swiper-wrapper:has(.slider-menu:nth-child(3) a.active)~.mover { + left: calc(var(--account-menu-item-width) * 2); + opacity: 1; +} + +.navbar-slider .swiper-wrapper:has(.slider-menu:nth-child(4) a.active)~.mover { + left: calc(var(--account-menu-item-width) * 3); + opacity: 1; +} + +.navbar-slider .swiper-wrapper:has(.slider-menu:nth-child(5) a.active)~.mover { + left: calc(var(--account-menu-item-width) * 4); + opacity: 1; +} + +.navbar-slider .swiper-wrapper:has(.slider-menu:nth-child(6) a.active)~.mover { + left: calc(var(--account-menu-item-width) * 5); + opacity: 1; +} + +.navbar-slider .swiper-wrapper:has(.slider-menu:nth-child(7) a.active)~.mover { + left: calc(var(--account-menu-item-width) * 6); + opacity: 1; +} + +.two-tab-menu { + position: relative; + --menu-width: 140px; +} + +.two-tab-menu.small { + --menu-width: 120px; +} + +.two-tab-menu.smaller { + --menu-width: 105px; +} + +.two-tab-menu .mover { + background-color: black; + width: var(--menu-width); +} + +.two-tab-menu button:nth-child(1)[data-headlessui-state="selected"]~.mover { + left: 0; + opacity: 1; +} + +.two-tab-menu button:nth-child(2)[data-headlessui-state="selected"]~.mover { + left: calc(var(--menu-width) * 1); + opacity: 1; +} + +.horizontal-scroll-hosts { + display: grid; + grid-auto-flow: column; + grid-auto-columns: max-content; + overflow-x: auto; + overscroll-behavior-inline: contain; + scroll-snap-type: inline mandatory; + scroll-padding-left: 1rem; + padding-left: 1rem; +} + +.horizontal-scroll-hosts>* { + scroll-snap-align: start; +} + +.horizontal-scroll-hosts::-webkit-scrollbar { + height: 0px !important; +} + +.horizontal-scroll-accounts { + display: grid; + grid-auto-flow: column; + grid-auto-columns: max-content; + overflow-x: auto; + overscroll-behavior-inline: contain; + scroll-snap-type: inline mandatory; + scroll-padding-inline: 1rem; +} + +.horizontal-scroll-accounts>* { + scroll-snap-align: start; + padding-inline: 1.5rem; + padding-bottom: 0.5rem; +} + +.horizontal-scroll-accounts>.active { + border-bottom: 2px solid black; + font-weight: 600; +} + +.slider-menu:has(.active) { + font-weight: 600; +} + +.fade-in { + -webkit-animation: fade-in 1.2s cubic-bezier(0.39, 0.575, 0.565, 1) both; + animation: fade-in 1.2s cubic-bezier(0.39, 0.575, 0.565, 1) both; +} + +@-webkit-keyframes fade-in { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +@keyframes fade-in { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +@media (max-width: 1000px) { + .slider-menu:has(.active) { + border-bottom: 2px solid black; + } + + .navbar-slider .mover { + display: none; + } +} + +.horizontal-scroll-accounts::-webkit-scrollbar { + height: 0px !important; +} + +.date-picker .react-calendar__tile { + height: 48px !important; + font-size: 14px !important; +} + +.error-vibrate { + animation: error-vibrate 0.3s linear both; +} + +.property-space-grid { + display: grid; + gap: 4rem 2rem; + grid-template-columns: repeat(auto-fit, minmax(min(var(--property-card-width), 100%), 1fr)); +} + +@keyframes error-vibrate { + 0% { + -webkit-transform: translate(0); + transform: translate(0); + } + + 20% { + -webkit-transform: translate(-2px, 2px); + transform: translate(-2px, 2px); + } + + 40% { + -webkit-transform: translate(-2px, -2px); + transform: translate(-2px, -2px); + } + + 60% { + -webkit-transform: translate(2px, 2px); + transform: translate(2px, 2px); + } + + 80% { + -webkit-transform: translate(2px, -2px); + transform: translate(2px, -2px); + } + + 100% { + -webkit-transform: translate(0); + transform: translate(0); + } +} + +@media (max-width: 900px) { + .messages-grid { + display: grid; + grid-template-columns: 1fr; + grid-auto-rows: 1fr; + } + + .popup-tablet { + position: fixed; + background-color: rgba(0, 0, 0, 0.6); + top: 0; + right: 0; + left: 0; + height: 100vh; + z-index: 54; + display: flex; + align-items: center; + justify-content: center; + padding-top: 5rem; + width: 100%; + } +} + +/* tailwind breakpoints */ +@media (min-width: 640px) { + + .swiper-button-next, + .swiper-button-prev { + top: calc(50% - 40px) !important; + } + + .scheduling-calendar .react-calendar__navigation { + display: grid !important; + grid-template-columns: 205px 40px 250px; + gap: 12px; + align-items: center; + padding-inline: 5px; + } + + .scheduling-calendar .react-calendar__viewContainer { + margin-top: 0px; + } + + .scheduling-calendar .react-calendar__navigation__next2-button { + display: none; + } + + .property-grid { + grid-template-columns: repeat(2, minmax(292px, 1fr)); + padding-inline: 0; + column-gap: 31px; + row-gap: 80px; + max-width: unset; + } + + .browse-grid { + padding-inline: 0; + } + + .sticky-price-summary { + box-shadow: none; + } + + .pagination-image { + display: inline !important; + } + + .property-space-card .aspect { + aspect-ratio: 292/227 !important; + min-height: 227px; + } + + .browse-grid { + display: grid; + grid-template-columns: repeat(2, minmax(163px, 1fr)); + column-gap: 19px; + row-gap: 20px; + padding-inline: 1rem; + } +} + +@media (min-width: 768px) { + .horizontal-scroll-hosts { + scroll-padding-left: unset; + padding-left: unset; + } +} + +@media (min-width: 1024px) { + .property-grid { + grid-template-columns: repeat(3, minmax(292px, 1fr)); + } + + .addons-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + column-gap: 50px; + row-gap: 8px; + } + + .amenities-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + row-gap: 8px; + } + + .navbar-slider .swiper-wrapper { + transform: none !important; + } +} + +@media (min-width: 1280px) { + .property-grid { + grid-template-columns: repeat(4, minmax(292px, 1fr)); + } + + .browse-grid { + column-gap: 32px; + row-gap: 32px; + } + + .messages-grid { + display: grid; + grid-template-columns: 1.2fr 2fr 1fr; + width: 100%; + } + + .my-shadow { + box-shadow: none; + border-radius: none; + } +} + +@media (max-width: 600px) { + .react-tooltip { + display: none !important; + } +} + +@media (max-width: 650px) { + .property-space-grid { + padding-inline: 4rem; + } +} + +@media (max-width: 500px) { + .property-space-grid { + padding-inline: 1rem; + } +} + +.toast-animation { + animation: toast-animation 250ms ease-in-out; +} + +@keyframes toast-animation { + 0% { + width: 0px; + opacity: 0; + } + + 100% { + width: 20rem; + opacity: 1; + } +} + +input[type="date"] { + display: block; + -webkit-appearance: textfield; + -moz-appearance: textfield; + min-height: 1.2em; +} + +.fade-in-top { + -webkit-animation: fade-in-top 0.6s cubic-bezier(0.39, 0.575, 0.565, 1) both; + animation: fade-in-top 0.6s cubic-bezier(0.39, 0.575, 0.565, 1) both; +} + +@-webkit-keyframes fade-in-top { + 0% { + -webkit-transform: translateY(-50px); + transform: translateY(-50px); + opacity: 0; + } + + 100% { + -webkit-transform: translateY(0); + transform: translateY(0); + opacity: 1; + } +} + +@keyframes fade-in-top { + 0% { + -webkit-transform: translateY(-50px); + transform: translateY(-50px); + opacity: 0; + } + + 100% { + -webkit-transform: translateY(0); + transform: translateY(0); + opacity: 1; + } +} \ No newline at end of file diff --git a/src/index.jsx b/src/index.jsx new file mode 100644 index 0000000..8b069ac --- /dev/null +++ b/src/index.jsx @@ -0,0 +1,16 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import "./output.css"; +import App from "./App"; + +import * as serviceWorkerRegistration from './serviceWorkerRegistration'; + +const root = ReactDOM.createRoot(document.getElementById("root")); + +root.render( + // + + // +); + +serviceWorkerRegistration.register(); \ No newline at end of file diff --git a/src/layouts/AddAdminPageLayout.jsx b/src/layouts/AddAdminPageLayout.jsx new file mode 100644 index 0000000..252c68f --- /dev/null +++ b/src/layouts/AddAdminPageLayout.jsx @@ -0,0 +1,33 @@ +import React from "react"; +import Icon from "@/components/Icons"; +import { useNavigate } from "react-router-dom"; + +const AddAdminPageLayout = ({ title, backTo, children }) => { + const navigate = useNavigate(); + return ( +
+
+
+ +
+
+

Add New {title}

+
+
+
{children}
+
+ ); +}; + +export default AddAdminPageLayout; diff --git a/src/layouts/EditAdminPageLayout.jsx b/src/layouts/EditAdminPageLayout.jsx new file mode 100644 index 0000000..8b42023 --- /dev/null +++ b/src/layouts/EditAdminPageLayout.jsx @@ -0,0 +1,63 @@ +import React from "react"; +import { useNavigate } from "react-router-dom"; +import Icon from "@/components/Icons"; +import { GlobalContext } from "@/globalContext"; + +const EditAdminPageLayout = ({ title, type, backTo, children, table1, table2, deleteMessage, id, showDelete = true }) => { + const navigate = useNavigate(); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + return ( +
+
+
+ +
+
+

Edit {title}

+ {showDelete && ( + + )} +
+
+
{children}
+
+ ); +}; + +export default EditAdminPageLayout; diff --git a/src/layouts/ViewAdminPageLayout.jsx b/src/layouts/ViewAdminPageLayout.jsx new file mode 100644 index 0000000..4f80c52 --- /dev/null +++ b/src/layouts/ViewAdminPageLayout.jsx @@ -0,0 +1,57 @@ +import React, { useContext } from "react"; +import { useNavigate } from "react-router-dom"; +import Icon from "@/components/Icons"; +import { GlobalContext } from "@/globalContext"; +import { ArrowLeftIcon, TrashIcon } from "@heroicons/react/24/outline"; + +const ViewAdminPageLayout = ({ title, backTo, children, table1, table2, deleteMessage, id, showDelete = true, name }) => { + const { dispatch: globalDispatch } = useContext(GlobalContext); + const navigate = useNavigate(); + + return ( +
+
+
+ +
+
+

{name ? `${title} - ${name}` : `View ${title}`}

+ {showDelete ? ( + + ) : null} +
+
+
{children}
+
+ ); +}; + +export default ViewAdminPageLayout; diff --git a/src/logo.svg b/src/logo.svg new file mode 100644 index 0000000..6b60c10 --- /dev/null +++ b/src/logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 0000000..585e55b --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,2458 @@ +import React, { useState } from "react"; +import { AuthContext, tokenExpireError } from "./authContext"; +import { GlobalContext } from "./globalContext"; +import { Routes, Route, Navigate, useLocation, useNavigate, Link } from "react-router-dom"; +import SnackBar from "@/components/SnackBar"; +import PublicHeader from "@/components/PublicHeader"; +import TopHeader from "@/components/TopHeader"; +import AdminHeader from "@/components/AdminHeader"; +import HostHeader from "@/components/HostHeader"; +import CustomerHeader from "@/components/CustomerHeader"; +import Modal from "@/components/Modal"; +import ReviewPopUp from "@/components/ReviewPopUp"; + +import AdminForgotPage from "./pages/Admin/Auth/AdminForgotPage"; +import AdminResetPage from "./pages/Admin/Auth/AdminResetPage"; +import AdminDashboardPage from "./pages/Admin/AdminDashboardPage"; +import AdminProfilePage from "./pages/Admin/AdminProfilePage"; + +import NotFoundPage from "./pages/Admin/NotFoundPage"; + +import AdminFaqListPage from "./pages/Admin/Faq/AdminFaqListPage"; +import AddAdminFaqPage from "./pages/Admin/Faq/AddAdminFaqPage"; +import EditAdminFaqPage from "./pages/Admin/Faq/EditAdminFaqPage"; + +import AdminEmailListPage from "./pages/Admin/Email/AdminEmailListPage"; +import AddAdminEmailPage from "./pages/Admin/Email/AddAdminEmailPage"; +import EditAdminEmailPage from "./pages/Admin/Email/EditAdminEmailPage"; +import ViewAdminEmailPage from "./pages/Admin/Email/ViewAdminEmailPage"; + +import AdminAddOnListPage from "./pages/Admin/Addon/AdminAddOnListPage"; +import AddAdminAddOnPage from "./pages/Admin/Addon/AddAdminAddOnPage"; +import EditAdminAddOnPage from "./pages/Admin/Addon/EditAdminAddOnPage"; + +import AdminUserListPage from "./pages/Admin/User/AdminUserListPage"; +import AddAdminUserPage from "./pages/Admin/User/AddAdminUserPage"; +import EditAdminUserPage from "./pages/Admin/User/EditAdminUserPage"; +import ViewAdminUserPage from "./pages/Admin/User/ViewAdminUserPage"; + +import AdminHostListPage from "./pages/Admin/Host/AdminHostListPage"; +import AddAdminHostPage from "./pages/Admin/Host/AddAdminHostPage"; +import ViewAdminHostPage from "./pages/Admin/Host/ViewAdminHostPage"; + +import AdminCustomerListPage from "./pages/Admin/Customer/AdminCustomerListPage"; +import AddAdminCustomerPage from "./pages/Admin/Customer/AddAdminCustomerPage"; +import ViewAdminCustomerPage from "./pages/Admin/Customer/ViewAdminCustomerPage"; + +import AdminCustomerReviewListPage from "./pages/Admin/Review/AdminCustomerReviewListPage"; +import AdminHostReviewListPage from "./pages/Admin/Review/AdminHostReviewPage"; +import AddAdminReviewPage from "./pages/Admin/Review/AddAdminReviewPage"; +import EditAdminReviewPage from "./pages/Admin/Review/EditAdminReviewPage"; + +import AdminSpacesListPage from "./pages/Admin/Space/AdminSpacesListPage"; +import AddAdminSpacesPage from "./pages/Admin/Space/AddAdminSpacesPage"; +import EditAdminSpacesPage from "./pages/Admin/Space/EditAdminSpacesPage"; + +import AdminPropertySpacesAmenititesListPage from "./pages/Admin/PropertySpaceAmenity/AdminPropertySpacesAmenititesListPage"; +import AddAdminPropertySpacesAmenititesPage from "./pages/Admin/PropertySpaceAmenity/AddAdminPropertySpacesAmenititesPage"; +import EditAdminPropertySpacesAmenititesPage from "./pages/Admin/PropertySpaceAmenity/EditAdminPropertySpacesAmenititesPage"; + +import AdminPayoutListPage from "./pages/Admin/Payout/AdminPayoutListPage"; +import AddAdminPayoutPage from "./pages/Admin/Payout/AddAdminPayoutPage"; +import EditAdminPayoutPage from "./pages/Admin/Payout/EditAdminPayoutPage"; + +import AdminPropertyListPage from "./pages/Admin/Property/AdminPropertyListPage"; +import AddAdminPropertyPage from "./pages/Admin/Property/AddAdminPropertyPage"; +import ViewAdminPropertyPage from "./pages/Admin/Property/ViewAdminPropertyPage"; + +import AdminBookingAddonsListPage from "./pages/Admin/BookingAddon/AdminBookingAddonsListPage"; +import AddAdminBookingAddonsPage from "./pages/Admin/BookingAddon/AddAdminBookingAddonsPage"; +import EditAdminBookingAddonsPage from "./pages/Admin/BookingAddon/EditAdminBookingAddonsPage"; + +import AdminPropertySpacesListPage from "./pages/Admin/PropertySpace/AdminPropertySpacesListPage"; +import AddAdminPropertySpacesPage from "./pages/Admin/PropertySpace/AddAdminPropertySpacesPage"; +import EditAdminPropertySpacesPage from "./pages/Admin/PropertySpace/EditAdminPropertySpacesPage"; +import ViewAdminPropertySpacesPage from "./pages/Admin/PropertySpace/ViewAdminPropertySpacesPage"; + +import AdminSettingsListPage from "./pages/Admin/Setting/AdminSettingsListPage"; +import AddAdminSettingsPage from "./pages/Admin/Setting/AddAdminSettingsPage"; +import EditAdminSettingsPage from "./pages/Admin/Setting/EditAdminSettingsPage"; + +import AdminPropertySpacesImagesListPage from "./pages/Admin/PropertySpaceImage/AdminPropertySpacesImagesListPage"; +import AddAdminPropertySpacesImagesPage from "./pages/Admin/PropertySpaceImage/AddAdminPropertySpacesImagesPage"; +import EditAdminPropertySpacesImagesPage from "./pages/Admin/PropertySpaceImage/EditAdminPropertySpacesImagesPage"; + +import AdminIdVerificationListPage from "./pages/Admin/IdVerification/AdminIdVerificationListPage"; +import AddAdminIdVerificationPage from "./pages/Admin/IdVerification/AddAdminIdVerificationPage"; +import EditAdminIdVerificationPage from "./pages/Admin/IdVerification/EditAdminIdVerificationPage"; + +import AdminPropertyAddOnListPage from "./pages/Admin/PropertyAddon/AdminPropertyAddOnListPage"; +import AddAdminPropertyAddOnPage from "./pages/Admin/PropertyAddon/AddAdminPropertyAddOnPage"; +import EditAdminPropertyAddOnPage from "./pages/Admin/PropertyAddon/EditAdminPropertyAddOnPage"; + +import AdminBookingListPage from "./pages/Admin/Booking/AdminBookingListPage"; +import AddAdminBookingPage from "./pages/Admin/Booking/AddAdminBookingPage"; +import EditAdminBookingPage from "./pages/Admin/Booking/EditAdminBookingPage"; +import ViewAdminBookingPage from "./pages/Admin/Booking/ViewAdminBookingPage"; + +import AdminAmenityListPage from "./pages/Admin/Amenity/AdminAmenityListPage"; +import AddAdminAmenityPage from "./pages/Admin/Amenity/AddAdminAmenityPage"; +import EditAdminAmenityPage from "./pages/Admin/Amenity/EditAdminAmenityPage"; + +import AdminHashTagPage from "./pages/Admin/Hashtag/AdminHashTagPage"; +import AddAdminHashTagPage from "./pages/Admin/Hashtag/AddAdminHashtagPage"; +import EditAdminHashTagPage from "./pages/Admin/Hashtag/EditAdminHashTagPage"; + +import AdminPropertySpaceFaqListPage from "./pages/Admin/PropertySpaceFaq/AdminPropertySpaceFaqListPage"; +import AddAdminPropertySpaceFaqPage from "./pages/Admin/PropertySpaceFaq/AddAdminPropertySpaceFaqPage"; +import EditAdminPropertySpaceFaqPage from "./pages/Admin/PropertySpaceFaq/EditAdminPropertySpaceFaqPage"; + +import AdminPrivacyPage from "./pages/Admin/CMS/AdminPrivacyPage"; +import AdminTermsAndConditionsPage from "./pages/Admin/CMS/AdminTermsAndConditionsPage"; +import AdminCancellationPolicyPage from "./pages/Admin/CMS/AdminCancellationPolicyPage"; + +import AdminNotificationPage from "./pages/Admin/Notification/AdminNotificationListPage"; + +import ResetForm from "./pages/Common/Login/ResetForm"; +import LoginPage from "./pages/Common/Login/LoginPage"; +import RequestReset from "./pages/Common/Login/RequestReset"; +import OauthRedirect from "./pages/Common/Login/OauthRedirect"; +import ResetRedirect from "./pages/Common/Login/ResetRedirect"; + +import SignUpDetailsForm from "./pages/Common/SignUp/SignUpDetailsForm"; +import SignUpPageWrapper from "./pages/Common/SignUp/PageWrapper"; +import SignUpForm from "./pages/Common/SignUp/SignUpForm"; + +import HomePage from "./pages/Common/HomePage"; +import FaqPage from "./pages/Common/FaqPage"; +import ContactUsPage from "./pages/Common/ContactUsPage"; +import ExplorePage from "./pages/Common/ExplorePage"; +import SearchPage from "./pages/Common/SearchPage"; + +import MessagesPage from "./pages/Common/Messages/MessagesPage"; +import HostPaymentsPage from "./pages/Host/Payments/HostPaymentsPage"; + +import SpaceDetailsOne from "./pages/Host/Spaces/Add/SpaceDetailsOne"; +import SpaceDetailsTwo from "./pages/Host/Spaces/Add/SpaceDetailsTwo"; +import SpaceDetailsThree from "./pages/Host/Spaces/Add/SpaceDetailsThree"; +import SpaceDetailsFour from "./pages/Host/Spaces/Add/SpaceDetailsFour"; +import SpaceSubmitted from "./pages/Host/Spaces/Add/SpaceSubmitted"; +import SpacesPageWrapper from "./pages/Host/Spaces/Add/PageWrapper"; + +import MySpaceWrapper from "./pages/Host/Spaces/Edit/PageWrapper"; +import MySpaceDetailsPage from "./pages/Host/Spaces/MySpaceDetailsPage"; +import EditScheduleWrapper from "./pages/Host/Spaces/Edit/EditScheduleWrapper"; +import EditPropertyImagesPage from "./pages/Host/Spaces/Edit/EditPropertyImagesPage"; +import EditPropertySpacePage from "./pages/Host/Spaces/Edit/EditPropertySpacePage"; + +import BookingPageWrapper from "./pages/Common/Booking/PageWrapper"; +import FavoritesPage from "./pages/Common/FavoritesPage"; +import PropertyPage from "./pages/Common/Booking/PropertyPage"; +import BookingPreviewPage from "./pages/Common/Booking/BookingPreviewPage"; +import CustomerVerificationPage from "./pages/Customer/Verification/CustomerVerificationPage"; +import HostVerificationPage from "./pages/Host/Verification/HostVerificationPage"; +import BookingConfirmationPage from "./pages/Common/Booking/BookingConfirmationPage"; +import VerifyEmailPage from "./pages/Common/SignUp/VerifyEmailPage"; +import AdminReportsPage from "./pages/Admin/AdminReportsPage"; +import ScrollToTop from "./utils/ScrollToTop"; +import AdminColumnOrderPage from "./pages/Admin/AdminColumnOrderPage"; +import Footer from "./components/frontend/Footer"; +import { useEffect } from "react"; +import PrivacyPolicyPage from "./pages/Common/PrivacyPolicyPage"; +import CheckVerificationPage from "./pages/Common/SignUp/CheckVerificationPage"; +import CancellationPolicyPage from "./pages/Common/CancelationPolicyPage"; +import TermsAndConditionsPage from "./pages/Common/TermsAndConditionsPage"; +import AdminDevicesPage from "./pages/Admin/Devices/AdminDevicesPage.jsx"; +import ConfirmationModal from "./components/ConfirmationModal"; +import ErrorModal from "./components/ErrorModal"; +import LoadingSpinner from "./components/LoadingSpinner"; +import SessionExpiredModal from "./components/SessionExpiredModal"; +import SignUpSelectRole from "./pages/Common/SignUp/SignUpSelectRole"; +import { useMemo } from "react"; +import BecomeAHostPage from "./pages/Common/SignUp/BecomeAHostPage"; +import CheckDeleteEmailPage from "./pages/Common/CheckDeleteEmailPage"; +import ConfirmDeletePage from "./pages/Common/ConfirmDeletePage"; +import HostBookingListPage from "./pages/Host/Bookings/HostBookingListPage"; +import HostBookingDetailsPage from "./pages/Host/Bookings/HostBookingDetailsPage"; +import CustomerBookingListPage from "./pages/Customer/Bookings/CustomerBookingListPage"; +import CustomerBookingDetailsPage from "./pages/Customer/Bookings/CustomerBookingDetailsPage"; +import CustomerReviewsPage from "./pages/Customer/Reviews/CustomerReviewsPage"; +import CustomerPaymentsPage from "./pages/Customer/Payments/CustomerPaymentsPage"; +import HostReviewsPage from "./pages/Host/Reviews/HostReviewsPage"; +import NotVerifiedModal from "./components/NotVerifiedModal"; +import CustomerProfilePage from "./pages/Customer/Profile/CustomerProfilePage"; +import HostProfilePage from "./pages/Host/Profile/HostProfilePage"; +import CustomerBillingsPage from "./pages/Customer/Billings/CustomerBillingsPage"; +import HostBillingsPage from "./pages/Host/Billings/HostBillingsPage"; +import MySpacesListPage from "./pages/Host/Spaces/MySpacesListPage"; +import EditBookingPage from "./pages/Customer/Bookings/EditBookingPage"; +import HostAccountHeader from "./pages/Host/HostAccountHeader"; +import CustomerAccountHeader from "./pages/Customer/CustomerAccountHeader"; +import CustomerGettingStartedTour from "./components/CustomerGettingStartedTour"; +import HostGettingStartedTour from "./components/HostGettingStartedTour"; +import useSpaceCategories from "./hooks/api/useSpaceCategories"; +import HostPropertyRulesTemplatePage from "./pages/Host/PropertyRulesTemplate/HostPropertyRulesTemplatePage"; +import CreatePropertyRuleTemplatePage from "./pages/Host/PropertyRulesTemplate/CreatePropertyRulesTemplatePage"; +import AdminPayoutMethodListPage from "./pages/Admin/PayoutMethods/AdminPayoutMethodListPage"; +import EditPropertyRuleTemplatePage from "./pages/Host/PropertyRulesTemplate/EditPropertyRulesTemplatePage"; +import EditPropertyDetails from "./pages/Host/Spaces/Edit/EditPropertyDetails"; +import AdminRecycleBinUsers from "./pages/Admin/RecycleBin/AdminRecycleBinUsers"; +import AdminRecycleBinDevices from "./pages/Admin/RecycleBin/AdminRecycleBinDevices"; +import AdminRecycleBinProperties from "./pages/Admin/RecycleBin/AdminRecycleBinProperties"; +import AdminRecycleBinPropertySpaces from "./pages/Admin/RecycleBin/AdminRecycleBinPropertySpaces"; +import AdminRecycleBinSpaceImages from "./pages/Admin/RecycleBin/AdminRecycleSpaceImages"; +import AdminRecycleBinPropertyAmenities from "./pages/Admin/RecycleBin/AdminRecycleBinAmenities"; +import AdminRecycleBinBookingAddons from "./pages/Admin/RecycleBin/AdminRecycleBinBookingAddon"; +import AdminRecycleBinFaqs from "./pages/Admin/RecycleBin/AdminRecycleBinFaqs"; +import AdminRecycleBinHashtags from "./pages/Admin/RecycleBin/AdminRecycleHashtags"; +import AdminRecycleBinSpaces from "./pages/Admin/RecycleBin/AdminRecycleSpaces"; +import AdminRecycleBinPropertyAddons from "./pages/Admin/RecycleBin/AdminRecycleBinPropertyAddon"; +import AdminRecycleBinPropertySpaceFaqs from "./pages/Admin/RecycleBin/AdminRecycleBinSpaceFaq"; +import AdminRecycleBinPayout from "./pages/Admin/RecycleBin/AdminRecycleBinPayout"; +import AdminRecycleBinBookings from "./pages/Admin/RecycleBin/AdminRecycleBinBooking"; +import { TourProvider, useTour } from "@reactour/tour"; +import MkdSDK from "@/utils/MkdSDK"; +import { disableBodyScroll, enableBodyScroll } from "body-scroll-lock"; +import HostAddOnListPage from "./pages/Host/Addons/HostAddOnListPage"; +import EditHostAddOnPage from "./pages/Host/Addons/EditHostAddons"; +import HostAmenitiesListPage from "./pages/Host/Amenities/HostAmenitiesListPage"; +import EditHostAmenitiesPage from "./pages/Host/Amenities/EditHostAmenities"; + +function renderHeader(role) { + switch (role) { + case "superadmin": + case "admin": + return ; + + case "host": + return ; + + case "customer": + return ; + + default: + return ; + } +} + +function renderRoutes(role) { + switch (role) { + case "superadmin": + case "admin": + return ( + + } + > + } + > + } + > + + } + > + } + > + } + > + + } + > + } + > + } + > + } + > + + } + > + } + > + } + > + + } + > + } + > + + } + > + } + > + + } + > + } + > + + } + > + } + > + + } + > + } + > + + } + > + } + > + + } + > + } + > + } + > + } + > + + } + > + } + > + } + > + + } + > + } + > + } + > + + } + > + } + > + } + > + } + > + } + > + } + > + + } + > + } + > + } + > + } + > + + } + > + } + > + } + > + + } + > + } + > + } + > + } + > + } + > + } + > + } + > + + } + > + } + > + } + > + + } + > + } + > + } + > + + } + > + } + > + } + > + + } + > + } + > + } + > + + } + > + } + > + } + > + } + > + } + > + } + > + } + > + + } + > + } + > + } + > + } + > + } + > + } + > + } + > + } + > + } + > + {/* } + > */} + } + > + } + > + } + > + } + > + } + > + } + > + } + > + } + > + } + > + } + > + } + > + } + > + } + > + } + > + } + > + || } + > + + ); + break; + + case "host": + return ( + + } + /> + } + /> + } + /> + } + /> + } + /> + } + > + } + /> + } + /> + } + /> + } + /> + + + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + > + } + /> + } + /> + } + /> + + } + > + } + /> + } + /> + } + /> + } + /> + } + /> + + } + > + } + /> + } + /> + + + } + > + } + /> + } + /> + + + + } + /> + } + > + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + + } + /> + } + /> + } + /> + + } + /> + } + /> + } + /> + } + /> + } + /> + } + > + } + /> + } + /> + } + /> + } + /> + } + /> + + } + > + } + /> + } + /> + } + /> + } + /> + } + /> + + ); + + case "customer": + return ( + + } + /> + } + /> + } + /> + } + /> + } + /> + } + > + } + /> + } + /> + } + /> + } + /> + + + } + > + + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + > + } + /> + } + /> + } + /> + + } + > + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + > + + ); + + default: + return ( + + } + > + } + > + } + > + } + > + } + > + } + > + } + > + } + > + + {/* frontend login screens */} + + } + /> + } + /> + } + /> + } + /> + } + > + } + /> + } + /> + } + /> + } + /> + + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + > + } + /> + } + /> + + + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + /> + } + > + + ); + break; + } +} + +function Main() { + const { state, dispatch } = React.useContext(AuthContext); + const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext); + const { pathname } = useLocation(); + const tidioBlacklist = useMemo(() => ["/admin", "/login", "/signup", "/account/messages"], []); + const shouldInsertTidio = tidioBlacklist.every((path) => !pathname.startsWith(path)); + // const [currentStep, setCurrentStep] = useState(0); + + const sdk = new MkdSDK(); + + function insertTidio() { + // we are not using tidio right now + return; + if (document.getElementById("tidio-script")) return; + console.log("inserting tidio script"); + const script = document.createElement("script"); + + script.src = "//code.tidio.co/h0tpq7blt8pa6septktw5zcdj85psftv.js"; + script.async = true; + script.setAttribute("id", "tidio-script"); + document.body.appendChild(script); + } + + useEffect(() => { + let tidioChat = document.getElementById("tidio-chat"); + if (!tidioChat) { + if (shouldInsertTidio) { + insertTidio(); + } + return; + } + if (!shouldInsertTidio) { + tidioChat.style.display = "none"; + } else { + tidioChat.style.display = "block"; + } + }, [pathname]); + + useEffect(() => { + if (!shouldInsertTidio) return; + insertTidio(); + }, []); + + useSpaceCategories(); + + const navigate = useNavigate(); + const [step, setStep] = useState(0); + const setCurrentStep = (step) => { + setTimeout(() => { + setStep(step); + }, 1000); + }; + + async function markAsNotFirstTimeUser() { + try { + await sdk.callRawAPI("/v2/api/custom/ergo/edit-self", { profile: { getting_started: 1 } }, "POST"); + globalDispatch({ + type: "SET_USER_DATA", + payload: { + ...globalState.user, + getting_started: 1, + }, + }); + } catch (err) { + tokenExpireError(dispatch, err.message); + console.log("err", err); + } + } + + const endTour = () => { + globalDispatch({ type: "END_TOUR" }); + globalDispatch({ type: "CLOSE_MENU_ICON" }); + globalDispatch({ type: "CLOSE_ADD_PAYMENT_METHOD" }); + setCurrentStep(0) + markAsNotFirstTimeUser(); + } + const disableBody = (target) => disableBodyScroll(target) + const enableBody = (target) => enableBodyScroll(target) + + const styles = { + highlightedArea: (base, { x, y }) => ({ + ...base, + x: x + 10, + y: y + 10, + padding: "10px" + }), + maskArea: (base) => ({ ...base, rx: '10px' }), + badge: (base) => ({ ...base, right: '-0.8125em' }), + controls: (base) => ({ ...base, marginTop: 100 }), + styles: { + popover: (base) => ({ + ...base, + boxShadow: '0 0 3em rgba(0, 0, 0, 0.5)', + padding: '30px', + backgroundColor: '#dedede', + }) + }, + maskWrapper: (base) => ({ + ...base, + boxShadow: '0 0 3em rgba(0, 0, 0, 0.5)', + padding: '40px', + backgroundColor: '#dedede', + }), + popover: (base) => ({ + ...base, + boxShadow: '0 0 3em rgba(0, 0, 0, 0.5)', + padding: '40px', + backgroundColor: '#dedede', + }), + badge: (base) => ({ ...base, color: 'white', background: '#0ba68a' }), + } + + const hostSteps = + [ + { + selector: '.first-step', + content: ({ goTo, inDOM, setCurrentStep, isHighlightingObserved }) => ( +
+

Navigate to the Profile menu

+ From the profile menu, users can manage their profile, view bookings, message hosts, view reviews, manage payments and billing information. + +
+ ), + action: () => { + setCurrentStep(0) + if (!globalState?.menuIconOpen) { + globalDispatch({ type: "OPEN_MENU_ICON" }); + } + }, + position: "center", + highlightedSelectors: [".first-step"], + resizeObservables: [".first-step"], + mutationObservables: ['[data-tour="photo-step"]'], + + }, + + { + selector: '[data-tour="photo-step"]', + content: ({ goTo, inDOM, setCurrentStep }) => ( +
+ Upload your photo. All photos are subject to approval. For further questions, please visit our FAQs or + User Agreement page. +
+ + +
+
+ ), + styles: { + popover: (base) => ({ + ...base, + boxShadow: '0 0 3em rgba(0, 0, 0, 0.5)', + marginLeft: '30px', + marginTop: '30px', + backgroundColor: '#dedede', + }) + }, + position: "bottom", + action: () => { + navigate("/account/profile"); + globalDispatch({ type: "CLOSE_MENU_ICON" }); + }, + mutationObservables: ['[data-tour="photo-step"]'], + }, + { + selector: '[data-tour="profile-step"]', + content: ({ setCurrentStep }) => ( +
+ Complete your About Me and include information about yourself and/or your company. +
+ + +
+
+ ), + styles: { + popover: (base) => ({ + ...base, + boxShadow: '0 0 3em rgba(0, 0, 0, 0.5)', + marginLeft: '30px', + backgroundColor: '#dedede', + }) + }, + action: () => { + navigate("/account/profile"); + globalDispatch({ type: "CLOSE_MENU_ICON" }); + }, + mutationObservables: ['[data-tour="about-step"]'], + }, + { + selector: '[data-tour="email-step"]', + content: ({ setCurrentStep }) => ( +
+ Enable or disable Email Alerts for Site Activity if you want to be alerted via email on all site actions. +
+ + +
+
+ ), + styles: { + popover: (base) => ({ + ...base, + boxShadow: '0 0 3em rgba(0, 0, 0, 0.5)', + backgroundColor: '#dedede', + }) + }, + position: "center", + action: () => { + navigate("/account/profile"); + globalDispatch({ type: "CLOSE_MENU_ICON" }); + }, + mutationObservables: ['[data-tour="email-step"]'], + }, + { + selector: '[data-tour="fourth-step"]', + onTransition: { + position: "center" + }, + content: ({ goTo, inDOM, setCurrentStep, transition }) => ( +
+

Click on Get Verified to submit your identity for verification.

+
+
+ + +
+
+
+ ), + styles: { + popover: (base) => ({ + ...base, + boxShadow: '0 0 3em rgba(0, 0, 0, 0.5)', + marginTop: '30px', + backgroundColor: '#dedede', + }) + }, + action: () => { + navigate("/account/profile"); + globalDispatch({ type: "CLOSE_MENU_ICON" }); + }, + mutationObservables: ['[data-tour="fourth-step"]'], + }, + { + selector: '[data-tour="fifth-step"]', + content: ({ goTo, inDOM, setCurrentStep }) => ( +
+

Select and Upload a Government issued ID or Passport.

+ Identification is subject to approval. The image must be current, legible and expiration date must be valid. For further questions, please review our User Agreement. +
+ + +
+
+ ), + position: 'bottom', + styles: { + popover: (base) => ({ + ...base, + boxShadow: '0 0 3em rgba(0, 0, 0, 0.5)', + marginTop: '30px', + marginLeft: '30px', + backgroundColor: '#dedede', + }) + }, + mutationObservables: [`[data-tour-id="mask-position-recompute"]`] + }, + + { + selector: "", + content: ({ goTo, inDOM, setCurrentStep }) => ( +
+ +
+ ), + styles: { + popover: (base) => ({ + ...base, + boxShadow: '0 0 3em rgba(0, 0, 0, 0.5)', + marginTop: '-30px', + marginLeft: '20px', + backgroundColor: '#dedede', + }) + }, + action: () => { + if (pathname != "/account/verification") { + navigate("/account/verification"); + } + }, + }, + { + selector: ".tenth-step", + content: ({ goTo, inDOM, setCurrentStep }) => { + return ( +
+

Under Billing, please add your payment and payout methods

+
+ + +
+
+ ) + }, + styles: { + popover: (base) => ({ + ...base, + boxShadow: '0 0 3em rgba(0, 0, 0, 0.5)', + marginTop: '30px', + backgroundColor: '#dedede', + }) + }, + action: () => { + navigate("/account/billing"); + globalDispatch({ type: "CLOSE_MENU_ICON" }); + }, + }, + { + selector: ".twelfth-step", + content: ({ goTo, inDOM, setCurrentStep }) => ( +
+

Under Payments, view your payment history

+ Payments you’ve received from Customers after bookings are completed. + {/* Once approved, you will receive an email with approval confirmation from our support team and your account will be activated. For questions or concerns, please navigate to the FAQs page. */} +
+ + +
+
+ ), + position: "bottom", + action: () => { + if (pathname != "/account/payments") { + navigate("/account/payments"); + globalDispatch({ type: "CLOSE_MENU_ICON" }); + } + }, + }, + { + selector: ".thirteenth-step", + content: ({ goTo, inDOM, setCurrentStep }) => ( +
+

See reviews given by your customers

+ {/* Once approved, you will receive an email with approval confirmation from our support team and your account will be activated. For questions or concerns, please navigate to the FAQs page. */} +
+ + +
+
+ ), + position: "top", + styles: { + popover: (base) => ({ + ...base, + marginLeft: '30px', + marginTop: '30px', + }) + }, + action: () => { + if (globalState.menuIconOpen) { + globalDispatch({ type: "CLOSE_MENU_ICON" }); + } + if (globalState.addPaymentMethodModal) { + globalDispatch({ type: "CLOSE_ADD_PAYMENT_METHOD" }); + } + if (pathname != "/account/reviews") { + navigate("/account/reviews"); + } + }, + }, + { + selector: ".seventeen-step", + content: ({ goTo, inDOM, setCurrentStep }) => ( +
+

Manage all bookings made for your space

+
+ + +
+
+ ), + position: "center", + styles: { + popover: (base) => ({ + ...base, + boxShadow: '0 0 3em rgba(0, 0, 0, 0.5)', + marginTop: '10px', + backgroundColor: '#dedede', + }) + }, + action: () => { + if (pathname != "/account/my-bookings") { + navigate("/account/my-bookings"); + } + }, + }, + { + selector: ".nineteen-step", + content: ({ goTo, inDOM, setCurrentStep }) => ( +
+

Chat with your customers

+ You will be notified via email when new messages are received. +
+ + +
+
+ ), + position: "center", + styles: { + popover: (base) => ({ + ...base, + boxShadow: '0 0 3em rgba(0, 0, 0, 0.5)', + backgroundColor: '#dedede', + }) + }, + action: () => { + // if (pathname != "/account/messages") { + navigate("/account/messages"); + // } + }, + }, + { + selector: ".fourteen-step", + content: ({ goTo, inDOM, setCurrentStep }) => ( +
+

Add a new space

+
+ + +
+
+ ), + styles: { + popover: (base) => ({ + ...base, + boxShadow: '0 0 3em rgba(0, 0, 0, 0.5)', + marginTop: '30px', + marginLeft: '30px', + display: 'flex', + justify: 'center', + position: 'center', + backgroundColor: '#dedede', + }) + }, + position: "center", + action: () => { + // if (pathname != "/account/my-spaces") { + navigate("/account/my-spaces"); + // } + }, + }, + { + selector: ".fifteen-step", + content: ({ goTo, inDOM, setCurrentStep }) => ( +
+

Input details of your space

+
+ + +
+
+ ), + position: "center", + action: () => { + if (pathname != "/spaces/add") { + navigate("/spaces/add"); + } + }, + }, + { + selector: ".eighteen-step-imag", + content: ({ goTo, inDOM, setCurrentStep }) => ( +
+

Add Images, Addons, Amenities, Faqs for your space

+
+ + +
+
+ ), + position: "center", + styles: { + popover: (base) => ({ + ...base, + boxShadow: '0 0 3em rgba(0, 0, 0, 0.5)', + marginTop: '-30px', + backgroundColor: '#dedede', + }) + }, + action: () => { + if (pathname != "/spaces/add/2") { + navigate("/spaces/add/2"); + } + }, + }, + { + selector: ".eighteen-step-schedul", + content: ({ goTo, inDOM, setCurrentStep }) => ( +
+

Add available slots for your space

+ Customize and maintain the available slots for your space. +
+ + +
+
+ ), + position: "center", + action: () => { + if (pathname != "/spaces/add/3") { + navigate("/spaces/add/3"); + } + }, + }, + { + selector: ".eighteen-step-summary", + content: ({ goTo, inDOM, setCurrentStep }) => ( +
+

Review your space details

+ Review the final details for your space for approval and posting. +
+ + +
+
+ ), + position: "center", + action: () => { + if (pathname != "/spaces/add/4") { + navigate("/spaces/add/4"); + } + }, + }, + { + selector: ".sixteen-step", + content: ({ goTo, inDOM, setCurrentStep }) => ( +
+

Submit your space for admin approval

+ Once approved, you will receive an email with approval confirmation from our support team and your account will be activated. For questions or concerns, please navigate to the FAQs page. +
+ + +
+
+ ), + styles: { + popover: (base) => ({ + ...base, + boxShadow: '0 0 3em rgba(0, 0, 0, 0.5)', + marginTop: '-30px', + marginLeft: '20px', + backgroundColor: '#dedede', + }) + }, + action: () => { + if (pathname != "/spaces/add/4") { + navigate("/spaces/add/4"); + } + }, + }, + { + selector: "last_step", + content: ({ goTo, inDOM, setIsOpen }) => ( +
+ +
+ ), + position: "center", + action: () => { + navigate("/"); + }, + }, + + ] + + const customerSteps = [ + { + selector: '.first-step', + content: ({ goTo, inDOM, setCurrentStep }) => ( +
+

Navigate to the Profile menu

+ From the profile menu, users can manage their profile, view bookings, message hosts, view reviews, manage payments and billing information. + +
+ ), + action: () => { + if (!globalState?.menuIconOpen) { + globalDispatch({ type: "OPEN_MENU_ICON" }); + } + }, + position: "center", + highlightedSelectors: [".first-step"], + resizeObservables: [".first-step2"], + mutationObservables: [".first-step2"], + }, + { + selector: '[data-tour="photo-step"]', + content: ({ goTo, inDOM, setCurrentStep }) => ( +
+ Upload your photo. All photos are subject to approval. For further questions, please visit our FAQs or + User Agreement page. +
+ + +
+
+ ), + styles: { + popover: (base) => ({ + ...base, + boxShadow: '0 0 3em rgba(0, 0, 0, 0.5)', + marginLeft: '30px', + marginTop: '30px', + backgroundColor: '#dedede', + }) + }, + position: "bottom", + action: () => { + navigate("/account/profile"); + globalDispatch({ type: "CLOSE_MENU_ICON" }); + }, + mutationObservables: ['[data-tour="photo-step"]'], + }, + { + selector: '[data-tour="profile-step"]', + content: ({ goTo, inDOM, setCurrentStep }) => ( +
+ Complete your About Me and include information about yourself. +
+ + +
+
+ ), + styles: { + popover: (base) => ({ + ...base, + boxShadow: '0 0 3em rgba(0, 0, 0, 0.5)', + marginLeft: '30px', + backgroundColor: '#dedede', + }) + }, + action: () => { + navigate("/account/profile"); + globalDispatch({ type: "CLOSE_MENU_ICON" }); + }, + mutationObservables: ['[data-tour="about-step"]'], + }, + { + selector: '[data-tour="email-step"]', + content: ({ goTo, inDOM, setCurrentStep }) => ( +
+ Enable or disable Email Alerts for Site Activity if you want to be alerted via email on all site actions. +
+ + +
+
+ ), + styles: { + popover: (base) => ({ + ...base, + boxShadow: '0 0 3em rgba(0, 0, 0, 0.5)', + backgroundColor: '#dedede', + }) + }, + position: "center", + action: () => { + navigate("/account/profile"); + globalDispatch({ type: "CLOSE_MENU_ICON" }); + }, + mutationObservables: ['[data-tour="email-step"]'], + }, + { + selector: '[data-tour="fourth-step"]', + content: ({ goTo, inDOM, setCurrentStep }) => ( +
+

Click on Get Verified to submit your identity for verification.

+
+
+ + +
+
+
+ ), + styles: { + popover: (base) => ({ + ...base, + boxShadow: '0 0 3em rgba(0, 0, 0, 0.5)', + marginTop: '30px', + backgroundColor: '#dedede', + }) + }, + action: () => { + navigate("/account/profile"); + globalDispatch({ type: "CLOSE_MENU_ICON" }); + }, + mutationObservables: ['[data-tour="fourth-step"]'], + }, + { + selector: '[data-tour="fifth-step"]', + content: ({ goTo, inDOM, setCurrentStep }) => ( +
+

Select and Upload a Government issued ID or Passport.

+ Identification is subject to approval. The image must be current, legible and expiration date must be valid. For further questions, please review our User Agreement. +
+ + +
+
+ ), + styles: { + popover: (base) => ({ + ...base, + boxShadow: '0 0 3em rgba(0, 0, 0, 0.5)', + marginTop: '30px', + marginLeft: '30px', + display: 'flex', + justify: 'center', + position: 'center', + backgroundColor: '#dedede', + }) + }, + mutationObservables: ['[data-tour="fifth-step"]'], + }, + + { + selector: "", + content: ({ goTo, inDOM, setCurrentStep }) => ( +
+
+ ), + styles: { + popover: (base) => ({ + ...base, + boxShadow: '0 0 3em rgba(0, 0, 0, 0.5)', + backgroundColor: '#dedede', + marginLeft: '20px', + marginTop: '-30px', + }) + }, + action: () => { + if (pathname != "/account/verification") { + navigate("/account/verification"); + } + }, + }, + { + selector: ".tenth-step", + content: ({ goTo, inDOM, setCurrentStep }) => { + return ( +
+

Under Billing, please add your payment methods

+
+ + +
+
+ ) + }, + styles: { + popover: (base) => ({ + ...base, + boxShadow: '0 0 3em rgba(0, 0, 0, 0.5)', + marginTop: '30px', + backgroundColor: '#dedede', + }) + }, + action: () => { + if (pathname != "/account/billing") { + navigate("/account/billing"); + globalDispatch({ type: "CLOSE_MENU_ICON" }); + } + }, + }, + { + selector: ".twelfth-step", + content: ({ goTo, inDOM, setCurrentStep }) => ( +
+

Under Payments, view your payment history

+ Payments you’ve made after bookings are completed. +
+ + +
+
+ ), + position: "center", + action: () => { + if (pathname != "/account/payments") { + navigate("/account/payments"); + globalDispatch({ type: "CLOSE_MENU_ICON" }); + } + }, + }, + { + selector: ".thirteenth-step", + content: ({ goTo, inDOM, setCurrentStep }) => ( +
+

View your review history

+
+ + +
+
+ ), + position: "center", + action: () => { + if (globalState.menuIconOpen) { + globalDispatch({ type: "CLOSE_MENU_ICON" }); + } + if (globalState.addPaymentMethodModal) { + globalDispatch({ type: "CLOSE_ADD_PAYMENT_METHOD" }); + } + if (pathname != "/account/reviews") { + navigate("/account/reviews"); + } + }, + }, + { + selector: ".seventeen-step", + content: ({ goTo, inDOM, setCurrentStep }) => ( +
+

View your booking history

+
+ + +
+
+ ), + position: "center", + action: () => { + if (pathname != "/account/my-bookings") { + navigate("/account/my-bookings"); + } + }, + }, + { + selector: ".nineteen-step", + content: ({ goTo, inDOM, setCurrentStep }) => ( +
+

View and manage your messages with your hosts

+ You will be notified via email when new messages are received. +
+ + +
+
+ ), + styles: { + popover: (base) => ({ + ...base, + boxShadow: '0 0 3em rgba(0, 0, 0, 0.5)', + marginLeft: '10px', + backgroundColor: '#dedede', + }) + }, + action: () => { + if (pathname != "/account/messages") { + navigate("/account/messages"); + } + }, + }, + { + selector: "last_step", + content: ({ goTo, inDOM, setIsOpen, setCurrentStep }) => ( +
+ +
+ ), + position: "center", + action: () => { + navigate("/"); + }, + }, + + + ] + + + return ( +
+ + + {state.role == "host" && + { + return [prev.x, prev.y] + }} + setCurrentStep={setCurrentStep} + currentStep={step} + accentColor="#0ba68a" + steps={hostSteps} + onClickClose={({ setIsOpen, setCurrentStep }) => { setIsOpen(false); globalDispatch({ type: "CLOSE_MENU_ICON" }); setCurrentStep(0) }} + onClickMask={({ setIsOpen, setCurrentStep }) => { setIsOpen(false); globalDispatch({ type: "CLOSE_MENU_ICON" }); setCurrentStep(0) }} + isOpen={globalState?.tourOpen}> +
+ {!state.isAuthenticated ? : renderHeader(state.role)} +
+ {state.isAuthenticated && ["superadmin", "admin"].includes(state.role) ? : null} +
+ {!state.isAuthenticated ? renderRoutes("none") : renderRoutes(state.role)} + + {globalState.showModal ? ( + + ) : null} + {globalState.showReview ? ( + + ) : null} + + + {state.role == "host" && } +
+
+
+
+
+ } + {state.role != "host" && + { setIsOpen(false); globalDispatch({ type: "CLOSE_MENU_ICON" }); setCurrentStep(0) }} + onClickMask={({ setIsOpen, setCurrentStep }) => { setIsOpen(false); globalDispatch({ type: "CLOSE_MENU_ICON" }); setCurrentStep(0) }} + isOpen={globalState?.tourOpen}> +
+ {!state.isAuthenticated ? : renderHeader(state.role)} +
+ {state.isAuthenticated && ["superadmin", "admin"].includes(state.role) ? : null} +
+ {!state.isAuthenticated ? renderRoutes("none") : renderRoutes(state.role)} + + {globalState.showModal ? ( + + ) : null} + {globalState.showReview ? ( + + ) : null} + + + {state.role == "customer" && } +
+
+
+
+
+ } + + + + + +
+ ); +} + +export default Main; diff --git a/src/output.css b/src/output.css new file mode 100644 index 0000000..007094e --- /dev/null +++ b/src/output.css @@ -0,0 +1,7614 @@ +@import url("https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;1,300;1,400&display=swap"); +@tailwind base; +@tailwind components; +@tailwind utilities; + +*, +*::before, +*::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --navbar-height: 160px; + --messages-page-height: calc(100vh - var(--navbar-height)); + --property-card-img-height: 250px; + --property-card-width: 300px; + --category-slider-width: 120px; + --account-menu-item-width: 120px; +} + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-family: medium-content-sans-serif-font, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; + font-family: "General Sans"; + font-family: "Noto Sans", sans-serif; + scroll-behavior: auto !important; +} + +.shepherd-element { + background-color: red; + position: relative; + /* top: 50%; */ +} + +html { + overflow-x: hidden; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; +} + +.reactour__mask { + background-color: transparent !important; +} + +input, +textarea, +button, +select, +a { + -webkit-tap-highlight-color: transparent; +} + +.hidden-scrollbar { + overflow: -moz-scrollbars-none; /* For older Firefox versions */ + scrollbar-width: none; /* For Firefox */ + -ms-overflow-style: none; /* IE and Edge */ +} + +.hidden-scrollbar::-webkit-scrollbar { + display: none; /* For WebKit browsers */ +} + +.sidebar-holder { + width: 100%; + min-width: 240px; + max-width: 240px; + position: relative; + background: #ffff; + color: #475467; + z-index: 2; + transition: all 0.3s; + min-height: 100vh; + max-height: 100vh; + transition: 0.2s; +} + +/* Hide scrollbar for Chrome, Safari and Opera */ + +.sidebar-holder::-webkit-scrollbar { + display: none; +} + +.sidebar-holdee::-webkit-scrollbar { + display: none; +} + +/* Hide scrollbar for IE, Edge and Firefox */ + +.sidebar-holder { + -ms-overflow-style: none; + /* IE and Edge */ + scrollbar-width: none; + /* Firefox */ +} + +.sidebar-holdee { + -ms-overflow-style: none; + /* IE and Edge */ + scrollbar-width: none; + /* Firefox */ +} + +.open-nav { + min-width: 0px !important; + max-width: 0px !important; + width: 0 !important; + transition: 0.2s; + opacity: 0; +} + +.sidebar-list ul li a { + padding: 10px; + display: flex; + width: 100%; + margin: 2px 0; + font-size: 15px; + font-weight: 600; + transition: 0.2s ease-in; + text-transform: capitalize; +} + +.sidebar-list .sidebar-item { + padding: 10px; + display: flex; + width: 100%; + margin: 2px 0; + font-size: 15px; + font-weight: 600; + transition: 0.2s ease-in; + text-transform: capitalize; + cursor: pointer; +} + +.page-header { + width: 100%; + padding: 20px; + background: white; +} + +.page-header span { + cursor: pointer; + display: block; + width: -moz-fit-content; + width: fit-content; + font-size: 20px; +} + +.center-svg { + aspect-ratio: 1/1; + align-items: center; + justify-content: center; + line-height: 1.2em !important; +} + +.uppy-Dashboard-inner { + width: 100% !important; +} + +/* ! tailwindcss v3.2.7 | MIT License | https://tailwindcss.com */ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +*/ + +html { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font family by default. +2. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-size: 1em; + /* 2 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, +textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden] { + display: none; +} + +*, +::before, +::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +.container { + width: 100%; +} + +@media (min-width: 640px) { + .container { + max-width: 640px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 768px; + } +} + +@media (min-width: 1024px) { + .container { + max-width: 1024px; + } +} + +@media (min-width: 1280px) { + .container { + max-width: 1280px; + } +} + +@media (min-width: 1536px) { + .container { + max-width: 1536px; + } +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; +} + +.pointer-events-none { + pointer-events: none; +} + +.pointer-events-auto { + pointer-events: auto; +} + +.invisible { + visibility: hidden; +} + +.fixed { + position: fixed; +} + +.\!absolute { + position: absolute !important; +} + +.absolute { + position: absolute; +} + +.relative { + position: relative; +} + +.sticky { + position: sticky; +} + +.inset-0 { + top: 0px; + right: 0px; + bottom: 0px; + left: 0px; +} + +.inset-y-0 { + top: 0px; + bottom: 0px; +} + +.-bottom-1 { + bottom: -0.25rem; +} + +.-left-0 { + left: -0px; +} + +.-left-1\/2 { + left: -50%; +} + +.-left-1\/3 { + left: -33.333333%; +} + +.-left-4 { + left: -1rem; +} + +.-left-6 { + left: -1.5rem; +} + +.-left-8 { + left: -2rem; +} + +.-right-1 { + right: -0.25rem; +} + +.-right-1\/3 { + right: -33.333333%; +} + +.-top-1 { + top: -0.25rem; +} + +.bottom-0 { + bottom: 0px; +} + +.bottom-4 { + bottom: 1rem; +} + +.bottom-\[27px\] { + bottom: 27px; +} + +.bottom-\[6rem\] { + bottom: 6rem; +} + +.left-0 { + left: 0px; +} + +.left-1\/2 { + left: 50%; +} + +.right-0 { + right: 0px; +} + +.right-1 { + right: 0.25rem; +} + +.right-2 { + right: 0.5rem; +} + +.right-5 { + right: 1.25rem; +} + +.right-6 { + right: 1.5rem; +} + +.right-\[unset\] { + right: unset; +} + +.top-0 { + top: 0px; +} + +.top-1 { + top: 0.25rem; +} + +.top-1\/2 { + top: 50%; +} + +.top-5 { + top: 1.25rem; +} + +.top-\[20\%\] { + top: 20%; +} + +.top-full { + top: 100%; +} + +.z-10 { + z-index: 10; +} + +.z-40 { + z-index: 40; +} + +.z-50 { + z-index: 50; +} + +.z-\[200\] { + z-index: 200; +} + +.float-right { + float: right; +} + +.-mx-3 { + margin-left: -0.75rem; + margin-right: -0.75rem; +} + +.-mx-4 { + margin-left: -1rem; + margin-right: -1rem; +} + +.mx-1 { + margin-left: 0.25rem; + margin-right: 0.25rem; +} + +.mx-auto { + margin-left: auto; + margin-right: auto; +} + +.my-2 { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} + +.my-3 { + margin-top: 0.75rem; + margin-bottom: 0.75rem; +} + +.my-4 { + margin-top: 1rem; + margin-bottom: 1rem; +} + +.my-6 { + margin-top: 1.5rem; + margin-bottom: 1.5rem; +} + +.my-8 { + margin-top: 2rem; + margin-bottom: 2rem; +} + +.my-\[10px\] { + margin-top: 10px; + margin-bottom: 10px; +} + +.my-\[16px\] { + margin-top: 16px; + margin-bottom: 16px; +} + +.my-\[20px\] { + margin-top: 20px; + margin-bottom: 20px; +} + +.my-\[24px\] { + margin-top: 24px; + margin-bottom: 24px; +} + +.my-\[2px\] { + margin-top: 2px; + margin-bottom: 2px; +} + +.my-\[30px\] { + margin-top: 30px; + margin-bottom: 30px; +} + +.my-\[32px\] { + margin-top: 32px; + margin-bottom: 32px; +} + +.my-\[37px\] { + margin-top: 37px; + margin-bottom: 37px; +} + +.my-\[47px\] { + margin-top: 47px; + margin-bottom: 47px; +} + +.my-\[48px\] { + margin-top: 48px; + margin-bottom: 48px; +} + +.my-auto { + margin-top: auto; + margin-bottom: auto; +} + +.-mb-1 { + margin-bottom: -0.25rem; +} + +.-mb-px { + margin-bottom: -1px; +} + +.-mt-16 { + margin-top: -4rem; +} + +.-mt-2 { + margin-top: -0.5rem; +} + +.-mt-5 { + margin-top: -1.25rem; +} + +.mb-0 { + margin-bottom: 0px; +} + +.mb-1 { + margin-bottom: 0.25rem; +} + +.mb-10 { + margin-bottom: 2.5rem; +} + +.mb-12 { + margin-bottom: 3rem; +} + +.mb-16 { + margin-bottom: 4rem; +} + +.mb-2 { + margin-bottom: 0.5rem; +} + +.mb-20 { + margin-bottom: 5rem; +} + +.mb-24 { + margin-bottom: 6rem; +} + +.mb-3 { + margin-bottom: 0.75rem; +} + +.mb-32 { + margin-bottom: 8rem; +} + +.mb-4 { + margin-bottom: 1rem; +} + +.mb-40 { + margin-bottom: 10rem; +} + +.mb-5 { + margin-bottom: 1.25rem; +} + +.mb-6 { + margin-bottom: 1.5rem; +} + +.mb-8 { + margin-bottom: 2rem; +} + +.mb-\[11px\] { + margin-bottom: 11px; +} + +.mb-\[12px\] { + margin-bottom: 12px; +} + +.mb-\[13px\] { + margin-bottom: 13px; +} + +.mb-\[15px\] { + margin-bottom: 15px; +} + +.mb-\[16px\] { + margin-bottom: 16px; +} + +.mb-\[17px\] { + margin-bottom: 17px; +} + +.mb-\[18px\] { + margin-bottom: 18px; +} + +.mb-\[19px\] { + margin-bottom: 19px; +} + +.mb-\[20px\] { + margin-bottom: 20px; +} + +.mb-\[21px\] { + margin-bottom: 21px; +} + +.mb-\[22px\] { + margin-bottom: 22px; +} + +.mb-\[24px\] { + margin-bottom: 24px; +} + +.mb-\[26px\] { + margin-bottom: 26px; +} + +.mb-\[28px\] { + margin-bottom: 28px; +} + +.mb-\[2px\] { + margin-bottom: 2px; +} + +.mb-\[30px\] { + margin-bottom: 30px; +} + +.mb-\[32px\] { + margin-bottom: 32px; +} + +.mb-\[34px\] { + margin-bottom: 34px; +} + +.mb-\[40px\] { + margin-bottom: 40px; +} + +.mb-\[44px\] { + margin-bottom: 44px; +} + +.mb-\[48px\] { + margin-bottom: 48px; +} + +.mb-\[60px\] { + margin-bottom: 60px; +} + +.mb-\[66px\] { + margin-bottom: 66px; +} + +.mb-\[6px\] { + margin-bottom: 6px; +} + +.mb-\[70px\] { + margin-bottom: 70px; +} + +.mb-\[8px\] { + margin-bottom: 8px; +} + +.ml-1 { + margin-left: 0.25rem; +} + +.ml-2 { + margin-left: 0.5rem; +} + +.ml-3 { + margin-left: 0.75rem; +} + +.ml-4 { + margin-left: 1rem; +} + +.ml-5 { + margin-left: 1.25rem; +} + +.ml-6 { + margin-left: 1.5rem; +} + +.ml-7 { + margin-left: 1.75rem; +} + +.ml-8 { + margin-left: 2rem; +} + +.ml-auto { + margin-left: auto; +} + +.mr-1 { + margin-right: 0.25rem; +} + +.mr-10 { + margin-right: 2.5rem; +} + +.mr-2 { + margin-right: 0.5rem; +} + +.mr-3 { + margin-right: 0.75rem; +} + +.mr-4 { + margin-right: 1rem; +} + +.mr-5 { + margin-right: 1.25rem; +} + +.mr-\[11px\] { + margin-right: 11px; +} + +.mr-\[16px\] { + margin-right: 16px; +} + +.mr-\[22px\] { + margin-right: 22px; +} + +.mr-\[31px\] { + margin-right: 31px; +} + +.mt-0 { + margin-top: 0px; +} + +.mt-1 { + margin-top: 0.25rem; +} + +.mt-10 { + margin-top: 2.5rem; +} + +.mt-16 { + margin-top: 4rem; +} + +.mt-2 { + margin-top: 0.5rem; +} + +.mt-3 { + margin-top: 0.75rem; +} + +.mt-4 { + margin-top: 1rem; +} + +.mt-6 { + margin-top: 1.5rem; +} + +.mt-8 { + margin-top: 2rem; +} + +.mt-96 { + margin-top: 24rem; +} + +.mt-\[120px\] { + margin-top: 120px; +} + +.mt-\[12px\] { + margin-top: 12px; +} + +.mt-\[14px\] { + margin-top: 14px; +} + +.mt-\[19px\] { + margin-top: 19px; +} + +.mt-\[24px\] { + margin-top: 24px; +} + +.mt-\[2px\] { + margin-top: 2px; +} + +.mt-\[40px\] { + margin-top: 40px; +} + +.mt-\[6px\] { + margin-top: 6px; +} + +.block { + display: block; +} + +.inline-block { + display: inline-block; +} + +.inline { + display: inline; +} + +.flex { + display: flex; +} + +.inline-flex { + display: inline-flex; +} + +.table { + display: table; +} + +.grid { + display: grid; +} + +.\!hidden { + display: none !important; +} + +.hidden { + display: none; +} + +.h-0 { + height: 0px; +} + +.h-1\/2 { + height: 50%; +} + +.h-10 { + height: 2.5rem; +} + +.h-12 { + height: 3rem; +} + +.h-16 { + height: 4rem; +} + +.h-24 { + height: 6rem; +} + +.h-32 { + height: 8rem; +} + +.h-4 { + height: 1rem; +} + +.h-40 { + height: 10rem; +} + +.h-5 { + height: 1.25rem; +} + +.h-6 { + height: 1.5rem; +} + +.h-8 { + height: 2rem; +} + +.h-\[120px\] { + height: 120px; +} + +.h-\[130px\] { + height: 130px; +} + +.h-\[150px\] { + height: 150px; +} + +.h-\[16px\] { + height: 16px; +} + +.h-\[180px\] { + height: 180px; +} + +.h-\[18px\] { + height: 18px; +} + +.h-\[1px\] { + height: 1px; +} + +.h-\[20px\] { + height: 20px; +} + +.h-\[23px\] { + height: 23px; +} + +.h-\[24px\] { + height: 24px; +} + +.h-\[28px\] { + height: 28px; +} + +.h-\[300px\] { + height: 300px; +} + +.h-\[30px\] { + height: 30px; +} + +.h-\[32px\] { + height: 32px; +} + +.h-\[36px\] { + height: 36px; +} + +.h-\[381px\] { + height: 381px; +} + +.h-\[38px\] { + height: 38px; +} + +.h-\[40px\] { + height: 40px; +} + +.h-\[45px\] { + height: 45px; +} + +.h-\[48px\] { + height: 48px; +} + +.h-\[56px\] { + height: 56px; +} + +.h-\[60px\] { + height: 60px; +} + +.h-\[60vh\] { + height: 60vh; +} + +.h-\[68px\] { + height: 68px; +} + +.h-\[70vh\] { + height: 70vh; +} + +.h-\[72px\] { + height: 72px; +} + +.h-\[80px\] { + height: 80px; +} + +.h-\[83vh\] { + height: 83vh; +} + +.h-\[84vh\] { + height: 84vh; +} + +.h-\[var\(--messages-page-height\)\] { + height: var(--messages-page-height); +} + +.h-\[var\(--property-card-img-height\)\] { + height: var(--property-card-img-height); +} + +.h-auto { + height: auto; +} + +.h-fit { + height: -moz-fit-content; + height: fit-content; +} + +.h-full { + height: 100%; +} + +.h-screen { + height: 100vh; +} + +.max-h-0 { + max-height: 0px; +} + +.max-h-60 { + max-height: 15rem; +} + +.max-h-\[150px\] { + max-height: 150px; +} + +.max-h-\[200px\] { + max-height: 200px; +} + +.max-h-\[300px\] { + max-height: 300px; +} + +.max-h-\[400px\] { + max-height: 400px; +} + +.max-h-\[40vh\] { + max-height: 40vh; +} + +.max-h-\[500px\] { + max-height: 500px; +} + +.max-h-\[600px\] { + max-height: 600px; +} + +.max-h-\[60vh\] { + max-height: 60vh; +} + +.max-h-\[68px\] { + max-height: 68px; +} + +.max-h-\[70vh\] { + max-height: 70vh; +} + +.max-h-\[80px\] { + max-height: 80px; +} + +.max-h-\[900px\] { + max-height: 900px; +} + +.max-h-\[var\(--messages-page-height\)\] { + max-height: var(--messages-page-height); +} + +.max-h-fit { + max-height: -moz-fit-content; + max-height: fit-content; +} + +.\!min-h-\[40px\] { + min-height: 40px !important; +} + +.min-h-\[180px\] { + min-height: 180px; +} + +.min-h-\[200px\] { + min-height: 200px; +} + +.min-h-\[300px\] { + min-height: 300px; +} + +.min-h-\[400px\] { + min-height: 400px; +} + +.min-h-\[40px\] { + min-height: 40px; +} + +.min-h-\[45px\] { + min-height: 45px; +} + +.min-h-\[500px\] { + min-height: 500px; +} + +.min-h-\[var\(--messages-page-height\)\] { + min-height: var(--messages-page-height); +} + +.min-h-full { + min-height: 100%; +} + +.min-h-screen { + min-height: 100vh; +} + +.\!w-3\/4 { + width: 75% !important; +} + +.\!w-\[120px\] { + width: 120px !important; +} + +.\!w-\[50px\] { + width: 50px !important; +} + +.\!w-\[60px\] { + width: 60px !important; +} + +.w-1\/2 { + width: 50%; +} + +.w-1\/3 { + width: 33.333333%; +} + +.w-10\/12 { + width: 83.333333%; +} + +.w-11 { + width: 2.75rem; +} + +.w-11\/12 { + width: 91.666667%; +} + +.w-20 { + width: 5rem; +} + +.w-4 { + width: 1rem; +} + +.w-40 { + width: 10rem; +} + +.w-5 { + width: 1.25rem; +} + +.w-5\/6 { + width: 83.333333%; +} + +.w-56 { + width: 14rem; +} + +.w-6 { + width: 1.5rem; +} + +.w-72 { + width: 18rem; +} + +.w-8 { + width: 2rem; +} + +.w-80 { + width: 20rem; +} + +.w-96 { + width: 24rem; +} + +.w-\[105px\] { + width: 105px; +} + +.w-\[10rem\] { + width: 10rem; +} + +.w-\[135px\] { + width: 135px; +} + +.w-\[150\%\] { + width: 150%; +} + +.w-\[150px\] { + width: 150px; +} + +.w-\[152px\] { + width: 152px; +} + +.w-\[15rem\] { + width: 15rem; +} + +.w-\[16px\] { + width: 16px; +} + +.w-\[178px\] { + width: 178px; +} + +.w-\[18px\] { + width: 18px; +} + +.w-\[200px\] { + width: 200px; +} + +.w-\[20px\] { + width: 20px; +} + +.w-\[23px\] { + width: 23px; +} + +.w-\[24px\] { + width: 24px; +} + +.w-\[250px\] { + width: 250px; +} + +.w-\[292px\] { + width: 292px; +} + +.w-\[30px\] { + width: 30px; +} + +.w-\[32px\] { + width: 32px; +} + +.w-\[333px\] { + width: 333px; +} + +.w-\[36px\] { + width: 36px; +} + +.w-\[382px\] { + width: 382px; +} + +.w-\[400px\] { + width: 400px; +} + +.w-\[403px\] { + width: 403px; +} + +.w-\[40px\] { + width: 40px; +} + +.w-\[410px\] { + width: 410px; +} + +.w-\[422px\] { + width: 422px; +} + +.w-\[472px\] { + width: 472px; +} + +.w-\[48px\] { + width: 48px; +} + +.w-\[510px\] { + width: 510px; +} + +.w-\[51px\] { + width: 51px; +} + +.w-\[53px\] { + width: 53px; +} + +.w-\[55px\] { + width: 55px; +} + +.w-\[56px\] { + width: 56px; +} + +.w-\[60px\] { + width: 60px; +} + +.w-\[72px\] { + width: 72px; +} + +.w-\[80\%\] { + width: 80%; +} + +.w-\[80px\] { + width: 80px; +} + +.w-\[80vw\] { + width: 80vw; +} + +.w-\[9rem\] { + width: 9rem; +} + +.w-\[unset\] { + width: unset; +} + +.w-auto { + width: auto; +} + +.w-fit { + width: -moz-fit-content; + width: fit-content; +} + +.w-full { + width: 100%; +} + +.w-screen { + width: 100vw; +} + +.min-w-\[103px\] { + min-width: 103px; +} + +.min-w-\[10rem\] { + min-width: 10rem; +} + +.min-w-\[150px\] { + min-width: 150px; +} + +.min-w-\[16rem\] { + min-width: 16rem; +} + +.min-w-\[170px\] { + min-width: 170px; +} + +.min-w-\[190px\] { + min-width: 190px; +} + +.min-w-\[200px\] { + min-width: 200px; +} + +.min-w-\[219px\] { + min-width: 219px; +} + +.min-w-\[223px\] { + min-width: 223px; +} + +.min-w-\[232px\] { + min-width: 232px; +} + +.min-w-\[550px\] { + min-width: 550px; +} + +.min-w-\[60px\] { + min-width: 60px; +} + +.min-w-\[72px\] { + min-width: 72px; +} + +.min-w-full { + min-width: 100%; +} + +.max-w-2xl { + max-width: 42rem; +} + +.max-w-3xl { + max-width: 48rem; +} + +.max-w-4xl { + max-width: 56rem; +} + +.max-w-5xl { + max-width: 64rem; +} + +.max-w-6xl { + max-width: 72rem; +} + +.max-w-7xl { + max-width: 80rem; +} + +.max-w-\[12rem\] { + max-width: 12rem; +} + +.max-w-\[135px\] { + max-width: 135px; +} + +.max-w-\[150px\] { + max-width: 150px; +} + +.max-w-\[180px\] { + max-width: 180px; +} + +.max-w-\[219px\] { + max-width: 219px; +} + +.max-w-\[250px\] { + max-width: 250px; +} + +.max-w-\[300px\] { + max-width: 300px; +} + +.max-w-\[413px\] { + max-width: 413px; +} + +.max-w-\[500px\] { + max-width: 500px; +} + +.max-w-\[510px\] { + max-width: 510px; +} + +.max-w-\[60px\] { + max-width: 60px; +} + +.max-w-\[656px\] { + max-width: 656px; +} + +.max-w-\[72px\] { + max-width: 72px; +} + +.max-w-\[80\%\] { + max-width: 80%; +} + +.max-w-\[80vw\] { + max-width: 80vw; +} + +.max-w-full { + max-width: 100%; +} + +.max-w-lg { + max-width: 32rem; +} + +.max-w-md { + max-width: 28rem; +} + +.max-w-none { + max-width: none; +} + +.max-w-screen-sm { + max-width: 640px; +} + +.max-w-sm { + max-width: 24rem; +} + +.max-w-xl { + max-width: 36rem; +} + +.max-w-xs { + max-width: 20rem; +} + +.flex-1 { + flex: 1 1 0%; +} + +.flex-auto { + flex: 1 1 auto; +} + +.shrink-0 { + flex-shrink: 0; +} + +.flex-grow { + flex-grow: 1; +} + +.origin-top { + transform-origin: top; +} + +.origin-top-left { + transform-origin: top left; +} + +.origin-top-right { + transform-origin: top right; +} + +.-translate-x-1\/2 { + --tw-translate-x: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.-translate-y-1\/2 { + --tw-translate-y: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.translate-x-0 { + --tw-translate-x: 0px; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.translate-x-7 { + --tw-translate-x: 1.75rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.translate-y-0 { + --tw-translate-y: 0px; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.translate-y-1 { + --tw-translate-y: 0.25rem; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.-rotate-45 { + --tw-rotate: -45deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.rotate-180 { + --tw-rotate: 180deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.rotate-90 { + --tw-rotate: 90deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.scale-100 { + --tw-scale-x: 1; + --tw-scale-y: 1; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.scale-95 { + --tw-scale-x: .95; + --tw-scale-y: .95; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.transform { + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.animate-spin { + animation: spin 1s linear infinite; +} + +.\!cursor-pointer { + cursor: pointer !important; +} + +.cursor-default { + cursor: default; +} + +.cursor-pointer { + cursor: pointer; +} + +.select-none { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.resize-none { + resize: none; +} + +.list-none { + list-style-type: none; +} + +.appearance-none { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.grid-cols-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.flex-row-reverse { + flex-direction: row-reverse; +} + +.flex-col { + flex-direction: column; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.flex-wrap-reverse { + flex-wrap: wrap-reverse; +} + +.items-start { + align-items: flex-start; +} + +.items-end { + align-items: flex-end; +} + +.items-center { + align-items: center; +} + +.justify-start { + justify-content: flex-start; +} + +.justify-end { + justify-content: flex-end; +} + +.justify-center { + justify-content: center; +} + +.justify-between { + justify-content: space-between; +} + +.justify-around { + justify-content: space-around; +} + +.gap-1 { + gap: 0.25rem; +} + +.gap-12 { + gap: 3rem; +} + +.gap-16 { + gap: 4rem; +} + +.gap-2 { + gap: 0.5rem; +} + +.gap-3 { + gap: 0.75rem; +} + +.gap-4 { + gap: 1rem; +} + +.gap-6 { + gap: 1.5rem; +} + +.gap-8 { + gap: 2rem; +} + +.gap-\[10px\] { + gap: 10px; +} + +.gap-\[12px\] { + gap: 12px; +} + +.gap-\[14px\] { + gap: 14px; +} + +.gap-\[15px\] { + gap: 15px; +} + +.gap-\[16px\] { + gap: 16px; +} + +.gap-\[24px\] { + gap: 24px; +} + +.gap-\[32px\] { + gap: 32px; +} + +.gap-\[8px\] { + gap: 8px; +} + +.gap-x-2 { + -moz-column-gap: 0.5rem; + column-gap: 0.5rem; +} + +.gap-x-24 { + -moz-column-gap: 6rem; + column-gap: 6rem; +} + +.gap-y-4 { + row-gap: 1rem; +} + +.gap-y-\[20px\] { + row-gap: 20px; +} + +.space-x-2> :not([hidden])~ :not([hidden]) { + --tw-space-x-reverse: 0; + margin-right: calc(0.5rem * var(--tw-space-x-reverse)); + margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); +} + +.space-y-2> :not([hidden])~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.5rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.5rem * var(--tw-space-y-reverse)); +} + +.space-y-3> :not([hidden])~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.75rem * var(--tw-space-y-reverse)); +} + +.space-y-6> :not([hidden])~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); +} + +.divide-y> :not([hidden])~ :not([hidden]) { + --tw-divide-y-reverse: 0; + border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); + border-bottom-width: calc(1px * var(--tw-divide-y-reverse)); +} + +.divide-gray-100> :not([hidden])~ :not([hidden]) { + --tw-divide-opacity: 1; + border-color: rgb(243 244 246 / var(--tw-divide-opacity)); +} + +.divide-gray-200> :not([hidden])~ :not([hidden]) { + --tw-divide-opacity: 1; + border-color: rgb(229 231 235 / var(--tw-divide-opacity)); +} + +.self-start { + align-self: flex-start; +} + +.self-end { + align-self: flex-end; +} + +.self-center { + align-self: center; +} + +.self-stretch { + align-self: stretch; +} + +.overflow-auto { + overflow: auto; +} + +.overflow-hidden { + overflow: hidden; +} + +.overflow-x-auto { + overflow-x: auto; +} + +.overflow-y-auto { + overflow-y: auto; +} + +.overflow-x-hidden { + overflow-x: hidden; +} + +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.whitespace-normal { + white-space: normal; +} + +.whitespace-nowrap { + white-space: nowrap; +} + +.whitespace-pre-line { + white-space: pre-line; +} + +.break-words { + overflow-wrap: break-word; +} + +.break-all { + word-break: break-all; +} + +.\!rounded-lg { + border-radius: 0.5rem !important; +} + +.rounded { + border-radius: 0.25rem; +} + +.rounded-2xl { + border-radius: 1rem; +} + +.rounded-3xl { + border-radius: 1.5rem; +} + +.rounded-\[3px\] { + border-radius: 3px; +} + +.rounded-\[50px\] { + border-radius: 50px; +} + +.rounded-\[6px\] { + border-radius: 6px; +} + +.rounded-circle { + border-radius: 50%; +} + +.rounded-full { + border-radius: 9999px; +} + +.rounded-lg { + border-radius: 0.5rem; +} + +.rounded-md { + border-radius: 0.375rem; +} + +.rounded-pill { + border-radius: 100vw; +} + +.rounded-sm { + border-radius: 0.125rem; +} + +.rounded-xl { + border-radius: 0.75rem; +} + +.rounded-b { + border-bottom-right-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.rounded-b-none { + border-bottom-right-radius: 0px; + border-bottom-left-radius: 0px; +} + +.rounded-l-md { + border-top-left-radius: 0.375rem; + border-bottom-left-radius: 0.375rem; +} + +.rounded-l-none { + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; +} + +.rounded-r-pill { + border-top-right-radius: 100vw; + border-bottom-right-radius: 100vw; +} + +.rounded-t { + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; +} + +.rounded-t-lg { + border-top-left-radius: 0.5rem; + border-top-right-radius: 0.5rem; +} + +.rounded-t-md { + border-top-left-radius: 0.375rem; + border-top-right-radius: 0.375rem; +} + +.rounded-bl { + border-bottom-left-radius: 0.25rem; +} + +.rounded-bl-md { + border-bottom-left-radius: 0.375rem; +} + +.rounded-bl-none { + border-bottom-left-radius: 0px; +} + +.rounded-br { + border-bottom-right-radius: 0.25rem; +} + +.rounded-br-md { + border-bottom-right-radius: 0.375rem; +} + +.rounded-tl { + border-top-left-radius: 0.25rem; +} + +.rounded-tl-none { + border-top-left-radius: 0px; +} + +.rounded-tr { + border-top-right-radius: 0.25rem; +} + +.border { + border-width: 1px; +} + +.border-0 { + border-width: 0px; +} + +.border-2 { + border-width: 2px; +} + +.border-\[1px\] { + border-width: 1px; +} + +.\!border-l-0 { + border-left-width: 0px !important; +} + +.border-b { + border-bottom-width: 1px; +} + +.border-b-0 { + border-bottom-width: 0px; +} + +.border-b-2 { + border-bottom-width: 2px; +} + +.border-l { + border-left-width: 1px; +} + +.border-l-0 { + border-left-width: 0px; +} + +.border-l-2 { + border-left-width: 2px; +} + +.border-l-8 { + border-left-width: 8px; +} + +.border-r { + border-right-width: 1px; +} + +.border-r-0 { + border-right-width: 0px; +} + +.border-r-4 { + border-right-width: 4px; +} + +.border-t { + border-top-width: 1px; +} + +.border-t-0 { + border-top-width: 0px; +} + +.border-solid { + border-style: solid; +} + +.border-dashed { + border-style: dashed; +} + +.border-none { + border-style: none; +} + +.border-\[\#\#98A2B3\] { + border-color: #98A2B3; +} + +.border-\[\#111827\] { + --tw-border-opacity: 1; + border-color: rgb(17 24 39 / var(--tw-border-opacity)); +} + +.border-\[\#33D4B7\] { + --tw-border-opacity: 1; + border-color: rgb(51 212 183 / var(--tw-border-opacity)); +} + +.border-\[\#475467\] { + --tw-border-opacity: 1; + border-color: rgb(71 84 103 / var(--tw-border-opacity)); +} + +.border-\[\#667085\] { + --tw-border-opacity: 1; + border-color: rgb(102 112 133 / var(--tw-border-opacity)); +} + +.border-\[\#98A2B3\] { + --tw-border-opacity: 1; + border-color: rgb(152 162 179 / var(--tw-border-opacity)); +} + +.border-\[\#C42945\] { + --tw-border-opacity: 1; + border-color: rgb(196 41 69 / var(--tw-border-opacity)); +} + +.border-\[\#D0D5DD\] { + --tw-border-opacity: 1; + border-color: rgb(208 213 221 / var(--tw-border-opacity)); +} + +.border-\[\#E5E5EA\] { + --tw-border-opacity: 1; + border-color: rgb(229 229 234 / var(--tw-border-opacity)); +} + +.border-\[\#EAECF0\] { + --tw-border-opacity: 1; + border-color: rgb(234 236 240 / var(--tw-border-opacity)); +} + +.border-\[\#F2F4F7\] { + --tw-border-opacity: 1; + border-color: rgb(242 244 247 / var(--tw-border-opacity)); +} + +.border-black { + --tw-border-opacity: 1; + border-color: rgb(0 0 0 / var(--tw-border-opacity)); +} + +.border-blue-200 { + --tw-border-opacity: 1; + border-color: rgb(191 219 254 / var(--tw-border-opacity)); +} + +.border-blue-500 { + --tw-border-opacity: 1; + border-color: rgb(59 130 246 / var(--tw-border-opacity)); +} + +.border-gray-100 { + --tw-border-opacity: 1; + border-color: rgb(243 244 246 / var(--tw-border-opacity)); +} + +.border-gray-200 { + --tw-border-opacity: 1; + border-color: rgb(229 231 235 / var(--tw-border-opacity)); +} + +.border-gray-300 { + --tw-border-opacity: 1; + border-color: rgb(209 213 219 / var(--tw-border-opacity)); +} + +.border-gray-500 { + --tw-border-opacity: 1; + border-color: rgb(107 114 128 / var(--tw-border-opacity)); +} + +.border-gray-700 { + --tw-border-opacity: 1; + border-color: rgb(55 65 81 / var(--tw-border-opacity)); +} + +.border-green-500 { + --tw-border-opacity: 1; + border-color: rgb(34 197 94 / var(--tw-border-opacity)); +} + +.border-green-600 { + --tw-border-opacity: 1; + border-color: rgb(22 163 74 / var(--tw-border-opacity)); +} + +.border-primary-dark { + --tw-border-opacity: 1; + border-color: rgb(13 152 149 / var(--tw-border-opacity)); +} + +.border-red-400 { + --tw-border-opacity: 1; + border-color: rgb(248 113 113 / var(--tw-border-opacity)); +} + +.border-red-500 { + --tw-border-opacity: 1; + border-color: rgb(239 68 68 / var(--tw-border-opacity)); +} + +.border-red-600 { + --tw-border-opacity: 1; + border-color: rgb(220 38 38 / var(--tw-border-opacity)); +} + +.border-slate-200 { + --tw-border-opacity: 1; + border-color: rgb(226 232 240 / var(--tw-border-opacity)); +} + +.border-transparent { + border-color: transparent; +} + +.border-white { + --tw-border-opacity: 1; + border-color: rgb(255 255 255 / var(--tw-border-opacity)); +} + +.\!bg-\[\#F2F4F7\] { + --tw-bg-opacity: 1 !important; + background-color: rgb(242 244 247 / var(--tw-bg-opacity)) !important; +} + +.bg-\[\#00000080\] { + background-color: #00000080; +} + +.bg-\[\#0ba68a\] { + --tw-bg-opacity: 1; + background-color: rgb(11 166 138 / var(--tw-bg-opacity)); +} + +.bg-\[\#13131366\] { + background-color: #13131366; +} + +.bg-\[\#15212A\] { + --tw-bg-opacity: 1; + background-color: rgb(21 33 42 / var(--tw-bg-opacity)); +} + +.bg-\[\#1D2939\] { + --tw-bg-opacity: 1; + background-color: rgb(29 41 57 / var(--tw-bg-opacity)); +} + +.bg-\[\#667085\] { + --tw-bg-opacity: 1; + background-color: rgb(102 112 133 / var(--tw-bg-opacity)); +} + +.bg-\[\#D92D20\] { + --tw-bg-opacity: 1; + background-color: rgb(217 45 32 / var(--tw-bg-opacity)); +} + +.bg-\[\#EAECF0\] { + --tw-bg-opacity: 1; + background-color: rgb(234 236 240 / var(--tw-bg-opacity)); +} + +.bg-\[\#F0F5F3\] { + --tw-bg-opacity: 1; + background-color: rgb(240 245 243 / var(--tw-bg-opacity)); +} + +.bg-\[\#F2F4F7\] { + --tw-bg-opacity: 1; + background-color: rgb(242 244 247 / var(--tw-bg-opacity)); +} + +.bg-\[\#F3F9F7\] { + --tw-bg-opacity: 1; + background-color: rgb(243 249 247 / var(--tw-bg-opacity)); +} + +.bg-\[\#F9FAF8\] { + --tw-bg-opacity: 1; + background-color: rgb(249 250 248 / var(--tw-bg-opacity)); +} + +.bg-\[\#F9FAFB\] { + --tw-bg-opacity: 1; + background-color: rgb(249 250 251 / var(--tw-bg-opacity)); +} + +.bg-amber-100 { + --tw-bg-opacity: 1; + background-color: rgb(254 243 199 / var(--tw-bg-opacity)); +} + +.bg-black { + --tw-bg-opacity: 1; + background-color: rgb(0 0 0 / var(--tw-bg-opacity)); +} + +.bg-blue-100 { + --tw-bg-opacity: 1; + background-color: rgb(219 234 254 / var(--tw-bg-opacity)); +} + +.bg-blue-500 { + --tw-bg-opacity: 1; + background-color: rgb(59 130 246 / var(--tw-bg-opacity)); +} + +.bg-blue-600 { + --tw-bg-opacity: 1; + background-color: rgb(37 99 235 / var(--tw-bg-opacity)); +} + +.bg-gray-100 { + --tw-bg-opacity: 1; + background-color: rgb(243 244 246 / var(--tw-bg-opacity)); +} + +.bg-gray-200 { + --tw-bg-opacity: 1; + background-color: rgb(229 231 235 / var(--tw-bg-opacity)); +} + +.bg-gray-300 { + --tw-bg-opacity: 1; + background-color: rgb(209 213 219 / var(--tw-bg-opacity)); +} + +.bg-gray-50 { + --tw-bg-opacity: 1; + background-color: rgb(249 250 251 / var(--tw-bg-opacity)); +} + +.bg-green-100 { + --tw-bg-opacity: 1; + background-color: rgb(220 252 231 / var(--tw-bg-opacity)); +} + +.bg-red-400 { + --tw-bg-opacity: 1; + background-color: rgb(248 113 113 / var(--tw-bg-opacity)); +} + +.bg-red-500 { + --tw-bg-opacity: 1; + background-color: rgb(239 68 68 / var(--tw-bg-opacity)); +} + +.bg-red-800 { + --tw-bg-opacity: 1; + background-color: rgb(153 27 27 / var(--tw-bg-opacity)); +} + +.bg-teal-600 { + --tw-bg-opacity: 1; + background-color: rgb(13 148 136 / var(--tw-bg-opacity)); +} + +.bg-transparent { + background-color: transparent; +} + +.bg-white { + --tw-bg-opacity: 1; + background-color: rgb(255 255 255 / var(--tw-bg-opacity)); +} + +.bg-green-600 { + --tw-bg-opacity: 1; + background-color: rgb(22 163 74 / var(--tw-bg-opacity)); +} + +.bg-opacity-25 { + --tw-bg-opacity: 0.25; +} + +.bg-opacity-50 { + --tw-bg-opacity: 0.5; +} + +.bg-opacity-60 { + --tw-bg-opacity: 0.6; +} + +.\!bg-gradient-to-r { + background-image: linear-gradient(to right, var(--tw-gradient-stops)) !important; +} + +.bg-gradient-to-r { + background-image: linear-gradient(to right, var(--tw-gradient-stops)); +} + +.from-\[\#33D4B7\] { + --tw-gradient-from: #33D4B7; + --tw-gradient-to: rgb(51 212 183 / 0); + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); +} + +.from-primary { + --tw-gradient-from: #33d4b7; + --tw-gradient-to: rgb(51 212 183 / 0); + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); +} + +.from-primary-dark { + --tw-gradient-from: #0D9895; + --tw-gradient-to: rgb(13 152 149 / 0); + --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); +} + +.to-\[\#0D9895\] { + --tw-gradient-to: #0D9895; +} + +.to-primary-dark { + --tw-gradient-to: #0D9895; +} + +.bg-cover { + background-size: cover; +} + +.bg-clip-text { + -webkit-background-clip: text; + background-clip: text; +} + +.bg-center { + background-position: center; +} + +.bg-no-repeat { + background-repeat: no-repeat; +} + +.fill-\[\#0D9895\] { + fill: #0D9895; +} + +.fill-\[\#101828\] { + fill: #101828; +} + +.fill-\[\#FEC84B\] { + fill: #FEC84B; +} + +.stroke-\[\#33D4B7\] { + stroke: #33D4B7; +} + +.stroke-\[\#667085\] { + stroke: #667085; +} + +.stroke-\[\#98A2B3\] { + stroke: #98A2B3; +} + +.stroke-\[\#FEC84B\] { + stroke: #FEC84B; +} + +.stroke-white { + stroke: #fff; +} + +.object-contain { + -o-object-fit: contain; + object-fit: contain; +} + +.object-cover { + -o-object-fit: cover; + object-fit: cover; +} + +.p-0 { + padding: 0px; +} + +.p-0\.5 { + padding: 0.125rem; +} + +.p-1 { + padding: 0.25rem; +} + +.p-1\.5 { + padding: 0.375rem; +} + +.p-2 { + padding: 0.5rem; +} + +.p-2\.5 { + padding: 0.625rem; +} + +.p-3 { + padding: 0.75rem; +} + +.p-4 { + padding: 1rem; +} + +.p-5 { + padding: 1.25rem; +} + +.p-6 { + padding: 1.5rem; +} + +.p-8 { + padding: 2rem; +} + +.p-\[10px\] { + padding: 10px; +} + +.p-\[12px\] { + padding: 12px; +} + +.p-\[16px\] { + padding: 16px; +} + +.p-\[20px\] { + padding: 20px; +} + +.px-1 { + padding-left: 0.25rem; + padding-right: 0.25rem; +} + +.px-10 { + padding-left: 2.5rem; + padding-right: 2.5rem; +} + +.px-12 { + padding-left: 3rem; + padding-right: 3rem; +} + +.px-16 { + padding-left: 4rem; + padding-right: 4rem; +} + +.px-2 { + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +.px-3 { + padding-left: 0.75rem; + padding-right: 0.75rem; +} + +.px-4 { + padding-left: 1rem; + padding-right: 1rem; +} + +.px-5 { + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +.px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; +} + +.px-8 { + padding-left: 2rem; + padding-right: 2rem; +} + +.px-\[10px\] { + padding-left: 10px; + padding-right: 10px; +} + +.px-\[12px\] { + padding-left: 12px; + padding-right: 12px; +} + +.px-\[14px\] { + padding-left: 14px; + padding-right: 14px; +} + +.px-\[15px\] { + padding-left: 15px; + padding-right: 15px; +} + +.px-\[16px\] { + padding-left: 16px; + padding-right: 16px; +} + +.px-\[17px\] { + padding-left: 17px; + padding-right: 17px; +} + +.px-\[18px\] { + padding-left: 18px; + padding-right: 18px; +} + +.px-\[20px\] { + padding-left: 20px; + padding-right: 20px; +} + +.px-\[21px\] { + padding-left: 21px; + padding-right: 21px; +} + +.px-\[24px\] { + padding-left: 24px; + padding-right: 24px; +} + +.px-\[66px\] { + padding-left: 66px; + padding-right: 66px; +} + +.px-\[8px\] { + padding-left: 8px; + padding-right: 8px; +} + +/* .px-\[\] { + padding-left: ; + padding-right: ; +} */ + +.py-1 { + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + +.py-10 { + padding-top: 2.5rem; + padding-bottom: 2.5rem; +} + +.py-12 { + padding-top: 3rem; + padding-bottom: 3rem; +} + +.py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.py-2\.5 { + padding-top: 0.625rem; + padding-bottom: 0.625rem; +} + +.py-3 { + padding-top: 0.75rem; + padding-bottom: 0.75rem; +} + +.py-4 { + padding-top: 1rem; + padding-bottom: 1rem; +} + +.py-5 { + padding-top: 1.25rem; + padding-bottom: 1.25rem; +} + +.py-6 { + padding-top: 1.5rem; + padding-bottom: 1.5rem; +} + +.py-7 { + padding-top: 1.75rem; + padding-bottom: 1.75rem; +} + +.py-8 { + padding-top: 2rem; + padding-bottom: 2rem; +} + +.py-\[10px\] { + padding-top: 10px; + padding-bottom: 10px; +} + +.py-\[12px\] { + padding-top: 12px; + padding-bottom: 12px; +} + +.py-\[13px\] { + padding-top: 13px; + padding-bottom: 13px; +} + +.py-\[16px\] { + padding-top: 16px; + padding-bottom: 16px; +} + +.py-\[19px\] { + padding-top: 19px; + padding-bottom: 19px; +} + +.py-\[24px\] { + padding-top: 24px; + padding-bottom: 24px; +} + +.py-\[26px\] { + padding-top: 26px; + padding-bottom: 26px; +} + +.py-\[2px\] { + padding-top: 2px; + padding-bottom: 2px; +} + +.py-\[32px\] { + padding-top: 32px; + padding-bottom: 32px; +} + +.py-\[5px\] { + padding-top: 5px; + padding-bottom: 5px; +} + +.py-\[64px\] { + padding-top: 64px; + padding-bottom: 64px; +} + +.py-\[6px\] { + padding-top: 6px; + padding-bottom: 6px; +} + +.py-\[7px\] { + padding-top: 7px; + padding-bottom: 7px; +} + +.py-\[8px\] { + padding-top: 8px; + padding-bottom: 8px; +} + +.pb-0 { + padding-bottom: 0px; +} + +.pb-10 { + padding-bottom: 2.5rem; +} + +.pb-12 { + padding-bottom: 3rem; +} + +.pb-16 { + padding-bottom: 4rem; +} + +.pb-2 { + padding-bottom: 0.5rem; +} + +.pb-20 { + padding-bottom: 5rem; +} + +.pb-3 { + padding-bottom: 0.75rem; +} + +.pb-4 { + padding-bottom: 1rem; +} + +.pb-40 { + padding-bottom: 10rem; +} + +.pb-6 { + padding-bottom: 1.5rem; +} + +.pb-8 { + padding-bottom: 2rem; +} + +.pb-\[100px\] { + padding-bottom: 100px; +} + +.pb-\[10px\] { + padding-bottom: 10px; +} + +.pb-\[12px\] { + padding-bottom: 12px; +} + +.pb-\[13px\] { + padding-bottom: 13px; +} + +.pb-\[140px\] { + padding-bottom: 140px; +} + +.pb-\[3px\] { + padding-bottom: 3px; +} + +.pb-\[40px\] { + padding-bottom: 40px; +} + +.pb-\[64px\] { + padding-bottom: 64px; +} + +.pb-\[7px\] { + padding-bottom: 7px; +} + +.pb-\[80px\] { + padding-bottom: 80px; +} + +.pl-1 { + padding-left: 0.25rem; +} + +.pl-10 { + padding-left: 2.5rem; +} + +.pl-2 { + padding-left: 0.5rem; +} + +.pl-3 { + padding-left: 0.75rem; +} + +.pl-4 { + padding-left: 1rem; +} + +.pl-6 { + padding-left: 1.5rem; +} + +.pr-0 { + padding-right: 0px; +} + +.pr-1 { + padding-right: 0.25rem; +} + +.pr-16 { + padding-right: 4rem; +} + +.pr-2 { + padding-right: 0.5rem; +} + +.pr-3 { + padding-right: 0.75rem; +} + +.pr-4 { + padding-right: 1rem; +} + +.pr-5 { + padding-right: 1.25rem; +} + +.pr-6 { + padding-right: 1.5rem; +} + +.pr-\[12px\] { + padding-right: 12px; +} + +.pr-\[13px\] { + padding-right: 13px; +} + +.pt-12 { + padding-top: 3rem; +} + +.pt-2 { + padding-top: 0.5rem; +} + +.pt-24 { + padding-top: 6rem; +} + +.pt-4 { + padding-top: 1rem; +} + +.pt-40 { + padding-top: 10rem; +} + +.pt-5 { + padding-top: 1.25rem; +} + +.pt-6 { + padding-top: 1.5rem; +} + +.pt-\[100px\] { + padding-top: 100px; +} + +.pt-\[10px\] { + padding-top: 10px; +} + +.pt-\[120px\] { + padding-top: 120px; +} + +.pt-\[12px\] { + padding-top: 12px; +} + +.pt-\[140px\] { + padding-top: 140px; +} + +.pt-\[170px\] { + padding-top: 170px; +} + +.pt-\[20px\] { + padding-top: 20px; +} + +.pt-\[2px\] { + padding-top: 2px; +} + +.pt-\[30px\] { + padding-top: 30px; +} + +.pt-\[40px\] { + padding-top: 40px; +} + +.pt-\[44px\] { + padding-top: 44px; +} + +.pt-\[70px\] { + padding-top: 70px; +} + +.text-left { + text-align: left; +} + +.text-center { + text-align: center; +} + +.text-right { + text-align: right; +} + +.text-end { + text-align: end; +} + +.align-baseline { + vertical-align: baseline; +} + +.align-middle { + vertical-align: middle; +} + +.text-2xl { + font-size: 1.25rem; +} + +.text-3xl { + font-size: 1.5rem; +} + +.text-4xl { + font-size: 1.953rem; +} + +.text-5xl { + font-size: 2.441rem; +} + +.text-7xl { + font-size: 4rem; +} + +.text-\[14px\] { + font-size: 14px; +} + +.text-\[16px\] { + font-size: 16px; +} + +.text-\[18px\] { + font-size: 18px; +} + +.text-\[30px\] { + font-size: 30px; +} + +.text-base { + font-size: 0.9rem; +} + +.text-sm { + font-size: 0.875rem; +} + +.text-xl { + font-size: 1rem; +} + +.text-xs { + font-size: 0.75rem; +} + +.text-xxs { + font-size: 0.5rem; +} + +.font-bold { + font-weight: 700; +} + +.font-light { + font-weight: 300; +} + +.font-medium { + font-weight: 500; +} + +.font-normal { + font-weight: 400; +} + +.font-semibold { + font-weight: 600; +} + +.font-thin { + font-weight: 100; +} + +.uppercase { + text-transform: uppercase; +} + +.lowercase { + text-transform: lowercase; +} + +.capitalize { + text-transform: capitalize; +} + +.normal-case { + text-transform: none; +} + +.italic { + font-style: italic; +} + +.leading-5 { + line-height: 1.25rem; +} + +.leading-6 { + line-height: 1.5rem; +} + +.leading-\[1\.7\] { + line-height: 1.7; +} + +.leading-none { + line-height: 1; +} + +.leading-normal { + line-height: 1.5; +} + +.leading-relaxed { + line-height: 1.625; +} + +.leading-tight { + line-height: 1.25; +} + +.tracking-wide { + letter-spacing: 0.025em; +} + +.tracking-wider { + letter-spacing: 0.05em; +} + +.text-\[\#101828\] { + --tw-text-opacity: 1; + color: rgb(16 24 40 / var(--tw-text-opacity)); +} + +.text-\[\#111827\] { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + +.text-\[\#1570EF\] { + --tw-text-opacity: 1; + color: rgb(21 112 239 / var(--tw-text-opacity)); +} + +.text-\[\#33D4B7\] { + --tw-text-opacity: 1; + color: rgb(51 212 183 / var(--tw-text-opacity)); +} + +.text-\[\#344054\] { + --tw-text-opacity: 1; + color: rgb(52 64 84 / var(--tw-text-opacity)); +} + +.text-\[\#475467\] { + --tw-text-opacity: 1; + color: rgb(71 84 103 / var(--tw-text-opacity)); +} + +.text-\[\#667085\] { + --tw-text-opacity: 1; + color: rgb(102 112 133 / var(--tw-text-opacity)); +} + +.text-\[\#8E8E93\] { + --tw-text-opacity: 1; + color: rgb(142 142 147 / var(--tw-text-opacity)); +} + +.text-\[\#98A2B3\] { + --tw-text-opacity: 1; + color: rgb(152 162 179 / var(--tw-text-opacity)); +} + +.text-\[\#C42945\] { + --tw-text-opacity: 1; + color: rgb(196 41 69 / var(--tw-text-opacity)); +} + +.text-\[\#D92D20\] { + --tw-text-opacity: 1; + color: rgb(217 45 32 / var(--tw-text-opacity)); +} + +.text-\[\#DC6803\] { + --tw-text-opacity: 1; + color: rgb(220 104 3 / var(--tw-text-opacity)); +} + +/* .text-\[\] { + color: ; +} */ + +.text-amber-600 { + --tw-text-opacity: 1; + color: rgb(217 119 6 / var(--tw-text-opacity)); +} + +.text-amber-900 { + --tw-text-opacity: 1; + color: rgb(120 53 15 / var(--tw-text-opacity)); +} + +.text-black { + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity)); +} + +.text-blue-500 { + --tw-text-opacity: 1; + color: rgb(59 130 246 / var(--tw-text-opacity)); +} + +.text-blue-900 { + --tw-text-opacity: 1; + color: rgb(30 58 138 / var(--tw-text-opacity)); +} + +.text-gray-300 { + --tw-text-opacity: 1; + color: rgb(209 213 219 / var(--tw-text-opacity)); +} + +.text-gray-400 { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); +} + +.text-gray-500 { + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity)); +} + +.text-gray-600 { + --tw-text-opacity: 1; + color: rgb(75 85 99 / var(--tw-text-opacity)); +} + +.text-gray-700 { + --tw-text-opacity: 1; + color: rgb(55 65 81 / var(--tw-text-opacity)); +} + +.text-gray-800 { + --tw-text-opacity: 1; + color: rgb(31 41 55 / var(--tw-text-opacity)); +} + +.text-gray-900 { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + +.text-green-600 { + --tw-text-opacity: 1; + color: rgb(22 163 74 / var(--tw-text-opacity)); +} + +.text-green-800 { + --tw-text-opacity: 1; + color: rgb(22 101 52 / var(--tw-text-opacity)); +} + +.text-primary { + --tw-text-opacity: 1; + color: rgb(51 212 183 / var(--tw-text-opacity)); +} + +.text-red-500 { + --tw-text-opacity: 1; + color: rgb(239 68 68 / var(--tw-text-opacity)); +} + +.text-red-600 { + --tw-text-opacity: 1; + color: rgb(220 38 38 / var(--tw-text-opacity)); +} + +.text-red-700 { + --tw-text-opacity: 1; + color: rgb(185 28 28 / var(--tw-text-opacity)); +} + +.text-slate-500 { + --tw-text-opacity: 1; + color: rgb(100 116 139 / var(--tw-text-opacity)); +} + +.text-transparent { + color: transparent; +} + +.text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.text-green-700 { + --tw-text-opacity: 1; + color: rgb(21 128 61 / var(--tw-text-opacity)); +} + +.text-opacity-90 { + --tw-text-opacity: 0.9; +} + +.underline { + -webkit-text-decoration-line: underline; + text-decoration-line: underline; +} + +.opacity-0 { + opacity: 0; +} + +.opacity-100 { + opacity: 1; +} + +.opacity-25 { + opacity: 0.25; +} + +.opacity-40 { + opacity: 0.4; +} + +.opacity-50 { + opacity: 0.5; +} + +.opacity-75 { + opacity: 0.75; +} + +.shadow { + --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-lg { + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-md { + --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-sm { + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.shadow-xl { + --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.outline-none { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.ring-0 { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(0px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.ring-1 { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.ring-2 { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.ring-\[\#0d9895\] { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(13 152 149 / var(--tw-ring-opacity)); +} + +.ring-black { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(0 0 0 / var(--tw-ring-opacity)); +} + +.ring-blue-600 { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(37 99 235 / var(--tw-ring-opacity)); +} + +.ring-indigo-600 { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(79 70 229 / var(--tw-ring-opacity)); +} + +.ring-primary { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(51 212 183 / var(--tw-ring-opacity)); +} + +.ring-primary-dark { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(13 152 149 / var(--tw-ring-opacity)); +} + +.ring-red-500 { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(239 68 68 / var(--tw-ring-opacity)); +} + +.ring-transparent { + --tw-ring-color: transparent; +} + +.ring-opacity-5 { + --tw-ring-opacity: 0.05; +} + +.ring-offset-2 { + --tw-ring-offset-width: 2px; +} + +.filter { + filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); +} + +.backdrop-blur-sm { + --tw-backdrop-blur: blur(4px); + -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); +} + +.backdrop-filter { + -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); +} + +.transition { + transition-property: color, background-color, border-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-text-decoration-color, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-text-decoration-color, -webkit-backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-all { + transition-property: all; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.transition-colors { + transition-property: color, background-color, border-color, fill, stroke, -webkit-text-decoration-color; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, -webkit-text-decoration-color; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.duration-100 { + transition-duration: 100ms; +} + +.duration-150 { + transition-duration: 150ms; +} + +.duration-200 { + transition-duration: 200ms; +} + +.duration-300 { + transition-duration: 300ms; +} + +.duration-500 { + transition-duration: 500ms; +} + +.duration-75 { + transition-duration: 75ms; +} + +.ease-in { + transition-timing-function: cubic-bezier(0.4, 0, 1, 1); +} + +.ease-in-out { + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} + +.ease-out { + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); +} + +@media screen and (max-width: 767px) { + .sidebar-holder { + width: 100%; + min-width: 200px; + max-width: 200px; + position: fixed; + top: 0; + left: 0; + } + + .page-header span { + margin-left: auto; + } +} + +/* FRONTEND STYLES */ + +.customer-section { + /* font-family: General Sans sans-serif !important; */ +} + +.my-text-gradient { + background: -webkit-linear-gradient(230.69deg, #33d4b7 9.11%, #0d9895 69.45%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.hr { + position: relative; + color: #667085; + font-weight: 400; + font-size: 14px; + line-height: 20px; +} + +.hr::before, +.hr::after { + content: ""; + position: absolute; + height: 1.5px; + background-color: #eee; + width: 170px; + top: 50%; +} + +.hr::after { + right: 140%; +} + +.hr::before { + left: 140%; +} + +.customer-section { + width: 100% !important; +} + +.google-btn { + border: 2px solid #eee !important; + box-shadow: none !important; + color: black !important; +} + +.bg-login { + background-color: #f0f5f3; +} + +.login-btn-gradient { + background: linear-gradient(230.69deg, #33d4b7 9.11%, #0d9895 69.45%); +} + +.login-btn-gradient.loading { + pointer-events: none; +} + +.login-btn-gradient.loading::before { + position: absolute; + content: ""; + inset: 0; + background-color: rgba(225, 225, 225, 0.3); +} + +.login-btn-gradient:disabled { + background: #d0d5dd; +} + +.my-background-image { + background-size: cover !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-blend-mode: multiply !important; +} + +.fadeIn { + animation: fadeIn 800ms ease-in; +} + +.fadeOut { + animation: fadeOut 800ms ease-in; +} + +@keyframes fadeIn { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +@keyframes fadeOut { + 0% { + opacity: 1; + } + + 100% { + opacity: 0; + } +} + +.property-grid { + display: grid; + grid-template-columns: repeat(2, 250px); + -moz-column-gap: 21px; + column-gap: 21px; + row-gap: 40px; + padding-left: 1rem; + max-width: 100%; +} + +.property-space-card .aspect { + aspect-ratio: 292/227 !important; + min-height: 194px; +} + +.browse-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + -moz-column-gap: 19px; + column-gap: 19px; + row-gap: 20px; + padding-inline: 1rem; +} + +.react-tooltip { + text-transform: none !important; + background: black !important; + border-radius: 0.6em !important; + font-size: 11px !important; + padding: 4px 9px !important; +} + +.snap-scroll { + /* scroll-snap-type: inline mandatory; */ + overflow-x: auto; + overscroll-behavior-inline: contain; +} + +.snap-scroll::-webkit-scrollbar { + height: 0px !important; +} + +.snap-scroll button { + scroll-snap-align: start; +} + +/* custom checkbox */ + +.checkbox-containerr { + display: flex; + align-items: center; +} + +.checkbox-containerr label { + cursor: pointer; + display: flex; + align-items: center; +} + +.checkbox-containerr input[type="checkbox"] { + cursor: pointer; + opacity: 0; + position: absolute; +} + +.checkbox-containerr label::before { + content: ""; + width: 20px; + height: 20px; + border-radius: 4px; + margin-right: 10.5px; + color: #fff; + -webkit-appearance-color: #fff; + -moz-appearance-color: #fff; + -o-appearance-color: #fff; + border: 1px solid #0D9895; +} + +.checkbox-containerr input[type="checkbox"]:disabled+label, +.checkbox-containerr input[type="checkbox"]:disabled { + color: #aaa; + cursor: default; +} + +.checkbox-containerr input[type="checkbox"]:checked+label::before { + content: "\002714"; + background-color: #0D9895; + display: flex; + justify-content: center; + align-items: center; + color: #fff; + -webkit-appearance-color: #fff; + -moz-appearance-color: #fff; + -o-appearance-color: #fff; + +} + +.checkbox-containerr input[type="checkbox"]:disabled+label::before { + background-color: #ccc; + border-color: #999; +} + +/* Safari 11+ */ +@media not all and (min-resolution:.001dpcm) { + @supports (-webkit-appearance:none) and (stroke-color:transparent) { + + .checkbox-containerr input[type="checkbox"]:checked+label::before, + .checkbox-containerr label::before { + color: #fff; + } + } +} + +/* Test website on real Safari 11+ */ + +/* Safari 10.1 */ +@media not all and (min-resolution:.001dpcm) { + @supports (-webkit-appearance:none) and (not (stroke-color:transparent)) { + + .checkbox-containerr input[type="checkbox"]:checked+label::before, + .checkbox-containerr label::before { + color: #fff; + } + } +} + +/* Safari 6.1-10.0 (but not 10.1) */ +@media screen and (min-color-index:0) and(-webkit-min-device-pixel-ratio:0) { + + .checkbox-containerr input[type="checkbox"]:checked+label::before, + .checkbox-containerr label::before { + color: #fff; + } +} + +/* custom checkbox */ + +.checkbox-container { + display: flex; + align-items: center; +} + +.checkbox-container label { + cursor: pointer; + display: flex; + align-items: center; +} + +.checkbox-container input[type="checkbox"] { + cursor: pointer; + opacity: 0; + position: absolute; +} + +.checkbox-container label::before { + content: ""; + width: 20px; + height: 20px; + border-radius: 4px; + margin-right: 10.5px; + border: 1px solid #0D9895; +} + +.checkbox-container input[type="checkbox"]:disabled+label, +.checkbox-container input[type="checkbox"]:disabled { + color: #aaa; + cursor: default; +} +.checkbox-container input[type="radio"]:disabled+label, +.checkbox-container input[type="checkbox"]:disabled { + color: #aaa; + cursor: default; +} + +.checkbox-container input[type="checkbox"]:checked+label::before { + content: ""; + background-color: #0D9895; + background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0id2hpdGUiIHdpZHRoPSIyNHB4IiBoZWlnaHQ9IjI0cHgiPjxwYXRoIGQ9Ik0wIDBoMjR2MjRIMFYweiIgZmlsbD0ibm9uZSIvPjxwYXRoIGQ9Ik05IDE2LjJsLTMuNS0zLjUgMS40LTEuNEw5IDEzLjRsNy4xLTcuMSAxLjQgMS40eiIvPjwvc3ZnPg=='); + background-size: contain; + background-repeat: no-repeat; + vertical-align: middle; + display: flex; + justify-content: center; + align-items: center; + color: white; + -webkit-text-fill-color: #fff !important; + color: #fff !important; +} +.checkbox-container input[type="radio"]:checked+label::before { + content: "\002714"; + background-color: #0D9895; + display: flex; + justify-content: center; + align-items: center; + color: white; + -webkit-text-fill-color: #fff !important; + color: #fff !important; +} + +.checkbox-container input[type="checkbox"]:disabled+label::before { + background-color: #ccc; + border-color: #999; +} + +.remove-arrow::-webkit-outer-spin-button, +.remove-arrow::-webkit-inner-spin-button { + appearance: unset; + -webkit-appearance: none; + margin: 0; +} + +/* Firefox */ + +.remove-arrow[type="number"] { + -webkit-appearance: unset; + appearance: unset; + -moz-appearance: textfield; +} + +.date-placeholder::-moz-placeholder { + background-image: url("/date-placeholder.png"); + background-repeat: no-repeat; + background-position: left; + background-size: contain; +} + +.date-placeholder::placeholder { + background-image: url("/date-placeholder.png"); + background-repeat: no-repeat; + background-position: left; + background-size: contain; +} + +.date-placeholder, +.people-placeholder { + text-indent: 26px; +} + +.people-placeholder::-moz-placeholder { + background-image: url("/people-placeholder.png"); + background-repeat: no-repeat; + background-position: left; + background-size: contain; +} + +.people-placeholder::placeholder { + background-image: url("/people-placeholder.png"); + background-repeat: no-repeat; + background-position: left; + background-size: contain; +} + +.addons-grid { + display: grid; + /* grid-template-columns: repeat(1, 200px); */ + -moz-column-gap: 50px; + column-gap: 50px; + row-gap: 12px; +} + +.amenities-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + row-gap: 8px; +} + +.list-disk-important { + list-style-type: disc !important; + list-style-position: inside; +} + +.popup-container { + position: fixed; + background-color: rgba(0, 0, 0, 0.6); + top: 0; + right: 0; + left: 0; + height: 100vh; + z-index: 54; +} + +.review-scroll::-webkit-scrollbar { + width: 10px; +} + +.custom-calendar-::-webkit-scrollbar { + width: 5px; +} + +/* Track */ + +.review-scroll::-webkit-scrollbar-track { + background: #fff; +} + +/* Handle */ + +.review-scroll::-webkit-scrollbar-thumb { + background: #eaecf0; + border-radius: 10px; +} + +/* Handle on hover */ + +.review-scroll::-webkit-scrollbar-thumb:hover { + background: #d0d5dd; +} + +.tiny-scroll::-webkit-scrollbar { + width: 5px; +} + +/* Track */ + +.tiny-scroll::-webkit-scrollbar-track { + background: #fff; +} + +/* Handle */ + +.tiny-scroll::-webkit-scrollbar-thumb { + background: #eaecf0; + border-radius: 10px; +} + +/* Handle on hover */ + +.tiny-scroll::-webkit-scrollbar-thumb:hover { + background: #d0d5dd; +} + +.property-swiper-slide { + height: 100%; +} + +.property-swiper-slide .swiper-pagination { + display: none; +} + +.property-swiper-slide .swiper-button-next, +.property-swiper-slide .swiper-button-prev { + display: none; +} + +.remove-select { + -webkit-user-select: none; + -moz-user-select: none; + -o-user-select: none; + user-select: none; +} + +.property-swiper-image { + -o-object-fit: cover; + object-fit: cover; + -webkit-user-select: none; + -moz-user-select: none; + -o-user-select: none; + user-select: none; +} + +.swiper-pagination { + text-align: unset !important; +} + +.swiper-button-next, +.swiper-button-prev { + top: 50% !important; + background-color: #908d8c; + width: 56px !important; + height: 56px !important; + border-radius: 50% !important; + color: white !important; +} + +.swiper-button-next::after, +.swiper-button-prev::after { + font-size: 20px !important; + font-weight: 700 !important; +} + +.swiper-button-next::before, +.swiper-button-prev::before { + content: " "; + position: absolute; + background-color: transparent; + border: 2px solid white; + width: 33px; + height: 33px; + border-radius: 50%; +} + +.pagination-image { + height: 75px !important; + width: 120px !important; + display: none !important; + border-radius: 4px !important; + margin-right: 24px !important; + opacity: 1 !important; + -webkit-user-select: none; + -moz-user-select: none; + -o-user-select: none; + user-select: none; +} + +.pagination-image.swiper-pagination-bullet-active { + box-shadow: 0 0 0 5px #0d9895; +} + +.my-shadow2 { + box-shadow: 0px 8px 24px rgb(29 17 96 / 15%); +} + +.my-shadow { + box-shadow: 0px 8px 24px rgb(29 17 96 / 15%); + border-radius: 10px; + overflow: hidden; +} + +.property-swiper-slide .swiper-pagination-horizontal { + max-width: 100%; + overflow-x: auto !important; + white-space: nowrap !important; + overflow-y: visible; + text-align: start !important; + padding: 1rem 0; +} + +.full-circle { + border-radius: 50%; + aspect-ratio: 1 /1; +} + +.property-swiper-slide .swiper-pagination-horizontal::-webkit-scrollbar { + width: 5px !important; +} + +/* Track */ + +.property-swiper-slide .swiper-pagination-horizontal::-webkit-scrollbar-track { + background: #fff; +} + +/* Handle */ + +.property-swiper-slide .swiper-pagination-horizontal::-webkit-scrollbar-thumb { + background: #eaecf0; + border-radius: 10px; +} + +/* Handle on hover */ + +.property-swiper-slide .swiper-pagination-horizontal::-webkit-scrollbar-thumb:hover { + background: #d0d5dd; +} + +.sticky-price-summary { + box-shadow: 0px -4px 8px -2px rgba(16, 24, 40, 0.05), 0px -4px 4px -2px rgba(16, 24, 40, 0.04); +} + +.static-search-bar { + box-shadow: 0px 8px 24px rgb(29 17 96 / 15%); + background-color: white; + /* padding: 8px; */ + border-radius: 100vw; +} + +.animated-nav { + position: relative; + display: flex; +} + +.animated-nav a { + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 24px; + letter-spacing: 0.02em; + color: #475467; + width: 105px; + transition: 300ms; + text-align: center; +} + +.animated-nav a.active { + position: relative; + color: #101828; +} + +.bottom-nav a.active { + font-weight: 600; +} + +.animated-nav .slide { + width: 105px; + border-radius: 2px; + height: 2px; + position: absolute; + left: 0; + top: 100%; + transition: 300ms; + opacity: 0; + background-color: #101828; + /* z-index: -1; */ + pointer-events: none; +} + +.animated-nav .slide.white { + background-color: white !important; +} + +.animated-nav a:nth-child(1).active~.slide, +.animated-nav button:nth-child(1).active~.slide { + left: 0; + opacity: 1; +} + +.animated-nav a:nth-child(2).active~.slide, +.animated-nav button:nth-child(2).active~.slide { + left: 105px; + opacity: 1; +} + +.animated-nav a:nth-child(3).active~.slide, +.animated-nav button:nth-child(3).active~.slide { + left: calc(2 * 105px); + opacity: 1; +} + +.animated-nav a:nth-child(4).active~.slide, +.animated-nav button:nth-child(4).active~.slide { + left: calc(3 * 105px); + opacity: 1; +} + +.animated-nav a:nth-child(5).active~.slide, +.animated-nav button:nth-child(5).active~.slide { + left: calc(4 * 105px); + opacity: 1; +} + +.animated-nav a:nth-child(6).active~.slide, +.animated-nav button:nth-child(6).active~.slide { + left: calc(5 * 105px); + opacity: 1; +} + +.animated-nav a:nth-child(7).active~.slide, +.animated-nav button:nth-child(7).active~.slide { + left: calc(6 * 105px); + opacity: 1; +} + +.animated-nav a:nth-child(8).active~.slide, +.animated-nav button:nth-child(8).active~.slide { + left: calc(7 * 105px); + opacity: 1; +} + +.animated-nav a:nth-child(9).active~.slide, +.animated-nav button:nth-child(9).active~.slide { + left: calc(8 * 105px); + opacity: 1; +} + +/* ACCOUNT PAGES HEADER */ + +.account-header { + position: relative; + display: flex; +} + +.account-header a { + font-style: normal; + font-weight: 500; + font-size: 16px; + line-height: 24px; + letter-spacing: 0.02em; + color: #475467; + width: 120px; + transition: 300ms; + text-align: center; + white-space: nowrap; +} + +.account-header a.active { + position: relative; + color: #101828; +} + +.account-header .slide { + width: 120px; + border-radius: 2px; + height: 2px; + position: absolute; + left: 0; + top: 100%; + transition: 300ms; + opacity: 0; + background-color: #101828; + /* z-index: -1; */ + pointer-events: none; +} + +.account-header .slide.white { + background-color: white !important; +} + +.account-header a:nth-child(1).active~.slide, +.account-header button:nth-child(1).active~.slide { + left: 0; + opacity: 1; +} + +.account-header a:nth-child(2).active~.slide, +.account-header button:nth-child(2).active~.slide { + left: 120px; + opacity: 1; +} + +.account-header a:nth-child(3).active~.slide, +.account-header button:nth-child(3).active~.slide { + left: calc(2 * 120px); + opacity: 1; +} + +.account-header a:nth-child(4).active~.slide, +.account-header button:nth-child(4).active~.slide { + left: calc(3 * 120px); + opacity: 1; +} + +.account-header a:nth-child(5).active~.slide, +.account-header button:nth-child(5).active~.slide { + left: calc(4 * 120px); + opacity: 1; +} + +.account-header a:nth-child(6).active~.slide, +.account-header button:nth-child(6).active~.slide { + left: calc(5 * 120px); + opacity: 1; +} + +.account-header a:nth-child(7).active~.slide, +.account-header button:nth-child(7).active~.slide { + left: calc(6 * 120px); + opacity: 1; +} + +.account-header a:nth-child(8).active~.slide, +.account-header button:nth-child(8).active~.slide { + left: calc(7 * 120px); + opacity: 1; +} + +.account-header a:nth-child(9).active~.slide, +.account-header button:nth-child(9).active~.slide { + left: calc(8 * 120px); + opacity: 1; +} + +.pop-in { + animation: pop-in 300ms; +} + +.pop-out { + animation: pop-out 500ms; +} + +.gallery-in { + animation: gallery-in 400ms ease-in-out; +} + +.gallery-out { + animation: gallery-out 200ms reverse; +} + +@keyframes gallery-in { + 0% { + opacity: 0; + transform: translateY(0px); + } + + 50% { + opacity: 0.2; + } + + 100% { + opacity: 1; + transform: translateY(300px); + } + + 0% { + opacity: 0; + transform: translateY(300px); + } + + 50% { + opacity: 0.2; + } + + 100% { + opacity: 1; + transform: translateY(0px); + } +} + +@keyframes pop-in { + 0% { + opacity: 0; + transform: translateY(-100px); + } + + 100% { + opacity: 1; + transform: translateY(0px); + } +} + +@keyframes pop-out { + 0% { + opacity: 1; + transform: translateY(0px); + } + + 100% { + opacity: 0; + transform: translateY(-5px); + } +} + +.select-rating-container input { + display: none; +} + +.select-rating-container label { + cursor: pointer; +} + +.radio-container input { + display: none; +} + +.radio-container label { + display: flex; + align-items: center; + gap: 0.6rem; +} + +.radio-container span { + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid #1d2939; + position: relative; +} + +.radio-container span::before { + content: ""; + position: absolute; + inset: 10px; + background-color: #1d2939; + border-radius: 50%; + top: 50%; + left: 50%; + transition: 300ms; +} + +.radio-container input[type="radio"]:checked+span::before { + inset: 2px; +} + +.bg-my-gradient { + background: linear-gradient(230.69deg, #33d4b7 9.11%, #0d9895 69.45%); +} + +.chat-active { + position: relative; +} + +.chat-active::before { + position: absolute; + content: ""; + width: 4px; + top: 0; + bottom: 0; + left: 0; + background: linear-gradient(230.69deg, #33d4b7 9.11%, #0d9895 69.45%); +} + +.scale-in-ver-center { + animation: scale-in-ver-center 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94) both; +} + +/* .image-not-approved { + border: 2px solid red; + position: relative; +} + +.image-not-approved::before { + content: "-"; + position: absolute; + top: 10px; + right: 10px; + width: 100px; + height: 100px; + background-color: red; + z-index: 23; + border: 2px solid limegreen; +} */ + +@keyframes scale-in-ver-center { + 0% { + transform: scaleY(0); + opacity: 1; + } + + 100% { + transform: scaleY(1); + opacity: 1; + } +} + +.header-transparent { + color: white; + background-color: transparent; +} + +.header-light { + background-color: black; + color: white; +} + +.header-white { + color: black; + background-color: white; +} + +.my-border-white { + border-color: #00261c; +} + +.my-border-transparent { + border-color: white; +} + +.my-border-light { + /* border-color: #00261c; */ + border-color: white; +} + +.my-stroke-white { + stroke: #00261c; +} + +.my-stroke-transparent { + stroke: white; +} + +.my-stroke-light { + /* stroke: #00261c; */ + stroke: white; +} + +.template-grid { + display: grid; + grid-template-columns: 340px 170px 170px; +} + +.react-calendar { + font-family: "General Sans" sans-serif !important; + color: #344054; + border: 1px solid #eaecf0 !important; + width: 300px; +} + +.react-calendar abbr:where([title]) { + text-decoration: none; + text-transform: none; +} + +.react-calendar__tile { + height: 40px; + font-size: 14px !important; +} + +.react-calendar__tile:enabled:hover { + background: white; +} + +.react-calendar__month-view__days__day--weekend { + color: #344054 !important; +} + +.react-calendar__tile--now { + background: none !important; + position: relative; +} + +.react-calendar__tile--now::after { + position: absolute; + bottom: 5px; + content: ""; + background-color: #00261c; + width: 5px; + height: 5px; + left: calc(50% - 2.5px); + border-radius: 50%; +} + +.react-calendar__tile--active { + background: linear-gradient(230.69deg, #33d4b7 9.11%, #0d9895 69.45%) !important; + border-radius: 50%; + color: white !important; +} + +.react-calendar__tile--active.react-calendar__tile--now::after { + background-color: white; +} + +.react-calendar__tile:enabled:hover.react-calendar__tile--active:hover { + background: linear-gradient(230.69deg, #33d4b7 9.11%, #0d9895 69.45%) !important; +} + +.react-calendar__navigation button:enabled:hover, +.react-calendar__navigation button:enabled:focus { + background: none !important; +} + +.react-calendar__month-view__days__day--neighboringMonth { + color: #98a2b3 !important; +} + +.nav-dropdown-btn { + position: relative; +} + +.nav-dropdown-content { + display: none; + position: absolute; + top: 100%; + left: -150px; + color: #00261c; + width: 180px; + padding-top: 10px; +} + +.nav-dropdown-content.mobile { + left: -180px; + top: 150%; + display: block; +} + +.nav-dropdown-content>div { + background-color: white; + border-radius: 15px; + overflow: hidden; + border: 2px solid #0d9895; +} + +.animated-nav-vert { + position: relative; +} + +.animated-nav-vert .slide { + width: 100%; + height: 30px; + position: absolute; + left: 0; + top: 0; + transition: 200ms; + opacity: 0; + background: linear-gradient(230.69deg, #33d4b7 9.11%, #0d9895 69.45%); + /* z-index: -1; */ + pointer-events: none; +} + +.nav-dropdown-content a, +.nav-dropdown-content button { + display: flex; + align-items: center; + justify-content: center; + height: 30px; + text-align: center; + width: 100%; + color: black; + transition: 100ms; + position: relative; + z-index: 1; +} + +.nav-dropdown-btn:hover .nav-dropdown-content { + display: block; +} + +.animated-nav-vert a:hover, +.animated-nav-vert button:hover { + color: white; +} + +.animated-nav-vert a:nth-child(2):hover~.slide { + top: 30px; + opacity: 1; +} + +.animated-nav-vert a:nth-child(1):hover~.slide { + top: 0; + opacity: 1; +} + +.animated-nav-vert a:nth-child(3):hover~.slide { + top: calc(2 * 30px); + opacity: 1; +} + +.animated-nav-vert a:nth-child(4):hover~.slide { + top: calc(3 * 30px); + opacity: 1; +} + +.animated-nav-vert button:nth-child(5):hover~.slide { + top: calc(4 * 30px); + opacity: 1; +} + +.custom-calendar { + border: 0px !important; + width: 290px !important; +} + +.custom-calendar .react-calendar_tile { + font-size: 12px !important; + padding: 5px !important; + line-height: 1 !important; +} + +.custom-calendar .react-calendar__navigation { + margin-bottom: 0; +} + +.custom-calendar .react-calendar__tile:disabled { + /* position: relative; */ + background: transparent !important; + opacity: 0.4; + text-decoration: line-through; +} + +.custom-calendar .react-calendar__tile:disabled.react-calendar__tile--active { + background: transparent !important; + color: rgba(0, 0, 0, 0.6) !important; +} + +.date-picker .react-calendar__tile:disabled { + /* position: relative; */ + background: transparent !important; + opacity: 0.4; + text-decoration: line-through; +} + +.to-chat { + position: relative; +} + +.to-chat::before { + content: ""; + position: absolute; + bottom: 0; + right: 0; + width: 10px; + height: 10px; + border: 0px solid #f2f4f7; + background-color: white; + border-right-width: 1px; + border-bottom-width: 1px; + z-index: 2; +} + +.from-chat { + position: relative; +} + +.from-chat::before { + content: ""; + position: absolute; + top: 0; + left: 0; + background-color: #15212a; + width: 10px; + height: 10px; +} + +.reduce-z-index { + z-index: -1; +} + +.property-space-card:hover>div:first-child { + transition: 200ms; +} + +.animate-filter { + animation: slideDown 200ms ease-in-out; +} + +.slideUp { + animation: slideUp 200ms ease-in-out; +} + +.emoji-picker { + bottom: -100%; + left: -400px; + z-index: 3; + animation: slideUp 400ms ease-in-out; +} + +.scheduling-calendar { + width: 100% !important; + border: none !important; +} + +.scheduling-calendar .react-calendar__tile abbr { + display: none; +} + +.scheduling-calendar .react-calendar__navigation { + display: grid !important; + grid-template-columns: 205px 40px 40px; + gap: 12px; + align-items: center; + padding-inline: 5px; +} + +.scheduling-calendar .react-calendar__navigation__prev2-button { + display: none; +} + +.scheduling-calendar .react-calendar__navigation__next2-button { + display: flex; +} + +.scheduling-calendar .react-calendar__navigation__label { + grid-column: 1/2; +} + +.scheduling-calendar .react-calendar__navigation__prev-button { + grid-column: 2/3; + border: 1px solid #d0d5dd; + grid-row: 1; + padding: 10px 0; +} + +.scheduling-calendar .react-calendar__navigation__next-button { + grid-column: 3/4; +} + +.use-template { + width: 150px; + padding: 8px 0; + display: inline-flex; + justify-content: center; + background: linear-gradient(230.69deg, #33d4b7 9.11%, #0d9895 69.45%) !important; + color: white; +} + +.react-calendar__navigation button.use-template:enabled:hover { + background: linear-gradient(230.69deg, #33d4b7 9.11%, #0d9895 69.45%) !important; +} + +.react-calendar__navigation button.use-template:enabled:focus { + background: linear-gradient(230.69deg, #33d4b7 9.11%, #0d9895 69.45%) !important; +} + +.scheduling-calendar .react-calendar__tile { + padding: 15px 8px !important; + font-size: 14px !important; + height: unset; +} + +.scheduling-calendar .react-calendar__viewContainer { + margin-top: 3rem; +} + +.scheduling-calendar .react-calendar__month-view__weekdays { + display: grid !important; + grid-template-columns: repeat(7, minmax(170px, 1fr)); + border-bottom-width: 1px; +} + +.scheduling-calendar .react-calendar__month-view__days { + display: grid !important; + grid-template-columns: repeat(7, minmax(170px, 1fr)); +} + +.scheduling-calendar .react-calendar__month-view__weekdays__weekday { + justify-self: flex-start; + color: #475467; + font-size: 16px; + font-weight: normal !important; +} + +.scheduling-calendar .react-calendar__tile { + padding: 0px !important; + color: #667085 !important; +} + +.scheduling-calendar .react-calendar__month-view__days__day--neighboringMonth { + color: #98a2b3 !important; +} + +.scheduling-calendar .react-calendar__tile--active { + background: #f0f5f3 !important; + border-radius: unset !important; + border: 2px solid #0d9895; + color: #667085 !important; + overflow: visible !important; +} + +.scheduling-calendar .react-calendar__tile:enabled:hover { + background: unset !important; +} + +.scheduling-calendar .react-calendar__tile:enabled:hover.react-calendar__tile--active:hover { + background: #f0f5f3 !important; +} + +.scheduling-calendar .react-calendar__tile--now::after { + display: none !important; +} + +.react-calendar__viewContainer { + overflow: auto; + scroll-margin-right: 1rem; +} + +.react-calendar__viewContainer::-webkit-scrollbar { + height: 0px !important; +} + +.my-z-index { + z-index: 1000; +} + +.schedule-options { + text-align: start !important; + align-items: flex-start !important; +} + +.between-slots { + background: linear-gradient(230.69deg, rgba(51, 212, 183, 0.05) 9.11%, rgba(13, 152, 149, 0.05) 69.45%) !important; +} + +.absolute-middle { + height: 10px; + top: calc(50% - 5px); +} + +.draft-stage { + border-radius: 50%; + width: 30px; + height: 30px; + box-shadow: 0 0 0 5px white, 0 0 0 7px #ccc; + display: flex; + justify-content: center; + align-items: center; + color: white; + background-color: #ccc; +} + +.draft-stage.complete { + box-shadow: 0 0 0 5px white, 0 0 0 7px #0d9895; + background-color: #0d9895; +} + +.draft-stage~.absolute { + top: calc(100% + 1rem); + width: 180px; +} + +.nav-item-dropdown { + height: 0px; + overflow: hidden; + opacity: 0; + transition: 400ms; +} + +.open .nav-item-dropdown { + height: unset; + opacity: 1; +} + +.open .caret-up { + display: none; +} + +.caret-down { + display: none; +} + +.open .caret-down { + display: inline; +} + +.super-nav { + transition: 400ms; + height: 50px; +} + +.super-nav .sidebar-item { + background-color: white; + color: #475467; +} + +.super-nav.highlight .sidebar-item, +.sidebar-item:hover { + background-color: #475467; + color: white; +} + +.super-nav.open { + height: 150px; +} + +.super-nav.open.large { + height: 200px; +} + +.super-nav.open.larger { + height: 710px; +} + + +.super-nav.open.small { + height: 100px; +} + +.sun-editor-editable *::-moz-placeholder { + font-size: 14px !important; + font-family: "Noto Sans", sans-serif !important; +} + +.sun-editor-editable, +.sun-editor-editable *::placeholder { + font-size: 14px !important; + font-family: "Noto Sans", sans-serif !important; +} + +.infinite-scroll-component { + overflow: hidden !important; +} + +.hover-show-edit { + position: relative; +} + +.hover-show-edit .edit-btn { + display: none; + position: absolute; +} + +.hover-show-edit input, +.hover-show-edit .save-btn { + display: none; +} + +.hover-show-edit.edit-mode input, +.hover-show-edit.edit-mode .save-btn { + display: inline; +} + +.hover-show-edit.edit-mode span, +.hover-show-edit.edit-mode .edit-btn { + display: none !important; +} + +.hover-show-edit:hover .edit-btn { + display: inline; +} + +.options-btn { + display: none; +} + +.schedule-day:hover .options-btn { + display: flex; +} + +.sun-editor { + border-radius: 5px; + border-style: solid; +} + +.slide-down { + animation: dropDown 300ms; +} + +.slide-up { + animation: dropUp 300ms; +} + +.show-on-parent-hover { + display: none; +} + +*:has(> .show-on-parent-hover):hover .show-on-parent-hover { + display: inline; +} + +.space-tile { + aspect-ratio: 262/167; +} + +button.login-btn-gradient { + transition: opacity 400ms; + position: relative; + background: unset; + z-index: 4; + overflow: hidden; +} + +button.login-btn-gradient:disabled { + cursor: auto; +} + +button.login-btn-gradient:not(:disabled):after { + content: ""; + position: absolute; + z-index: -1; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0.8; + background: linear-gradient(230.69deg, hsl(169, 65%, 42%) 9.11%, hsl(179, 84%, 22%) 69.45%); + transition: opacity 200ms linear; +} + +button.login-btn-gradient:not(:disabled):hover::after, +button.login-btn-gradient:not(:disabled):focus::after { + opacity: 0.9; +} + +button.login-btn-gradient:active::after { + opacity: 1; +} + +.react-calendar__navigation__prev-button:disabled, +.react-calendar__navigation__prev2-button:disabled { + background-color: transparent !important; +} + +.react-calendar__navigation__prev-button:disabled .prev-icon path { + opacity: 0.4; +} + +.booking-card-grid { + display: grid; + grid-template-columns: 3fr 2fr 1fr; +} + +.react-calendar__tile:disabled:has(> .schedule-day) { + opacity: 0.6; +} + +@keyframes dropUp { + 0% { + max-height: 200px; + } + + 100% { + max-height: 70px; + } +} + +@keyframes dropDown { + 0% { + max-height: 70px; + } + + 100% { + max-height: 200px; + } +} + +@media (min-width: 1200px) { + .ov-visible { + overflow: visible !important; + } + + .account-header { + min-width: -moz-fit-content; + min-width: fit-content; + } + + .property-swiper-slide .swiper-pagination { + display: block; + } + + .property-swiper-slide .swiper-button-next, + .property-swiper-slide .swiper-button-prev { + display: flex; + } +} + +@media (max-width: 600px) { + .account-header a { + padding-inline: 5rem !important; + } + + .popup-mobile { + position: fixed; + background-color: rgba(0, 0, 0, 0.6); + top: 0; + right: 0; + left: 0; + height: 100vh; + z-index: 54; + display: flex; + align-items: center; + justify-content: center; + padding-top: 5rem; + } + + .popup-mobile-2 { + position: absolute; + /* background-color: rgba(0, 0, 0, 0.3); */ + top: 0; + right: 0; + left: 0; + height: 100%; + z-index: 54; + display: flex; + padding: 1rem 0; + } +} + +@keyframes slideDown { + 0% { + transform: translateY(-30px); + opacity: 0; + } + + 100% { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes slideUp { + 0% { + transform: translateY(30px); + opacity: 0; + } + + 100% { + transform: translateY(0); + opacity: 1; + } +} + +.pill { + border-radius: 100vw; +} + +.horizontal-scroll-categories { + display: grid; + grid-auto-flow: column; + grid-auto-columns: var(--category-slider-width); + overflow-x: auto; + overscroll-behavior-inline: contain; + scroll-snap-type: inline mandatory; + position: relative; +} + +.horizontal-scroll-categories>* { + scroll-snap-align: start; +} + +.horizontal-scroll-categories::-webkit-scrollbar { + display: none; +} + +.mover { + display: block; + width: 105px; + border-radius: 2px; + height: 2px; + position: absolute; + left: 0; + bottom: 0; + transition: 300ms; + opacity: 0; + background-color: white; +} + +.horizontal-scroll-categories button:nth-child(1).active~.mover { + left: 0; + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(2).active~.mover { + left: var(--category-slider-width); + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(3).active~.mover { + left: calc(var(--category-slider-width) * 2); + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(4).active~.mover { + left: calc(var(--category-slider-width) * 3); + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(5).active~.mover { + left: calc(var(--category-slider-width) * 4); + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(6).active~.mover { + left: calc(var(--category-slider-width) * 5); + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(7).active~.mover { + left: calc(var(--category-slider-width) * 6); + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(8).active~.mover { + left: calc(var(--category-slider-width) * 7); + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(9).active~.mover { + left: calc(var(--category-slider-width) * 8); + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(10).active~.mover { + left: calc(var(--category-slider-width) * 9); + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(11).active~.mover { + left: calc(var(--category-slider-width) * 10); + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(12).active~.mover { + left: calc(var(--category-slider-width) * 11); + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(13).active~.mover { + left: calc(var(--category-slider-width) * 12); + opacity: 1; +} + +.horizontal-scroll-categories button:nth-child(14).active~.mover { + left: calc(var(--category-slider-width) * 13); + opacity: 1; +} + +.navbar-slider .mover { + background-color: black; + width: var(--account-menu-item-width); +} + +.navbar-slider .swiper-wrapper:has(.slider-menu:nth-child(1) a.active)~.mover { + left: 0; + opacity: 1; +} + +.navbar-slider .swiper-wrapper:has(.slider-menu:nth-child(2) a.active)~.mover { + left: calc(var(--account-menu-item-width) * 1); + opacity: 1; +} + +.navbar-slider .swiper-wrapper:has(.slider-menu:nth-child(3) a.active)~.mover { + left: calc(var(--account-menu-item-width) * 2); + opacity: 1; +} + +.navbar-slider .swiper-wrapper:has(.slider-menu:nth-child(4) a.active)~.mover { + left: calc(var(--account-menu-item-width) * 3); + opacity: 1; +} + +.navbar-slider .swiper-wrapper:has(.slider-menu:nth-child(5) a.active)~.mover { + left: calc(var(--account-menu-item-width) * 4); + opacity: 1; +} + +.navbar-slider .swiper-wrapper:has(.slider-menu:nth-child(6) a.active)~.mover { + left: calc(var(--account-menu-item-width) * 5); + opacity: 1; +} + +.navbar-slider .swiper-wrapper:has(.slider-menu:nth-child(7) a.active)~.mover { + left: calc(var(--account-menu-item-width) * 6); + opacity: 1; +} + +.two-tab-menu { + position: relative; + --menu-width: 140px; +} + +.two-tab-menu.small { + --menu-width: 120px; +} + +.two-tab-menu.smaller { + --menu-width: 105px; +} + +.two-tab-menu .mover { + background-color: black; + width: var(--menu-width); +} + +.two-tab-menu button:nth-child(1)[data-headlessui-state="selected"]~.mover { + left: 0; + opacity: 1; +} + +.two-tab-menu button:nth-child(2)[data-headlessui-state="selected"]~.mover { + left: calc(var(--menu-width) * 1); + opacity: 1; +} + +.horizontal-scroll-hosts { + display: grid; + grid-auto-flow: column; + grid-auto-columns: max-content; + overflow-x: auto; + overscroll-behavior-inline: contain; + scroll-snap-type: inline mandatory; + scroll-padding-left: 1rem; + padding-left: 1rem; +} + +.horizontal-scroll-hosts>* { + scroll-snap-align: start; +} + +.horizontal-scroll-hosts::-webkit-scrollbar { + height: 0px !important; +} + +.horizontal-scroll-accounts { + display: grid; + grid-auto-flow: column; + grid-auto-columns: max-content; + overflow-x: auto; + overscroll-behavior-inline: contain; + scroll-snap-type: inline mandatory; + scroll-padding-inline: 1rem; +} + +.horizontal-scroll-accounts>* { + scroll-snap-align: start; + padding-inline: 1.5rem; + padding-bottom: 0.5rem; +} + +.horizontal-scroll-accounts>.active { + border-bottom: 2px solid black; + font-weight: 600; +} + +.slider-menu:has(.active) { + font-weight: 600; +} + +.fade-in { + animation: fade-in 1.2s cubic-bezier(0.39, 0.575, 0.565, 1) both; +} + +@keyframes fade-in { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } +} + +@media (max-width: 1000px) { + .slider-menu:has(.active) { + border-bottom: 2px solid black; + } + + .navbar-slider .mover { + display: none; + } +} + +.horizontal-scroll-accounts::-webkit-scrollbar { + height: 0px !important; +} + +.date-picker .react-calendar__tile { + height: 48px !important; + font-size: 14px !important; +} + +.error-vibrate { + animation: error-vibrate 0.3s linear both; +} + +.property-space-grid { + display: grid; + gap: 4rem 2rem; + grid-template-columns: repeat(auto-fit, minmax(min(var(--property-card-width), 100%), 1fr)); +} + +@keyframes error-vibrate { + 0% { + transform: translate(0); + } + + 20% { + transform: translate(-2px, 2px); + } + + 40% { + transform: translate(-2px, -2px); + } + + 60% { + transform: translate(2px, 2px); + } + + 80% { + transform: translate(2px, -2px); + } + + 100% { + transform: translate(0); + } +} + +@media (max-width: 900px) { + .messages-grid { + display: grid; + grid-template-columns: 1fr; + grid-auto-rows: 1fr; + } + + .popup-tablet { + position: fixed; + background-color: rgba(0, 0, 0, 0.6); + top: 0; + right: 0; + left: 0; + height: 100vh; + z-index: 54; + display: flex; + align-items: center; + justify-content: center; + padding-top: 5rem; + width: 100%; + } +} + +/* tailwind breakpoints */ + +@media (min-width: 640px) { + + .swiper-button-next, + .swiper-button-prev { + top: calc(50% - 40px) !important; + } + + .scheduling-calendar .react-calendar__navigation { + display: grid !important; + grid-template-columns: 205px 40px 250px; + gap: 12px; + align-items: center; + padding-inline: 5px; + } + + .scheduling-calendar .react-calendar__viewContainer { + margin-top: 0px; + } + + .scheduling-calendar .react-calendar__navigation__next2-button { + display: none; + } + + .property-grid { + grid-template-columns: repeat(2, minmax(292px, 1fr)); + padding-inline: 0; + -moz-column-gap: 31px; + column-gap: 31px; + row-gap: 80px; + max-width: unset; + } + + .browse-grid { + padding-inline: 0; + } + + .sticky-price-summary { + box-shadow: none; + } + + .pagination-image { + display: inline !important; + } + + .property-space-card .aspect { + aspect-ratio: 292/227 !important; + min-height: 227px; + } + + .browse-grid { + display: grid; + grid-template-columns: repeat(2, minmax(163px, 1fr)); + -moz-column-gap: 19px; + column-gap: 19px; + row-gap: 20px; + padding-inline: 1rem; + } +} + +@media (min-width: 768px) { + .horizontal-scroll-hosts { + scroll-padding-left: unset; + padding-left: unset; + } +} + +@media (min-width: 1024px) { + .property-grid { + grid-template-columns: repeat(3, minmax(292px, 1fr)); + } + + .addons-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + -moz-column-gap: 50px; + column-gap: 50px; + row-gap: 20px; + } + + .amenities-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + row-gap: 8px; + } + + .navbar-slider .swiper-wrapper { + transform: none !important; + } +} + +@media (min-width: 1280px) { + .property-grid { + grid-template-columns: repeat(4, minmax(292px, 1fr)); + } + + .browse-grid { + -moz-column-gap: 32px; + column-gap: 32px; + row-gap: 32px; + } + + .messages-grid { + display: grid; + grid-template-columns: 1.2fr 2fr 1fr; + width: 100%; + } + + .my-shadow { + box-shadow: none; + border-radius: none; + } +} + +@media (max-width: 600px) { + .react-tooltip { + display: none !important; + } +} + +@media (max-width: 650px) { + .property-space-grid { + padding-inline: 4rem; + } +} + +@media (max-width: 500px) { + .property-space-grid { + padding-inline: 1rem; + } +} + +.toast-animation { + animation: toast-animation 250ms ease-in-out; +} + +@keyframes toast-animation { + 0% { + width: 0px; + opacity: 0; + } + + 100% { + width: 20rem; + opacity: 1; + } +} + +input[type="date"] { + display: block; + -webkit-appearance: textfield; + -moz-appearance: textfield; + min-height: 1.2em; +} + +.fade-in-top { + animation: fade-in-top 0.6s cubic-bezier(0.39, 0.575, 0.565, 1) both; +} + +@keyframes fade-in-top { + 0% { + transform: translateY(-50px); + opacity: 0; + } + + 100% { + transform: translateY(0); + opacity: 1; + } +} + +.empty\:mb-0:empty { + margin-bottom: 0px; +} + +.empty\:mt-0:empty { + margin-top: 0px; +} + +.empty\:hidden:empty { + display: none; +} + +.focus-within\:outline-primary:focus-within { + outline-color: #33d4b7; +} + +.hover\:scale-150:hover { + --tw-scale-x: 1.5; + --tw-scale-y: 1.5; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.hover\:border-gray-300:hover { + --tw-border-opacity: 1; + border-color: rgb(209 213 219 / var(--tw-border-opacity)); +} + +.hover\:border-transparent:hover { + border-color: transparent; +} + +.hover\:bg-\[var\(--outline-color\)\]:hover { + background-color: var(--outline-color); +} + +.hover\:bg-blue-200:hover { + --tw-bg-opacity: 1; + background-color: rgb(191 219 254 / var(--tw-bg-opacity)); +} + +.hover\:bg-blue-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(29 78 216 / var(--tw-bg-opacity)); +} + +.hover\:bg-gray-100:hover { + --tw-bg-opacity: 1; + background-color: rgb(243 244 246 / var(--tw-bg-opacity)); +} + +.hover\:bg-gray-200:hover { + --tw-bg-opacity: 1; + background-color: rgb(229 231 235 / var(--tw-bg-opacity)); +} + +.hover\:bg-gray-50:hover { + --tw-bg-opacity: 1; + background-color: rgb(249 250 251 / var(--tw-bg-opacity)); +} + +.hover\:bg-primary-dark:hover { + --tw-bg-opacity: 1; + background-color: rgb(13 152 149 / var(--tw-bg-opacity)); +} + +.hover\:bg-red-500:hover { + --tw-bg-opacity: 1; + background-color: rgb(239 68 68 / var(--tw-bg-opacity)); +} + +.hover\:text-black:hover { + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity)); +} + +.hover\:text-blue-800:hover { + --tw-text-opacity: 1; + color: rgb(30 64 175 / var(--tw-text-opacity)); +} + +.hover\:text-gray-600:hover { + --tw-text-opacity: 1; + color: rgb(75 85 99 / var(--tw-text-opacity)); +} + +.hover\:text-gray-900:hover { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + +.hover\:text-primary:hover { + --tw-text-opacity: 1; + color: rgb(51 212 183 / var(--tw-text-opacity)); +} + +.hover\:text-white:hover { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.hover\:underline:hover { + -webkit-text-decoration-line: underline; + text-decoration-line: underline; +} + +.hover\:shadow-lg:hover { + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.hover\:ring-2:hover { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.hover\:ring-primary:hover { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(51 212 183 / var(--tw-ring-opacity)); +} + +.focus\:outline-none:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.focus\:outline-primary:focus { + outline-color: #33d4b7; +} + +.focus\:outline-red-500:focus { + outline-color: #ef4444; +} + +.focus\:ring-2:focus { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.focus\:ring-\[\#33D4B7\]:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(51 212 183 / var(--tw-ring-opacity)); +} + +.focus\:ring-gray-300:focus { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(209 213 219 / var(--tw-ring-opacity)); +} + +.focus\:ring-offset-2:focus { + --tw-ring-offset-width: 2px; +} + +.focus\:ring-offset-gray-100:focus { + --tw-ring-offset-color: #f3f4f6; +} + +.focus-visible\:ring-2:focus-visible { + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); +} + +.focus-visible\:ring-blue-500:focus-visible { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity)); +} + +.focus-visible\:ring-white:focus-visible { + --tw-ring-opacity: 1; + --tw-ring-color: rgb(255 255 255 / var(--tw-ring-opacity)); +} + +.focus-visible\:ring-opacity-75:focus-visible { + --tw-ring-opacity: 0.75; +} + +.focus-visible\:ring-offset-2:focus-visible { + --tw-ring-offset-width: 2px; +} + +.active\:bg-gray-300:active { + --tw-bg-opacity: 1; + background-color: rgb(209 213 219 / var(--tw-bg-opacity)); +} + +.active\:outline-none:active { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.disabled\:border-0:disabled { + border-width: 0px; +} + +.disabled\:border-\[\#D0D5DD\]:disabled { + --tw-border-opacity: 1; + border-color: rgb(208 213 221 / var(--tw-border-opacity)); +} + +.disabled\:bg-\[\#F2F4F7\]:disabled { + --tw-bg-opacity: 1; + background-color: rgb(242 244 247 / var(--tw-bg-opacity)); +} + +.disabled\:text-\[\#98A2B3\]:disabled { + --tw-text-opacity: 1; + color: rgb(152 162 179 / var(--tw-text-opacity)); +} + +.disabled\:text-gray-500:disabled { + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity)); +} + +.disabled\:line-through:disabled { + -webkit-text-decoration-line: line-through; + text-decoration-line: line-through; +} + +.disabled\:opacity-50:disabled { + opacity: 0.5; +} + +.group:hover .group-hover\:bg-\[\#1D2939\] { + --tw-bg-opacity: 1; + background-color: rgb(29 41 57 / var(--tw-bg-opacity)); +} + +.group:hover .group-hover\:text-white { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); +} + +.ui-open\:rotate-0[data-headlessui-state~="open"] { + --tw-rotate: 0deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.ui-open\:rotate-180[data-headlessui-state~="open"] { + --tw-rotate: 180deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.ui-open\:text-opacity-90[data-headlessui-state~="open"] { + --tw-text-opacity: 0.9; +} + +:where([data-headlessui-state~="open"]) .ui-open\:rotate-0 { + --tw-rotate: 0deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +:where([data-headlessui-state~="open"]) .ui-open\:rotate-180 { + --tw-rotate: 180deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +:where([data-headlessui-state~="open"]) .ui-open\:text-opacity-90 { + --tw-text-opacity: 0.9; +} + +.ui-not-open\:-rotate-90[data-headlessui-state]:not([data-headlessui-state~="open"]) { + --tw-rotate: -90deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +:where([data-headlessui-state]:not([data-headlessui-state~="open"])) .ui-not-open\:-rotate-90:not([data-headlessui-state]) { + --tw-rotate: -90deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.ui-selected\:font-semibold[data-headlessui-state~="selected"] { + font-weight: 600; +} + +.ui-selected\:text-black[data-headlessui-state~="selected"] { + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity)); +} + +:where([data-headlessui-state~="selected"]) .ui-selected\:font-semibold { + font-weight: 600; +} + +:where([data-headlessui-state~="selected"]) .ui-selected\:text-black { + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity)); +} + +.ui-active\:bg-amber-100[data-headlessui-state~="active"] { + --tw-bg-opacity: 1; + background-color: rgb(254 243 199 / var(--tw-bg-opacity)); +} + +.ui-active\:bg-gray-100[data-headlessui-state~="active"] { + --tw-bg-opacity: 1; + background-color: rgb(243 244 246 / var(--tw-bg-opacity)); +} + +.ui-active\:text-amber-900[data-headlessui-state~="active"] { + --tw-text-opacity: 1; + color: rgb(120 53 15 / var(--tw-text-opacity)); +} + +.ui-active\:text-black[data-headlessui-state~="active"] { + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity)); +} + +.ui-active\:text-gray-900[data-headlessui-state~="active"] { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + +:where([data-headlessui-state~="active"]) .ui-active\:bg-amber-100 { + --tw-bg-opacity: 1; + background-color: rgb(254 243 199 / var(--tw-bg-opacity)); +} + +:where([data-headlessui-state~="active"]) .ui-active\:bg-gray-100 { + --tw-bg-opacity: 1; + background-color: rgb(243 244 246 / var(--tw-bg-opacity)); +} + +:where([data-headlessui-state~="active"]) .ui-active\:text-amber-900 { + --tw-text-opacity: 1; + color: rgb(120 53 15 / var(--tw-text-opacity)); +} + +:where([data-headlessui-state~="active"]) .ui-active\:text-black { + --tw-text-opacity: 1; + color: rgb(0 0 0 / var(--tw-text-opacity)); +} + +:where([data-headlessui-state~="active"]) .ui-active\:text-gray-900 { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + +.ui-not-active\:text-gray-700[data-headlessui-state]:not([data-headlessui-state~="active"]) { + --tw-text-opacity: 1; + color: rgb(55 65 81 / var(--tw-text-opacity)); +} + +.ui-not-active\:text-gray-800[data-headlessui-state]:not([data-headlessui-state~="active"]) { + --tw-text-opacity: 1; + color: rgb(31 41 55 / var(--tw-text-opacity)); +} + +.ui-not-active\:text-gray-900[data-headlessui-state]:not([data-headlessui-state~="active"]) { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + +:where([data-headlessui-state]:not([data-headlessui-state~="active"])) .ui-not-active\:text-gray-700:not([data-headlessui-state]) { + --tw-text-opacity: 1; + color: rgb(55 65 81 / var(--tw-text-opacity)); +} + +:where([data-headlessui-state]:not([data-headlessui-state~="active"])) .ui-not-active\:text-gray-800:not([data-headlessui-state]) { + --tw-text-opacity: 1; + color: rgb(31 41 55 / var(--tw-text-opacity)); +} + +:where([data-headlessui-state]:not([data-headlessui-state~="active"])) .ui-not-active\:text-gray-900:not([data-headlessui-state]) { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity)); +} + +@media (prefers-color-scheme: dark) { + .dark\:text-gray-400 { + --tw-text-opacity: 1; + color: rgb(156 163 175 / var(--tw-text-opacity)); + } + + .dark\:text-gray-500 { + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity)); + } + + .dark\:hover\:bg-gray-700:hover { + --tw-bg-opacity: 1; + background-color: rgb(55 65 81 / var(--tw-bg-opacity)); + } + + .dark\:hover\:text-white:hover { + --tw-text-opacity: 1; + color: rgb(255 255 255 / var(--tw-text-opacity)); + } +} + +@media (min-width: 640px) { + .sm\:ml-4 { + margin-left: 1rem; + } + + .sm\:flex { + display: flex; + } + + .sm\:px-0 { + padding-left: 0px; + padding-right: 0px; + } + + .sm\:text-left { + text-align: left; + } + + .sm\:text-sm { + font-size: 0.875rem; + } +} + +@media (min-width: 768px) { + .md\:invisible { + visibility: hidden; + } + + .md\:static { + position: static; + } + + .md\:relative { + position: relative; + } + + .md\:-left-10 { + left: -2.5rem; + } + + .md\:-top-10 { + top: -2.5rem; + } + + .md\:right-\[unset\] { + right: unset; + } + + .md\:mx-0 { + margin-left: 0px; + margin-right: 0px; + } + + .md\:my-\[32px\] { + margin-top: 32px; + margin-bottom: 32px; + } + + .md\:my-\[47px\] { + margin-top: 47px; + margin-bottom: 47px; + } + + .md\:-mt-10 { + margin-top: -2.5rem; + } + + .md\:mb-0 { + margin-bottom: 0px; + } + + .md\:mb-2 { + margin-bottom: 0.5rem; + } + + .md\:mb-4 { + margin-bottom: 1rem; + } + + .md\:mb-\[103px\] { + margin-bottom: 103px; + } + + .md\:mb-\[24px\] { + margin-bottom: 24px; + } + + .md\:mb-\[70px\] { + margin-bottom: 70px; + } + + .md\:ml-0 { + margin-left: 0px; + } + + .md\:ml-\[unset\] { + margin-left: unset; + } + + .md\:mr-2 { + margin-right: 0.5rem; + } + + .md\:mr-\[65px\] { + margin-right: 65px; + } + + .md\:mr-\[unset\] { + margin-right: unset; + } + + .md\:mt-0 { + margin-top: 0px; + } + + .md\:mt-2 { + margin-top: 0.5rem; + } + + .md\:block { + display: block; + } + + .md\:\!inline { + display: inline !important; + } + + .md\:inline { + display: inline; + } + + .md\:flex { + display: flex; + } + + .md\:grid { + display: grid; + } + + .md\:hidden { + display: none; + } + + .md\:h-40 { + height: 10rem; + } + + .md\:h-\[100px\] { + height: 100px; + } + + .md\:h-\[40px\] { + height: 40px; + } + + .md\:h-\[600px\] { + height: 600px; + } + + .md\:h-\[64px\] { + height: 64px; + } + + .md\:h-\[80px\] { + height: 80px; + } + + .md\:h-\[unset\] { + height: unset; + } + + .md\:max-h-\[250px\] { + max-height: 250px; + } + + .md\:max-h-\[270px\] { + max-height: 270px; + } + + .md\:max-h-\[unset\] { + max-height: unset; + } + + .md\:\!w-1\/2 { + width: 50% !important; + } + + .md\:\!w-\[unset\] { + width: unset !important; + } + + .md\:w-1\/2 { + width: 50%; + } + + .md\:w-1\/3 { + width: 33.333333%; + } + + .md\:w-2\/5 { + width: 40%; + } + + .md\:w-24 { + width: 6rem; + } + + .md\:w-3\/5 { + width: 60%; + } + + .md\:w-4\/5 { + width: 80%; + } + + .md\:w-\[100px\] { + width: 100px; + } + + .md\:w-\[152px\] { + width: 152px; + } + + .md\:w-\[158px\] { + width: 158px; + } + + .md\:w-\[178px\] { + width: 178px; + } + + .md\:w-\[189px\] { + width: 189px; + } + + .md\:w-\[204px\] { + width: 204px; + } + + .md\:w-\[24\%\] { + width: 24%; + } + + .md\:w-\[28\%\] { + width: 28%; + } + + .md\:w-\[333px\] { + width: 333px; + } + + .md\:w-\[340px\] { + width: 340px; + } + + .md\:w-\[40\%\] { + width: 40%; + } + + .md\:w-\[40px\] { + width: 40px; + } + + .md\:w-\[43\%\] { + width: 43%; + } + + .md\:w-\[473px\] { + width: 473px; + } + + .md\:w-\[48\%\] { + width: 48%; + } + + .md\:w-\[500px\] { + width: 500px; + } + + .md\:w-\[50px\] { + width: 50px; + } + + .md\:w-\[55\%\] { + width: 55%; + } + + .md\:w-\[64px\] { + width: 64px; + } + + .md\:w-\[80px\] { + width: 80px; + } + + .md\:w-\[unset\] { + width: unset; + } + + .md\:w-full { + width: 100%; + } + + .md\:min-w-\[35rem\] { + min-width: 35rem; + } + + .md\:min-w-\[370px\] { + min-width: 370px; + } + + .md\:max-w-\[120px\] { + max-width: 120px; + } + + .md\:max-w-\[200px\] { + max-width: 200px; + } + + .md\:max-w-\[656px\] { + max-width: 656px; + } + + .md\:max-w-\[72px\] { + max-width: 72px; + } + + .md\:max-w-\[82vw\] { + max-width: 82vw; + } + + .md\:max-w-lg { + max-width: 32rem; + } + + .md\:translate-y-0 { + --tw-translate-y: 0px; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + } + + .md\:list-disc { + list-style-type: disc; + } + + .md\:flex-row { + flex-direction: row; + } + + .md\:flex-col { + flex-direction: column; + } + + .md\:flex-nowrap { + flex-wrap: nowrap; + } + + .md\:items-start { + align-items: flex-start; + } + + .md\:items-end { + align-items: flex-end; + } + + .md\:items-center { + align-items: center; + } + + .md\:justify-start { + justify-content: flex-start; + } + + .md\:gap-0 { + gap: 0px; + } + + .md\:gap-5 { + gap: 1.25rem; + } + + .md\:gap-\[24px\] { + gap: 24px; + } + + .md\:gap-\[32px\] { + gap: 32px; + } + + .md\:gap-\[41px\] { + gap: 41px; + } + + .md\:gap-\[67px\] { + gap: 67px; + } + + .md\:truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .md\:whitespace-nowrap { + white-space: nowrap; + } + + .md\:rounded-b-\[3rem\] { + border-bottom-right-radius: 3rem; + border-bottom-left-radius: 3rem; + } + + .md\:rounded-r-lg { + border-top-right-radius: 0.5rem; + border-bottom-right-radius: 0.5rem; + } + + .md\:rounded-bl-\[32px\] { + border-bottom-left-radius: 32px; + } + + .md\:rounded-br-\[32px\] { + border-bottom-right-radius: 32px; + } + + .md\:border { + border-width: 1px; + } + + .md\:border-0 { + border-width: 0px; + } + + .md\:border-2 { + border-width: 2px; + } + + .md\:p-\[32px\] { + padding: 32px; + } + + .md\:px-0 { + padding-left: 0px; + padding-right: 0px; + } + + .md\:px-12 { + padding-left: 3rem; + padding-right: 3rem; + } + + .md\:px-24 { + padding-left: 6rem; + padding-right: 6rem; + } + + .md\:px-4 { + padding-left: 1rem; + padding-right: 1rem; + } + + .md\:px-5 { + padding-left: 1.25rem; + padding-right: 1.25rem; + } + + .md\:px-6 { + padding-left: 1.5rem; + padding-right: 1.5rem; + } + + .md\:px-\[12px\] { + padding-left: 12px; + padding-right: 12px; + } + + .md\:px-\[16px\] { + padding-left: 16px; + padding-right: 16px; + } + + .md\:px-\[18px\] { + padding-left: 18px; + padding-right: 18px; + } + + .md\:px-\[20px\] { + padding-left: 20px; + padding-right: 20px; + } + + .md\:px-\[24px\] { + padding-left: 24px; + padding-right: 24px; + } + + .md\:px-\[32px\] { + padding-left: 32px; + padding-right: 32px; + } + + .md\:py-\[32px\] { + padding-top: 32px; + padding-bottom: 32px; + } + + .md\:pb-\[120px\] { + padding-bottom: 120px; + } + + .md\:pb-\[140px\] { + padding-bottom: 140px; + } + + .md\:pl-16 { + padding-left: 4rem; + } + + .md\:pl-32 { + padding-left: 8rem; + } + + .md\:pl-4 { + padding-left: 1rem; + } + + .md\:pr-\[18px\] { + padding-right: 18px; + } + + .md\:pt-\[110px\] { + padding-top: 110px; + } + + .md\:pt-\[120px\] { + padding-top: 120px; + } + + .md\:pt-\[40px\] { + padding-top: 40px; + } + + .md\:pt-\[90px\] { + padding-top: 90px; + } + + .md\:text-center { + text-align: center; + } + + .md\:text-2xl { + font-size: 1.25rem; + } + + .md\:text-3xl { + font-size: 1.5rem; + } + + .md\:text-4xl { + font-size: 1.953rem; + } + + .md\:text-5xl { + font-size: 2.441rem; + } + + .md\:text-6xl { + font-size: 3.114rem; + } + + .md\:text-7xl { + font-size: 4rem; + } + + .md\:text-\[30px\] { + font-size: 30px; + } + + .md\:text-base { + font-size: 0.9rem; + } + + .md\:text-sm { + font-size: 0.875rem; + } + + .md\:text-xl { + font-size: 1rem; + } + + .md\:font-bold { + font-weight: 700; + } + + .md\:font-semibold { + font-weight: 600; + } +} + +@media (min-width: 1024px) { + .lg\:relative { + position: relative; + } + + .lg\:left-\[unset\] { + left: unset; + } + + .lg\:right-0 { + right: 0px; + } + + .lg\:mb-0 { + margin-bottom: 0px; + } + + .lg\:mb-\[9px\] { + margin-bottom: 9px; + } + + .lg\:mr-2 { + margin-right: 0.5rem; + } + + .lg\:mt-0 { + margin-top: 0px; + } + + .lg\:mt-\[21px\] { + margin-top: 21px; + } + + .lg\:mt-\[50px\] { + margin-top: 50px; + } + + .lg\:block { + display: block; + } + + .lg\:inline { + display: inline; + } + + .lg\:flex { + display: flex; + } + + .lg\:hidden { + display: none; + } + + .lg\:h-\[167px\] { + height: 167px; + } + + .lg\:h-\[unset\] { + height: unset; + } + + .lg\:h-full { + height: 100%; + } + + .lg\:max-h-screen { + max-height: 100vh; + } + + .lg\:min-h-\[167px\] { + min-height: 167px; + } + + .lg\:\!w-1\/3 { + width: 33.333333% !important; + } + + .lg\:w-1\/2 { + width: 50%; + } + + .lg\:w-\[200px\] { + width: 200px; + } + + .lg\:w-\[262px\] { + width: 262px; + } + + .lg\:w-\[90\%\] { + width: 90%; + } + + .lg\:w-\[unset\] { + width: unset; + } + + .lg\:min-w-\[230px\] { + min-width: 230px; + } + + .lg\:min-w-\[370px\] { + min-width: 370px; + } + + .lg\:max-w-\[174px\] { + max-width: 174px; + } + + .lg\:max-w-\[230px\] { + max-width: 230px; + } + + .lg\:max-w-\[331px\] { + max-width: 331px; + } + + .lg\:max-w-\[413px\] { + max-width: 413px; + } + + .lg\:max-w-none { + max-width: none; + } + + .lg\:max-w-sm { + max-width: 24rem; + } + + .lg\:flex-grow { + flex-grow: 1; + } + + .lg\:flex-row { + flex-direction: row; + } + + .lg\:flex-col { + flex-direction: column; + } + + .lg\:flex-nowrap { + flex-wrap: nowrap; + } + + .lg\:items-start { + align-items: flex-start; + } + + .lg\:items-center { + align-items: center; + } + + .lg\:justify-start { + justify-content: flex-start; + } + + .lg\:justify-center { + justify-content: center; + } + + .lg\:justify-between { + justify-content: space-between; + } + + .lg\:gap-0 { + gap: 0px; + } + + .lg\:gap-16 { + gap: 4rem; + } + + .lg\:gap-\[32px\] { + gap: 32px; + } + + .lg\:space-y-0> :not([hidden])~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0px * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0px * var(--tw-space-y-reverse)); + } + + .lg\:overflow-y-auto { + overflow-y: auto; + } + + .lg\:rounded-none { + border-radius: 0px; + } + + .lg\:rounded-bl-none { + border-bottom-left-radius: 0px; + } + + .lg\:rounded-tl-none { + border-top-left-radius: 0px; + } + + .lg\:border-2 { + border-width: 2px; + } + + .lg\:border-b-2 { + border-bottom-width: 2px; + } + + .lg\:border-r { + border-right-width: 1px; + } + + .lg\:border-r-0 { + border-right-width: 0px; + } + + .lg\:px-8 { + padding-left: 2rem; + padding-right: 2rem; + } + + .lg\:py-0 { + padding-top: 0px; + padding-bottom: 0px; + } + + .lg\:pl-0 { + padding-left: 0px; + } + + .lg\:pr-2 { + padding-right: 0.5rem; + } + + .lg\:pr-8 { + padding-right: 2rem; + } + + .lg\:text-7xl { + font-size: 4rem; + } + + .lg\:text-xs { + font-size: 0.75rem; + } +} + +@media (min-width: 1280px) { + .xl\:bottom-\[unset\] { + bottom: unset; + } + + .xl\:top-16 { + top: 4rem; + } + + .xl\:ml-16 { + margin-left: 4rem; + } + + .xl\:block { + display: block; + } + + .xl\:hidden { + display: none; + } + + .xl\:\!w-1\/4 { + width: 25% !important; + } + + .xl\:w-1\/5 { + width: 20%; + } + + .xl\:w-3\/5 { + width: 60%; + } + + .xl\:w-4\/5 { + width: 80%; + } + + .xl\:w-\[unset\] { + width: unset; + } + + .xl\:min-w-\[190px\] { + min-width: 190px; + } + + .xl\:min-w-\[616px\] { + min-width: 616px; + } + + .xl\:max-w-3xl { + max-width: 48rem; + } + + .xl\:flex-row { + flex-direction: row; + } + + .xl\:gap-12 { + gap: 3rem; + } + + .xl\:gap-24 { + gap: 6rem; + } +} + +@media (min-width: 1536px) { + .\32xl\:block { + display: block; + } + + .\32xl\:gap-32 { + gap: 8rem; + } + + .\32xl\:px-16 { + padding-left: 4rem; + padding-right: 4rem; + } + + .\32xl\:px-32 { + padding-left: 8rem; + padding-right: 8rem; + } +} \ No newline at end of file diff --git a/src/pages/Admin/Addon/AddAdminAddOnPage.jsx b/src/pages/Admin/Addon/AddAdminAddOnPage.jsx new file mode 100644 index 0000000..3525ef6 --- /dev/null +++ b/src/pages/Admin/Addon/AddAdminAddOnPage.jsx @@ -0,0 +1,178 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate } from "react-router-dom"; +import { tokenExpireError, AuthContext } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import AddAdminPageLayout from "@/layouts/AddAdminPageLayout"; + +const AddAdminAddOnPage = () => { + let sdk = new MkdSDK(); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const [spaceCategories, setSpaceCategories] = React.useState([]); + const schema = yup + .object({ + name: yup.string().required("Name is required"), + cost: yup.number().required().typeError("Cost must be a number"), + space_id: yup.number().required().typeError("This field is required"), + }) + .required(); + + const { dispatch } = React.useContext(AuthContext); + + const navigate = useNavigate(); + const { + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + async function fetchSpaceCategories() { + try { + sdk.setTable("spaces"); + const result = await sdk.callRestAPI({}, "GETALL"); + if (Array.isArray(result.list)) { + setSpaceCategories(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + const onSubmit = async (data) => { + try { + sdk.setTable("add_on"); + + const result = await sdk.callRestAPI( + { + name: data.name, + cost: data.cost, + space_id: data.space_id || null, + }, + "POST", + ); + if (!result.error) { + showToast(globalDispatch, "Added"); + navigate("/admin/add_on"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("name", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "add_on", + }, + }); + fetchSpaceCategories(); + }, []); + + return ( + +
+
+ + +

{errors.name?.message}

+
+
+ + +

{errors.space_id?.message}

+
+ +
+ + +

{errors.cost?.message}

+
+
+ + +
+
+
+ ); +}; + +export default AddAdminAddOnPage; diff --git a/src/pages/Admin/Addon/AdminAddOnListPage.jsx b/src/pages/Admin/Addon/AdminAddOnListPage.jsx new file mode 100644 index 0000000..b698bcb --- /dev/null +++ b/src/pages/Admin/Addon/AdminAddOnListPage.jsx @@ -0,0 +1,351 @@ +import React from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { Link, useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import AddButton from "@/components/AddButton"; +import Button from "@/components/Button"; +import Table from "@/components/Table"; +import PaginationHeader from "@/components/PaginationHeader"; +import ReactHtmlTableToExcel from "react-html-table-to-excel"; +import { ID_PREFIX } from "@/utils/constants"; +import { adminColumns, applySetting } from "@/utils/adminPortalColumns"; +import TreeSDK from "@/utils/TreeSDK"; + +let sdk = new MkdSDK(); +let treeSdk = new TreeSDK(); + +const AdminAddOnListPage = () => { + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch, state } = React.useContext(GlobalContext); + const [tableColumns, setTableColumns] = React.useState([]); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_addon_filter") ?? ""); + const [spaceCategories, setSpaceCategories] = React.useState([]); + + const schema = yup.object({ + name: yup.string(), + }); + const { + register, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: parseSearchParams(searchParams), + }); + + function onSort(accessor) { + const columns = tableColumns; + const index = columns.findIndex((column) => column.accessor === accessor); + const column = columns[index]; + column.isSortedDesc = !column.isSortedDesc; + columns.splice(index, 1, column); + setTableColumns(() => [...columns]); + const sortedList = selector(data, column.isSortedDesc, accessor); + setCurrentTableData(sortedList); + } + function selector(users, isSortedDesc, accessor) { + if (accessor?.split(",").length > 1) { + accessor = accessor.split(",")[0]; + } + + return users.sort((a, b) => { + if (isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1; + } else { + return a[accessor] < b[accessor] ? 1 : -1; + } + } + if (!isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1; + } else { + return a[accessor] < b[accessor] ? -1 : 1; + } + } + }); + } + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum) { + const data = parseSearchParams(searchParams); + data.id = data.id?.replace(ID_PREFIX.ADDON_CATEGORY, ""); + + try { + let filter = ["ergo_add_on.deleted_at,is"]; + if (data.id) { + filter.push(`ergo_add_on.id,eq,${data.id}`); + } + if (data.name) { + filter.push(`name,cs,${data.name}`); + } + if (data.space_id) { + filter.push(`space_id,eq,${data.space_id}`); + } + + let result = await treeSdk.getPaginate("add_on", { + filter, + join: ["spaces|space_id"], + page: pageNum || 1, + size: limitNum, + order: "update_at", + }); + + const { list, total, limit, num_pages, page } = result; + + const sortedList = selector(list, false); + setCurrentTableData(sortedList); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + async function fetchSpaceCategories() { + try { + let filter = ["deleted_at,is"]; + const result = await treeSdk.getList("spaces", { + filter, + join: [], + }); + if (Array.isArray(result.list)) { + setSpaceCategories(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("name", data.name); + searchParams.set("space_id", data.space_id); + + setSearchParams(searchParams); + localStorage.setItem("admin_addons_filter", searchParams.toString()); + + getData(1, pageSize); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "add_on", + }, + }); + + (async function () { + await fetchColumnOrder(); + await fetchSpaceCategories(); + getData(1, pageSize); + })(); + }, []); + + React.useEffect(() => { + if (state.deleted) { + globalDispatch({ + type: "DELETED", + payload: { + deleted: false, + }, + }); + getData(currentPage, pageSize); + } + }, [state.deleted]); + + async function fetchColumnOrder() { + sdk.setTable("settings"); + const payload = { key_name: "admin_addon_categories_column_order" }; + try { + const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE"); + if (Array.isArray(result.list) && result.list.length > 0) { + setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_addon_categories)); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + return ( + <> +
+
+

Add-On Search

+ +
+ +
+
+ + +

{errors.id?.message}

+
+ +
+ + +

{errors.name?.message}

+
+ +
+ + +

{errors.space_id?.message}

+
+
+ +
+ + + +
+ + Change Column Order + {" "} + +
+ +
+
+ + + + + + ); +}; + +export default AdminAddOnListPage; diff --git a/src/pages/Admin/Addon/EditAdminAddOnPage.jsx b/src/pages/Admin/Addon/EditAdminAddOnPage.jsx new file mode 100644 index 0000000..9240393 --- /dev/null +++ b/src/pages/Admin/Addon/EditAdminAddOnPage.jsx @@ -0,0 +1,200 @@ +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import { useNavigate, useParams } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import EditAdminPageLayout from "@/layouts/EditAdminPageLayout"; + +let sdk = new MkdSDK(); + +const EditAdminAddOnPage = () => { + const [spaceCategories, setSpaceCategories] = React.useState([]); + const { dispatch } = React.useContext(AuthContext); + const schema = yup + .object({ + name: yup.string().required("Name is required"), + cost: yup.number().required().typeError("Cost must be a number"), + space_id: yup.number().nullable(), + }) + .required(); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const navigate = useNavigate(); + const [id, setId] = useState(0); + const { + register, + handleSubmit, + setError, + setValue, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const params = useParams(); + + useEffect(function () { + (async function () { + try { + sdk.setTable("add_on"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + if (!result.error) { + setValue("name", result.model.name); + setValue("cost", result.model.cost); + setValue("space_id", result.model.space_id); + setId(result.model.id); + } + } catch (error) { + console.log("error", error); + tokenExpireError(dispatch, error.message); + } + })(); + }, []); + + async function fetchSpaceCategories() { + try { + sdk.setTable("spaces"); + const result = await sdk.callRestAPI({}, "GETALL"); + if (Array.isArray(result.list)) { + setSpaceCategories(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + const onSubmit = async (data) => { + sdk.setTable("add_on"); + try { + const result = await sdk.callRestAPI( + { + id: id, + name: data.name, + cost: data.cost, + space_id: data.space_id, + }, + "PUT", + ); + + if (!result.error) { + showToast(globalDispatch, "Updated"); + navigate("/admin/add_on"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("name", { + type: "manual", + message: error.message, + }); + } + }; + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "add_on", + }, + }); + fetchSpaceCategories(); + }, []); + + return ( + +
+
+ + +

{errors.name?.message}

+
+ +
+ + +

{errors.space_id?.message}

+
+ +
+ + +

{errors.cost?.message}

+
+ +
+ + +
+ +
+ ); +}; + +export default EditAdminAddOnPage; diff --git a/src/pages/Admin/AdminColumnOrderPage.jsx b/src/pages/Admin/AdminColumnOrderPage.jsx new file mode 100644 index 0000000..6c4b12f --- /dev/null +++ b/src/pages/Admin/AdminColumnOrderPage.jsx @@ -0,0 +1,192 @@ +import PencilIcon from "@/components/frontend/icons/PencilIcon"; +import Icon from "@/components/Icons"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { parseJsonSafely } from "@/utils/utils"; +import React, { useEffect, useState } from "react"; +import { useContext } from "react"; +import { useNavigate, useParams } from "react-router"; + +const AdminColumnOrderPage = () => { + const { sectionId } = useParams(); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const [columns, setColumns] = useState([]); + const [settingId, setSettingId] = useState(null); + const navigate = useNavigate(); + const sdk = new MkdSDK(); + + const sortByOrderNumber = (a, b) => { + return a.orderNumber - b.orderNumber; + }; + + async function fetchSetting() { + sdk.setTable("settings"); + const payload = { key_name: `admin_${sectionId}_column_order` }; + try { + const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE"); + if (Array.isArray(result.list) && result.list.length > 0) { + setColumns(parseJsonSafely(result.list[0].optional_data, [])); + console.log(parseJsonSafely(result.list[0].optional_data, [])); + setSettingId(result.list[0].id); + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + const saveOrder = async () => { + sdk.setTable("settings"); + try { + await sdk.callRestAPI( + { + id: settingId, + optional_data: JSON.stringify(columns), + }, + "PUT", + ); + navigate(-1); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + }; + + const changeOrder = (idx, newOrder) => { + if (newOrder < 1) return; + setColumns((prev) => { + const copy = [...prev]; + // find max orderNumber + let maxOrderNum = prev.reduce((acc, curr) => { + if (curr.orderNumber > acc) return curr.orderNumber; + return acc; + }, 0); + + // find column with newOrder + prev.forEach((col, j) => { + if (col.orderNumber == newOrder) { + copy[j].orderNumber = prev[idx].orderNumber; + } + }); + + if (newOrder >= maxOrderNum) { + copy[prev.length - 1].orderNumber = prev[idx].orderNumber; + copy[idx].orderNumber = maxOrderNum; + return copy; + } + copy[idx].orderNumber = newOrder; + return copy; + }); + }; + + const changeShouldDisplay = (idx, newValue) => { + setColumns((prev) => { + let copy = [...prev]; + copy[idx].shouldShow = newValue; + return copy; + }); + }; + + useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: sectionId, + }, + }); + fetchSetting(); + }, []); + + return ( +
+
+ +
+

{sectionId.replace(/([-_]\w)/g, (g) => " " + g[1].toUpperCase())}

+
+ + + + + + + + + {columns.sort(sortByOrderNumber).map((col, idx) => ( + + + + + + ))} + +
Column NameOrder NumberShould Display
{col.header} + + {col.orderNumber}{" "} + + + + {col.shouldShow ? "Yes" : "No"} + changeShouldDisplay(idx, e.target.checked)} + /> +
+
+ +
+
+ ); +}; + +export default AdminColumnOrderPage; diff --git a/src/pages/Admin/AdminDashboardPage.jsx b/src/pages/Admin/AdminDashboardPage.jsx new file mode 100644 index 0000000..6e1cd1c --- /dev/null +++ b/src/pages/Admin/AdminDashboardPage.jsx @@ -0,0 +1,107 @@ +import React, { useEffect } from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import { useState } from "react"; +import { BOOKING_STATUS } from "@/utils/constants"; +import MkdSDK from "@/utils/MkdSDK"; +import TreeSDK from "@/utils/TreeSDK"; + +const AdminDashboardPage = () => { + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const [totalUsers, setTotalUsers] = useState([]); + const [totalBookings, setTotalBookings] = useState([]); + const sdk = new MkdSDK(); + const treeSdk = new TreeSDK(); + + async function fetchUsers() { + try { + const result = await treeSdk.getList("user", { filter: ["deleted_at,is"], join: [] }); + if (Array.isArray(result.list)) { + setTotalUsers(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + async function fetchBookings() { + sdk.setTable("booking"); + try { + const result = await sdk.callRestAPI({}, "GETALL"); + if (Array.isArray(result.list)) { + setTotalBookings(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "dashboard", + }, + }); + + (async () => { + await fetchUsers(); + await fetchBookings(); + })(); + }, []); + + const hostCount = totalUsers.reduce((acc, curr) => acc + (curr.role == "host" ? 1 : 0), 0); + const customerCount = totalUsers.reduce((acc, curr) => acc + (curr.role == "customer" ? 1 : 0), 0); + + const ongoingBookingCount = totalBookings.reduce((acc, curr) => acc + (curr.status == BOOKING_STATUS.ONGOING ? 1 : 0), 0); + const upcomingBookingCount = totalUsers.reduce((acc, curr) => acc + (curr.status == BOOKING_STATUS.UPCOMING ? 1 : 0), 0); + + return ( + <> +
+

Stats

+

Users

+
+
+
Hosts
+

+ {hostCount} +

+
+
+
Customers
+

+ {customerCount} +

+
+
+

Bookings

+
+
+
Active Bookings
+

+ {ongoingBookingCount} +

+
+
+
Upcoming Bookings
+

+ {upcomingBookingCount} +

+
+
+
Total Bookings
+

+ {totalBookings.length} +

+
+
+
+ + ); +}; + +export default AdminDashboardPage; diff --git a/src/pages/Admin/AdminProfilePage.jsx b/src/pages/Admin/AdminProfilePage.jsx new file mode 100644 index 0000000..dafc8ba --- /dev/null +++ b/src/pages/Admin/AdminProfilePage.jsx @@ -0,0 +1,362 @@ +import React, { useState } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import { tokenExpireError, AuthContext } from "@/authContext"; +import Icon from "@/components/Icons"; +import { useNavigate } from "react-router"; + +let sdk = new MkdSDK(); + +const AdminProfilePage = () => { + const schema = yup + .object({ + email: yup.string().email().required(), + }) + .required(); + + const { dispatch, state } = React.useContext(AuthContext); + const [oldEmail, setOldEmail] = useState(""); + const [userId, setUserId] = useState(); + const [profile, setProfile] = useState(); + const [edit, setEdit] = useState(false); + const [activeTab, setActiveTab] = useState(0); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const { + register, + handleSubmit, + setError, + setValue, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "profile", + }, + }); + + (async function () { + try { + const result = await sdk.getProfile(); + setProfile(result); + setValue("email", result.email); + setValue("first_name", result.first_name); + setValue("last_name", result.last_name); + setOldEmail(result.email); + } catch (error) { + console.log("Error", error); + tokenExpireError(dispatch, error.message); + } + })(); + }, []); + + const onSubmit = async (data) => { + try { + sdk.setTable("user"); + await sdk.callRestAPI( + { + id: state.user, + first_name: data.first_name, + last_name: data.last_name, + email: data.email, + }, + "PUT", + ); + + showToast(globalDispatch, "Profile updated Successfully"); + setProfile({ ...profile, first_name: data.first_name, last_name: data.last_name, email: data.email }); + } catch (error) { + console.log("Error", error); + setError("email", { + type: "manual", + message: error.message, + }); + tokenExpireError(globalDispatch, error.message); + } + }; + + const tabs = [ + { + key: 0, + name: "Profile", + component: !edit ? ( + + ) : ( + + ), + }, + { + key: 1, + name: "Password", + component: , + }, + ]; + + return ( + <> +
+
+
+
+

Profile

+
+
+
+
    + {tabs.map((tab) => ( +
  • + +
  • + ))} +
+
+ {tabs[activeTab].component} +
+
+ + ); +}; + +const ViewProfilePage = ({ profileInfo, setEdit }) => { + return ( +
+
+
+
+ +
+
+
+

First name

+

{profileInfo?.first_name}

+
+
+

Last name

+

{profileInfo?.last_name}

+
+
+

Email

+

{profileInfo?.email}

+
+
+
+ ); +}; + +const EditProfilePage = ({ register, onSubmit, handleSubmit, errors, setEdit }) => { + return ( +
+
+
+ +
+
+
+
+ + +

{errors.first_name?.message}

+
+
+ + +

{errors.last_name?.message}

+
+
+ + +

{errors.email?.message}

+
+
+ +
+
+
+ ); +}; + +const EditPasswordPage = () => { + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const navigate = useNavigate(); + const schema = yup + .object({ + current_password: yup.string().required(), + new_password: yup.string().required(), + confirm_password: yup.string().oneOf([yup.ref("new_password"), null], "Passwords must match"), + }) + .required(); + const { + register, + handleSubmit, + setError, + setValue, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const onSubmit = async (data) => { + try { + if (data.new_password.length > 0 && data.current_password.length > 0) { + const passwordresult = await sdk.updatePassword({ + currentPassword: data.current_password, + password: data.new_password, + }); + if (!passwordresult.error) { + showToast(globalDispatch, "Password Updated", 2000); + } else { + if (passwordresult.validation) { + const keys = Object.keys(passwordresult.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: passwordresult.validation[field], + }); + } + } + } + } + } catch (error) { + console.log("Error", error); + setError("email", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + return ( +
+

Enter your current password to change your password.

+
+
+ + +

{errors.current_password?.message}

+
+
+
+ + +

{errors.new_password?.message}

+
+
+ + +

{errors.confirm_password?.message}

+
+
+ +
+
+
+ ); +}; + +export default AdminProfilePage; diff --git a/src/pages/Admin/AdminReportsPage.jsx b/src/pages/Admin/AdminReportsPage.jsx new file mode 100644 index 0000000..2c9ded7 --- /dev/null +++ b/src/pages/Admin/AdminReportsPage.jsx @@ -0,0 +1,329 @@ +import React from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import { useForm } from "react-hook-form"; +import { Link, useNavigate } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import * as yup from "yup"; +import { yupResolver } from "@hookform/resolvers/yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import countries from "@/utils/countries.json"; +import ReactHtmlTableToExcel from "react-html-table-to-excel"; +import { adminColumns, applySetting } from "@/utils/adminPortalColumns"; + +const monthMapping = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; +let sdk = new MkdSDK(); +const bookingStatusMapping = ["Pending", "Upcoming", "Ongoing", "Completed", "Declined", "Cancelled"]; + +const AdminReportsPage = () => { + const { dispatch } = React.useContext(AuthContext); + const [columns, setColumns] = React.useState([]); + const [rows, setRows] = React.useState([]); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const [bookingColumns, setBookingColumns] = React.useState([]); + const [analyticColumns, setAnalyticColumns] = React.useState([]); + const [heading, setHeading] = React.useState([]); + + const schema = yup.object({ + start_date: yup.string(), + end_date: yup.string(), + report_type: yup.string(), + status: yup.string(), + }); + + const { + register, + handleSubmit, + watch, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const reportType = watch("report_type"); + + const onSubmit = (data) => { + console.log("submitting", data); + switch (data.report_type) { + case "bookings": + setHeading("Bookings"); + fetchBookingRows(data.start_date, data.end_date, data.status); + setColumns(bookingColumns); + break; + case "analytics": + setHeading("Analytics"); + fetchAnalyticRows(data.start_date, data.end_date); + setColumns(analyticColumns); + break; + default: + setHeading(""); + setColumns([]); + setRows([]); + } + }; + + async function fetchBookingRows(start, end, status) { + const where = [ + `${start && end ? `ergo_booking.create_at BETWEEN '${start}' AND '${end} 23:59:59'` : "1"} AND ${status ? `ergo_booking.status = ${status}` : "1"}`, + "ergo_booking.deleted_at IS NULL", + ]; + console.log("where", where); + try { + const result = await callCustomAPI("booking", "post", { where, page: 1, limit: 100000 }, "PAGINATE"); + if (Array.isArray(result.list)) { + setRows(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + async function fetchAnalyticRows(start, end) { + sdk.setTable("analytic_log"); + // TODO: need solution here + // const payload = [`ergo_analytic_log.create_at BETWEEN '${start}' AND '${end} 23:59:59' AND hostname = '${window.location.origin + "/"}' `]; + try { + const result = await sdk.callRestAPI({ payload: {} }, "GETALL"); + if (Array.isArray(result.list)) { + setRows(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "reports", + }, + }); + fetchBookingColumnOrder(); + fetchAnalyticColumnOrder(); + }, []); + + async function fetchBookingColumnOrder() { + sdk.setTable("settings"); + try { + const result = await sdk.callRestAPI({ limit: 1, page: 1, payload: { key_name: "admin_booking_reports_column_order" } }, "PAGINATE"); + if (Array.isArray(result.list) && result.list.length > 0) { + setBookingColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_booking_report)); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + async function fetchAnalyticColumnOrder() { + sdk.setTable("settings"); + try { + const result = await sdk.callRestAPI({ limit: 1, page: 1, payload: { key_name: "admin_analytics_column_order" } }, "PAGINATE"); + if (Array.isArray(result.list) && result.list.length > 0) { + setAnalyticColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_analytics)); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + return ( + <> +
+
+

Reports

+
+ +
+
+ + +

{errors.start_date?.message}

+
+ +
+ + +

{errors.end_date?.message}

+
+ +
+ + +

{errors.report_type?.message}

+
+ {reportType == "bookings" && ( +
+ + +

{errors.end_date?.message}

+
+ )} +
+ +
+
+

{heading}

+
+ + Change Column Order + + +
+
+
+
+ + + + {columns.map((column, index) => ( + + ))} + + + + {rows.map((row, i) => { + return ( + + {columns.map((cell, index) => { + if (cell.mapping) { + return ( + + ); + } + if (cell.formatDate) { + var date = new Date(row[cell.accessor]); + return ( + + ); + } + if (cell.isCountry) { + return ( + + ); + } + + if (cell.idPrefix) { + return ( + + ); + } + + if (cell.joinFields) { + let [field_1, field_2] = cell.accessor.split(","); + console.log(cell.accessor.split(",")); + return ( + + ); + } + + return ( + + ); + })} + + ); + })} + +
+ {column.header} +
+ {cell.mapping[row[cell.accessor]]} + + {monthMapping[date.getMonth()] + " " + date.getDate() + "/" + date.getFullYear()} + + {countries.find((country) => country.code == row[cell.accessor])?.name} + + {cell.idPrefix + row[cell.accessor]} + + {row[field_1] + " " + row[field_2?.trim()]} + + {row[cell.accessor]} +
+
+
+ + ); +}; + +export default AdminReportsPage; diff --git a/src/pages/Admin/Amenity/AddAdminAmenityPage.jsx b/src/pages/Admin/Amenity/AddAdminAmenityPage.jsx new file mode 100644 index 0000000..514907b --- /dev/null +++ b/src/pages/Admin/Amenity/AddAdminAmenityPage.jsx @@ -0,0 +1,162 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate } from "react-router-dom"; +import { tokenExpireError, AuthContext } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import AddAdminPageLayout from "@/layouts/AddAdminPageLayout"; +let sdk = new MkdSDK(); + +const AddAdminAmenityPage = () => { + const [spaceCategories, setSpaceCategories] = React.useState([]); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const schema = yup + .object({ + name: yup.string().required("Category is required"), + space_id: yup.number().required().typeError("This field is required"), + }) + .required(); + + const { dispatch } = React.useContext(AuthContext); + + const navigate = useNavigate(); + const { + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + async function fetchSpaceCategories() { + try { + sdk.setTable("spaces"); + const result = await sdk.callRestAPI({}, "GETALL"); + if (Array.isArray(result.list)) { + setSpaceCategories(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + const onSubmit = async (data) => { + try { + sdk.setTable("amenity"); + + const result = await sdk.callRestAPI( + { + name: data.name, + space_id: data.space_id || null, + }, + "POST", + ); + if (!result.error) { + showToast(globalDispatch, "Added"); + navigate("/admin/amenity"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("name", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "amenity", + }, + }); + fetchSpaceCategories(); + }, []); + + return ( + +
+
+ + +

{errors.name?.message}

+
+ +
+ + +

{errors.space_id?.message}

+
+ +
+ + +
+
+
+ ); +}; + +export default AddAdminAmenityPage; diff --git a/src/pages/Admin/Amenity/AdminAmenityListPage.jsx b/src/pages/Admin/Amenity/AdminAmenityListPage.jsx new file mode 100644 index 0000000..1df074f --- /dev/null +++ b/src/pages/Admin/Amenity/AdminAmenityListPage.jsx @@ -0,0 +1,349 @@ +import React from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { clearSearchParams, getNonNullValue, parseSearchParams } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import AddButton from "@/components/AddButton"; +import Button from "@/components/Button"; +import Table from "@/components/Table"; +import PaginationHeader from "@/components/PaginationHeader"; +import ReactHtmlTableToExcel from "react-html-table-to-excel"; +import { ID_PREFIX } from "@/utils/constants"; +import { adminColumns, applySetting } from "@/utils/adminPortalColumns"; +import TreeSDK from "@/utils/TreeSDK"; + +let sdk = new MkdSDK(); +let treeSdk = new TreeSDK(); + +const AdminAmenityListPage = () => { + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch, state } = React.useContext(GlobalContext); + const [tableColumns, setTableColumns] = React.useState([]); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_amenity_filter") ?? ""); + const [spaceCategories, setSpaceCategories] = React.useState([]); + + const schema = yup.object({ + name: yup.string(), + }); + const { + reset, + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: parseSearchParams(searchParams), + }); + + function onSort(accessor) { + const columns = tableColumns; + const index = columns.findIndex((column) => column.accessor === accessor); + const column = columns[index]; + column.isSortedDesc = !column.isSortedDesc; + columns.splice(index, 1, column); + setTableColumns(() => [...columns]); + const sortedList = selector(data, column.isSortedDesc, accessor); + setCurrentTableData(sortedList); + } + function selector(users, isSortedDesc, accessor) { + if (accessor?.split(",").length > 1) { + accessor = accessor.split(",")[0]; + } + + return users.sort((a, b) => { + if (isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1; + } else { + return a[accessor] < b[accessor] ? 1 : -1; + } + } + if (!isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1; + } else { + return a[accessor] < b[accessor] ? -1 : 1; + } + } + }); + } + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum) { + const data = parseSearchParams(searchParams); + data.id = data.id?.replace(ID_PREFIX.AMENITY_CATEGORY, ""); + + try { + let filter = ["ergo_amenity.deleted_at,is"]; + if (data.id) { + filter.push(`ergo_amenity.id,eq,${data.id}`); + } + if (data.name) { + filter.push(`name,cs,${data.name}`); + } + if (data.space_id) { + filter.push(`space_id,eq,${data.space_id}`); + } + + + let result = await treeSdk.getPaginate("amenity", { + filter, + join: ["spaces|space_id"], + page: pageNum || 1, + size: limitNum, + order: "update_at", + }); + const { list, total, limit, num_pages, page } = result; + + const sortedList = selector(list, false); + setCurrentTableData(sortedList); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + async function fetchSpaceCategories() { + try { + let filter = ["deleted_at,is"]; + const result = await treeSdk.getList("spaces", { + filter, + join: [], + }); + if (Array.isArray(result.list)) { + setSpaceCategories(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("name", data.name); + searchParams.set("space_id", data.space_id); + + setSearchParams(searchParams); + localStorage.setItem("admin_amenity_filter", searchParams.toString()); + + getData(1, pageSize); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "amenity", + }, + }); + + (async function () { + await fetchColumnOrder(); + await fetchSpaceCategories(); + getData(1, pageSize); + })(); + }, []); + + React.useEffect(() => { + if (state.deleted) { + globalDispatch({ + type: "DELETED", + payload: { + deleted: false, + }, + }); + getData(currentPage, pageSize); + } + }, [state.deleted]); + + async function fetchColumnOrder() { + sdk.setTable("settings"); + const payload = { key_name: "admin_amenity_categories_column_order" }; + try { + const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE"); + if (Array.isArray(result.list) && result.list.length > 0) { + setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_amenity_categories)); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + return ( + <> +
+
+

Amenity Search

+ +
+ +
+
+ + +

{errors.id?.message}

+
+ +
+ + +

{errors.name?.message}

+
+
+ + +

{errors.space_id?.message}

+
+
+ +
+ + +
+ + Change Column Order + {" "} + +
+ +
+
+ + + + + + ); +}; + +export default AdminAmenityListPage; diff --git a/src/pages/Admin/Amenity/EditAdminAmenityPage.jsx b/src/pages/Admin/Amenity/EditAdminAmenityPage.jsx new file mode 100644 index 0000000..28bc740 --- /dev/null +++ b/src/pages/Admin/Amenity/EditAdminAmenityPage.jsx @@ -0,0 +1,183 @@ +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import { useNavigate, useParams } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import EditAdminPageLayout from "@/layouts/EditAdminPageLayout"; + +let sdk = new MkdSDK(); + +const EditAdminAmenityPage = () => { + const [spaceCategories, setSpaceCategories] = React.useState([]); + const { dispatch } = React.useContext(AuthContext); + const schema = yup + .object({ + name: yup.string().required("Category is required"), + space_id: yup.number().nullable(), + }) + .required(); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const navigate = useNavigate(); + const [name, setName] = useState(""); + const [id, setId] = useState(0); + const { + register, + handleSubmit, + setError, + setValue, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const params = useParams(); + + useEffect(function () { + (async function () { + try { + sdk.setTable("amenity"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + if (!result.error) { + setValue("name", result.model.name); + setId(result.model.id); + setValue("space_id", result.model.space_id); + } + } catch (error) { + console.log("error", error); + tokenExpireError(dispatch, error.message); + } + })(); + }, []); + + async function fetchSpaceCategories() { + try { + sdk.setTable("spaces"); + const result = await sdk.callRestAPI({}, "GETALL"); + if (Array.isArray(result.list)) { + setSpaceCategories(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + const onSubmit = async (data) => { + try { + sdk.setTable("amenity"); + const result = await sdk.callRestAPI( + { + id: id, + name: data.name, + space_id: data.space_id, + }, + "PUT", + ); + + if (!result.error) { + showToast(globalDispatch, "Updated"); + navigate("/admin/amenity"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("name", { + type: "manual", + message: error.message, + }); + } + }; + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "amenity", + }, + }); + fetchSpaceCategories(); + }, []); + + return ( + +
+
+ + +

{errors.name?.message}

+
+ +
+ + +

{errors.space_id?.message}

+
+ +
+ + +
+ +
+ ); +}; + +export default EditAdminAmenityPage; diff --git a/src/pages/Admin/Auth/AdminForgotPage.jsx b/src/pages/Admin/Auth/AdminForgotPage.jsx new file mode 100644 index 0000000..091a5ff --- /dev/null +++ b/src/pages/Admin/Auth/AdminForgotPage.jsx @@ -0,0 +1,100 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { Link, useNavigate } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; + +const AdminForgotPage = () => { + const navigate = useNavigate(); + const schema = yup + .object({ + email: yup.string().email().required(), + }) + .required(); + + const { + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const { dispatch } = React.useContext(GlobalContext); + + const onSubmit = async (data) => { + let sdk = new MkdSDK(); + try { + const result = await sdk.forgot(data.email); + if (!result.error) { + showToast(dispatch, "Reset Code Sent"); + localStorage.setItem("token", result.token); + navigate("/admin/reset"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("email", { + type: "manual", + message: error.message, + }); + } + }; + + return ( + <> +
+
+
+ + +

{errors.email?.message}

+
+ +
+ + + Login? + +
+ +

© {new Date().getFullYear()} manaknightdigital inc. All rights reserved.

+
+ + ); +}; + +export default AdminForgotPage; diff --git a/src/pages/Admin/Auth/AdminLoginPage.jsx b/src/pages/Admin/Auth/AdminLoginPage.jsx new file mode 100644 index 0000000..94f023d --- /dev/null +++ b/src/pages/Admin/Auth/AdminLoginPage.jsx @@ -0,0 +1,119 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { Link, useSearchParams } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; +import { AuthContext } from "@/authContext"; + +const AdminLoginPage = () => { + const [searchParams] = useSearchParams(); + + const schema = yup + .object({ + email: yup.string().email().required(), + password: yup.string().required(), + }) + .required(); + + const { dispatch } = React.useContext(AuthContext); + const navigate = useNavigate(); + const { + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const onSubmit = async (data) => { + let sdk = new MkdSDK(); + try { + const result = await sdk.customLogin({ email: data.email, password: data.password, role: "admin" }); + if (result.role != "admin") throw new Error("This user is not an admin"); + if (!result.error) { + dispatch({ + type: "LOGIN", + payload: { ...result, originalRole: "admin" }, + }); + navigate(searchParams.get("redirect_uri") ?? "/admin/dashboard"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("email", { + type: "manual", + message: error.message, + }); + } + }; + + return ( +
+
+
+ + +

{errors.email?.message}

+
+ +
+ + +

{errors.password?.message}

+
+
+ + + Forgot Password? + +
+ +

© {new Date().getFullYear()} manaknightdigital inc. All rights reserved.

+
+ ); +}; + +export default AdminLoginPage; diff --git a/src/pages/Admin/Auth/AdminResetPage.jsx b/src/pages/Admin/Auth/AdminResetPage.jsx new file mode 100644 index 0000000..cdc4f0b --- /dev/null +++ b/src/pages/Admin/Auth/AdminResetPage.jsx @@ -0,0 +1,141 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { Link } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; +import { AuthContext } from "@/authContext"; +import { showToast, GlobalContext } from "@/globalContext"; + +const AdminResetPage = () => { + const { dispatch } = React.useContext(AuthContext); + const search = window.location.search; + const params = new URLSearchParams(search); + const token = localStorage.getItem("token"); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + + const schema = yup + .object({ + code: yup.string().required(), + password: yup.string().required(), + confirmPassword: yup.string().oneOf([yup.ref("password"), null], "Passwords must match"), + }) + .required(); + + const navigate = useNavigate(); + + const { + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const onSubmit = async (data) => { + let sdk = new MkdSDK(); + try { + const result = await sdk.reset(token, data.code, data.password); + if (!result.error) { + showToast(globalDispatch, "Password Reset"); + setTimeout(() => { + navigate("/admin/login"); + }, 2000); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("code", { + type: "manual", + message: error.message, + }); + } + }; + + return ( + <> +
+
+
+ + +

{errors.code?.message}

+
+
+ + +

{errors.password?.message}

+
+
+ + +

{errors.confirmPassword?.message}

+
+
+ + + Login? + +
+ +

© {new Date().getFullYear()} manaknightdigital inc. All rights reserved.

+
+ + ); +}; + +export default AdminResetPage; diff --git a/src/pages/Admin/Booking/AddAdminBookingPage.jsx b/src/pages/Admin/Booking/AddAdminBookingPage.jsx new file mode 100644 index 0000000..7f64b8d --- /dev/null +++ b/src/pages/Admin/Booking/AddAdminBookingPage.jsx @@ -0,0 +1,514 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate } from "react-router-dom"; +import { tokenExpireError, AuthContext } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import AddAdminPageLayout from "@/layouts/AddAdminPageLayout"; +import { addHours } from "@/utils/utils"; +import SmartSearch from "@/components/SmartSearch"; +import { useEffect } from "react"; +import TreeSDK from "@/utils/TreeSDK"; + +const treeSdk = new TreeSDK(); +const AddAdminBookingPage = () => { + let sdk = new MkdSDK(); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const [settings, setSettings] = React.useState([]); + + const [selectedSpace, setSelectedSpace] = React.useState(); + const [propertySpaces, setPropertySpaces] = React.useState([]); + + const [selectedCustomer, setSelectedCustomer] = React.useState({}); + const [customers, setCustomers] = React.useState([]); + + const [selectedHost, setSelectedHost] = React.useState({}); + + const schema = yup + .object({ + status: yup.number().required().typeError("This field is required"), + payment_status: yup.number().required().typeError("This field is required"), + // booked_unit: yup.number().required().positive().integer().typeError("Booked unit must be a number"), + // payment_method: yup.string(), + booking_date: yup + .string() + .test("is-not-in-past", "Not a valid booking date", (val) => { + const date = new Date(val); + return date.setDate(date.getDate() + 1) > new Date(); + }) + .required("Booking date is required"), + booking_time: yup.string().required("Booking time is required"), + duration: yup.number().required().positive().integer().typeError("Duration must be a number"), + host_id: yup.string().required("Host is required"), + }) + .required(); + + const { dispatch } = React.useContext(AuthContext); + + const navigate = useNavigate(); + const { + register, + handleSubmit, + setError, + setValue, + formState: { errors, isSubmitting, isValidating }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const selectStatus = [ + { key: "0", value: "Pending" }, + { key: "1", value: "Upcoming" }, + { key: "2", value: "Ongoing" }, + { key: "3", value: "Complete" }, + { key: "4", value: "Declined" }, + { key: "5", value: "Cancelled" }, + ]; + + const selectPaymentStatus = [ + { key: "0", value: "Pending" }, + { key: "1", value: "Paid" }, + { key: "2", value: "Declined" }, + { key: "3", value: "Cancelled" }, + ]; + + async function getSettings() { + try { + sdk.setTable("settings"); + // TODO: figure out a solution here for OR operation + const result = await sdk.callRestAPI( + { + page: 1, + limit: 2, + }, + "PAGINATE", + ); + const { list } = result; + setSettings(list); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + + async function getCustomerData(pageNum, limitNum, data) { + try { + let filter = ["deleted_at,is"]; + if (data.email) { + filter.push(`email,cs,${data.email}`); + } + const result = await treeSdk.getList("user", { join: [], filter }); + const { list } = result; + setCustomers(list); + } catch (error) { + tokenExpireError(dispatch, error.message); + } + } + + async function getHostData(pageNum, limitNum, data) { + try { + sdk.setTable("user"); + const payload = { id: data.id || undefined, role: "host" }; + const result = await sdk.callRestAPI( + { + payload, + page: pageNum, + limit: limitNum, + }, + "PAGINATE", + ); + const { list } = result; + setSelectedHost(list[0]); + setValue("host_id", list[0].email); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + + async function getPropertySpaceData(pageNum, limit, data) { + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/property-spaces/PAGINATE", + { + where: [data?.property_name ? `ergo_property.name LIKE '%${data.property_name}%' OR ergo_spaces.category LIKE '%${data.property_name}%'` : 1, "ergo_property_spaces.deleted_at IS NULL"], + page: pageNum, + limit: limit, + }, + "POST", + ); + const { list } = result; + setPropertySpaces(list); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + + const onSubmit = async (data) => { + console.log("submitting", data); + if (selectedCustomer?.id && selectedHost?.id && selectedSpace?.id) { + data.customer_id = selectedCustomer.id; + data.host_id = selectedHost.id; + data.property_space_id = selectedSpace.id; + try { + let bookingStartTime = new Date(`${data.booking_date} ${data.booking_time}`); + let bookingEndTime = addHours(data.duration, bookingStartTime); + data.duration = data.duration * 60 * 60; + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/booking/POST", + { + property_space_id: data.property_space_id, + customer_id: data.customer_id, + host_id: data.host_id, + booked_unit: 1, + payment_method: 1, + status: data.status, + payment_status: data.payment_status, + booking_start_time: bookingStartTime.toISOString(), + booking_end_time: bookingEndTime.toISOString(), + duration: data.duration, + tax_rate: settings.find((setting) => setting.key_name === "tax")?.key_value, + commission_rate: settings.find((setting) => setting.key_name === "commission")?.key_value, + }, + "POST", + ); + // create payout is status = 3 + if (data.status == 3) { + sdk.setTable("booking"); + const newBookingResult = await sdk.callRawAPI( + "/v2/api/custom/ergo/booking/details", + { + where: [`ergo_booking.id=${result.message}`], + }, + "POST", + ); + console.log("newBookingResult", newBookingResult); + const payoutResult = await sdk.callRawAPI( + "/v2/api/custom/ergo/payout/POST", + { + initiated_at: bookingStartTime.toISOString(), + host_id: data.host_id, + customer_id: data.customer_id, + property_space_id: data.property_space_id, + total: newBookingResult.list.total + newBookingResult.list.addon_cost, + tax: settings.find((setting) => setting.key_name === "tax").key_value, + commission: settings.find((setting) => setting.key_name === "commission").key_value, + booking_id: result.message, + status: 0, + }, + "POST", + ); + console.log("payoutResult", payoutResult); + } + + if (!result.error) { + showToast(globalDispatch, "Added"); + navigate("/admin/booking"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + showToast(globalDispatch, error.message); + tokenExpireError(dispatch, error.message); + } + } else { + if (!selectedCustomer) { + setError("customer_id", { + type: "manual", + message: "Please select a customer", + }); + } + if (!selectedHost) { + setError("host_id", { + type: "manual", + message: "Please select a host", + }); + } + if (!selectedSpace) { + setError("property_space_id", { + type: "manual", + message: "Please select a property space", + }); + } + } + }; + + const onError = () => { + if (!selectedCustomer?.id) { + setError("customer_id", { + type: "manual", + message: "Please select a customer", + }); + } + if (!selectedHost?.id) { + setError("host_id", { + type: "manual", + message: "Please select a host", + }); + } + if (!selectedSpace?.id) { + setError("property_space_id", { + type: "manual", + message: "Please select a property space", + }); + } + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "booking", + }, + }); + (async function () { + await getSettings(); + await getCustomerData(); + await getPropertySpaceData(1, 10, { property_name: "" }); + })(); + }, []); + + // set host automatically + useEffect(() => { + console.log("selectedSpace", selectedSpace); + if (!selectedSpace?.property_id) return; + (async function () { + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/property/PAGINATE", + { + where: [selectedSpace ? `${selectedSpace.property_id ? `ergo_property.id = '${selectedSpace.property_id}'` : "1"} ` : 1], + page: 1, + limit: 1, + }, + "POST", + ); + console.log("result", result.list[0].host_id); + await getHostData(1, 1, { id: result.list[0].host_id }); + } catch (err) { + console.log("err", err); + } + })(); + }, [selectedSpace]); + + return ( + +
+
+ + +

{errors.property_space_id?.message}

+
+ +
+ + +

{errors.customer_id?.message}

+
+ +
+ + +

{errors.host_id?.message}

+
+ +
+ + +

{errors.status?.message}

+
+ +
+ + +

{errors.payment_status?.message}

+
+ {/*
+ + +

{errors.payment_method?.message}

+
*/} + +
+ + +

{errors.booking_date?.message}

+
+
+ + +

{errors.booking_time?.message}

+
+ +
+ + +

{errors.duration?.message}

+
+
+ + +
+ +
+ ); +}; + +export default AddAdminBookingPage; diff --git a/src/pages/Admin/Booking/AdminBookingListPage.jsx b/src/pages/Admin/Booking/AdminBookingListPage.jsx new file mode 100644 index 0000000..be13f92 --- /dev/null +++ b/src/pages/Admin/Booking/AdminBookingListPage.jsx @@ -0,0 +1,511 @@ +import React, { Fragment } from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { clearSearchParams, parseSearchParams, secondsToHour } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import AddButton from "@/components/AddButton"; +import Button from "@/components/Button"; +import { Menu, Transition } from "@headlessui/react"; +import Icon from "@/components/Icons"; +import moment from "moment"; +import SmartSearch from "@/components/SmartSearch"; +import CsvDownloadButton from "react-json-to-csv"; +import { ID_PREFIX } from "@/utils/constants"; + +let sdk = new MkdSDK(); + +const AdminBookingListPage = () => { + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + const [resetClicked, setResetClicked] = React.useState(false); + const [selectedSpace, setSelectedSpace] = React.useState(); + const [propertySpaces, setPropertySpaces] = React.useState([]); + const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); + // TODO: find a better way to do this + const [searchParams2] = useSearchParams(localStorage.getItem("admin_booking_filter") ?? ""); + + const schema = yup.object({ + id: yup.string(), + property_space_id: yup.string(), + customer_name: yup.string(), + customer_email: yup.string(), + host_email: yup.string(), + status: yup.string(), + payment_status: yup.string(), + booking_start_time: yup.string(), + booking_time: yup.string(), + duration: yup.string(), + }); + const { + reset, + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: (() => { + let fromSearch = parseSearchParams(searchParams); + if (Object.keys(fromSearch).length > 0) { + return fromSearch; + } + return parseSearchParams(searchParams2); + })(), + }); + + const selectStatus = [ + { key: "", value: "All" }, + { key: "0", value: "Pending" }, + { key: "1", value: "Upcoming" }, + { key: "2", value: "Ongoing" }, + { key: "3", value: "Complete" }, + { key: "4", value: "Declined" }, + { key: "5", value: "Cancelled" }, + ]; + + const selectPaymentStatus = [ + { key: "", value: "All" }, + { key: "0", value: "Pending" }, + { key: "1", value: "Paid" }, + { key: "2", value: "Declined" }, + { key: "3", value: "Cancelled" }, + ]; + + const statusMapping = [ + { key: "0", value: "Pending" }, + { key: "1", value: "Upcoming" }, + { key: "2", value: "Ongoing" }, + { key: "3", value: "Complete" }, + { key: "4", value: "Declined" }, + { key: "5", value: "Cancelled" }, + ]; + + async function getPropertySpacesData(pageNum, limit, data) { + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/property-spaces/PAGINATE", + { + where: [data?.property_name ? `ergo_property.name LIKE '%${data.property_name}%' OR ergo_spaces.category LIKE '%${data.property_name}%'` : 1, "ergo_property.deleted_at IS NULL"], + page: pageNum, + limit: limit, + }, + "POST", + ); + const { list } = result; + setPropertySpaces(list); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + function onSort(accessor, direction) { } + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum, clicked) { + let data = parseSearchParams(searchParams); + let data2 = parseSearchParams(searchParams2); + + data = Object.keys(data).length < 1 ? parseSearchParams(searchParams2) : data; + + if (clicked) { + data = {}; + data.id = data.id?.replace(ID_PREFIX.BOOKINGS, ""); + } + data.id = data.id?.replace(ID_PREFIX.BOOKINGS, ""); + + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/booking/PAGINATE", + { + where: [ + data + ? `${data.id ? `ergo_booking.id = '${data.id}'` : "1"} AND ${data.customer_name ? `customer.first_name LIKE '%${data.customer_name}%' OR customer.last_name LIKE '%${data.customer_name}%'` : "1" + } AND ${data.status ? `ergo_booking.status = ${data.status}` : "1"} AND ${data.payment_status ? `ergo_booking.payment_status = ${data.payment_status}` : "1"} AND ${data.booking_start_time ? `ergo_booking.booking_start_time LIKE '%${data.booking_start_time}%'` : "1" + } AND ${data.property_space_id ? `ergo_booking.property_space_id LIKE '%${data.property_space_id}%'` : "1"} AND ${data.host_email ? `ergo_user.email LIKE '%${data.host_email}%'` : "1" + } AND ${data.customer_email ? `customer.email LIKE '%${data.customer_email}%'` : "1"}` + : 1, + "ergo_booking.deleted_at IS NULL", + ], + page: pageNum, + limit: limitNum, + sortId: "update_at", + direction: "DESC", + }, + "POST", + ); + + const { list, total, limit, num_pages, page } = result; + setCurrentTableData(list); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("property_space_id", selectedSpace?.id ?? ""); + searchParams.set("customer_name", data.customer_name); + searchParams.set("status", data.status); + searchParams.set("payment_status", data.payment_status); + searchParams.set("booking_start_time", data.booking_start_time); + searchParams.set("customer_email", data.customer_email); + searchParams.set("host_email", data.host_email); + + setSearchParams(searchParams); + localStorage.setItem("admin_booking_filter", searchParams.toString()); + + getData(1, pageSize); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "booking", + }, + }); + + (async function () { + await getData(1, pageSize); + getPropertySpacesData(1, 10); + })(); + }, []); + + return ( + <> +
+
+

Booking Search

+ +
+ +
+
+ + +

{errors.id?.message}

+
+ +
+ + +

{errors.property_spaces_id?.message}

+
+ +
+ + +

{errors.customer_name?.message}

+
+ +
+ + +

+
+ +
+ + +

+
+ +
+ + +

{errors.booking_start_time?.message}

+
+
+ + +

{errors.customer_email?.message}

+
+
+ + +

{errors.host_email?.message}

+
+
+ + + +
+ +
+ +
+ {data.map((data) => ( +
+
{ID_PREFIX.BOOKINGS + data.id}
+ property_image +
+

{data.property_name}

+

{data.space_category}

+

{statusMapping.find((status) => status.key == data.status)?.value}

+
+
+
+

Host

+

+ {data.host_last_name}, {data.host_first_name}{" "} +

+
+
+

Customer

+

+ {data.customer_last_name}, {data.customer_first_name}{" "} +

+
+
+
+
+

Date

+

{moment(data.booking_start_time).format("MM/DD/YY")}

+
+
+

Duration

+

{secondsToHour(data.duration)}

+
+
+
+
+

Rate

+

${data?.rate?.toFixed(2)}

+
+
+

Tax

+

${data?.tax?.toFixed(2)}

+
+
+
+
+

Total

+

${((data?.total ?? 0) + (data?.addon_cost ?? 0)).toFixed(2)}

+
+
+

Commission

+

${data?.commission?.toFixed(2)}

+
+
+ +
+ + + +
+ + +
+ + {({ active }) => ( + + )} + + + {({ active }) => ( + + )} + +
+
+
+
+
+ ))} +
+ + + ); +}; + +export default AdminBookingListPage; diff --git a/src/pages/Admin/Booking/EditAdminBookingPage.jsx b/src/pages/Admin/Booking/EditAdminBookingPage.jsx new file mode 100644 index 0000000..43a4161 --- /dev/null +++ b/src/pages/Admin/Booking/EditAdminBookingPage.jsx @@ -0,0 +1,506 @@ +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import { useNavigate, useParams } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import EditAdminPageLayout from "@/layouts/EditAdminPageLayout"; +import { addHours } from "@/utils/utils"; +import moment from "moment"; +import SmartSearch from "@/components/SmartSearch"; + +let sdk = new MkdSDK(); + +const EditAdminBookingPage = () => { + const { dispatch } = React.useContext(AuthContext); + const schema = yup.object({ + property_space_id: yup.number(), + customer_id: yup.number(), + host_id: yup.number(), + status: yup.number().required(), + payment_status: yup.number().required(), + booked_unit: yup.number().required().positive().integer(), + payment_method: yup.string(), + booking_start_time: yup.string(), + duration: yup.number().required().positive().integer(), + }); + + const [selectedSpace, setSelectedSpace] = React.useState({}); + const [propertySpaces, setPropertySpaces] = React.useState([]); + + const [selectedCustomer, setSelectedCustomer] = React.useState({}); + const [customers, setCustomers] = React.useState([]); + const [booking, setBooking] = React.useState({}); + + const [selectedHost, setSelectedHost] = React.useState({}); + const [hosts, setHosts] = React.useState([]); + const [initialStatus, setInitialStatus] = React.useState(0); + const [settings, setSettings] = React.useState([]); + + async function getHostData(pageNum, limitNum, data) { + try { + sdk.setTable("user"); + const payload = { email: data.email || undefined, role: "host" }; + const result = await sdk.callRestAPI( + { + payload, + page: pageNum, + limit: limitNum, + }, + "PAGINATE", + ); + const { list } = result; + setHosts(list || []); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + async function getCustomerData(pageNum, limitNum, data) { + try { + sdk.setTable("user"); + const payload = { email: data.email || undefined }; + const result = await sdk.callRestAPI( + { + payload, + page: pageNum, + limit: limitNum, + }, + "PAGINATE", + ); + const { list } = result; + setCustomers(list || []); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + + async function getSettings() { + try { + sdk.setTable("settings"); + + const result = await sdk.callRestAPI( + { + // payload: "key_name = 'tax' OR key_name = 'commission'", + page: 1, + limit: 2, + }, + "PAGINATE", + ); + const { list } = result; + setSettings(list); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + + async function getPropertySpacesData(pageNum, limit, data) { + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/property-spaces/PAGINATE", + { + where: [data?.property_name ? `ergo_property.name LIKE '%${data.property_name}%' OR ergo_spaces.category LIKE '%${data.property_name}%'` : 1], + page: pageNum, + limit: limit, + }, + "POST", + ); + const { list } = result; + setPropertySpaces(list); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const navigate = useNavigate(); + const [id, setId] = useState(0); + const { + register, + handleSubmit, + setError, + setValue, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const params = useParams(); + + const selectStatus = [ + { key: "0", value: "Pending" }, + { key: "1", value: "Upcoming" }, + { key: "2", value: "Ongoing" }, + { key: "3", value: "Complete" }, + { key: "4", value: "Declined" }, + { key: "5", value: "Cancelled" }, + ]; + + const selectPaymentStatus = [ + { key: "0", value: "Pending" }, + { key: "1", value: "Paid" }, + { key: "2", value: "Declined" }, + { key: "3", value: "Cancelled" }, + ]; + + const paymentMethod = [ + { + key: "credit_card", + value: "Credit Card", + }, + ]; + + useEffect(() => { + if (customers.length > 0 && hosts.length > 0 && propertySpaces.length > 0 && !selectedCustomer?.email && !selectedHost?.email && !selectedSpace.category) { + (async function () { + try { + sdk.setTable("booking"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + if (!result.error) { + setSelectedSpace(propertySpaces.find((sp) => sp.id == result.model.property_space_id) || {}); + setSelectedCustomer(customers.find((c) => c.id == result.model.customer_id) || {}); + setSelectedHost(hosts.find((h) => h.id == result.model.host_id) || {}); + setValue("status", result.model.status); + setInitialStatus(result.model.status); + setValue("payment_status", result.model.payment_status); + setValue("payment_method", result.model.payment_method); + setValue("booking_start_time", moment(result.model.booking_start_time).format("yyyy-MM-DDTHH:mm")); + setValue("duration", result.model.duration / 3600); + setValue("booked_unit", result.model.booked_unit); + setBooking(result.model); + + setId(result.model.id); + } + } catch (error) { + console.log("error", error); + tokenExpireError(dispatch, error.message); + } + })(); + } + }, [customers.length, hosts.length, propertySpaces.length]); + + const onSubmit = async (data) => { + if (!selectedHost?.id) { + setError("host_id", { + type: "manual", + message: "Please select a valid host", + }); + return; + } + if (!selectedCustomer?.id) { + setError("customer_id", { + type: "manual", + message: "Please select a valid customer", + }); + return; + } + if (!selectedSpace?.id) { + setError("property_space_id", { + type: "manual", + message: "Please select a valid space", + }); + return; + } + data.host_id = selectedHost.id; + data.customer_id = selectedCustomer.id; + data.property_space_id = selectedSpace.id; + let bookingStartTime = new Date(data.booking_start_time); + let bookingEndTime = addHours(data.duration, bookingStartTime); + data.duration = data.duration * 60 * 60; + + const dataPayload = { + id: Number(params?.id), + booked_unit: 1, + status: data.status === booking.status ? null : data.status, + payment_status: data.payment_status === booking.payment_status ? null : data.payment_status, + booking_start_time: new Date(data.booking_start_time).getTime() === new Date(booking.booking_start_time).getTime() ? null : bookingStartTime.toISOString(), + booking_end_time: new Date(data.booking_start_time).getTime() === new Date(booking.booking_start_time).getTime() ? null : bookingEndTime.toISOString(), + duration: data.duration, + } + + const removeNullValues = () => { + const cleanedObject = {}; + for (const key in dataPayload) { + if (dataPayload[key] !== null) { + cleanedObject[key] = dataPayload[key]; + } + } + return cleanedObject; + }; + + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/booking/PUT", + removeNullValues(), + "POST", + ); + // create payout is status = 3 + if (data.status == 3 && initialStatus != 3) { + sdk.setTable("booking"); + const newBookingResult = await sdk.callRawAPI( + "/v2/api/custom/ergo/booking/details", + { + where: [`ergo_booking.id=${result.message}`], + }, + "POST", + ); + const payoutResult = await sdk.callRawAPI( + "/v2/api/custom/ergo/payout/POST", + { + initiated_at: bookingStartTime.toISOString(), + host_id: data.host_id, + customer_id: data.customer_id, + property_space_id: data.property_space_id, + total: newBookingResult.list.total + newBookingResult.list.addon_cost, + tax: settings.find((setting) => setting.key_name === "tax").key_value, + commission: settings.find((setting) => setting.key_name === "commission").key_value, + booking_id: result.message, + status: 0, + }, + "POST", + ); + console.log("payoutResult", payoutResult); + } + if (!result.error) { + showToast(globalDispatch, "Updated"); + navigate("/admin/booking"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + }; + + const onError = () => { + if (!selectedCustomer?.id) { + setError("customer_id", { + type: "manual", + message: "Please select a customer", + }); + } + if (!selectedHost?.id) { + setError("host_id", { + type: "manual", + message: "Please select a host", + }); + } + if (!selectedSpace?.id) { + setError("property_space_id", { + type: "manual", + message: "Please select a property space", + }); + } + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "booking", + }, + }); + (async function () { + await getSettings(); + await getCustomerData(1, 200, { email: "" }); + await getHostData(1, 200, { email: "" }); + await getPropertySpacesData(1, 100, { property_name: "" }); + })(); + }, []); + + return ( + +
+
+ + + + +

{errors.property_space_id?.message}

+
+ +
+ + +

{errors.customer_id?.message}

+
+ +
+ + +

{errors.host_id?.message}

+
+ +
+ + +

{errors.status?.message}

+
+ +
+ + +

{errors.payment_status?.message}

+
+
+ + +

{errors.booking_start_time?.message}

+
+ +
+ + +

{errors.duration?.message}

+
+
+ + +
+ +
+ ); +}; + +export default EditAdminBookingPage; diff --git a/src/pages/Admin/Booking/ViewAdminBookingPage.jsx b/src/pages/Admin/Booking/ViewAdminBookingPage.jsx new file mode 100644 index 0000000..797b5c9 --- /dev/null +++ b/src/pages/Admin/Booking/ViewAdminBookingPage.jsx @@ -0,0 +1,160 @@ +import React, { useState } from "react"; +import MkdSDK from "@/utils/MkdSDK"; +import { Link, useNavigate, useParams } from "react-router-dom"; +import { GlobalContext } from "@/globalContext"; +import ViewAdminPageLayout from "@/layouts/ViewAdminPageLayout"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import moment from "moment"; + +const ViewAdminBookingPage = () => { + const [bookingInfo, setBookingInfo] = useState({}); + const [addons, setAddons] = useState([]); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const { dispatch } = React.useContext(AuthContext); + const params = useParams(); + const navigate = useNavigate(); + + const selectStatus = [{ value: "Pending" }, { value: "Upcoming" }, { value: "Ongoing" }, { value: "Complete", color: "#0D9895" }, { value: "Declined" }, { value: "Cancelled" }]; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "booking", + }, + }); + + const removeDuplicates = (bookingAddons) => { + let removed = []; + const idExists = (id) => { + return removed.some((duplicate) => duplicate.id === id); + }; + bookingAddons.forEach((addon) => { + if (idExists(addon.id)) { + let index = removed.findIndex((add) => add.id === addon.id); + removed[index].count += removed[index].count; + } else { + removed.push({ ...addon, count: 1 }); + } + }); + return removed; + }; + + (async function () { + try { + let sdk = new MkdSDK(); + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/booking/details", + { + where: [`ergo_booking.id=${Number(params?.id)}`], + }, + "POST", + ); + + if (!result.error && result.list) { + setBookingInfo(result.list); + // console.log("list", result.list); + setAddons(removeDuplicates(result.list.add_ons)); + } + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + })(); + }, []); + + return ( + +
+
+
+

Booking #{params?.id}

+
+
+

Host

+

+ {bookingInfo.host_first_name}, {bookingInfo.host_last_name} +

+
+
+

Guest

+

+ {bookingInfo.customer_first_name}{" "}{bookingInfo.customer_last_name} +

+
+
+

Property

+

{bookingInfo.property_name}

+
+
+

Space Name

+

{bookingInfo.space_category}

+
+
+

From

+

{moment(bookingInfo.booking_start_time).format("MM/DD/YY hh:mm a")}

+
+
+

Till

+

{moment(bookingInfo.booking_end_time).format("MM/DD/YY hh:mm a")}

+
+
+ + View Addons + +
+
+
+
+
+
+

+ Status:{" "} + {selectStatus[bookingInfo?.status]?.value} +

+
+
+
+

Charges

+

Payment method: {bookingInfo?.payment_method?.replaceAll("_", " ")}

+
+
+

Rate

+

${bookingInfo?.hourly_rate ? bookingInfo.hourly_rate : 0}/h

+
+
+

Price

+

${bookingInfo?.hourly_rate ? bookingInfo.hourly_rate * (bookingInfo.duration / 3600) : 0}

+
+ {addons.map((addon) => ( +
+

+ {addon.name} (x{addon.count}) +

+

${addon.cost * addon.count}

+
+ ))} +
+

Tax

+

${bookingInfo?.tax ? bookingInfo.tax : 0}

+
+
+

Total

+

${bookingInfo?.total ? bookingInfo.total : 0}

+
+
+
+
+
+ ); +}; + +export default ViewAdminBookingPage; diff --git a/src/pages/Admin/BookingAddon/AddAdminBookingAddonsPage.jsx b/src/pages/Admin/BookingAddon/AddAdminBookingAddonsPage.jsx new file mode 100644 index 0000000..8d4db32 --- /dev/null +++ b/src/pages/Admin/BookingAddon/AddAdminBookingAddonsPage.jsx @@ -0,0 +1,204 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate } from "react-router-dom"; +import { tokenExpireError, AuthContext } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import AddAdminPageLayout from "@/layouts/AddAdminPageLayout"; +import SmartSearch from "@/components/SmartSearch"; +import { useEffect } from "react"; + +const AddAdminBookingAddonsPage = () => { + let sdk = new MkdSDK(); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const [addOns, setAddOns] = React.useState([]); + const [propertyName, setPropertyName] = React.useState(""); + const [query, setQuery] = React.useState(""); + const schema = yup + .object({ + booking_id: yup.number().required().positive().integer().typeError("Booking ID must be a number"), + property_add_on_id: yup.number(), + }) + .required(); + + const { dispatch } = React.useContext(AuthContext); + + const navigate = useNavigate(); + const { + register, + handleSubmit, + setError, + clearErrors, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + async function getAddOnData() { + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/property-addons/PAGINATE", + { + where: [propertyName ? `${propertyName ? `ergo_property.name = '${propertyName}'` : "1"}` : 1], + page: 1, + limit: 1000, + }, + "POST", + ); + const { list } = result; + console.log("addon", list); + setAddOns(list); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + + const confirmBookingId = async (id) => { + if (!id) { + clearErrors("booking_id"); + return; + } + try { + let sdk = new MkdSDK(); + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/booking/details", + { + where: [`ergo_booking.id=${id}`], + }, + "POST", + ); + + if (result.error || !result.list || !result.list.id) throw new Error(); + clearErrors("booking_id"); + setPropertyName(result.list.property_name); + } catch (error) { + console.log("ERROR", error); + setError("booking_id", { + type: "manual", + message: "Booking with this ID does not exist", + }); + } + }; + + const onSubmit = async (data) => { + try { + sdk.setTable("booking_addons"); + const result = await sdk.callRestAPI( + { + booking_id: data.booking_id, + property_add_on_id: data.property_add_on_id, + }, + "POST", + ); + if (!result.error) { + showToast(globalDispatch, "Added"); + navigate("/admin/booking_addons"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("booking_id", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + useEffect(() => { + if (propertyName != "") { + getAddOnData(); + } + }, [propertyName]); + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "booking_addons", + }, + }); + }, []); + + return ( + +
+
+ + confirmBookingId(e.target.value)} + /> +

{errors.booking_id?.message}

+
+ +
+ + +

{errors.property_add_on_id?.message}

+
+ +
+ + +
+ +
+ ); +}; + +export default AddAdminBookingAddonsPage; diff --git a/src/pages/Admin/BookingAddon/AdminBookingAddonsListPage.jsx b/src/pages/Admin/BookingAddon/AdminBookingAddonsListPage.jsx new file mode 100644 index 0000000..c8adeff --- /dev/null +++ b/src/pages/Admin/BookingAddon/AdminBookingAddonsListPage.jsx @@ -0,0 +1,355 @@ +import React from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { Link, useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import AddButton from "@/components/AddButton"; +import Button from "@/components/Button"; +import Table from "@/components/Table"; +import PaginationHeader from "@/components/PaginationHeader"; +import ReactHtmlTableToExcel from "react-html-table-to-excel"; +import { ID_PREFIX } from "@/utils/constants"; +import { adminColumns, applySetting } from "@/utils/adminPortalColumns"; +import TreeSDK from "@/utils/TreeSDK"; + +let sdk = new MkdSDK(); +let treeSdk = new TreeSDK(); + +const AdminBookingAddonsListPage = () => { + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch, state } = React.useContext(GlobalContext); + const [tableColumns, setTableColumns] = React.useState([]); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + + const [addOns, setAddOns] = React.useState([]); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_bka_filter") ?? ""); + + const schema = yup.object({ + id: yup.string(), + booking_id: yup.string(), + property_add_on_id: yup.string(), + }); + const { + reset, + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: parseSearchParams(searchParams), + }); + + function onSort(accessor) { + const columns = tableColumns; + const index = columns.findIndex((column) => column.accessor === accessor); + const column = columns[index]; + column.isSortedDesc = !column.isSortedDesc; + columns.splice(index, 1, column); + setTableColumns(() => [...columns]); + const sortedList = selector(data, column.isSortedDesc, accessor); + setCurrentTableData(sortedList); + } + function selector(users, isSortedDesc, accessor) { + if (accessor?.split(",").length > 1) { + accessor = accessor.split(",")[0]; + } + + return users.sort((a, b) => { + if (isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1; + } else { + return a[accessor] < b[accessor] ? 1 : -1; + } + } + if (!isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1; + } else { + return a[accessor] < b[accessor] ? -1 : 1; + } + } + }); + } + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + const getAllAddOns = async () => { + try { + const result = await treeSdk.getList("add_on", { filter: ["deleted_at,is"], join: [] }); + if (!result.error) { + setAddOns(result.list); + } + } catch (error) { + console.log("Error", error); + setError("property_add_on_id", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + async function getData(pageNum, limitNum) { + const data = parseSearchParams(searchParams); + data.id = data.id?.replace(ID_PREFIX.BOOKING_ADDON, ""); + data.booking_id = data.booking_id?.replace(ID_PREFIX.BOOKINGS, ""); + + sdk.setTable("booking_addons"); + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/booking-addon/PAGINATE", + { + where: [ + data + ? `${data.id ? `ergo_booking_addons.id = '${data.id}'` : "1"} + AND ${data.property_add_on_id ? `ergo_add_on.id = '${data.property_add_on_id}'` : "1"} + AND ${data.booking_id ? `booking_id = '${data.booking_id}'` : "1"}` + : 1, + "ergo_booking_addons.deleted_at IS NULL", + ], + page: pageNum, + limit: limitNum, + sortId: "update_at", + direction: "DESC", + }, + "POST", + ); + const { list, total, limit, num_pages, page } = result; + console.log("list", list); + const sortedList = selector(list, false); + setCurrentTableData(sortedList); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + const onSubmit = (data) => { + console.log("submitting", data); + searchParams.set("id", data.id); + searchParams.set("booking_id", data.booking_id); + searchParams.set("property_add_on_id", data.property_add_on_id); + + setSearchParams(searchParams); + localStorage.setItem("admin_bka_filter", searchParams.toString()); + + getData(1, pageSize); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "booking_addons", + }, + }); + + getAllAddOns(); + + (async function () { + await fetchColumnOrder(); + getData(1, pageSize); + })(); + }, []); + + React.useEffect(() => { + if (state.deleted) { + globalDispatch({ + type: "DELETED", + payload: { + deleted: false, + }, + }); + getData(currentPage, pageSize); + } + }, [state.deleted]); + + async function fetchColumnOrder() { + sdk.setTable("settings"); + const payload = { key_name: "admin_booking_addons_column_order" }; + try { + const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE"); + if (Array.isArray(result.list) && result.list.length > 0) { + setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_booking_addons)); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + return ( + <> +
+
+

Booking Addons Search

+ +
+ +
+
+ + +

{errors.id?.message}

+
+ +
+ + +

{errors.booking_id?.message}

+
+ +
+ + +

{errors.property_add_on_id?.message}

+
+
+ + + + +
+ + Change Column Order + {" "} + +
+ +
+
+
+ + + + + ); +}; + +export default AdminBookingAddonsListPage; diff --git a/src/pages/Admin/BookingAddon/EditAdminBookingAddonsPage.jsx b/src/pages/Admin/BookingAddon/EditAdminBookingAddonsPage.jsx new file mode 100644 index 0000000..65733de --- /dev/null +++ b/src/pages/Admin/BookingAddon/EditAdminBookingAddonsPage.jsx @@ -0,0 +1,228 @@ +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import { useNavigate, useParams } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import EditAdminPageLayout from "@/layouts/EditAdminPageLayout"; + +let sdk = new MkdSDK(); + +const EditAdminBookingAddonsPage = () => { + const { dispatch } = React.useContext(AuthContext); + const schema = yup + .object({ + booking_id: yup.number().required().positive().integer(), + property_add_on_id: yup.number().required().positive().integer(), + }) + .required(); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const navigate = useNavigate(); + const [addOns, setAddOns] = React.useState([]); + const [propertyName, setPropertyName] = React.useState(""); + + const [id, setId] = useState(0); + const { + register, + handleSubmit, + setError, + setValue, + clearErrors, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const params = useParams(); + + const getAllAddOns = async () => { + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/property-addons/PAGINATE", + { + where: [propertyName ? `${propertyName ? `ergo_property.name = '${propertyName}'` : "1"}` : 1], + page: 1, + limit: 1000, + }, + "POST", + ); + const { list } = result; + console.log("addon", list); + setAddOns(list); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + }; + + const confirmBookingId = async (id) => { + if (!id) { + clearErrors("booking_id"); + return; + } + try { + let sdk = new MkdSDK(); + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/booking/details", + { + where: [`ergo_booking.id=${id}`], + }, + "POST", + ); + if (result.error || !result.list || !result.list.id) throw new Error(); + clearErrors("booking_id"); + setPropertyName(result.list.property_name); + } catch (error) { + console.log("ERROR", error); + setError("booking_id", { + type: "manual", + message: "Booking with this ID does not exist", + }); + } + }; + + useEffect( + function () { + // this function should only run once + if (addOns.length > 0 && id == 0) { + (async function () { + try { + sdk.setTable("booking_addons"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + if (!result.error) { + setValue("booking_id", result.model.booking_id); + confirmBookingId(result.model.booking_id); + setValue("property_add_on_id", result.model.property_add_on_id); + setId(result.model.id); + } + } catch (error) { + console.log("error", error); + tokenExpireError(dispatch, error.message); + } + })(); + } + }, + [addOns.length], + ); + + useEffect(() => { + if (propertyName != "") { + getAllAddOns(); + } + }, [propertyName]); + + const onSubmit = async (data) => { + try { + const result = await sdk.callRestAPI( + { + id: id, + booking_id: data.booking_id, + property_add_on_id: data.property_add_on_id, + }, + "PUT", + ); + + if (!result.error) { + showToast(globalDispatch, "Updated"); + navigate("/admin/booking_addons"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("booking_id", { + type: "manual", + message: error.message, + }); + } + }; + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "booking_addons", + }, + }); + getAllAddOns(); + }, []); + + return ( + +
+
+ + +

{errors.booking_id?.message}

+
+ +
+ + +

{errors.property_add_on_id?.message}

+
+ +
+ + +
+ +
+ ); +}; + +export default EditAdminBookingAddonsPage; diff --git a/src/pages/Admin/CMS/AdminCancellationPolicyPage.jsx b/src/pages/Admin/CMS/AdminCancellationPolicyPage.jsx new file mode 100644 index 0000000..843a465 --- /dev/null +++ b/src/pages/Admin/CMS/AdminCancellationPolicyPage.jsx @@ -0,0 +1,92 @@ +import React, { useState } from "react"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import SunEditor, { buttonList } from "suneditor-react"; +import "suneditor/dist/css/suneditor.min.css"; // Import Sun Editor's CSS File +import { LoadingButton } from "@/components/frontend"; + +let sdk = new MkdSDK(); + +export default function AdminCancellationPolicyPage() { + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const [id, setId] = useState(0); + const [content, setContent] = useState(""); + const [loading, setLoading] = useState(false); + + async function fetchCancellationPolicy() { + sdk.setTable("cms"); + try { + const result = await sdk.callRestAPI({ payload: { content_key: "cancellation_policy" }, limit: 1, page: 1 }, "PAGINATE"); + + if (Array.isArray(result.list) && result.list.length > 0) { + setId(result.list[0].id); + setContent(result.list[0].content_value); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + const onSubmit = async (e) => { + setLoading(true); + e.preventDefault(); + try { + const result = await sdk.callRestAPI( + { + id, + content_value: content, + }, + "PUT", + ); + showToast(globalDispatch, "Saved", 3000); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + setLoading(false); + }; + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "cancellation_policy", + }, + }); + fetchCancellationPolicy(); + }, []); + + return ( +
+

Cancellation policy

+
+
+ setContent(content)} + setContents={content} + name="content" + setOptions={{ buttonList: buttonList.complex }} + /> +
+
+ Submitting} + type="submit" + className="login-btn-gradient text-white py-2 px-4 rounded focus:outline-none focus:shadow-outline" + disabled={loading} + > + Submit + +
+ +
+ ); +} diff --git a/src/pages/Admin/CMS/AdminPrivacyPage.jsx b/src/pages/Admin/CMS/AdminPrivacyPage.jsx new file mode 100644 index 0000000..b406139 --- /dev/null +++ b/src/pages/Admin/CMS/AdminPrivacyPage.jsx @@ -0,0 +1,94 @@ +import React, { useEffect, useState } from "react"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import SunEditor, { buttonList } from "suneditor-react"; +import "suneditor/dist/css/suneditor.min.css"; // Import Sun Editor's CSS File +import { Link } from "react-router-dom"; +import { parseJsonSafely } from "@/utils/utils"; +import { LoadingButton } from "@/components/frontend"; + +let sdk = new MkdSDK(); + +export default function AdminPrivacyPage() { + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const [id, setId] = useState(0); + const [content, setContent] = useState(""); + const [loading, setLoading] = useState(false); + + async function fetchPrivacy() { + sdk.setTable("cms"); + try { + const result = await sdk.callRestAPI({ payload: { content_key: "privacy_policy" }, limit: 1, page: 1 }, "PAGINATE"); + + if (Array.isArray(result.list) && result.list.length > 0) { + setId(result.list[0].id); + setContent(result.list[0].content_value); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + const onSubmit = async (e) => { + setLoading(true); + e.preventDefault(); + try { + const result = await sdk.callRestAPI( + { + id, + content_value: content, + }, + "PUT", + ); + showToast(globalDispatch, "Saved", 3000); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + setLoading(false); + }; + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "privacy", + }, + }); + fetchPrivacy(); + }, []); + + return ( +
+

Privacy policy

+
+
+ setContent(content)} + setContents={content} + name="content" + setOptions={{ buttonList: buttonList.complex }} + /> +
+
+ Submitting} + type="submit" + className="login-btn-gradient text-white py-2 px-4 rounded focus:outline-none focus:shadow-outline" + disabled={loading} + > + Submit + +
+ +
+ ); +} diff --git a/src/pages/Admin/CMS/AdminTermsAndConditionsPage.jsx b/src/pages/Admin/CMS/AdminTermsAndConditionsPage.jsx new file mode 100644 index 0000000..355e575 --- /dev/null +++ b/src/pages/Admin/CMS/AdminTermsAndConditionsPage.jsx @@ -0,0 +1,94 @@ +import React, { useEffect, useState } from "react"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import SunEditor, { buttonList } from "suneditor-react"; +import "suneditor/dist/css/suneditor.min.css"; // Import Sun Editor's CSS File +import { Link } from "react-router-dom"; +import { parseJsonSafely } from "@/utils/utils"; +import { LoadingButton } from "@/components/frontend"; + +let sdk = new MkdSDK(); + +export default function AdminTermsAndConditionsPage() { + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const [id, setId] = useState(0); + const [content, setContent] = useState(""); + const [loading, setLoading] = useState(false); + + async function fetchTermsAndConditions() { + sdk.setTable("cms"); + try { + const result = await sdk.callRestAPI({ payload: { content_key: "terms_and_conditions" }, limit: 1, page: 1 }, "PAGINATE"); + + if (Array.isArray(result.list) && result.list.length > 0) { + setId(result.list[0].id); + setContent(result.list[0].content_value); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + const onSubmit = async (e) => { + setLoading(true); + e.preventDefault(); + try { + const result = await sdk.callRestAPI( + { + id, + content_value: content, + }, + "PUT", + ); + showToast(globalDispatch, "Saved", 3000); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + setLoading(false); + }; + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "terms_and_conditions", + }, + }); + fetchTermsAndConditions(); + }, []); + + return ( +
+

Terms and conditions

+
+
+ setContent(content)} + setContents={content} + name="content" + setOptions={{ buttonList: buttonList.complex }} + /> +
+
+ Submitting} + type="submit" + className="login-btn-gradient text-white py-2 px-4 rounded focus:outline-none focus:shadow-outline" + disabled={loading} + > + Submit + +
+ +
+ ); +} diff --git a/src/pages/Admin/Customer/AddAdminCustomerPage.jsx b/src/pages/Admin/Customer/AddAdminCustomerPage.jsx new file mode 100644 index 0000000..fda861a --- /dev/null +++ b/src/pages/Admin/Customer/AddAdminCustomerPage.jsx @@ -0,0 +1,334 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { tokenExpireError, AuthContext } from "@/authContext"; +import AddAdminPageLayout from "@/layouts/AddAdminPageLayout"; +import moment from "moment"; +import commonPasswords from "@/assets/json/common-passwords.json"; + +const AddAdminCustomerPage = () => { + const schema = yup.object({ + firstName: yup.string().required("First name is required"), + lastName: yup.string().required("Last name is required"), + email: yup.string().email().required("Email is required"), + dob: yup + .string() + .test("is-not-in-future", "Not a valid date", (val) => { + console.log("testing here", val); + if (val == "") return true; + const date = new Date(val); + return date < new Date(); + }) + .test("must-be-at-least-18yo", "Must be at least 18 years of age", (val) => { + return moment().diff(moment(val), "years") > 18; + }), + password: yup + .string() + .required("Password is required") + .min(10, "Password must be at least 10 characters long") + .matches(/^(?=.*[0-9])/, "Password must contain at least one digit(0-9)") + .matches(/^(?=.*[a-z])/, "Password must contain at least one lowercase letter") + .matches(/^(?=.*[A-Z])/, "Password must contain at least one uppercase letter") + .matches(/^(?=.*[!@#\$%\^&\*])/, "Password must contain at least one symbol") + .test("is-not-dictionary", "Password must not contain a common word", (val) => { + return commonPasswords.every((pass) => !val.includes(pass)); + }) + .test("does-not-contain-user-info", "Password must not contain your name or date of birth", (val, ctx) => { + const d = moment(ctx.parent.dob); + return [ctx.parent.firstName, ctx.parent.lastName, d.format("yyyyMMDD"), d.format("DDMMyyyy"), d.format("MMDDyyyy"), d.format("YYMMDD"), d.format("MMDDYY"), d.format("DDMMYY")].every( + (field) => field.trim() == "" || !val.toLowerCase().includes(field.toLowerCase()), + ); + }), + role: yup.string().required(), + status: yup.string().required(), + verify: yup.string().required(), + }); + + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const navigate = useNavigate(); + const { + register, + handleSubmit, + setError, + trigger, + formState: { errors, dirtyFields }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { password: "" }, + criteriaMode: "all", + mode: "all", + }); + + const onSubmit = async (data) => { + console.log("submitting", data); + let sdk = new MkdSDK(); + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/register", + { + firstName: data.firstName, + lastName: data.lastName, + status: data.status || 0, + email: data.email, + password: data.password, + dob: data.dob || null, + verify: data.verify || 0, + role: "customer", + payment_method_set: 0, + }, + "POST", + ); + + if (!result.error) { + showToast(dispatch, "Added"); + navigate("/admin/customer"); + } else { + if (result?.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + + // register device + sdk.setTable("device"); + await sdk.callRestAPI({ active: 1, user_id: result.user_id, last_login_time: new Date().toISOString().split("T")[0], uid: localStorage.getItem("device-uid") }, "POST"); + } catch (error) { + setError("firstName", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "customer", + }, + }); + }, []); + + function getPasswordErrors() { + var arr = []; + if (Array.isArray(errors.password?.types.matches)) { + arr = [...errors.password.types.matches]; + } + if (typeof errors.password?.types?.matches === "string") { + arr.push(errors.password.types.matches); + } + if (errors.password?.types?.min) { + arr.push(errors.password.types.min); + } + if (errors.password?.types["does-not-contain-user-info"]) { + arr.push(errors.password?.types["does-not-contain-user-info"]); + } + if (errors.password?.types["is-not-dictionary"]) { + arr.push(errors.password?.types["is-not-dictionary"]); + } + return arr; + } + const passwordErrors = getPasswordErrors(); + + return ( + +
+
+
+ + +

{errors.firstName?.message}

+
+
+ + +

{errors.lastName?.message}

+
+
+ + +

{errors.email?.message}

+
+
+ + +

{errors.dob?.message}

+
+
+ + +
+
+ + +
+
+ + +
+
+ + { + trigger("password"); + }, + })} + autoComplete="new-password" + className={` mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.password?.message ? "border-red-500" : ""}`} + /> + {dirtyFields.password && ( +
+ {passwordErrors.map((msg, idx) => ( +

{msg}

+ ))} +
+ )} +
+
+ + +
+ +
+
+ ); +}; + +export default AddAdminCustomerPage; diff --git a/src/pages/Admin/Customer/AdminCustomerListPage.jsx b/src/pages/Admin/Customer/AdminCustomerListPage.jsx new file mode 100644 index 0000000..1c9504c --- /dev/null +++ b/src/pages/Admin/Customer/AdminCustomerListPage.jsx @@ -0,0 +1,512 @@ +import React from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { useForm } from "react-hook-form"; +import * as yup from "yup"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import PaginationHeader from "@/components/PaginationHeader"; +import AddButton from "@/components/AddButton"; +import Button from "@/components/Button"; +import ReactHtmlTableToExcel from "react-html-table-to-excel"; +import { ID_PREFIX, IMAGE_STATUS } from "@/utils/constants"; +import { adminColumns, applySetting } from "@/utils/adminPortalColumns"; +import ProfileImagePreviewModal from "../User/ProfileImagePreviewModal"; +import RejectProfileImageModal from "../User/RejectProfileImageModal"; +import TreeSDK from "@/utils/TreeSDK"; + +let sdk = new MkdSDK(); +let treeSdk = new TreeSDK(); + +const AdminCustomerListPage = () => { + const { dispatch: globalDispatch, state } = React.useContext(GlobalContext); + const { dispatch } = React.useContext(AuthContext); + const [query, setQuery] = React.useState(""); + const [tableColumns, setTableColumns] = React.useState([]); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_customer_filter") ?? ""); + const [activePicture, setActivePicture] = React.useState(""); + const [pictureModal, setPictureModal] = React.useState(false); + const [activeRow, setActiveRow] = React.useState({}); + + const schema = yup.object({ + id: yup.string(), + email: yup.string(), + role: yup.string(), + status: yup.string(), + first_name: yup.string(), + last_name: yup.string(), + }); + + const { + reset, + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: parseSearchParams(searchParams), + }); + + const selectStatus = [ + { key: "", value: "All" }, + { key: "0", value: "Inactive" }, + { key: "1", value: "Active" }, + { key: "2", value: "Suspend" }, + ]; + const selectVerified = [ + { key: "", value: "All" }, + { key: "0", value: "No" }, + { key: "1", value: "Yes" }, + ]; + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + function onSort(accessor) { + const columns = tableColumns; + const index = columns.findIndex((column) => column.accessor === accessor); + const column = columns[index]; + column.isSortedDesc = !column.isSortedDesc; + columns.splice(index, 1, column); + setTableColumns(() => [...columns]); + const sortedList = selector(data, column.isSortedDesc, accessor); + setCurrentTableData(sortedList); + } + function selector(users, isSortedDesc, accessor) { + if (accessor?.split(",").length > 1) { + accessor = accessor.split(",")[0]; + } + + return users.sort((a, b) => { + if (isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1; + } else { + return a[accessor] < b[accessor] ? 1 : -1; + } + } + if (!isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1; + } else { + return a[accessor] < b[accessor] ? -1 : 1; + } + } + }); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum, dob) { + const data = parseSearchParams(searchParams); + data.id = data.id?.replace(ID_PREFIX.CUSTOMER, ""); + + try { + let filter = ["deleted_at,is", "role,eq,'customer'"]; + if (data.id) { + filter.push(`id,eq,${data.id}`); + } + if (data.email) { + filter.push(`email,cs,${data.email}`); + } + if (data.first_name) { + filter.push(`first_name,cs,${data.first_name}`); + } + if (data.last_name) { + filter.push(`last_name,cs,${data.last_name}`); + } + if (data.verify) { + filter.push(`verify,eq,${data.verify}`); + } + if (data.status) { + filter.push(`status,eq,${data.status}`); + } + const result = await treeSdk.getPaginate("user", { join: [], filter, page: pageNum || 1, size: limitNum, order: "update_at" }); + const { list, total, limit, num_pages, page } = result; + + const sortedList = selector(list, false); + setCurrentTableData(sortedList); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + const onSubmit = (data) => { + console.log("submitting", data); + + searchParams.set("email", data.email); + searchParams.set("first_name", data.first_name); + searchParams.set("last_name", data.last_name); + searchParams.set("verify", data.verify); + searchParams.set("status", data.status); + searchParams.set("id", data.id); + setSearchParams(searchParams); + localStorage.setItem("admin_customer_filter", searchParams.toString()); + getData(0, pageSize, data.dob); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "customer", + }, + }); + + (async function () { + await fetchColumnOrder(); + await getData(0, pageSize); + })(); + }, []); + + React.useEffect(() => { + if (state.deleted) { + globalDispatch({ + type: "DELETED", + payload: { + deleted: false, + }, + }); + getData(currentPage, pageSize); + } + }, [state.deleted]); + + async function fetchColumnOrder() { + sdk.setTable("settings"); + const payload = { key_name: "admin_customer_column_order" }; + try { + const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE"); + if (Array.isArray(result.list) && result.list.length > 0) { + setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_customer)); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + async function rejectImage(id) { + sdk.setTable("user"); + try { + await sdk.callRestAPI({ id, is_photo_approved: IMAGE_STATUS.NOT_APPROVED }, "PUT"); + showToast(globalDispatch, "Successful"); + getData(1, pageSize); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + async function approveImage(id) { + sdk.setTable("user"); + try { + await sdk.callRestAPI({ id, is_photo_approved: IMAGE_STATUS.APPROVED }, "PUT"); + showToast(globalDispatch, "Successful"); + getData(1, pageSize); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + return ( + <> +
+
+

Search

+ +
+ +
+
+ + +

{errors.id?.message}

+
+
+ + +

{errors.first_name?.message}

+
+
+ + +

{errors.last_name?.message}

+
+
+ + +

{errors.email?.message}

+
+
+ + +

+
+ +
+ + +

+
+
+ + + + +
+ + Change Column Order + + +
+ +
+
+
+ + + {tableColumns.map((column, index) => ( + + ))} + + + + {data.map((row, i) => { + return ( + + {tableColumns.map((cell, index) => { + if (cell.accessor === "") { + return ( + + ); + } + if (cell.statusMapping) { + return ( + + ); + } + if (cell.idPrefix) { + return ( + + ); + } + + if (cell.mapping) { + return ( + + ); + } + + return ( + + ); + })} + + ); + })} + +
onSort(column.accessor)} + > + {column.header} + {column.isSorted} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
+ {row.photo ? ( + + ) : ( + No Photo + )} + + + + + {" "} + {cell.statusMapping[row[cell.accessor]]} + + + {cell.idPrefix + row[cell.accessor]} + + {cell.mapping[row[cell.accessor]] ?? "N/A"} + + {row[cell.accessor]} +
+
+
+ + setPictureModal(false)} + /> + setActiveRow({})} + data={activeRow} + onSuccess={() => getData(currentPage, pageSize)} + noSettings + /> + + ); +}; + +export default AdminCustomerListPage; diff --git a/src/pages/Admin/Customer/EditAdminCustomerPage.jsx b/src/pages/Admin/Customer/EditAdminCustomerPage.jsx new file mode 100644 index 0000000..128b2cf --- /dev/null +++ b/src/pages/Admin/Customer/EditAdminCustomerPage.jsx @@ -0,0 +1,354 @@ +import React, { useState, useRef } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate, useParams } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import EditAdminPageLayout from "@/layouts/EditAdminPageLayout"; +import moment from "moment"; + +let sdk = new MkdSDK(); + +const EditAdminCustomerPage = () => { + const schema = yup + .object({ + firstName: yup.string().required(), + lastName: yup.string().required(), + email: yup.string().email().required(), + password: yup.string(), + status: yup.string(), + dob: yup.string(), + role: yup.string(), + verify: yup.string(), + }) + .required(); + + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch, state } = React.useContext(GlobalContext); + const navigate = useNavigate(); + const buttonRef = useRef(null); + const params = useParams(); + const [oldEmail, setOldEmail] = useState(""); + const [oldFirstName, setOldFirstName] = useState(""); + const [oldLastName, setOldLastName] = useState(""); + const [id, setId] = useState(0); + + const { + register, + handleSubmit, + setError, + setValue, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const selectRole = [{ name: "role", value: "Customer" }]; + const selectStatus = [ + { key: "0", value: "Inactive" }, + { key: "1", value: "Active" }, + ]; + + const verify = [ + { key: "0", value: "No" }, + { key: "1", value: "Yes" }, + ]; + + const onSubmit = async (data) => { + try { + if (oldEmail !== data.email) { + const emailresult = await sdk.updateEmailByAdmin(data.email, id); + if (!emailresult.error) { + showToast(globalDispatch, "Email Updated", 1000); + } else { + if (emailresult.validation) { + const keys = Object.keys(emailresult.validation); + + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: emailresult.validation[field], + }); + } + } + } + } + sdk.setTable("user"); + const result = await sdk.callRestAPI( + { + id, + first_name: data.firstName, + last_name: data.lastName, + email: data.email, + role: data.role.toLowerCase(), + status: data.status, + verify: data.verify, + }, + "PUT", + ); + sdk.setTable("profile"); + const resultDob = await sdk.callRestAPI({ set: { dob: data.dob || null }, where: { user_id: id } }, "PUTWHERE"); // Note: Ideally it should be user_id but existing sdk only supports updating by id + + if (resultDob.error) { + setError("dob", { + type: "manual", + message: "Date of birth is required", + }); + } else if (!result.error) { + showToast(globalDispatch, "Updated", 4000); + navigate("/admin/customer"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("email", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + React.useEffect(() => { + if (state.saveChanges) { + buttonRef.current.click(); + globalDispatch({ + type: "SAVE_CHANGES", + payload: { + saveChanges: false, + }, + }); + } + }, [state.saveChanges]); + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "customer", + }, + }); + + (async function () { + try { + sdk.setTable("user"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + + sdk.setTable("profile"); + const { + list: [profile], + } = await sdk.callRestAPI({ payload: { user_id: result.model.id } }, "GETALL"); + + if (!result.error) { + setValue("firstName", result.model?.first_name); + setValue("lastName", result.model?.last_name); + setValue("email", result.model?.email); + setValue("role", result.model.role[0].toUpperCase() + result.model.role.slice(1)); + setValue("dob", !profile?.dob ? null : moment(profile.dob).format("yyyy-MM-DD")); + setValue("status", result.model?.status); + setValue("verify", result.model?.verify); + setOldEmail(result.model?.email); + setOldFirstName(result.model?.first_name); + setOldLastName(result.model?.last_name); + setId(result.model.id); + } + } catch (error) { + console.log("Error", error); + tokenExpireError(dispatch, error.message); + } + })(); + }, []); + return ( +
+
+

Edit Customer

+ +
+
+

ID

+

{id}

+
+
+ + +

{false}

+
+
+ + +

{false}

+
+
+ + +

{errors.email?.message}

+
+
+ + +

{false}

+
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+ ); +}; + +export default EditAdminCustomerPage; diff --git a/src/pages/Admin/Customer/ViewAdminCustomerPage.jsx b/src/pages/Admin/Customer/ViewAdminCustomerPage.jsx new file mode 100644 index 0000000..923a74b --- /dev/null +++ b/src/pages/Admin/Customer/ViewAdminCustomerPage.jsx @@ -0,0 +1,226 @@ +import React, { useState } from "react"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate, useParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import ViewAdminPageLayout from "@/layouts/ViewAdminPageLayout"; +import Icon from "@/components/Icons"; +import History from "@/components/History"; +import EditAdminCustomerPage from "./EditAdminCustomerPage"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import moment from "moment"; + +let sdk = new MkdSDK(); + +const ViewAdminCustomerPage = ({ page }) => { + const [userInfo, setUserInfo] = useState({}); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const { dispatch } = React.useContext(AuthContext); + const params = useParams(); + const navigate = useNavigate(); + const [activeTab, setActiveTab] = useState(0); + const [loading, setLoading] = useState(false); + + async function sendPasswordReset() { + setLoading(true); + try { + await sdk.forgot(userInfo.email, userInfo.role); + showToast(globalDispatch, "Email Sent"); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + setLoading(false); + } + + async function sendEmailVerification() { + try { + await sdk.callRawAPI("/v2/api/custom/ergo/resend-verification-email", { email: userInfo.email }, "POST"); + showToast(globalDispatch, "Email Sent"); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + const tabs = [ + { + key: 0, + name: "Profile Details", + component: + page === "view" ? ( + + ) : ( + + ), + }, + { + key: 1, + name: "History", + component: ( + + ), + }, + ]; + + async function fetchUser() { + try { + sdk.setTable("user"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + + sdk.setTable("profile"); + const { + list: [resultDob], + } = await sdk.callRestAPI( + { payload: { user_id: result.model.id } }, // Note: Should be user_id + "GETALL", + ); + + sdk.setTable("id_verification"); + const { + list: [resultIdVerification], + } = await sdk.callRestAPI( + { payload: { user_id: result.model.id } }, // Note: Should be user_id + "GETALL", + ); + setUserInfo({ ...result.model, dob: resultDob?.dob, id_verified: resultIdVerification?.status }); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "customer", + }, + }); + + fetchUser(); + }, []); + + return ( + +
+
    + {tabs.map((tab) => ( +
  • + +
  • + ))} +
+
+ + {tabs[activeTab].component} +
+ ); +}; + +const ProfileDetails = ({ userInfo, loading, sendPasswordReset, sendEmailVerification }) => { + const params = useParams(); + const navigate = useNavigate(); + const status = ["No", "Yes"]; + const id_verified = ["Pending", "Yes", "No"]; + + return ( + <> +
+
+
+

Profile Details

+
+ +
+
+
+

ID

+

{userInfo?.id}

+
+
+

First Name

+

{userInfo?.first_name}

+
+
+

Last Name

+

{userInfo?.last_name}

+
+
+

Email

+

{userInfo?.email}

+
+
+

Date of Birth

+

{userInfo?.dob == null ? "N/A" : moment(userInfo?.dob).format("MM/DD/yyyy")}

+
+
+

Role

+

{userInfo?.role}

+
+
+

Email Verified

+

{status[userInfo?.verify]}

+
+
+

ID Verified

+

{id_verified[userInfo.id_verified] ?? "N/A"}

+
+
+

Actions

+ + +
+
+
+ + ); +}; + +export default ViewAdminCustomerPage; diff --git a/src/pages/Admin/Devices/AdminDevicesPage.jsx.jsx b/src/pages/Admin/Devices/AdminDevicesPage.jsx.jsx new file mode 100644 index 0000000..23b0dbd --- /dev/null +++ b/src/pages/Admin/Devices/AdminDevicesPage.jsx.jsx @@ -0,0 +1,501 @@ +import React from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { useNavigate, useSearchParams, Link } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import Button from "@/components/Button"; +import PaginationHeader from "@/components/PaginationHeader"; +import { ID_PREFIX, NOTIFICATION_STATUS, NOTIFICATION_TYPE } from "@/utils/constants"; +import SwitchBulkMode from "@/components/SwitchBulkMode"; +import moment from "moment/moment"; +import TreeSDK from "@/utils/TreeSDK"; + +let sdk = new MkdSDK(); +const treeSdk = new TreeSDK(); +const loginStatusMapping = ["NO", "YES"]; +const statusMapping = ["INACTIVE", "ACTIVE"]; + +const columns = [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.DEVICE, + }, + { + header: "User ID", + accessor: "user_id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.USER, + }, + { + header: "Device UID", + accessor: "uid", + isSorted: true, + isSortedDesc: true, + }, + { + header: "Logged In", + accessor: "active", + isSorted: true, + isSortedDesc: true, + mapping: loginStatusMapping, + }, + { + header: "Last Login", + accessor: "last_login_time", + isSorted: true, + isSortedDesc: true, + format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"), + }, +]; + +export default function AdminDevicesPage() { + const { dispatch } = React.useContext(AuthContext); + const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext); + const [tableColumns, setTableColumns] = React.useState(columns); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + const [bulkMode, setBulkMode] = React.useState(false); + const [bulkSelected, setBulkSelected] = React.useState([]); + const [bulkStatus, setBulkStatus] = React.useState(""); + const [currentDevice, setCurrentDevice] = React.useState({}); + const [viewDevice, setViewDevice] = React.useState(false); + const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_device_filter") ?? ""); + + const { + reset, + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + defaultValues: parseSearchParams(searchParams), + }); + + function onSort(accessor) { + const columns = tableColumns; + const index = columns.findIndex((column) => column.accessor === accessor); + const column = columns[index]; + column.isSortedDesc = !column.isSortedDesc; + columns.splice(index, 1, column); + setTableColumns(() => [...columns]); + const sortedList = selector(data, column.isSortedDesc, accessor); + setCurrentTableData(sortedList); + } + function selector(users, isSortedDesc, accessor) { + if (accessor?.split(",").length > 1) { + accessor = accessor.split(",")[0]; + } + + return users.sort((a, b) => { + if (isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1; + } else { + return a[accessor] < b[accessor] ? 1 : -1; + } + } + if (!isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1; + } else { + return a[accessor] < b[accessor] ? -1 : 1; + } + } + }); + } + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum) { + const data = parseSearchParams(searchParams); + data.id = data.id?.replace(ID_PREFIX.DEVICE, ""); + try { + let filter = ["deleted_at,is"]; + if (data.id) { + filter.push(`id,eq,'${data.id}'`); + } + if (data.user_id) { + filter.push(`user_id,eq,${data.user_id}`); + } + if (data.active) { + filter.push(`active,eq,${data.active}`); + } + if (data.status) { + filter.push(`status,eq,${data.status}`); + } + const result = await treeSdk.getPaginate("device", { join: [], filter, page: pageNum || 1, size: limitNum, order: "update_at" }); + + const { list, total, limit, num_pages, page } = result; + const sortedList = selector(list, false); + setCurrentTableData(sortedList); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("status", data.status); + searchParams.set("active", data.active); + searchParams.set("user_id", data.user_id); + setSearchParams(searchParams); + localStorage.setItem("admin_device_filter", searchParams.toString()); + + getData(1, pageSize); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "device", + }, + }); + getData(1, pageSize); + }, []); + + async function logout() { } + + return ( + <> +
+
+
+

Notification

+
+
+
+ + +

{errors.id?.message}

+
+
+ + +

{errors.user_id?.message}

+
+ +
+ + +

{errors.status?.message}

+
+
+ + +

{errors.active?.message}

+
+
+ +
+
+ + + + {/*
+ +
*/} + + {bulkMode && ( +
+ + {bulkSelected.length > 0 ? ( +
+ + +
+ ) : null} +
+ )} + +
+
+ + + + {bulkMode && ( + + )} + {columns.map((column, index) => ( + + ))} + + + + {data.map((row, i) => { + return ( + + {bulkMode && ( + + )} + {tableColumns.map((cell, index) => { + if (cell.format) { + return ( + + ); + } + if (cell.accessor == "") { + return ( + + ); + } + if (cell.mapping) { + return ( + + ); + } + + if (cell.idPrefix) { + return ( + + ); + } + + return ( + + ); + })} + + ); + })} + +
onSort(column.accessor)} + > + {column.header} + {column.isSorted} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
+ { + if (bulkSelected.includes(row.id)) { + setBulkSelected((prev) => { + let copy = [...prev]; + copy.splice( + prev.findIndex((id) => id == row.id), + 1, + ); + return copy; + }); + } else { + setBulkSelected((prev) => [...prev, row.id]); + } + }} + checked={bulkSelected.includes(row.id)} + onChange={() => { }} + /> + + {cell.format(row[cell.accessor])} + + + + {cell.mapping[row[cell.accessor] ?? 0]} + + {cell.idPrefix + row[cell.accessor]} + + {row[cell.accessor]} +
+
+
+ + + ); +} diff --git a/src/pages/Admin/Email/AddAdminEmailPage.jsx b/src/pages/Admin/Email/AddAdminEmailPage.jsx new file mode 100644 index 0000000..2df6942 --- /dev/null +++ b/src/pages/Admin/Email/AddAdminEmailPage.jsx @@ -0,0 +1,158 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { tokenExpireError, AuthContext } from "@/authContext"; +const AddAdminEmailPage = () => { + const schema = yup + .object({ + slug: yup.string().required(), + subject: yup.string().required(), + html: yup.string().required(), + tag: yup.string().required(), + }) + .required(); + + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + + const navigate = useNavigate(); + const { + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const onSubmit = async (data) => { + let sdk = new MkdSDK(); + + try { + sdk.setTable("email"); + + const result = await sdk.callRestAPI( + { + slug: data.slug, + subject: data.subject, + html: data.html, + tag: data.tag, + }, + "POST", + ); + if (!result.error) { + navigate("/admin/email"); + showToast(globalDispatch, "Added"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("subject", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "email", + }, + }); + }, []); + + return ( +
+

Add Email

+
+
+ + +
+
+ + +

{errors.subject?.message}

+
+
+ + +

{errors.tag?.message}

+
+
+ + +

{errors.html?.message}

+
+ +
+
+ ); +}; + +export default AddAdminEmailPage; diff --git a/src/pages/Admin/Email/AdminEmailListPage.jsx b/src/pages/Admin/Email/AdminEmailListPage.jsx new file mode 100644 index 0000000..176321b --- /dev/null +++ b/src/pages/Admin/Email/AdminEmailListPage.jsx @@ -0,0 +1,92 @@ +import React from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import AddButton from "@/components/AddButton"; +import Table from "@/components/Table"; +import { ID_PREFIX } from "@/utils/constants"; +import TreeSDK from "@/utils/TreeSDK"; + +let sdk = new MkdSDK(); +let treeSdk = new TreeSDK(); + +const columns = [ + { + header: "ID", + accessor: "id", + idPrefix: ID_PREFIX.EMAIL, + }, + { + header: "Email Type", + accessor: "slug", + }, + { + header: "Subject", + accessor: "subject", + }, + { + header: "Tags", + accessor: "tag", + }, + + { + header: "Actions", + accessor: "", + }, +]; + +const AdminEmailListPage = () => { + const { dispatch } = React.useContext(AuthContext); + const [data, setCurrentTableData] = React.useState([]); + + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + + async function getData() { + try { + let filter = ["deleted_at,is"]; + const result = await treeSdk.getList("email", { join: [], filter, order: "update_at" }); + const { list } = result; + setCurrentTableData(list); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "email", + }, + }); + + getData(); + }, []); + + return ( + <> +
+
+

Emails

+ +
+
+ + + + + ); +}; + +export default AdminEmailListPage; diff --git a/src/pages/Admin/Email/EditAdminEmailPage.jsx b/src/pages/Admin/Email/EditAdminEmailPage.jsx new file mode 100644 index 0000000..15f1bf5 --- /dev/null +++ b/src/pages/Admin/Email/EditAdminEmailPage.jsx @@ -0,0 +1,170 @@ +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import { useNavigate, useParams } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "@/authContext"; + +let sdk = new MkdSDK(); + +const EditAdminEmailPage = () => { + const schema = yup + .object({ + subject: yup.string().required(), + html: yup.string().required(), + tag: yup.string().required(), + }) + .required(); + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const navigate = useNavigate(); + const [id, setId] = useState(0); + const [slug, setSlug] = useState(""); + const { + register, + handleSubmit, + setError, + setValue, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const params = useParams(); + + useEffect(function () { + globalDispatch({ + type: "SETPATH", + payload: { + path: "email", + }, + }); + + (async function () { + try { + sdk.setTable("email"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + if (!result.error) { + setValue("subject", result.model.subject); + setValue("html", result.model.html); + setValue("tag", result.model.tag); + setSlug(result.model.slug); + setId(result.model.id); + } + } catch (error) { + console.log("error", error); + tokenExpireError(dispatch, error.message); + } + })(); + }, []); + + const onSubmit = async (data) => { + try { + const result = await sdk.callRestAPI({ id, slug, subject: data.subject, html: data.html, tag: data.tag }, "PUT"); + + if (!result.error) { + showToast(globalDispatch, "Updated"); + navigate("/admin/email"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("html", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + return ( +
+

Edit Email

+
+
+ + +
+
+ + +

{errors.subject?.message}

+
+
+ + +

{errors.tag?.message}

+
+
+ + +

{errors.html?.message}

+
+ + +
+ ); +}; + +export default EditAdminEmailPage; diff --git a/src/pages/Admin/Email/ViewAdminEmailPage.jsx b/src/pages/Admin/Email/ViewAdminEmailPage.jsx new file mode 100644 index 0000000..43668c0 --- /dev/null +++ b/src/pages/Admin/Email/ViewAdminEmailPage.jsx @@ -0,0 +1,75 @@ +import React, { useState } from "react"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate, useParams } from "react-router-dom"; +import { GlobalContext } from "@/globalContext"; +import ViewAdminPageLayout from "@/layouts/ViewAdminPageLayout"; +import { AuthContext, tokenExpireError } from "@/authContext"; + +let sdk = new MkdSDK(); + +const ViewAdminEmailPage = () => { + const [emailInfo, setEmailInfo] = useState({}); + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const params = useParams(); + const navigate = useNavigate(); + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "email", + }, + }); + + (async function () { + try { + sdk.setTable("email"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + + setEmailInfo(result.model || {}); + console.log(result.model); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + })(); + }, []); + + return ( + +
+
+
+

Email Details

+
+
+
+

ID

+

{emailInfo.id}

+
+
+

Type

+

{emailInfo.slug}

+
+
+

Subject

+

{emailInfo.subject}

+
+
+

Tags

+

{emailInfo.tag}

+
+
+
+
+ ); +}; + +export default ViewAdminEmailPage; diff --git a/src/pages/Admin/Faq/AddAdminFaqPage.jsx b/src/pages/Admin/Faq/AddAdminFaqPage.jsx new file mode 100644 index 0000000..1aea3b2 --- /dev/null +++ b/src/pages/Admin/Faq/AddAdminFaqPage.jsx @@ -0,0 +1,176 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate } from "react-router-dom"; +import { tokenExpireError, AuthContext } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import AddAdminPageLayout from "@/layouts/AddAdminPageLayout"; + +import SunEditor, { buttonList } from "suneditor-react"; +import "suneditor/dist/css/suneditor.min.css"; +import { useState } from "react"; + +const AddAdminFaqPage = () => { + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const [answer, setAnswer] = useState(""); + + const schema = yup + .object({ + question: yup.string().required("Question is required"), + answer: yup.string(), + status: yup.number(), + }) + .required(); + + const { dispatch } = React.useContext(AuthContext); + + const navigate = useNavigate(); + const { + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const onSubmit = async (data) => { + if (answer == "") { + setError("answer", { + type: "manual", + message: "Answer is required", + }); + return; + } + let sdk = new MkdSDK(); + + try { + sdk.setTable("faq"); + + const result = await sdk.callRestAPI( + { + question: data.question, + answer, + status: data.status, + }, + "POST", + ); + if (!result.error) { + showToast(globalDispatch, "Added"); + navigate("/admin/faq"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("question", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + const onError = () => { + setError("answer", { + type: "manual", + message: "Answer is required", + }); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "faq", + }, + }); + }, []); + + return ( + +
+
+ + +

{errors.question?.message}

+
+
+ + +
+
+ + setAnswer(content)} + placeholder="Add your answer here" + setOptions={{ buttonList: buttonList.complex }} + /> +

{errors.answer?.message}

+
+ +
+ + +
+ +
+ ); +}; + +export default AddAdminFaqPage; diff --git a/src/pages/Admin/Faq/AdminFaqListPage.jsx b/src/pages/Admin/Faq/AdminFaqListPage.jsx new file mode 100644 index 0000000..44240f8 --- /dev/null +++ b/src/pages/Admin/Faq/AdminFaqListPage.jsx @@ -0,0 +1,143 @@ +import React from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { GlobalContext, showToast } from "@/globalContext"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { getNonNullValue } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import AddButton from "@/components/AddButton"; +import Faq from "@/components/Faq"; +import { ID_PREFIX } from "@/utils/constants"; +import TreeSDK from "@/utils/TreeSDK"; + +let sdk = new MkdSDK(); +let treeSdk = new TreeSDK(); + +const AdminFaqListPage = () => { + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch, state } = React.useContext(GlobalContext); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + + const schema = yup.object({ + question: yup.string(), + answer: yup.string(), + }); + const { + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum, data) { + try { + let filter = ["deleted_at,is"]; + + const result = await treeSdk.getPaginate("faq", { join: [], filter, page: pageNum || 1, size: limitNum, order: "update_at" }); + + const { list, total, limit, num_pages, page } = result; + + setCurrentTableData(list); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "faq", + }, + }); + + getData(1, pageSize); + }, []); + + React.useEffect(() => { + if (state.deleted) { + globalDispatch({ + type: "DELETED", + payload: { + deleted: false, + }, + }); + getData(currentPage, pageSize); + } + }, [state.deleted]); + + return ( + <> +
+
+

FAQ

+ +
+
+ +
+
+ {data && + data.map((faq) => ( + + ))} +
+
+ + + ); +}; + +export default AdminFaqListPage; diff --git a/src/pages/Admin/Faq/EditAdminFaqPage.jsx b/src/pages/Admin/Faq/EditAdminFaqPage.jsx new file mode 100644 index 0000000..cfd212c --- /dev/null +++ b/src/pages/Admin/Faq/EditAdminFaqPage.jsx @@ -0,0 +1,229 @@ +import React, { useEffect, useState, useRef } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import { useNavigate, useParams } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import EditAdminPageLayout from "@/layouts/EditAdminPageLayout"; +import SunEditor, { buttonList } from "suneditor-react"; +import "suneditor/dist/css/suneditor.min.css"; + +let sdk = new MkdSDK(); + +const EditAdminFaqPage = () => { + const { dispatch } = React.useContext(AuthContext); + const [answer, setAnswer] = useState(""); + const schema = yup + .object({ + question: yup.string().required(), + answer: yup.string(), + }) + .required(); + const { dispatch: globalDispatch, state } = React.useContext(GlobalContext); + const buttonRef = useRef(null); + const navigate = useNavigate(); + const [id, setId] = useState(0); + const { + register, + handleSubmit, + setError, + setValue, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const params = useParams(); + + useEffect(function () { + (async function () { + try { + sdk.setTable("faq"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + console.log(result); + if (!result.error) { + setValue("question", result.model.question); + setValue("status", result.model.status); + setAnswer(result.model.answer); + setId(result.model.id); + } + } catch (error) { + console.log("error", error); + tokenExpireError(dispatch, error.message); + } + })(); + }, []); + + const onError = () => { + if (answer == "") { + setError("answer", { + type: "manual", + message: "Answer is required", + }); + } + }; + + const onSubmit = async (data) => { + if (answer == "") { + setError("answer", { + type: "manual", + message: "Answer is required", + }); + return; + } + try { + const result = await sdk.callRestAPI( + { + id: id, + question: data.question, + answer, + status: data.status, + }, + "PUT", + ); + + if (!result.error) { + showToast(globalDispatch, "Updated"); + navigate("/admin/faq"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("question", { + type: "manual", + message: error.message, + }); + } + }; + useEffect(() => { + if (state.saveChanges) { + buttonRef.current.click(); + globalDispatch({ + type: "SAVE_CHANGES", + payload: { + saveChanges: false, + }, + }); + } + }, [state.saveChanges]); + + useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "faq", + }, + }); + }, []); + + return ( + +
+
+ + +

{errors.question?.message}

+
+ +
+ + +
+ +
+ + setAnswer(content)} + setContents={answer} + name="answer" + setOptions={{ buttonList: buttonList.complex }} + /> +

{errors.answer?.message}

+
+ +
+ + + +
+ +
+ ); +}; + +export default EditAdminFaqPage; diff --git a/src/pages/Admin/Hashtag/AddAdminHashtagPage.jsx b/src/pages/Admin/Hashtag/AddAdminHashtagPage.jsx new file mode 100644 index 0000000..fc6cd39 --- /dev/null +++ b/src/pages/Admin/Hashtag/AddAdminHashtagPage.jsx @@ -0,0 +1,121 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate } from "react-router-dom"; +import { tokenExpireError, AuthContext } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import AddAdminPageLayout from "@/layouts/AddAdminPageLayout"; + +const AddAdminHashtag = () => { + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const schema = yup + .object({ + name: yup.string().required("Name is required"), + }) + .required(); + + const { dispatch } = React.useContext(AuthContext); + + const navigate = useNavigate(); + const { + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const onSubmit = async (data) => { + let sdk = new MkdSDK(); + + try { + sdk.setTable("hashtag"); + const result = await sdk.callRestAPI( + { + name: data.name, + }, + "POST", + ); + if (!result.error) { + showToast(globalDispatch, "Added"); + navigate("/admin/hashtag"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("name", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "hashtag", + }, + }); + }, []); + + return ( + +
+
+
+ + +

{errors.name?.message}

+
+
+ + +
+ +
+
+ ); +}; + +export default AddAdminHashtag; diff --git a/src/pages/Admin/Hashtag/AdminHashTagPage.jsx b/src/pages/Admin/Hashtag/AdminHashTagPage.jsx new file mode 100644 index 0000000..40691ff --- /dev/null +++ b/src/pages/Admin/Hashtag/AdminHashTagPage.jsx @@ -0,0 +1,294 @@ +import React from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import AddButton from "@/components/AddButton"; +import Button from "@/components/Button"; +import Table from "@/components/Table"; +import PaginationHeader from "@/components/PaginationHeader"; +import ReactHtmlTableToExcel from "react-html-table-to-excel"; +import { ID_PREFIX } from "@/utils/constants"; +import { adminColumns, applySetting } from "@/utils/adminPortalColumns"; +import TreeSDK from "@/utils/TreeSDK"; + +let sdk = new MkdSDK(); +let treeSdk = new TreeSDK(); + +const AdminHashTagPage = () => { + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch, state } = React.useContext(GlobalContext); + const [tableColumns, setTableColumns] = React.useState([]); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); + + const schema = yup.object({ + id: yup.string(), + type: yup.string(), + category: yup.string(), + }); + const { + reset, + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: parseSearchParams(searchParams), + }); + + function onSort(accessor) { + const columns = tableColumns; + const index = columns.findIndex((column) => column.accessor === accessor); + const column = columns[index]; + column.isSortedDesc = !column.isSortedDesc; + columns.splice(index, 1, column); + setTableColumns(() => [...columns]); + const sortedList = selector(data, column.isSortedDesc, accessor); + setCurrentTableData(sortedList); + } + function selector(users, isSortedDesc, accessor) { + if (accessor?.split(",").length > 1) { + accessor = accessor.split(",")[0]; + } + + return users.sort((a, b) => { + if (isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1; + } else { + return a[accessor] < b[accessor] ? 1 : -1; + } + } + if (!isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1; + } else { + return a[accessor] < b[accessor] ? -1 : 1; + } + } + }); + } + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum) { + const data = parseSearchParams(searchParams); + data.id = data.id?.replace(ID_PREFIX.HASHTAGS, ""); + + try { + let filter = ["deleted_at,is"]; + if (data.id) { + filter.push(`id,eq,${data.id}`); + } + if (data.name) { + filter.push(`name,cs,${data.name}`); + } + const result = await treeSdk.getPaginate("hashtag", { join: [], filter, page: pageNum || 1, size: limitNum, order: "update_at" }); + const { list, total, limit, num_pages, page } = result; + + const sortedList = selector(list, false); + setCurrentTableData(sortedList); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("name", data.name); + + setSearchParams(searchParams); + getData(1, pageSize); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "hashtag", + }, + }); + + (async function () { + await fetchColumnOrder(); + getData(1, pageSize); + })(); + }, []); + + React.useEffect(() => { + if (state.deleted) { + globalDispatch({ + type: "DELETED", + payload: { + deleted: false, + }, + }); + getData(currentPage, pageSize); + } + }, [state.deleted]); + + async function fetchColumnOrder() { + sdk.setTable("settings"); + const payload = { key_name: "admin_hashtag_column_order" }; + try { + const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE"); + if (Array.isArray(result.list) && result.list.length > 0) { + setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_hashtag)); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + return ( + <> +
+
+

Hashtag Search

+ +
+ +
+
+ + +

{errors.id?.message}

+
+ +
+ + +

{errors.name?.message}

+
+
+ + + + + +
+ + Change Column Order + {" "} + +
+ +
+
+
+ + + + + ); +}; + +export default AdminHashTagPage; diff --git a/src/pages/Admin/Hashtag/EditAdminHashTagPage.jsx b/src/pages/Admin/Hashtag/EditAdminHashTagPage.jsx new file mode 100644 index 0000000..6dbd30d --- /dev/null +++ b/src/pages/Admin/Hashtag/EditAdminHashTagPage.jsx @@ -0,0 +1,138 @@ +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import { useNavigate, useParams } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import EditAdminPageLayout from "@/layouts/EditAdminPageLayout"; + +let sdk = new MkdSDK(); + +const EditAdminHashTagPage = () => { + const { dispatch } = React.useContext(AuthContext); + const schema = yup + .object({ + name: yup.string().required("Name is required"), + }) + .required(); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const navigate = useNavigate(); + const [category, setCategory] = useState(""); + const [id, setId] = useState(0); + const { + register, + handleSubmit, + setError, + setValue, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const params = useParams(); + + useEffect(function () { + (async function () { + try { + sdk.setTable("hashtag"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + if (!result.error) { + setValue("name", result.model.name); + setId(result.model.id); + } + } catch (error) { + console.log("error", error); + tokenExpireError(dispatch, error.message); + } + })(); + }, []); + + const onSubmit = async (data) => { + try { + const result = await sdk.callRestAPI( + { + id: id, + name: data.name, + }, + "PUT", + ); + + if (!result.error) { + showToast(globalDispatch, "Updated"); + navigate("/admin/hashtag"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("name", { + type: "manual", + message: error.message, + }); + } + }; + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "hashtag", + }, + }); + }, []); + + return ( + +
+
+ + +

{errors.name?.message}

+
+
+ + +
+ +
+ ); +}; + +export default EditAdminHashTagPage; diff --git a/src/pages/Admin/Host/AddAdminHostPage.jsx b/src/pages/Admin/Host/AddAdminHostPage.jsx new file mode 100644 index 0000000..5118d33 --- /dev/null +++ b/src/pages/Admin/Host/AddAdminHostPage.jsx @@ -0,0 +1,334 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { tokenExpireError, AuthContext } from "@/authContext"; +import AddAdminPageLayout from "@/layouts/AddAdminPageLayout"; +import moment from "moment"; +import commonPasswords from "@/assets/json/common-passwords.json"; + +const AddAdminHostPage = () => { + const schema = yup.object({ + firstName: yup.string().required("First name is required"), + lastName: yup.string().required("Last name is required"), + email: yup.string().email().required("Email is required"), + dob: yup + .string() + .test("is-not-in-future", "Not a valid date", (val) => { + console.log("testing here", val); + if (val == "") return true; + const date = new Date(val); + return date < new Date(); + }) + .test("must-be-at-least-18yo", "Must be at least 18 years of age", (val) => { + return moment().diff(moment(val), "years") > 18; + }), + password: yup + .string() + .required("Password is required") + .min(10, "Password must be at least 10 characters long") + .matches(/^(?=.*[0-9])/, "Password must contain at least one digit(0-9)") + .matches(/^(?=.*[a-z])/, "Password must contain at least one lowercase letter") + .matches(/^(?=.*[A-Z])/, "Password must contain at least one uppercase letter") + .matches(/^(?=.*[!@#\$%\^&\*])/, "Password must contain at least one symbol") + .test("is-not-dictionary", "Password must not contain a common word", (val) => { + return commonPasswords.every((pass) => !val.includes(pass)); + }) + .test("does-not-contain-user-info", "Password must not contain your name or date of birth", (val, ctx) => { + const d = moment(ctx.parent.dob); + return [ctx.parent.firstName, ctx.parent.lastName, d.format("yyyyMMDD"), d.format("DDMMyyyy"), d.format("MMDDyyyy"), d.format("YYMMDD"), d.format("MMDDYY"), d.format("DDMMYY")].every( + (field) => field.trim() == "" || !val.toLowerCase().includes(field.toLowerCase()), + ); + }), + role: yup.string().required(), + status: yup.string().required(), + verify: yup.string().required(), + }); + + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const navigate = useNavigate(); + const { + register, + handleSubmit, + setError, + trigger, + formState: { errors, dirtyFields }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { password: "" }, + criteriaMode: "all", + mode: "all", + }); + + const onSubmit = async (data) => { + console.log("submitting", data); + let sdk = new MkdSDK(); + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/register", + { + firstName: data.firstName, + lastName: data.lastName, + status: data.status || 0, + email: data.email, + password: data.password, + dob: data.dob || null, + verify: data.verify || 0, + role: "host", + payment_method_set: 0, + }, + "POST", + ); + + if (!result.error) { + showToast(dispatch, "Added"); + navigate("/admin/host"); + } else { + if (result?.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + + // register device + sdk.setTable("device"); + await sdk.callRestAPI({ active: 1, user_id: result.user_id, last_login_time: new Date().toISOString().split("T")[0], uid: localStorage.getItem("device-uid") }, "POST"); + } catch (error) { + setError("firstName", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "host", + }, + }); + }, []); + + function getPasswordErrors() { + var arr = []; + if (Array.isArray(errors.password?.types.matches)) { + arr = [...errors.password.types.matches]; + } + if (typeof errors.password?.types?.matches === "string") { + arr.push(errors.password.types.matches); + } + if (errors.password?.types?.min) { + arr.push(errors.password.types.min); + } + if (errors.password?.types["does-not-contain-user-info"]) { + arr.push(errors.password?.types["does-not-contain-user-info"]); + } + if (errors.password?.types["is-not-dictionary"]) { + arr.push(errors.password?.types["is-not-dictionary"]); + } + return arr; + } + const passwordErrors = getPasswordErrors(); + + return ( + +
+
+
+ + +

{errors.firstName?.message}

+
+
+ + +

{errors.lastName?.message}

+
+
+ + +

{errors.email?.message}

+
+
+ + +

{errors.dob?.message}

+
+
+ + +
+
+ + +
+
+ + +
+
+ + { + trigger("password"); + }, + })} + autoComplete="new-password" + className={` mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.password?.message ? "border-red-500" : ""}`} + /> + {dirtyFields.password && ( +
+ {passwordErrors.map((msg, idx) => ( +

{msg}

+ ))} +
+ )} +
+
+ + +
+ +
+
+ ); +}; + +export default AddAdminHostPage; diff --git a/src/pages/Admin/Host/AdminHostListPage.jsx b/src/pages/Admin/Host/AdminHostListPage.jsx new file mode 100644 index 0000000..10c6c21 --- /dev/null +++ b/src/pages/Admin/Host/AdminHostListPage.jsx @@ -0,0 +1,446 @@ +import React from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { useForm } from "react-hook-form"; +import * as yup from "yup"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import Button from "@/components/Button"; +import AddButton from "@/components/AddButton"; +import Table from "@/components/Table"; +import PaginationHeader from "@/components/PaginationHeader"; +import ReactHtmlTableToExcel from "react-html-table-to-excel"; +import { ID_PREFIX, IMAGE_STATUS } from "@/utils/constants"; +import { adminColumns, applySetting } from "@/utils/adminPortalColumns"; +import ProfileImagePreviewModal from "../User/ProfileImagePreviewModal"; +import RejectProfileImageModal from "../User/RejectProfileImageModal"; + +let sdk = new MkdSDK(); + +const AdminHostListPage = () => { + const { dispatch: globalDispatch, state } = React.useContext(GlobalContext); + const { dispatch } = React.useContext(AuthContext); + const [tableColumns, setTableColumns] = React.useState([]); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_host_filter") ?? ""); + const navigate = useNavigate(); + + const [activePicture, setActivePicture] = React.useState(""); + const [pictureModal, setPictureModal] = React.useState(false); + const [activeRow, setActiveRow] = React.useState({}); + + const schema = yup.object({ + id: yup.string(), + email: yup.string(), + }); + + const { + reset, + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: parseSearchParams(searchParams), + }); + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + function onSort(accessor) { + const columns = tableColumns; + const index = columns.findIndex((column) => column.accessor === accessor); + const column = columns[index]; + column.isSortedDesc = !column.isSortedDesc; + columns.splice(index, 1, column); + setTableColumns(() => [...columns]); + const sortedList = selector(data, column.isSortedDesc, accessor); + setCurrentTableData(sortedList); + } + function selector(users, isSortedDesc, accessor) { + if (accessor?.split(",").length > 1) { + accessor = accessor.split(",")[0]; + } + + return users.sort((a, b) => { + if (isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1; + } else { + return a[accessor] < b[accessor] ? 1 : -1; + } + } + if (!isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1; + } else { + return a[accessor] < b[accessor] ? -1 : 1; + } + } + }); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum) { + const data = parseSearchParams(searchParams); + data.id = data.id?.replace(ID_PREFIX.HOST, ""); + + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/user/PAGINATEHOST", + { + where: [data ? `${data.id ? `ergo_user.id = '${data.id}'` : "1"} AND ${data.email ? `ergo_user.email LIKE '%${data.email}%'` : "1"}` : "role = 'host'", "ergo_user.deleted_at IS NULL"], + page: pageNum, + sortId: "create_at", + direction: "DESC", + limit: limitNum, + }, + "POST", + ); + const { list, total, limit, num_pages, page } = result; + + const sortedList = selector(list, false); + setCurrentTableData(sortedList); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + const onSubmit = (data) => { + searchParams.set("email", data.email); + searchParams.set("id", data.id); + setSearchParams(searchParams); + localStorage.setItem("admin_host_filter", searchParams.toString()); + getData(0, pageSize); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "host", + }, + }); + + (async function () { + await fetchColumnOrder(); + await getData(0, pageSize); + })(); + }, []); + + React.useEffect(() => { + if (state.deleted) { + globalDispatch({ + type: "DELETED", + payload: { + deleted: false, + }, + }); + getData(currentPage, pageSize); + } + }, [state.deleted]); + + async function fetchColumnOrder() { + sdk.setTable("settings"); + const payload = { key_name: "admin_host_column_order" }; + try { + const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE"); + if (Array.isArray(result.list) && result.list.length > 0) { + setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_host)); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + async function approveImage(id) { + sdk.setTable("user"); + try { + await sdk.callRestAPI({ id, is_photo_approved: IMAGE_STATUS.APPROVED }, "PUT"); + await getData(1, pageSize); + showToast(globalDispatch, "Successful"); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + return ( + <> +
+
+

Hosts

+ +
+
+
+ + +

{errors.id?.message}

+
+
+ + +

{errors.email?.message}

+
+
+ + + + + +
+ + Change Column Order + + +
+ +
+
+
+ + + {tableColumns.map((column, index) => ( + + ))} + + + + {data.map((row, i) => { + return ( + + {tableColumns.map((cell, index) => { + if (cell.accessor.split(",").length > 1) { + return ( + + ); + } + + if (cell.accessor === "") { + return ( + + ); + } + if (cell.statusMapping) { + return ( + + ); + } + + if (cell.accessor == "num_properties") { + return ( + + ); + } + if (cell.accessor.includes("payout") || cell.amountField) { + return ( + + ); + } + + if (cell.idPrefix) { + return ( + + ); + } + + return ( + + ); + })} + + ); + })} + +
onSort(column.accessor)} + > + {column.header} + {column.isSorted} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
+ {cell.accessor.split(",").map((accessor, i) => ( + + {row[accessor.trim()]} + + ))} + + {row.photo ? ( + + ) : ( + No Photo + )} + + + + + {" "} + {cell.statusMapping[row[cell.accessor]]} + + + + + ${(row[cell.accessor] ? row[cell.accessor] : 0).toFixed(2)} + + {cell.idPrefix + row[cell.accessor]} + + {row[cell.accessor]} +
+
+
+ + setPictureModal(false)} + /> + setActiveRow({})} + data={activeRow} + onSuccess={() => getData(currentPage, pageSize)} + /> + + ); +}; + +export default AdminHostListPage; diff --git a/src/pages/Admin/Host/EditAdminHostPage.jsx b/src/pages/Admin/Host/EditAdminHostPage.jsx new file mode 100644 index 0000000..3d37794 --- /dev/null +++ b/src/pages/Admin/Host/EditAdminHostPage.jsx @@ -0,0 +1,361 @@ +import React, { useState, useRef } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate, useParams } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import moment from "moment"; + +let sdk = new MkdSDK(); + +const EditAdminHostPage = () => { + const schema = yup + .object({ + firstName: yup.string().required(), + lastName: yup.string().required(), + email: yup.string().email().required(), + password: yup.string(), + status: yup.string(), + dob: yup.string(), + role: yup.string(), + verify: yup.string(), + }) + .required(); + + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch, state } = React.useContext(GlobalContext); + const navigate = useNavigate(); + const params = useParams(); + const buttonRef = useRef(null); + const [oldEmail, setOldEmail] = useState(""); + const [oldFirstName, setOldFirstName] = useState(""); + const [oldLastName, setOldLastName] = useState(""); + const [id, setId] = useState(0); + + const { + register, + handleSubmit, + setError, + setValue, + trigger, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const selectRole = [ + // { name: "role", value: "Admin" }, + { name: "role", value: "Host" }, + // { name: "role", value: "Customer" } + ]; + const selectStatus = [ + { key: "0", value: "Inactive" }, + { key: "1", value: "Active" }, + ]; + + const verify = [ + { key: "0", value: "No" }, + { key: "1", value: "Yes" }, + ]; + + const onSubmit = async (data) => { + console.log("submitting", data); + try { + if (oldEmail !== data.email) { + const emailresult = await sdk.updateEmailByAdmin(data.email, id); + if (!emailresult.error) { + showToast(globalDispatch, "Email Updated", 1000); + } else { + if (emailresult.validation) { + const keys = Object.keys(emailresult.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: emailresult.validation[field], + }); + } + } + } + } + + sdk.setTable("user"); + const result = await sdk.callRestAPI( + { + id, + first_name: data.firstName, + last_name: data.lastName, + email: data.email, + role: data.role.toLowerCase(), + status: data.status, + verify: data.verify || 0, + }, + "PUT", + ); + sdk.setTable("profile"); + const resultDob = await sdk.callRestAPI({ set: { dob: data.dob }, where: { user_id: id } }, "PUTWHERE"); // Note: Ideally it should be user_id but existing sdk only supports updating by id + + if (resultDob.error) { + setError("dob", { + type: "manual", + message: "Date of birth is required", + }); + } else if (!result.error) { + showToast(globalDispatch, "Updated", 4000); + navigate("/admin/host"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("email", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + React.useEffect(() => { + if (state.saveChanges) { + buttonRef.current.click(); + globalDispatch({ + type: "SAVE_CHANGES", + payload: { + saveChanges: false, + }, + }); + } + }, [state.saveChanges]); + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "host", + }, + }); + + (async function () { + try { + sdk.setTable("user"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + + sdk.setTable("profile"); + const { + list: [profile], + } = await sdk.callRestAPI({ payload: { user_id: result.model.id } }, "GETALL"); + + if (!result.error) { + setValue("firstName", result.model.first_name); + setValue("lastName", result.model.last_name); + setValue("email", result.model.email); + setValue("role", result.model.role[0].toUpperCase() + result.model.role.slice(1)); + setValue("dob", !profile?.dob ? null : moment(profile.dob).format("yyyy-MM-DD")); + setValue("status", result.model.status); + setOldEmail(result.model.email); + setValue("verify", result.model.verify); + setOldFirstName(result.model.first_name); + setOldLastName(result.model.last_name); + setId(result.model.id); + } + } catch (error) { + console.log("Error", error); + tokenExpireError(dispatch, error.message); + } + })(); + }, []); + return ( +
+
+

Edit Host

+ +
+
+

ID

+

{id}

+
+
+ + +

{false}

+
+
+ + +

{false}

+
+
+ + +

{errors.email?.message}

+
+
+ + +

{false}

+
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+ ); +}; + +export default EditAdminHostPage; diff --git a/src/pages/Admin/Host/ViewAdminHostPage.jsx b/src/pages/Admin/Host/ViewAdminHostPage.jsx new file mode 100644 index 0000000..d347b21 --- /dev/null +++ b/src/pages/Admin/Host/ViewAdminHostPage.jsx @@ -0,0 +1,259 @@ +import React, { useState } from "react"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate, useParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import ViewAdminPageLayout from "@/layouts/ViewAdminPageLayout"; +import History from "@/components/History"; +import Payment from "@/components/Payment"; +import Icon from "@/components/Icons"; +import EditAdminHostPage from "./EditAdminHostPage"; +import { ID_PREFIX } from "@/utils/constants"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import moment from "moment"; + +let sdk = new MkdSDK(); + +const ViewAdminHostPage = ({ page }) => { + const [userInfo, setUserInfo] = useState({}); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const { dispatch } = React.useContext(AuthContext); + const params = useParams(); + const [activeTab, setActiveTab] = useState(0); + const [loading, setLoading] = useState(false); + + const tabs = [ + { + key: 0, + name: "Profile Details", + component: + page === "view" ? ( + + ) : ( + + ), + }, + { + key: 1, + name: "History", + component: ( + + ), + }, + { + key: 2, + name: "Payment", + component: ( + + ), + }, + ]; + + async function fetchUser() { + try { + sdk.setTable("user"); + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/user/PAGINATEHOST", + { + where: [params?.id ? `${params?.id ? `ergo_user.id = ${Number(params?.id)}` : "1"} ` : "role = 'host'"], + page: 1, + limit: 1, + }, + "POST", + ); + + sdk.setTable("profile"); + const { + list: [resultDob], + } = await sdk.callRestAPI( + { payload: { user_id: result.list[0].id } }, // Note: Should be user_id + "GETALL", + ); + + sdk.setTable("id_verification"); + const { + list: [resultIdVerification], + } = await sdk.callRestAPI( + { payload: { user_id: result.list[0].id } }, // Note: Should be user_id + "GETALL", + ); + setUserInfo({ ...result.list[0], dob: resultDob?.dob, id_verified: resultIdVerification?.status }); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + async function sendPasswordReset() { + setLoading(true); + try { + await sdk.forgot(userInfo.email, userInfo.role); + showToast(globalDispatch, "Email Sent"); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + setLoading(false); + } + + async function sendEmailVerification() { + try { + await sdk.callRawAPI("/v2/api/custom/ergo/resend-verification-email", { email: userInfo.email }, "POST"); + showToast(globalDispatch, "Email Sent"); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "host", + }, + }); + fetchUser(); + }, []); + + return ( + +
+
    + {tabs.map((tab) => ( +
  • + +
  • + ))} +
+
+ + {tabs[activeTab].component} +
+ ); +}; + +const ProfileDetails = ({ userInfo, loading, sendPasswordReset, sendEmailVerification }) => { + const status = ["Inactive", "Active", "Suspend"]; + const verified = ["No", "Yes"]; + const id_verified = ["Pending", "Yes", "No"]; + const params = useParams(); + const navigate = useNavigate(); + + return ( + <> +
+
+
+

Profile Details

+
+ +
+
+
+

ID

+

{ID_PREFIX.HOST + userInfo?.id}

+
+
+

First Name

+

{userInfo?.first_name}

+
+
+

Last Name

+

{userInfo?.last_name}

+
+
+

Email

+

{userInfo?.email}

+
+
+

Date of Birth

+

{userInfo.dob == null ? "N/A" : moment(userInfo.dob).format("MM/DD/yyyy")}

+
+
+

Properties

+

+ {userInfo?.num_properties} + +

+
+
+

Status

+

{status[userInfo?.status]}

+
+
+

Email Verified

+

{verified[userInfo?.verify]}

+
+
+

ID Verified

+

{id_verified[userInfo?.id_verified] ?? "N/A"}

+
+
+

Actions

+ + +
+
+
+ + ); +}; + +export default ViewAdminHostPage; diff --git a/src/pages/Admin/IdVerification/AddAdminIdVerificationPage.jsx b/src/pages/Admin/IdVerification/AddAdminIdVerificationPage.jsx new file mode 100644 index 0000000..bfa916d --- /dev/null +++ b/src/pages/Admin/IdVerification/AddAdminIdVerificationPage.jsx @@ -0,0 +1,372 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate } from "react-router-dom"; +import { tokenExpireError, AuthContext } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import AddAdminPageLayout from "@/layouts/AddAdminPageLayout"; +import CustomComboBoxV2 from "@/components/CustomComboBoxV2"; + +const AddAdminIdVerificationPage = () => { + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const [frontImage, setFrontImage] = React.useState(); + const [backImage, setBackImage] = React.useState(); + const [selectedVerification, setSelectedVerification] = React.useState("Passport"); + let sdk = new MkdSDK(); + + const schema = yup + .object({ + type: yup.string().required(), + expiry_date: yup.string().test("is-not-in-past", "Not a valid date", (val) => { + if (val == "") return false; + const date = new Date(val); + return date > new Date(); + }), + status: yup.number().required().integer(), + user_id: yup.string().required("Please select a user"), + }) + .required(); + + const { dispatch } = React.useContext(AuthContext); + + const navigate = useNavigate(); + const { + control, + setValue, + register, + handleSubmit, + setError, + clearErrors, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { user_id: "" }, + }); + + const selectStatus = [ + { key: "0", value: "Pending" }, + { key: "1", value: "Verified" }, + { key: "2", value: "Declined" }, + ]; + + const selectType = [ + { key: "Passport", value: "Passport" }, + { key: "Driver's License", value: "Driver's License" }, + ]; + + const handleTypeChange = (e) => { + setSelectedVerification(e.target.value); + }; + + const handleImageUpload = async (file) => { + const formData = new FormData(); + for (let i = 0; i < file.length; i++) { + formData.append("file", file[i]); + } + try { + const upload = await sdk.uploadImage(formData); + return upload.url; + } catch (error) { + tokenExpireError(dispatch, error.message); + } + }; + + async function fetchUsersFiltered(emailFilter, setter, initialUserId) { + try { + var initial = []; + if (+initialUserId) { + const initialUserResult = await sdk.callRawAPI("/v2/api/custom/ergo/user/PAGINATE", { page: 1, limit: 1, where: [`${initialUserId ? `ergo_user.id = ${+initialUserId}` : ""}`] }, "POST"); + if (Array.isArray(initialUserResult.list)) { + initial = initialUserResult.list; + } + } + if (emailFilter) { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/user/PAGINATE", { page: 1, limit: 10, where: [`ergo_user.email LIKE '%${emailFilter}%'`] }, "POST"); + if (Array.isArray(result.list)) { + setter([...initial, ...result.list]); + } + } + } catch (err) { + console.log("err", err); + } + } + + const verifyUserAndUploadImage = async (data) => { + try { + if (!frontImage) { + setError("front_image", { + type: "manual", + message: "Image is required", + }); + } + if (!backImage) { + setError("back_image", { + type: "manual", + message: "Image is required", + }); + } + if (!frontImage || (!backImage && selectedVerification != "Passport")) return; + data.frontImage = await handleImageUpload(frontImage); + if (selectedVerification == "Passport") { + data.backImage = null; + } + if (backImage) { + data.backImage = await handleImageUpload(backImage); + } + onSubmit(data); + } catch (error) { + console.log("Error", error); + setError("type", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + const onSubmit = async (data) => { + console.log("data", data); + try { + sdk.setTable("id_verification"); + const result = await sdk.callRestAPI( + { + type: data.type, + expiry_date: data.expiry_date, + status: data.status, + image_front: data.frontImage, + image_back: data.backImage, + user_id: data.user_id, + }, + "POST", + ); + if (!result.error) { + showToast(globalDispatch, "Added"); + navigate("/admin/id_verification"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + tokenExpireError(dispatch, error.message); + if (error.message == "Validation error") { + updateIdVerification({ + type: data.type, + expiry_date: data.expiry_date, + status: data.status, + image_front: data.frontImage, + image_back: data.backImage, + user_id: data.user_id, + }); + return; + } + setError("type", { + type: "manual", + message: error.message, + }); + } + }; + + const onError = (x, y) => { + if (!frontImage) { + setError("front_image", { + type: "manual", + message: "Image is required", + }); + } + if (!backImage && selectedVerification != "Password") { + setError("back_image", { + type: "manual", + message: "Image is required", + }); + } + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "id_verification", + }, + }); + }, []); + + async function updateIdVerification(data) { + try { + sdk.setTable("id_verification"); + await sdk.callRestAPI({ set: data, where: { user_id: data.user_id } }, "PUTWHERE"); + showToast(globalDispatch, "Updated"); + navigate("/admin/id_verification"); + } catch (err) { + tokenExpireError(dispatch, err); + showToast(globalDispatch, err, 4000, "ERROR"); + } + } + + return ( + +
+
+
+ + setValue("user_id", val)} + valueField={"id"} + labelField={"email"} + getItems={fetchUsersFiltered} + className="relative flex h-[40px] items-center rounded border px-3" + placeholder="User email" + /> +

{errors.user_id?.message}

+
+ + +

{errors.type?.message}

+
+ +
+ + +

{errors.expiry_date?.message}

+
+ +
+ + +

{errors.status?.message}

+
+ +
+ + { + setFrontImage(e.target.files); + clearErrors("front_image"); + }} + /> +

{errors.front_image?.message}

+
+
+ + { + setBackImage(e.target.files); + clearErrors("back_image"); + }} + /> +

{errors.back_image?.message}

+
+ +
+ + +
+
+
+ ); +}; + +export default AddAdminIdVerificationPage; diff --git a/src/pages/Admin/IdVerification/AdminIdVerificationListPage.jsx b/src/pages/Admin/IdVerification/AdminIdVerificationListPage.jsx new file mode 100644 index 0000000..ed39439 --- /dev/null +++ b/src/pages/Admin/IdVerification/AdminIdVerificationListPage.jsx @@ -0,0 +1,528 @@ +import React from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import Button from "@/components/Button"; +import PaginationHeader from "@/components/PaginationHeader"; +import AddButton from "@/components/AddButton"; +import CsvDownloadButton from "react-json-to-csv"; +import { ID_PREFIX } from "@/utils/constants"; +import { adminColumns, applySetting } from "@/utils/adminPortalColumns"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import DeclineVerificationModal from "./DeclineVerificationModal"; + +let sdk = new MkdSDK(); + +const AdminIdVerificationListPage = () => { + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const [tableColumns, setTableColumns] = React.useState([]); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + + const [searchParams, setSearchParams] = useSearchParams(); + // TODO: find a better way to do this + const [searchParams2] = useSearchParams(localStorage.getItem("admin_idv_filter") ?? ""); + + const navigate = useNavigate(); + const [activeRow, setActiveRow] = React.useState({}); + + const schema = yup.object({ + status: yup.string(), + email: yup.string(), + }); + const { + reset, + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: (() => { + let fromSearch = parseSearchParams(searchParams); + if (Object.keys(fromSearch).length > 0) { + return fromSearch; + } + return parseSearchParams(searchParams2); + })(), + }); + + function onSort(accessor) { + const columns = tableColumns; + const index = columns.findIndex((column) => column.accessor === accessor); + const column = columns[index]; + column.isSortedDesc = !column.isSortedDesc; + columns.splice(index, 1, column); + setTableColumns(() => [...columns]); + const sortedList = selector(data, column.isSortedDesc, accessor); + setCurrentTableData(sortedList); + } + function selector(users, isSortedDesc, accessor) { + if (accessor?.split(",").length > 1) { + accessor = accessor.split(",")[0]; + } + + return users.sort((a, b) => { + if (isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1; + } else { + return a[accessor] < b[accessor] ? 1 : -1; + } + } + if (!isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1; + } else { + return a[accessor] < b[accessor] ? -1 : 1; + } + } + }); + } + + const selectStatus = [ + { key: "", value: "All" }, + { key: "0", value: "Pending" }, + { key: "1", value: "Verified" }, + { key: "2", value: "Declined" }, + ]; + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum) { + let data = parseSearchParams(searchParams); + data = Object.keys(data).length < 1 ? parseSearchParams(searchParams2) : data; + + data.id = data.id?.replace(ID_PREFIX.ID_VERIFICATION, ""); + data.user_id = data.user_id?.replace(ID_PREFIX.USER, ""); + + try { + sdk.setTable("id_verification"); + + const result = await callCustomAPI( + "id-verification", + "post", + { + where: [ + data + ? `${data.user_id ? `ergo_user.id = ${data.user_id}` : "1"} AND ${data.id ? `ergo_id_verification.id = ${data.id}` : "1"} AND ${ + data.email ? `ergo_user.email LIKE '%${data.email}%'` : "1" + } AND ${![null, undefined].includes(data.status) ? `ergo_id_verification.status = ${data.status}` : "1"} AND ${ + data.type ? `ergo_id_verification.type LIKE '%${data.type}%'` : "1" + } AND ${data.dob ? `dob = ${data.dob}` : "1"} AND ${data.first_name ? `first_name LIKE '%${data.first_name}%'` : "1"} AND ${ + data.last_name ? `last_name LIKE '%${data.last_name}%'` : "1" + } AND ${data.role ? `role = ${data.role}` : "1"}` + : 1, + ], + page: pageNum, + limit: limitNum, + sortId: "update_at", + direction: "DESC", + }, + "PAGINATE", + ); + const { list, total, limit, num_pages, page } = result; + + const sortedList = selector(list, false); + setCurrentTableData(sortedList); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + const changeVerificationStatus = async (data, status) => { + try { + sdk.setTable("id_verification"); + const result = await sdk.callRestAPI( + { + id: data.id, + status: status, + }, + "PUT", + ); + + if (!result.error) { + showToast(globalDispatch, "Successful"); + await getData(currentPage, pageSize); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + showToast(globalDispatch, result.validation[field], 4000, "ERROR"); + } + } + } + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + }; + + const onSubmit = (data) => { + console.log("submitting", data); + searchParams.set("email", data.email); + searchParams.set("status", data.status); + searchParams.set("user_id", data.user_id); + searchParams.set("id", data.id); + setSearchParams(searchParams); + localStorage.setItem("admin_idv_filter", searchParams.toString()); + getData(1, pageSize); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "id_verification", + }, + }); + + (async function () { + await fetchColumnOrder(); + await getData(1, pageSize); + })(); + }, []); + + async function fetchColumnOrder() { + sdk.setTable("settings"); + const payload = { key_name: "admin_id_verification_column_order" }; + try { + const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE"); + if (Array.isArray(result.list) && result.list.length > 0) { + setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_id_verification)); + } + } catch (err) { + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + async function sendApproveEmail(data) { + try { + const tmpl = await sdk.getEmailTemplate("id-verification-approved"); + const body = tmpl.html?.replace(new RegExp("{{{type}}}", "g"), data.type).replace(new RegExp("{{{first_name}}}", "g"), data.first_name); + + await sdk.sendEmail(data.email, tmpl.subject, body); + } catch (err) { + console.log("err", err); + } + } + + return ( + <> +
+
+

ID Verification

+ +
+
+
+ + +

{errors.id?.message}

+
+
+ + +

{errors.user_id?.message}

+
+
+ + +

{errors.type?.message}

+
+
+ + +

+
+
+ +
+ + + +
+ + Change Column Order + + +
+ +
+
+ + + + {tableColumns.map((column, index) => ( + + ))} + + + + {data.map((row, i) => { + return ( + + {tableColumns.map((cell, index) => { + if (cell.accessor == "") { + return ( + + ); + } + if (cell.accessor == "image_front") { + return ( + + ); + } + if (cell.accessor == "image_back") { + return ( + + ); + } + if (cell.mapping) { + return ( + + ); + } + if (cell.accessor.includes("email")) { + return ( + + ); + } + if (cell.idPrefix) { + return ( + + ); + } + + return ( + + ); + })} + + ); + })} + +
onSort(column.accessor)} + > + {column.header} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
+
+ {row.status != 1 && ( + + )} + {row.status != 2 && ( + + )} +
+
+
+ image +
+
+
+ {row[cell.accessor] != null && ( + image + )} +
+
+ + {" "} + {cell.mapping[row[cell.accessor]]} + + + {row[cell.accessor]} + + {cell.idPrefix + row[cell.accessor]} + + {row[cell.accessor]} +
+
+
+ + setActiveRow({})} + data={activeRow} + onSuccess={() => getData(currentPage, pageSize)} + /> + + ); +}; + +export default AdminIdVerificationListPage; diff --git a/src/pages/Admin/IdVerification/DeclineVerificationModal.jsx b/src/pages/Admin/IdVerification/DeclineVerificationModal.jsx new file mode 100644 index 0000000..d95f59f --- /dev/null +++ b/src/pages/Admin/IdVerification/DeclineVerificationModal.jsx @@ -0,0 +1,122 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { Dialog, Transition } from "@headlessui/react"; +import { useContext, useState } from "react"; +import { Fragment } from "react"; + +export default function DeclineVerificationModal({ modalOpen, data, closeModal, onSuccess }) { + const { dispatch } = useContext(AuthContext); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const [loading, setLoading] = useState(); + + async function onSubmit(e) { + e.preventDefault(); + setLoading(true); + const sdk = new MkdSDK(); + const formData = new FormData(e.target); + const reason = formData.get("reason"); + sdk.setTable("property_spaces_images"); + try { + sdk.setTable("id_verification"); + await sdk.callRestAPI( + { + id: data.id, + status: 2, + }, + "PUT", + ); + + const tmpl = await sdk.getEmailTemplate("id-verification-declined"); + const body = tmpl.html?.replace(new RegExp("{{{reason}}}", "g"), reason).replace(new RegExp("{{{type}}}", "g"), data.type).replace(new RegExp("{{{first_name}}}", "g"), data.first_name); + + await sdk.sendEmail(data.email, tmpl.subject, body); + showToast(globalDispatch, "Successful, email sent to user"); + + onSuccess(); + e.target.reset(); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + closeModal(); + setLoading(false); + } + + return ( + <> + + + +
+ + +
+
+ + + + Decline Reason + + +
+ + +
+
+
+
+
+
+
+ + ); +} diff --git a/src/pages/Admin/IdVerification/EditAdminIdVerificationPage.jsx b/src/pages/Admin/IdVerification/EditAdminIdVerificationPage.jsx new file mode 100644 index 0000000..9b047cf --- /dev/null +++ b/src/pages/Admin/IdVerification/EditAdminIdVerificationPage.jsx @@ -0,0 +1,204 @@ +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import { useNavigate, useParams } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "@/authContext"; + +let sdk = new MkdSDK(); + +const EditAdminIdVerificationPage = () => { + const { dispatch } = React.useContext(AuthContext); + const schema = yup + .object({ + type: yup.string().required(), + expiry_date: yup.string().matches(/[0-9]{4}-[0-9]{2}-[0-9]{2}/, "Date Format YYYY-MM-DD"), + status: yup.number().required().positive().integer(), + image: yup.string().required(), + user_id: yup.number().required().positive().integer(), + }) + .required(); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const navigate = useNavigate(); + const [type, setType] = useState(""); + const [expiry_date, setExpiryDate] = useState(""); + const [status, setStatus] = useState(0); + const [image, setImage] = useState(""); + const [user_id, setUserId] = useState(0); + const [id, setId] = useState(0); + const { + register, + handleSubmit, + setError, + setValue, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const params = useParams(); + + useEffect(function () { + (async function () { + try { + sdk.setTable("id_verification"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + if (!result.error) { + setType(result.model.type); + setExpiryDate(result.model.expiry_date); + setStatus(result.model.status); + setImage(result.model.image); + setUserId(result.model.user_id); + setId(result.model.id); + } + } catch (error) { + console.log("error", error); + tokenExpireError(dispatch, error.message); + } + })(); + }, []); + + const onSubmit = async (data) => { + try { + const result = await sdk.callRestAPI( + { + id: id, + type: data.type, + expiry_date: data.expiry_date, + status: data.status, + image: data.image, + user_id: data.user_id, + }, + "PUT", + ); + + if (!result.error) { + showToast(globalDispatch, "Updated"); + navigate("/admin/id_verification"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("type", { + type: "manual", + message: error.message, + }); + } + }; + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "id_verification", + }, + }); + }, []); + + return ( +
+

Edit IdVerification

+
+
+ + +

{errors.type?.message}

+
+ +
+ + +

{errors.expiry_date?.message}

+
+ +
+ + +

{errors.status?.message}

+
+ +
+ + +

{errors.image?.message}

+
+ +
+ + +

{errors.user_id?.message}

+
+ + +
+
+ ); +}; + +export default EditAdminIdVerificationPage; diff --git a/src/pages/Admin/NotFoundPage.jsx b/src/pages/Admin/NotFoundPage.jsx new file mode 100644 index 0000000..3826a0b --- /dev/null +++ b/src/pages/Admin/NotFoundPage.jsx @@ -0,0 +1,7 @@ +import React from "react"; + +const NotFoundPage = () => { + return
Not found
; +}; + +export default NotFoundPage; diff --git a/src/pages/Admin/Notification/AdminNotificationListPage.jsx b/src/pages/Admin/Notification/AdminNotificationListPage.jsx new file mode 100644 index 0000000..ea11706 --- /dev/null +++ b/src/pages/Admin/Notification/AdminNotificationListPage.jsx @@ -0,0 +1,658 @@ +import React from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { useSearchParams, Link } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { clearSearchParams, notificationTime, parseSearchParams } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import Button from "@/components/Button"; +import PaginationHeader from "@/components/PaginationHeader"; +import { ID_PREFIX, NOTIFICATION_STATUS, NOTIFICATION_TYPE } from "@/utils/constants"; +import SwitchBulkMode from "@/components/SwitchBulkMode"; +import moment from "moment"; +import TreeSDK from "@/utils/TreeSDK"; +import { LoadingButton } from "@/components/frontend"; + +let sdk = new MkdSDK(); +let treeSdk = new TreeSDK(); + +const statusMapping = ["Not Viewed", "Viewed"]; +const typeMapping = ["New Space Added", "New Property Space Images Added", "Profile Picture Changed", "Property Space Edited", "New Review Added", "New Payout", "New Id Verification Submitted"]; + +const columns = [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.NOTIFICATION, + }, + { + header: "User ID", + accessor: "user_id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.USER, + }, + { + header: "Type", + accessor: "type", + isSorted: true, + isSortedDesc: true, + mapping: typeMapping, + }, + { + header: "Message", + accessor: "message", + isSorted: true, + isSortedDesc: true, + }, + { + header: "Notification Time", + accessor: "notification_time", + isSorted: true, + isSortedDesc: true, + }, + { + header: "Email", + nested: "user", + accessor: "email", + isSorted: true, + isSortedDesc: true, + }, + { + header: "Status", + accessor: "status", + isSorted: true, + isSortedDesc: true, + mapping: statusMapping, + }, + { + header: "Actions", + accessor: "", + }, +]; + +export default function AdminNotificationPage() { + const { dispatch } = React.useContext(AuthContext); + const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext); + const [tableColumns, setTableColumns] = React.useState(columns); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + const [bulkMode, setBulkMode] = React.useState(false); + const [bulkSelected, setBulkSelected] = React.useState([]); + const [bulkStatus, setBulkStatus] = React.useState(""); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_notification_filter") ?? ""); + const [bulkLoading, setBulkLoading] = React.useState(false); + + const { + reset, + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + defaultValues: parseSearchParams(searchParams), + }); + + function onSort(accessor) { + const columns = tableColumns; + const index = columns.findIndex((column) => column.accessor === accessor); + const column = columns[index]; + column.isSortedDesc = !column.isSortedDesc; + columns.splice(index, 1, column); + setTableColumns(() => [...columns]); + const sortedList = selector(data, column.isSortedDesc, accessor); + setCurrentTableData(sortedList); + } + function selector(users, isSortedDesc, accessor) { + if (accessor?.split(",").length > 1) { + accessor = accessor.split(",")[0]; + } + + return users.sort((a, b) => { + if (isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1; + } else { + return a[accessor] < b[accessor] ? 1 : -1; + } + } + if (!isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1; + } else { + return a[accessor] < b[accessor] ? -1 : 1; + } + } + }); + } + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum) { + const data = parseSearchParams(searchParams); + data.id = data.id?.replace(ID_PREFIX.NOTIFICATION, ""); + + sdk.setTable("notification"); + + try { + let filter = []; + if (data.id) { + filter.push(`ergo_notification.id,eq,${data.id}`); + } + if (data.status) { + filter.push(`ergo_notification.status,eq,${data.status}`); + } + if (data.create_at) { + filter.push(`ergo_notification.create_at,eq,'${data.create_at}'`); + } + if (data.type) { + filter.push(`ergo_notification.type,eq,${data.type}`); + } + if (data.email) { + filter.push(`ergo_user.email,cs,${data.email}`); + } +console.log("filter",filter) + let result = await treeSdk.getPaginate("notification", { + filter, + join: ["user"], + page: pageNum || 1, + size: limitNum, + order: "update_at", + }); + + const { list, total, limit, num_pages, page } = result; + + const sortedList = selector(list, false); + setCurrentTableData(sortedList); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("status", data.status); + searchParams.set("create_at", data.create_at); + searchParams.set("type", data.type); + searchParams.set("email", data.email); + setSearchParams(searchParams); + localStorage.setItem("admin_notification_filter", searchParams.toString()); + + getData(1, pageSize); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "notification", + }, + }); + getData(1, pageSize); + }, []); + + async function bulkChangeStatus() { + if (bulkStatus == "") return; + setBulkLoading(true) + sdk.setTable("notification"); + try { + await Promise.all(bulkSelected.map((id) => sdk.callRestAPI({ id: Number(id), status: bulkStatus }, "PUT"))); + + const actualChangeCount = data.reduce((acc, curr) => (curr.status != Number(bulkStatus) && bulkSelected.includes(curr.id) ? acc + 1 : acc), 0); + if (Number(bulkStatus) == NOTIFICATION_STATUS.NOT_ADDRESSED) { + globalDispatch({ type: "SET_NOTIFICATION_COUNT", payload: globalState.adminNotificationCount + actualChangeCount }); + } else { + globalDispatch({ type: "SET_NOTIFICATION_COUNT", payload: globalState.adminNotificationCount - actualChangeCount }); + } + showToast(globalDispatch, "Successful"); + setBulkStatus(""); + setBulkSelected([]); + document.querySelector(".none").value = ""; + getData(1, pageSize); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + setBulkLoading(false) + } + + async function markAsAddressed(id) { + sdk.setTable("notification"); + try { + await sdk.callRestAPI({ id, status: NOTIFICATION_STATUS.ADDRESSED }, "PUT"); + globalDispatch({ type: "SET_NOTIFICATION_COUNT", payload: globalState.adminNotificationCount > 0 ? globalState.adminNotificationCount - 1 : 0 }); + showToast(globalDispatch, "Successful"); + getData(1, pageSize); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + async function markAsUnAddressed(id) { + sdk.setTable("notification"); + try { + await sdk.callRestAPI({ id, status: NOTIFICATION_STATUS.NOT_ADDRESSED }, "PUT"); + globalDispatch({ type: "SET_NOTIFICATION_COUNT", payload: globalState.adminNotificationCount + 1 }); + showToast(globalDispatch, "Successful"); + getData(1, pageSize); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + function getActionRoute(type, actor_id, user_id) { + switch (type) { + case NOTIFICATION_TYPE.EDIT_PROPERTY_SPACE: + case NOTIFICATION_TYPE.CREATE_SPACE: + return `/admin/property_spaces?id=${actor_id}`; + case NOTIFICATION_TYPE.CREATE_PROPERTY_SPACE_IMAGE: + return `/admin/property_spaces_images?id=${actor_id}`; + case NOTIFICATION_TYPE.EDIT_USER_PICTURE: + return `/admin/user?id=${actor_id}`; + case NOTIFICATION_TYPE.ADD_PAYOUT: + return `/admin/payout?id=${actor_id}`; + case NOTIFICATION_TYPE.ADD_REVIEW: + return `/admin/review?id=${actor_id}`; + case NOTIFICATION_TYPE.NEW_ID_VERIFICATION: + return `/admin/id_verification?id=${actor_id}`; + default: + return ""; + } + } + + return ( + <> +
+
+
+

Notification

+
+
+
+ + +

{errors.id?.message}

+
+ +
+ + +

{errors.email?.message}

+
+ +
+ + +

{errors.status?.message}

+
+
+ + +

{errors.type?.message}

+
+
+ + +

{errors.create_at?.message}

+
+
+ +
+
+ + + +
+ +
+ + {bulkMode && ( +
+ + {bulkSelected.length > 0 ? ( +
+ + bulkChangeStatus()} + > + Bulk Save + +
+ ) : null} +
+ )} + +
+
+ + + + {bulkMode && ( + + )} + {columns.map((column, index) => ( + + ))} + + + + {data.map((row, i) => { + return ( + + {bulkMode && ( + + )} + {tableColumns.map((cell, index) => { + if (cell.format) { + return ( + + ); + } + if (cell.accessor == "") { + return ( + + ); + } + if (cell.mapping) { + return ( + + ); + } + if (cell.accessor == "notification_time") { + return ( + + ); + } + + if (cell.idPrefix) { + return ( + + ); + } + + if (cell.nested) { + return ( + + ); + } + + return ( + + ); + })} + + ); + })} + +
onSort(column.accessor)} + > + {column.header} + {column.isSorted} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
+ { + if (bulkSelected.includes(row.id)) { + setBulkSelected((prev) => { + let copy = [...prev]; + copy.splice( + prev.findIndex((id) => id == row.id), + 1, + ); + return copy; + }); + setBulkStatus() + } else { + setBulkSelected((prev) => [...prev, row.id]); + } + }} + checked={bulkSelected.includes(row.id)} + onChange={() => { }} + /> + + {cell.format(row[cell.accessor])} + + {row.status == NOTIFICATION_STATUS.NOT_ADDRESSED ? ( + + ) : ( + + )} + + + + {cell.mapping[row[cell.accessor] ?? 0]} + + {notificationTime(row["notification_time"])} + + {cell.idPrefix + row[cell.accessor]} + + {row[cell.nested][cell.accessor]} + + {row[cell.accessor]} +
+
+
+ + + ); +} diff --git a/src/pages/Admin/Payout/AddAdminPayoutPage.jsx b/src/pages/Admin/Payout/AddAdminPayoutPage.jsx new file mode 100644 index 0000000..9287e00 --- /dev/null +++ b/src/pages/Admin/Payout/AddAdminPayoutPage.jsx @@ -0,0 +1,313 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate } from "react-router-dom"; +import { tokenExpireError, AuthContext } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import AddAdminPageLayout from "@/layouts/AddAdminPageLayout"; + +const AddAdminPayoutPage = () => { + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const schema = yup.object({ + host_id: yup.string(), + customer_id: yup.string(), + host_name: yup.string(), + customer_name: yup.string(), + total: yup.number().typeError("Total must be a number").required(), + tax: yup.number().typeError("Tax must be a number").required(), + commission: yup.number().typeError("Commission must be a number").required(), + booking_id: yup.number().typeError("Booking id must be a number").required().positive().integer(), + status: yup.number().required(), + }); + + const { dispatch } = React.useContext(AuthContext); + let sdk = new MkdSDK(); + + const navigate = useNavigate(); + const { + clearErrors, + register, + handleSubmit, + setError, + setValue, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const selectStatus = [ + { key: 0, value: "Pending" }, + { key: 1, value: "Initiated" }, + { key: 2, value: "Paid" }, + { key: 3, value: "Cancelled" }, + ]; + + async function getSettings() { + try { + sdk.setTable("settings"); + // TODO: figure out a solution here for OR operation + const result = await sdk.callRestAPI( + { + // payload: "key_name = 'tax' OR key_name = 'commission'", + page: 1, + limit: 2, + }, + "PAGINATE", + ); + const { list } = result; + setValue("tax", list.find((setting) => setting.key_name === "tax").key_value); + setValue("commission", list.find((setting) => setting.key_name === "commission").key_value); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + + async function checkBookingID(id) { + if (!id) return; + try { + let sdk = new MkdSDK(); + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/booking/details", + { + where: [`ergo_booking.id=${id}`], + }, + "POST", + ); + + if (result.error || !result.list || !result.list.id) throw new Error(); + clearErrors("booking_id"); + setValue("host_name", result.list.host_first_name + " " + result.list.host_last_name); + setValue("customer_name", result.list.customer_first_name + " " + result.list.customer_last_name); + setValue("host_id", result.list.host_id); + setValue("customer_id", result.list.customer_id); + + console.log("booking", result.list); + } catch (error) { + console.log("ERROR", error); + setError("booking_id", { + type: "manual", + message: "Booking with this ID does not exist", + }); + } + } + + const onSubmit = async (data) => { + console.log("submitting,", data); + try { + console.log(data); + sdk.setTable("payout"); + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/payout/POST", + { + host_id: Number(data.host_id), + customer_id: Number(data.customer_id), + total: data.total, + tax: data.tax, + commission: data.commission, + booking_id: data.booking_id, + status: data.status, + }, + "POST", + ); + if (!result.error) { + showToast(globalDispatch, "Added"); + navigate("/admin/payout"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("host_id", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "payout", + }, + }); + getSettings(); + }, []); + + return ( + +
+
+ + checkBookingID(e.target.value)} + /> +

{errors.booking_id?.message}

+
+ +
+ + +

{errors.host_name?.message}

+
+
+ + +

{errors.customer_name?.message}

+
+ +
+ +
+ $ + +
+

{errors.total?.message}

+
+ +
+ +
+ $ + +
+ +

{errors.tax?.message}

+
+ +
+ +
+ $ + +
+

{errors.commission?.message}

+
+ +
+ + +

{errors.status?.message}

+
+ +
+ + +
+
+
+ ); +}; + +export default AddAdminPayoutPage; diff --git a/src/pages/Admin/Payout/AdminPayoutListPage.jsx b/src/pages/Admin/Payout/AdminPayoutListPage.jsx new file mode 100644 index 0000000..5dad96f --- /dev/null +++ b/src/pages/Admin/Payout/AdminPayoutListPage.jsx @@ -0,0 +1,450 @@ +import React, { Fragment } from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import AddButton from "@/components/AddButton"; +import Button from "@/components/Button"; +import PaginationHeader from "@/components/PaginationHeader"; +import { Menu, Transition } from "@headlessui/react"; +import Icon from "@/components/Icons"; +import moment from "moment"; +import CsvDownloadButton from "react-json-to-csv"; +import { ID_PREFIX } from "@/utils/constants"; + +let sdk = new MkdSDK(); + +const AdminPayoutListPage = () => { + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + const [massPayout, setMassPayout] = React.useState(false); + const [payouts, setPayouts] = React.useState([]); + + const [searchParams, setSearchParams] = useSearchParams(); + // TODO: find a better way to do this + const [searchParams2] = useSearchParams(localStorage.getItem("admin_payout_filter") ?? ""); + + const navigate = useNavigate(); + + const schema = yup.object({ + host_name: yup.string(), + customer_name: yup.string(), + status: yup.string(), + }); + const { + reset, + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: (() => { + let fromSearch = parseSearchParams(searchParams); + if (Object.keys(fromSearch).length > 0) { + return fromSearch; + } + return parseSearchParams(searchParams2); + })(), + }); + + const selectPayoutStatus = [ + { key: "", value: "All" }, + { key: "0", value: "Pending" }, + { key: "1", value: "Initiated" }, + { key: "2", value: "Paid" }, + { key: "3", value: "Cancelled" }, + ]; + + const payoutMapping = [ + { key: "0", value: "Pending" }, + { key: "1", value: "Initiated" }, + { key: "2", value: "Paid" }, + { key: "3", value: "Cancelled" }, + ]; + + function onSort(accessor, direction) {} + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum) { + let data = parseSearchParams(searchParams); + data = Object.keys(data).length < 1 ? parseSearchParams(searchParams2) : data; + + data.id = data.id?.replace(ID_PREFIX.PAYOUT, ""); + + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/payout/PAGINATE", + { + where: [ + data + ? `${data.id ? `ergo_payout.id LIKE '%${data.id}%'` : "1"} AND ${ + data.customer_name ? `customer.first_name LIKE '%${data.customer_name}%' OR customer.last_name LIKE '%${data.customer_name}%'` : "1" + } AND ${data.status ? `ergo_payout.status LIKE '%${data.status}%'` : "1"} AND ${ + data.host_name ? `ergo_user.first_name LIKE '%${data.host_name}%' OR ergo_user.last_name LIKE '%${data.host_name}%'` : "1" + }` + : 1, + "ergo_payout.deleted_at IS NULL", + ], + page: pageNum, + limit: limitNum, + sortId: "update_at", + direction: "DESC", + }, + "POST", + ); + + const { list, total, limit, num_pages, page } = result; + setCurrentTableData(list); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("host_name", data.host_name); + searchParams.set("customer_name", data.customer_name); + searchParams.set("status", data.status); + + setSearchParams(searchParams); + localStorage.setItem("admin_payout_filter", searchParams.toString()); + + getData(1, pageSize); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "payout", + }, + }); + + (async function () { + await getData(1, pageSize); + })(); + }, []); + + const onBulkSubmit = async (data) => { + if (data.bulk_status == 1) { + data.initiated_at = new Date().toISOString(); + } + try { + await Promise.all(payouts.map((id) => sdk.callRawAPI("/v2/api/custom/ergo/payout/PUT", { id, status: data.bulk_status, initiated_at: data.initiated_at }, "POST"))); + showToast(globalDispatch, "Successful"); + setPayouts([]); + setMassPayout(false); + getData(1, pageSize); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + }; + + return ( + <> +
+
+

Payout Search

+ +
+
+
+ + +

{errors.id?.message}

+
+ +
+ + +

{errors.host_name?.message}

+
+ +
+ + +

{errors.customer_name?.message}

+
+
+ + +

{errors.status?.message}

+
+
+ +
+ + +
+ + +
+ + {payouts.length > 0 && massPayout ? ( + <> +
+
+ + +
+ +
+ + ) : null} + +
+ {data.map((data, index) => ( + + ))} +
+ + + ); +}; + +export default AdminPayoutListPage; diff --git a/src/pages/Admin/Payout/EditAdminPayoutPage.jsx b/src/pages/Admin/Payout/EditAdminPayoutPage.jsx new file mode 100644 index 0000000..f179e8a --- /dev/null +++ b/src/pages/Admin/Payout/EditAdminPayoutPage.jsx @@ -0,0 +1,317 @@ +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import { useNavigate, useParams } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import EditAdminPageLayout from "@/layouts/EditAdminPageLayout"; + +let sdk = new MkdSDK(); + +const EditAdminPayoutPage = () => { + const { dispatch } = React.useContext(AuthContext); + const schema = yup + .object({ + // host_id: yup.number().required().positive().integer(), + // customer_id: yup.number().required().positive().integer(), + // property_id: yup.number().required().positive().integer(), + total: yup.number().required().positive().typeError("total must be a number"), + tax: yup.number().required().typeError("tax must be a number"), + commission: yup.number().required().typeError("commission must be a number"), + booking_id: yup.number().required().positive().integer().typeError("booking id must be a number"), + status: yup.string(), + }) + .required(); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const navigate = useNavigate(); + const [id, setId] = useState(0); + const [customerId, setCustomerId] = useState(0); + const [hostId, setHostId] = useState(0); + const [propertyId, setPropertyId] = useState(0); + const { + register, + handleSubmit, + setError, + setValue, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const params = useParams(); + + const selectStatus = [ + { key: "0", value: "Pending" }, + { key: "1", value: "initiated" }, + { key: "2", value: "Paid" }, + { key: "3", value: "Cancelled" }, + ]; + + useEffect(function () { + (async function () { + try { + sdk.setTable("payout"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + if (!result.error) { + setHostId(result.model.host_id); + setCustomerId(result.model.customer_id); + setPropertyId(result.model.property_id); + setValue("total", result.model.total); + setValue("tax", result.model.tax ?? 0); + setValue("commission", result.model.commission ?? 0); + setValue("booking_id", result.model.booking_id); + setValue("status", result.model.status); + setId(result.model.id); + } + } catch (error) { + console.log("error", error); + tokenExpireError(dispatch, error.message); + } + })(); + }, []); + + const onSubmit = async (data) => { + try { + let editedPayout = { + id: id, + host_id: hostId, + customer_id: customerId, + property_id: propertyId, + total: data.total, + tax: data.tax, + commission: data.commission, + booking_id: data.booking_id, + status: data.status, + }; + + if (editedPayout.status == "1") { + let todayDate = new Date(); + editedPayout.initiated_at = todayDate.toISOString(); + } + const result = await sdk.callRawAPI("/v2/api/custom/ergo/payout/PUT", { ...editedPayout }, "POST"); + + if (!result.error) { + showToast(globalDispatch, "Updated"); + navigate("/admin/payout"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("host_id", { + type: "manual", + message: error.message, + }); + } + }; + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "payout", + }, + }); + }, []); + + return ( + +
+ {/*
+ + +

+ {errors.host_id?.message} +

+
+ + +
+ + +

+ {errors.customer_id?.message} +

+
+ + +
+ + +

+ {errors.property_id?.message} +

+
+
+ + +

+ {errors.booking_id?.message} +

+
*/} +
+ +
+ $ + +
+

{errors.total?.message}

+
+ +
+ +
+ $ + +
+ +

{errors.tax?.message}

+
+ +
+ +
+ $ + +
+

{errors.commission?.message}

+
+ +
+ + +
+ +
+ + +
+
+
+ ); +}; + +export default EditAdminPayoutPage; diff --git a/src/pages/Admin/PayoutMethods/AdminPayoutMethodListPage.jsx b/src/pages/Admin/PayoutMethods/AdminPayoutMethodListPage.jsx new file mode 100644 index 0000000..2ec67bb --- /dev/null +++ b/src/pages/Admin/PayoutMethods/AdminPayoutMethodListPage.jsx @@ -0,0 +1,529 @@ +import React from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { useSearchParams, Link } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import Button from "@/components/Button"; +import PaginationHeader from "@/components/PaginationHeader"; +import { ID_PREFIX, NOTIFICATION_STATUS, NOTIFICATION_TYPE } from "@/utils/constants"; +import SwitchBulkMode from "@/components/SwitchBulkMode"; +import moment from "moment"; +import TreeSDK from "@/utils/TreeSDK"; + +let sdk = new MkdSDK(); +let treeSdk = new TreeSDK(); + +const columns = [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.NOTIFICATION, + }, + { + header: "Host ID", + accessor: "host_id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.HOST, + }, + { + header: "Account Holder Name", + accessor: "account_name", + isSorted: true, + isSortedDesc: true, + }, + { + header: "Routing number", + accessor: "routing_number", + isSorted: true, + isSortedDesc: true, + }, + { + header: "Account Number", + accessor: "account_number", + isSorted: true, + isSortedDesc: true, + }, + { + header: "Host Email", + nested: "user", + accessor: "email", + isSorted: true, + isSortedDesc: true, + }, + // { + // header: "Actions", + // accessor: "", + // }, +]; + +export default function AdminPayoutMethodListPage() { + const { dispatch } = React.useContext(AuthContext); + const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext); + const [tableColumns, setTableColumns] = React.useState(columns); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + const [bulkMode, setBulkMode] = React.useState(false); + const [bulkSelected, setBulkSelected] = React.useState([]); + const [bulkStatus, setBulkStatus] = React.useState(""); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_payout_method_filter") ?? ""); + + const { + reset, + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + defaultValues: parseSearchParams(searchParams), + }); + + function onSort(accessor) { + const columns = tableColumns; + const index = columns.findIndex((column) => column.accessor === accessor); + const column = columns[index]; + column.isSortedDesc = !column.isSortedDesc; + columns.splice(index, 1, column); + setTableColumns(() => [...columns]); + const sortedList = selector(data, column.isSortedDesc, accessor); + setCurrentTableData(sortedList); + } + function selector(users, isSortedDesc, accessor) { + if (accessor?.split(",").length > 1) { + accessor = accessor.split(",")[0]; + } + + return users.sort((a, b) => { + if (isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1; + } else { + return a[accessor] < b[accessor] ? 1 : -1; + } + } + if (!isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1; + } else { + return a[accessor] < b[accessor] ? -1 : 1; + } + } + }); + } + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum) { + const data = parseSearchParams(searchParams); + data.id = data.id?.replace(ID_PREFIX.PAYMENT_METHOD, ""); + data.host_id = data.host_id?.replace(ID_PREFIX.HOST, ""); + + try { + let filter = []; + if (data.id) { + filter.push(`ergo_payout_method.id,eq,${data.id}`); + } + if (data.host_id) { + filter.push(`ergo_payout_method.host_id,eq,${data.host_id}`); + } + if (data.account_number) { + filter.push(`ergo_payout_method.account_number,cs,${data.account_number}`); + } + if (data.account_name) { + filter.push(`ergo_payout_method.account_name,cs,${data.account_name}`); + } + if (data.routing_number) { + filter.push(`ergo_payout_method.routing_number,cs,${data.routing_number}`); + } + if (data.host_email) { + filter.push(`ergo_user.email,cs,${data.host_email}`); + } + + console.log("filter", filter); + + let result = await treeSdk.getPaginate("payout_method", { + filter, + join: ["user|host_id"], + page: pageNum || 1, + size: limitNum, + order: "update_at", + }); + console.log("res", result); + + const { list, total, limit, num_pages, page } = result; + + const sortedList = selector(list, false); + setCurrentTableData(sortedList); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("host_id", data.host_id); + searchParams.set("account_name", data.account_name); + searchParams.set("account_number", data.account_number); + searchParams.set("routing_number", data.routing_number); + searchParams.set("host_email", data.host_email); + setSearchParams(searchParams); + localStorage.setItem("admin_payout_method_filter", searchParams.toString()); + + getData(1, pageSize); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "payout_method", + }, + }); + getData(1, pageSize); + }, []); + + return ( + <> +
+
+
+

Payout methods

+
+
+
+ + +

{errors.id?.message}

+
+
+ + +

{errors.host_id?.message}

+
+ +
+ + +

{errors.host_email?.message}

+
+
+ + +

{errors.account_name?.message}

+
+
+ + +

{errors.account_number?.message}

+
+
+ + +

{errors.routing_number?.message}

+
+
+ +
+
+ + + +
+ +
+ + {false && ( +
+ + {bulkSelected.length > 0 ? ( +
+ + +
+ ) : null} +
+ )} + +
+
+ + + + {false && ( + + )} + {columns.map((column, index) => ( + + ))} + + + + {data.map((row, i) => { + return ( + + {false && ( + + )} + {tableColumns.map((cell, index) => { + if (cell.format) { + return ( + + ); + } + if (cell.accessor == "") { + return ( + + ); + } + if (cell.mapping) { + return ( + + ); + } + + if (cell.idPrefix) { + return ( + + ); + } + + if (cell.nested) { + return ( + + ); + } + + return ( + + ); + })} + + ); + })} + +
onSort(column.accessor)} + > + {column.header} + {column.isSorted} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
+ { + if (bulkSelected.includes(row.id)) { + setBulkSelected((prev) => { + let copy = [...prev]; + copy.splice( + prev.findIndex((id) => id == row.id), + 1, + ); + return copy; + }); + } else { + setBulkSelected((prev) => [...prev, row.id]); + } + }} + checked={bulkSelected.includes(row.id)} + onChange={() => {}} + /> + + {cell.format(row[cell.accessor])} + + {cell.mapping[row[cell.accessor] ?? 0]} + + {cell.idPrefix + row[cell.accessor]} + + {row[cell.nested][cell.accessor]} + + {row[cell.accessor]} +
+
+
+ + + ); +} diff --git a/src/pages/Admin/Property/AddAdminPropertyPage.jsx b/src/pages/Admin/Property/AddAdminPropertyPage.jsx new file mode 100644 index 0000000..b09c15f --- /dev/null +++ b/src/pages/Admin/Property/AddAdminPropertyPage.jsx @@ -0,0 +1,260 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate } from "react-router-dom"; +import { tokenExpireError, AuthContext } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import AddAdminPageLayout from "@/layouts/AddAdminPageLayout"; +import SmartSearchV2 from "@/components/SmartSearchV2"; + +const AddAdminPropertyPage = () => { + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const [selectedHost, setSelectedHost] = React.useState({}); + const [hosts, setHosts] = React.useState([]); + const [loading, setLoading] = React.useState(false); + + let sdk = new MkdSDK(); + const schema = yup + .object({ + address_line_1: yup.string().required("Address line one is required"), + address_line_2: yup.string("Address line 2 is required"), + city: yup.string().required("City is required"), + country: yup.string().required("Country is required"), + zip: yup.number().required("Zip is required").typeError("Zip code must be a number"), + name: yup.string().required("Name is required"), + }) + .required(); + + const { dispatch } = React.useContext(AuthContext); + + const navigate = useNavigate(); + const { + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + async function fetchHosts() { + try { + sdk.setTable("user"); + const result = await sdk.callRestAPI({}, "GETALL"); + const { list } = result; + setHosts(list); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + + const onSubmit = async (data) => { + setLoading(true); + if (selectedHost?.id) { + data.host_id = selectedHost.id; + try { + sdk.setTable("property"); + const result = await sdk.callRestAPI( + { + address_line_1: data.address_line_1, + address_line_2: data.address_line_2, + city: data.city, + country: data.country, + zip: data.zip, + status: 1, + verified: 1, + host_id: data.host_id, + name: data.name, + }, + "POST", + ); + if (!result.error) { + showToast(globalDispatch, "Added"); + navigate("/admin/property"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("address_line_1", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + } else { + return setError("host_email", { + type: "manual", + message: "Please select a valid host email", + }); + } + setLoading(false); + }; + + const onError = () => { + if (!selectedHost?.id) { + return setError("host_email", { + type: "manual", + message: "Please select a valid host email", + }); + } + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "property", + }, + }); + + fetchHosts(); + }, []); + + return ( + +
+
+ + +

{errors.name?.message}

+
+
+ + +

{errors.host_email?.message}

+
+
+ + +

{errors.address_line_1?.message}

+
+ +
+ + +

{errors.address_line_2?.message}

+
+ +
+ + +

{errors.city?.message}

+
+ +
+ + +

{errors.country?.message}

+
+ +
+ + +

{errors.zip?.message}

+
+ +
+ + +
+
+
+ ); +}; + +export default AddAdminPropertyPage; diff --git a/src/pages/Admin/Property/AdminPropertyListPage.jsx b/src/pages/Admin/Property/AdminPropertyListPage.jsx new file mode 100644 index 0000000..83f6427 --- /dev/null +++ b/src/pages/Admin/Property/AdminPropertyListPage.jsx @@ -0,0 +1,324 @@ +import React from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { useSearchParams, Link } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import Button from "@/components/Button"; +import Table from "@/components/Table"; +import AddButton from "@/components/AddButton"; +import PaginationHeader from "@/components/PaginationHeader"; +import ReactHtmlTableToExcel from "react-html-table-to-excel"; +import { ID_PREFIX } from "@/utils/constants"; +import { adminColumns, applySetting } from "@/utils/adminPortalColumns"; + +let sdk = new MkdSDK(); + +const AdminPropertyListPage = () => { + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const [tableColumns, setTableColumns] = React.useState([]); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_property_filter") ?? ""); + + const schema = yup.object({ + address_line_1: yup.string(), + address_line_2: yup.string(), + city: yup.string(), + country: yup.string(), + zip: yup.string(), + host_id: yup.number().positive().integer(), + name: yup.string(), + }); + + const { + reset, + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: parseSearchParams(searchParams), + }); + + function onSort(accessor) { + const columns = tableColumns; + const index = columns.findIndex((column) => column.accessor === accessor); + const column = columns[index]; + column.isSortedDesc = !column.isSortedDesc; + columns.splice(index, 1, column); + setTableColumns(() => [...columns]); + const sortedList = selector(data, column.isSortedDesc, accessor); + setCurrentTableData(sortedList); + } + function selector(users, isSortedDesc, accessor) { + if (accessor?.split(",").length > 1) { + accessor = accessor.split(",")[0]; + } + + return users.sort((a, b) => { + if (isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1; + } else { + return a[accessor] < b[accessor] ? 1 : -1; + } + } + if (!isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1; + } else { + return a[accessor] < b[accessor] ? -1 : 1; + } + } + }); + } + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum) { + const data = parseSearchParams(searchParams); + data.id = data.id?.replace(ID_PREFIX.PROPERTY, ""); + + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/property/PAGINATE", + { + where: [ + data + ? `${data.id ? `ergo_property.id = '${data.id}'` : "1"} + AND ${data.host_id ? `ergo_property.host_id = ${data.host_id}` : "1"} + AND ${data.email ? `ergo_user.email LIKE '%${data.email}%'` : "1"} + AND ${data.zip ? `ergo_property.zip LIKE '%${data.zip}%'` : "1"} + AND ${data.country ? `ergo_property.country LIKE '%${data.country}%'` : "1"}` + : 1, + "ergo_property.deleted_at IS NULL", + ], + page: pageNum, + limit: limitNum, + sortId: "update_at", + direction: "DESC", + }, + "POST", + ); + + const { list, total, limit, num_pages, page } = result; + + const sortedList = selector(list, false); + setCurrentTableData(sortedList); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("city", data.city); + searchParams.set("zip", data.zip); + searchParams.set("email", data.email); + setSearchParams(searchParams); + localStorage.setItem("admin_property_filter", searchParams.toString()); + + getData(1, pageSize); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "property", + }, + }); + fetchColumnOrder(); + getData(1, pageSize); + }, []); + + async function fetchColumnOrder() { + sdk.setTable("settings"); + try { + const result = await sdk.callRestAPI({ limit: 1, page: 1, payload: { key_name: "admin_property_space_column_order" } }, "PAGINATE"); + if (Array.isArray(result.list) && result.list.length > 0) { + setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_property)); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + return ( + <> +
+
+

Property

+ +
+
+
+ + +

{errors.id?.message}

+
+ +
+ + +

{errors.host_id?.message}

+
+ +
+ + +

{errors.city?.message}

+
+ +
+ + +

{errors.zip?.message}

+
+
+ +
+ + + +
+ + Change Column Order + {" "} + +
+ +
+
+ + + + + + ); +}; + +export default AdminPropertyListPage; diff --git a/src/pages/Admin/Property/EditAdminPropertyPage.jsx b/src/pages/Admin/Property/EditAdminPropertyPage.jsx new file mode 100644 index 0000000..dbfed07 --- /dev/null +++ b/src/pages/Admin/Property/EditAdminPropertyPage.jsx @@ -0,0 +1,276 @@ +import React, { useState } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import { useNavigate, useParams } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import CustomComboBoxV2 from "@/components/CustomComboBoxV2"; + +let sdk = new MkdSDK(); + +const EditAdminPropertyPage = () => { + const { dispatch } = React.useContext(AuthContext); + const schema = yup + .object({ + address_line_1: yup.string().required("Address line one is required"), + address_line_2: yup.string("Address line 2 is required"), + city: yup.string().required("City is required"), + country: yup.string().required("Country is required"), + zip: yup.string().required("Zip is required"), + host_id: yup.number(), + }) + .required(); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const navigate = useNavigate(); + const [id, setId] = useState(0); + const { + register, + handleSubmit, + setError, + setValue, + control, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const params = useParams(); + + async function fetchProperty() { + try { + sdk.setTable("property"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + if (!result.error) { + setValue("address_line_1", result.model.address_line_1); + setValue("address_line_2", result.model.address_line_2); + setValue("city", result.model.city); + setValue("country", result.model.country); + setValue("zip", result.model.zip); + setValue("name", result.model.name); + setValue("host_id", result.model.host_id); + setId(result.model.id); + } + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + async function fetchHostFiltered(emailFilter, setter, initialUserId) { + try { + var list = []; + if (+initialUserId) { + const initialUserResult = await sdk.callRawAPI( + "/v2/api/custom/ergo/user/PAGINATE", + { page: 1, limit: 1, where: [`${initialUserId ? `ergo_user.id = ${+initialUserId}` : ""} AND ergo_user.role != 'customer'`] }, + "POST", + ); + if (Array.isArray(initialUserResult.list)) { + list = initialUserResult.list; + } + } + if (emailFilter) { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/user/PAGINATE", { page: 1, limit: 10, where: [`ergo_user.email LIKE '%${emailFilter}%' AND ergo_user.role != 'customer'`] }, "POST"); + if (Array.isArray(result.list)) { + list = [...list, ...result.list]; + } + } + setter(list); + } catch (err) { + console.log("err", err); + } + } + + const onSubmit = async (data) => { + console.log("submitting", data); + try { + const result = await sdk.callRestAPI( + { + id: id, + address_line_1: data.address_line_1, + address_line_2: data.address_line_2, + city: data.city, + country: data.country, + zip: data.zip, + host_id: data.host_id, + name: data.name, + }, + "PUT", + ); + + if (!result.error) { + showToast(globalDispatch, "Updated"); + navigate("/admin/property"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("address_line_1", { + type: "manual", + message: error.message, + }); + } + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "property", + }, + }); + fetchProperty(); + }, []); + + return ( +
+
+

Edit Property

+ +
+
+

ID

+

{id}

+
+
+ + +

{errors.name?.message}

+
+
+ + setValue("host_id", val)} + valueField={"id"} + labelField={"email"} + getItems={fetchHostFiltered} + className="relative flex h-[40px] items-center rounded border px-3" + placeholder="Host email" + /> +

{errors.host_id?.message}

+
+
+ + +

{errors.address_line_1?.message}

+
+ +
+ + +

{errors.address_line_2?.message}

+
+ +
+ + +

{errors.city?.message}

+
+ +
+ + +

{errors.country?.message}

+
+ +
+ + +

{errors.zip?.message}

+
+ +
+ + +
+ + ); +}; + +export default EditAdminPropertyPage; diff --git a/src/pages/Admin/Property/ViewAdminPropertyPage.jsx b/src/pages/Admin/Property/ViewAdminPropertyPage.jsx new file mode 100644 index 0000000..a89b075 --- /dev/null +++ b/src/pages/Admin/Property/ViewAdminPropertyPage.jsx @@ -0,0 +1,193 @@ +import React, { useState } from "react"; +import MkdSDK from "@/utils/MkdSDK"; +import { Link, useNavigate, useParams } from "react-router-dom"; +import { GlobalContext } from "@/globalContext"; +import ViewAdminPageLayout from "@/layouts/ViewAdminPageLayout"; +import History from "@/components/History"; +import Icon from "@/components/Icons"; +import EditAdminPropertyPage from "./EditAdminPropertyPage"; + +let sdk = new MkdSDK(); + +const ViewAdminPropertyPage = ({ page }) => { + const [profileInfo, setProfileInfo] = useState(); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const params = useParams(); + const [activeTab, setActiveTab] = useState(0); + + const tabs = [ + { + key: 0, + name: "Profile Details", + component: page === "view" ? : , + }, + { + key: 1, + name: "History", + component: ( + + ), + }, + // { + // key: 2, + // name: "Spaces", + // component:
+ // }, + // { + // key: 3, + // name: "Addons", + // component:
+ // } + ]; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "property", + }, + }); + + (async function () { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/property/PAGINATE", + { + where: [params?.id ? `${params?.id ? `ergo_property.id = '${params?.id}'` : "1"}` : 1], + page: 1, + limit: 1, + }, + "POST", + ); + + if (!result.error) { + setProfileInfo(result.list[0]); + } + })(); + }, []); + + return ( + +
+
    + {tabs.map((tab) => ( +
  • + +
  • + ))} +
+
+ + {tabs[activeTab].component} +
+ ); +}; + +const ProfileDetails = ({ profileInfo }) => { + const navigate = useNavigate(); + const params = useParams(); + const selectVerified = [ + { key: "0", value: "No" }, + { key: "1", value: "Yes" }, + ]; + + const selectStatus = [ + { + key: "0", + value: "Inactive", + }, + { key: "1", value: "Active" }, + ]; + + return ( + <> +
+
+
+

Profile Details

+
+ +
+
+
+

Host ID

+

{profileInfo?.host_id}

+
+
+

Host Email

+

{profileInfo?.email}

+
+
+

Address

+

+ {profileInfo?.address_line_1} {profileInfo?.address_line_2} +

+
+
+

City

+

{profileInfo?.city}

+
+
+

Zip Code

+

{profileInfo?.zip}

+
+
+

Country

+

{profileInfo?.country}

+
+
+

Verified

+

{selectVerified[profileInfo?.verified]?.value}

+
+
+

Num of Spaces

+

{profileInfo?.spaces}

+
+
+

Status

+

{selectStatus[profileInfo?.status]?.value}

+
+
+ + View Addons + +
+
+
+ + ); +}; + +export default ViewAdminPropertyPage; diff --git a/src/pages/Admin/PropertyAddon/AddAdminPropertyAddOnPage.jsx b/src/pages/Admin/PropertyAddon/AddAdminPropertyAddOnPage.jsx new file mode 100644 index 0000000..82bbb9a --- /dev/null +++ b/src/pages/Admin/PropertyAddon/AddAdminPropertyAddOnPage.jsx @@ -0,0 +1,229 @@ +import React, { useState } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate } from "react-router-dom"; +import { tokenExpireError, AuthContext } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import AddAdminPageLayout from "@/layouts/AddAdminPageLayout"; +import SmartSearch from "@/components/SmartSearch"; +import TreeSDK from "@/utils/TreeSDK"; + +const treeSdk = new TreeSDK(); +const AddAdminPropertyAddOnPage = () => { + const [selectedProperty, setSelectedProperty] = useState({}); + const [properties, setPropertyData] = useState([]); + + async function getPropertyData(pageNum, limitNum, data) { + try { + let filter = ["deleted_at,is"]; + if (data.name) { + filter.push(`name,cs,${data.name}`); + } + const result = await treeSdk.getList("property", { join: [], filter }); + const { list } = result; + setPropertyData(list); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + + let sdk = new MkdSDK(); + const [addOns, setAddOns] = React.useState([]); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const schema = yup + .object({ + property_id: yup.string(), + add_on_id: yup.number("Please select an Add on").required().positive().integer().typeError("Please select an Add on"), + }) + .required(); + + const { dispatch } = React.useContext(AuthContext); + + const navigate = useNavigate(); + const { + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + async function confirmPropertyID(data) { + try { + sdk.setTable("property"); + const result = await sdk.callRestAPI( + { + id: data.property_id, + }, + "GET", + ); + if (!result.error && result?.model) { + onSubmit(data); + } else { + setError("property_id", { + type: "manual", + message: "Property with this ID doesn't exist", + }); + } + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + + const getAllAddOns = async () => { + try { + sdk.setTable("add_on"); + const result = await sdk.callRestAPI({}, "GETALL"); + if (!result.error) { + setAddOns(result.list); + } + } catch (error) { + console.log("Error", error); + setError("add_on_id", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + const onSubmit = async (data) => { + if (!selectedProperty?.id) { + setError("property_id", "Property Name is Required"); + return; + } + data.property_id = selectedProperty.id; + try { + sdk.setTable("property_add_on"); + + const result = await sdk.callRestAPI( + { + property_id: data.property_id, + add_on_id: data.add_on_id, + }, + "POST", + ); + if (!result.error) { + showToast(globalDispatch, "Added"); + navigate("/admin/property_add_on"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("property_id", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + const onError = () => { + if (!selectedProperty?.id) { + setError("property_id", { + type: "manual", + message: "Please select a property", + }); + } + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "property_add_on", + }, + }); + (async function () { + await getPropertyData(); + await getAllAddOns(); + })(); + }, []); + + return ( + +
+
+ + +

{errors.property_id?.message}

+
+ +
+ + +

{errors.add_on_id?.message}

+
+
+ + +
+ +
+ ); +}; + +export default AddAdminPropertyAddOnPage; diff --git a/src/pages/Admin/PropertyAddon/AdminPropertyAddOnListPage.jsx b/src/pages/Admin/PropertyAddon/AdminPropertyAddOnListPage.jsx new file mode 100644 index 0000000..978bfa2 --- /dev/null +++ b/src/pages/Admin/PropertyAddon/AdminPropertyAddOnListPage.jsx @@ -0,0 +1,355 @@ +import React from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import AddButton from "@/components/AddButton"; +import Button from "@/components/Button"; +import Table from "@/components/Table"; +import PaginationHeader from "@/components/PaginationHeader"; +import ReactHtmlTableToExcel from "react-html-table-to-excel"; +import { ID_PREFIX } from "@/utils/constants"; + +let sdk = new MkdSDK(); + +const columns = [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.PROPERTY_ADDON, + }, + { + header: "Property", + accessor: "property_name", + isSorted: true, + isSortedDesc: true, + }, + { + header: "Add-on name", + accessor: "add_on_name", + isSorted: true, + isSortedDesc: true, + }, + { + header: "Cost", + accessor: "cost", + isSorted: true, + isSortedDesc: true, + amountField: true, + }, + { + header: "Actions", + accessor: "", + }, +]; + +const AdminPropertyAddOnListPage = () => { + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch, state } = React.useContext(GlobalContext); + const [tableColumns, setTableColumns] = React.useState(columns); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + + const [addOns, setAddOns] = React.useState([]); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_property_addon_filter") ?? ""); + + const navigate = useNavigate(); + + const schema = yup.object({ + property_name: yup.string(), + addon_name: yup.string(), + id: yup.string(), + }); + const { + reset, + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: parseSearchParams(searchParams), + }); + + function onSort(accessor) { + const columns = tableColumns; + const index = columns.findIndex((column) => column.accessor === accessor); + const column = columns[index]; + column.isSortedDesc = !column.isSortedDesc; + columns.splice(index, 1, column); + setTableColumns(() => [...columns]); + const sortedList = selector(data, column.isSortedDesc, accessor); + setCurrentTableData(sortedList); + } + function selector(users, isSortedDesc, accessor) { + if (accessor?.split(",").length > 1) { + accessor = accessor.split(",")[0]; + } + + return users.sort((a, b) => { + if (isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1; + } else { + return a[accessor] < b[accessor] ? 1 : -1; + } + } + if (!isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1; + } else { + return a[accessor] < b[accessor] ? -1 : 1; + } + } + }); + } + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum) { + const data = parseSearchParams(searchParams); + data.id = data.id?.replace(ID_PREFIX.PROPERTY_ADDON, ""); + + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/property-addons/PAGINATE", + { + where: [ + data + ? `${data.id ? `ergo_property_add_on.id = '${data.id}'` : "1"} + AND ${data.property_name ? `ergo_property.name LIKE '%${data.property_name}%'` : "1"} + AND ${data.addon_name ? `ergo_add_on.name LIKE '%${data.addon_name}%'` : "1"} AND ${data.property_id ? `ergo_property.id = ${data.property_id}` : "1"}` + : 1, + "ergo_property_add_on.deleted_at IS NULL", + ], + page: pageNum, + limit: limitNum, + sortId: "update_at", + direction: "DESC", + }, + "POST", + ); + + const { list, total, limit, num_pages, page } = result; + + const sortedList = selector(list, false); + setCurrentTableData(sortedList); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("property_name", data.property_name); + searchParams.set("addon_name", data.addon_name); + setSearchParams(searchParams); + localStorage.setItem("admin_property_addon_filter", searchParams.toString()); + + getData(1, pageSize); + }; + + const getAllAddOns = async () => { + try { + sdk.setTable("add_on"); + const result = await sdk.callRestAPI({}, "GETALL"); + if (!result.error) { + setAddOns(result.list); + } + } catch (error) { + console.log("Error", error); + setError("add_on_id", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "property_add_on", + }, + }); + getAllAddOns(); + getData(1, pageSize); + }, []); + + React.useEffect(() => { + if (state.deleted) { + globalDispatch({ + type: "DELETED", + payload: { + deleted: false, + }, + }); + getData(currentPage, pageSize); + } + }, [state.deleted]); + + return ( + <> +
+
+

Property Add-on Search

+ +
+ +
+
+ + +

{errors.id?.message}

+
+ +
+ + +

{errors.property_name?.message}

+
+ +
+ + +

{errors.addon_name?.message}

+
+
+ + + +
+ +
+ +
+
+
+ + + + + ); +}; + +export default AdminPropertyAddOnListPage; diff --git a/src/pages/Admin/PropertyAddon/EditAdminPropertyAddOnPage.jsx b/src/pages/Admin/PropertyAddon/EditAdminPropertyAddOnPage.jsx new file mode 100644 index 0000000..67296db --- /dev/null +++ b/src/pages/Admin/PropertyAddon/EditAdminPropertyAddOnPage.jsx @@ -0,0 +1,248 @@ +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import { useNavigate, useParams } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import EditAdminPageLayout from "@/layouts/EditAdminPageLayout"; +import SmartSearch from "@/components/SmartSearch"; + +let sdk = new MkdSDK(); + +const EditAdminPropertyAddOnPage = () => { + const [selectedProperty, setSelectedProperty] = useState({}); + const [properties, setPropertyData] = useState([]); + + async function getPropertyData(pageNum, limitNum, data) { + try { + sdk.setTable("property"); + const payload = { name: data.name || undefined }; + const result = await sdk.callRestAPI( + { + payload, + page: pageNum, + limit: limitNum, + }, + "PAGINATE", + ); + const { list } = result; + setPropertyData(list); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + + const { dispatch } = React.useContext(AuthContext); + const schema = yup + .object({ + property_id: yup.string(), + add_on_id: yup.number().required().positive().integer(), + }) + .required(); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const [addOns, setAddOns] = React.useState([]); + const navigate = useNavigate(); + const [id, setId] = useState(0); + const { + register, + handleSubmit, + setError, + setValue, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const params = useParams(); + + useEffect(function () { + (async function () { + await getPropertyData(1, 0, { name: null }); + })(); + }, []); + + useEffect(() => { + // this effect should only be called once + if (addOns.length > 0 && properties.length > 0 && !selectedProperty.name) { + (async function () { + try { + sdk.setTable("property_add_on"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + if (!result.error) { + // setValue("property_id", result.model.property_id); + setSelectedProperty(properties.find((prop) => prop.id == result.model.property_id) || { name: "" }); + setValue("add_on_id", result.model.add_on_id); + setId(result.model.id); + } + } catch (error) { + console.log("error", error); + tokenExpireError(dispatch, error.message); + } + })(); + } + }, [addOns.length, properties.length]); + + const onSubmit = async (data) => { + if (!selectedProperty?.id) { + setError("property_id", "Property Name is Required"); + return; + } + data.property_spaces_id = selectedProperty.id; + sdk.setTable("property_add_on"); + try { + const result = await sdk.callRestAPI( + { + id: id, + property_id: data.property_spaces_id, + add_on_id: data.add_on_id, + }, + "PUT", + ); + + if (!result.error) { + showToast(globalDispatch, "Updated"); + navigate("/admin/property_add_on"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("property_spaces_id", { + type: "manual", + message: error.message, + }); + } + }; + + const onError = () => { + if (!selectedProperty?.id) { + setError("property_id", { + type: "manual", + message: "Please select a property", + }); + } + }; + + const getAllAddOns = async () => { + try { + sdk.setTable("add_on"); + const result = await sdk.callRestAPI({}, "GETALL"); + if (!result.error) { + setAddOns(result.list); + } + } catch (error) { + console.log("Error", error); + setError("add_on_id", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "property_add_on", + }, + }); + (async function () { + await getPropertyData(1, 10, { name: "" }); + await getAllAddOns(); + })(); + }, []); + + return ( + +
+
+ + + +

{errors.property_id?.message}

+
+ +
+ + +

{errors.add_on_id?.message}

+
+ +
+ + +
+ +
+ ); +}; + +export default EditAdminPropertyAddOnPage; diff --git a/src/pages/Admin/PropertySpace/AddAdminPropertySpacesPage.jsx b/src/pages/Admin/PropertySpace/AddAdminPropertySpacesPage.jsx new file mode 100644 index 0000000..bbc0db7 --- /dev/null +++ b/src/pages/Admin/PropertySpace/AddAdminPropertySpacesPage.jsx @@ -0,0 +1,360 @@ +import React, { useState, useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate } from "react-router-dom"; +import { tokenExpireError, AuthContext } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import AddAdminPageLayout from "@/layouts/AddAdminPageLayout"; +import SmartSearchV2 from "@/components/SmartSearchV2"; +import TreeSDK from "@/utils/TreeSDK"; + +const treeSdk = new TreeSDK(); +const AddAdminPropertySpacesPage = () => { + const [spaces, setSpacesData] = useState([]); + const [selectedProperty, setSelectedProperty] = useState({}); + const [properties, setPropertyData] = useState([]); + + let sdk = new MkdSDK(); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const schema = yup.object({ + max_capacity: yup.number().typeError("Max Capacity must be a number").required().positive().integer(), + description: yup.string().required("Description is required"), + rate: yup.number().typeError("Rate must be a number").required(), + tax: yup.number().typeError("Tax must be a number").required(), + availability: yup.number(), + space_status: yup.number(), + space_id: yup.number("Please select a space category").typeError("Please select a space category").required("Space category is required"), + additional_guest_rate: yup.string(), + }); + + const { dispatch } = React.useContext(AuthContext); + + const navigate = useNavigate(); + const { + register, + handleSubmit, + setError, + setValue, + watch, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const space_id = watch("space_id"); + const hasSizes = spaces.find((sp) => sp.id == Number(space_id))?.has_sizes == 1; + + async function getPropertyData() { + try { + const result = await treeSdk.getList("property", { filter: ["deleted_at,is"], join: [] }); + const { list } = result; + setPropertyData(list); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + + async function getSpacesData() { + try { + const result = await treeSdk.getList("spaces", { filter: ["deleted_at,is"], join: [] }); + const { list } = result; + setSpacesData(list); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + const onSubmit = async (data) => { + if (!selectedProperty?.id) { + setError("property_id", { + type: "manual", + message: "Please select a valid property", + }); + return; + } + data.property_id = selectedProperty.id; + try { + sdk.setTable("property_spaces"); + const result = await sdk.callRestAPI( + { + property_id: data.property_id, + space_id: data.space_id, + max_capacity: data.max_capacity, + description: data.description, + rate: data.rate, + tax: data?.tax, + availability: data.availability, + space_status: data.space_status, + additional_guest_rate: data.additional_guest_rate || undefined, + size: Number(data.size) || null, + }, + "POST", + ); + if (!result.error) { + showToast(globalDispatch, "Added"); + navigate("/admin/property_spaces"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + if (error.message == "Validation error") { + setError("property_id", { + type: "manual", + message: "Property Space Already Exists!", + }); + } else { + setError("property_id", { + type: "manual", + message: error.message, + }); + } + tokenExpireError(dispatch, error.message); + } + }; + + const onError = () => { + if (!selectedProperty?.id) { + setError("property_id", { + type: "manual", + message: "Please select a valid property", + }); + } + }; + + useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "property_spaces", + }, + }); + (async function () { + await getPropertyData(); + await getSpacesData(); + })(); + }, []); + + return ( + +
+
+ + +

{errors.property_id?.message}

+
+ +
+ + +

{errors.space_id?.message}

+
+ {hasSizes && ( +
+ + +

{errors.size?.message}

+
+ )} + +
+ + +

{errors.max_capacity?.message}

+
+ +
+ + +

{errors.description?.message}

+
+ +
+ + +

{errors.tax?.message}

+
+ +
+ + +

{errors.rate?.message}

+
+ +
+ + +

{errors.additional_guest_rate?.message}

+
+ +
+ + + +

{errors.availability?.message}

+
+ +
+ + + +

{errors.space_status?.message}

+
+ + +
+ + +
+ +
+ ); +}; + +export default AddAdminPropertySpacesPage; diff --git a/src/pages/Admin/PropertySpace/AdminPropertySpacesListPage.jsx b/src/pages/Admin/PropertySpace/AdminPropertySpacesListPage.jsx new file mode 100644 index 0000000..a88cad5 --- /dev/null +++ b/src/pages/Admin/PropertySpace/AdminPropertySpacesListPage.jsx @@ -0,0 +1,497 @@ +import React, { useState } from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { Link, useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { parseSearchParams, clearSearchParams } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import AddButton from "@/components/AddButton"; +import Button from "@/components/Button"; +import Table from "@/components/Table"; +import PaginationHeader from "@/components/PaginationHeader"; +import ReactHtmlTableToExcel from "react-html-table-to-excel"; +import { DRAFT_STATUS, ID_PREFIX } from "@/utils/constants"; +import { adminColumns, applySetting } from "@/utils/adminPortalColumns"; +import TreeSDK from "@/utils/TreeSDK"; + +let sdk = new MkdSDK(); +let treeSdk = new TreeSDK(); + +const selectStatus = [ + { key: "", value: "All" }, + { key: "0", value: "HIDDEN" }, + { key: "1", value: "VISIBLE" }, +]; +const selectSpaceStatus = [ + { key: "", value: "All" }, + { key: "0", value: "UNDER REVIEW" }, + { key: "1", value: "APPROVED" }, + { key: "2", value: "DECLINED" }, +]; + +const AdminPropertySpacesListPage = () => { + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch, state } = React.useContext(GlobalContext); + const [tableColumns, setTableColumns] = React.useState([]); + const [spaces, setSpacesData] = useState([]); + + const [searchParams, setSearchParams] = useSearchParams(); + // TODO: find a better way to do this + const [searchParams2] = useSearchParams(localStorage.getItem("admin_property_space_filter") ?? ""); + + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + + const schema = yup.object({ + property_name: yup.string(), + status: yup.string(), + category_name: yup.string(), + }); + const { + reset, + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: (() => { + let fromSearch = parseSearchParams(searchParams); + if (Object.keys(fromSearch).length > 0) { + return fromSearch; + } + return parseSearchParams(searchParams2); + })(), + }); + + function onSort(accessor) { + const columns = tableColumns; + const index = columns.findIndex((column) => column.accessor === accessor); + const column = columns[index]; + column.isSortedDesc = !column.isSortedDesc; + columns.splice(index, 1, column); + setTableColumns(() => [...columns]); + const sortedList = selector(data, column.isSortedDesc, accessor); + setCurrentTableData(sortedList); + } + function selector(users, isSortedDesc, accessor) { + if (accessor?.split(",").length > 1) { + accessor = accessor.split(",")[0]; + } + + return users.sort((a, b) => { + if (isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1; + } else { + return a[accessor] < b[accessor] ? 1 : -1; + } + } + if (!isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1; + } else { + return a[accessor] < b[accessor] ? -1 : 1; + } + } + }); + } + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getSpacesData() { + try { + let filter = ["deleted_at,is"]; + const result = await treeSdk.getList("spaces", { + filter, + join: [], + }); + if (Array.isArray(result.list)) { + setSpacesData(result.list); + } + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + async function getData(pageNum, limitNum) { + let data = parseSearchParams(searchParams); + data = Object.keys(data).length < 1 ? parseSearchParams(searchParams2) : data; + data.id = data.id?.replace(ID_PREFIX.PROPERTY_SPACE, ""); + + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/property-spaces/PAGINATE", + { + where: [ + data + ? `${data.id ? `ergo_property_spaces.id = '${data.id}'` : "1"} AND ${data.host_email ? `email LIKE '${data.host_email}'` : "1"} AND ${data.property_name ? `ergo_property.name LIKE '%${data.property_name}%'` : "1"} AND ${data.category_name ? `ergo_spaces.category LIKE '%${data.category_name}%' ` : "1" + } AND ${data.status ? `ergo_property_spaces.availability LIKE '%${data.status}%'` : "1"} AND ${data.space_status ? `ergo_property_spaces.space_status LIKE '%${data.space_status}%'` : "1" + } AND ${data.host_id ? `ergo_property.host_id = ${data.host_id}` : "1"} AND ${data.size != undefined ? `ergo_property_spaces.size = ${data.size}` : "1"} AND ${data.is_draft == 1 ? `ergo_property_spaces.draft_status < ${DRAFT_STATUS.COMPLETED}` : `ergo_property_spaces.draft_status = ${DRAFT_STATUS.COMPLETED}` + }` + : 1, + "ergo_property_spaces.deleted_at IS NULL", + ], + page: pageNum, + limit: limitNum, + sortId: "update_at", + direction: "DESC", + }, + "POST", + ); + const { list, total, limit, num_pages, page } = result; + + const sortedList = selector(list, false); + setCurrentTableData(sortedList); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("host_id", data.host_id); + searchParams.set("host_email", data.host_email); + searchParams.set("property_name", data.property_name); + searchParams.set("category_name", data.category_name); + searchParams.set("status", data.status); + searchParams.set("space_status", data.space_status); + searchParams.set("size", data.size); + searchParams.set("is_draft", data.is_draft); + setSearchParams(searchParams); + localStorage.setItem("admin_property_space_filter", searchParams.toString()); + + getData(1, pageSize); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "property_spaces", + }, + }); + + (async function () { + await fetchColumnOrder(); + await getData(1, pageSize); + getSpacesData(); + })(); + }, []); + + React.useEffect(() => { + if (state.deleted) { + globalDispatch({ + type: "DELETED", + payload: { + deleted: false, + }, + }); + getData(currentPage, pageSize); + } + }, [state.deleted]); + + async function fetchColumnOrder() { + sdk.setTable("settings"); + try { + const result = await sdk.callRestAPI({ limit: 1, page: 1, payload: { key_name: "admin_property_space_column_order" } }, "PAGINATE"); + if (Array.isArray(result.list) && result.list.length > 0) { + setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_property_space)); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + return ( + <> +
+
+

Property Spaces Search

+ +
+ +
+
+ + +

{errors.id?.message}

+
+ +
+ + +

{errors.host_id?.message}

+
+
+ + +

{errors.host_email?.message}

+
+ +
+ + +

{errors.property_name?.message}

+
+ +
+ + +

{errors.category_name?.message}

+
+ +
+ + +

{errors.size?.message}

+
+
+ + +

{errors.is_draft?.message}

+
+
+ + +

+
+
+ + +

+
+
+ + + + + +
+ + Change Column Order + {" "} + +
+ +
+
+
+ + + + + ); +}; + +export default AdminPropertySpacesListPage; diff --git a/src/pages/Admin/PropertySpace/EditAdminPropertySpacesPage.jsx b/src/pages/Admin/PropertySpace/EditAdminPropertySpacesPage.jsx new file mode 100644 index 0000000..7f33022 --- /dev/null +++ b/src/pages/Admin/PropertySpace/EditAdminPropertySpacesPage.jsx @@ -0,0 +1,464 @@ +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import { useNavigate, useParams } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import EditAdminPageLayout from "@/layouts/EditAdminPageLayout"; +import SmartSearch from "@/components/SmartSearch"; + +let sdk = new MkdSDK(); + +const EditAdminPropertySpacesPage = () => { + const { dispatch } = React.useContext(AuthContext); + + const [spaces, setSpacesData] = useState([]); + const [selectedProperty, setSelectedProperty] = useState({}); + const [properties, setPropertyData] = useState([]); + const schema = yup + .object({ + property_id: yup.number(), + max_capacity: yup.number().required().positive().integer(), + description: yup.string().required(), + rate: yup.number().required(), + tax: yup.number().typeError("Tax must be a number").required(), + visibility: yup.number(), + space_status: yup.number(), + space_id: yup.number("Please select a space category").typeError("Please select a space category").required("Space category is required"), + reason: yup.string().when("space_status", { + is: (space_status) => { + return space_status == 2; + }, + then: yup.string().required("This field is required"), + otherwise: yup.string().notRequired(), + }), + additional_guest_rate: yup.string(), + }) + .required(); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const navigate = useNavigate(); + const [id, setId] = useState(0); + const { + register, + handleSubmit, + setError, + setValue, + watch, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const selectedSpaceStatus = watch("space_status"); + const space_id = watch("space_id"); + const hasSizes = spaces.find((sp) => sp.id == Number(space_id))?.has_sizes == 1; + + const params = useParams(); + const [initialSpaceStatus, setInitialSpaceStatus] = useState(null); + + async function getPropertyData(pageNum, limitNum, data) { + try { + sdk.setTable("property"); + const payload = { name: data.name || undefined }; + const result = await sdk.callRestAPI( + { + payload, + page: pageNum, + limit: limitNum, + }, + "PAGINATE", + ); + const { list } = result; + setPropertyData(list); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + + async function getSpacesData() { + try { + sdk.setTable("spaces"); + const result = await sdk.callRestAPI({}, "GETALL"); + const { list } = result; + setSpacesData(list); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + + async function sendEmailToHost(propertySpace, message) { + sdk.setTable("user"); + try { + // get user email + const result = await sdk.callRestAPI({ id: propertySpace.host_id }, "GET"); + var email = result.model.email; + const emailResult = await sdk.sendEmail(email, "Property Space Declined", message); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + useEffect(() => { + // make sure this effect will only be called once + if (properties.length > 0 && spaces.length > 0 && !selectedProperty.name) { + (async function () { + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/property-spaces/PAGINATE", + { + where: [`ergo_property_spaces.id = ${params?.id}`], + page: 1, + limit: 1, + }, + "POST", + ); + if (!result.error) { + const data = result.list[0] || {}; + console.log("properties", properties) + setSelectedProperty(properties.find((prop) => prop.name == data.property_name) || { name: "" }); + setValue("space_id", data.space_id); + setValue("max_capacity", data.max_capacity); + setValue("description", data.description); + setValue("rate", data.rate); + setValue("visibility", data.availability); + setValue("space_status", data.space_status); + setInitialSpaceStatus(data.space_status); + setValue("reason", data.reason); + setValue("tax", data?.tax); + setValue("additional_guest_rate", data.additional_guest_rate); + setValue("size", data.size); + setId(data.id); + } + } catch (error) { + console.log("error", error); + tokenExpireError(dispatch, error.message); + } + })(); + } + }, [properties.length, spaces.length]); + + const onSubmit = async (data) => { + // validate property and space + console.log("data", data) + if (!selectedProperty?.id) { + setError("property_id", { + type: "manual", + message: "Please select a valid property", + }); + return; + } + console.log(data.size) + data.property_id = selectedProperty.id; + + if (initialSpaceStatus != 2 && data.space_status == 2) { + // send email to customer + sendEmailToHost(selectedProperty, data.reason); + } + + try { + sdk.setTable("property_spaces"); + const result = await sdk.callRestAPI( + { + id: id, + property_id: data.property_id, + space_id: data.space_id, + max_capacity: data.max_capacity, + description: data.description, + rate: data.rate, + tax: data?.tax, + additional_guest_rate: Number(data.additional_guest_rate) || 0, + availability: data.visibility, + space_status: data.space_status, + size: Number(data.size) || null, + }, + "PUT", + ); + + if (!result.error) { + showToast(globalDispatch, "Updated"); + navigate("/admin/property_spaces"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("property_id", { + type: "manual", + message: error.message, + }); + } + }; + + const onError = (err) => { + console.log("erroring", err); + if (!selectedProperty?.id) { + setError("property_id", { + type: "manual", + message: "Please select a valid property", + }); + } + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "property_spaces", + }, + }); + (async function () { + await getPropertyData(1, 10000, { name: null }); + await getSpacesData(); + })(); + }, []); + + return ( + +
+
+ + + + +

{errors.property_id?.message}

+
+ +
+ + +

{errors.space_id?.message}

+
+ + {hasSizes && ( +
+ + +

{errors.size?.message}

+
+ )} + +
+ + +

{errors.max_capacity?.message}

+
+ +
+ + +

{errors.description?.message}

+
+
+ + +

{errors.tax?.message}

+
+ +
+ + +

{errors.rate?.message}

+
+ +
+ + +

{errors.additional_guest_price?.message}

+
+ +
+ + + +

{errors.visibility?.message}

+
+
+ + + +

{errors.space_status?.message}

+
+ {selectedSpaceStatus == 2 && ( +
+ + +

{errors.reason?.message}

+
+ )} + +
+ + +
+ +
+ ); +}; + +export default EditAdminPropertySpacesPage; diff --git a/src/pages/Admin/PropertySpace/ViewAdminPropertySpacesPage.jsx b/src/pages/Admin/PropertySpace/ViewAdminPropertySpacesPage.jsx new file mode 100644 index 0000000..85f2410 --- /dev/null +++ b/src/pages/Admin/PropertySpace/ViewAdminPropertySpacesPage.jsx @@ -0,0 +1,243 @@ +import React, { useState } from "react"; +import MkdSDK from "@/utils/MkdSDK"; +import { Link, useLocation, useNavigate, useParams } from "react-router-dom"; +import { GlobalContext } from "@/globalContext"; +import { useEffect } from "react"; +import { callCustomAPI } from "@/utils/callCustomAPI"; + +let sdk = new MkdSDK(); + +const ViewAdminPropertySpacesPage = ({ page }) => { + const { state: propertySpace } = useLocation(); + + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const { id } = useParams(); + const [activeTab, setActiveTab] = useState(1); + + const [images, setImages] = useState([]); + const [amenities, setAmenities] = useState([]); + const [reviews, setReviews] = useState([]); + const [faqs, setFaqs] = useState([]); + + async function fetchImages() { + const where = [`property_spaces_id = ${id}`]; + try { + const result = await callCustomAPI("property-space-images", "post", { page: 1, limit: 7, where }, "PAGINATE"); + if (Array.isArray(result.list)) { + setImages(result.list); + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function fetchAmenities() { + const where = [`property_spaces_id = ${id}`]; + try { + const result = await callCustomAPI("property-spaces-amenitites", "post", { page: 1, limit: 1000, where }, "PAGINATE"); + if (Array.isArray(result.list)) { + setAmenities(result.list.map((res) => res.amenity_name)); + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function fetchReviews() { + const role = localStorage.getItem("role") ?? "customer"; + const where = [`ergo_review.property_spaces_id = ${id} AND ergo_review.status = 1 AND ergo_review.given_by = 'customer'`]; + try { + const result = await callCustomAPI("review-hashtag", "post", { page: 1, limit: 1000, where, user: role }, "PAGINATE"); + if (Array.isArray(result.list)) { + setReviews(result.list); + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function fetchFaqs() { + sdk.setTable("property_space_faq"); + const payload = { property_space_id: Number(id) }; + try { + const result = await sdk.callRestAPI({ page: 1, limit: 1000, payload }, "PAGINATE"); + if (Array.isArray(result.list)) { + setFaqs(result.list); + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + useEffect(() => { + fetchImages(); + fetchAmenities(); + fetchFaqs(); + fetchReviews(); + }, []); + + const tabs = [ + { + key: 0, + name: "Summary", + component: ( + + ), + }, + { + key: 1, + name: "Links", + component: ( +
+ + View Images + + + View FAQs + + + View Amenities + + + View Bookings + + + View Reviews + +
+ ), + }, + ]; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "property_spaces", + }, + }); + }, []); + + return ( + <> +
+

{propertySpace?.property_name}

+
    + {tabs.map((tab) => ( +
  • + +
  • + ))} +
+
+ + {tabs[activeTab].component} + + ); +}; + +export default ViewAdminPropertySpacesPage; + +const SpaceSummary = ({ images, amenities, faqs, reviews }) => { + return ( +
+

Images

+
+ {images.map((img) => ( + + + + ))} +
+
+
+

Amenities

+
+ {amenities.map((am, idx) => ( +
  • {am}
  • + ))} +
    +
    +
    +

    FAQS

    +
    + {faqs.map((faq, idx) => ( +
  • {faq.question}
  • + ))} +
    +
    +
    +

    Reviews

    +
    + {reviews.map((rv) => ( +
  • {rv}
  • + ))} +
    +
    + ); +}; diff --git a/src/pages/Admin/PropertySpaceAmenity/AddAdminPropertySpacesAmenititesPage.jsx b/src/pages/Admin/PropertySpaceAmenity/AddAdminPropertySpacesAmenititesPage.jsx new file mode 100644 index 0000000..f6432ec --- /dev/null +++ b/src/pages/Admin/PropertySpaceAmenity/AddAdminPropertySpacesAmenititesPage.jsx @@ -0,0 +1,212 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate } from "react-router-dom"; +import { tokenExpireError, AuthContext } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import AddAdminPageLayout from "@/layouts/AddAdminPageLayout"; +import SmartSearch from "@/components/SmartSearch"; + +const AddAdminPropertySpacesAmenititesPage = () => { + let sdk = new MkdSDK(); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const [selectedSpace, setSelectedSpace] = React.useState(); + const [propertySpaces, setPropertySpaces] = React.useState([]); + const [amenities, setAmenities] = React.useState([]); + const schema = yup + .object({ + property_spaces_id: yup.string(), + amenity_id: yup.number().required().positive().integer().typeError("Please select an amenity"), + }) + .required(); + + const { dispatch } = React.useContext(AuthContext); + + const navigate = useNavigate(); + const { + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + async function getPropertySpacesData(pageNum, limit, data) { + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/property-spaces/PAGINATE", + { + where: [data?.property_name ? `ergo_property.name LIKE '%${data.property_name}%' OR ergo_spaces.category LIKE '%${data.property_name}%'` : 1], + page: pageNum, + limit: limit, + }, + "POST", + ); + const { list } = result; + setPropertySpaces(list); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + + const getAllAmenities = async () => { + try { + sdk.setTable("amenity"); + const result = await sdk.callRestAPI({}, "GETALL"); + if (!result.error) { + setAmenities(result.list); + } + } catch (error) { + console.log("Error", error); + setError("amenity_id", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + const onSubmit = async (data) => { + if (selectedSpace?.id) { + data.property_spaces_id = selectedSpace.id; + try { + sdk.setTable("property_spaces_amenitites"); + const result = await sdk.callRestAPI( + { + property_spaces_id: data.property_spaces_id, + amenity_id: data.amenity_id, + }, + "POST", + ); + if (!result.error) { + showToast(globalDispatch, "Added"); + navigate("/admin/property_spaces_amenitites"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("property_spaces_id", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + } else { + setError("property_spaces_id", { + type: "manual", + message: "Please Select a property space", + }); + } + }; + + const onError = () => { + if (!selectedSpace?.id) { + setError("property_spaces_id", { + type: "manual", + message: "Please Select a property space", + }); + } + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "property_spaces_amenitites", + }, + }); + getAllAmenities(); + }, []); + + return ( + +
    +
    + + +

    {errors.property_spaces_id?.message}

    +
    +
    + + +

    {errors.amenity_id?.message}

    +
    +
    + + +
    + +
    + ); +}; + +export default AddAdminPropertySpacesAmenititesPage; diff --git a/src/pages/Admin/PropertySpaceAmenity/AdminPropertySpacesAmenititesListPage.jsx b/src/pages/Admin/PropertySpaceAmenity/AdminPropertySpacesAmenititesListPage.jsx new file mode 100644 index 0000000..e27a8a4 --- /dev/null +++ b/src/pages/Admin/PropertySpaceAmenity/AdminPropertySpacesAmenititesListPage.jsx @@ -0,0 +1,359 @@ +import React from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { Link, useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import AddButton from "@/components/AddButton"; +import Button from "@/components/Button"; +import Table from "@/components/Table"; +import PaginationHeader from "@/components/PaginationHeader"; +import ReactHtmlTableToExcel from "react-html-table-to-excel"; +import { ID_PREFIX } from "@/utils/constants"; +import { adminColumns, applySetting } from "@/utils/adminPortalColumns"; +import TreeSDK from "@/utils/TreeSDK"; + +let sdk = new MkdSDK(); +let treeSdk = new TreeSDK(); + +const AdminPropertySpacesAmenititesListPage = () => { + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch, state } = React.useContext(GlobalContext); + const [tableColumns, setTableColumns] = React.useState([]); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + + const [amenities, setAmenities] = React.useState([]); + const [searchParams, setSearchParams] = useSearchParams(); + // TODO: find a better way to do this + const [searchParams2] = useSearchParams(localStorage.getItem("admin_psa_filter") ?? ""); + + const schema = yup.object({ + spaces_name: yup.string(), + amenity_name: yup.string(), + }); + const { + reset, + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: (() => { + let fromSearch = parseSearchParams(searchParams); + if (Object.keys(fromSearch).length > 0) { + return fromSearch; + } + return parseSearchParams(searchParams2); + })(), + }); + + function onSort(accessor, direction) { } + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + function onSort(accessor) { + const columns = tableColumns; + const index = columns.findIndex((column) => column.accessor === accessor); + const column = columns[index]; + column.isSortedDesc = !column.isSortedDesc; + columns.splice(index, 1, column); + setTableColumns(() => [...columns]); + const sortedList = selector(data, column.isSortedDesc, accessor); + setCurrentTableData(sortedList); + } + function selector(users, isSortedDesc, accessor) { + if (accessor?.split(",").length > 1) { + accessor = accessor.split(",")[0]; + } + + return users.sort((a, b) => { + if (isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1; + } else { + return a[accessor] < b[accessor] ? 1 : -1; + } + } + if (!isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1; + } else { + return a[accessor] < b[accessor] ? -1 : 1; + } + } + }); + } + + async function getData(pageNum, limitNum) { + let data = parseSearchParams(searchParams); + data = Object.keys(data).length < 1 ? parseSearchParams(searchParams2) : data; + data.id = data.id?.replace(ID_PREFIX.PROPERTY_SPACE_AMENITIES, ""); + + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/property-spaces-amenitites/PAGINATE", + { + where: [ + data + ? `${data.id ? `ergo_property_spaces_amenitites.id = '${data.id}'` : "1"} AND ${data.spaces_name ? `ergo_spaces.category LIKE '%${data.spaces_name}%'` : "1"} AND ${data.amenity_name ? `ergo_amenity.name LIKE '%${data.amenity_name}%'` : "1" + } AND ${data.property_spaces_id ? `property_spaces_id = ${data.property_spaces_id}` : "1"}` + : 1, + "ergo_property_spaces_amenitites.deleted_at IS NULL", + ], + page: pageNum, + limit: limitNum, + sortId: "update_at", + direction: "DESC", + }, + "POST", + ); + const { list, total, limit, num_pages, page } = result; + const sortedList = selector(list, false); + setCurrentTableData(sortedList); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("spaces_name", data.spaces_name); + searchParams.set("amenity_name", data.amenity_name); + + setSearchParams(searchParams); + localStorage.setItem("admin_psa_filter", searchParams.toString()); + + getData(1, pageSize); + }; + + const getAllAmenities = async () => { + try { + const result = await treeSdk.getList("amenity", { filter: ["deleted_at,is"], join: [] }); + if (!result.error) { + setAmenities(result.list); + } + } catch (error) { + console.log("Error", error); + setError("amenity_name", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "property_spaces_amenitites", + }, + }); + getAllAmenities(); + (async function () { + await fetchColumnOrder(); + getData(1, pageSize); + })(); + }, []); + + React.useEffect(() => { + if (state.deleted) { + globalDispatch({ + type: "DELETED", + payload: { + deleted: false, + }, + }); + getData(currentPage, pageSize); + } + }, [state.deleted]); + + async function fetchColumnOrder() { + sdk.setTable("settings"); + const payload = { key_name: "admin_property_spaces_amenities_column_order" }; + try { + const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE"); + if (Array.isArray(result.list) && result.list.length > 0) { + setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_property_space_amenities)); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + return ( + <> +
    +
    +

    Property Spaces Amenitites Search

    + +
    + +
    +
    + + +

    {errors.id?.message}

    +
    + +
    + + +

    {errors.spaces_name?.message}

    +
    + +
    + + +

    {errors.amenity_name?.message}

    +
    +
    + + + + +
    + + Change Column Order + {" "} + +
    + +
    +
    +
    + + + + + ); +}; + +export default AdminPropertySpacesAmenititesListPage; diff --git a/src/pages/Admin/PropertySpaceAmenity/EditAdminPropertySpacesAmenititesPage.jsx b/src/pages/Admin/PropertySpaceAmenity/EditAdminPropertySpacesAmenititesPage.jsx new file mode 100644 index 0000000..21aac24 --- /dev/null +++ b/src/pages/Admin/PropertySpaceAmenity/EditAdminPropertySpacesAmenititesPage.jsx @@ -0,0 +1,238 @@ +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import { useNavigate, useParams } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import EditAdminPageLayout from "@/layouts/EditAdminPageLayout"; +import SmartSearch from "@/components/SmartSearch"; + +let sdk = new MkdSDK(); + +const EditAdminPropertySpacesAmenititesPage = () => { + const { dispatch } = React.useContext(AuthContext); + const schema = yup + .object({ + property_spaces_id: yup.number(), + amenity_id: yup.number().required().positive().integer(), + }) + .required(); + + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const navigate = useNavigate(); + + const [selectedSpace, setSelectedSpace] = useState({}); + const [spaces, setSpacesData] = useState([]); + const [amenities, setAmenities] = React.useState([]); + + const [id, setId] = useState(0); + const { + register, + handleSubmit, + setError, + setValue, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const params = useParams(); + + async function getSpacesData(pageNum, limitNum, data) { + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/property-spaces/PAGINATE", + { + where: [data?.property_name ? `ergo_property.name LIKE '%${data.property_name}%' OR ergo_spaces.category LIKE '%${data.property_name}%'` : 1], + page: pageNum, + limit: limitNum, + }, + "POST", + ); + const { list } = result; + setSpacesData(list); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + + useEffect(() => { + // make sure this effect will only be called once + if (spaces.length > 0 && !selectedSpace.property_name) { + (async function () { + try { + sdk.setTable("property_spaces_amenitites"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + if (!result.error) { + // setValue("property_spaces_id", result.model.property_spaces_id); + console.log(result.model.property_spaces_id); + setSelectedSpace(spaces.find((sp) => sp.id == result.model.property_spaces_id)); + setValue("amenity_id", result.model.amenity_id); + setId(result.model.id); + } + } catch (error) { + console.log("error", error); + tokenExpireError(dispatch, error.message); + } + })(); + } + }, [spaces.length]); + + const getAllAmenities = async () => { + try { + sdk.setTable("amenity"); + const result = await sdk.callRestAPI({}, "GETALL"); + if (!result.error) { + setAmenities(result.list); + } + } catch (error) { + console.log("Error", error); + setError("amenity_id", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + const onSubmit = async (data) => { + console.log("submitting", data); + // validate space + if (!selectedSpace?.id) { + setError("property_spaces_id", { + type: "manual", + message: "Please select a valid property space", + }); + return; + } + data.property_spaces_id = selectedSpace.id; + try { + const result = await sdk.callRestAPI( + { + id: id, + property_spaces_id: data.property_spaces_id, + amenity_id: data.amenity_id, + }, + "PUT", + ); + + if (!result.error) { + showToast(globalDispatch, "Updated"); + navigate("/admin/property_spaces_amenitites"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("property_spaces_id", { + type: "manual", + message: error.message, + }); + } + }; + + const onError = () => { + if (!selectedSpace?.id) { + setError("property_spaces_id", { + type: "manual", + message: "Please Select a property space", + }); + } + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "property_spaces_amenitites", + }, + }); + getSpacesData(1, 0, { property_name: null }); + getAllAmenities(); + }, []); + + return ( + +
    +
    + + +

    {errors.property_spaces_id?.message}

    +
    + +
    + + +

    {errors.amenity_id?.message}

    +
    +
    + + +
    + +
    + ); +}; + +export default EditAdminPropertySpacesAmenititesPage; diff --git a/src/pages/Admin/PropertySpaceFaq/AddAdminPropertySpaceFaqPage.jsx b/src/pages/Admin/PropertySpaceFaq/AddAdminPropertySpaceFaqPage.jsx new file mode 100644 index 0000000..fa78416 --- /dev/null +++ b/src/pages/Admin/PropertySpaceFaq/AddAdminPropertySpaceFaqPage.jsx @@ -0,0 +1,207 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate } from "react-router-dom"; +import { tokenExpireError, AuthContext } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import AddAdminPageLayout from "@/layouts/AddAdminPageLayout"; + +import SunEditor, { buttonList } from "suneditor-react"; +import "suneditor/dist/css/suneditor.min.css"; +import { useState } from "react"; + +const AddAdminPropertySpaceFaqPage = () => { + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const [answer, setAnswer] = useState(""); + + const schema = yup + .object({ + property_space_id: yup.number().positive().integer().typeError("Invalid ID").required(), + question: yup.string().required("Question is required"), + answer: yup.string(), + }) + .required(); + + const { dispatch } = React.useContext(AuthContext); + const sdk = new MkdSDK(); + + const navigate = useNavigate(); + const { + register, + handleSubmit, + setError, + clearErrors, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const confirmPropertySpaceId = async (id) => { + if (id == "") { + clearErrors("property_space_id"); + return; + } + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/property-spaces/PAGINATE", + { + where: [`ergo_property_spaces.id = ${id}`], + page: 1, + limit: 1, + }, + "POST", + ); + if (result.error || !result.list || result.list.length < 1) throw new Error(); + clearErrors("property_space_id"); + } catch (error) { + console.log("ERROR", error); + setError("property_space_id", { + type: "manual", + message: "Property Space with this ID does not exist", + }); + } + }; + + const onSubmit = async (data) => { + if (answer == "") { + setError("answer", { + type: "manual", + message: "Answer is required", + }); + return; + } + let sdk = new MkdSDK(); + + try { + sdk.setTable("property_space_faq"); + + const result = await sdk.callRestAPI( + { + property_space_id: data.property_space_id, + question: data.question, + answer, + }, + "POST", + ); + if (!result.error) { + showToast(globalDispatch, "Added"); + console.log(result); + navigate("/admin/property_spaces_faq"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("question", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + const onError = () => { + setError("answer", { + type: "manual", + message: "Answer is required", + }); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "property_space_faq", + }, + }); + }, []); + + return ( + +
    +
    + + confirmPropertySpaceId(e.target.value)} + /> +

    {errors.property_space_id?.message}

    +
    +
    + + +

    {errors.question?.message}

    +
    +
    + + setAnswer(content)} + placeholder="Add your answer here" + setOptions={{ buttonList: buttonList.complex }} + /> +

    {errors.answer?.message}

    +
    + +
    + + +
    + +
    + ); +}; + +export default AddAdminPropertySpaceFaqPage; diff --git a/src/pages/Admin/PropertySpaceFaq/AdminPropertySpaceFaqListPage.jsx b/src/pages/Admin/PropertySpaceFaq/AdminPropertySpaceFaqListPage.jsx new file mode 100644 index 0000000..aa2c53b --- /dev/null +++ b/src/pages/Admin/PropertySpaceFaq/AdminPropertySpaceFaqListPage.jsx @@ -0,0 +1,341 @@ +import React from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { parseSearchParams, clearSearchParams } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import AddButton from "@/components/AddButton"; +import Button from "@/components/Button"; +import Table from "@/components/Table"; +import PaginationHeader from "@/components/PaginationHeader"; +import ReactHtmlTableToExcel from "react-html-table-to-excel"; +import { ID_PREFIX } from "@/utils/constants"; +import { adminColumns, applySetting } from "@/utils/adminPortalColumns"; +import TreeSDK from "@/utils/TreeSDK"; + +let sdk = new MkdSDK(); +let treeSdk = new TreeSDK(); + +const AdminPropertySpaceFaqListPage = () => { + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch, state } = React.useContext(GlobalContext); + const [tableColumns, setTableColumns] = React.useState([]); + + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + const [searchParams, setSearchParams] = useSearchParams(); + // TODO: find a better way to do this + const [searchParams2] = useSearchParams(localStorage.getItem("admin_psf_filter") ?? ""); + + const schema = yup.object({ + id: yup.string(), + property_space_id: yup.string(), + question: yup.string(), + }); + const { + reset, + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: (() => { + let fromSearch = parseSearchParams(searchParams); + if (Object.keys(fromSearch).length > 0) { + return fromSearch; + } + return parseSearchParams(searchParams2); + })(), + }); + + function onSort(accessor) { + const columns = tableColumns; + const index = columns.findIndex((column) => column.accessor === accessor); + const column = columns[index]; + column.isSortedDesc = !column.isSortedDesc; + columns.splice(index, 1, column); + setTableColumns(() => [...columns]); + const sortedList = selector(data, column.isSortedDesc, accessor); + setCurrentTableData(sortedList); + } + function selector(users, isSortedDesc, accessor) { + if (accessor?.split(",").length > 1) { + accessor = accessor.split(",")[0]; + } + + return users.sort((a, b) => { + if (isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1; + } else { + return a[accessor] < b[accessor] ? 1 : -1; + } + } + if (!isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1; + } else { + return a[accessor] < b[accessor] ? -1 : 1; + } + } + }); + } + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum) { + let data = parseSearchParams(searchParams); + data = Object.keys(data).length < 1 ? parseSearchParams(searchParams2) : data; + data.id = data.id?.replace(ID_PREFIX.PROPERTY_SPACE_FAQS, ""); + data.property_space_id = data.property_space_id?.replace(ID_PREFIX.PROPERTY_SPACE, ""); + + try { + let filter = ["deleted_at,is"]; + if (data.id) { + filter.push(`id,eq,${data.id}`); + } + if (data.question) { + filter.push(`question,cs,${data.question}`); + } + if (data.property_space_id) { + filter.push(`property_space_id,eq,${data.property_space_id}`); + } + + let result = await treeSdk.getPaginate("property_space_faq", { + filter, + join: [], + page: pageNum || 1, + size: limitNum, + order: "update_at", + }); + const { list, total, limit, num_pages, page } = result; + + console.log("result ", result); + + const sortedList = selector(list, false); + setCurrentTableData(sortedList); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + const onSubmit = (data) => { + console.log("submitting", data); + searchParams.set("id", data.id); + searchParams.set("property_space_id", data.property_space_id); + searchParams.set("question", data.question); + + setSearchParams(searchParams); + localStorage.setItem("admin_psf_filter", searchParams.toString()); + + getData(1, pageSize); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "property_spaces_faq", + }, + }); + + (async function () { + await fetchColumnOrder(); + getData(1, pageSize); + })(); + }, []); + + React.useEffect(() => { + if (state.deleted) { + globalDispatch({ + type: "DELETED", + payload: { + deleted: false, + }, + }); + getData(currentPage, pageSize); + } + }, [state.deleted]); + + async function fetchColumnOrder() { + sdk.setTable("settings"); + const payload = { key_name: "admin_property_spaces_faq_column_order" }; + try { + const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE"); + if (Array.isArray(result.list) && result.list.length > 0) { + setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_property_space_faqs)); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + return ( + <> +
    +
    +

    Property Spaces Faq Search

    + +
    + +
    +
    + + +

    {errors.id?.message}

    +
    + +
    + + +

    {errors.property_space_id?.message}

    +
    +
    + + +

    {errors.question?.message}

    +
    +
    + + + + + +
    + + Change Column Order + {" "} + +
    + +
    +
    +
    + + + + + ); +}; + +export default AdminPropertySpaceFaqListPage; diff --git a/src/pages/Admin/PropertySpaceFaq/EditAdminPropertySpaceFaqPage.jsx b/src/pages/Admin/PropertySpaceFaq/EditAdminPropertySpaceFaqPage.jsx new file mode 100644 index 0000000..326ce49 --- /dev/null +++ b/src/pages/Admin/PropertySpaceFaq/EditAdminPropertySpaceFaqPage.jsx @@ -0,0 +1,258 @@ +import React, { useEffect, useState, useRef } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import { useNavigate, useParams } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import EditAdminPageLayout from "@/layouts/EditAdminPageLayout"; +import SunEditor, { buttonList } from "suneditor-react"; +import "suneditor/dist/css/suneditor.min.css"; + +let sdk = new MkdSDK(); + +const EditAdminPropertySpaceFaqPage = () => { + const { dispatch } = React.useContext(AuthContext); + const [answer, setAnswer] = useState(""); + const schema = yup + .object({ + property_space_id: yup.number().positive().integer().typeError("Invalid ID").required(), + question: yup.string().required(), + answer: yup.string(), + }) + .required(); + const { dispatch: globalDispatch, state } = React.useContext(GlobalContext); + const buttonRef = useRef(null); + const navigate = useNavigate(); + const [id, setId] = useState(0); + const { + register, + handleSubmit, + setError, + clearErrors, + setValue, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const params = useParams(); + + const confirmPropertySpaceId = async (id) => { + if (id == "") { + clearErrors("property_space_id"); + return; + } + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/property-spaces/PAGINATE", + { + where: [`ergo_property_spaces.id = ${id}`], + page: 1, + limit: 1, + }, + "POST", + ); + if (result.error || !result.list || result.list.length < 1) throw new Error(); + clearErrors("property_space_id"); + } catch (error) { + console.log("ERROR", error); + setError("property_space_id", { + type: "manual", + message: "Property Space with this ID does not exist", + }); + } + }; + + useEffect(function () { + (async function () { + try { + sdk.setTable("property_space_faq"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + console.log(result); + if (!result.error) { + setValue("property_space_id", result.model.property_space_id); + setValue("question", result.model.question); + setAnswer(result.model.answer); + setId(result.model.id); + } + } catch (error) { + console.log("error", error); + tokenExpireError(dispatch, error.message); + } + })(); + }, []); + + const onError = () => { + if (answer == "") { + setError("answer", { + type: "manual", + message: "Answer is required", + }); + } + }; + + const onSubmit = async (data) => { + if (answer == "") { + setError("answer", { + type: "manual", + message: "Answer is required", + }); + return; + } + try { + const result = await sdk.callRestAPI( + { + id: id, + question: data.question, + answer, + property_space_id: data.property_space_id, + }, + "PUT", + ); + + if (!result.error) { + showToast(globalDispatch, "Updated"); + navigate("/admin/property_spaces_faq"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("question", { + type: "manual", + message: error.message, + }); + } + }; + useEffect(() => { + if (state.saveChanges) { + buttonRef.current.click(); + globalDispatch({ + type: "SAVE_CHANGES", + payload: { + saveChanges: false, + }, + }); + } + }, [state.saveChanges]); + + useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "property_space_faq", + }, + }); + }, []); + + return ( + +
    +
    + + confirmPropertySpaceId(e.target.value)} + /> +

    {errors.property_space_id?.message}

    +
    +
    + + +

    {errors.question?.message}

    +
    + +
    + + setAnswer(content)} + setContents={answer} + name="answer" + setOptions={{ buttonList: buttonList.complex }} + /> +

    {errors.answer?.message}

    +
    + +
    + + + +
    + +
    + ); +}; + +export default EditAdminPropertySpaceFaqPage; diff --git a/src/pages/Admin/PropertySpaceImage/AddAdminPropertySpacesImagesPage.jsx b/src/pages/Admin/PropertySpaceImage/AddAdminPropertySpacesImagesPage.jsx new file mode 100644 index 0000000..e3c73af --- /dev/null +++ b/src/pages/Admin/PropertySpaceImage/AddAdminPropertySpacesImagesPage.jsx @@ -0,0 +1,271 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate } from "react-router-dom"; +import { tokenExpireError, AuthContext } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import AddAdminPageLayout from "@/layouts/AddAdminPageLayout"; + +const AddAdminPropertySpacesImagesPage = () => { + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const [file, setFile] = React.useState(); + const [propertyId, setPropertyId] = React.useState(""); + const [spaces, setSpaces] = React.useState([]); + let sdk = new MkdSDK(); + const schema = yup + .object({ + property_id: yup.number().required("Property Id is required").typeError("Property ID must be a number"), + property_spaces_id: yup.number().required().positive().integer().typeError("No property selected"), + }) + .required(); + + const { dispatch } = React.useContext(AuthContext); + + const navigate = useNavigate(); + const { + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const handleFileUpload = async (data) => { + if (file) { + const formData = new FormData(); + for (let i = 0; i < file.length; i++) { + formData.append("file", file[i]); + } + try { + const upload = await sdk.uploadImage(formData); + data.image = upload.id; + onSubmit(data); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } else { + setError("image", { + type: "manual", + message: "Please include an image", + }); + } + }; + + const getPropertySpaces = async (propertyId) => { + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/property-spaces/PAGINATE", + { + where: [propertyId ? `ergo_property.id = ${propertyId}` : 1], + page: 1, + limit: 10, + }, + "POST", + ); + if (!result.error && result?.list) { + setSpaces(result.list); + } else { + setError("property_id", { + type: "manual", + message: "Property with this ID doesn't exist", + }); + } + } catch (error) { + console.log("Error", error); + setError("property_spaces_id", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + async function confirmPropertyID(propertyId) { + try { + sdk.setTable("property"); + const result = await sdk.callRestAPI( + { + id: propertyId, + }, + "GET", + ); + if (!result.error && result?.model) { + setError("property_id", { + type: "manual", + message: "", + }); + getPropertySpaces(propertyId); + } else { + setError("property_id", { + type: "manual", + message: "Property with this ID doesn't exist", + }); + } + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + + const onSubmit = async (data) => { + console.log("got here"); + try { + sdk.setTable("property_spaces_images"); + const result = await sdk.callRestAPI( + { + property_id: propertyId, + property_spaces_id: data.property_spaces_id, + photo_id: data.image, + }, + "POST", + ); + if (!result.error) { + showToast(globalDispatch, "Added"); + navigate("/admin/property_spaces_images"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("property_id", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + const onError = () => { + if (!file) { + setError("image", { + type: "manual", + message: "Please include an image", + }); + } + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "property_spaces_images", + }, + }); + }, []); + + return ( + +
    +
    + + { + setPropertyId(event.target.value); + confirmPropertyID(event.target.value); + }} + className={`"shadow border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline ${errors.property_id?.message ? "border-red-500" : ""}`} + /> +

    {errors.property_id?.message}

    +
    + +
    + + +

    {errors.property_spaces_id?.message}

    +
    + +
    + + { + setFile(e.target.files); + }} + /> +

    {errors.image?.message}

    +
    +
    + + +
    + +
    + ); +}; + +export default AddAdminPropertySpacesImagesPage; diff --git a/src/pages/Admin/PropertySpaceImage/AdminPropertySpacesImagesListPage.jsx b/src/pages/Admin/PropertySpaceImage/AdminPropertySpacesImagesListPage.jsx new file mode 100644 index 0000000..6c4101a --- /dev/null +++ b/src/pages/Admin/PropertySpaceImage/AdminPropertySpacesImagesListPage.jsx @@ -0,0 +1,714 @@ +import React from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { Link, useSearchParams, createSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import AddButton from "@/components/AddButton"; +import Button from "@/components/Button"; +import PaginationHeader from "@/components/PaginationHeader"; +import ReactHtmlTableToExcel from "react-html-table-to-excel"; +import { ID_PREFIX, IMAGE_STATUS } from "@/utils/constants"; +import { adminColumns, applySetting } from "@/utils/adminPortalColumns"; +import SwitchBulkMode from "@/components/SwitchBulkMode"; +import ImagePreviewPopup from "./ImagePreviewPopup"; +import RejectImageModal from "./RejectImageModal"; + +let sdk = new MkdSDK(); + +const AdminPropertySpacesImagesListPage = () => { + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch, state } = React.useContext(GlobalContext); + const [tableColumns, setTableColumns] = React.useState([]); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + const [bulkMode, setBulkMode] = React.useState(true); + const [bulkSelected, setBulkSelected] = React.useState([]); + const [searchParams, setSearchParams] = useSearchParams(); + // TODO: find a better way to do this + const [searchParams2] = useSearchParams(localStorage.getItem("admin_psi_filter") ?? ""); + const [modalImage, setModalImage] = React.useState(null); + const [modalOpen, setModalOpen] = React.useState(false); + const [activeRow, setActiveRow] = React.useState({}); + + const schema = yup.object({ + id: yup.string(), + property_id: yup.string(), + property_space_name: yup.string(), + property_name: yup.string(), + is_approved: yup.string(), + host_email: yup.string(), + }); + const { + reset, + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: (() => { + let fromSearch = parseSearchParams(searchParams); + if (Object.keys(fromSearch).length > 0) { + return fromSearch; + } + return parseSearchParams(searchParams2); + })(), + }); + + function onSort(accessor) { + const columns = tableColumns; + const index = columns.findIndex((column) => column.accessor === accessor); + const column = columns[index]; + column.isSortedDesc = !column.isSortedDesc; + columns.splice(index, 1, column); + setTableColumns(() => [...columns]); + const sortedList = selector(data, column.isSortedDesc, accessor); + setCurrentTableData(sortedList); + } + function selector(users, isSortedDesc, accessor) { + if (accessor?.split(",").length > 1) { + accessor = accessor.split(",")[0]; + } + + return users.sort((a, b) => { + if (isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1; + } else { + return a[accessor] < b[accessor] ? 1 : -1; + } + } + if (!isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1; + } else { + return a[accessor] < b[accessor] ? -1 : 1; + } + } + }); + } + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum) { + let data = parseSearchParams(searchParams); + data = Object.keys(data).length < 1 ? parseSearchParams(searchParams2) : data; + data.id = data.id?.replace(ID_PREFIX.PROPERTY_SPACE_IMAGES, ""); + + try { + sdk.setTable("property_spaces"); + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/property-space-images/PAGINATE", + { + where: [ + data + ? `${data.id ? `ergo_property_spaces_images.id = ${data.id}` : "1"} AND ${data.property_space_name ? `ergo_spaces.category LIKE '%${data.property_space_name}%'` : "1"} AND ${data.property_name ? `ergo_property.name LIKE '%${data.property_name}%'` : "1" + } AND ${data.property_spaces_id ? `property_spaces_id = ${data.property_spaces_id}` : "1"} AND ${data.is_approved != undefined ? `is_approved = ${data.is_approved}` : "1"} AND ${data.host_email ? `ergo_user.email LIKE '%${data.host_email}%'` : "1" + }` + : 1, + "ergo_property_spaces_images.deleted_at IS NULL", + ], + page: pageNum, + limit: limitNum, + sortId: "update_at", + direction: "DESC", + }, + "POST", + ); + + const { list, total, limit, num_pages, page } = result; + console.log("list", list); + const sortedList = selector(list, false); + setCurrentTableData(sortedList); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + const onSubmit = (data) => { + console.log("submitting", data); + searchParams.set("id", data.id); + searchParams.set("property_name", data.property_name); + searchParams.set("property_space_name", data.property_space_name); + searchParams.set("is_approved", data.is_approved); + searchParams.set("host_email", data.host_email); + searchParams.set("property_spaces_id", data.property_spaces_id); + setSearchParams(searchParams); + localStorage.setItem("admin_psi_filter", searchParams.toString()); + + getData(1, pageSize); + }; + + const setDefaultImage = async (data) => { + try { + sdk.setTable("property_spaces"); + const result = await sdk.callRestAPI( + { + id: data.id, + default_image_id: data.image_id, + }, + "PUT", + ); + + if (result.error) throw new Error(result.message || "Error when setting default image"); + showToast(globalDispatch, "Successful"); + getData(1, 10); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "property_spaces_images", + }, + }); + + (async function () { + await fetchColumnOrder(); + getData(1, pageSize); + })(); + }, []); + + React.useEffect(() => { + if (state.deleted) { + globalDispatch({ + type: "DELETED", + payload: { + deleted: false, + }, + }); + getData(currentPage, pageSize); + } + }, [state.deleted]); + + React.useEffect(() => { + let timeout; + if (!modalOpen) { + timeout = setTimeout(() => { + setModalImage(null); + }, 200); + } + + return () => clearTimeout(timeout); + }, [modalOpen]); + + async function fetchColumnOrder() { + sdk.setTable("settings"); + const payload = { key_name: "admin_property_spaces_images_column_order" }; + try { + const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE"); + if (Array.isArray(result.list) && result.list.length > 0) { + setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_property_space_images)); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + async function rejectImage(id) { + sdk.setTable("property_spaces_images"); + try { + await sdk.callRestAPI({ id, is_approved: IMAGE_STATUS.NOT_APPROVED }, "PUT"); + showToast(globalDispatch, "Successful"); + await getData(1, pageSize); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + async function approveImage(id) { + sdk.setTable("property_spaces_images"); + try { + await sdk.callRestAPI({ id, is_approved: IMAGE_STATUS.APPROVED }, "PUT"); + showToast(globalDispatch, "Successful"); + await getData(1, pageSize); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + async function bulkApprove() { + sdk.setTable("property_spaces_images"); + try { + await Promise.all(bulkSelected.map((id) => sdk.callRestAPI({ id, is_approved: IMAGE_STATUS.APPROVED }, "PUT"))); + showToast(globalDispatch, "Successful"); + await getData(1, pageSize); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + setBulkSelected([]); + } + + async function bulkReject() { + sdk.setTable("property_spaces_images"); + try { + await Promise.all(bulkSelected.map((id) => sdk.callRestAPI({ id, is_approved: IMAGE_STATUS.NOT_APPROVED }, "PUT"))); + showToast(globalDispatch, "Successful"); + await getData(1, pageSize); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + setBulkSelected([]); + } + + return ( + <> +
    +
    +

    Property Spaces Images

    + +
    +
    +
    + + +

    {errors.id?.message}

    +
    +
    + + +

    {errors.property_spaces_id?.message}

    +
    +
    + + +

    {errors.host_email?.message}

    +
    + +
    + + +

    {errors.property_name?.message}

    +
    + +
    + + +

    {errors.property_space_name?.message}

    +
    +
    + + +

    {errors.is_approved?.message}

    +
    +
    + + + + + +
    + + Change Column Order + {" "} + +
    + +
    + +
    + {bulkMode && ( +
    + + {bulkSelected.length > 0 ? ( +
    + + +
    + ) : null} +
    + )} + +
    +
    +
    + + + {bulkMode && ( + + )} + {tableColumns.map((column, index) => ( + + ))} + + + + {data.map((row, i) => { + return ( + + {bulkMode && ( + + )} + {tableColumns.map((cell, index) => { + if (cell.accessor.split(",").length > 1) { + return ( + + ); + } + if (cell.accessor === "") { + return ( + + ); + } + if (cell.accessor == "image" || cell.accessor == "photo_url") { + return ( + + ); + } + + if (cell.idPrefix) { + return ( + + ); + } + + if (cell.mapping) { + return ( + + ); + } + + return ( + + ); + })} + + ); + })} + +
    onSort(column.accessor)} + > + {column.header} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
    + { + if (bulkSelected.includes(row.id)) { + setBulkSelected((prev) => { + let copy = [...prev]; + copy.splice( + prev.findIndex((id) => id == row.id), + 1, + ); + return copy; + }); + } else { + setBulkSelected((prev) => [...prev, row.id]); + } + }} + checked={bulkSelected.includes(row.id)} + onChange={() => { }} + /> + + {cell.accessor.split(",").map((accessor) => ( + {row[accessor.trim()]} + ))} + + {row?.is_approved == IMAGE_STATUS.IN_REVIEW ? ( + <> + + + + ) : row?.is_approved === IMAGE_STATUS.APPROVED ? ( + + ) : ( + + )} + + {row?.default_image === 1 ? ( + (Default Image) + ) : ( + + )} + + + + {cell.idPrefix + row[cell.accessor]} + + {cell.mapping[row[cell.accessor] ?? 0]} + + {row[cell.accessor]} +
    +
    +
    + + setModalOpen(false)} + /> + setActiveRow({})} + data={activeRow} + onSuccess={() => getData(currentPage, pageSize)} + /> + + ); +}; + +export default AdminPropertySpacesImagesListPage; diff --git a/src/pages/Admin/PropertySpaceImage/EditAdminPropertySpacesImagesPage.jsx b/src/pages/Admin/PropertySpaceImage/EditAdminPropertySpacesImagesPage.jsx new file mode 100644 index 0000000..e0882a5 --- /dev/null +++ b/src/pages/Admin/PropertySpaceImage/EditAdminPropertySpacesImagesPage.jsx @@ -0,0 +1,171 @@ +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import { useNavigate, useParams } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import EditAdminPageLayout from "@/layouts/EditAdminPageLayout"; + +let sdk = new MkdSDK(); + +const EditAdminPropertySpacesImagesPage = () => { + const { dispatch } = React.useContext(AuthContext); + const schema = yup + .object({ + property_id: yup.number().required().positive().integer(), + property_spaces_id: yup.number().required().positive().integer(), + }) + .required(); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const navigate = useNavigate(); + const [id, setId] = useState(0); + const { + register, + handleSubmit, + setError, + setValue, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const params = useParams(); + + useEffect(function () { + (async function () { + try { + sdk.setTable("property_spaces_images"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + if (!result.error) { + setValue("property_id", result.model.property_id); + setValue("property_spaces_id", result.model.property_spaces_id); + setValue("photo_id", result.model.photo_id); + setId(result.model.id); + } + } catch (error) { + console.log("error", error); + tokenExpireError(dispatch, error.message); + } + })(); + }, []); + + const onSubmit = async (data) => { + try { + const result = await sdk.callRestAPI( + { + id: id, + property_id: data.property_id, + property_spaces_id: data.property_spaces_id, + photo_id: data.photo_id, + }, + "PUT", + ); + + if (!result.error) { + showToast(globalDispatch, "Updated"); + navigate("/admin/property_spaces_images"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("property_id", { + type: "manual", + message: error.message, + }); + } + }; + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "property_spaces_images", + }, + }); + }, []); + + return ( + +
    +
    + + +

    {errors.property_id?.message}

    +
    + +
    + + +

    {errors.property_spaces_id?.message}

    +
    +
    + + +

    {errors.photo_id?.message}

    +
    + +
    + + +
    +
    +
    + ); +}; + +export default EditAdminPropertySpacesImagesPage; diff --git a/src/pages/Admin/PropertySpaceImage/ImagePreviewPopup.jsx b/src/pages/Admin/PropertySpaceImage/ImagePreviewPopup.jsx new file mode 100644 index 0000000..d5e3ace --- /dev/null +++ b/src/pages/Admin/PropertySpaceImage/ImagePreviewPopup.jsx @@ -0,0 +1,52 @@ +import { Dialog, Transition } from "@headlessui/react"; +import { Fragment, useState } from "react"; + +export default function ImagePreviewPopup({ modalOpen, modalImage, closeModal }) { + return ( + <> + + + +
    + + +
    +
    + + + +
    +
    +
    +
    + + ); +} diff --git a/src/pages/Admin/PropertySpaceImage/RejectImageModal.jsx b/src/pages/Admin/PropertySpaceImage/RejectImageModal.jsx new file mode 100644 index 0000000..58e259c --- /dev/null +++ b/src/pages/Admin/PropertySpaceImage/RejectImageModal.jsx @@ -0,0 +1,121 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import { IMAGE_STATUS } from "@/utils/constants"; +import MkdSDK from "@/utils/MkdSDK"; +import { parseJsonSafely } from "@/utils/utils"; +import { Dialog, Transition } from "@headlessui/react"; +import { useContext, useState } from "react"; +import { Fragment } from "react"; + +export default function RejectImageModal({ modalOpen, data, closeModal, onSuccess }) { + const { dispatch } = useContext(AuthContext); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const [loading, setLoading] = useState(false); + + async function onSubmit(e) { + setLoading(true); + const sdk = new MkdSDK(); + e.preventDefault(); + const formData = new FormData(e.target); + const reason = formData.get("reason"); + sdk.setTable("property_spaces_images"); + try { + await sdk.callRestAPI({ id: data.id, is_approved: IMAGE_STATUS.NOT_APPROVED }, "PUT"); + + if (parseJsonSafely(data.settings, {}).email_on_space_image_declined == true) { + const tmpl = await sdk.getEmailTemplate("space-image-decline"); + const body = tmpl.html?.replace(new RegExp("{{{reason}}}", "g"), reason); + + await sdk.sendEmail(data.email, tmpl.subject, body); + showToast(globalDispatch, "Email sent to user"); + } else { + showToast(globalDispatch, "Successful"); + } + + onSuccess(); + e.target.reset(); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + closeModal(); + setLoading(false); + } + + return ( + <> + + + +
    + + +
    +
    + + + + Decline Reason + + +
    + + +
    +
    +
    +
    +
    +
    +
    + + ); +} diff --git a/src/pages/Admin/RecycleBin/AdminRecycleBinAmenities.jsx b/src/pages/Admin/RecycleBin/AdminRecycleBinAmenities.jsx new file mode 100644 index 0000000..77c6dcd --- /dev/null +++ b/src/pages/Admin/RecycleBin/AdminRecycleBinAmenities.jsx @@ -0,0 +1,438 @@ +import React, { useContext, useState } from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import Button from "@/components/Button"; +import SwitchBulkMode from "@/components/SwitchBulkMode"; +import moment from "moment"; +import TreeSDK from "@/utils/TreeSDK"; +import { ID_PREFIX } from "@/utils/constants"; +import RestoreModal from "./RestoreModal"; +import DeletePermanentlyModal from "./DeletePermanentlyModal"; +import RestoreAllModal from "./RestoreAllModal"; +import { Switch } from "@headlessui/react"; +import DeleteAllModal from "./DeleteAll"; + +let treeSdk = new TreeSDK() + +const columns = [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: true, + }, + + { + header: "Email", + nested: "user", + accessor: "email", + isSorted: true, + isSortedDesc: true, + }, + { + header: "Deleted At", + accessor: "deleted_at", + isSorted: true, + isSortedDesc: true, + format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"), + }, + { + header: "Actions", + accessor: "", + }, +]; + + + +export default function AdminRecycleBinPropertyAmenities() { + const { dispatch } = React.useContext(AuthContext); + const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext); + const [bulkMode, setBulkMode] = React.useState(false); + const [bulkSelected, setBulkSelected] = React.useState([]); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? ""); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedRestore, setSelectedRestore] = useState({}); + const [selectedDelete, setSelectedDelete] = useState({}); + const [restoreAll, setRestoreAll] = useState(false); + const [deleteAll, setDeleteAll] = useState(false); + + let sdk = new MkdSDK(); + let tdk = new TreeSDK(); + + const { + reset, + register, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: parseSearchParams(searchParams), + }); + + async function getData() { + setLoading(true); + try { + + let filter = ["ergo_property_spaces_amenitites.deleted_at IS NOT NULL"]; + if (data?.id) { + filter.push(`ergo_property_spaces_amenitites.id = ${data?.id}`); + + } + if (data?.deleted_at) { + filter[0] = (`DATE_FORMAT(ergo_property_spaces_amenitites.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`); + } + if (data?.email) { + filter.push(`ergo_user.email LIKE '${data?.email}'`); + } + const result = await sdk.callRawAPI("/v2/api/custom/ergo/property-spaces-amenitites/PAGINATE", + { + "where": filter, + "page": 1, + "limit": 10 + }, + "POST" + ) + setData(result.list); + + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + setLoading(false); + } + + function MyToggle(data) { + const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false) + const { dispatch: globalDispatch } = useContext(GlobalContext); + + let sdk = new MkdSDK(); + async function editUser() { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/soft-delete", { id: Number(data.user.id), entity: "user", type: "restore" }, "POST"); + if (!result.error) { + showToast(globalDispatch, result.message, 4000) + getData() + } + } + + + return ( + editUser()} + className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"} + relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`} + > + Use setting + + ) + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("entity_type", data.entity_type); + searchParams.set("deleted_at", data.deleted_at); + searchParams.set("email", data.email); + setSearchParams(searchParams); + localStorage.setItem("admin_recycle_filter", searchParams.toString()); + + getData(data); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "recycle_bin_properties_space_amenities`", + }, + }); + getData(); + }, []); + + return ( + <> +
    +
    +
    +

    Recycle Bin (Property Space Amenities)

    +
    +
    +
    + + +

    {errors.id?.message}

    +
    + +
    + + +

    {errors.email?.message}

    +
    + +
    + + +

    {errors.deleted_at?.message}

    +
    +
    + +
    +
    + +
    + +
    + + {bulkMode && ( +
    + + {bulkSelected.length > 0 ? ( +
    + {" "} + + +
    + ) : null} +
    + )} + +
    +
    + {loading ? ( +
    Loading...
    + ) : ( + + + + {bulkMode && ( + + )} + {columns.map((column, index) => ( + + ))} + + + + {data + .sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at)) + .map((row, i) => { + return ( + + {bulkMode && ( + + )} + {columns.map((cell, index) => { + if (cell.format) { + return ( + + ); + } + if (cell.accessor == "") { + return ( + + ); + } + if (cell.mapping) { + return ( + + ); + } + + + return ( + + ); + })} + + ); + })} + +
    + {column.header} + {column.isSorted} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
    + { + if (bulkSelected.some((item) => item.id == row.id)) { + setBulkSelected((prev) => { + let copy = [...prev]; + copy.splice( + prev.findIndex((item) => item.id == row.id), + 1, + ); + return copy; + }); + } else { + setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]); + } + }} + checked={bulkSelected.some((item) => item.id == row.id)} + onChange={() => { }} + /> + + {cell.format(row[cell.accessor])} + + {(row.email) && +
    + + Restore +
    + } + {(!row.email) && + + } + +
    + {cell.mapping[row[cell.accessor]]} + + {row[cell.accessor]} +
    + )} +
    +
    + setSelectedRestore({})} + data={selectedRestore} + onSuccess={() => getData()} + /> + setRestoreAll(false)} + records={bulkSelected} + onSuccess={() => { + setBulkSelected([]); + getData(); + }} + /> + setDeleteAll(false)} + records={bulkSelected} + table="property_spaces_amenitites" + onSuccess={() => { + setBulkSelected([]); + getData(); + }} + /> + setSelectedDelete({})} + data={selectedDelete} + onSuccess={() => getData()} + table="property_spaces_amenitites" + /> + + ); +} diff --git a/src/pages/Admin/RecycleBin/AdminRecycleBinBooking.jsx b/src/pages/Admin/RecycleBin/AdminRecycleBinBooking.jsx new file mode 100644 index 0000000..ae30796 --- /dev/null +++ b/src/pages/Admin/RecycleBin/AdminRecycleBinBooking.jsx @@ -0,0 +1,438 @@ +import React, { useContext, useState } from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import Button from "@/components/Button"; +import SwitchBulkMode from "@/components/SwitchBulkMode"; +import moment from "moment"; +import TreeSDK from "@/utils/TreeSDK"; +import { ID_PREFIX } from "@/utils/constants"; +import RestoreModal from "./RestoreModal"; +import DeletePermanentlyModal from "./DeletePermanentlyModal"; +import RestoreAllModal from "./RestoreAllModal"; +import { Switch } from "@headlessui/react"; +import DeleteAllModal from "./DeleteAll"; + +let treeSdk = new TreeSDK() + +const columns = [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: true, + }, + + { + header: "Email", + nested: "user", + accessor: "host_email", + isSorted: true, + isSortedDesc: true, + }, + { + header: "Deleted At", + accessor: "deleted_at", + isSorted: true, + isSortedDesc: true, + format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"), + }, + { + header: "Actions", + accessor: "", + }, +]; + + + +export default function AdminRecycleBinBookings() { + const { dispatch } = React.useContext(AuthContext); + const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext); + const [bulkMode, setBulkMode] = React.useState(false); + const [bulkSelected, setBulkSelected] = React.useState([]); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? ""); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedRestore, setSelectedRestore] = useState({}); + const [selectedDelete, setSelectedDelete] = useState({}); + const [restoreAll, setRestoreAll] = useState(false); + const [deleteAll, setDeleteAll] = useState(false); + + let sdk = new MkdSDK(); + let tdk = new TreeSDK(); + + const { + reset, + register, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: parseSearchParams(searchParams), + }); + + + async function getData(data) { + setLoading(true); + try { + + let filter = ["ergo_booking.deleted_at IS NOT NULL"]; + if (data?.id) { + filter.push(`ergo_booking.id = ${data?.id}`); + } + if (data?.deleted_at) { + filter[0] = (`DATE_FORMAT(ergo_booking.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`); + } + if (data?.email) { + filter.push(`ergo_user.email LIKE '${data?.email}'`); + } + const result = await sdk.callRawAPI("/v2/api/custom/ergo/booking/PAGINATE", + { + "where": filter, + "page": 1, + "limit": 10 + }, + "POST" + ) + setData(result.list); + + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + setLoading(false); + } + + function MyToggle(data) { + const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false) + const { dispatch: globalDispatch } = useContext(GlobalContext); + let sdk = new MkdSDK(); + sdk.setTable("device") + async function editUser() { + const result = await sdk.callRestAPI({ id: Number(data.user.id), deleted_at: null }, "PUT"); + if (!result.error) { + showToast(globalDispatch, "Booking Restored", 4000) + getData() + } + } + + + return ( + editUser()} + className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"} + relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`} + > + Use setting + + ) + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("deleted_at", data.deleted_at); + searchParams.set("email", data.email); + setSearchParams(searchParams); + localStorage.setItem("admin_recycle_filter", searchParams.toString()); + + getData(data); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "recycle_bin_booking", + }, + }); + getData(); + }, []); + + return ( + <> +
    +
    +
    +

    Recycle Bin (Booking)

    +
    +
    +
    + + +

    {errors.id?.message}

    +
    + +
    + + +

    {errors.email?.message}

    +
    + +
    + + +

    {errors.deleted_at?.message}

    +
    +
    + +
    +
    + +
    + +
    + + {bulkMode && ( +
    + + {bulkSelected.length > 0 ? ( +
    + {" "} + + +
    + ) : null} +
    + )} + +
    +
    + {loading ? ( +
    Loading...
    + ) : ( + + + + {bulkMode && ( + + )} + {columns.map((column, index) => ( + + ))} + + + + {data + .sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at)) + .map((row, i) => { + return ( + + {bulkMode && ( + + )} + {columns.map((cell, index) => { + if (cell.format) { + return ( + + ); + } + if (cell.accessor == "") { + return ( + + ); + } + + if (cell.mapping) { + return ( + + ); + } + + + return ( + + ); + })} + + ); + })} + +
    + {column.header} + {column.isSorted} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
    + { + if (bulkSelected.some((item) => item.id == row.id)) { + setBulkSelected((prev) => { + let copy = [...prev]; + copy.splice( + prev.findIndex((item) => item.id == row.id), + 1, + ); + return copy; + }); + } else { + setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]); + } + }} + checked={bulkSelected.some((item) => item.id == row.id)} + onChange={() => { }} + /> + + {cell.format(row[cell.accessor])} + + {(row.email) && +
    + +
    + } + {(!row.email) && + + } + +
    + {cell.mapping[row[cell.accessor]]} + + {row[cell.accessor]} +
    + )} +
    +
    + setSelectedRestore({})} + data={selectedRestore} + onSuccess={() => getData()} + /> + setRestoreAll(false)} + records={bulkSelected} + table="booking" + onSuccess={() => { + setBulkSelected([]); + getData(); + }} + /> + setDeleteAll(false)} + records={bulkSelected} + table="booking" + onSuccess={() => { + setBulkSelected([]); + getData(); + }} + /> + setSelectedDelete({})} + data={selectedDelete} + onSuccess={() => getData()} + table="booking" + /> + + ); +} diff --git a/src/pages/Admin/RecycleBin/AdminRecycleBinBookingAddon.jsx b/src/pages/Admin/RecycleBin/AdminRecycleBinBookingAddon.jsx new file mode 100644 index 0000000..3bb4461 --- /dev/null +++ b/src/pages/Admin/RecycleBin/AdminRecycleBinBookingAddon.jsx @@ -0,0 +1,424 @@ +import React, { useContext, useState } from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import Button from "@/components/Button"; +import SwitchBulkMode from "@/components/SwitchBulkMode"; +import moment from "moment"; +import TreeSDK from "@/utils/TreeSDK"; +import { ID_PREFIX } from "@/utils/constants"; +import RestoreModal from "./RestoreModal"; +import DeletePermanentlyModal from "./DeletePermanentlyModal"; +import RestoreAllModal from "./RestoreAllModal"; +import { Switch } from "@headlessui/react"; +import DeleteAllModal from "./DeleteAll"; + +let treeSdk = new TreeSDK() + +const columns = [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: true, + }, + { + header: "Addon Name", + accessor: "add_on_name", + isSorted: true, + isSortedDesc: true, + idPrefix: true, + }, + + { + header: "Deleted At", + accessor: "deleted_at", + isSorted: true, + isSortedDesc: true, + format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"), + }, + { + header: "Actions", + accessor: "", + }, +]; + + + +export default function AdminRecycleBinBookingAddons() { + const { dispatch } = React.useContext(AuthContext); + const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext); + const [bulkMode, setBulkMode] = React.useState(false); + const [bulkSelected, setBulkSelected] = React.useState([]); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? ""); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedRestore, setSelectedRestore] = useState({}); + const [selectedDelete, setSelectedDelete] = useState({}); + const [restoreAll, setRestoreAll] = useState(false); + const [deleteAll, setDeleteAll] = useState(false); + + let sdk = new MkdSDK(); + let tdk = new TreeSDK(); + + const { + reset, + register, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: parseSearchParams(searchParams), + }); + + + async function getData(data) { + setLoading(true); + try { + + let filter = ["ergo_booking_addons.deleted_at IS NOT NULL"]; + if (data?.id) { + filter.push(`ergo_booking_addons.id = ${data?.id}`); + } + if (data?.deleted_at) { + filter[0] = (`DATE_FORMAT(ergo_booking_addons.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`); + } + if (data?.email) { + filter.push(`ergo_user.email LIKE '${data?.email}'`); + } + const result = await sdk.callRawAPI("/v2/api/custom/ergo/booking-addon/PAGINATE", + { + "where": filter, + "page": 1, + "limit": 10 + }, + "POST" + ) + setData(result.list); + + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + setLoading(false); + } + + function MyToggle(data) { + const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false) + const { dispatch: globalDispatch } = useContext(GlobalContext); + let sdk = new MkdSDK(); + sdk.setTable("device") + async function editUser() { + const result = await sdk.callRestAPI({ id: Number(data.user.id), deleted_at: null }, "PUT"); + if (!result.error) { + showToast(globalDispatch, "Booking Addon Restored", 4000) + getData() + } + } + + + return ( + editUser()} + className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"} + relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`} + > + Use setting + + ) + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("deleted_at", data.deleted_at); + searchParams.set("email", data.email); + setSearchParams(searchParams); + localStorage.setItem("admin_recycle_filter", searchParams.toString()); + + getData(data); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "recycle_bin_booking_addon", + }, + }); + getData(); + }, []); + + return ( + <> +
    +
    +
    +

    Recycle Bin (Booking Addons)

    +
    +
    +
    + + +

    {errors.id?.message}

    +
    + +
    + + +

    {errors.deleted_at?.message}

    +
    +
    + +
    +
    + +
    + +
    + + {bulkMode && ( +
    + + {bulkSelected.length > 0 ? ( +
    + {" "} + + +
    + ) : null} +
    + )} + +
    +
    + {loading ? ( +
    Loading...
    + ) : ( + + + + {bulkMode && ( + + )} + {columns.map((column, index) => ( + + ))} + + + + {data + .sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at)) + .map((row, i) => { + return ( + + {bulkMode && ( + + )} + {columns.map((cell, index) => { + if (cell.format) { + return ( + + ); + } + if (cell.accessor == "") { + return ( + + ); + } + + if (cell.mapping) { + return ( + + ); + } + + + return ( + + ); + })} + + ); + })} + +
    + {column.header} + {column.isSorted} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
    + { + if (bulkSelected.some((item) => item.id == row.id)) { + setBulkSelected((prev) => { + let copy = [...prev]; + copy.splice( + prev.findIndex((item) => item.id == row.id), + 1, + ); + return copy; + }); + } else { + setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]); + } + }} + checked={bulkSelected.some((item) => item.id == row.id)} + onChange={() => { }} + /> + + {cell.format(row[cell.accessor])} + + {(row.email) && +
    + +
    + } + {(!row.email) && + + } + +
    + {cell.mapping[row[cell.accessor]]} + + {row[cell.accessor]} +
    + )} +
    +
    + setSelectedRestore({})} + data={selectedRestore} + onSuccess={() => getData()} + /> + setRestoreAll(false)} + records={bulkSelected} + table="booking_addons" + onSuccess={() => { + setBulkSelected([]); + getData(); + }} + /> + setDeleteAll(false)} + records={bulkSelected} + table="booking_addons" + onSuccess={() => { + setBulkSelected([]); + getData(); + }} + /> + setSelectedDelete({})} + data={selectedDelete} + onSuccess={() => getData()} + table="booking_addons" + /> + + ); +} diff --git a/src/pages/Admin/RecycleBin/AdminRecycleBinDevices.jsx b/src/pages/Admin/RecycleBin/AdminRecycleBinDevices.jsx new file mode 100644 index 0000000..6205209 --- /dev/null +++ b/src/pages/Admin/RecycleBin/AdminRecycleBinDevices.jsx @@ -0,0 +1,483 @@ +import React, { useContext, useState } from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import Button from "@/components/Button"; +import SwitchBulkMode from "@/components/SwitchBulkMode"; +import moment from "moment"; +import TreeSDK from "@/utils/TreeSDK"; +import { ID_PREFIX } from "@/utils/constants"; +import RestoreModal from "./RestoreModal"; +import DeletePermanentlyModal from "./DeletePermanentlyModal"; +import RestoreAllModal from "./RestoreAllModal"; +import { Switch } from "@headlessui/react"; +import DeleteAllModal from "./DeleteAll"; +import PaginationBar from "@/components/PaginationBar"; + +let treeSdk = new TreeSDK() + +const columns = [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: true, + }, + + { + header: "Email", + nested: "user", + accessor: "email", + isSorted: true, + isSortedDesc: true, + }, + { + header: "Deleted At", + accessor: "deleted_at", + isSorted: true, + isSortedDesc: true, + format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"), + }, + { + header: "Actions", + accessor: "", + }, +]; + + + +export default function AdminRecycleBinDevices() { + const { dispatch } = React.useContext(AuthContext); + const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext); + const [bulkMode, setBulkMode] = React.useState(false); + const [bulkSelected, setBulkSelected] = React.useState([]); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? ""); + const [data, setData] = useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + const [loading, setLoading] = useState(false); + const [selectedRestore, setSelectedRestore] = useState({}); + const [selectedDelete, setSelectedDelete] = useState({}); + const [restoreAll, setRestoreAll] = useState(false); + const [deleteAll, setDeleteAll] = useState(false); + + let sdk = new MkdSDK(); + let tdk = new TreeSDK(); + + const { + reset, + register, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: parseSearchParams(searchParams), + }); + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum, data) { + setLoading(true); + try { + + let filter = ["ergo_device.deleted_at IS NOT NULL"]; + if (data?.id) { + filter.push(`ergo_device.id = ${data?.id}`); + } + if (data?.deleted_at) { + filter[0] = (`DATE_FORMAT(ergo_device.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`); + } + if (data?.email) { + filter.push(`ergo_user.email LIKE '${data?.email}'`); + } + const result = await sdk.callRawAPI("/v2/api/custom/ergo/device/PAGINATE", + { + "where": filter, + "page": pageNum, + "limit": limitNum + }, + "POST" + ) + + const { list, total, limit, num_pages, page } = result; + + setData(list); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + setLoading(false); + } + + function MyToggle(data) { + const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false) + const { dispatch: globalDispatch } = useContext(GlobalContext); + let sdk = new MkdSDK(); + sdk.setTable("device") + async function editUser() { + const result = await sdk.callRestAPI({ id: Number(data.user.id), deleted_at: null }, "PUT"); + if (!result.error) { + showToast(globalDispatch, "Device Restored", 4000) + getData(currentPage, pageSize) + } + } + + + return ( + editUser()} + className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"} + relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`} + > + Use setting + + ) + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("deleted_at", data.deleted_at); + searchParams.set("email", data.email); + setSearchParams(searchParams); + localStorage.setItem("admin_recycle_filter", searchParams.toString()); + + getData(currentPage, pageSize, data); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "recycle_bin_devices", + }, + }); + getData(1, pageSize); + }, []); + + return ( + <> +
    +
    +
    +

    Recycle Bin (Devices)

    +
    +
    +
    + + +

    {errors.id?.message}

    +
    + +
    + + +

    {errors.email?.message}

    +
    + +
    + + +

    {errors.deleted_at?.message}

    +
    +
    + +
    +
    + +
    + +
    + + {bulkMode && ( +
    + + {bulkSelected.length > 0 ? ( +
    + {" "} + + +
    + ) : null} +
    + )} + +
    +
    + {loading ? ( +
    Loading...
    + ) : ( + + + + {bulkMode && ( + + )} + {columns.map((column, index) => ( + + ))} + + + + {data + .sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at)) + .map((row, i) => { + return ( + + {bulkMode && ( + + )} + {columns.map((cell, index) => { + if (cell.format) { + return ( + + ); + } + if (cell.accessor == "") { + return ( + + ); + } + + if (cell.mapping) { + return ( + + ); + } + + + return ( + + ); + })} + + ); + })} + +
    + {column.header} + {column.isSorted} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
    + { + if (bulkSelected.some((item) => item.id == row.id)) { + setBulkSelected((prev) => { + let copy = [...prev]; + copy.splice( + prev.findIndex((item) => item.id == row.id), + 1, + ); + return copy; + }); + } else { + setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]); + } + }} + checked={bulkSelected.some((item) => item.id == row.id)} + onChange={() => { }} + /> + + {cell.format(row[cell.accessor])} + + {(row.email) && +
    + + Restore +
    + } + {(!row.email) && + + } + +
    + {cell.mapping[row[cell.accessor]]} + + {row[cell.accessor]} +
    + )} +
    +
    + + setSelectedRestore({})} + data={selectedRestore} + onSuccess={() => getData(currentPage, pageSize)} + /> + setRestoreAll(false)} + records={bulkSelected} + table="device" + onSuccess={() => { + setBulkSelected([]); + getData(currentPage, pageSize); + }} + /> + setDeleteAll(false)} + records={bulkSelected} + table="device" + onSuccess={() => { + setBulkSelected([]); + getData(currentPage, pageSize); + }} + /> + setSelectedDelete({})} + data={selectedDelete} + onSuccess={() => getData(currentPage, pageSize)} + table="device" + /> + + ); +} diff --git a/src/pages/Admin/RecycleBin/AdminRecycleBinFaqs.jsx b/src/pages/Admin/RecycleBin/AdminRecycleBinFaqs.jsx new file mode 100644 index 0000000..f2ae414 --- /dev/null +++ b/src/pages/Admin/RecycleBin/AdminRecycleBinFaqs.jsx @@ -0,0 +1,453 @@ +import React, { useContext, useState } from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import Button from "@/components/Button"; +import SwitchBulkMode from "@/components/SwitchBulkMode"; +import moment from "moment"; +import TreeSDK from "@/utils/TreeSDK"; +import { ID_PREFIX } from "@/utils/constants"; +import RestoreModal from "./RestoreModal"; +import DeletePermanentlyModal from "./DeletePermanentlyModal"; +import RestoreAllModal from "./RestoreAllModal"; +import { Switch } from "@headlessui/react"; +import DeleteAllModal from "./DeleteAll"; + +let treeSdk = new TreeSDK() + +const columns = [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: true, + }, + + { + header: "Deleted At", + accessor: "deleted_at", + isSorted: true, + isSortedDesc: true, + format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"), + }, + { + header: "Actions", + accessor: "", + }, +]; + + + +export default function AdminRecycleBinFaqs() { + const { dispatch } = React.useContext(AuthContext); + const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext); + const [bulkMode, setBulkMode] = React.useState(false); + const [bulkSelected, setBulkSelected] = React.useState([]); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? ""); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedRestore, setSelectedRestore] = useState({}); + const [selectedDelete, setSelectedDelete] = useState({}); + const [restoreAll, setRestoreAll] = useState(false); + + const [deleteAll, setDeleteAll] = useState(false); + + let sdk = new MkdSDK(); + let tdk = new TreeSDK(); + + const { + reset, + register, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: parseSearchParams(searchParams), + }); + + async function getData(data) { + setLoading(true); + try { + + let filter = ["ergo_faq.deleted_at IS NOT NULL"]; + if (data?.id) { + filter.push(`ergo_faq.id = ${data?.id}`); + } + if (data?.deleted_at) { + filter[0] = (`DATE_FORMAT(ergo_faq.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`); + } + const result = await sdk.callRawAPI("/v2/api/custom/ergo/faq/PAGINATE", + { + "where": filter, + "page": 1, + "limit": 10 + }, + "POST" + ) + setData(result.list); + + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + setLoading(false); + } + + function MyToggle(data) { + const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false) + const { dispatch: globalDispatch } = useContext(GlobalContext); + + let sdk = new MkdSDK(); + async function editUser() { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/soft-delete", { id: Number(data.user.id), entity: "user", type: "restore" }, "POST"); + if (!result.error) { + showToast(globalDispatch, result.message, 4000) + getData() + } + } + + + return ( + editUser()} + className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"} + relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`} + > + Use setting + + ) + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("entity_type", data.entity_type); + searchParams.set("deleted_at", data.deleted_at); + searchParams.set("email", data.email); + setSearchParams(searchParams); + localStorage.setItem("admin_recycle_filter", searchParams.toString()); + + getData(data); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "recycle_bin_faqs", + }, + }); + getData(); + }, []); + + return ( + <> +
    +
    +
    +

    Recycle Bin (Faqs)

    +
    +
    +
    + + +

    {errors.id?.message}

    +
    + +
    + + +

    {errors.deleted_at?.message}

    +
    +
    + +
    +
    + +
    + +
    + + {bulkMode && ( +
    + + {bulkSelected.length > 0 ? ( +
    + {" "} + + +
    + ) : null} +
    + )} + +
    +
    + {loading ? ( +
    Loading...
    + ) : ( + + + + {bulkMode && ( + + )} + {columns.map((column, index) => ( + + ))} + + + + {data + .sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at)) + .map((row, i) => { + return ( + + {bulkMode && ( + + )} + {columns.map((cell, index) => { + if (cell.format) { + return ( + + ); + } + if (cell.accessor == "") { + return ( + + ); + } + if (cell.accessor == "email" && (row?.user_id || row?.host_id)) { + return ( + + ); + } + if (cell.accessor == "email" && (row?.email)) { + return ( + + ); + } + if (cell.accessor == "email" && ((!row?.user_id || !row?.host_id) && !row?.cost)) { + return ( + + ); + } + if (cell.accessor == "email" && (row?.entity_type == "add_on")) { + return ( + + ); + } + if (cell.mapping) { + return ( + + ); + } + + + return ( + + ); + })} + + ); + })} + +
    + {column.header} + {column.isSorted} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
    + { + if (bulkSelected.some((item) => item.id == row.id)) { + setBulkSelected((prev) => { + let copy = [...prev]; + copy.splice( + prev.findIndex((item) => item.id == row.id), + 1, + ); + return copy; + }); + } else { + setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]); + } + }} + checked={bulkSelected.some((item) => item.id == row.id)} + onChange={() => { }} + /> + + {cell.format(row[cell.accessor])} + + {(row.email) && +
    + +
    + } + {(!row.email) && + + } + +
    + {getUserDetail(row?.user_id || row?.host_id)} + + {getUserDetail(row?.id)} + + {getSpaceHost(row?.property_id)} + + {getAddonOwner(row?.id)} + + {cell.mapping[row[cell.accessor]]} + + {row[cell.accessor]} +
    + )} +
    +
    + setSelectedRestore({})} + data={selectedRestore} + onSuccess={() => getData()} + /> + setRestoreAll(false)} + records={bulkSelected} + onSuccess={() => { + setBulkSelected([]); + getData(); + }} + /> + setDeleteAll(false)} + records={bulkSelected} + table="faq" + onSuccess={() => { + setBulkSelected([]); + getData(); + }} + /> + setSelectedDelete({})} + data={selectedDelete} + onSuccess={() => getData()} + table="faq" + /> + + ); +} diff --git a/src/pages/Admin/RecycleBin/AdminRecycleBinPayout.jsx b/src/pages/Admin/RecycleBin/AdminRecycleBinPayout.jsx new file mode 100644 index 0000000..d59a7da --- /dev/null +++ b/src/pages/Admin/RecycleBin/AdminRecycleBinPayout.jsx @@ -0,0 +1,423 @@ +import React, { useContext, useState } from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import Button from "@/components/Button"; +import SwitchBulkMode from "@/components/SwitchBulkMode"; +import moment from "moment"; +import TreeSDK from "@/utils/TreeSDK"; +import { ID_PREFIX } from "@/utils/constants"; +import RestoreModal from "./RestoreModal"; +import DeletePermanentlyModal from "./DeletePermanentlyModal"; +import RestoreAllModal from "./RestoreAllModal"; +import { Switch } from "@headlessui/react"; + +let treeSdk = new TreeSDK() + +const columns = [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: true, + }, + + { + header: "Email", + nested: "user", + accessor: "host_email", + isSorted: true, + isSortedDesc: true, + }, + { + header: "Deleted At", + accessor: "deleted_at", + isSorted: true, + isSortedDesc: true, + format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"), + }, + { + header: "Actions", + accessor: "", + }, +]; + + + +export default function AdminRecycleBinPayout() { + const { dispatch } = React.useContext(AuthContext); + const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext); + const [bulkMode, setBulkMode] = React.useState(false); + const [bulkSelected, setBulkSelected] = React.useState([]); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? ""); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedRestore, setSelectedRestore] = useState({}); + const [selectedDelete, setSelectedDelete] = useState({}); + const [restoreAll, setRestoreAll] = useState(false); + + let sdk = new MkdSDK(); + let tdk = new TreeSDK(); + + const { + reset, + register, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: parseSearchParams(searchParams), + }); + + async function getData() { + setLoading(true); + try { + + let filter = ["deleted_at,nis"]; + if (data?.id) { + filter.push(`ergo_payout.id = ${data?.id}`); + } + if (data?.deleted_at) { + filter[0] = (`DATE_FORMAT(ergo_payout.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`); + + } + if (data.email) { + filter.push(`ergo_user,cs,${data.email}`); + } + const result = await sdk.callRawAPI("/v2/api/custom/ergo/payout/PAGINATE", + { + "where": ["ergo_payout.deleted_at IS NOT NULL"], + "page": 1, + "limit": 10 + }, + "POST" + ) + setData(result.list); + + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + setLoading(false); + } + + function MyToggle(data) { + const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false) + const { dispatch: globalDispatch } = useContext(GlobalContext); + + let sdk = new MkdSDK(); + async function editUser() { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/soft-delete", { id: Number(data.user.id), entity: "user", type: "restore" }, "POST"); + if (!result.error) { + showToast(globalDispatch, result.message, 4000) + getData() + } + } + + + return ( + editUser()} + className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"} + relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`} + > + Use setting + + ) + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("entity_type", data.entity_type); + searchParams.set("deleted_at", data.deleted_at); + searchParams.set("email", data.email); + setSearchParams(searchParams); + localStorage.setItem("admin_recycle_filter", searchParams.toString()); + + getData(); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "recycle_bin_payout`", + }, + }); + getData(); + }, []); + + return ( + <> +
    +
    +
    +

    Recycle Bin (Payout)

    +
    +
    +
    + + +

    {errors.id?.message}

    +
    + +
    + + +

    {errors.email?.message}

    +
    + +
    + + +

    {errors.deleted_at?.message}

    +
    +
    + +
    +
    + +
    + +
    + + {bulkMode && ( +
    + + {bulkSelected.length > 0 ? ( +
    + {" "} + + +
    + ) : null} +
    + )} + +
    +
    + {loading ? ( +
    Loading...
    + ) : ( + + + + {bulkMode && ( + + )} + {columns.map((column, index) => ( + + ))} + + + + {data + .sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at)) + .map((row, i) => { + return ( + + {bulkMode && ( + + )} + {columns.map((cell, index) => { + if (cell.format) { + return ( + + ); + } + if (cell.accessor == "") { + return ( + + ); + } + if (cell.mapping) { + return ( + + ); + } + + + return ( + + ); + })} + + ); + })} + +
    + {column.header} + {column.isSorted} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
    + { + if (bulkSelected.some((item) => item.id == row.id)) { + setBulkSelected((prev) => { + let copy = [...prev]; + copy.splice( + prev.findIndex((item) => item.id == row.id), + 1, + ); + return copy; + }); + } else { + setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]); + } + }} + checked={bulkSelected.some((item) => item.id == row.id)} + onChange={() => { }} + /> + + {cell.format(row[cell.accessor])} + + {(row.email) && +
    + +
    + } + {(!row.email) && + + } + +
    + {cell.mapping[row[cell.accessor]]} + + {row[cell.accessor]} +
    + )} +
    +
    + setSelectedRestore({})} + data={selectedRestore} + onSuccess={() => getData()} + /> + setRestoreAll(false)} + records={bulkSelected} + onSuccess={() => { + setBulkSelected([]); + getData(); + }} + /> + setSelectedDelete({})} + data={selectedDelete} + onSuccess={() => getData()} + /> + + ); +} diff --git a/src/pages/Admin/RecycleBin/AdminRecycleBinProperties.jsx b/src/pages/Admin/RecycleBin/AdminRecycleBinProperties.jsx new file mode 100644 index 0000000..9f57375 --- /dev/null +++ b/src/pages/Admin/RecycleBin/AdminRecycleBinProperties.jsx @@ -0,0 +1,443 @@ +import React, { useContext, useState } from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import Button from "@/components/Button"; +import SwitchBulkMode from "@/components/SwitchBulkMode"; +import moment from "moment"; +import TreeSDK from "@/utils/TreeSDK"; +import { ID_PREFIX } from "@/utils/constants"; +import RestoreModal from "./RestoreModal"; +import DeletePermanentlyModal from "./DeletePermanentlyModal"; +import RestoreAllModal from "./RestoreAllModal"; +import { Switch } from "@headlessui/react"; +import DeleteAllModal from "./DeleteAll"; + +let treeSdk = new TreeSDK() + +const columns = [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: true, + }, + + { + header: "Email", + nested: "user", + accessor: "email", + isSorted: true, + isSortedDesc: true, + }, + { + header: "Deleted At", + accessor: "deleted_at", + isSorted: true, + isSortedDesc: true, + format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"), + }, + { + header: "Actions", + accessor: "", + }, +]; + + + +export default function AdminRecycleBinProperties() { + const { dispatch } = React.useContext(AuthContext); + const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext); + const [bulkMode, setBulkMode] = React.useState(false); + const [bulkSelected, setBulkSelected] = React.useState([]); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? ""); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedRestore, setSelectedRestore] = useState({}); + const [selectedDelete, setSelectedDelete] = useState({}); + const [restoreAll, setRestoreAll] = useState(false); + const [deleteAll, setDeleteAll] = useState(false); + + let sdk = new MkdSDK(); + let tdk = new TreeSDK(); + + const { + reset, + register, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: parseSearchParams(searchParams), + }); + + async function getData(data) { + setLoading(true); + try { + + let filter = ["ergo_property.deleted_at IS NOT NULL"]; + if (data?.id) { + filter.push(`ergo_property.id = ${data?.id}`); + } + if (data?.deleted_at) { + filter[0] = (`DATE_FORMAT(ergo_property.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`); + + } + if (data?.email) { + filter.push(`ergo_user.email LIKE '${data?.email}'`); + } + const result = await sdk.callRawAPI("/v2/api/custom/ergo/property/PAGINATE", + { + "where": filter, + "page": 1, + "limit": 10 + }, + "POST" + ) + setData(result.list); + + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + setLoading(false); + } + + function MyToggle(data) { + const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false) + const { dispatch: globalDispatch } = useContext(GlobalContext); + + let sdk = new MkdSDK(); + async function editUser() { + // const result = await sdk.callRawAPI("/v2/api/custom/ergo/property", { id: Number(data.user.id), deleted_at: null }, "PUT"); + // if (!result.error) { + // showToast(globalDispatch, result.message, 4000) + // getData() + // } + const result = await sdk.callRawAPI("/v2/api/custom/ergo/soft-delete", { id: Number(data.user.id), entity: "property", type: "restore" }, "POST"); + if (!result.error) { + showToast(globalDispatch, result.message, 4000) + getData() + } + } + + + return ( + editUser()} + className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"} + relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`} + > + Use setting + + ) + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("entity_type", data.entity_type); + searchParams.set("deleted_at", data.deleted_at); + searchParams.set("email", data.email); + setSearchParams(searchParams); + localStorage.setItem("admin_recycle_filter", searchParams.toString()); + + getData(data); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "recycle_bin_properties`", + }, + }); + getData(); + }, []); + + return ( + <> +
    +
    +
    +

    Recycle Bin (Properties)

    +
    +
    +
    + + +

    {errors.id?.message}

    +
    + +
    + + +

    {errors.email?.message}

    +
    + +
    + + +

    {errors.deleted_at?.message}

    +
    +
    + +
    +
    + +
    + +
    + + {bulkMode && ( +
    + + {bulkSelected.length > 0 ? ( +
    + {" "} + + +
    + ) : null} +
    + )} + +
    +
    + {loading ? ( +
    Loading...
    + ) : ( + + + + {bulkMode && ( + + )} + {columns.map((column, index) => ( + + ))} + + + + {data + .sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at)) + .map((row, i) => { + return ( + + {bulkMode && ( + + )} + {columns.map((cell, index) => { + if (cell.format) { + return ( + + ); + } + if (cell.accessor == "") { + return ( + + ); + } + if (cell.mapping) { + return ( + + ); + } + + + return ( + + ); + })} + + ); + })} + +
    + {column.header} + {column.isSorted} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
    + { + if (bulkSelected.some((item) => item.id == row.id)) { + setBulkSelected((prev) => { + let copy = [...prev]; + copy.splice( + prev.findIndex((item) => item.id == row.id), + 1, + ); + return copy; + }); + } else { + setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]); + } + }} + checked={bulkSelected.some((item) => item.id == row.id)} + onChange={() => { }} + /> + + {cell.format(row[cell.accessor])} + + {(row.email) && +
    + + Restore +
    + } + {(!row.email) && + + } + +
    + {cell.mapping[row[cell.accessor]]} + + {row[cell.accessor]} +
    + )} +
    +
    + setSelectedRestore({})} + data={selectedRestore} + onSuccess={() => getData()} + /> + setRestoreAll(false)} + records={bulkSelected} + onSuccess={() => { + setBulkSelected([]); + getData(); + }} + /> + setDeleteAll(false)} + records={bulkSelected} + table="property" + onSuccess={() => { + setBulkSelected([]); + getData(); + }} + /> + setSelectedDelete({})} + data={selectedDelete} + onSuccess={() => getData()} + table="property" + /> + + ); +} diff --git a/src/pages/Admin/RecycleBin/AdminRecycleBinPropertyAddon.jsx b/src/pages/Admin/RecycleBin/AdminRecycleBinPropertyAddon.jsx new file mode 100644 index 0000000..5d32ab5 --- /dev/null +++ b/src/pages/Admin/RecycleBin/AdminRecycleBinPropertyAddon.jsx @@ -0,0 +1,408 @@ +import React, { useContext, useState } from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import Button from "@/components/Button"; +import SwitchBulkMode from "@/components/SwitchBulkMode"; +import moment from "moment"; +import TreeSDK from "@/utils/TreeSDK"; +import { ID_PREFIX } from "@/utils/constants"; +import RestoreModal from "./RestoreModal"; +import DeletePermanentlyModal from "./DeletePermanentlyModal"; +import RestoreAllModal from "./RestoreAllModal"; +import { Switch } from "@headlessui/react"; + +let treeSdk = new TreeSDK() + +const columns = [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: true, + }, + + { + header: "Deleted At", + accessor: "deleted_at", + isSorted: true, + isSortedDesc: true, + format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"), + }, + { + header: "Actions", + accessor: "", + }, +]; + + + +export default function AdminRecycleBinPropertyAddons() { + const { dispatch } = React.useContext(AuthContext); + const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext); + const [bulkMode, setBulkMode] = React.useState(false); + const [bulkSelected, setBulkSelected] = React.useState([]); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? ""); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedRestore, setSelectedRestore] = useState({}); + const [selectedDelete, setSelectedDelete] = useState({}); + const [restoreAll, setRestoreAll] = useState(false); + const [users, setUsers] = useState([]); + const [add, setAddons] = useState([]); + const [adminEmail, setAdminEmail] = useState(); + const [properties, setProperties] = useState(); + + let sdk = new MkdSDK(); + let tdk = new TreeSDK(); + + const { + reset, + register, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: parseSearchParams(searchParams), + }); + + + async function getData(data) { + setLoading(true); + try { + + let filter = ["ergo_property_add_on.deleted_at IS NOT NULL"]; + if (data?.id) { + filter.push(`ergo_property_add_on.id = ${data?.id}`); + } + if (data?.deleted_at) { + filter[0] = (`DATE_FORMAT(ergo_property_add_on.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`); + + } + if (data?.email) { + filter.push(`ergo_property_add_on.email LIKE '${data?.email}'`); + } + const result = await sdk.callRawAPI("/v2/api/custom/ergo/property_add_on/PAGINATE", + { + "where": filter, + "page": 1, + "limit": 10 + }, + "POST" + ) + setData(result.list); + + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + setLoading(false); + } + + function MyToggle(data) { + const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false) + const { dispatch: globalDispatch } = useContext(GlobalContext); + + let sdk = new MkdSDK(); + async function editUser() { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/property_add_on", { id: Number(data.id), deleted_at: NULL }, "PUT"); + if (!result.error) { + showToast(globalDispatch, result.message, 4000) + getData() + } + } + + + return ( + editUser()} + className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"} + relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`} + > + Use setting + + ) + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("entity_type", data.entity_type); + searchParams.set("deleted_at", data.deleted_at); + searchParams.set("email", data.email); + setSearchParams(searchParams); + localStorage.setItem("admin_recycle_filter", searchParams.toString()); + + getData(data); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "recycle_bin_properties_addon`", + }, + }); + getData(); + }, []); + + return ( + <> +
    +
    +
    +

    Recycle Bin (Property Addons)

    +
    +
    +
    + + +

    {errors.id?.message}

    +
    + +
    + + +

    {errors.deleted_at?.message}

    +
    +
    + +
    +
    + +
    + +
    + + {bulkMode && ( +
    + + {bulkSelected.length > 0 ? ( +
    + {" "} + + +
    + ) : null} +
    + )} + +
    +
    + {loading ? ( +
    Loading...
    + ) : ( + + + + {bulkMode && ( + + )} + {columns.map((column, index) => ( + + ))} + + + + {data + .sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at)) + .map((row, i) => { + return ( + + {bulkMode && ( + + )} + {columns.map((cell, index) => { + if (cell.format) { + return ( + + ); + } + if (cell.accessor == "") { + return ( + + ); + } + if (cell.mapping) { + return ( + + ); + } + + + return ( + + ); + })} + + ); + })} + +
    + {column.header} + {column.isSorted} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
    + { + if (bulkSelected.some((item) => item.id == row.id)) { + setBulkSelected((prev) => { + let copy = [...prev]; + copy.splice( + prev.findIndex((item) => item.id == row.id), + 1, + ); + return copy; + }); + } else { + setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]); + } + }} + checked={bulkSelected.some((item) => item.id == row.id)} + onChange={() => { }} + /> + + {cell.format(row[cell.accessor])} + + {(row.email) && +
    + +
    + } + {(!row.email) && + + } + +
    + {cell.mapping[row[cell.accessor]]} + + {row[cell.accessor]} +
    + )} +
    +
    + setSelectedRestore({})} + data={selectedRestore} + onSuccess={() => getData()} + /> + setRestoreAll(false)} + records={bulkSelected} + onSuccess={() => { + setBulkSelected([]); + getData(); + }} + /> + setSelectedDelete({})} + data={selectedDelete} + onSuccess={() => getData()} + table="property_add_on" + /> + + ); +} diff --git a/src/pages/Admin/RecycleBin/AdminRecycleBinPropertySpaces.jsx b/src/pages/Admin/RecycleBin/AdminRecycleBinPropertySpaces.jsx new file mode 100644 index 0000000..3c87ff4 --- /dev/null +++ b/src/pages/Admin/RecycleBin/AdminRecycleBinPropertySpaces.jsx @@ -0,0 +1,423 @@ +import React, { useContext, useState } from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import Button from "@/components/Button"; +import SwitchBulkMode from "@/components/SwitchBulkMode"; +import moment from "moment"; +import TreeSDK from "@/utils/TreeSDK"; +import { ID_PREFIX } from "@/utils/constants"; +import RestoreModal from "./RestoreModal"; +import DeletePermanentlyModal from "./DeletePermanentlyModal"; +import RestoreAllModal from "./RestoreAllModal"; +import { Switch } from "@headlessui/react"; + +let treeSdk = new TreeSDK() + +const columns = [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: true, + }, + + { + header: "Email", + nested: "user", + accessor: "email", + isSorted: true, + isSortedDesc: true, + }, + { + header: "Deleted At", + accessor: "deleted_at", + isSorted: true, + isSortedDesc: true, + format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"), + }, + { + header: "Actions", + accessor: "", + }, +]; + + + +export default function AdminRecycleBinPropertySpaces() { + const { dispatch } = React.useContext(AuthContext); + const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext); + const [bulkMode, setBulkMode] = React.useState(false); + const [bulkSelected, setBulkSelected] = React.useState([]); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? ""); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedRestore, setSelectedRestore] = useState({}); + const [selectedDelete, setSelectedDelete] = useState({}); + const [restoreAll, setRestoreAll] = useState(false); + + let sdk = new MkdSDK(); + let tdk = new TreeSDK(); + + const { + reset, + register, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: parseSearchParams(searchParams), + }); + + async function getData(data) { + setLoading(true); + try { + + let filter = ["ergo_property_spaces.deleted_at IS NOT NULL"]; + if (data?.id) { + filter.push(`ergo_property_spaces.id = ${data?.id}`); + } + if (data?.deleted_at) { + filter[0] = (`DATE_FORMAT(ergo_property_spaces.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`); + + } + if (data?.email) { + filter.push(`ergo_property_spaces.email LIKE '${data?.email}'`); + } + const result = await sdk.callRawAPI("/v2/api/custom/ergo/property-spaces/PAGINATE", + { + "where": filter, + "page": 1, + "limit": 10 + }, + "POST" + ) + setData(result.list); + + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + setLoading(false); + } + + function MyToggle(data) { + const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false) + const { dispatch: globalDispatch } = useContext(GlobalContext); + + let sdk = new MkdSDK(); + async function editUser() { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/soft-delete", { id: Number(data.user.id), entity: "user", type: "restore" }, "POST"); + if (!result.error) { + showToast(globalDispatch, result.message, 4000) + getData() + } + } + + + return ( + editUser()} + className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"} + relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`} + > + Use setting + + ) + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("deleted_at", data.deleted_at); + searchParams.set("email", data.email); + setSearchParams(searchParams); + localStorage.setItem("admin_recycle_filter", searchParams.toString()); + + getData(data); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "recycle_bin_properties_spaces`", + }, + }); + getData(); + }, []); + + return ( + <> +
    +
    +
    +

    Recycle Bin (Property Spaces)

    +
    +
    +
    + + +

    {errors.id?.message}

    +
    + +
    + + +

    {errors.email?.message}

    +
    + +
    + + +

    {errors.deleted_at?.message}

    +
    +
    + +
    +
    + +
    + +
    + + {bulkMode && ( +
    + + {bulkSelected.length > 0 ? ( +
    + {" "} + + +
    + ) : null} +
    + )} + +
    +
    + {loading ? ( +
    Loading...
    + ) : ( + + + + {bulkMode && ( + + )} + {columns.map((column, index) => ( + + ))} + + + + {data + .sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at)) + .map((row, i) => { + return ( + + {bulkMode && ( + + )} + {columns.map((cell, index) => { + if (cell.format) { + return ( + + ); + } + if (cell.accessor == "") { + return ( + + ); + } + if (cell.mapping) { + return ( + + ); + } + + + return ( + + ); + })} + + ); + })} + +
    + {column.header} + {column.isSorted} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
    + { + if (bulkSelected.some((item) => item.id == row.id)) { + setBulkSelected((prev) => { + let copy = [...prev]; + copy.splice( + prev.findIndex((item) => item.id == row.id), + 1, + ); + return copy; + }); + } else { + setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]); + } + }} + checked={bulkSelected.some((item) => item.id == row.id)} + onChange={() => { }} + /> + + {cell.format(row[cell.accessor])} + + {(row.email) && +
    + +
    + } + {(!row.email) && + + } + +
    + {cell.mapping[row[cell.accessor]]} + + {row[cell.accessor]} +
    + )} +
    +
    + setSelectedRestore({})} + data={selectedRestore} + onSuccess={() => getData()} + /> + setRestoreAll(false)} + records={bulkSelected} + onSuccess={() => { + setBulkSelected([]); + getData(); + }} + /> + setSelectedDelete({})} + data={selectedDelete} + onSuccess={() => getData()} + table="property_spaces" + /> + + ); +} diff --git a/src/pages/Admin/RecycleBin/AdminRecycleBinSpaceFaq.jsx b/src/pages/Admin/RecycleBin/AdminRecycleBinSpaceFaq.jsx new file mode 100644 index 0000000..e17507c --- /dev/null +++ b/src/pages/Admin/RecycleBin/AdminRecycleBinSpaceFaq.jsx @@ -0,0 +1,415 @@ +import React, { useContext, useState } from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import Button from "@/components/Button"; +import SwitchBulkMode from "@/components/SwitchBulkMode"; +import moment from "moment"; +import TreeSDK from "@/utils/TreeSDK"; +import { ID_PREFIX } from "@/utils/constants"; +import RestoreModal from "./RestoreModal"; +import DeletePermanentlyModal from "./DeletePermanentlyModal"; +import RestoreAllModal from "./RestoreAllModal"; +import { Switch } from "@headlessui/react"; +import DeleteAllModal from "./DeleteAll"; + +let treeSdk = new TreeSDK() + +const columns = [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: true, + }, + + { + header: "Deleted At", + accessor: "deleted_at", + isSorted: true, + isSortedDesc: true, + format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"), + }, + { + header: "Actions", + accessor: "", + }, +]; + + + +export default function AdminRecycleBinPropertySpaceFaqs() { + const { dispatch } = React.useContext(AuthContext); + const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext); + const [bulkMode, setBulkMode] = React.useState(false); + const [bulkSelected, setBulkSelected] = React.useState([]); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? ""); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedRestore, setSelectedRestore] = useState({}); + const [selectedDelete, setSelectedDelete] = useState({}); + const [restoreAll, setRestoreAll] = useState(false); + const [deleteAll, setDeleteAll] = useState(false); + + let sdk = new MkdSDK(); + let tdk = new TreeSDK(); + + const { + reset, + register, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: parseSearchParams(searchParams), + }); + + async function getData() { + setLoading(true); + try { + + let filter = ["ergo_property_space_faq.deleted_at IS NOT NULL"]; + if (data?.id) { + filter.push(`ergo_property_space_faq.id = ${data?.id}`); + } + if (data?.deleted_at) { + filter[0] = (`DATE_FORMAT(ergo_property_space_faq.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`); + } + if (data?.email) { + filter.push(`ergo_user.email LIKE '${data?.email}'`); + } + const result = await sdk.callRawAPI("/v2/api/custom/ergo/property_space_faq/PAGINATE", + { + "where": filter, + "page": 1, + "limit": 10 + }, + "POST" + ) + setData(result.list); + + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + setLoading(false); + } + + function MyToggle(data) { + const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false) + const { dispatch: globalDispatch } = useContext(GlobalContext); + + let sdk = new MkdSDK(); + async function editUser() { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/soft-delete", { id: Number(data.user.id), entity: "user", type: "restore" }, "POST"); + if (!result.error) { + showToast(globalDispatch, result.message, 4000) + getData() + } + } + + + return ( + editUser()} + className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"} + relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`} + > + Use setting + + ) + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("entity_type", data.entity_type); + searchParams.set("deleted_at", data.deleted_at); + searchParams.set("email", data.email); + setSearchParams(searchParams); + localStorage.setItem("admin_recycle_filter", searchParams.toString()); + + getData(data); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "recycle_bin_properties_space_faq`", + }, + }); + getData(); + }, []); + + return ( + <> +
    +
    +
    +

    Recycle Bin (Space Faqs)

    +
    +
    +
    + + +

    {errors.id?.message}

    +
    + +
    + + +

    {errors.deleted_at?.message}

    +
    +
    + +
    +
    + +
    + +
    + + {bulkMode && ( +
    + + {bulkSelected.length > 0 ? ( +
    + {" "} + + +
    + ) : null} +
    + )} + +
    +
    + {loading ? ( +
    Loading...
    + ) : ( + + + + {bulkMode && ( + + )} + {columns.map((column, index) => ( + + ))} + + + + {data + .sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at)) + .map((row, i) => { + return ( + + {bulkMode && ( + + )} + {columns.map((cell, index) => { + if (cell.format) { + return ( + + ); + } + if (cell.accessor == "") { + return ( + + ); + } + if (cell.mapping) { + return ( + + ); + } + + + return ( + + ); + })} + + ); + })} + +
    + {column.header} + {column.isSorted} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
    + { + if (bulkSelected.some((item) => item.id == row.id)) { + setBulkSelected((prev) => { + let copy = [...prev]; + copy.splice( + prev.findIndex((item) => item.id == row.id), + 1, + ); + return copy; + }); + } else { + setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]); + } + }} + checked={bulkSelected.some((item) => item.id == row.id)} + onChange={() => { }} + /> + + {cell.format(row[cell.accessor])} + + {(row.email) && +
    + +
    + } + {(!row.email) && + + } + +
    + {cell.mapping[row[cell.accessor]]} + + {row[cell.accessor]} +
    + )} +
    +
    + setSelectedRestore({})} + data={selectedRestore} + onSuccess={() => getData()} + /> + setRestoreAll(false)} + records={bulkSelected} + onSuccess={() => { + setBulkSelected([]); + getData(); + }} + /> + setDeleteAll(false)} + records={bulkSelected} + table="property_space_faq" + onSuccess={() => { + setBulkSelected([]); + getData(); + }} + /> + setSelectedDelete({})} + data={selectedDelete} + onSuccess={() => getData()} + table="property_space_faq" + /> + + ); +} diff --git a/src/pages/Admin/RecycleBin/AdminRecycleBinUsers.jsx b/src/pages/Admin/RecycleBin/AdminRecycleBinUsers.jsx new file mode 100644 index 0000000..6767d88 --- /dev/null +++ b/src/pages/Admin/RecycleBin/AdminRecycleBinUsers.jsx @@ -0,0 +1,507 @@ +import React, { useContext, useState } from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import Button from "@/components/Button"; +import SwitchBulkMode from "@/components/SwitchBulkMode"; +import moment from "moment"; +import TreeSDK from "@/utils/TreeSDK"; +import { ID_PREFIX } from "@/utils/constants"; +import RestoreModal from "./RestoreModal"; +import DeletePermanentlyModal from "./DeletePermanentlyModal"; +import RestoreAllModal from "./RestoreAllModal"; +import { Switch } from "@headlessui/react"; +import DeleteAllModal from "./DeleteAll"; + +let treeSdk = new TreeSDK() + +const columns = [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: true, + }, + + { + header: "Email", + nested: "user", + accessor: "email", + isSorted: true, + isSortedDesc: true, + }, + { + header: "Deleted At", + accessor: "deleted_at", + isSorted: true, + isSortedDesc: true, + format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"), + }, + { + header: "Actions", + accessor: "", + }, +]; + + + +export default function AdminRecycleBinUsers() { + const { dispatch } = React.useContext(AuthContext); + const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext); + const [bulkMode, setBulkMode] = React.useState(false); + const [bulkSelected, setBulkSelected] = React.useState([]); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? ""); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedRestore, setSelectedRestore] = useState({}); + const [selectedDelete, setSelectedDelete] = useState({}); + const [restoreAll, setRestoreAll] = useState(false); + const [users, setUsers] = useState([]); + const [add, setAddons] = useState([]); + const [adminEmail, setAdminEmail] = useState(); + const [properties, setProperties] = useState(); + const [deleteAll, setDeleteAll] = useState(false); + + + let sdk = new MkdSDK(); + let tdk = new TreeSDK(); + + const { + reset, + register, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: parseSearchParams(searchParams), + }); + + function getUserDetail(id) { + if (id !== undefined) { + const result = users.find((user) => user.id === Number(id)) + + if (result?.email !== null) { + return result?.email + } + } + } + + function getSpaceHost(id) { + if (id !== undefined) { + const result = properties?.find((property) => property.id === Number(id)) + const result2 = users?.find((user) => user.id == Number(result?.host_id)) + if (result2?.email !== null) { + return result2?.email + } + } + } + function getAddonOwner(id) { + if (id !== undefined) { + const addOn = add.find((a) => a.id == Number(id)) + console.log(addOn?.space_id) + // const result = properties?.find((property) => property.space_id == 18) + // console.log(result) + // const result2 = users?.find((user) => user.id == Number(result?.host_id)) + if (addOn?.space_id === null) { + console.log(adminEmail) + return adminEmail + } + else { + return "N/A" + } + } + } + + async function getUser() { + let filter = []; + const getEmail = await sdk.getProfile(); + setAdminEmail(getEmail.email); + + const result = await tdk.getList("user", { filter, join: [] }) + if (!result?.error) { + setUsers(result?.list) + } + } + async function getAddons() { + let filter = []; + const result = await tdk.getList("add_on", { filter, join: [] }) + if (!result?.error) { + setAddons(result?.list) + } + } + async function getProperties() { + await sdk.setTable("property") + const result = await sdk.callRestAPI({}, "GETALL") + if (!result?.error) { + setProperties(result?.list) + } + } + + async function getData(data) { + setLoading(true); + try { + + let filter = ["ergo_user.deleted_at IS NOT NULL"]; + if (data?.id) { + filter.push(`ergo_user.id = ${data?.id}`); + } + if (data?.deleted_at) { + filter[0] = (`DATE_FORMAT(ergo_user.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`); + } + if (data?.email) { + filter.push(`ergo_user.email LIKE '${data?.email}'`); + } + const result = await sdk.callRawAPI("/v2/api/custom/ergo/user/PAGINATE", + { + "where": filter, + "page": 1, + "limit": 10 + }, + "POST" + ) + setData(result.list); + + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + setLoading(false); + } + + function MyToggle(data) { + const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false) + const { dispatch: globalDispatch } = useContext(GlobalContext); + + let sdk = new MkdSDK(); + async function editUser() { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/soft-delete", { id: Number(data.user.id), entity: "user", type: "restore" }, "POST"); + if (!result.error) { + showToast(globalDispatch, result.message, 4000) + getData() + } + } + + + return ( + editUser()} + className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"} + relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`} + > + Use setting + + ) + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("deleted_at", data.deleted_at); + searchParams.set("email", data.email); + setSearchParams(searchParams); + localStorage.setItem("admin_recycle_filter", searchParams.toString()); + + getData(data); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "recycle_bin_users", + }, + }); + getData(); + getUser(); + getProperties(); + getAddons(); + }, []); + + return ( + <> +
    +
    +
    +

    Recycle Bin

    +
    +
    +
    + + +

    {errors.id?.message}

    +
    + +
    + + +

    {errors.email?.message}

    +
    + +
    + + +

    {errors.deleted_at?.message}

    +
    +
    + +
    +
    + +
    + +
    + + {bulkMode && ( +
    + + {bulkSelected.length > 0 ? ( +
    + {" "} + + +
    + ) : null} +
    + )} + +
    +
    + {loading ? ( +
    Loading...
    + ) : ( + + + + {bulkMode && ( + + )} + {columns.map((column, index) => ( + + ))} + + + + {data + .sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at)) + .map((row, i) => { + return ( + + {bulkMode && ( + + )} + {columns.map((cell, index) => { + if (cell.format) { + return ( + + ); + } + if (cell.accessor == "") { + return ( + + ); + } + + if (cell.mapping) { + return ( + + ); + } + + + return ( + + ); + })} + + ); + })} + +
    + {column.header} + {column.isSorted} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
    + { + if (bulkSelected.some((item) => item.id == row.id)) { + setBulkSelected((prev) => { + let copy = [...prev]; + copy.splice( + prev.findIndex((item) => item.id == row.id), + 1, + ); + return copy; + }); + } else { + setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]); + } + }} + checked={bulkSelected.some((item) => item.id == row.id)} + onChange={() => { }} + /> + + {cell.format(row[cell.accessor])} + + {(row.email) && +
    + + Restore +
    + } + {(!row.email) && + + } + +
    + {cell.mapping[row[cell.accessor]]} + + {row[cell.accessor]} +
    + )} +
    +
    + setSelectedRestore({})} + data={selectedRestore} + onSuccess={() => getData()} + /> + setRestoreAll(false)} + records={bulkSelected} + table="user" + onSuccess={() => { + setBulkSelected([]); + getData(); + }} + /> + setSelectedDelete({})} + data={selectedDelete} + onSuccess={() => getData()} + table="user" + /> + setDeleteAll(false)} + records={bulkSelected} + table="user" + onSuccess={() => { + setBulkSelected([]); + getData(); + }} + /> + + ); +} diff --git a/src/pages/Admin/RecycleBin/AdminRecycleHashtags.jsx b/src/pages/Admin/RecycleBin/AdminRecycleHashtags.jsx new file mode 100644 index 0000000..88dc067 --- /dev/null +++ b/src/pages/Admin/RecycleBin/AdminRecycleHashtags.jsx @@ -0,0 +1,416 @@ +import React, { useContext, useState } from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import Button from "@/components/Button"; +import SwitchBulkMode from "@/components/SwitchBulkMode"; +import moment from "moment"; +import TreeSDK from "@/utils/TreeSDK"; +import { ID_PREFIX } from "@/utils/constants"; +import RestoreModal from "./RestoreModal"; +import DeletePermanentlyModal from "./DeletePermanentlyModal"; +import RestoreAllModal from "./RestoreAllModal"; +import { Switch } from "@headlessui/react"; +import DeleteAllModal from "./DeleteAll"; + +let treeSdk = new TreeSDK() + +const columns = [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: true, + }, + + { + header: "Deleted At", + accessor: "deleted_at", + isSorted: true, + isSortedDesc: true, + format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"), + }, + { + header: "Actions", + accessor: "", + }, +]; + + + +export default function AdminRecycleBinHashtags() { + const { dispatch } = React.useContext(AuthContext); + const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext); + const [bulkMode, setBulkMode] = React.useState(false); + const [bulkSelected, setBulkSelected] = React.useState([]); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? ""); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedRestore, setSelectedRestore] = useState({}); + const [selectedDelete, setSelectedDelete] = useState({}); + const [restoreAll, setRestoreAll] = useState(false); + const [deleteAll, setDeleteAll] = useState(false); + + + let sdk = new MkdSDK(); + let tdk = new TreeSDK(); + + const { + reset, + register, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: parseSearchParams(searchParams), + }); + + async function getData(data) { + setLoading(true); + try { + + let filter = ["ergo_hashtag.deleted_at IS NOT NULL"]; + if (data?.id) { + filter.push(`ergo_hashtag.id = ${data?.id}`); + } + if (data?.deleted_at) { + filter[0] = (`ergo_hashtag.deleted_at = ${data?.deleted_at}`); + } + if (data?.email) { + filter[0] = (`DATE_FORMAT(ergo_hashtag.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`); + } + const result = await sdk.callRawAPI("/v2/api/custom/ergo/hashtag/PAGINATE", + { + "where": filter, + "page": 1, + "limit": 10 + }, + "POST" + ) + setData(result.list); + + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + setLoading(false); + } + + function MyToggle(data) { + const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false) + const { dispatch: globalDispatch } = useContext(GlobalContext); + + let sdk = new MkdSDK(); + async function editUser() { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/soft-delete", { id: Number(data.user.id), entity: "user", type: "restore" }, "POST"); + if (!result.error) { + showToast(globalDispatch, result.message, 4000) + getData() + } + } + + + return ( + editUser()} + className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"} + relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`} + > + Use setting + + ) + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("entity_type", data.entity_type); + searchParams.set("deleted_at", data.deleted_at); + searchParams.set("email", data.email); + setSearchParams(searchParams); + localStorage.setItem("admin_recycle_filter", searchParams.toString()); + + getData(data); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "recycle_bin_hashtag`", + }, + }); + getData(); + }, []); + + return ( + <> +
    +
    +
    +

    Recycle Bin (Hashtags)

    +
    +
    +
    + + +

    {errors.id?.message}

    +
    + +
    + + +

    {errors.deleted_at?.message}

    +
    +
    + +
    +
    + +
    + +
    + + {bulkMode && ( +
    + + {bulkSelected.length > 0 ? ( +
    + {" "} + + +
    + ) : null} +
    + )} + +
    +
    + {loading ? ( +
    Loading...
    + ) : ( + + + + {bulkMode && ( + + )} + {columns.map((column, index) => ( + + ))} + + + + {data + .sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at)) + .map((row, i) => { + return ( + + {bulkMode && ( + + )} + {columns.map((cell, index) => { + if (cell.format) { + return ( + + ); + } + if (cell.accessor == "") { + return ( + + ); + } + if (cell.mapping) { + return ( + + ); + } + + + return ( + + ); + })} + + ); + })} + +
    + {column.header} + {column.isSorted} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
    + { + if (bulkSelected.some((item) => item.id == row.id)) { + setBulkSelected((prev) => { + let copy = [...prev]; + copy.splice( + prev.findIndex((item) => item.id == row.id), + 1, + ); + return copy; + }); + } else { + setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]); + } + }} + checked={bulkSelected.some((item) => item.id == row.id)} + onChange={() => { }} + /> + + {cell.format(row[cell.accessor])} + + {(row.email) && +
    + +
    + } + {(!row.email) && + + } + +
    + {cell.mapping[row[cell.accessor]]} + + {row[cell.accessor]} +
    + )} +
    +
    + setSelectedRestore({})} + data={selectedRestore} + onSuccess={() => getData()} + /> + setRestoreAll(false)} + records={bulkSelected} + onSuccess={() => { + setBulkSelected([]); + getData(); + }} + /> + setDeleteAll(false)} + records={bulkSelected} + table="hashtag" + onSuccess={() => { + setBulkSelected([]); + getData(); + }} + /> + setSelectedDelete({})} + data={selectedDelete} + onSuccess={() => getData()} + table="hashtag" + /> + + ); +} diff --git a/src/pages/Admin/RecycleBin/AdminRecycleSpaceImages.jsx b/src/pages/Admin/RecycleBin/AdminRecycleSpaceImages.jsx new file mode 100644 index 0000000..d764c92 --- /dev/null +++ b/src/pages/Admin/RecycleBin/AdminRecycleSpaceImages.jsx @@ -0,0 +1,453 @@ +import React, { useContext, useState } from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import Button from "@/components/Button"; +import SwitchBulkMode from "@/components/SwitchBulkMode"; +import moment from "moment"; +import TreeSDK from "@/utils/TreeSDK"; +import { ID_PREFIX } from "@/utils/constants"; +import RestoreModal from "./RestoreModal"; +import DeletePermanentlyModal from "./DeletePermanentlyModal"; +import RestoreAllModal from "./RestoreAllModal"; +import { Switch } from "@headlessui/react"; +import DeleteAllModal from "./DeleteAll"; + +let treeSdk = new TreeSDK() + +const columns = [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: true, + }, + + { + header: "Email", + nested: "user", + accessor: "email", + isSorted: true, + isSortedDesc: true, + }, + { + header: "Image", + nested: "image", + accessor: "default_image", + isSorted: true, + isSortedDesc: true, + }, + { + header: "Deleted At", + accessor: "deleted_at", + isSorted: true, + isSortedDesc: true, + format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"), + }, + { + header: "Actions", + accessor: "", + }, +]; + + + +export default function AdminRecycleBinSpaceImages() { + const { dispatch } = React.useContext(AuthContext); + const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext); + const [bulkMode, setBulkMode] = React.useState(false); + const [bulkSelected, setBulkSelected] = React.useState([]); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? ""); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedRestore, setSelectedRestore] = useState({}); + const [selectedDelete, setSelectedDelete] = useState({}); + const [restoreAll, setRestoreAll] = useState(false); + const [deleteAll, setDeleteAll] = useState(false); + + let sdk = new MkdSDK(); + let tdk = new TreeSDK(); + + const { + reset, + register, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: parseSearchParams(searchParams), + }); + + + async function getData(data) { + setLoading(true); + try { + + let filter = ["ergo_property_spaces_images.deleted_at IS NOT NULL"]; + if (data?.id) { + filter.push(`ergo_property_spaces_images.id = ${data?.id}`); + } + if (data?.deleted_at) { + filter[0] = (`DATE_FORMAT(ergo_property_spaces_images.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`); + } + if (data?.email) { + filter.push(`ergo_user.email LIKE '${data?.email}'`); + } + const result = await sdk.callRawAPI("/v2/api/custom/ergo/property-space-images/PAGINATE", + { + "where": filter, + "page": 1, + "limit": 10 + }, + "POST" + ) + setData(result.list); + + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + setLoading(false); + } + + function MyToggle(data) { + const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false) + const { dispatch: globalDispatch } = useContext(GlobalContext); + + let sdk = new MkdSDK(); + async function editUser() { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/property-space-images", { id: Number(data.id), deleted_at: NULL }, "PUT"); + if (!result.error) { + showToast(globalDispatch, result.message, 4000) + getData() + } + } + + + return ( + editUser()} + className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"} + relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`} + > + Use setting + + ) + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("deleted_at", data.deleted_at); + searchParams.set("email", data.email); + setSearchParams(searchParams); + localStorage.setItem("admin_recycle_filter", searchParams.toString()); + + getData(data); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "recycle_bin_properties_space_images", + }, + }); + getData(); + }, []); + + return ( + <> +
    +
    +
    +

    Recycle Bin (Property Space Images)

    +
    +
    +
    + + +

    {errors.id?.message}

    +
    + +
    + + +

    {errors.email?.message}

    +
    + +
    + + +

    {errors.deleted_at?.message}

    +
    +
    + +
    +
    + +
    + +
    + + {bulkMode && ( +
    + + {bulkSelected.length > 0 ? ( +
    + {" "} + + +
    + ) : null} +
    + )} + +
    +
    + {loading ? ( +
    Loading...
    + ) : ( + + + + {bulkMode && ( + + )} + {columns.map((column, index) => ( + + ))} + + + + {data + .sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at)) + .map((row, i) => { + return ( + + {bulkMode && ( + + )} + {columns.map((cell, index) => { + if (cell.format) { + return ( + + ); + } + if (cell.accessor == "") { + return ( + + ); + } + if (cell.mapping) { + return ( + + ); + } + if (cell.nested === "image") { + return ( + + ); + } + + + return ( + + ); + })} + + ); + })} + +
    + {column.header} + {column.isSorted} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
    + { + if (bulkSelected.some((item) => item.id == row.id)) { + setBulkSelected((prev) => { + let copy = [...prev]; + copy.splice( + prev.findIndex((item) => item.id == row.id), + 1, + ); + return copy; + }); + } else { + setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]); + } + }} + checked={bulkSelected.some((item) => item.id == row.id)} + onChange={() => { }} + /> + + {cell.format(row[cell.accessor])} + + {(row.email) && +
    + +
    + } + {(!row.email) && + + } + +
    + {cell.mapping[row[cell.accessor]]} + + space_image + + {row[cell.accessor]} +
    + )} +
    +
    + setSelectedRestore({})} + data={selectedRestore} + onSuccess={() => getData()} + /> + setRestoreAll(false)} + records={bulkSelected} + onSuccess={() => { + setBulkSelected([]); + getData(); + }} + /> + setDeleteAll(false)} + records={bulkSelected} + table="property_spaces_images" + onSuccess={() => { + setBulkSelected([]); + getData(); + }} + /> + setSelectedDelete({})} + data={selectedDelete} + onSuccess={() => getData()} + table="property_spaces_images" + /> + + ); +} diff --git a/src/pages/Admin/RecycleBin/AdminRecycleSpaces.jsx b/src/pages/Admin/RecycleBin/AdminRecycleSpaces.jsx new file mode 100644 index 0000000..308bde1 --- /dev/null +++ b/src/pages/Admin/RecycleBin/AdminRecycleSpaces.jsx @@ -0,0 +1,411 @@ +import React, { useContext, useState } from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import Button from "@/components/Button"; +import SwitchBulkMode from "@/components/SwitchBulkMode"; +import moment from "moment"; +import TreeSDK from "@/utils/TreeSDK"; +import { ID_PREFIX } from "@/utils/constants"; +import RestoreModal from "./RestoreModal"; +import DeletePermanentlyModal from "./DeletePermanentlyModal"; +import RestoreAllModal from "./RestoreAllModal"; +import { Switch } from "@headlessui/react"; +import DeleteAllModal from "./DeleteAll"; + +let treeSdk = new TreeSDK() + +const columns = [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: true, + }, + + { + header: "Deleted At", + accessor: "deleted_at", + isSorted: true, + isSortedDesc: true, + format: (raw) => moment(raw).format("MM/DD/yyyy hh:mm:ss A"), + }, + { + header: "Actions", + accessor: "", + }, +]; + + + +export default function AdminRecycleBinSpaces() { + const { dispatch } = React.useContext(AuthContext); + const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext); + const [bulkMode, setBulkMode] = React.useState(false); + const [bulkSelected, setBulkSelected] = React.useState([]); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_recycle_filter") ?? ""); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedRestore, setSelectedRestore] = useState({}); + const [selectedDelete, setSelectedDelete] = useState({}); + const [restoreAll, setRestoreAll] = useState(false); + const [deleteAll, setDeleteAll] = useState(false); + + let sdk = new MkdSDK(); + let tdk = new TreeSDK(); + + const { + reset, + register, + handleSubmit, + formState: { errors }, + } = useForm({ + defaultValues: parseSearchParams(searchParams), + }); + + async function getData(data) { + setLoading(true); + try { + + let filter = ["ergo_spaces.deleted_at IS NOT NULL"]; + if (data?.id) { + filter.push(`ergo_spaces.id = ${data?.id}`); + } + if (data?.deleted_at) { + filter[0] = (`DATE_FORMAT(ergo_spaces.deleted_at, '%Y-%m-%d')= '${data?.deleted_at}'`); + } + const result = await sdk.callRawAPI("/v2/api/custom/ergo/spaces/PAGINATE", + { + "where": filter, + "page": 1, + "limit": 10 + }, + "POST" + ) + setData(result.list); + + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + setLoading(false); + } + + function MyToggle(data) { + const [enabled, setEnabled] = useState(data.user.status === 1 ? true : false) + const { dispatch: globalDispatch } = useContext(GlobalContext); + + let sdk = new MkdSDK(); + async function editUser() { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/spaces", { id: Number(data.id), deleted_at: NULL }, "PUT"); + if (!result.error) { + showToast(globalDispatch, result.message, 4000) + getData() + } + } + + + return ( + editUser()} + className={`${enabled ? "!bg-gradient-to-r from-primary-dark to-primary-dark" : "bg-gray-300"} + relative inline-flex h-[28px] w-[55px] shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75`} + > + Use setting + + ) + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("deleted_at", data.deleted_at); + searchParams.set("email", data.email); + setSearchParams(searchParams); + localStorage.setItem("admin_recycle_filter", searchParams.toString()); + + getData(data); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "recycle_bin_spaces`", + }, + }); + getData(); + }, []); + + return ( + <> +
    +
    +
    +

    Recycle Bin (Spaces)

    +
    +
    +
    + + +

    {errors.id?.message}

    +
    + +
    + + +

    {errors.deleted_at?.message}

    +
    +
    + +
    +
    + +
    + +
    + + {bulkMode && ( +
    + + {bulkSelected.length > 0 ? ( +
    + {" "} + + +
    + ) : null} +
    + )} + +
    +
    + {loading ? ( +
    Loading...
    + ) : ( + + + + {bulkMode && ( + + )} + {columns.map((column, index) => ( + + ))} + + + + {data + .sort((a, b) => new Date(b.deleted_at) - new Date(a.deleted_at)) + .map((row, i) => { + return ( + + {bulkMode && ( + + )} + {columns.map((cell, index) => { + if (cell.format) { + return ( + + ); + } + if (cell.accessor == "") { + return ( + + ); + } + if (cell.mapping) { + return ( + + ); + } + + + return ( + + ); + })} + + ); + })} + +
    + {column.header} + {column.isSorted} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
    + { + if (bulkSelected.some((item) => item.id == row.id)) { + setBulkSelected((prev) => { + let copy = [...prev]; + copy.splice( + prev.findIndex((item) => item.id == row.id), + 1, + ); + return copy; + }); + } else { + setBulkSelected((prev) => [...prev, { id: row.id, table: row.entity_type }]); + } + }} + checked={bulkSelected.some((item) => item.id == row.id)} + onChange={() => { }} + /> + + {cell.format(row[cell.accessor])} + + {(row.email) && +
    + + Restore +
    + } + {(!row.email) && + + } + +
    + {cell.mapping[row[cell.accessor]]} + + {row[cell.accessor]} +
    + )} +
    +
    + setSelectedRestore({})} + data={selectedRestore} + onSuccess={() => getData()} + /> + setRestoreAll(false)} + records={bulkSelected} + onSuccess={() => { + setBulkSelected([]); + getData(); + }} + /> + setDeleteAll(false)} + records={bulkSelected} + table="spaces" + onSuccess={() => { + setBulkSelected([]); + getData(); + }} + /> + setSelectedDelete({})} + data={selectedDelete} + onSuccess={() => getData()} + /> + + ); +} diff --git a/src/pages/Admin/RecycleBin/DeleteAll.jsx b/src/pages/Admin/RecycleBin/DeleteAll.jsx new file mode 100644 index 0000000..6c1f1c5 --- /dev/null +++ b/src/pages/Admin/RecycleBin/DeleteAll.jsx @@ -0,0 +1,109 @@ +import React from "react"; +import { GlobalContext, showToast } from "@/globalContext"; +import { AuthContext } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; + +let sdk = new MkdSDK(); + +export default function DeleteAllModal({ modalOpen, closeModal, onSuccess, records, table }) { + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const { dispatch } = React.useContext(AuthContext); + async function bulkRestore() { + try { + if (table === "user") { + for (let index = 0; index < records.length; index++) { + const data = records[index]; + var where = [`ergo_booking.host_id = ${data.id} OR ergo_booking.customer_id = ${data.id}`]; + const result = await sdk.callRawAPI("/v2/api/custom/ergo/booking/PAGINATE", { page: 1 ?? 1, limit: 999, where, sortId: "update_at", direction: "DESC" }, "POST"); + const isActive = result?.list.find((booking) => (booking.status === 2 || booking.status === 1 || booking.status === 0)) + + if (isActive === undefined) { + sdk.setTable("user"); + await sdk.callRestAPI({ id: data.id }, "DELETE"); + closeModal(); + onSuccess(); + } else { + showToast(globalDispatch, "These other user(s) have an active booking", 4000, "ERROR"); + closeModal(); + } + } + } + if (table === "property_spaces") { + for (let index = 0; index < records.length; index++) { + const data = records[index]; + var where = [`ergo_booking.property_space_id = ${data.space_id} AND ergo_booking.deleted_at IS NULL`]; + const result = await sdk.callRawAPI("/v2/api/custom/ergo/booking/PAGINATE", { page: 1 ?? 1, limit: 999, where, sortId: "update_at", direction: "DESC" }, "POST"); + const isActive = result?.list.find((booking) => (booking.status === 2 || booking.status === 1)) + if (isActive === undefined) { + sdk.setTable("property_spaces"); + await sdk.callRestAPI({ id: data.id }, "DELETE"); + closeModal(); + onSuccess(); + } else { + showToast(globalDispatch, "One of the Properties has active bookings", 4000, "ERROR"); + closeModal(); + } + } + } + else { + await Promise.all( + records.map((entity) => { + sdk.setTable(table); + return sdk.callRestAPI({ id: Number(entity.id), deleted_at: null }, "DELETE"); + }), + ); + showToast(globalDispatch, "Deleted Successful"); + onSuccess(); + closeModal(); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + + return ( + <> + {modalOpen ? ( + <> +
    +
    +
    +
    +

    Are you sure?

    + +
    +
    +

    Are you sure you want to delete {records.length} records

    +
    +
    + + +
    +
    +
    +
    +
    + + ) : null} + + ); +} diff --git a/src/pages/Admin/RecycleBin/DeletePermanentlyModal.jsx b/src/pages/Admin/RecycleBin/DeletePermanentlyModal.jsx new file mode 100644 index 0000000..aed054b --- /dev/null +++ b/src/pages/Admin/RecycleBin/DeletePermanentlyModal.jsx @@ -0,0 +1,107 @@ +import React from "react"; +import { GlobalContext, showToast } from "@/globalContext"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; + +let sdk = new MkdSDK(); + +export default function DeletePermanentlyModal({ modalOpen, closeModal, onSuccess, data, table }) { + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const { dispatch } = React.useContext(AuthContext); + + async function deleteRecord() { + try { + if (table === "user") { + var where = [`ergo_booking.host_id = ${data.id} OR ergo_booking.customer_id = ${data.id}`]; + const result = await sdk.callRawAPI("/v2/api/custom/ergo/booking/PAGINATE", { page: 1 ?? 1, limit: 999, where, sortId: "update_at", direction: "DESC" }, "POST"); + const isActive = result?.list.find((booking) => (booking.status === 2 || booking.status === 1 || booking.status === 0)) + + if (isActive === undefined) { + sdk.setTable("user"); + await sdk.callRestAPI({ id: data.id }, "DELETE"); + showToast(globalDispatch, "User deleted successfully"); + closeModal(); + onSuccess(); + } else { + showToast(globalDispatch, "User has active bookings", 4000, "ERROR"); + closeModal(); + } + } + else if (table === "property_spaces") { + var where = [`ergo_booking.property_space_id = ${data.space_id}`]; + const result = await sdk.callRawAPI("/v2/api/custom/ergo/booking/PAGINATE", { page: 1 ?? 1, limit: 999, where, sortId: "update_at", direction: "DESC" }, "POST"); + const isActive = result?.list.find((booking) => (booking.status === 2 || booking.status === 1 || booking.status === 0)) + + if (isActive === undefined) { + sdk.setTable("property_spaces"); + await sdk.callRestAPI({ id: data.id }, "DELETE"); + showToast(globalDispatch, "Property Space deleted successfully"); + closeModal(); + onSuccess(); + } else { + showToast(globalDispatch, "Property Space has active bookings", 4000, "ERROR"); + closeModal(); + } + } + + else { + // else if (data.email === undefined) { + sdk.setTable(table); + await sdk.callRestAPI({ id: data.id }, "DELETE"); + showToast(globalDispatch, "Record deleted successfully"); + closeModal(); + onSuccess(); + // } + } + + + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 5000, "ERROR"); + } + } + + return ( + <> + {modalOpen ? ( + <> +
    +
    +
    +
    +

    Warning!

    + +
    +
    +

    Are you sure you want to delete this record permanently?. This action is not reversible

    +
    +
    + + +
    +
    +
    +
    +
    + + ) : null} + + ); +} diff --git a/src/pages/Admin/RecycleBin/RestoreAllModal.jsx b/src/pages/Admin/RecycleBin/RestoreAllModal.jsx new file mode 100644 index 0000000..ce0ae67 --- /dev/null +++ b/src/pages/Admin/RecycleBin/RestoreAllModal.jsx @@ -0,0 +1,72 @@ +import React from "react"; +import { GlobalContext, showToast } from "@/globalContext"; +import { AuthContext } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; + +let sdk = new MkdSDK(); + +export default function RestoreAllModal({ modalOpen, closeModal, onSuccess, records, table }) { + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const { dispatch } = React.useContext(AuthContext); + async function bulkRestore() { + try { + await Promise.all( + records.map((entity) => { + sdk.setTable(table); + return sdk.callRestAPI({ id: Number(entity.id), deleted_at: null }, "PUT"); + }), + ); + + showToast(globalDispatch, "Restore Successful"); + onSuccess(); + closeModal(); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + return ( + <> + {modalOpen ? ( + <> +
    +
    +
    +
    +

    Are you sure?

    + +
    +
    +

    Are you sure you want to restore {records.length} records

    +
    +
    + + +
    +
    +
    +
    +
    + + ) : null} + + ); +} diff --git a/src/pages/Admin/RecycleBin/RestoreModal.jsx b/src/pages/Admin/RecycleBin/RestoreModal.jsx new file mode 100644 index 0000000..0ed1a7b --- /dev/null +++ b/src/pages/Admin/RecycleBin/RestoreModal.jsx @@ -0,0 +1,68 @@ +import React from "react"; +import { GlobalContext, showToast } from "@/globalContext"; +import { AuthContext } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; + +let sdk = new MkdSDK(); + +export default function RestoreModal({ modalOpen, closeModal, onSuccess, data }) { + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const { dispatch } = React.useContext(AuthContext); + + async function restoreRecord() { + try { + sdk.setTable(data.entity_type); + await sdk.callRestAPI({ id: data.id, deleted_at: null }, "PUT"); + showToast(globalDispatch, "Restored successfully"); + onSuccess(); + closeModal(); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 5000, "ERROR"); + } + } + + return ( + <> + {modalOpen ? ( + <> +
    +
    +
    +
    +

    Are you sure?

    + +
    +
    +

    Are you sure you want to restore this record?

    +
    +
    + + +
    +
    +
    +
    +
    + + ) : null} + + ); +} diff --git a/src/pages/Admin/Review/AddAdminReviewPage.jsx b/src/pages/Admin/Review/AddAdminReviewPage.jsx new file mode 100644 index 0000000..7848802 --- /dev/null +++ b/src/pages/Admin/Review/AddAdminReviewPage.jsx @@ -0,0 +1,568 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate } from "react-router-dom"; +import { tokenExpireError, AuthContext } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import AddAdminPageLayout from "@/layouts/AddAdminPageLayout"; +import SmartSearch from "@/components/SmartSearch"; + +const AddAdminReviewPage = () => { + let sdk = new MkdSDK(); + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + + const [selectedSpace, setSelectedSpace] = React.useState(); + const [propertySpaces, setPropertySpaces] = React.useState([]); + + const [selectedHost, setSelectedHost] = React.useState({}); + const [hosts, setHosts] = React.useState([]); + + const [selectedCustomer, setSelectedCustomer] = React.useState({}); + const [customers, setCustomers] = React.useState([]); + + const [selectedHashtag, setSelectedHashtag] = React.useState([]); + const [hashtags, setHashtags] = React.useState([]); + + const schema = yup + .object({ + booking_id: yup.number().required("Booking ID is required").positive().integer().typeError("Booking ID must be a number"), + comment: yup.string().required("Comment is required"), + }) + .required(); + const userType = [ + { + key: "customer", + value: "customer", + }, + { + key: "host", + value: "host", + }, + ]; + const [selectedUserType, setSelectedUserType] = React.useState(userType[1].value); + + const ratings = [ + { key: "1", value: "1" }, + { + key: "2", + value: "2", + }, + { key: "3", value: "3" }, + { key: "4", value: "4" }, + { key: "5", value: "5" }, + ]; + + const navigate = useNavigate(); + const { + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const checkRating = (data) => { + if (selectedUserType === "customer") { + if (isNaN(data.host_rating)) { + return setError("host_rating", { + type: "manual", + message: "Host rating is required", + }); + } + if (isNaN(data.space_rating)) { + return setError("space_rating", { + type: "manual", + message: "Space rating is required", + }); + } + } else { + if (isNaN(data.customer_rating)) { + return setError("customer_rating", { + type: "manual", + message: "Customer rating is required", + }); + } + } + confirmBookingId(data); + }; + + const confirmBookingId = async (data) => { + try { + sdk.setTable("booking"); + const result = await sdk.callRestAPI( + { + id: data.booking_id, + }, + "GET", + ); + if (!result.error && result?.model) { + onSubmit(data); + } else { + setError("booking_id", { + type: "manual", + message: "Booking with this ID doesn't exist", + }); + } + } catch (error) { + console.log("Error", error); + setError("booking_id", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + async function getCustomerData(pageNum, limitNum, data) { + try { + sdk.setTable("user"); + const payload = { email: data.email || undefined, role: "customer" }; + const result = await sdk.callRestAPI( + { + payload, + page: pageNum, + limit: limitNum, + }, + "PAGINATE", + ); + const { list } = result; + setCustomers(list); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + + async function getHostData(pageNum, limitNum, data) { + try { + sdk.setTable("user"); + const payload = { email: data.email || undefined, role: "host" }; + const result = await sdk.callRestAPI( + { + payload, + page: pageNum, + limit: limitNum, + }, + "PAGINATE", + ); + const { list } = result; + setHosts(list); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + + async function getPropertySpaceData(pageNum, limit, data) { + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/property-spaces/PAGINATE", + { + where: [data?.property_name ? `ergo_property.name LIKE '%${data.property_name}%' OR ergo_spaces.category LIKE '%${data.property_name}%'` : 1], + page: pageNum, + limit: limit, + }, + "POST", + ); + const { list } = result; + setPropertySpaces(list); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + + async function getHashTags(pageNum, limit, data) { + try { + sdk.setTable("hashtag"); + const payload = { name: data.name || undefined }; + const result = await sdk.callRestAPI( + { + payload, + page: pageNum, + limit: limit, + }, + "PAGINATE", + ); + const { list } = result; + setHashtags(list); + } catch (error) { + console.log("ERROR", error); + tokenExpireError(dispatch, error.message); + } + } + + const addHashTagToReview = async (reviewId, selected) => { + try { + sdk.setTable("review_hashtag"); + const hashtags = selected.map((hashtag) => + sdk.callRestAPI( + { + hashtag_id: hashtag.id, + review_id: reviewId, + }, + "POST", + ), + ); + await Promise.all(hashtags); + } catch (error) { + console.log("Error", error); + tokenExpireError(dispatch, error.message); + } + }; + + const onSubmit = async (data) => { + if (selectedCustomer?.id && selectedHost?.id && selectedSpace?.id) { + let postDate = new Date(); + let review = { + customer_id: selectedCustomer.id, + host_id: selectedHost.id, + property_spaces_id: selectedSpace.id, + booking_id: data.booking_id, + comment: data.comment, + customer_rating: null, + host_rating: null, + space_rating: null, + post_date: postDate.toISOString(), + status: 0, + }; + + if (selectedUserType === "host") { + review.customer_rating = data.customer_rating || 0; + review.given_by = "host"; + review.received_by = "customer"; + } else { + review.host_rating = data.host_rating || 0; + review.space_rating = data.space_rating || 0; + review.given_by = "customer"; + review.received_by = "host"; + } + try { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/review/POST", { ...review }, "POST"); + if (!result.error) { + if (selectedHashtag.length > 0) { + await addHashTagToReview(result.message, selectedHashtag); + } + showToast(globalDispatch, "Added"); + navigate("/admin/review"); + } + } catch (error) { + console.log("Error", error); + showToast(globalDispatch, error.message); + tokenExpireError(dispatch, error.message); + } + } else { + if (!selectedCustomer?.id) { + setError("customer_email", { + type: "manual", + message: "Please select a customer", + }); + } + if (!selectedHost?.id) { + setError("host_email", { + type: "manual", + message: "Please select a host", + }); + } + if (!selectedSpace?.id) { + setError("property_spaces_id", { + type: "manual", + message: "Please select a Property space", + }); + } + } + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "review", + }, + }); + getHashTags(); + }, []); + + const onError = () => { + if (!selectedCustomer?.id) { + setError("customer_email", { + type: "manual", + message: "Please select a customer", + }); + } + if (!selectedHost?.id) { + setError("host_email", { + type: "manual", + message: "Please select a host", + }); + } + if (!selectedSpace?.id) { + setError("property_spaces_id", { + type: "manual", + message: "Please select a Property space", + }); + } + }; + + return ( + +
    + + +
    +
    +
    + + +

    {errors.customer_email?.message}

    +
    + +
    + + +

    {errors.host_email?.message}

    +
    +
    + + +

    {errors.property_spaces_id?.message}

    +
    + +
    + + +

    {errors.booking_id?.message}

    +
    + + {selectedUserType === "customer" ? ( + <> +
    + + +

    {errors.host_rating?.message}

    +
    +
    + + +

    {errors.space_rating?.message}

    +
    + + ) : ( +
    + + +

    {errors.customer_rating?.message}

    +
    + )} +
    + + +

    {errors.comment?.message}

    +
    + +
    + + +
    +
    + + +
    +
    +
    + ); +}; + +export default AddAdminReviewPage; diff --git a/src/pages/Admin/Review/AdminCustomerReviewListPage.jsx b/src/pages/Admin/Review/AdminCustomerReviewListPage.jsx new file mode 100644 index 0000000..f16f937 --- /dev/null +++ b/src/pages/Admin/Review/AdminCustomerReviewListPage.jsx @@ -0,0 +1,543 @@ +import React from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import AddButton from "@/components/AddButton"; +import Button from "@/components/Button"; +import PaginationHeader from "@/components/PaginationHeader"; +import Icon from "@/components/Icons"; +import ReactHtmlTableToExcel from "react-html-table-to-excel"; +import { ID_PREFIX } from "@/utils/constants"; +import { adminColumns, applySetting } from "@/utils/adminPortalColumns"; + +let sdk = new MkdSDK(); + +const AdminReviewListPage = () => { + const { dispatch } = React.useContext(AuthContext); + const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext); + const [tableColumns, setTableColumns] = React.useState([]); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); + // TODO: find a better way to do this + const [searchParams2] = useSearchParams(localStorage.getItem("admin_customer_review_filter") ?? ""); + + const schema = yup.object({ + id: yup.string(), + customer_first_name: yup.string(), + customer_last_name: yup.string(), + rating: yup.string(), + type: yup.string(), + }); + const { + reset, + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: (() => { + let fromSearch = parseSearchParams(searchParams); + if (Object.keys(fromSearch).length > 0) { + return fromSearch; + } + return parseSearchParams(searchParams2); + })(), + }); + + function onSort(accessor) { + const columns = tableColumns; + const index = columns.findIndex((column) => column.accessor === accessor); + const column = columns[index]; + column.isSortedDesc = !column.isSortedDesc; + columns.splice(index, 1, column); + setTableColumns(() => [...columns]); + const sortedList = selector(data, column.isSortedDesc, accessor); + setCurrentTableData(sortedList); + } + function selector(users, isSortedDesc, accessor) { + if (accessor?.split(",").length > 1) { + accessor = accessor.split(",")[0]; + } + + return users.sort((a, b) => { + if (isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1; + } else { + return a[accessor] < b[accessor] ? 1 : -1; + } + } + if (!isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1; + } else { + return a[accessor] < b[accessor] ? -1 : 1; + } + } + }); + } + + const rating = [ + { key: "", value: "All" }, + { key: "1", value: "1" }, + { key: "2", value: "2" }, + { key: "3", value: "3" }, + { key: "4", value: "4" }, + { key: "5", value: "5" }, + ]; + + const type = [ + { key: "", value: "All" }, + { key: "0", value: "Given" }, + { key: "1", value: "Received" }, + ]; + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum) { + let data = parseSearchParams(searchParams); + data = Object.keys(data).length < 1 ? parseSearchParams(searchParams2) : data; + data.id = data.id?.replace(ID_PREFIX.REVIEWS, ""); + + if (data && (data.type != undefined || data.type != null)) { + data.type = data.type == 0 ? "customer" : "host"; + } + try { + sdk.setTable("review"); + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/review/PAGINATE", + { + where: [ + data + ? `${data.id ? `ergo_review.id = '${data.id}'` : "1"} AND ${data.customer_first_name ? `customer.first_name LIKE '%${data.customer_first_name}%'` : "1"} AND ${ + data.customer_last_name ? `customer.last_name LIKE '%${data.customer_last_name}%'` : "1" + } AND ${data.rating ? `customer_rating = ${data.rating}` : "1"} AND ${data.type ? `given_by = '${data.type}'` : "1"} AND ${ + data.status ? `ergo_review.status = ${data.status}` : "1" + } AND ${data.property_spaces_id ? `ergo_review.property_spaces_id = ${data.property_spaces_id}` : "1"}` + : 1, + ], + page: pageNum, + limit: limitNum, + sortId: "update_at", + direction: "DESC", + user: "customer", + }, + "POST", + ); + + const { list, total, limit, num_pages, page } = result; + const sortedList = selector(list, false); + setCurrentTableData(sortedList); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("customer_first_name", data.customer_first_name); + searchParams.set("customer_last_name", data.customer_last_name); + searchParams.set("rating", data.rating); + searchParams.set("type", data.type); + searchParams.set("status", data.status); + + setSearchParams(searchParams); + localStorage.setItem("admin_customer_review_filter", searchParams.toString()); + + getData(1, pageSize); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "review", + }, + }); + + (async function () { + await fetchColumnOrder(); + getData(1, pageSize); + })(); + }, []); + + React.useEffect(() => { + if (!globalState.showReview) { + getData(1, 10); + } + }, [globalState.showReview]); + + async function fetchColumnOrder() { + sdk.setTable("settings"); + const payload = { key_name: "admin_customer_review_column_order" }; + try { + const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE"); + if (Array.isArray(result.list) && result.list.length > 0) { + setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_customer_reviews)); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + return ( + <> +
    +
    +

    Review

    + +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    +
    +
    +
    + + +

    {errors.id?.message}

    +
    + +
    + + +

    {errors.customer_last_name?.message}

    +
    +
    + + +

    {errors.customer_first_name?.message}

    +
    + +
    + + +

    {errors.rating?.message}

    +
    + +
    + + +

    {errors.type?.message}

    +
    + +
    + + +

    {errors.status?.message}

    +
    +
    + +
    + +
    + + Change Column Order + {" "} + +
    + +
    +
    + + + + {tableColumns.map((column, index) => ( + + ))} + + + + {data.map((row, i) => { + return ( + + {tableColumns.map((cell, index) => { + if (cell.accessor.split(",").length > 1) { + return ( + + ); + } + if (cell.accessor === "") { + return ( + + ); + } + if (cell.accessor === "rating") { + return ( + + ); + } + if (cell.mapping) { + return ( + + ); + } + + if (cell.idPrefix) { + return ( + + ); + } + + return ( + + ); + })} + + ); + })} + +
    onSort(column.accessor)} + > + {column.header} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
    + {cell.accessor.split(",").map((accessor) => ( + {row[accessor.trim()]} + ))} + + + + + + {row[cell.accessor]} + + + {cell.mapping[row[cell.accessor]]} + + {cell.idPrefix + row[cell.accessor]} + + {row[cell.accessor]} +
    +
    +
    + + + ); +}; + +export default AdminReviewListPage; diff --git a/src/pages/Admin/Review/AdminHostReviewPage.jsx b/src/pages/Admin/Review/AdminHostReviewPage.jsx new file mode 100644 index 0000000..749cae9 --- /dev/null +++ b/src/pages/Admin/Review/AdminHostReviewPage.jsx @@ -0,0 +1,544 @@ +import React from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import AddButton from "@/components/AddButton"; +import Button from "@/components/Button"; +import PaginationHeader from "@/components/PaginationHeader"; +import Icon from "@/components/Icons"; +import ReactHtmlTableToExcel from "react-html-table-to-excel"; +import { ID_PREFIX } from "@/utils/constants"; +import { adminColumns, applySetting } from "@/utils/adminPortalColumns"; + +let sdk = new MkdSDK(); + +const AdminReviewListPage = () => { + const { dispatch } = React.useContext(AuthContext); + const { state: globalState, dispatch: globalDispatch } = React.useContext(GlobalContext); + const [tableColumns, setTableColumns] = React.useState([]); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + const navigate = useNavigate(); + + const [searchParams, setSearchParams] = useSearchParams(); + // TODO: find a better way to do this + const [searchParams2] = useSearchParams(localStorage.getItem("admin_host_review_filter") ?? ""); + + const schema = yup.object({ + id: yup.string(), + host_first_name: yup.string(), + host_last_name: yup.string(), + rating: yup.string(), + type: yup.string(), + }); + const { + reset, + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: (() => { + let fromSearch = parseSearchParams(searchParams); + if (Object.keys(fromSearch).length > 0) { + return fromSearch; + } + return parseSearchParams(searchParams2); + })(), + }); + + function onSort(accessor) { + const columns = tableColumns; + const index = columns.findIndex((column) => column.accessor === accessor); + const column = columns[index]; + column.isSortedDesc = !column.isSortedDesc; + columns.splice(index, 1, column); + setTableColumns(() => [...columns]); + const sortedList = selector(data, column.isSortedDesc, accessor); + setCurrentTableData(sortedList); + } + function selector(users, isSortedDesc, accessor) { + if (accessor?.split(",").length > 1) { + accessor = accessor.split(",")[0]; + } + + return users.sort((a, b) => { + if (isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1; + } else { + return a[accessor] < b[accessor] ? 1 : -1; + } + } + if (!isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1; + } else { + return a[accessor] < b[accessor] ? -1 : 1; + } + } + }); + } + + const rating = [ + { key: "", value: "All" }, + { key: "1", value: "1" }, + { key: "2", value: "2" }, + { key: "3", value: "3" }, + { key: "4", value: "4" }, + { key: "5", value: "5" }, + ]; + + const type = [ + { key: "", value: "All" }, + { key: "0", value: "Given" }, + { key: "1", value: "Received" }, + ]; + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum) { + let data = parseSearchParams(searchParams); + data = Object.keys(data).length < 1 ? parseSearchParams(searchParams2) : data; + + data.id = data.id?.replace(ID_PREFIX.REVIEWS, ""); + + if (data && (data.type != undefined || data.type != null)) { + data.type = data.type == 0 ? "host" : "customer"; + } + try { + sdk.setTable("review"); + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/review/PAGINATE", + { + where: [ + data + ? `${data.id ? `ergo_review.id = '${data.id}'` : "1"} AND ${data.host_first_name ? `host.first_name LIKE '%${data.host_first_name}%'` : "1"} AND ${ + data.host_last_name ? `host.last_name LIKE '%${data.host_last_name}%'` : "1" + } AND ${data.rating ? `host_rating = ${data.rating}` : "1"} AND ${data.type ? `given_by = '${data.type}'` : "1"} AND ${data.status ? `ergo_review.status = ${data.status}` : "1"}` + : 1, + ], + page: pageNum, + limit: limitNum, + sortId: "update_at", + direction: "DESC", + user: "host", + }, + "POST", + ); + + const { list, total, limit, num_pages, page } = result; + + const sortedList = selector(list, false); + setCurrentTableData(sortedList); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("host_first_name", data.host_first_name); + searchParams.set("host_last_name", data.host_last_name); + searchParams.set("rating", data.rating); + searchParams.set("type", data.type); + searchParams.set("status", data.status); + + setSearchParams(searchParams); + localStorage.setItem("admin_host_review_filter", searchParams.toString()); + + getData(1, pageSize); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "review", + }, + }); + + (async function () { + await fetchColumnOrder(); + getData(1, pageSize); + })(); + }, []); + + React.useEffect(() => { + if (!globalState.showReview) { + getData(1, 10); + } + }, [globalState.showReview]); + + async function fetchColumnOrder() { + sdk.setTable("settings"); + const payload = { key_name: "admin_host_review_column_order" }; + try { + const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE"); + if (Array.isArray(result.list) && result.list.length > 0) { + setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_host_reviews)); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + return ( + <> +
    +
    +

    Review

    + +
    +
    +
      +
    • + +
    • +
    • + +
    • +
    +
    +
    +
    + + +

    {errors.id?.message}

    +
    + +
    + + +

    {errors.host_last_name?.message}

    +
    +
    + + +

    {errors.host_first_name?.message}

    +
    + +
    + + +

    {errors.rating?.message}

    +
    + +
    + + +

    {errors.type?.message}

    +
    +
    + + +

    {errors.status?.message}

    +
    +
    + +
    + +
    + + Change Column Order + {" "} + +
    + +
    +
    + + + + {tableColumns.map((column, index) => ( + + ))} + + + + {data.map((row, i) => { + return ( + + {tableColumns.map((cell, index) => { + if (cell.accessor.split(",").length > 1) { + return ( + + ); + } + if (cell.accessor === "") { + return ( + + ); + } + if (cell.accessor === "rating") { + return ( + + ); + } + if (cell.mapping) { + return ( + + ); + } + + if (cell.idPrefix) { + return ( + + ); + } + + return ( + + ); + })} + + ); + })} + +
    onSort(column.accessor)} + > + {column.header} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
    + {cell.accessor.split(",").map((accessor) => ( + {row[accessor.trim()]} + ))} + + + + + + {row[cell.accessor] ? row[cell.accessor] : 0} + + + {cell.mapping[row[cell.accessor]]} + + {cell.idPrefix + row[cell.accessor]} + + {row[cell.accessor]} +
    +
    +
    + + + ); +}; + +export default AdminReviewListPage; diff --git a/src/pages/Admin/Review/EditAdminReviewPage.jsx b/src/pages/Admin/Review/EditAdminReviewPage.jsx new file mode 100644 index 0000000..146fcb1 --- /dev/null +++ b/src/pages/Admin/Review/EditAdminReviewPage.jsx @@ -0,0 +1,272 @@ +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import { useNavigate, useParams } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import EditAdminPageLayout from "@/layouts/EditAdminPageLayout"; + +let sdk = new MkdSDK(); + +const EditAdminReviewPage = () => { + const { dispatch } = React.useContext(AuthContext); + const schema = yup + .object({ + customer_id: yup.number().required().positive().integer(), + property_id: yup.number().required().positive().integer(), + host_id: yup.number().required().positive().integer(), + property_spaces_id: yup.number().required().positive().integer(), + host_rating: yup.number().required().positive().integer(), + space_rating: yup.number().required().positive().integer(), + comment: yup.string().required(), + hashtags: yup.string().required(), + }) + .required(); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const navigate = useNavigate(); + const [customer_id, setCustomerId] = useState(0); + const [property_id, setPropertyId] = useState(0); + const [host_id, setHostId] = useState(0); + const [property_spaces_id, setPropertySpacesId] = useState(0); + const [host_rating, setHostRating] = useState(0); + const [space_rating, setSpaceRating] = useState(0); + const [comment, setComment] = useState(""); + const [hashtags, setHashtags] = useState(""); + const [id, setId] = useState(0); + const { + register, + handleSubmit, + setError, + setValue, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const params = useParams(); + + useEffect(function () { + (async function () { + try { + sdk.setTable("review"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + if (!result.error) { + setValue("customer_id", result.model.customer_id); + setValue("property_id", result.model.property_id); + setValue("host_id", result.model.host_id); + setValue("property_spaces_id", result.model.property_spaces_id); + setValue("host_rating", result.model.host_rating); + setValue("space_rating", result.model.space_rating); + setValue("comment", result.model.comment); + setValue("hashtags", result.model.hashtags); + setId(result.model.id); + } + } catch (error) { + console.log("error", error); + tokenExpireError(dispatch, error.message); + } + })(); + }, []); + + const onSubmit = async (data) => { + try { + const result = await sdk.callRestAPI( + { + id: id, + customer_id: data.customer_id, + property_id: data.property_id, + host_id: data.host_id, + property_spaces_id: data.property_spaces_id, + host_rating: data.host_rating, + space_rating: data.space_rating, + comment: data.comment, + hashtags: data.hashtags, + }, + "PUT", + ); + + if (!result.error) { + showToast(globalDispatch, "Updated"); + navigate("/admin/review"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("customer_id", { + type: "manual", + message: error.message, + }); + } + }; + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "review", + }, + }); + }, []); + + return ( + +
    +
    + + +

    {errors.customer_id?.message}

    +
    + +
    + + +

    {errors.property_id?.message}

    +
    + +
    + + +

    {errors.host_id?.message}

    +
    + +
    + + +

    {errors.property_spaces_id?.message}

    +
    + +
    + + +

    {errors.host_rating?.message}

    +
    + +
    + + +

    {errors.space_rating?.message}

    +
    + +
    + + +

    {errors.comment?.message}

    +
    + +
    + + +

    {errors.hashtags?.message}

    +
    + +
    + + +
    +
    +
    + ); +}; + +export default EditAdminReviewPage; diff --git a/src/pages/Admin/Setting/AddAdminSettingsPage.jsx b/src/pages/Admin/Setting/AddAdminSettingsPage.jsx new file mode 100644 index 0000000..c125ed9 --- /dev/null +++ b/src/pages/Admin/Setting/AddAdminSettingsPage.jsx @@ -0,0 +1,138 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate } from "react-router-dom"; +import { tokenExpireError, AuthContext } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import AddAdminPageLayout from "@/layouts/AddAdminPageLayout"; +import { addHours } from "@/utils/utils"; + +const AddAdminSettingsPage = () => { + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const schema = yup + .object({ + key_name: yup.string().required(), + key_value: yup.string().required(), + }) + .required(); + + const { dispatch } = React.useContext(AuthContext); + + const navigate = useNavigate(); + const { + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const onSubmit = async (data) => { + let sdk = new MkdSDK(); + + try { + sdk.setTable("settings"); + + const result = await sdk.callRestAPI( + { + key_name: data.key_name.toLowerCase(), + key_value: data.key_value, + }, + "POST", + ); + if (!result.error) { + showToast(globalDispatch, "Added"); + navigate("/admin/settings"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("key_name", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "settings", + }, + }); + }, []); + + return ( + +
    +
    + + +

    {errors.key_name?.message}

    +
    + +
    + + +

    {errors.key_value?.message}

    +
    + +
    + + +
    +
    +
    + ); +}; + +export default AddAdminSettingsPage; diff --git a/src/pages/Admin/Setting/AdminSettingsListPage.jsx b/src/pages/Admin/Setting/AdminSettingsListPage.jsx new file mode 100644 index 0000000..bb8b32b --- /dev/null +++ b/src/pages/Admin/Setting/AdminSettingsListPage.jsx @@ -0,0 +1,273 @@ +import React from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { createSearchParams, useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import AddButton from "@/components/AddButton"; +import PaginationHeader from "@/components/PaginationHeader"; +import Table from "@/components/Table"; +import Button from "@/components/Button"; +import { ID_PREFIX } from "@/utils/constants"; + +let sdk = new MkdSDK(); + +const columns = [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.SETTING, + }, + { + header: "Key Name", + accessor: "key_name", + isSorted: true, + isSortedDesc: true, + }, + { + header: "Key Value", + accessor: "key_value", + isSorted: true, + isSortedDesc: true, + }, + { + header: "Actions", + accessor: "", + }, +]; + +const AdminSettingsListPage = () => { + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const [tableColumns, setTableColumns] = React.useState(columns); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + const [searchParams, setSearchParams] = useSearchParams(); + + const schema = yup.object({ + id: yup.string(), + key_name: yup.string(), + }); + const { + reset, + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + function onSort(accessor) { + const columns = tableColumns; + const index = columns.findIndex((column) => column.accessor === accessor); + const column = columns[index]; + column.isSortedDesc = !column.isSortedDesc; + columns.splice(index, 1, column); + setTableColumns(() => [...columns]); + const sortedList = selector(data, column.isSortedDesc, accessor); + setCurrentTableData(sortedList); + } + function selector(users, isSortedDesc, accessor) { + if (accessor?.split(",").length > 1) { + accessor = accessor.split(",")[0]; + } + + return users.sort((a, b) => { + if (isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1; + } else { + return a[accessor] < b[accessor] ? 1 : -1; + } + } + if (!isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1; + } else { + return a[accessor] < b[accessor] ? -1 : 1; + } + } + }); + } + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum) { + const data = parseSearchParams(searchParams); + try { + sdk.setTable("settings"); + const payload = { id: data.id || undefined, key_name: data.key_name || undefined }; + const result = await sdk.callRestAPI( + { + payload, + page: pageNum, + limit: limitNum, + }, + "PAGINATE", + ); + + const { list, total, limit, num_pages, page } = result; + const sortedList = selector( + list.filter((stg) => stg.key_value), + false, + ); + setCurrentTableData(sortedList); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + const onSubmit = (data) => { + data.id = data.id.replace(ID_PREFIX.SETTING, ""); + let id = data.id ?? undefined; + let key_name = data.key_name ?? undefined; + setSearchParams( + createSearchParams({ + id, + key_name: key_name, + }), + ); + getData(1, pageSize); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "settings", + }, + }); + + getData(1, pageSize); + }, []); + + return ( + <> +
    +
    +

    Settings

    + +
    +
    +
    + + +

    {errors.id?.message}

    +
    + +
    + + +

    {errors.key_name?.message}

    +
    +
    + +
    + + +
    +
    + + + + + + ); +}; + +export default AdminSettingsListPage; diff --git a/src/pages/Admin/Setting/EditAdminSettingsPage.jsx b/src/pages/Admin/Setting/EditAdminSettingsPage.jsx new file mode 100644 index 0000000..83d50d4 --- /dev/null +++ b/src/pages/Admin/Setting/EditAdminSettingsPage.jsx @@ -0,0 +1,158 @@ +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import { useNavigate, useParams } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import EditAdminPageLayout from "@/layouts/EditAdminPageLayout"; + +let sdk = new MkdSDK(); + +const EditAdminSettingsPage = () => { + const { dispatch } = React.useContext(AuthContext); + const schema = yup + .object({ + key_value: yup.string().required(), + }) + .required(); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const navigate = useNavigate(); + const [id, setId] = useState(0); + const { + register, + handleSubmit, + setError, + setValue, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const params = useParams(); + + useEffect(function () { + (async function () { + try { + sdk.setTable("settings"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + if (!result.error) { + setValue("key_name", result.model.key_name); + setValue("key_value", result.model.key_value); + setId(result.model.id); + } + } catch (error) { + console.log("error", error); + tokenExpireError(dispatch, error.message); + } + })(); + }, []); + + const onSubmit = async (data) => { + try { + const result = await sdk.callRestAPI( + { + id: id, + key_value: data.key_value, + }, + "PUT", + ); + + if (!result.error) { + showToast(globalDispatch, "Updated"); + navigate("/admin/settings"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("key_name", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "settings", + }, + }); + }, []); + + return ( + +
    +
    + + +

    {errors.key_name?.message}

    +
    + +
    + + +

    {errors.key_value?.message}

    +
    + +
    + + +
    + +
    + ); +}; + +export default EditAdminSettingsPage; diff --git a/src/pages/Admin/Space/AddAdminSpacesPage.jsx b/src/pages/Admin/Space/AddAdminSpacesPage.jsx new file mode 100644 index 0000000..815545e --- /dev/null +++ b/src/pages/Admin/Space/AddAdminSpacesPage.jsx @@ -0,0 +1,196 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate } from "react-router-dom"; +import { tokenExpireError, AuthContext } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import AddAdminPageLayout from "@/layouts/AddAdminPageLayout"; + +const AddAdminSpacesPage = () => { + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const schema = yup + .object({ + category: yup.string().required(), + has_sizes: yup.string(), + }) + .required(); + + const { dispatch } = React.useContext(AuthContext); + const [loading, setLoading] = React.useState(false); + + const navigate = useNavigate(); + const { + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const onSubmit = async (data) => { + setLoading(true); + if (data.image.length < 1 || !data.image[0]) { + setError("image", { type: "manual", message: "This field is required" }); + return; + } + if (data.icon.length < 1 || !data.icon[0]) { + setError("icon", { type: "manual", message: "This field is required" }); + return; + } + let sdk = new MkdSDK(); + try { + const formData = new FormData(); + formData.append("file", data.image[0]); + const upload = await sdk.uploadImage(formData); + const formIconData = new FormData(); + formIconData.append("file", data.icon[0]); + const uploadIcon = await sdk.uploadImage(formIconData); + sdk.setTable("spaces"); + + const result = await sdk.callRestAPI( + { + category: data.category, + image: upload.url, + icon: uploadIcon.url, + has_sizes: data.has_sizes, + }, + "POST", + ); + if (!result.error) { + showToast(globalDispatch, "Added"); + navigate("/admin/spaces"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("category", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + setLoading(false); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "spaces", + }, + }); + }, []); + + return ( + +
    +
    +
    + + +

    {errors.category?.message}

    +
    + +
    + + +

    {errors.image?.message}

    +
    +
    + + +

    {errors.icon?.message}

    +
    + +
    + + + +

    {errors.has_sizes?.message}

    +
    + +
    + + +
    + +
    +
    + ); +}; + +export default AddAdminSpacesPage; diff --git a/src/pages/Admin/Space/AdminSpacesListPage.jsx b/src/pages/Admin/Space/AdminSpacesListPage.jsx new file mode 100644 index 0000000..3f30d95 --- /dev/null +++ b/src/pages/Admin/Space/AdminSpacesListPage.jsx @@ -0,0 +1,320 @@ +import React from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import AddButton from "@/components/AddButton"; +import Button from "@/components/Button"; +import Table from "@/components/Table"; +import PaginationHeader from "@/components/PaginationHeader"; +import ReactHtmlTableToExcel from "react-html-table-to-excel"; +import { ID_PREFIX } from "@/utils/constants"; +import { adminColumns, applySetting } from "@/utils/adminPortalColumns"; +import TreeSDK from "@/utils/TreeSDK"; + +let sdk = new MkdSDK(); +const treeSdk = new TreeSDK(); + +const AdminSpacesListPage = () => { + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch, state } = React.useContext(GlobalContext); + const [tableColumns, setTableColumns] = React.useState([]); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_spaces_filter") ?? ""); + + const schema = yup.object({ + category: yup.string(), + id: yup.string(), + }); + const { + reset, + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + function onSort(accessor) { + const columns = tableColumns; + const index = columns.findIndex((column) => column.accessor === accessor); + const column = columns[index]; + column.isSortedDesc = !column.isSortedDesc; + columns.splice(index, 1, column); + setTableColumns(() => [...columns]); + const sortedList = selector(data, column.isSortedDesc, accessor); + setCurrentTableData(sortedList); + } + function selector(users, isSortedDesc, accessor) { + if (accessor?.split(",").length > 1) { + accessor = accessor.split(",")[0]; + } + + return users.sort((a, b) => { + if (isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1; + } else { + return a[accessor] < b[accessor] ? 1 : -1; + } + } + if (!isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1; + } else { + return a[accessor] < b[accessor] ? -1 : 1; + } + } + }); + } + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum) { + const data = parseSearchParams(searchParams); + data.id = data.id?.replace(ID_PREFIX.SPACE_CATEGORY, ""); + + try { + let filter = ["deleted_at,is"]; + if (data.id) { + filter.push(`id,eq,${data.id}`); + } + if (data.category) { + filter.push(`category,cs,${data.category}`); + } + if (data.has_sizes) { + filter.push(`has_sizes,eq,${data.has_sizes}`); + } + const result = await treeSdk.getPaginate("spaces", { join: [], filter, page: pageNum || 1, size: limitNum, order: "update_at" }); + + const { list, total, limit, num_pages, page } = result; + + const sortedList = selector(list, false); + setCurrentTableData(sortedList); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + const onSubmit = (data) => { + searchParams.set("id", data.id); + searchParams.set("category", data.category); + searchParams.set("has_sizes", data.has_sizes); + + setSearchParams(searchParams); + localStorage.setItem("admin_spaces_filter", searchParams.toString()); + + getData(1, pageSize); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "spaces", + }, + }); + + (async function () { + await fetchColumnOrder(); + getData(1, pageSize); + })(); + }, []); + + React.useEffect(() => { + if (state.deleted) { + globalDispatch({ + type: "DELETED", + payload: { + deleted: false, + }, + }); + getData(currentPage, pageSize); + } + }, [state.deleted]); + + async function fetchColumnOrder() { + sdk.setTable("settings"); + const payload = { key_name: "admin_space_categories_column_order" }; + try { + const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE"); + console.log(JSON.parse(result.list[0].optional_data)) + if (Array.isArray(result.list) && result.list.length > 0) { + setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_space_categories)); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + return ( + <> +
    +
    +

    Spaces Search

    + +
    + +
    +
    + + +

    {errors.id?.message}

    +
    + +
    + + + +

    {errors.id?.message}

    +
    + +
    + + +

    {errors.category?.message}

    +
    +
    + + + + + +
    + + Change Column Order + + +
    + +
    +
    +
    + + + + + ); +}; + +export default AdminSpacesListPage; diff --git a/src/pages/Admin/Space/EditAdminSpacesPage.jsx b/src/pages/Admin/Space/EditAdminSpacesPage.jsx new file mode 100644 index 0000000..f4959bc --- /dev/null +++ b/src/pages/Admin/Space/EditAdminSpacesPage.jsx @@ -0,0 +1,207 @@ +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import { useNavigate, useParams } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import EditAdminPageLayout from "@/layouts/EditAdminPageLayout"; + +let sdk = new MkdSDK(); + +const EditAdminSpacesPage = () => { + const { dispatch } = React.useContext(AuthContext); + const schema = yup + .object({ + category: yup.string().required(), + has_sizes: yup.string(), + }) + .required(); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const navigate = useNavigate(); + const [id, setId] = useState(0); + const { + register, + handleSubmit, + setError, + setValue, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const params = useParams(); + + useEffect(function () { + (async function () { + try { + sdk.setTable("spaces"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + if (!result.error) { + setValue("category", result.model.category); + setValue("has_sizes", result.model.has_sizes); + setId(result.model.id); + } + } catch (error) { + console.log("error", error); + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + })(); + }, []); + + const onSubmit = async (data) => { + try { + var image = undefined; + var icon = undefined; + if (data.image.length > 0 && data.image[0]) { + const formData = new FormData(); + formData.append("file", data.image[0]); + const upload = await sdk.uploadImage(formData); + image = upload.url; + } + if (data.icon.length > 0 && data.icon[0]) { + const formData = new FormData(); + formData.append("file", data.icon[0]); + const upload = await sdk.uploadImage(formData); + icon = upload.url; + } + sdk.setTable("spaces"); + const result = await sdk.callRestAPI( + { + id: id, + category: data.category, + image, + icon, + has_sizes: data.has_sizes, + }, + "PUT", + ); + + if (!result.error) { + showToast(globalDispatch, "Updated"); + navigate("/admin/spaces"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("category", { + type: "manual", + message: error.message, + }); + } + }; + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "spaces", + }, + }); + }, []); + + return ( + +
    +
    + + +

    {errors.category?.message}

    +
    +
    + + +

    {errors.image?.message}

    +
    +
    + + +

    {errors.icon?.message}

    +
    +
    + + + +

    {errors.has_sizes?.message}

    +
    +
    + + +
    + +
    + ); +}; + +export default EditAdminSpacesPage; diff --git a/src/pages/Admin/User/AddAdminUserPage.jsx b/src/pages/Admin/User/AddAdminUserPage.jsx new file mode 100644 index 0000000..58a938a --- /dev/null +++ b/src/pages/Admin/User/AddAdminUserPage.jsx @@ -0,0 +1,334 @@ +import React from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { tokenExpireError, AuthContext } from "@/authContext"; +import AddAdminPageLayout from "@/layouts/AddAdminPageLayout"; +import moment from "moment"; +import commonPasswords from "@/assets/json/common-passwords.json"; + +const AddAdminUserPage = () => { + const schema = yup.object({ + firstName: yup.string().required("First name is required"), + lastName: yup.string().required("Last name is required"), + email: yup.string().email().required("Email is required"), + dob: yup + .string() + .test("is-not-in-future", "Not a valid date", (val) => { + console.log("testing here", val); + if (val == "") return true; + const date = new Date(val); + return date < new Date(); + }) + .test("must-be-at-least-18yo", "Must be at least 18 years of age", (val) => { + return moment().diff(moment(val), "years") > 18; + }), + password: yup + .string() + .required("Password is required") + .min(10, "Password must be at least 10 characters long") + .matches(/^(?=.*[0-9])/, "Password must contain at least one digit(0-9)") + .matches(/^(?=.*[a-z])/, "Password must contain at least one lowercase letter") + .matches(/^(?=.*[A-Z])/, "Password must contain at least one uppercase letter") + .matches(/^(?=.*[!@#\$%\^&\*])/, "Password must contain at least one symbol") + .test("is-not-dictionary", "Password must not contain a common word", (val) => { + return commonPasswords.every((pass) => !val.includes(pass)); + }) + .test("does-not-contain-user-info", "Password must not contain your name or date of birth", (val, ctx) => { + const d = moment(ctx.parent.dob); + return [ctx.parent.firstName, ctx.parent.lastName, d.format("yyyyMMDD"), d.format("DDMMyyyy"), d.format("MMDDyyyy"), d.format("YYMMDD"), d.format("MMDDYY"), d.format("DDMMYY")].every( + (field) => field.trim() == "" || !val.toLowerCase().includes(field.toLowerCase()), + ); + }), + role: yup.string().required(), + status: yup.string().required(), + verify: yup.string().required(), + }); + + const { dispatch, state: authState } = React.useContext(AuthContext); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const navigate = useNavigate(); + const { + register, + handleSubmit, + setError, + trigger, + formState: { errors, dirtyFields }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { password: "" }, + criteriaMode: "all", + mode: "all", + }); + + const onSubmit = async (data) => { + console.log("submitting", data); + let sdk = new MkdSDK(); + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/register", + { + firstName: data.firstName, + lastName: data.lastName, + status: data.status || 0, + email: data.email, + password: data.password, + dob: data.dob || null, + verify: data.verify || 0, + role: data.role, + payment_method_set: 0, + }, + "POST", + ); + + if (!result.error) { + showToast(dispatch, "Added"); + navigate("/admin/user"); + } else { + if (result?.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + + // register device + sdk.setTable("device"); + await sdk.callRestAPI({ active: 1, user_id: result.user_id, last_login_time: new Date().toISOString().split("T")[0], uid: localStorage.getItem("device-uid") }, "POST"); + } catch (error) { + setError("firstName", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "user", + }, + }); + }, []); + + function getPasswordErrors() { + var arr = []; + if (Array.isArray(errors.password?.types.matches)) { + arr = [...errors.password.types.matches]; + } + if (typeof errors.password?.types?.matches === "string") { + arr.push(errors.password.types.matches); + } + if (errors.password?.types?.min) { + arr.push(errors.password.types.min); + } + if (errors.password?.types["does-not-contain-user-info"]) { + arr.push(errors.password?.types["does-not-contain-user-info"]); + } + if (errors.password?.types["is-not-dictionary"]) { + arr.push(errors.password?.types["is-not-dictionary"]); + } + return arr; + } + const passwordErrors = getPasswordErrors(); + + return ( + +
    +
    +
    + + +

    {errors.firstName?.message}

    +
    +
    + + +

    {errors.lastName?.message}

    +
    +
    + + +

    {errors.email?.message}

    +
    +
    + + +

    {errors.dob?.message}

    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + { + trigger("password"); + }, + })} + autoComplete="new-password" + className={` mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none ${errors.password?.message ? "border-red-500" : ""}`} + /> + {dirtyFields.password && ( +
    + {passwordErrors.map((msg, idx) => ( +

    {msg}

    + ))} +
    + )} +
    +
    + + +
    + +
    +
    + ); +}; + +export default AddAdminUserPage; diff --git a/src/pages/Admin/User/AdminUserListPage.jsx b/src/pages/Admin/User/AdminUserListPage.jsx new file mode 100644 index 0000000..d874b68 --- /dev/null +++ b/src/pages/Admin/User/AdminUserListPage.jsx @@ -0,0 +1,583 @@ +import React from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { useForm } from "react-hook-form"; +import * as yup from "yup"; +import { clearSearchParams, parseJsonSafely, parseSearchParams } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import AddButton from "@/components/AddButton"; +import Button from "@/components/Button"; +import PaginationHeader from "@/components/PaginationHeader"; +import ReactHtmlTableToExcel from "react-html-table-to-excel"; +import { ID_PREFIX, IMAGE_STATUS } from "@/utils/constants"; +import { adminColumns, applySetting } from "@/utils/adminPortalColumns"; +import ProfileImagePreviewModal from "./ProfileImagePreviewModal"; +import RejectProfileImageModal from "./RejectProfileImageModal"; +import moment from "moment"; +import ViewPreferencesModal from "./ViewPreferencesModal"; + +let sdk = new MkdSDK(); + +const AdminUserListPage = () => { + const { dispatch: globalDispatch, state } = React.useContext(GlobalContext); + const { dispatch } = React.useContext(AuthContext); + const [tableColumns, setTableColumns] = React.useState([]); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + + const [searchParams, setSearchParams] = useSearchParams(); + // TODO: find a better way to do this + const [searchParams2] = useSearchParams(localStorage.getItem("admin_user_filter") ?? ""); + + const [activePicture, setActivePicture] = React.useState(""); + const [pictureModal, setPictureModal] = React.useState(false); + const [activeRow, setActiveRow] = React.useState({}); + const [preferences, setPreferences] = React.useState({}); + const [preferenceModal, setPreferenceModal] = React.useState(false); + const navigate = useNavigate(); + + const schema = yup.object({ + id: yup.string(), + email: yup.string(), + role: yup.string(), + dob: yup.string().test("is-not-in-future", "Not a valid date", (val) => { + if (val == "") return true; + const date = new Date(val); + return date < new Date(); + }), + status: yup.string(), + first_name: yup.string(), + last_name: yup.string(), + }); + + const { + reset, + register, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: (() => { + let fromSearch = parseSearchParams(searchParams); + if (Object.keys(fromSearch).length > 0) { + return fromSearch; + } + return parseSearchParams(searchParams2); + })(), + }); + + const selectRole = ["Superadmin", "Admin", "Host", "Customer"]; + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + function onSort(accessor) { + const columns = tableColumns; + const index = columns.findIndex((column) => column.accessor === accessor); + const column = columns[index]; + column.isSortedDesc = !column.isSortedDesc; + columns.splice(index, 1, column); + setTableColumns(() => [...columns]); + const sortedList = selector(data, column.isSortedDesc, accessor); + setCurrentTableData(sortedList); + } + function selector(users, isSortedDesc, accessor) { + if (accessor?.split(",").length > 1) { + accessor = accessor.split(",")[0]; + } + + return users.sort((a, b) => { + if (isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1; + } else { + return a[accessor] < b[accessor] ? 1 : -1; + } + } + if (!isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1; + } else { + return a[accessor] < b[accessor] ? -1 : 1; + } + } + }); + } + async function getData(pageNum, limitNum) { + let data = parseSearchParams(searchParams); + data = Object.keys(data).length < 1 ? parseSearchParams(searchParams2) : data; + + data.id = data.id?.replace(ID_PREFIX.USER, ""); + data.id = data.id?.replace(ID_PREFIX.CUSTOMER, ""); + data.id = data.id?.replace(ID_PREFIX.HOST, ""); + + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/user/PAGINATE", + { + page: pageNum, + limit: limitNum, + where: [ + data + ? `${data.id ? `ergo_user.id = ${data.id}` : "1"} AND ${data.first_name ? `ergo_user.first_name LIKE '%${data.first_name}%'` : "1"} AND ${data.last_name ? `ergo_user.last_name LIKE '%${data.last_name}%'` : "1" + } AND ${data.dob ? `ergo_profile.dob = '${data.dob}'` : "1"} AND ${data.role ? `ergo_user.role = '${data.role}'` : "1"} AND ${data.email ? `ergo_user.email LIKE '%${data.email}%'` : "1" + } AND ergo_user.deleted_at IS NULL` + : "1", + ], + sortId: "create_at", + direction: "DESC", + }, + "POST", + ); + + const { list, total, limit, num_pages, page } = result; + + + const sortedList = selector(list, false); + setCurrentTableData(sortedList); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + } + + const onSubmit = (data) => { + searchParams.set("email", data.email); + searchParams.set("first_name", data.first_name); + searchParams.set("last_name", data.last_name); + searchParams.set("role", data.role); + searchParams.set("dob", data.dob); + // searchParams.set("status", data.status); + searchParams.set("id", data.id); + setSearchParams(searchParams); + localStorage.setItem("admin_user_filter", searchParams.toString()); + getData(currentPage, pageSize); + }; + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "user", + }, + }); + + (async function () { + await fetchColumnOrder(); + await getData(1, pageSize); + })(); + }, []); + + React.useEffect(() => { + if (state.deleted) { + globalDispatch({ + type: "DELETED", + payload: { + deleted: false, + }, + }); + getData(currentPage, pageSize); + } + }, [state.deleted]); + + async function fetchColumnOrder() { + sdk.setTable("settings"); + const payload = { key_name: "admin_user_column_order" }; + try { + const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE"); + if (Array.isArray(result.list) && result.list.length > 0) { + setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.admin_user)); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + async function approveImage(id) { + sdk.setTable("user"); + try { + await sdk.callRestAPI({ id, is_photo_approved: IMAGE_STATUS.APPROVED }, "PUT"); + showToast(globalDispatch, "Successful"); + await getData(1, pageSize); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + React.useEffect(() => { + let timeout; + if (!preferenceModal) { + timeout = setTimeout(() => { + setPreferences({}); + }, 200); + } + + return () => clearTimeout(timeout); + }, [preferenceModal]); + + const photoOptions = (row) => { + switch (row.is_photo_approved) { + case IMAGE_STATUS.IN_REVIEW: + return ( + <> + + + + ); + case IMAGE_STATUS.APPROVED: + return ( + + ); + case IMAGE_STATUS.NOT_APPROVED: + return ( + + ); + default: + return ""; + } + }; + + return ( + <> +
    +
    +

    Users

    + +
    + +
    +
    + + +

    {errors.id?.message}

    +
    +
    + + +

    {errors.first_name?.message}

    +
    +
    + + +

    {errors.last_name?.message}

    +
    +
    + + +

    {errors.email?.message}

    +
    +
    + + +

    {errors.dob?.message}

    +
    + +
    + + +

    +
    +
    + + + + +
    + + Change Column Order + + +
    + +
    +
    +
    + + + {tableColumns.map((column, index) => ( + + ))} + + + + {data.map((row, i) => { + return ( + + {tableColumns.map((cell, index) => { + if (cell.accessor === "") { + return ( + + ); + } + if (cell.statusMapping) { + return ( + + ); + } + if (cell.mapping) { + return ( + + ); + } + + if (cell.accessor == "dob") { + return ( + + ); + } + if (cell.idPrefix) { + return ( + + ); + } + + if (cell.accessor == "settings") { + return ( + + ); + } + + return ( + + ); + })} + + ); + })} + +
    onSort(column.accessor)} + > + {column.header} + {column.isSorted} + {column.isSorted ? (column.isSortedDesc ? " â–¼" : " â–²") : ""} +
    + + {photoOptions(row)} + + + + {" "} + {cell.statusMapping[row[cell.accessor]]} + + + {cell.mapping[row[cell.accessor]] ?? cell.default ?? "N/A"} + + {row[cell.accessor] ? moment(row[cell.accessor]).format("DD/MM/YYYY") : ""} + + {cell.idPrefix + row[cell.accessor]} + + + + {row[cell.accessor]} +
    +
    +
    + + setPictureModal(false)} + /> + setActiveRow({})} + data={activeRow} + onSuccess={() => getData(currentPage, pageSize)} + /> + setPreferenceModal(false)} + preferences={preferences} + /> + + ); +}; + +export default AdminUserListPage; diff --git a/src/pages/Admin/User/EditAdminUserPage.jsx b/src/pages/Admin/User/EditAdminUserPage.jsx new file mode 100644 index 0000000..a7c6a4d --- /dev/null +++ b/src/pages/Admin/User/EditAdminUserPage.jsx @@ -0,0 +1,382 @@ +import React, { useState, useRef, useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate, useParams } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import EditAdminPageLayout from "@/layouts/EditAdminPageLayout"; +import moment from "moment"; + +let sdk = new MkdSDK(); + +const EditAdminUserPage = () => { + const schema = yup + .object({ + firstName: yup.string().required(), + lastName: yup.string().required(), + email: yup.string().email().required(), + password: yup.string(), + status: yup.string(), + dob: yup.string().nullable(), + role: yup.string(), + verify: yup.string(), + }) + .required(); + + const { dispatch, state: authState } = React.useContext(AuthContext); + const { dispatch: globalDispatch, state } = React.useContext(GlobalContext); + const buttonRef = useRef(null); + const navigate = useNavigate(); + const params = useParams(); + const [oldEmail, setOldEmail] = useState(""); + const [oldFirstName, setOldFirstName] = useState(""); + const [oldLastName, setOldLastName] = useState(""); + const [id, setId] = useState(0); + + const { + trigger, + register, + handleSubmit, + setError, + setValue, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const selectStatus = [ + { key: "0", value: "Inactive" }, + { key: "1", value: "Active" }, + ]; + + const verify = [ + { key: "0", value: "No" }, + { key: "1", value: "Yes" }, + ]; + + const onSubmit = async (data) => { + console.log("got here", data); + try { + if (oldEmail !== data.email) { + console.log("here", oldEmail, data.email); + const emailresult = await sdk.updateEmailByAdmin(data.email, id); + if (!emailresult.error) { + showToast(globalDispatch, "Email Updated", 1000); + } else { + if (emailresult.validation) { + const keys = Object.keys(emailresult.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: emailresult.validation[field], + }); + } + } + } + } + + sdk.setTable("user"); + const result = await sdk.callRestAPI( + { + id, + first_name: data.firstName, + last_name: data.lastName, + email: data.email, + role: data.role.toLowerCase(), + status: data.status, + verify: data.verify || 0, + }, + "PUT", + ); + sdk.setTable("profile"); + const resultDob = await sdk.callRestAPI({ set: { dob: data.dob }, where: { user_id: id } }, "PUTWHERE"); // Note: Ideally it should be user_id but existing sdk only supports updating by id + + if (resultDob.error) { + setError("dob", { + type: "manual", + message: "Date of birth is required", + }); + } else if (!result.error) { + showToast(globalDispatch, "Updated", 4000); + navigate("/admin/user"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + console.log("Error", error); + setError("email", { + type: "manual", + message: error.message, + }); + tokenExpireError(dispatch, error.message); + } + }; + + useEffect(() => { + if (state.saveChanges) { + // check form then submit if all good + console.log("triggering"); + trigger().then((res) => { + if (res) { + handleSubmit(onSubmit)(); + } + }); + + globalDispatch({ + type: "SAVE_CHANGES", + payload: { + saveChanges: false, + }, + }); + } + }, [state.saveChanges]); + + useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "user", + }, + }); + + (async function () { + try { + sdk.setTable("user"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + + sdk.setTable("profile"); + const { + list: [profile], + } = await sdk.callRestAPI({ payload: { user_id: result.model.id } }, "GETALL"); + + if (!result.error) { + setValue("firstName", result.model.first_name); + setValue("lastName", result.model.last_name); + setValue("email", result.model.email); + setValue("role", result.model.role); + setValue("dob", !profile?.dob ? null : moment(profile.dob).format("yyyy-MM-DD")); + setValue("status", result.model.status); + setValue("verify", result.model.verify); + setOldEmail(result.model.email); + setOldFirstName(result.model.first_name); + setOldLastName(result.model.last_name); + setId(result.model.id); + } + } catch (error) { + console.log("Error", error); + tokenExpireError(dispatch, error.message); + } + })(); + }, []); + return ( + +
    +
    + + +

    {errors.firstName?.message}

    +
    +
    + + +

    {errors.lastName?.message}

    +
    +
    + + +

    {errors.email?.message}

    +
    +
    + + +

    {errors.dob?.message}

    +
    +
    + + +
    +
    + + +
    +
    + + +
    + {/*
    + + +

    + {errors.password?.message} +

    +
    */} +
    + + + +
    +
    +
    + ); +}; + +export default EditAdminUserPage; diff --git a/src/pages/Admin/User/ProfileImagePreviewModal.jsx b/src/pages/Admin/User/ProfileImagePreviewModal.jsx new file mode 100644 index 0000000..4c54a99 --- /dev/null +++ b/src/pages/Admin/User/ProfileImagePreviewModal.jsx @@ -0,0 +1,52 @@ +import { Dialog, Transition } from "@headlessui/react"; +import { Fragment, useState } from "react"; + +export default function ProfileImagePreviewModal({ modalOpen, modalImage, closeModal }) { + return ( + <> + + + +
    + + +
    +
    + + + +
    +
    +
    +
    + + ); +} diff --git a/src/pages/Admin/User/RejectProfileImageModal.jsx b/src/pages/Admin/User/RejectProfileImageModal.jsx new file mode 100644 index 0000000..d9036b0 --- /dev/null +++ b/src/pages/Admin/User/RejectProfileImageModal.jsx @@ -0,0 +1,128 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import { IMAGE_STATUS } from "@/utils/constants"; +import MkdSDK from "@/utils/MkdSDK"; +import { parseJsonSafely } from "@/utils/utils"; +import { Dialog, Transition } from "@headlessui/react"; +import { useContext, useState } from "react"; +import { Fragment } from "react"; + +export default function RejectProfileImageModal({ modalOpen, data, closeModal, onSuccess, noSettings }) { + const { dispatch } = useContext(AuthContext); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const [loading, setLoading] = useState(false); + + async function onSubmit(e) { + setLoading(true); + const sdk = new MkdSDK(); + e.preventDefault(); + const formData = new FormData(e.target); + const reason = formData.get("reason"); + sdk.setTable("user"); + try { + await sdk.callRestAPI({ id: data.id, is_photo_approved: IMAGE_STATUS.NOT_APPROVED }, "PUT"); + + let settings = data.settings; + if (noSettings) { + const userData = await callCustomAPI("get-user", "post", { id: data.id }, ""); + settings = userData.settings; + } + + if (parseJsonSafely(settings, {}).email_on_profile_photo_declined == true) { + const tmpl = await sdk.getEmailTemplate("profile-image-decline"); + const body = tmpl.html?.replace(new RegExp("{{{reason}}}", "g"), reason); + + await sdk.sendEmail(data.email, tmpl.subject, body); + showToast(globalDispatch, "Email sent to user"); + } else { + showToast(globalDispatch, "Successful"); + } + + onSuccess(); + e.target.reset(); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + closeModal(); + setLoading(false); + } + + return ( + <> + + + +
    + + +
    +
    + + + + Decline Reason + + +
    + + +
    +
    +
    +
    +
    +
    +
    + + ); +} diff --git a/src/pages/Admin/User/ViewAdminUserPage.jsx b/src/pages/Admin/User/ViewAdminUserPage.jsx new file mode 100644 index 0000000..655d43f --- /dev/null +++ b/src/pages/Admin/User/ViewAdminUserPage.jsx @@ -0,0 +1,172 @@ +import React, { useState } from "react"; +import MkdSDK from "@/utils/MkdSDK"; +import { useNavigate, useParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import ViewAdminPageLayout from "@/layouts/ViewAdminPageLayout"; +import Icon from "@/components/Icons"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import moment from "moment"; + +let sdk = new MkdSDK(); + +const ViewAdminUserPage = () => { + const status = ["Inactive", "Active", "Suspend"]; + const verified = ["No", "Yes"]; + const id_verified = ["Pending", "Yes", "No"]; + const [userInfo, setUserInfo] = useState({}); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const { dispatch, state: authState } = React.useContext(AuthContext); + const params = useParams(); + const navigate = useNavigate(); + const [loading, setLoading] = useState(false); + + async function fetchUser() { + try { + sdk.setTable("user"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + + sdk.setTable("profile"); + const { + list: [resultDob], + } = await sdk.callRestAPI( + { payload: { user_id: result.model.id } }, // Note: Should be user_id + "GETALL", + ); + + sdk.setTable("id_verification"); + const { + list: [resultIdVerification], + } = await sdk.callRestAPI( + { payload: { user_id: result.model.id } }, // Note: Should be user_id + "GETALL", + ); + setUserInfo({ ...result.model, dob: resultDob?.dob, id_verified: resultIdVerification?.status }); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + async function sendPasswordReset() { + setLoading(true); + try { + await sdk.forgot(userInfo.email, userInfo.role); + showToast(globalDispatch, "Email Sent"); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + setLoading(false); + } + + async function sendEmailVerification() { + try { + await sdk.callRawAPI("/v2/api/custom/ergo/resend-verification-email", { email: userInfo.email }, "POST"); + showToast(globalDispatch, "Email Sent"); + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "user", + }, + }); + + fetchUser(); + }, []); + + return ( + +
    +
    +
    +

    Profile Details

    + {!(authState.role != "superadmin" && userInfo.role == "superadmin") && ( +
    + +
    + )} +
    +
    +

    ID

    +

    {userInfo.id}

    +
    +
    +

    First Name

    +

    {userInfo.first_name}

    +
    +
    +

    Last Name

    +

    {userInfo.last_name}

    +
    +
    +

    Email

    +

    {userInfo.email}

    +
    +
    +

    Date of Birth

    +

    {userInfo.dob == null ? "N/A" : moment(userInfo.dob).format("MM/DD/yyyy")}

    +
    +
    +

    Role

    +

    {userInfo.role}

    +
    +
    +

    Status

    +

    {status[userInfo.status]}

    +
    +
    +

    Email Verified

    +

    {verified[userInfo.verify]}

    +
    +
    +

    ID Verified

    +

    {id_verified[userInfo.id_verified] ?? "N/A"}

    +
    +
    +

    Actions

    + + +
    +
    +
    +
    + ); +}; + +export default ViewAdminUserPage; diff --git a/src/pages/Admin/User/ViewPreferencesModal.jsx b/src/pages/Admin/User/ViewPreferencesModal.jsx new file mode 100644 index 0000000..297ebe4 --- /dev/null +++ b/src/pages/Admin/User/ViewPreferencesModal.jsx @@ -0,0 +1,64 @@ +import { parseJsonSafely } from "@/utils/utils"; +import { Dialog, Transition } from "@headlessui/react"; +import { Fragment } from "react"; + +export default function ViewPreferencesModal({ modalOpen, preferences, closeModal }) { + function titleCase(s) { + return s.replace(/^_*(.)|_+(.)/g, (s, c, d) => (c ? c.toUpperCase() : " " + d.toUpperCase())); + } + return ( + <> + + + +
    + + +
    +
    + + + {/*
    {JSON.stringify(preferences, null, 5)}
    */} +
      + {Object.entries(parseJsonSafely(preferences, {})).map(([k, v], idx) => ( +
    • + {titleCase(k)}: {v ? "YES" : "NO"} +
    • + ))} +
    +
    +
    +
    +
    +
    +
    + + ); +} diff --git a/src/pages/Common/Booking/BookingConfirmationPage.jsx b/src/pages/Common/Booking/BookingConfirmationPage.jsx new file mode 100644 index 0000000..10261d6 --- /dev/null +++ b/src/pages/Common/Booking/BookingConfirmationPage.jsx @@ -0,0 +1,253 @@ +import React from "react"; +import { useEffect } from "react"; +import { useState } from "react"; +import { Link, useNavigate, useParams } from "react-router-dom"; +import CircleCheckIcon from "@/components/frontend/icons/CircleCheckIcon"; +import DateTimeIcon from "@/components/frontend/icons/DateTimeIcon"; +import GreenCheckIcon from "@/components/frontend/icons/GreenCheckIcon"; +import PersonIcon from "@/components/frontend/icons/PersonIcon"; +import StarIcon from "@/components/frontend/icons/StarIcon"; +import Icon from "@/components/Icons"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import MkdSDK from "@/utils/MkdSDK"; +import { useBookingContext } from "./bookingContext"; +import { daysMapping, monthsMapping } from "@/utils/date-time-utils"; +import FavoriteButton from "@/components/frontend/FavoriteButton"; +import PropertySpaceMapImage from "@/components/frontend/PropertySpaceMapImage"; +import { usePropertySpace } from "@/hooks/api"; + +let sdk = new MkdSDK(); + +const BookingConfirmationPage = () => { + const { bookingData } = useBookingContext(); + const [booking, setBooking] = useState({}); + const { id } = useParams(); + const navigate = useNavigate(); + const [render, forceRender] = useState(false); + const [showMap, setShowMap] = useState(false); + const { propertySpace } = usePropertySpace(booking.property_space_id, render); + + async function fetchBooking(booking_id) { + const where = [`ergo_booking.id = ${booking_id} AND ergo_booking.deleted_at IS NULL`]; + try { + const result = await callCustomAPI("booking/details", "post", { where }, ""); + setBooking(result.list ?? {}); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + useEffect(() => { + fetchBooking(bookingData.id ?? id); + }, []); + + return ( +
    +
    + +
    +
    +
    + +

    Booking request successful

    +
    + + Go to my bookings + +
    +
    +
    +
    +
    + + + +
    +

    What's next

    +

    + Your booking has been sent to the host and will be reviewed within 2 hours. If you don’t hear form the host withing 2h you can cancel your booking or reach out to them via{" "} + Messages.
    + Note: Payment is only after the host accepts your booking +

    +
    +
    +
    +
  • Booking Time
  • +
    +
    +

    {monthsMapping[new Date(booking.booking_start_time).getMonth()] ?? "N/A"}

    + {new Date(booking.booking_start_time).getDate() || "N/A"} +

    {daysMapping[new Date(booking.booking_start_time).getDay()] ?? "N/A"}

    +
    +
    +
    + +

    From

    + {bookingData.from} +
    +
    + +

    Until

    + {bookingData.to} +
    +
    +
    +
  • Space
  • +
    +
    + +
    +
    +
    +

    {booking.property_name ?? bookingData.name}

    +

    {propertySpace.city}

    +

    {propertySpace.country}

    +
    +

    + from: ${booking.hourly_rate}/day +

    +
    + + {propertySpace.max_capacity} +
    +
    +
    +
    +

    + + {(Number(propertySpace.average_space_rating) || 0).toFixed(1)} +

    + +
    +
    +
    +
  • Add-ons:
  • + + {/*
      + {(booking.add_ons ?? []).map((addon, idx) => ( +
    • + + {" "} +
      + {addon.name} +
      {" "} +
      {" "} +
    • + ))} +
    */} +
      + {(booking.add_ons ?? []).map((addon, idx) => ( +
    • + + {" "} +
      + {addon.name} +
      {" "} +
      {" "} +
    • + ))} +
    +
    +
    +
    +

    Charges

    +

    (You will not be charged until the host accepts your booking)

    +
    +

    Rate

    +

    ${booking.hourly_rate}

    +
    +
    +

    Price

    +

    ${(booking.hourly_rate ?? 0) * ((booking.duration ?? 0) / 3600)}

    +
    + {(booking.add_ons ?? []).map((addon) => ( +
    +

    {addon.name}

    +

    ${addon.cost}

    +
    + ))} + +
    +

    Tax

    +

    ${booking.tax}

    +
    +
    +

    Total

    +

    ${booking.total}

    +
    +
    + + Cancellation policy + +
    +
    + setShowMap(false)} + /> +
    + ); +}; + +export default BookingConfirmationPage; diff --git a/src/pages/Common/Booking/BookingPreviewPage.jsx b/src/pages/Common/Booking/BookingPreviewPage.jsx new file mode 100644 index 0000000..86ef752 --- /dev/null +++ b/src/pages/Common/Booking/BookingPreviewPage.jsx @@ -0,0 +1,449 @@ +import { useStripe } from "@stripe/react-stripe-js"; +import moment from "moment"; +import React, { useState, useContext, useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { Navigate, useNavigate, useParams } from "react-router"; +import AddIcon from "@/components/frontend/icons/AddIcon"; +import DateTimeIcon from "@/components/frontend/icons/DateTimeIcon"; +import Icon from "@/components/Icons"; +import MkdSDK from "@/utils/MkdSDK"; +import { useBookingContext } from "./bookingContext"; +import { formatDate, getDuration } from "@/utils/date-time-utils"; +import { GlobalContext, showToast } from "@/globalContext"; +import { FavoriteButton, LoadingButton, AddOnCounter } from "@/components/frontend"; +import { usePropertyAddons, useTaxAndCommission, useCards } from "@/hooks/api"; +import { Link } from "react-router-dom"; +import { parseJsonSafely, sleep } from "@/utils/utils"; +import MultipleBookingErrorModal from "./MultipleBookingErrorModal"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import { loadStripe } from "@stripe/stripe-js"; +import SelectExistingCardsModal from "@/pages/Customer/Bookings/SelectExistingCardsModal"; + + + +const cardIcons = { + MasterCard: "/mastercard.jpg", + Visa: "/visa.jpg", + "American Express": "/american-express.png", + Discover: "/discover.png", +}; +const sdk = new MkdSDK(); +const ctrl = new AbortController(); +const BOOKING_ERRORS = { + ERR_MULTIPLE_BOOKING: "You already have a pending booking for this slot!", +}; + +const BookingPreviewPage = () => { + localStorage.removeItem("paying"); + const { bookingData, dispatch } = useBookingContext(); + const { dispatch: authDispatch } = useContext(AuthContext); + const { id } = useParams(); + const navigate = useNavigate(); + const [paymentOptions, setPaymentOptions] = useState(false); + const bookingDetails = bookingData?.from !== "" ? bookingData : JSON.parse(localStorage.getItem("booking_details")); + + const { register, watch } = useForm(); + const selectedAddons = watch(); + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const [loading, setLoading] = useState(false); + const { cards } = useCards({ loader: false }); + const [errMultipleBooking, setErrMultipleBooking] = useState(false); + const [paying, setPaying] = useState(false); + const [existingCardsModal, setExistingCardsModal] = useState(localStorage.getItem("paying") ? true : false); + const [paymentMethod, setPaymentMethod] = useState(); + const [confirmPayment, setConfirmPayment] = useState(false); + const [clientSecret, setClientSecret] = useState(undefined); + + + const stripePromise = loadStripe(import.meta.env.VITE_REACT_STRIPE_PUBLIC_KEY); + if (id != bookingDetails.id) { + return ; + } + + // const handleBooking = async () => { + // setLoading(true); + // const dateFormat = moment(bookingDetails.selectedDate).format("MM/DD/YY"); + // const user_id = localStorage.getItem("user"); + + // try { + // const result = await sdk.callRawAPI( + // "/v2/api/custom/ergo/booking/POST", + // { + // booked_unit: 1, + // booking_start_time: new Date(dateFormat + " " + bookingDetails.from).toISOString(), + // booking_end_time: new Date(dateFormat + " " + bookingDetails.to).toISOString(), + // commission_rate: Number(commission), + // customer_id: Number(user_id), + // duration: getDuration(bookingDetails.from, bookingDetails.to) * 3600, + // host_id: bookingDetails.host_id, + // payment_method: "0", + // payment_status: 0, + // property_space_id: Number(id), + // status: 0, + // num_guests: bookingDetails.num_guests - 1, + // tax_rate: Number(tax), + // }, + // "POST", + // ctrl.signal, + // ); + // // create booking addons + + // for (const [k, v] of Object.entries(selectedAddons)) { + // const property_add_on_id = document.querySelector(`input[name='${k}']`)?.getAttribute("id").replace("cb", ""); + // if (!property_add_on_id || !v) continue; + // sdk.setTable("booking_addons"); + // await sdk.callRestAPI({ booking_id: result.message, property_add_on_id: Number(property_add_on_id) }, "POST"); + // } + // sendEmailAlert(bookingDetails.host_id, bookingDetails.name, bookingDetails.id); + // dispatch({ type: "SET_BOOKING_ID", payload: result.message }); + // // navigate(`/property/${id}/booking-confirmation`); + // navigate(`/account/my-bookings/${result.message}`) + + // } catch (err) { + // tokenExpireError(authDispatch, err.message); + // if (err.name == "AbortError") { + // setLoading(false); + // return; + // } + // await handleBookingErrors(err); + // } + // setLoading(false); + // }; + + // async function createPaymentIntent() { + // try { + // setPaymentMethod(result) + // } catch (err) { + // tokenExpireError(dispatch, err.message); + // globalDispatch({ type: "SHOW_ERROR", payload: { heading: "Failed to create payment intent", message: err.message } }); + // } + // } + + + const stripe = useStripe() + const [paymentRequest, setPaymentRequest] = useState(null) + + useEffect(() => { + (async () => { + if (stripe) { + const pr = stripe.paymentRequest({ + country: 'US', + currency: 'usd', + total: { + label: 'Booking total', + amount: Number(total_additional_guest_rate + total_rate + addon_cost), + }, + requestPayerName: true, + requestPayerEmail: true, + }); + + pr.canMakePayment().then(result => { + if (result) { + console.log("result", result) + setPaymentRequest(pr); + } + }); + } + })(); + }, [stripe]) + + if (paymentRequest) { + paymentRequest.on('paymentmethod', async (ev) => { + const { paymentIntent, error: confirmError } = await stripe.confirmCardPayment( + clientSecret, + { payment_method: ev.paymentMethod.id }, + { handleActions: false } + ); + + if (confirmError) { + ev.complete('fail'); + } + else { + ev.complete('success') + if (paymentIntent.status === "requires_action") { + const { error } = await stripe.confirmCardPayment(clientSecret); + if (error) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Payment failed", + message: error.message, + }, + }); + } else { + await fetchBooking(id) + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Payment success", + message: "Your payment was successful", + btn: "Ok got it", + }, + }); + } + } else { + await fetchBooking(id); + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Payment success", + message: "Your payment was successful", + btn: "Ok got it", + }, + }); + } + } + }); + } + + const makePayment = () => { + if (cards.length > 0) { + setExistingCardsModal(true) + } else { + showToast(globalDispatch, "Please add cards in your billing page", 5000, "ERROR") + } + } + + async function handleBookingErrors(err) { + switch (err.message) { + case BOOKING_ERRORS.ERR_MULTIPLE_BOOKING: + setErrMultipleBooking(true); + break; + default: + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Booking Failed", + message: err.message, + }, + }); + } + } + + async function sendEmailAlert(to, property_name, booking_id) { + try { + // get receiver preferences + const result = await sdk.callRawAPI("/v2/api/custom/ergo/get-user", { id: to }, "POST"); + + if (parseJsonSafely(result.settings, {}).email_on_space_booked == true) { + let customer_name = globalState.user.first_name + " " + globalState.user.last_name; + // get email template + const tmpl = await sdk.getEmailTemplate("space-booked-alert"); + const body = tmpl.html?.replace(new RegExp("{{{customer_name}}}", "g"), customer_name) + .replace(new RegExp("{{{property_name}}}", "g"), property_name) + .replace(new RegExp("{{{booking_id}}}", "g"), booking_id); + + // send email + await sdk.sendEmail(result.email, tmpl.subject, body); + } + } catch (err) { + console.log("ERROR", err); + } + } + + + const { tax, commission } = useTaxAndCommission(); + const addons = usePropertyAddons(bookingDetails.property_id); + + const total_rate = bookingDetails.rate * getDuration(bookingDetails.from, bookingDetails.to); + const total_additional_guest_rate = bookingDetails.additional_guest_rate * getDuration(bookingDetails.from, bookingDetails.to) * (bookingDetails.num_guests - 1); + const addon_cost = Number( + Object.entries(selectedAddons) + .map(([k, v]) => v) + .reduce((acc, price) => { + return Number(acc) + (Number(price) ?? 0); + }, 0), + ); + + return ( +
    + +
    +
    +

    Review and payment

    +
    +
    + +
    +
    +

    {bookingDetails.name}

    +
    +
    +
    +
    + +

    Date & time

    +
    + +
    +
    +

    Date

    +

    {formatDate(bookingDetails.selectedDate)}

    +
    +
    +

    Time

    +

    + {bookingDetails.from} - {bookingDetails.to} +

    +
    +
    +

    Duration

    +

    {getDuration(bookingDetails.from, bookingDetails.to) + " hours"}

    +
    +
    + +

    Add Ons

    +
    + {addons.map((addon) => ( + + ))} +
    +
    +
    +

    Booking summary

    + +
    +
    +
    +

    Rate

    +

    ${bookingDetails.rate.toFixed(2)}/h

    +
    +
    +

    Price

    +

    ${total_rate.toFixed(2)}

    +
    + {bookingDetails.additional_guest_rate && bookingDetails.num_guests - 1 ? ( +
    +

    Extra guests

    +

    ${total_additional_guest_rate.toFixed(2)}

    +
    + ) : null} + {Object.entries(selectedAddons).map(([addon_name, price], idx) => { + if (!price) return null; + return ( +
    +

    {addon_name}

    +

    ${Number(price).toFixed(2)}

    +
    + ); + })} +
    +

    Tax

    +

    ${((((total_additional_guest_rate + total_rate) * (bookingDetails?.tax ?? tax)) / 100)).toFixed(2)}

    +
    + {/*
    +

    Commission

    +

    ${(((total_additional_guest_rate + total_rate + addon_cost) * commission) / 100).toFixed(2)}

    +
    */} +
    +

    Total

    +

    ${(total_additional_guest_rate + total_rate + addon_cost + (((total_additional_guest_rate + total_rate) * (bookingDetails?.tax ?? tax)) / 100)).toFixed(2)}

    +
    +
    + {/* {!(tax == null || commission == null) && ( + + )} */} + + makePayment()} + + disabled={(tax == null ?? bookingDetails?.tax) || commission == null} + > + Make Payment + + { + !clientSecret && paymentOptions && ( +

    Loading...

    + ) + } + {/* {() && ( +
    +
    + {cards.length > 0 && ( + + )} + + +
    +
    + )} */} +

    (funds will be put on hold, pending when host accepts/rejects your booking)

    +
    + + Cancellation Policy + +
    +
    + + setExistingCardsModal(false)} + cards={cards} + bookingData={bookingDetails} + selectedAddons={selectedAddons} + paying={paying} + setloadingBtn={setLoading} + setPaying={setPaying} + /> + + setErrMultipleBooking(false)} + spaceId={id} + /> +
    + ); +}; + +export default BookingPreviewPage; diff --git a/src/pages/Common/Booking/MultipleBookingErrorModal.jsx b/src/pages/Common/Booking/MultipleBookingErrorModal.jsx new file mode 100644 index 0000000..19b8689 --- /dev/null +++ b/src/pages/Common/Booking/MultipleBookingErrorModal.jsx @@ -0,0 +1,96 @@ +import { BOOKING_STATUS } from "@/utils/constants"; +import MkdSDK from "@/utils/MkdSDK"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment, useEffect, useState } from "react"; +import { Link } from "react-router-dom"; + +export default function MultipleBookingErrorModal({ modalOpen, closeModal, spaceId }) { + const [bookingId, setBookingId] = useState(""); + + async function fetchBooking() { + const sdk = new MkdSDK(); + sdk.setTable("booking"); + try { + const result = await sdk.callRestAPI( + { page: 1, limit: 1, payload: { property_space_id: spaceId, customer_id: +localStorage.getItem("user"), status: BOOKING_STATUS.PENDING }, sortId: "id", direction: "DESC" }, + "PAGINATE", + ); + if (Array.isArray(result.list) && result.list.length > 0) { + setBookingId(result.list[0].id); + } + } catch (err) { + console.log("err", err); + } + } + + useEffect(() => { + fetchBooking(); + }, [spaceId]); + return ( + + + +
    + + +
    +
    + + + + This is a duplicate request + +
    +

    Once your host approves this booking, you will be able to enjoy your reservation!

    +
    + +
    + + + View Status + +
    +
    +
    +
    +
    +
    +
    + ); +} diff --git a/src/pages/Common/Booking/PageWrapper.jsx b/src/pages/Common/Booking/PageWrapper.jsx new file mode 100644 index 0000000..e3583ea --- /dev/null +++ b/src/pages/Common/Booking/PageWrapper.jsx @@ -0,0 +1,14 @@ +import React from "react"; +import { Outlet } from "react-router"; +import { BookingContextProvider } from "./bookingContext"; + +const PageWrapper = () => { + return ( + +
    + +
    +
    + ); +}; +export default PageWrapper; diff --git a/src/pages/Common/Booking/PropertyPage.jsx b/src/pages/Common/Booking/PropertyPage.jsx new file mode 100644 index 0000000..7f04a84 --- /dev/null +++ b/src/pages/Common/Booking/PropertyPage.jsx @@ -0,0 +1,550 @@ +import React, { useContext, useEffect } from "react"; +import { useState } from "react"; +import { Link, Navigate, useLocation, useNavigate, useParams } from "react-router-dom"; +import FaqAccordion from "@/components/frontend/FaqAccordion"; +import ReviewCard from "@/components/frontend/ReviewCard"; + +import StarIcon from "@/components/frontend/icons/StarIcon"; +import MkdSDK from "@/utils/MkdSDK"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import DateTimePicker from "@/components/frontend/DateTimePicker"; +import { useForm } from "react-hook-form"; +import { useBookingContext } from "./bookingContext"; +import CustomSelect from "@/components/frontend/CustomSelect"; +import { GlobalContext } from "@/globalContext"; +import FavoriteButton from "@/components/frontend/FavoriteButton"; +import Counter from "@/components/frontend/Counter"; +import { Tooltip } from "react-tooltip"; +import { usePropertyAddons, usePropertySpace, usePropertySpaceImages, usePublicUserData, usePropertySpaceAmenities, usePropertySpaceFaqs, usePropertySpaceReviews } from "@/hooks/api"; +import PropertyImageSlider from "@/components/frontend/PropertyImageSlider"; +import PropertySpaceMapImage from "@/components/frontend/PropertySpaceMapImage"; +import AllReviewsModal from "@/components/frontend/AllReviewsModal"; +import { AuthContext } from "@/authContext"; +import CircleCheckIcon from "@/components/frontend/icons/CircleCheckIcon"; + +let sdk = new MkdSDK(); + +const PropertyPage = () => { + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const { state: authState, dispatch: authDispatch } = useContext(AuthContext); + const { state: spaceData } = useLocation(); + const { bookingData, dispatch } = useBookingContext(); + const [galleryOpen, setGalleryOpen] = useState(false); + const [reviewsPopup, setReviewsPopup] = useState(false); + const [fetching, setFetching] = useState(true); + const navigate = useNavigate(); + const { id } = useParams(); + const bookingDetails = bookingData?.from === "" ? bookingData : JSON.parse(localStorage.getItem("booking_details")); + const [reviewDirection, setReviewDirection] = useState("DESC"); + const [bookedSlots, setBookedSlots] = useState([]); + const [scheduleTemplate, setScheduleTemplate] = useState({}); + const [render, forceRender] = useState(false); + const { handleSubmit, register, setValue } = useForm({ + defaultValues: bookingDetails, + }); + + const [showCalendar, setShowCalendar] = useState(false); + + const { propertySpace, notFound } = usePropertySpace(id, render); + const hostData = usePublicUserData(propertySpace.host_id); + const spaceImages = usePropertySpaceImages(propertySpace.id, true, setFetching); + const spaceAddons = usePropertyAddons(propertySpace.property_id); + const spaceAmenities = usePropertySpaceAmenities(propertySpace.id); + const faqs = usePropertySpaceFaqs(propertySpace.id); + const reviews = usePropertySpaceReviews(propertySpace.id); + const [showMap, setShowMap] = useState(false); + const { pathname } = useLocation(); + + if (!fetching && spaceImages.length === 0) { + navigate("*") + } + + async function fetchBookedSlots(id) { + try { + const result = await callCustomAPI("customer/schedule", "post", { property_spaces_id: id }, "", null, "v3"); + if (Array.isArray(result.list)) { + setBookedSlots(result.list); + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function fetchScheduleTemplate(id) { + try { + const result = await callCustomAPI( + "property_spaces_schedule_template", + "post", + { + page: 1, + limit: 1, + where: [`property_spaces_id = ${id}`], + }, + "PAGINATE", + ); + if (Array.isArray(result.list) && result.list.length > 0) { + setScheduleTemplate({ custom_slots: result.list[0].custom_slots }); + + } + if (result.list[0]?.schedule_template_id) { + const templateResult = await callCustomAPI( + "schedule_template", + "post", + { + page: 1, + limit: 1, + where: [`id = ${result.list[0].schedule_template_id}`], + }, + "PAGINATE", + ); + if (Array.isArray(templateResult.list) && (templateResult.list[0] ?? {})) { + setScheduleTemplate((prev) => { + let updated = { ...prev, ...templateResult.list[0] }; + return updated; + }); + } + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + function switchToCustomer() { + authDispatch({ type: "SWITCH_TO_CUSTOMER" }); + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Success", + message: `You are now signed in as a customer`, + btn: "Ok got it", + }, + }); + } + + const onSubmit = async (data) => { + if (!authState.isAuthenticated) { + navigate(`/login?redirect_uri=${pathname}`); + return; + } + + if (authState.user == propertySpace.host_id) { + globalDispatch({ type: "SHOW_ERROR", payload: { heading: "error", message: "Owners can't book their own spaces" } }) + return + } + + if (globalState.user.verificationStatus != 1) { + globalDispatch({ type: "OPEN_NOT_VERIFIED_MODAL" }); + return; + } + dispatch({ type: "SET_BOOKING_DETAILS", payload: { ...data, ...propertySpace } }); + navigate("booking-preview"); + }; + + useEffect(() => { + if (isNaN(id)) return; + fetchBookedSlots(id); + fetchScheduleTemplate(id); + + // if (spaceImages.length === 0) { + // navigate("*") + // } + }, []); + + const sortByPostDate = (a, b) => { + if (reviewDirection == "DESC") { + return new Date(b.post_date) - new Date(a.post_date); + } + return new Date(a.post_date) - new Date(b.post_date); + }; + + if (notFound || isNaN(id)) return ; + + return ( +
    { + setShowCalendar(false); + }} + > +
    +
    +

    {propertySpace.name ?? spaceData?.name}

    + +
    +
    +

    + + + {(Number(propertySpace.average_space_rating ?? spaceData?.average_space_rating) || 0).toFixed(1)} + ({propertySpace.space_rating_count ?? spaceData?.space_rating_count}) + +

    +
    + + Save +
    +
    +
    +
    + {spaceImages[0]?.photo_url && + + } + {spaceImages[1]?.photo_url && + + } +
    + {spaceImages[2]?.photo_url && + + } + {spaceImages[3]?.photo_url && + + } +
    + {spaceImages[4]?.photo_url && + + } + +
    +
    +
    +

    Description

    +

    {propertySpace.description ?? spaceData?.description}

    +
    +

    Amenities

    +
      + {spaceAmenities.map((am, idx) => ( +
    • + + {am.amenity_name} +
    • + ))} +
    +
    +

    Add ons

    +
      + {spaceAddons.map((addon) => ( +
    • + + {" "} +
      + {addon.add_on_name} +
      {" "} +
      {" "} + ${addon.cost}/h +
    • + ))} +
    +
    +
    +

    About the host

    + {(authState.role == "customer" && propertySpace?.id) && ( + + Contact the host + + )} +
    +
    +
    + +
    +
    +

    {propertySpace.first_name}

    +

    {propertySpace.about ?? spaceData?.about}

    +
    + {(authState.role == "customer" && propertySpace?.id) && ( + + Contact the host + + )} +
    +

    {propertySpace.about ?? spaceData?.about}

    + +
    +
    +

    Reviews

    + +
    +
    + {reviews.length == 0 &&

    No reviews yet

    } + {reviews + .sort(sortByPostDate) + .slice(0, 10) + .map((rw) => ( + + ))} +
    + {reviews.length > 10 ? ( + + ) : null} +
    +
    +
    +

    FAQs

    + {faqs.map((faq) => ( + + ))} +
    +

    Property rules

    +

    {propertySpace.rule ?? spaceData?.rule}

    +
    +
    +
    +

    Price and availability

    +
    + Max capacity + + {" "} + {propertySpace.max_capacity ?? spaceData?.max_capacity} people + +
    +
    + Pricing from + + from: ${propertySpace.rate ?? spaceData?.rate}/h + +
    + +
    +
    + Number of guests + +
    +
    +
    + ({ fromTime: new Date(slot.start_time), toTime: new Date(slot.end_time) }))} + scheduleTemplate={scheduleTemplate} + defaultDate={bookingDetails.selectedDate || undefined} + /> +
    + {authState.role != "customer" && authState.isAuthenticated ? ( + + ) : ( + + )} +
    +
    +
    +
    +

    Price and availability

    +
    + Max capacity + + {" "} + {propertySpace.max_capacity ?? spaceData?.max_capacity} people + +
    +
    + Pricing from + + from: ${propertySpace.rate ?? spaceData?.rate}/h + +
    + +
    +
    + Number of guests + +
    +
    +
    + ({ fromTime: new Date(slot.start_time), toTime: new Date(slot.end_time) }))} + scheduleTemplate={scheduleTemplate} + /> +
    + {authState.role != "customer" && authState.isAuthenticated ? ( + + ) : ( + + )} +
    +
    +
    + setGalleryOpen(false)} + /> + setReviewsPopup(false)} + reviews={reviews} + onDirectionChange={setReviewDirection} + /> + + + setShowMap(false)} + /> +
    + ); +}; + +export default PropertyPage; diff --git a/src/pages/Common/Booking/bookingContext.jsx b/src/pages/Common/Booking/bookingContext.jsx new file mode 100644 index 0000000..e46b444 --- /dev/null +++ b/src/pages/Common/Booking/bookingContext.jsx @@ -0,0 +1,39 @@ +import React, { createContext, useContext, useReducer } from "react"; + +const initialBookingData = { + from: "", + to: "", + selectedDate: "", + num_guests: 0, +}; + + +// localStorage.setItem("booking_details", JSON.stringify(initialBookingData)); + +const reducer = (state, action) => { + switch (action.type) { + case "SET_BOOKING_DETAILS": + localStorage.setItem("booking_details", JSON.stringify(action.payload)); + localStorage.setItem("booking_id", action.payload.id); + return { ...state, ...action.payload }; + case "SET_BOOKING_ID": + return { ...state, id: action.payload }; + default: + return state; + } +}; + +// create context here +const bookingContext = createContext({}); + +// wrap this component around App.tsx to get access to userData in all components +const BookingContextProvider = ({ children }) => { + const [bookingData, dispatch] = useReducer(reducer, initialBookingData); + + return {children}; +}; + +// use this custom hook to get the data in any component in component tree +const useBookingContext = () => useContext(bookingContext); + +export { useBookingContext, BookingContextProvider }; diff --git a/src/pages/Common/Booking/formattedBooking,jsx b/src/pages/Common/Booking/formattedBooking,jsx new file mode 100644 index 0000000..9fe4172 --- /dev/null +++ b/src/pages/Common/Booking/formattedBooking,jsx @@ -0,0 +1,441 @@ +import { useStripe } from "@stripe/react-stripe-js"; +import moment from "moment"; +import React, { useState, useContext, useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { Navigate, useNavigate, useParams } from "react-router"; +import AddIcon from "@/components/frontend/icons/AddIcon"; +import DateTimeIcon from "@/components/frontend/icons/DateTimeIcon"; +import Icon from "@/components/Icons"; +import MkdSDK from "@/utils/MkdSDK"; +import { useBookingContext } from "./bookingContext"; +import { formatDate, getDuration } from "@/utils/date-time-utils"; +import { GlobalContext, showToast } from "@/globalContext"; +import { FavoriteButton, LoadingButton, AddOnCounter } from "@/components/frontend"; +import { usePropertyAddons, useTaxAndCommission, useCards } from "@/hooks/api"; +import { Link } from "react-router-dom"; +import { parseJsonSafely, sleep } from "@/utils/utils"; +import MultipleBookingErrorModal from "./MultipleBookingErrorModal"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import { loadStripe } from "@stripe/stripe-js"; +import SelectExistingCardsModal from "@/pages/Customer/Bookings/SelectExistingCardsModal"; + + + +const cardIcons = { + MasterCard: "/mastercard.jpg", + Visa: "/visa.jpg", + "American Express": "/american-express.png", + Discover: "/discover.png", +}; +const sdk = new MkdSDK(); +const ctrl = new AbortController(); +const BOOKING_ERRORS = { + ERR_MULTIPLE_BOOKING: "You already have a pending booking for this slot!", +}; + +const BookingPreviewPage = () => { + const { bookingData, dispatch } = useBookingContext(); + const { dispatch: authDispatch } = useContext(AuthContext); + const { id } = useParams(); + const navigate = useNavigate(); + const [paymentOptions, setPaymentOptions] = useState(false); + + const { register, watch } = useForm(); + const selectedAddons = watch(); + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const [loading, setLoading] = useState(false); + const { cards } = useCards({ loader: false }); + const [errMultipleBooking, setErrMultipleBooking] = useState(false); + const [existingCardsModal, setExistingCardsModal] = useState(false); + const [paymentMethod, setPaymentMethod] = useState(); + const [confirmPayment, setConfirmPayment] = useState(false); + const [clientSecret, setClientSecret] = useState(undefined); + + + const stripePromise = loadStripe(import.meta.env.VITE_REACT_STRIPE_PUBLIC_KEY); + + + const handleBooking = async () => { + setLoading(true); + const dateFormat = moment(bookingData.selectedDate).format("MM/DD/YY"); + const user_id = localStorage.getItem("user"); + + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/booking/POST", + { + booked_unit: 1, + booking_start_time: new Date(dateFormat + " " + bookingData.from).toISOString(), + booking_end_time: new Date(dateFormat + " " + bookingData.to).toISOString(), + commission_rate: Number(commission), + customer_id: Number(user_id), + duration: getDuration(bookingData.from, bookingData.to) * 3600, + host_id: bookingData.host_id, + payment_method: "0", + payment_status: 0, + property_space_id: Number(id), + status: 0, + num_guests: bookingData.num_guests - 1, + tax_rate: Number(tax), + }, + "POST", + ctrl.signal, + ); + // create booking addons + + for (const [k, v] of Object.entries(selectedAddons)) { + const property_add_on_id = document.querySelector(`input[name='${k}']`)?.getAttribute("id").replace("cb", ""); + if (!property_add_on_id || !v) continue; + sdk.setTable("booking_addons"); + await sdk.callRestAPI({ booking_id: result.message, property_add_on_id: Number(property_add_on_id) }, "POST"); + } + sendEmailAlert(bookingData.host_id, bookingData.name, bookingData.id); + dispatch({ type: "SET_BOOKING_ID", payload: result.message }); + // navigate(`/property/${id}/booking-confirmation`); + navigate(`/account/my-bookings/${result.message}`) + + } catch (err) { + tokenExpireError(authDispatch, err.message); + if (err.name == "AbortError") { + setLoading(false); + return; + } + await handleBookingErrors(err); + } + setLoading(false); + }; + + async function createPaymentIntent() { + try { + setPaymentMethod(result) + } catch (err) { + tokenExpireError(dispatch, err.message); + globalDispatch({ type: "SHOW_ERROR", payload: { heading: "Failed to create payment intent", message: err.message } }); + } + } + + const stripe = useStripe() + const [paymentRequest, setPaymentRequest] = useState(null) + const { tax, commission } = useTaxAndCommission(); + const addons = usePropertyAddons(bookingData.property_id); + + const total_rate = bookingData.rate * getDuration(bookingData.from, bookingData.to); + const total_additional_guest_rate = bookingData.additional_guest_rate * getDuration(bookingData.from, bookingData.to) * (bookingData.num_guests - 1); + const addon_cost = Number( + Object.entries(selectedAddons) + .map(([k, v]) => v) + .reduce((acc, price) => { + return Number(acc) + (Number(price) ?? 0); + }, 0), + ); + + useEffect(() => { + (async () => { + if (stripe) { + const pr = stripe.paymentRequest({ + country: 'US', + currency: 'usd', + total: { + label: 'Booking total', + amount: parseFloat(((total_additional_guest_rate + total_rate + addon_cost) + (((total_additional_guest_rate + total_rate + addon_cost) * (bookingData?.tax ?? tax)) / 100) + (((total_additional_guest_rate + total_rate + addon_cost) * commission) / 100)).toString().slice(0, -1)), + }, + requestPayerName: true, + requestPayerEmail: true, + }); + pr.canMakePayment().then(result => { + if (result) { + console.log("result", result) + setPaymentRequest(pr); + } + }); + } + })(); + }, [stripe]) + + if (paymentRequest) { + paymentRequest.on('paymentmethod', async (ev) => { + const { paymentIntent, error: confirmError } = await stripe.confirmCardPayment( + clientSecret, + { payment_method: ev.paymentMethod.id }, + { handleActions: false } + ); + + if (confirmError) { + ev.complete('fail'); + } + else { + ev.complete('success') + if (paymentIntent.status === "requires_action") { + const { error } = await stripe.confirmCardPayment(clientSecret); + if (error) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Payment failed", + message: error.message, + }, + }); + } else { + await fetchBooking(id) + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Payment success", + message: "Your payment was successful", + btn: "Ok got it", + }, + }); + } + } else { + await fetchBooking(id); + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Payment success", + message: "Your payment was successful", + btn: "Ok got it", + }, + }); + } + } + }); + } + + const makePayment = () => { + if (cards.length > 0) { + setExistingCardsModal(true) + } else { + showToast(globalDispatch, "Please add cards in your billing page", 5000, "ERROR") + } + } + + async function handleBookingErrors(err) { + switch (err.message) { + case BOOKING_ERRORS.ERR_MULTIPLE_BOOKING: + setErrMultipleBooking(true); + break; + default: + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Booking Failed", + message: err.message, + }, + }); + } + } + + async function sendEmailAlert(to, property_name, booking_id) { + try { + // get receiver preferences + const result = await sdk.callRawAPI("/v2/api/custom/ergo/get-user", { id: to }, "POST"); + + if (parseJsonSafely(result.settings, {}).email_on_space_booked == true) { + let customer_name = globalState.user.first_name + " " + globalState.user.last_name; + // get email template + const tmpl = await sdk.getEmailTemplate("space-booked-alert"); + const body = tmpl.html?.replace(new RegExp("{{{customer_name}}}", "g"), customer_name) + .replace(new RegExp("{{{property_name}}}", "g"), property_name) + .replace(new RegExp("{{{booking_id}}}", "g"), booking_id); + + // send email + await sdk.sendEmail(result.email, tmpl.subject, body); + } + } catch (err) { + console.log("ERROR", err); + } + } + + if (id != bookingData.id) { + return ; + } + + return ( +
    + +
    +
    +

    Review and payment

    +
    +
    + +
    +
    +

    {bookingData.name}

    +
    +
    +
    +
    + +

    Date & time

    +
    + +
    +
    +

    Date

    +

    {formatDate(bookingData.selectedDate)}

    +
    +
    +

    Time

    +

    + {bookingData.from} - {bookingData.to} +

    +
    +
    +

    Duration

    +

    {getDuration(bookingData.from, bookingData.to) + " hours"}

    +
    +
    + +

    Add Ons

    +
    + {addons.map((addon) => ( + + ))} +
    +
    +
    +

    Booking summary

    + +
    +
    +
    +

    Rate

    +

    ${bookingData.rate.toFixed(2)}/h

    +
    +
    +

    Price

    +

    ${total_rate.toFixed(2)}

    +
    + {bookingData.additional_guest_rate && bookingData.num_guests - 1 ? ( +
    +

    Extra guests

    +

    ${total_additional_guest_rate.toFixed(2)}

    +
    + ) : null} + {Object.entries(selectedAddons).map(([addon_name, price], idx) => { + if (!price) return null; + return ( +
    +

    {addon_name}

    +

    ${Number(price).toFixed(2)}

    +
    + ); + })} +
    +

    Tax

    +

    ${(((total_additional_guest_rate + total_rate + addon_cost) * (bookingData?.tax ?? tax)) / 100).toFixed(2)}

    +
    +
    +

    Commission

    +

    ${(((total_additional_guest_rate + total_rate + addon_cost) * commission) / 100).toFixed(2)}

    +
    +
    +

    Total

    +

    ${((total_additional_guest_rate + total_rate + addon_cost) + (((total_additional_guest_rate + total_rate + addon_cost) * (bookingData?.tax ?? tax)) / 100) + (((total_additional_guest_rate + total_rate + addon_cost) * commission) / 100)).toFixed(2)}

    +
    +
    + {/* {!(tax == null || commission == null) && ( + + )} */} + + makePayment()} + + disabled={(tax === null ?? bookingData?.tax) || commission === null} + > + Make Payment + + { + !clientSecret && paymentOptions && ( +

    Loading...

    + ) + } + {/* {() && ( +
    +
    + {cards.length > 0 && ( + + )} + + +
    +
    + )} */} +

    (funds will be put on hold, pending when host accepts/rejects your booking)

    +
    + + Cancellation Policy + +
    +
    + + setExistingCardsModal(false)} + cards={cards} + bookingData={bookingData} + selectedAddons={selectedAddons} + /> + + setErrMultipleBooking(false)} + spaceId={id} + /> +
    + ); +}; + +export default BookingPreviewPage; diff --git a/src/pages/Common/CancelationPolicyPage.jsx b/src/pages/Common/CancelationPolicyPage.jsx new file mode 100644 index 0000000..2a3e95b --- /dev/null +++ b/src/pages/Common/CancelationPolicyPage.jsx @@ -0,0 +1,48 @@ +import { GlobalContext } from "@/globalContext"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import MkdSDK from "@/utils/MkdSDK"; +import React, { useState } from "react"; +import { useContext } from "react"; +import { useEffect } from "react"; + +export default function CancellationPolicyPage() { + const [content, setContent] = useState(""); + const { dispatch: globalDispatch } = useContext(GlobalContext); + + async function fetchCancellationPolicy() { + globalDispatch({ type: "START_LOADING" }); + const sdk = new MkdSDK(); + sdk.setTable("cms"); + try { + const result = await callCustomAPI("cms", "post", { payload: { content_key: "cancellation_policy" }, limit: 1000, page: 1 }, "PAGINATE"); + + if (Array.isArray(result.list) && result.list.length > 0) { + setContent(result.list.find((stg) => stg.content_key == "cancellation_policy")?.content_value); + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Cannot get Cancellation policy", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + useEffect(() => { + fetchCancellationPolicy(); + }, []); + + return ( +
    +
    +
    +
    +
    + ); +} diff --git a/src/pages/Common/CheckDeleteEmailPage.jsx b/src/pages/Common/CheckDeleteEmailPage.jsx new file mode 100644 index 0000000..0d11657 --- /dev/null +++ b/src/pages/Common/CheckDeleteEmailPage.jsx @@ -0,0 +1,14 @@ +import React, { useEffect } from "react"; + +export default function CheckDeleteEmailPage() { + + useEffect(() => { + localStorage.clear(); + }, []) + + return ( +
    +

    You have requested to delete your account. Please check your email to confirm this operation

    +
    + ); +} diff --git a/src/pages/Common/ConfirmDeletePage.jsx b/src/pages/Common/ConfirmDeletePage.jsx new file mode 100644 index 0000000..5ab566f --- /dev/null +++ b/src/pages/Common/ConfirmDeletePage.jsx @@ -0,0 +1,33 @@ +import { GlobalContext } from "@/globalContext"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import React, { useContext, useEffect, useState } from "react"; +import { Navigate, useSearchParams } from "react-router-dom"; + +export default function ConfirmDeletePage() { + const [searchParams] = useSearchParams(); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const [pageText, setPageText] = useState("Deleting your account"); + + async function deleteAccount() { + globalDispatch({ type: "START_LOADING" }); + try { + const result = await callCustomAPI("delete-account", "post", { token: searchParams.get("token") }, ""); + setPageText("Your account has been deleted"); + localStorage.clear(); + } catch (err) { + globalDispatch({ type: "SHOW_ERROR", payload: { heading: "Operation failed", message: err.message } }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + useEffect(() => { + deleteAccount(); + }, []); + + if (!searchParams.get("token")) return ; + return ( +
    +

    {pageText}

    +
    + ); +} diff --git a/src/pages/Common/ContactUsPage.jsx b/src/pages/Common/ContactUsPage.jsx new file mode 100644 index 0000000..40c8341 --- /dev/null +++ b/src/pages/Common/ContactUsPage.jsx @@ -0,0 +1,112 @@ +import React from "react"; +import { useContext } from "react"; +import { useForm } from "react-hook-form"; +import { Link } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import CustomSelect from "@/components/frontend/CustomSelect"; + +const ContactUsPage = () => { + const { handleSubmit, register, reset, setValue } = useForm(); + let sdk = new MkdSDK(); + const { dispatch: globalDispatch } = useContext(GlobalContext); + + const onSubmit = async (data) => { + console.log("submitting", data); + globalDispatch({ type: "START_LOADING" }); + try { + const tmpl = await sdk.getEmailTemplate("contact"); + const body = tmpl.html?.replace(new RegExp("{{{name}}}", "g"), data.name)?.replace(new RegExp("{{{email}}}", "g"), data.email)?.replace(new RegExp("{{{message}}}", "g"), data.message); + + await sdk.sendEmail(data.email, tmpl.subject, body); + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Email Sent", + message: "Email has been sent, we will get back to you shortly", + btn: "Ok got it", + }, + }); + } catch (err) { + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + reset(); + globalDispatch({ type: "STOP_LOADING" }); + }; + + return ( + <> +
    +

    Contact Us

    +
    +
    +

    We are here to help. Copy to be provided

    +
    +
    + FAQs - Frequently Asked Questions +

    Read most common questions others have.

    +
    + + Visit FAQs + +
    +

    Feel free to reach out to our team. We usually reply within 24 hours.

    +
    +
    + + + + + + +
    + +
    +
    +
    + + ); +}; + +export default ContactUsPage; diff --git a/src/pages/Common/ExplorePage.jsx b/src/pages/Common/ExplorePage.jsx new file mode 100644 index 0000000..dda6f61 --- /dev/null +++ b/src/pages/Common/ExplorePage.jsx @@ -0,0 +1,626 @@ +import React, { useEffect, useState } from "react"; +import PropertySpaceCard from "@/components/frontend/PropertySpaceCard"; +import { useForm } from "react-hook-form"; +import { useSearchParams } from "react-router-dom"; +import InfiniteScroll from "react-infinite-scroll-component"; +import NoteIcon from "@/components/frontend/icons/NoteIcon"; +import { isValidDate, parseSearchParams } from "@/utils/utils"; +import { useContext } from "react"; +import { GlobalContext } from "@/globalContext"; +import HostCardSlider from "@/components/frontend/HostCardSlider"; +import CustomSelectV2 from "@/components/CustomSelectV2"; +import CustomLocationAutoCompleteV2 from "@/components/CustomLocationAutoCompleteV2"; +import DatePickerV3 from "@/components/DatePickerV3"; +import { DRAFT_STATUS, SPACE_STATUS, SPACE_VISIBILITY } from "@/utils/constants"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import PropertySpaceFiltersModal from "@/components/PropertySpaceFiltersModal"; +import { AdjustmentsHorizontalIcon } from "@heroicons/react/24/solid"; + +const prices = [ + { + label: "All Prices", + value: "", + }, + { + label: "$0 - $30", + value: "$0 - $30", + }, + { + label: "$31 - $60", + value: "$31 - $60", + }, + { + label: "$60 - $90", + value: "$60 - $90", + }, + { + label: "$90 - $120", + value: "$90 - $120", + }, + { + label: "$120 - $150", + value: "$120 - $150", + }, + { + label: "$150 - $180", + value: "$150 - $180", + }, +]; + +const sdk = new MkdSDK(); + +const ExplorePage = () => { + const FETCH_PER_SCROLL = 12; + const [searchParams, setSearchParams] = useSearchParams(); + const section = searchParams.get("section") ?? "all"; + const [hosts, setHosts] = useState([]); + const [popularSpaces, setPopularSpaces] = useState([]); + const [newSpaces, setNewSpaces] = useState([]); + const [showFilter, setShowFilter] = useState(false); + const [forceRender, setForceRender] = useState(""); + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const { dispatch } = useContext(AuthContext); + const [ctrl] = useState(new AbortController()); + + const { handleSubmit, register, watch, reset, setValue, control, formState, resetField } = useForm({ + defaultValues: (() => { + const params = parseSearchParams(searchParams); + return { + location: params.location ?? "", + from: isValidDate(params.from ?? "") ? new Date(params.from) : new Date(), + to: isValidDate(params.to ?? "") ? new Date(params.to) : new Date(), + space_name: params.space_name ?? "", + category: params.category ?? "", + price_range: params.price_range ?? "", + direction: "DESC", + }; + })(), + }); + + const { dirtyFields } = formState; + + const direction = watch("direction"); + const fromDate = watch("from"); + + const [popularTotal, setPopularTotal] = useState(10000); + const [newSpaceTotal, setNewSpaceTotal] = useState(10000); + + async function fetchPopularSpaces(page) { + setPopularSpaces([]); + setPopularSpaces((prev) => { + const amountToFetch = popularTotal - prev.length > FETCH_PER_SCROLL ? FETCH_PER_SCROLL : Math.abs(popularTotal - prev.length - FETCH_PER_SCROLL); + return [...prev, ...Array(amountToFetch).fill({})]; + }); + const data = parseSearchParams(searchParams); + const user_id = localStorage.getItem("user"); + const location = (data.location?.split(",")) + + var from_price, to_price; + if (data.price_range) { + var arr = data.price_range.split("-"); + if (arr.length > 1) { + from_price = arr[0].trim().slice(1); + to_price = arr[1].trim().slice(1); + } + } + + let where = [ + `ergo_property_spaces.space_status = ${SPACE_STATUS.APPROVED} AND ergo_property_spaces_images.is_approved = 1 AND schedule_template_id IS NOT NULL AND ergo_property_spaces.draft_status = ${DRAFT_STATUS.COMPLETED} AND ergo_property_spaces.deleted_at IS NULL`, + ]; + + if (data.category) { + where.push(`ergo_spaces.category = '${data.category}'`); + } + + if (data.space_name) { + where.push(`ergo_property.name LIKE '%${data.space_name}%'`); + } + + if (data.price_range) { + where.push(`ergo_property_spaces.rate BETWEEN ${from_price} AND ${to_price}`); + } + + if (data.location) { + where.push( + `(ergo_property.address_line_1 LIKE '%${data.location}%' OR ergo_property.address_line_2 LIKE '%${data.location}%' OR ergo_property.city LIKE '%${location[0]}%' OR ergo_property.country LIKE '%${location.length === 1 ? location[0] : location.length === 2 ? location[1] : location[2]}%' OR ergo_property.zip LIKE '%${data.location}%' OR ergo_property.name LIKE '%${data.location}%')`, + ); + } + + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/popular/PAGINATE", + { + page: page ?? 1, + limit: FETCH_PER_SCROLL, + user_id: Number(user_id), + where, + booking_start_time: isValidDate(data.from || "") ? new Date(data.from).toISOString() : undefined, + booking_end_time: isValidDate(data.to || "") ? new Date(data.to).toISOString() : undefined, + sortId: direction == "NONE" ? undefined : "id", + direction: direction == "NONE" ? undefined : direction, + }, + "POST", + ctrl.signal, + ); + if (Array.isArray(result.list)) { + setPopularSpaces((prev) => { + return [...prev.filter((item) => Object.keys(item).length > 0), ...result.list].filter((v, i, a) => a.findIndex((v2) => v2.id === v.id) === i); + }); + setPopularTotal(result.total); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function fetchNewSpaces(page) { + setNewSpaces([]); + setNewSpaces((prev) => { + const amountToFetch = newSpaceTotal - prev.length > FETCH_PER_SCROLL ? FETCH_PER_SCROLL : Math.abs(newSpaceTotal - prev.length - FETCH_PER_SCROLL); + return [...prev, ...Array(amountToFetch).fill({})]; + }); + const data = parseSearchParams(searchParams); + + const user_id = localStorage.getItem("user"); + + var from_price, to_price; + if (data.price_range) { + var arr = data.price_range.split("-"); + if (arr.length > 1) { + from_price = arr[0].trim().slice(1); + to_price = arr[1].trim().slice(1); + } + } + + let where = [ + `ergo_property_spaces.space_status = ${SPACE_STATUS.APPROVED} AND ergo_property_spaces.draft_status = ${DRAFT_STATUS.COMPLETED} AND ergo_property_spaces.availability = ${SPACE_VISIBILITY.VISIBLE} AND ergo_property_spaces_images.is_approved = 1`, + ]; + + if (data.category) { + where.push(`ergo_spaces.category = '${data.category}'`); + } + + if (data.space_name) { + where.push(`ergo_property.name LIKE '%${data.space_name}%'`); + } + + if (data.price_range) { + where.push(`ergo_property_spaces.rate BETWEEN ${from_price} AND ${to_price}`); + } + + if (data.location) { + where.push( + `(ergo_property.address_line_1 LIKE '%${location}%' OR ergo_property.address_line_2 LIKE '%${location}%' OR ergo_property.city LIKE '%${location[0] ?? ""}%' OR ergo_property.country LIKE '%${location.length === 1 ? location[0] : location.length === 2 ? location[1] : location[2]}%' OR ergo_property.zip LIKE '%${location}%' OR ergo_property.name LIKE '%${location}%')`, + ); + } + + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/popular/PAGINATE", + { + page: page ?? 1, + limit: FETCH_PER_SCROLL, + user_id: Number(user_id), + where, + sortId: "update_at", + direction: "DESC", + booking_start_time: isValidDate(data.from || "") ? new Date(data.from).toISOString() : undefined, + booking_end_time: isValidDate(data.to || "") ? new Date(data.to).toISOString() : undefined, + }, + "POST", + ctrl.signal, + ); + if (Array.isArray(result.list)) { + setNewSpaces((prev) => { + return [...prev.filter((item) => Object.keys(item).length > 0), ...result.list].filter((v, i, a) => a.findIndex((v2) => v2.id === v.id) === i); + }); + setNewSpaceTotal(result.total); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function fetchHosts() { + const filter = parseSearchParams(searchParams); + const data = parseSearchParams(searchParams); + const location = (data.location?.replace(', undefined', '')?.split(",")) + + const user_id = localStorage.getItem("user"); + + var from_price, to_price; + if (data.price_range) { + var arr = data.price_range.split("-"); + if (arr.length > 1) { + from_price = arr[0].trim().slice(1); + to_price = arr[1].trim().slice(1); + } + } + + let where = []; + where.push('ergo_property.id IS NOT NULL'); + if (data.category) { + where.push(`ergo_spaces.category = '${data.category}'`); + } + + if (data.space_name) { + where.push(`ergo_property.name LIKE '%${data.space_name}%'`); + } + if (data.from) { + where.push(`ergo_user.create_at BETWEEN '${data.from}' AND '${data.to}'`); + } + if (data.price_range) { + where.push(`ergo_property_spaces.rate BETWEEN ${from_price} AND ${to_price}`); + } + if (data.location) { + where.push([ + `(ergo_profile.address_line_1 LIKE '%${data.location}%' OR ergo_profile.address_line_2 LIKE '%${data.location}%' OR ergo_profile.city LIKE '%${location[0]}%' OR ergo_profile.country LIKE '%${location[1]}%' OR ergo_profile.zip LIKE '%${data.location}%')`, + ]); + } + + try { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/top-hosts/PAGINATE", + { + page: 1, + limit: 1000, + sortId: "avg_host_rating", + direction: "DESC", + where, + booking_start_time: isValidDate(data.from || "") ? new Date(data.from).toISOString() : undefined, + booking_end_time: isValidDate(data.to || "") ? new Date(data.to).toISOString() : undefined, + }, "POST", ctrl.signal); + setHosts(result.list); + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + useEffect(() => { + switch (searchParams.get("section")) { + case "popular": + fetchPopularSpaces(); + break; + case "hosts": + fetchHosts(); + break; + case "new-spaces": + fetchNewSpaces(); + break; + default: + fetchHosts(); + fetchPopularSpaces(); + fetchNewSpaces(); + } + }, [searchParams]); + + useEffect(() => { + if (forceRender) { + setPopularSpaces([]); + setNewSpaces([]); + fetchPopularSpaces(); + fetchNewSpaces(); + } + }, [forceRender]); + + useEffect(() => { + return () => { + // TODO: abort this only when component unmounts + // console.log("aborting"); + // ctrl.abort(); + }; + }, []); + + const onSubmit = async (data) => { + if (window.innerWidth < 700) { + setShowFilter(false); + } + + if (data.location.includes("undefined")) { + const parts = inputString.split(","); + const result = parts[0].trim(); + data.location = result; + } + searchParams.set("category", data.category); + searchParams.set("price_range", data.price_range); + searchParams.set("space_name", data.space_name); + searchParams.set("location", data.location); + searchParams.set("from", dirtyFields?.from ? data.from.toISOString() : ""); + searchParams.set("to", dirtyFields?.to ? data.to.toISOString() : ""); + setSearchParams(searchParams); + }; + + const sortByDate = (a, b) => { + if (direction == "NONE") return 0; + if (direction == "DESC") { + return new Date(b.id) - new Date(a.id); + } + return new Date(a.id) - new Date(b.id); + }; + + return ( +
    +
    +
    +
    + + +
    + +
    +
    + {(section == "popular" || section == "all") && ( + + )} + {section == "all" && ( +
    +
    +

    Browse By Category

    +
    +
    + {globalState.spaceCategories.map((tab, idx) => ( + + ))} +
    +
    + )} + + {(section == "hosts" || section == "all") && ( +
    +
    +

    Top rated hosts

    +
    +
    + +
    +
    + )} + {(section == "new-spaces" || section == "all") && ( +
    +
    +

    New Spaces

    +
    + {newSpaces.length == 0 && ( +
    +

    + No spaces found +

    +
    + )} + { + fetchNewSpaces(Math.round(newSpaces.length / FETCH_PER_SCROLL + 1)); + }} + scrollThreshold={1} + hasMore={newSpaces.length < newSpaceTotal} + loader={<>} + endMessage={ +

    + +

    + } + > + { +
    + {newSpaces.sort(sortByDate).map((property, idx) => ( + + ))} + {newSpaces.length < 4 ? ( + <> +
    +
    +
    + + ) : null} +
    + } +
    +
    + )} + setShowFilter(false)} + /> +
    + ); +}; + +export default ExplorePage; diff --git a/src/pages/Common/FaqPage.jsx b/src/pages/Common/FaqPage.jsx new file mode 100644 index 0000000..a1671e3 --- /dev/null +++ b/src/pages/Common/FaqPage.jsx @@ -0,0 +1,106 @@ +import React, { useState } from "react"; +import { useContext } from "react"; +import { useEffect } from "react"; +import { Link, useSearchParams } from "react-router-dom"; +import FaqTile from "@/components/frontend/FaqTile"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import { Tab } from "@headlessui/react"; + +const FaqPage = () => { + const [searchParams, setSearchParams] = useSearchParams(); + const [faqs, setFaqs] = useState([]); + const { dispatch: globalDispatch } = useContext(GlobalContext); + + async function fetchFaqs() { + globalDispatch({ type: "START_LOADING" }); + try { + const result = await callCustomAPI( + "faq", + "post", + { + page: 1, + limit: 1000, + where: [`1`], + }, + "PAGINATE", + ); + if (Array.isArray(result.list)) { + setFaqs(result.list); + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + useEffect(() => { + fetchFaqs(); + }, []); + + return ( + <> +
    +

    Frequently asked questions

    +
    +
    +

    Below are some common questions people ask.

    + { + setSearchParams({ tab: v == 0 ? "customers" : "hosts" }); + window.scrollTo({ top: 0, left: 0 }); + }} + defaultIndex={localStorage.getItem("role") == "host" ? 1 : 0 || searchParams.get("tab") == "hosts" ? 1 : 0} + > + + For guests + For hosts +
    +
    + + + {faqs + .filter((faq) => faq.status != 1) + .map((faq) => ( + + ))} + + + {faqs + .filter((faq) => faq.status == 1) + .map((faq) => ( + + ))} + + +
    +

    + If you can’t find your answers we’re here to help.
    +

    + + Contact us + +
    + + ); +}; + +export default FaqPage; diff --git a/src/pages/Common/FavoritesPage.jsx b/src/pages/Common/FavoritesPage.jsx new file mode 100644 index 0000000..aaeb04b --- /dev/null +++ b/src/pages/Common/FavoritesPage.jsx @@ -0,0 +1,298 @@ +import React from "react"; +import { useEffect } from "react"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import PropertySpaceCard from "@/components/frontend/PropertySpaceCard"; +import { useContext } from "react"; +import { GlobalContext } from "@/globalContext"; +import CustomSelectV2 from "@/components/CustomSelectV2"; +import CustomLocationAutoCompleteV2 from "@/components/CustomLocationAutoCompleteV2"; +import DatePickerV3 from "@/components/DatePickerV3"; +import { isValidDate, parseSearchParams } from "@/utils/utils"; +import { DRAFT_STATUS, SPACE_STATUS, SPACE_VISIBILITY } from "@/utils/constants"; +import MkdSDK from "@/utils/MkdSDK"; +import { useSearchParams } from "react-router-dom"; +import PropertySpaceFiltersModal from "@/components/PropertySpaceFiltersModal"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import { AdjustmentsHorizontalIcon } from "@heroicons/react/24/solid"; + +const sdk = new MkdSDK(); +const ctrl = new AbortController(); +const prices = [ + { + label: "All Prices", + value: "", + }, + { + label: "$0 - $30", + value: "$0 - $30", + }, + { + label: "$31 - $60", + value: "$31 - $60", + }, + { + label: "$60 - $90", + value: "$60 - $90", + }, + { + label: "$90 - $120", + value: "$90 - $120", + }, + { + label: "$120 - $150", + value: "$120 - $150", + }, + { + label: "$150 - $180", + value: "$150 - $180", + }, +]; + +const FavoritesPage = () => { + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const { dispatch } = useContext(AuthContext); + const [propertySpaces, setPropertySpaces] = useState(Array(4).fill({})); + const [showFilter, setShowFilter] = useState(false); + const [forceRender, setForceRender] = useState(new Date()); + const [searchParams, setSearchParams] = useSearchParams(); + + const { handleSubmit, register, watch, setValue, control, formState, resetField } = useForm({ + defaultValues: (() => { + const params = parseSearchParams(searchParams); + return { + location: params.location ?? "", + from: isValidDate(params.from ?? "") ? new Date(params.from) : new Date(), + to: isValidDate(params.to ?? "") ? new Date(params.to) : new Date(), + space_name: params.space_name ?? "", + category: params.category ?? "", + price_range: params.price_range ?? "", + direction: "DESC", + }; + })(), + }); + + const { dirtyFields } = formState; + + const direction = watch("direction"); + const fromDate = watch("from"); + + async function fetchPropertySpaces() { + setPropertySpaces(Array(4).fill({})); + const data = parseSearchParams(searchParams); + const user_id = localStorage.getItem("user"); + + var from_price, to_price; + if (data.price_range) { + var arr = data.price_range.split("-"); + if (arr.length > 1) { + from_price = arr[0].trim().slice(1); + to_price = arr[1].trim().slice(1); + } + } + + let where = [ + `ergo_property_spaces.space_status = ${SPACE_STATUS.APPROVED} AND ergo_property_spaces.draft_status = ${DRAFT_STATUS.COMPLETED} AND ergo_property_spaces.availability = ${SPACE_VISIBILITY.VISIBLE} AND ergo_user_property_spaces.user_id = ${user_id} AND ergo_property_spaces.deleted_at IS NULL`, + ]; + + if (data.category) { + where.push(`ergo_spaces.category = '${data.category}'`); + } + + if (data.space_name) { + where.push(`ergo_property.name LIKE '%${data.space_name}%'`); + } + + if (data.price_range) { + where.push(`ergo_property_spaces.rate BETWEEN ${from_price} AND ${to_price}`); + } + + if (data.location) { + where.push( + `(ergo_property.address_line_1 LIKE '%${data.location}%' OR ergo_property.address_line_2 LIKE '%${data.location}%' OR ergo_property.city LIKE '%${data.location}%' OR ergo_property.country LIKE '%${data.location}%' OR ergo_property.zip LIKE '%${data.location}%' OR ergo_property.name LIKE '%${data.location}%')`, + ); + } + + console.log("favorites where ", where); + + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/popular/PAGINATE", + { + page: 1, + limit: 10000, + user_id: Number(user_id), + where, + booking_start_time: isValidDate(data.from || "") ? new Date(data.from).toISOString() : undefined, + booking_end_time: isValidDate(data.to || "") ? new Date(data.to).toISOString() : undefined, + sortId: direction == "NONE" ? undefined : "id", + direction: direction == "NONE" ? undefined : direction, + }, + "POST", + ctrl.signal, + ); + if (Array.isArray(result.list)) { + setPropertySpaces(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + useEffect(() => { + fetchPropertySpaces(); + }, [searchParams, forceRender]); + + const onSubmit = async (data) => { + if (window.innerWidth < 700) { + setShowFilter(false); + } + console.log("submitting ", data); + searchParams.set("category", data.category); + searchParams.set("price_range", data.price_range); + searchParams.set("space_name", data.space_name); + searchParams.set("location", data.location); + searchParams.set("from", dirtyFields?.from ? data.from.toISOString() : ""); + searchParams.set("to", dirtyFields?.to ? data.to.toISOString() : ""); + setSearchParams(searchParams); + }; + + const sortByDate = (a, b) => { + if (direction == "DESC") { + return new Date(b.id) - new Date(a.id); + } + return new Date(a.id) - new Date(b.id); + }; + + return ( + <> +
    +

    My Favorite spaces

    +
    +
    +
    + + +
    + +
    +
    +
    + {propertySpaces.sort(sortByDate).map((property, idx) => ( + + ))} + {propertySpaces.length < 4 ? ( + <> +
    +
    +
    + + ) : null} +
    +
    + setShowFilter(false)} + /> + + ); +}; + +export default FavoritesPage; diff --git a/src/pages/Common/HomePage.jsx b/src/pages/Common/HomePage.jsx new file mode 100644 index 0000000..49c1d2b --- /dev/null +++ b/src/pages/Common/HomePage.jsx @@ -0,0 +1,574 @@ +import React, { useContext, useEffect, useState } from "react"; +import { createSearchParams, Link, useLocation, useNavigate, useSearchParams } from "react-router-dom"; + +import PropertySpaceCard from "@/components/frontend/PropertySpaceCard"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import PeopleIcon from "@/components/frontend/icons/PeopleIcon"; +import TrustedIcon from "@/components/frontend/icons/TrustedIcon"; +import FlexibleIcon from "@/components/frontend/icons/FlexibleIcon"; +import InfiniteScroll from "react-infinite-scroll-component"; +import SearchIcon from "@/components/frontend/icons/SearchIcon"; +import { GlobalContext, showToast } from "@/globalContext"; +import { Tooltip } from "react-tooltip"; +import "react-tooltip/dist/react-tooltip.css"; +import HostCardSlider from "@/components/frontend/HostCardSlider"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import useUserCurrentLocation from "@/hooks/api/useUserCurrentLocation"; +import { DRAFT_STATUS } from "@/utils/constants"; +import { useForm } from "react-hook-form"; +import CustomLocationAutoCompleteV2 from "@/components/CustomLocationAutoCompleteV2"; +import DatePickerV3 from "@/components/DatePickerV3"; +import CustomSelectV2 from "@/components/CustomSelectV2"; +import MkdSDK from "@/utils/MkdSDK"; +import CustomStaticLocationAutoCompleteV2 from "@/components/CustomStaticLocationAutoCompleteV2 "; + +const sdk = new MkdSDK(); +const ctrl = new AbortController(); + +const HomePage = () => { + const FETCH_PER_SCROLL = 12; + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const { state } = useContext(AuthContext); + const [searchParams, setSearchParams] = useSearchParams(); + const [activeTab, setActiveTab] = useState(searchParams.get("category") || "all"); + const [hosts, setHosts] = useState(Array(5).fill({})); + const [popularSpaces, setPopularSpaces] = useState([]); + const [newSpaces, setNewSpaces] = useState([]); + const [forceRender, setForceRender] = useState(""); + const location = useLocation() + const { state: authState, dispatch: authDispatch } = useContext(AuthContext); + + const { dispatch } = useContext(AuthContext); + const [popularTotal, setPopularTotal] = useState(1000); + const [newTotal, setNewTotal] = useState(1000); + const spaceCategories = globalState.spaceCategories; + + const { handleSubmit, control, setValue, resetField, formState, register } = useForm({ + defaultValues: { + booking_start_time: new Date(), + location: globalState.location, + size: "", + }, + }); + + const { touchedFields } = formState; + + const { city, country, done: currentLocationChecked } = useUserCurrentLocation(); + const [noCurrentLocationData, setNoCurrentLocationData] = useState(false); + + const navigate = useNavigate(); + const userRole = localStorage.getItem("role"); + const isLoggedIn = !!localStorage.getItem("token"); + + async function fetchPopularSpaces(page) { + // only add empty spaces if there's no empty card i.e we are not currently fetching + if (popularSpaces.every((space) => Object.keys(space).length > 0)) { + setPopularSpaces((prev) => { + const amountToFetch = popularTotal - prev.length > FETCH_PER_SCROLL ? FETCH_PER_SCROLL : Math.abs(popularTotal - prev.length - FETCH_PER_SCROLL); + return [...prev, ...Array(amountToFetch).fill({})]; + }); + } + const user_id = localStorage.getItem("user"); + const where = [ + `${activeTab != "all" ? `ergo_spaces.category LIKE '%${activeTab}%'` : "1"} AND ergo_property_spaces.space_status = 1 AND ergo_property_spaces.draft_status = ${DRAFT_STATUS.COMPLETED + } AND ergo_property_spaces_images.is_approved = 1 AND ergo_property_spaces.deleted_at IS NULL AND schedule_template_id IS NOT NULL AND (${city && !noCurrentLocationData ? `ergo_property.city LIKE '%${city}%'` : "1"} OR ${country && !noCurrentLocationData ? `ergo_property.country LIKE '%${country}%'` : "1" + })`, + ]; + try { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/popular/PAGINATE", { page: page ?? 1, limit: FETCH_PER_SCROLL, user_id: Number(user_id), where }, "POST", ctrl.signal); + if (Array.isArray(result.list)) { + + setPopularSpaces((prev) => { + return [...prev.filter((item) => Object.keys(item).length > 0), ...result.list].filter((v, i, a) => a.findIndex((v2) => v2.id === v.id) === i); + }); + setPopularTotal(result.total); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function fetchNewSpaces(page) { + if (newSpaces.every((space) => Object.keys(space).length > 0)) { + setNewSpaces((prev) => { + const amountToFetch = newTotal - prev.length > FETCH_PER_SCROLL ? FETCH_PER_SCROLL : Math.abs(newTotal - prev.length - FETCH_PER_SCROLL); + return [...prev, ...Array(amountToFetch).fill({})]; + }); + } + const user_id = localStorage.getItem("user"); + const where = [ + `${activeTab != "all" ? `ergo_spaces.category LIKE '%${activeTab}%'` : "1"} AND ergo_property_spaces.space_status = 1 AND ergo_property_spaces.draft_status = ${DRAFT_STATUS.COMPLETED + } AND ergo_property_spaces_images.is_approved = 1 AND schedule_template_id IS NOT NULL AND ergo_property_spaces.deleted_at IS NULL AND (${city && !noCurrentLocationData ? `ergo_property.city LIKE '%${city}%'` : "1"} OR ${country && !noCurrentLocationData ? `ergo_property.country LIKE '%${country}%'` : "1" + })`, + ]; + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/popular/PAGINATE", + { page: page ?? 1, limit: 6, user_id: Number(user_id), where, sortId: "update_at", direction: "DESC" }, + // { page: page ?? 1, limit: FETCH_PER_SCROLL, user_id: null, where, sortId: "update_at", direction: "DESC" }, + "POST", + ctrl?.signal, + ); + if (Array.isArray(result.list)) { + setNewSpaces((prev) => { + return [...prev.filter((item) => Object.keys(item).length > 0), ...result.list].filter((v, i, a) => a.findIndex((v2) => v2.id === v.id) === i); + }); + setNewTotal(result.total); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function fetchHosts() { + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/top-hosts/PAGINATE", + { + page: 1, + limit: 10, + sortId: "avg_host_rating", + direction: "DESC", + where: [ + `${city && !noCurrentLocationData ? `ergo_profile.city LIKE '%${city}%'` : "1"} AND ${country && !noCurrentLocationData ? `ergo_profile.country LIKE '%${country}%'` : "1"}`, + "ergo_user.deleted_at IS NULL", "ergo_property.id IS NOT NULL", + ], + }, + "POST", + ctrl.signal, + ); + + setHosts(result.list); + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + function onSubmit(data) { + navigate({ + pathname: "/search", + search: createSearchParams({ + location: globalState.location ?? "", + booking_start_time: touchedFields.booking_start_time ? data.booking_start_time.toISOString() : "", + max_capacity: data.max_capacity ?? "", + capacity: data.capacity ?? "", + size: data.size ?? "", + }).toString(), + }); + } + + async function setDevice() { + if (!localStorage.getItem("token") || localStorage.getItem("token") !== undefined) { + return; + } + try { + await sdk.setUUId() + } catch (error) { + console.log(error.message) + } + } + + useEffect(() => { + let setter; + if (!setter) { + setDevice() + } + return () => { + setter = true + } + }, []) + + useEffect(() => { + if (!currentLocationChecked) return; + if (!noCurrentLocationData) { + globalDispatch({ + type: "SETLOCATION", + payload: { + location:(city ?? "") + (city && country ? ", " : "") + (country ?? "") + }, + }) + setValue("location", (city ?? "") + (city && country ? ", " : "") + (country ?? "")); + } + fetchHosts(); + }, [currentLocationChecked, noCurrentLocationData]); + + useEffect(() => { + if (!currentLocationChecked) return; + setPopularSpaces([]); + setNewSpaces([]); + fetchPopularSpaces(); + fetchNewSpaces(); + }, [activeTab, currentLocationChecked, noCurrentLocationData]); + + useEffect(() => { + if (forceRender && currentLocationChecked) { + setPopularSpaces([]); + setNewSpaces([]); + fetchPopularSpaces(); + fetchNewSpaces(); + } + }, [forceRender]); + + function switchToCustomer() { + authDispatch({ type: "SWITCH_TO_CUSTOMER" }); + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Success", + message: `You are now signed in as a customer`, + btn: "Ok got it", + }, + }); + navigate("/signup") + } + + return ( + <> +
    activeTab == cat.category)?.image ?? "/jumbotron1.jpg"}'), linear-gradient(0deg, rgba(16, 24, 40, 0.79), rgba(16, 24, 40, 0.79))`, + }} + className="my-background-image mb-6 pt-[70px] md:rounded-b-[3rem]" + > + +

    Spaces tailored to your needs

    +
    + +
    +

    Top-quality spaces and customer service

    +
    Your number one stop for renting and offering space(s) for work and leisure
    +
    +
    + {spaceCategories.map((cat, idx) => ( +
    + + + {cat.category} + +
    + ))} +
    +
    +
    + +
    +
    +

    Popular

    + + VIEW ALL POPULAR + +
    + {popularSpaces.length < 1 && ( +

    + No Spaces found +

    + )} + { + console.log("calling next", popularSpaces.length / FETCH_PER_SCROLL + 1); + fetchPopularSpaces(Math.round(popularSpaces.length / FETCH_PER_SCROLL + 1)); + }} + scrollThreshold={0.5} + hasMore={popularSpaces.length < popularTotal} + loader={<>} + endMessage={<>} + > + { +
    + {popularSpaces.slice(0, 6).map((property, idx) => ( + + ))} + {popularSpaces.length < 4 ? ( + <> +
    +
    +
    + + ) : null} +
    + } +
    +
    +
    +
    +

    Browse By Category

    + + VIEW ALL CATEGORIES + +
    + +
    + {spaceCategories.slice(0,spaceCategories.length-4).map((tab, idx) => ( + + {tab.category} +

    {tab.category}

    + + ))} +
    +
    +
    +
    +

    Top rated hosts

    + + VIEW ALL HOSTS + +
    + +
    + +
    +
    +

    New Spaces

    + + VIEW ALL NEW SPACES + +
    + {newSpaces.length == 0 && ( +

    + No Spaces found +

    + )} + + { + fetchNewSpaces(Math.round(newSpaces.length / FETCH_PER_SCROLL + 1)); + }} + scrollThreshold={0.9} + hasMore={newSpaces.length < newTotal} + loader={<>} + endMessage={ +

    + +

    + } + > + { +
    + {newSpaces.slice(0, 6).map((property, idx) => ( + + ))} + {newSpaces.length < 4 ? ( + <> +
    +
    +
    + + ) : null} +
    + } +
    +
    + + {(!isLoggedIn || userRole === "customer") && ( +
    +
    +

    Host Your Space Today!

    +

    + Unlock new income opportunities by listing your space on our platform. Join a community of successful hosts, reach thousands of potential guests, and maximize your property's potential. +

    + +
    +
    + Descriptive Alt Text +
    +
    + )} + + + + + + + ); +}; + +export default HomePage; diff --git a/src/pages/Common/Login/LoginPage.jsx b/src/pages/Common/Login/LoginPage.jsx new file mode 100644 index 0000000..1143dcb --- /dev/null +++ b/src/pages/Common/Login/LoginPage.jsx @@ -0,0 +1,428 @@ +import React, { useEffect, useState } from "react"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { useForm } from "react-hook-form"; +import MkdSDK from "@/utils/MkdSDK"; +import { AuthContext } from "@/authContext"; +import { oauthLoginApi } from "@/utils/callCustomAPI"; +import LoadingButton from "@/components/frontend/LoadingButton"; +import Icon from "@/components/Icons"; +import SuggestPasswordChangeModal from "./SuggestPasswordChangeModal"; +import SuggestResendVerificationModal from "./SuggestResendVerificationModal"; +import { GlobalContext, showToast } from "@/globalContext"; +import axios from "axios"; +const sdk = new MkdSDK(); + +export default function LoginPage() { + const [searchParams] = useSearchParams(); + const role = searchParams.get("role") || "customer"; + const [hideDownloadButton, setHideDownloadButton] = useState(false); + const [deferredPrompt, setDeferredPrompt] = useState(null); + + const schema = yup.object({ + email: yup.string().email("Email must be valid").required("Email is required"), + password: yup.string().required("Password is required"), + }); + + const { dispatch: authDispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const [showPassword, setShowPassword] = React.useState(false); + const [suggestPasswordChange, setSuggestPasswordChange] = React.useState(false); + const [suggestResendVerification, setSuggestResendVerification] = React.useState(false); + const [inCorrectPasswordCount, setInCorrectPasswordCount] = React.useState(0); + const [disableEmails, setDisableEmails] = React.useState([]); + const [disableLogin, setDisableLogin] = React.useState(false); + const [locationInfo, setLocationInfo] = useState(null); + const [located, setLocated] = useState(false); + const [latitude, setLatitude] = useState(''); + const [longitude, setLongitude] = useState(''); + + const navigate = useNavigate(); + const { + register, + handleSubmit, + setError, + watch, + formState: { errors, isSubmitting, isDirty }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + email: "", + password: "", + }, + }); + + const email = watch("email"); + + const checkParams = () => { + if (searchParams.get('error')) { + showToast(dispatch, searchParams.get('message'), 5000, "error") + navigate("/login") + } + } + + useEffect(() => { + let setter; + if (!setter) { + checkParams() + } + return () => { + setter = true + } + }, []) + + useEffect(() => { + const handleBeforeInstallPrompt = (event) => { + event.preventDefault(); + setDeferredPrompt(event); + }; + + window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt); + + return () => { + window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt); + }; + }, []); + + + const handleGoogleLogin = async () => { + try { + const result = await sdk.oauthLoginApi("google", role); + const data = (searchParams.get("oauth")) + window.open(result, "_self"); + } catch (error) { + console.log(error) + showToast(authDispatch, error.message) + } + }; + const handleFacebookLogin = async () => { + const data = (JSON.parse(searchParams.get("oauth"))) + try { + const result = await sdk.oauthLoginApi("facebook", role); + window.open(result, "_self"); + } catch (error) { + console.log(error) + showToast(authDispatch, error.message) + } + }; + const handleAppleLogin = async () => { + try { + const result = await sdk.oauthLoginApi("apple", role); + window.open(result, "_self"); + } catch (error) { + console.log(error) + } + }; + + var options = { + enableHighAccuracy: true, + timeout: 5000, + maximumAge: 0, + }; + function success(pos) { + var crd = pos.coords; + setLatitude(crd.latitude); + setLongitude(crd.longitude); + setLocated(true) + } + + const handleDownloadNowClick = () => { + console.log(deferredPrompt) + if (deferredPrompt) { + deferredPrompt.prompt(); + + deferredPrompt.userChoice.then((choiceResult) => { + if (choiceResult.outcome === 'accepted') { + console.log('User accepted the A2HS prompt'); + } else { + console.log('User dismissed the A2HS prompt'); + } + }); + + setDeferredPrompt(null); + } + }; + + function _errors(err) { + console.warn(`ERROR(${err.code}): ${err.message}`); + } + + // useEffect(() => { + // if (navigator && navigator?.geolocation && navigator?.geolocation) { + // navigator?.permissions + // .query({ name: "geolocation" }) + // .then(function (result) { + // if (result.state === "granted") { + // //If granted then you can directly call your function here + // showToast(globalDispatch, "Access to location is granted"); + // navigator.geolocation.getCurrentPosition(success, _errors, options); + // } else if (result.state === "prompt") { + // //If prompt then the user will be asked to give permission + // // showToast(globalDispatch, "Access to location needs to be granted"); + // navigator.geolocation.getCurrentPosition(success, _errors, options); + // globalDispatch({ + // type: "SHOW_ERROR", + // payload: { + // heading: "Location Access", + // message: "Access to location needs to be granted", + // }, + // }); + // } else if (result.state === "denied") { + // //If denied then you have to show instructions to enable location + // // showToast(globalDispatch, "Access to location is denied"); + // globalDispatch({ + // type: "SHOW_ERROR", + // payload: { + // heading: "Location Access", + // message: "Access to location needs to be granted", + // }, + // }); + // } + // }).catch(error => { + // globalDispatch({ + // type: "SHOW_ERROR", + // payload: { + // heading: "Location Access", + // message: "Access to location needs to be granted", + // }, + // }); + // }); + // } else { + // // console.log("Geolocation is not supported by this browser."); + // globalDispatch({ + // type: "SHOW_ERROR", + // payload: { + // heading: "Location Access", + // message: "Geolocation is not supported by this browser.", + // }, + // }); + // } + // }, []) + + + const onSubmit = async (data) => { + + // if (located || !located) { + try { + // const response = await axios.get( + // `https://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}&key=${import.meta.env.VITE_GOOGLE_API_KEY}` + // ); + + // const addressComponents = response.data.results[0]?.address_components; + // const state = addressComponents.find( + // component => component.types.includes('administrative_area_level_1') + // ); + + // if (["Florida", "California", "New York", "Lagos", "Islamabad", "Rawalpindi"].includes(state.long_name)) { + setLocationInfo('Location Granted.'); + try { + const result = await sdk.customLogin(data); + if (!result.error) { + authDispatch({ type: "LOGIN", payload: { ...result, originalRole: result.role } }); + if (["superadmin", "admin"].includes(result.role)) { + navigate(searchParams.get("redirect_uri") ?? "/admin/dashboard"); + } else { + navigate(searchParams.get("redirect_uri") ?? "/"); + } + } + } catch (err) { + if (err.message == "Your account is inactive" || err.message == "This email is not registered") { + setDisableEmails((prev) => { + const copy = [...prev, data.email.toLowerCase()]; + return copy; + }); + setDisableLogin(true); + } + if (err.message == "Your email is not verified") { + setSuggestResendVerification(true); + } + if (err.message == "Invalid Password") { + setInCorrectPasswordCount((prev) => prev + 1); + } else { + setInCorrectPasswordCount(0); + } + setError("email", { + type: "manual", + message: err.message, + }); + if (inCorrectPasswordCount >= 3) { + setSuggestPasswordChange(true); + } + } + // } else { + // setLocationInfo('Location Denied.'); + // globalDispatch({ + // type: "SHOW_ERROR", + // payload: { + // heading: "Location Denied", + // message: "Access to site is only allowed for Florida, California and New York Residents", + // }, + // }); + // } + } catch (error) { + console.error('Error fetching location information', error); + // setDisableLogin(true); + }; + + // } + + + }; + + return ( +
    +
    + + + +
    +
    +
    +
    +

    Log In

    + { + if (disableEmails.includes(e.target.value.toLowerCase())) { + setDisableLogin(true); + } else { + setDisableLogin(false); + } + }, + })} + placeholder="Email" + /> +
    + {" "} + +
    + + Forgot Password + + {isDirty && Object.entries(errors).length > 0 ? ( +

    {Object.values(errors)[0].message}

    + ) : ( + <> + )} + + + Continue + +
    +
    OR
    +
    + + + +
    +

    + Don't have an account?{" "} + + Sign up + {" "} +

    +

    + Account issues? Please visit our{" "} + + FAQ page + {" "} +

    +
    +
    + +
    +
    +
    + + setSuggestPasswordChange(false)} + /> + setSuggestResendVerification(false)} + email={email} + /> +
    + ); +} diff --git a/src/pages/Common/Login/OauthRedirect.jsx b/src/pages/Common/Login/OauthRedirect.jsx new file mode 100644 index 0000000..6d02b2f --- /dev/null +++ b/src/pages/Common/Login/OauthRedirect.jsx @@ -0,0 +1,40 @@ +import { parseJsonSafely } from "@/utils/utils"; +import React from "react"; +import { useEffect } from "react"; +import { useNavigate } from "react-router"; +import { AuthContext } from "@/authContext"; +import { GlobalContext, showToast } from "@/globalContext"; +import TreeSDK from "@/utils/TreeSDK"; +import { v4 as uuidv4 } from 'uuid'; +import MkdSDK from "@/utils/MkdSDK"; + +const OauthRedirect = () => { + const { dispatch: authDispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const navigate = useNavigate(); + + const treeSdk = new TreeSDK(); + const sdk = new MkdSDK(); + + useEffect(() => { + const urlParams = new URLSearchParams(window.location.search); + const data = parseJsonSafely(urlParams.get("data"), {}); + if (data?.error) { + showToast(globalDispatch, data?.message, 3000, "error") + navigate("/login"); + } else { + authDispatch({ type: "LOGIN", payload: data }); + localStorage.setItem("first_login", data.user_id); + localStorage.setItem("token", data.token ?? data.access_token); + //register db + if (!localStorage.getItem("device-uid") || localStorage.getItem("device-uid") !== undefined) { + sdk.setUUId(); + } + navigate("/"); + } + }, []); + + return

    ; +}; + +export default OauthRedirect; diff --git a/src/pages/Common/Login/PageWrapper.jsx b/src/pages/Common/Login/PageWrapper.jsx new file mode 100644 index 0000000..58183f3 --- /dev/null +++ b/src/pages/Common/Login/PageWrapper.jsx @@ -0,0 +1,25 @@ +import React from "react"; +import { Outlet } from "react-router"; +import { Link } from "react-router-dom"; +import Icon from "@/components/Icons"; +import { LoginContextProvider } from "./loginContext"; + +const PageWrapper = () => { + return ( + +
    +
    + + + +
    + +
    +
    + ); +}; + +export default PageWrapper; diff --git a/src/pages/Common/Login/RequestReset.jsx b/src/pages/Common/Login/RequestReset.jsx new file mode 100644 index 0000000..11794de --- /dev/null +++ b/src/pages/Common/Login/RequestReset.jsx @@ -0,0 +1,120 @@ +import React from "react"; +import { useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; + +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { useForm } from "react-hook-form"; +import GreenCheckIcon from "@/components/frontend/icons/GreenCheckIcon"; +import MkdSDK from "@/utils/MkdSDK"; +import { LoadingButton } from "@/components/frontend"; +import Icon from "@/components/Icons"; + +export default function RequestReset() { + const [codeSent, setCodeSent] = useState(false); + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + + const sdk = new MkdSDK(); + + const schema = yup.object({ + email: yup.string().email("Invalid email").required("Email is required"), + }); + + const { + register, + handleSubmit, + formState: { errors }, + setError, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + email: "", + }, + }); + + const onSubmit = async (data) => { + console.log("submitting", data); + setLoading(true); + const role = await sdk.callRawAPI("/v2/api/custom/ergo/userinfo/PAGINATE", { + "where": [ + `email='${data.email}'` + ], + "page": 1, + "limit": 10 + }, "POST"); + const originalRole = (role?.list[0]?.role) + + try { + await sdk.forgot(data.email, originalRole); + setCodeSent(true); + setTimeout(() => { + navigate("/login"); + }, 6000); + } catch (err) { + setError("email", { + type: "manual", + message: err.message, + }); + } + setLoading(false); + }; + + return ( +
    +
    + + + +
    +
    + {!codeSent && ( +
    +

    Request Password Reset

    +

    We will email you a link to reset your password.

    + + {errors.email?.message ? ( +

    {errors.email.message}

    + ) : ( + <> + )} + + Continue + +
    + )} + + {codeSent && ( +
    +

    + + Email Sent! +

    +

    You should receive an email with the instruction to reset your password. Sometimes it will go to your spam folder.

    +
    + )} +
    +

    2022 in ergo

    +

    Contact: Support@ergobooking.com

    +
    +
    +
    + ); +} diff --git a/src/pages/Common/Login/ResetForm.jsx b/src/pages/Common/Login/ResetForm.jsx new file mode 100644 index 0000000..7bf1d88 --- /dev/null +++ b/src/pages/Common/Login/ResetForm.jsx @@ -0,0 +1,184 @@ +import { yupResolver } from "@hookform/resolvers/yup"; +import React, { useState } from "react"; +import { useForm } from "react-hook-form"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { LoadingButton } from "@/components/frontend"; +import moment from "moment"; +import commonPasswords from "../SignUp/common-passwords.json"; +import { callCustomAPI } from "@/utils/callCustomAPI"; + +const ResetForm = () => { + const [searchParams] = useSearchParams(); + const role = searchParams.get("role") || "customer"; + + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + const [loading, setLoading] = useState(false); + + const schema = yup.object({ + code: yup.number().required("Code is required").typeError("Code is required").positive("Invalid Code").integer(), + password: yup + .string() + .required("Password is required") + .min(10, "Password must be at least 10 characters long") + .matches(/^(?=.*[0-9])/, "Password must contain at least one digit(0-9)") + .matches(/^(?=.*[a-z])/, "Password must contain at least one lowercase letter") + .matches(/^(?=.*[A-Z])/, "Password must contain at least one uppercase letter") + .matches(/^(?=.*[!@#\$%\^&\*])/, "Password must contain at least one symbol") + .test("is-not-dictionary", "Password must not contain a common word", (val) => { + return commonPasswords.every((pass) => !val.includes(pass)); + }) + .test("does-not-contain-user-info", "Password must not contain your name or date of birth", (val) => { + const d = moment("2001-01-01"); + return ["john", "doe", d.format("yyyyMMDD"), d.format("DDMMyyyy"), d.format("MMDDyyyy"), d.format("YYMMDD"), d.format("MMDDYY"), d.format("DDMMYY")].every( + (field) => field.trim() == "" || !val.toLowerCase().includes(field.toLowerCase()), + ); + }), + confirm_password: yup.string().oneOf([yup.ref("password"), null], "Passwords don't match"), + }); + const { + handleSubmit, + register, + trigger, + formState: { errors, dirtyFields }, + setError, + } = useForm({ resolver: yupResolver(schema), defaultValues: { password: "" }, criteriaMode: "all" }); + + const navigate = useNavigate(); + + const sdk = new MkdSDK(); + + const onSubmit = async (data) => { + console.log("submitting", data); + setLoading(true); + try { + // await sdk.reset(searchParams.get("token"), data.code, data.password); + await callCustomAPI("reset", "post", { code: data.code, password: data.password, token: searchParams.get("token") }, ""); + navigate("/login?role=" + role); + } catch (err) { + setError("code", { message: err.message == "Password is same as old password" ? "Please use a different password" : err.message }); + } + setLoading(false); + }; + + function getPasswordErrors() { + var arr = []; + if (Array.isArray(errors.password?.types.matches)) { + arr = [...errors.password.types.matches]; + } + if (typeof errors.password?.types.matches === "string") { + arr.push(errors.password.types.matches); + } + if (errors.password?.types.min) { + arr.push(errors.password.types.min); + } + if (errors.password?.types["does-not-contain-user-info"]) { + arr.push(errors.password?.types["does-not-contain-user-info"]); + } + if (errors.password?.types["is-not-dictionary"]) { + arr.push(errors.password?.types["is-not-dictionary"]); + } + return arr; + } + const passwordErrors = getPasswordErrors(); + + return ( +
    +
    +

    Set New Password

    + + +
    + trigger("password") })} + className="flex-grow border-0 p-2 px-4 focus:outline-none active:outline-none " + placeholder="Password" + />{" "} + +
    + {dirtyFields.password && ( +
    + {passwordErrors.map((msg) => ( +

    {msg}

    + ))} +
    + )} +
    + {" "} + +
    + {Object.entries(errors).length > 0 && dirtyFields.password && !errors.password ? ( +

    {Object.values(errors)[0].message}

    + ) : null} + + Continue + +
    +
    + ); +}; + +export default ResetForm; diff --git a/src/pages/Common/Login/ResetRedirect.jsx b/src/pages/Common/Login/ResetRedirect.jsx new file mode 100644 index 0000000..95f33ce --- /dev/null +++ b/src/pages/Common/Login/ResetRedirect.jsx @@ -0,0 +1,10 @@ +import React from "react"; +import { Navigate } from "react-router"; +import { useSearchParams } from "react-router-dom"; + +const ResetRedirect = ({ role }) => { + const [searchParams] = useSearchParams(); + return ; +}; + +export default ResetRedirect; diff --git a/src/pages/Common/Login/SuggestPasswordChangeModal.jsx b/src/pages/Common/Login/SuggestPasswordChangeModal.jsx new file mode 100644 index 0000000..18aa239 --- /dev/null +++ b/src/pages/Common/Login/SuggestPasswordChangeModal.jsx @@ -0,0 +1,73 @@ +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment } from "react"; +import { Link } from "react-router-dom"; + +export default function SuggestPasswordChangeModal({ modalOpen, closeModal }) { + return ( + + + +
    + + +
    +
    + + + + Can't remember your password? + +
    +

    We noticed you tried to login several times, would you like to change your password

    +
    + +
    + + + Reset Password + +
    +
    +
    +
    +
    +
    +
    + ); +} diff --git a/src/pages/Common/Login/SuggestResendVerificationModal.jsx b/src/pages/Common/Login/SuggestResendVerificationModal.jsx new file mode 100644 index 0000000..1ceedfd --- /dev/null +++ b/src/pages/Common/Login/SuggestResendVerificationModal.jsx @@ -0,0 +1,107 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import { LoadingButton } from "@/components/frontend"; +import { GlobalContext, showToast } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment, useContext, useState } from "react"; + +export default function SuggestResendVerificationModal({ modalOpen, closeModal, email }) { + const [loading, setLoading] = useState(false); + const { dispatch } = useContext(AuthContext); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const [ctrl] = useState(new AbortController()); + + async function sendEmailVerification() { + setLoading(true); + const sdk = new MkdSDK(); + try { + await sdk.callRawAPI("/v2/api/custom/ergo/resend-verification-email", { email }, "POST", ctrl.signal); + showToast(globalDispatch, "Email sent, Please check your inbox", 8000); + closeModal(); + } catch (err) { + if (err.name == "AbortError") { + setLoading(false); + return; + } + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + setLoading(false); + } + + return ( + + + +
    + + +
    +
    + + + + Resend verification email? + +
    +

    + Your email address is not verified, would you like us to resend verification email to {email} +

    +
    + +
    + + + Yes, resend + +
    +
    +
    +
    +
    +
    +
    + ); +} diff --git a/src/pages/Common/Login/loginContext.jsx b/src/pages/Common/Login/loginContext.jsx new file mode 100644 index 0000000..f8930e8 --- /dev/null +++ b/src/pages/Common/Login/loginContext.jsx @@ -0,0 +1,31 @@ +import React, { createContext, useContext, useReducer } from "react"; + +const initialLoginData = { + email: "", + password: "", + forgetPasswordEmail: "", + reset_token: "" +}; + +const reducer = (state, action) => { + switch (action.type) { + case "SET_DATA": + return { ...state, email: action.payload.email, password: action.payload.password }; + default: + return state; + } +}; + +// create context here +const loginContext = createContext({}); + +// wrap this component around App.tsx to get access to userData in all components +const LoginContextProvider = ({ children }) => { + const [loginData, dispatch] = useReducer(reducer, initialLoginData); + + return {children}; +}; + +// use this custom hook to get the data in any component in component tree +const useLoginContext = () => useContext(loginContext); +export { useLoginContext, LoginContextProvider }; diff --git a/src/pages/Common/Messages/ChatTile.jsx b/src/pages/Common/Messages/ChatTile.jsx new file mode 100644 index 0000000..d42a962 --- /dev/null +++ b/src/pages/Common/Messages/ChatTile.jsx @@ -0,0 +1,141 @@ +import ThreeDotsMenu from "@/components/frontend/ThreeDotsMenu"; +import { ARCHIVE_STATUS } from "@/utils/constants"; +import moment from "moment"; +import React, { useEffect, useState } from "react"; +import Skeleton from "react-loading-skeleton"; +import MkdSDK from "@/utils/MkdSDK"; +import { useSearchParams } from "react-router-dom"; + + +const formatDate = (time) => { + let currentTime = moment(new Date()); + let messageDate = moment(time); + if (currentTime.diff(messageDate, "days") > 1) { + return moment(messageDate).format("Do MMM"); + } else { + return moment(messageDate).format("hh:mm A"); + } +}; + +export default function ChatTile({ markMessagesAsRead, first, virtual, last, room, rooms, activeRoomId, setActiveRoom, setActiveBooking, setActiveProperty, setMobileChatSection, messages, deleteRoom, archiveRoom, unArchiveRoom }) { + const ctrl = new AbortController(); + let sdk = new MkdSDK(); + const [photo, setPhoto] = useState() + const [roomUnreadCounter, setRoomUnreadCounter] = useState([]) + const [searchParams, setSearchParams] = useSearchParams(); + const params = searchParams.get("room_id") + + async function getOtherUser() { + await sdk.setTable("user") + const payload = { id: room?.other_user_id } + const result = await sdk.callRestAPI(payload, "GET"); + setPhoto(result?.model?.is_photo_approved !== 1 ? null : result?.model?.photo) + return "yes" + } + + async function getMessages(room_id) { + const result = await sdk.getChats(room_id); + const unread = result.model.filter((msg) => msg.unread === 1 && msg.chat.user_id !== Number(localStorage.getItem("user"))).map((msg) => msg.id); + markMessagesAsRead(room.id, unread); + return result.model + } + async function getAllMessages(room_id) { + const result = await sdk.getChats(room_id); + const unread = result.model.filter((msg) => msg.unread === 1 && msg.chat.user_id !== Number(localStorage.getItem("user"))); + setRoomUnreadCounter(unread) + } + + async function markMessagesAsRead(room_id, arr) { + sdk.setTable("chat"); + await Promise.all(arr.map((id) => sdk.callRestAPI({ id, unread: 0 }, "PUT"))); + setRoomUnreadCounter((prev) => (arr.length > prev ? 0 : prev - arr.length)); + } + + async function getBookingDetails() { + setActiveProperty({}); + setActiveBooking({}); + if (room?.booking_id !== null) { + await sdk.setTable("booking") + const payload = { id: room?.booking_id } + const result = await sdk.callRestAPI(payload, "GET"); + setActiveBooking(result.model) + setActiveProperty({}); + } else { + const user_id = localStorage.getItem("user"); + const where = [`ergo_property_spaces.id = ${Number(room?.property_id)} AND ergo_property_spaces.deleted_at IS NULL`]; + const result = await sdk.callRawAPI("/v2/api/custom/ergo/popular/PAGINATE", { page: 1, limit: 1, user_id: Number(user_id), where, all: true }, "POST", ctrl.signal); + if (Array.isArray(result.list) && result.list.length > 0) { + setActiveProperty(result.list[0]); + setActiveBooking({}) + } else setActiveProperty({}) + } + } + + useEffect(() => { + getOtherUser() + + }, []) + + useEffect(() => { + if (room?.id === "temp") return; + getAllMessages(room.id) + }, [room]); + + + + return ( +
    +
    { + setActiveRoom(room); + setMobileChatSection(true); + getMessages(room.id) + getBookingDetails() + }} + className="flex gap-2 mr-2 cursor-pointer items-center justify-between"> + +
    +
    + {first || } {last} +
    +

    {" " || }

    +
    +
    +
    + {room?.id !== "temp" && roomUnreadCounter.length > 0 && {roomUnreadCounter.length}} + {formatDate(room.update_at)} +
    + deleteRoom(room.id), + }, + { + label: "Archive chat", + icon: <>, + onClick: () => archiveRoom(room.id), + notShow: room.is_archive == ARCHIVE_STATUS.IS_ARCHIVE, + }, + { + label: "Unarchive chat", + icon: <>, + onClick: () => unArchiveRoom(room.id), + notShow: room.is_archive == ARCHIVE_STATUS.NOT_ARCHIVE, + }, + ]} + /> +
    +
    +
    + ); +} diff --git a/src/pages/Common/Messages/ImagePreviewModal.jsx b/src/pages/Common/Messages/ImagePreviewModal.jsx new file mode 100644 index 0000000..d40cca9 --- /dev/null +++ b/src/pages/Common/Messages/ImagePreviewModal.jsx @@ -0,0 +1,219 @@ +import { LoadingButton } from "@/components/frontend"; +import { BOOKING_STATUS } from "@/utils/constants"; +import * as yup from "yup"; +import { yupResolver } from "@hookform/resolvers/yup"; +import React, { useContext } from "react"; +import { useForm } from "react-hook-form"; +import MkdSDK from "@/utils/MkdSDK"; +import { useSearchParams } from "react-router-dom"; +import { GlobalContext } from "@/globalContext"; + + +const ImagePreviewModal = ({ activeRoom, getRooms, setMessages, state, setRooms, spaceId, setShowImagePreviewModal, activeBooking }) => { + const [imageSrc, setImageSrc] = React.useState(""); + const [messageError, setMessageError] = React.useState(""); + const [uploadedFile, setUploadedFile] = React.useState(); + const [loading, setLoading] = React.useState(false); + const [searchParams, setSearchParams] = useSearchParams(); + const { state: globalState, dispatch: globalDispatch } = useContext(GlobalContext); + + const sdk = new MkdSDK(); + + const schema = yup + .object({ + photo: yup.string() + }) + .required(); + + const { + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + function handleFilePreview(file){ + if (file) { + setUploadedFile(file[0]) + setImageSrc(URL.createObjectURL(file[0])); + } + }; + + async function sendImageMessage() { + + if (imageSrc === "") { + setMessageError("Please select an Image"); + return; + } + + setLoading(true); + const handleImageUpload = async (file) => { + const formData = new FormData(); + formData.append("file", file); + try { + const upload = await sdk.uploadImage(formData); + return upload; + } catch (err) { + console.log("err", err); + return ""; + } + }; + + const upload = await handleImageUpload(uploadedFile); + if (upload?.id) { + setLoading(false); + } + try { + let date = new Date().toISOString().split("T")[0]; + let date2 = new Date().toISOString().replace("T", " ").split(".")[0]; + // if its temporary chat then create new room before message + let result = null; + + let is_temp_chat = activeRoom?.is_temp_chat; + + let room_id = searchParams.get("room_id"); + + if (is_temp_chat && room_id === "temp") { + sdk.setTable("room"); + result = await sdk.callRestAPI( + { + user_id: state.user, + other_user_id: Number(activeRoom?.other_user_id), + booking_id: activeRoom.booking_id === null ? null : Number(activeRoom?.booking_id), + property_id: spaceId, + user_update_at: date2, + other_user_update_at: date2, + chat_id: -1, + }, + "POST", + ) + setRooms((prev) => { + const copy = [...prev]; + copy[prev.findIndex((rm) => rm?.is_temp_chat)].id = result?.message; + return copy; + }) + // setRooms((prev) => [...prev,r]) + setActiveRoom((prev) => ({ ...prev, id: result.message })); + } + + await sdk.postMessage({ + room_id: room_id === "temp" ? result?.message : Number(room_id), + user_id: state.user, + message: upload.url, + date, + other_user_id: activeRoom.other_user_id, + }); + let newMessageObj = { + room_id: activeRoom.id, + chat: { + message: upload.url, + user_id: state.user, + // other_user_id: activeRoom.other_user_id, + is_image: true, + timestamp: new Date(), + }, + unread: 1, + create_at: new Date().toISOString(), + update_at: new Date().toISOString(), + }; + + setMessages((prev) => { + const copy = { ...prev }; + copy[room_id === "temp" ? result?.message : Number(room_id)] = [...copy[room_id === "temp" ? result?.message : Number(room_id)], newMessageObj]; + return copy; + }); + // is_temp_chat = false; + setLoading(false); + setShowImagePreviewModal(false); + getRooms() + + // send email alert + // sendEmailAlert(activeRoom.other_user_id, activeBooking.property_name, message, activeRoom.id); + + } catch (err) { + console.log(err) + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Sending image failed", + message: err.message, + }, + }); + } + setLoading(false); + } + + return ( + <> +
    +
    {setShowImagePreviewModal(false);setImageSrc("")}} + >
    + +
    +
    +
    +

    Please select an Image

    +
    +
    + {messageError} +
    + + +
    +
    + {imageSrc && + + } +
    + + + + {uploadedFile && + + Send + + } +
    +
    +
    +
    +
    +
    + + ); +}; + +export default ImagePreviewModal; diff --git a/src/pages/Common/Messages/MessagesContainer.jsx b/src/pages/Common/Messages/MessagesContainer.jsx new file mode 100644 index 0000000..1b81d73 --- /dev/null +++ b/src/pages/Common/Messages/MessagesContainer.jsx @@ -0,0 +1,49 @@ +import { AuthContext } from "@/authContext"; +import moment from "moment"; +import React from "react"; +import { useContext } from "react"; + +export default function MessagesContainer({ messages, messageErr }) { + const { state } = useContext(AuthContext); + return ( +
    +
    + {messages && ( +
    + {messages.map((message, idx) => ( +
    +
    +
    + {message?.chat?.message.startsWith("https://s3.us-east-2.amazonaws.com") ? ( +
    + +
    + ) : ( +

    + {message?.chat?.message} +

    + )} +
    +
    + {moment(message?.chat?.timestamp).format("DD-MM, hh:mm A")} +
    +
    +
    + ))} + {messageErr && ( +
    +

    {messageErr}

    +
    + )} +
    + )} +
    +
    + ); +} diff --git a/src/pages/Common/Messages/MessagesPage.jsx b/src/pages/Common/Messages/MessagesPage.jsx new file mode 100644 index 0000000..073bb0f --- /dev/null +++ b/src/pages/Common/Messages/MessagesPage.jsx @@ -0,0 +1,994 @@ +import React, { useContext, useState, useEffect } from "react"; +import MkdSDK from "@/utils/MkdSDK"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; +import SmileIcon from "@/components/frontend/icons/SmileIcon"; +import PictureIcon from "@/components/frontend/icons/PictureIcon"; +import EmojiPicker from "emoji-picker-react"; +import badWords from "./badWords.json"; +import * as linkify from "linkifyjs"; +import { formatAMPM, monthsMapping } from "@/utils/date-time-utils"; +import CircleCheckIcon from "@/components/frontend/icons/CircleCheckIcon"; +import { GlobalContext, showToast } from "@/globalContext"; +import FavoriteButton from "@/components/frontend/FavoriteButton"; +import ChatTile from "./ChatTile"; +import MessagesContainer from "./MessagesContainer"; +import { ARCHIVE_STATUS, BOOKING_STATUS } from "@/utils/constants"; +import { parseJsonSafely } from "@/utils/utils"; +import { ArrowLeftIcon, PaperAirplaneIcon } from "@heroicons/react/24/outline"; +import TreeSDK from "@/utils/TreeSDK"; +import StarIcon from "@/components/frontend/icons/StarIcon"; +import PersonIcon from "@/components/frontend/icons/PersonIcon"; +import PropertySpaceMapImage from "@/components/frontend/PropertySpaceMapImage"; +import ImagePreviewModal from "./ImagePreviewModal"; + + +let sdk = new MkdSDK(); +let treeSdk = new TreeSDK(); +const ctrl = new AbortController(); + +const MessagesPage = () => { + const { state, dispatch } = useContext(AuthContext); + const [rooms, setRooms] = useState(Array(4).fill({})); + const [roomUnread, setRoomUnread] = useState([]); + const { state: globalState, dispatch: globalDispatch } = useContext(GlobalContext); + const [message, setMessage] = useState(""); + + const [searchParams, setSearchParams] = useSearchParams(); + const [activeRoom, setActiveRoom] = useState({}); + const [activeBooking, setActiveBooking] = useState({}); + const [activeProperty, setActiveProperty] = useState({}); + const [spaceId, setSpaceId] = useState(); + const [messageErr, setMessageErr] = useState(""); + const [showMap, setShowMap] = useState(false); + const [virtual, setVirtual] = useState(false); + + const [showEmoji, setShowEmoji] = useState(false); + const [unReadCount, setUnreadCount] = useState(globalState.unreadMessages); + const [archivedCount, setArchivedAccount] = useState(0); + const [mobileChatSection, setMobileChatSection] = useState(false); + const [mobilePreviewOpen, setMobilePreviewOpen] = useState(false); + + const [favoriteId, setFavoriteId] = useState(null); + const [render, forceRender] = useState(false); + const [fetchingExtra, setFetchingExtra] = useState(true); + const navigate = useNavigate(); + + const [messages, setMessages] = useState({}); + const [sending, setSending] = useState(false); + const [roomsFetched, setRoomsFetched] = useState(false); + + const [showImagePreviewModal, setShowImagePreviewModal] = useState(false) + + const formatAmenities = (propertyAmenities) => { + var amenities = (propertyAmenities ?? "").split(","); + amenities = Array.from(new Set(amenities)); + return amenities + } + + const bookingExpired = activeBooking.booking_start_time && activeBooking.status < BOOKING_STATUS.ONGOING ? new Date(activeBooking.booking_end_time) < Date.now() : false; + + async function getRooms() { + try { + // const result2 = await treeSdk.getList("room", { join: ["user|other_user_id", "booking"], filter: [`user_id,eq,${state.user}`] }); + const result = await sdk.getMyRoom(); + if (Array.isArray(result.messages)) { + setUnreadCount( + result.messages.filter((msg) => { + const messageSenderId = JSON.parse(msg.chat).user_id; + return Number(messageSenderId) != Number(state.user); + }).length, + ); + globalDispatch({ + type: "SET_UNREAD_MESSAGES_COUNT", + payload: result.messages.filter((msg) => { + const messageSenderId = JSON.parse(msg.chat).user_id; + return Number(messageSenderId) != Number(state.user); + }).length, + }); + } + setRooms(result?.list); + setRoomUnread(result?.messages) + + setRoomsFetched(true); + globalDispatch({ type: "STOP_LOADING" }); + + } catch (err) { + tokenExpireError(dispatch, err.message); + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed getting rooms", + message: err.message, + }, + }); + } + } + + async function getArchivedRooms() { + try { + const result = await treeSdk.getList("room", { join: ["user|other_user_id", "booking"], filter: [`user_id,eq,${state.user}`] }); + setArchivedAccount((result?.list.filter((item) => item.is_archive == 1)).length) + setRooms(result.list.filter((item) => item.is_archive == 1)); + + console.log((result?.list.filter((item) => item.is_archive == 1)).length) + globalDispatch({ type: "STOP_LOADING" }); + + } catch (err) { + tokenExpireError(dispatch, err.message); + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed getting rooms", + message: err.message, + }, + }); + } + } + + + async function getMessages(room_id) { + try { + const result = await sdk.getChats(room_id); + if (Array.isArray(result.model)) { + setMessages((prev) => { + const copy = { ...prev }; + copy[room_id] = result.model.sort(sortByUpdateAt); + return copy; + }); + } + return result.model + } catch (err) { + tokenExpireError(dispatch, err.message); + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed getting messages " + room_id, + message: err.message, + }, + }); + } + } + + async function sendMessage() { + if (message == "") return; + + setShowEmoji(false); + + //Add checks to validate message based on active booking + + setSending(true); + + try { + let date = new Date().toISOString().split("T")[0]; + let date2 = new Date().toISOString().replace("T", " ").split(".")[0]; + // if its temporary chat then create new room before message + let result = null; + + let is_temp_chat = activeRoom.is_temp_chat; + + let room_id = searchParams.get("room_id"); + + if (is_temp_chat && room_id === "temp") { + sdk.setTable("room"); + result = await sdk.callRestAPI( + { + user_id: state.user, + other_user_id: Number(activeRoom.other_user_id), + booking_id: activeRoom.booking_id === null ? null : Number(activeRoom.booking_id), + property_id: spaceId, + user_update_at: date2, + other_user_update_at: date2, + chat_id: -1, + }, + "POST", + ) + setRooms((prev) => { + const copy = [...prev]; + copy[prev.findIndex((rm) => rm?.is_temp_chat)].id = result?.message; + return copy; + }) + // setRooms((prev) => [...prev,r]) + setActiveRoom((prev) => ({ ...prev, id: result.message })); + } + + await sdk.postMessage({ + room_id: room_id === "temp" ? result?.message : Number(room_id), + user_id: state.user, + message, + date, + other_user_id: activeRoom.other_user_id, + }); + let newMessageObj = { + room_id: activeRoom.id, + chat: { + message: message, + user_id: state.user, + // other_user_id: activeRoom.other_user_id, + is_image: false, + timestamp: new Date(), + }, + unread: 1, + create_at: new Date().toISOString(), + update_at: new Date().toISOString(), + }; + + setMessages((prev) => { + const copy = { ...prev }; + copy[room_id === "temp" ? result?.message : Number(room_id)] = [...copy[room_id === "temp" ? result?.message : Number(room_id)], newMessageObj]; + return copy; + }); + // is_temp_chat = false; + getRooms() + + // send email alert + sendEmailAlert(activeRoom.other_user_id, activeBooking.property_name, message, activeRoom.id); + setMessage(""); + + // TODO: scroll to bottom + } catch (err) { + console.log(err) + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Sending message failed", + message: err.message, + }, + }); + } + setSending(false); + } + + async function sendEmailAlert(to, property_name, message, room_id) { + try { + // get receiver preferences + const result = await sdk.callRawAPI("/v2/api/custom/ergo/get-user", { id: to }, "POST"); + + if (parseJsonSafely(result.settings, {}).email_on_new_chat_message == true) { + let sender_name = globalState.user.first_name + " " + globalState.user.last_name; + // get email template + const tmpl = await sdk.getEmailTemplate("chat-message-alert"); + const body = tmpl.html + ?.replace(new RegExp("{{{sender_name}}}", "g"), sender_name) + .replace(new RegExp("{{{property_name}}}", "g"), property_name) + .replace(new RegExp("{{{message}}}", "g"), message) + .replace(new RegExp("{{{room_id}}}", "g"), room_id); + + // send email + await sdk.sendEmail(result.email, tmpl.subject, body); + } + } catch (err) { + console.log("ERROR", err); + } + } + + async function fetchFavoriteStatus(property_spaces_id, user_id) { + const payload = { property_spaces_id, user_id }; + sdk.setTable("user_property_spaces"); + try { + const result = await sdk.callRestAPI({ payload }, "GETALL"); + + if (Array.isArray(result.list) && result.list.length > 0) { + setFavoriteId(result.list[0].id); + } else { + throw new Error(""); + } + } catch (err) { + setFavoriteId(null); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + async function deleteRoom(id) { + sdk.setTable("room"); + try { + const result = await sdk.callRestAPI({ id }, "DELETE"); + if (!result.error) { + getRooms() + setActiveRoom({}) + setActiveBooking({}) + setActiveProperty({}) + showToast(globalDispatch, result.message, 5000) + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function archiveRoom(id) { + sdk.setTable("room"); + // call API - callRestAPI (You can see callRestAPI implementation in other functions) here to archive room chat. Method is PUT + // update archived state of selected room chat without refreshing the page and toast a success message + //Also switch to the archive tab on success of the API, with the archived room chat showing under there + } + + async function unArchiveRoom(id) { + sdk.setTable("room"); + // call API - callRestAPI (You can see callRestAPI implementation in other functions) here to unarchive room chat. Method is PUT + // update unarchived state of selected room chat without refreshing the page and toast a success message + //Also switch to the inbox tab on success of the API, with the unarchived room chat showing under there + } + + async function fetchExtraBookingDetails() { + if (activeBooking?.extrasFetched) return; + setFetchingExtra(true); + try { + const result = await sdk.callRawAPI(`/v2/api/custom/ergo/booking/details`, { where: [`ergo_booking.id = ${activeRoom.booking_id} AND (ergo_booking.deleted_at IS NULL AND ergo_booking.status = ${BOOKING_STATUS.ONGOING} OR ergo_booking.status = ${BOOKING_STATUS.UPCOMING})`] }, "POST"); + if (result.list.id && new Date(new Date(result.list.booking_end_time).setDate(new Date(result.list.booking_end_time).getDate() + 1)) > new Date()) { + const fullBooking = { + ...result.list, + ...activeBooking, + add_ons: result.list.add_ons, + property_name: result.list.property_name, + image: result.list.image_url, + address_line_1: result.list.address_line_1, + address_line_2: result.list.address_line_2, + extrasFetched: true, + }; + setRooms((prev) => { + const copy = [...prev]; + const pos = copy.findIndex((r) => r.id == activeRoom.id); + if (pos != -1) { + copy[pos].booking = fullBooking; + } + return copy; + }); + setActiveRoom((prev) => { + const copy = { ...prev }; + copy.booking = fullBooking; + return copy; + }); + } + + setTimeout(() => { + setFetchingExtra(false); + }, 500); + } catch (err) { + tokenExpireError(dispatch, err.message); + globalDispatch({ type: "SHOW_ERROR", payload: { heading: "Error fetching booking details", message: err.message } }); + } + } + + async function markMessagesAsRead(room_id, arr) { + try { + sdk.setTable("chat"); + await Promise.all(arr.map((id) => sdk.callRestAPI({ id, unread: 0 }, "PUT"))); + setMessages((prev) => { + const copy = { ...prev }; + copy[room_id] = (copy[room_id] ?? []).map((msg) => ({ ...msg, unread: 0 })); + return copy; + }); + setUnreadCount((prev) => { + const newCount = arr.length > prev ? 0 : prev - arr.length; + globalDispatch({ + type: "SET_UNREAD_MESSAGES_COUNT", + payload: newCount, + }); + return newCount; + }); + } catch (err) { + tokenExpireError(dispatch, err.message); + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Error marking messages as read", + message: err.message, + }, + }); + } + } + + async function getPropertyDetails(id) { + const user_id = localStorage.getItem("user"); + const where = [`ergo_property_spaces.id = ${id} AND ergo_property_spaces.deleted_at IS NULL`]; + try { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/popular/PAGINATE", { page: 1, limit: 1, user_id: Number(user_id), where, all: true }, "POST", ctrl.signal); + if (Array.isArray(result.list) && result.list.length > 0) { + setActiveProperty(result.list[0]); + fetchFavoriteStatus(Number(result.list[0].id), Number(user_id)) + } else setActiveProperty({}) + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + async function createVirtualRoom(other_user_id, booking_id, new_booking_id) { + setSpaceId(Number(new_booking_id)) + try { + const result = await treeSdk.getOne("user", other_user_id, { join: [] }); + const room = { + id: "temp", + is_temp_chat: true, + user_id: state.user, + other_user_id, + booking_id, + is_archive: 0, + property_id: new_booking_id, + create_at: new Date(), + update_at: new Date(), + user_update_at: new Date(), + other_user_update_at: new Date(), + deleted_at: null, + user: { + id: other_user_id, + first_name: result.model.deleted_at == null ? result.model.first_name : "[Deleted User]", + last_name: result.model.deleted_at == null ? result.model.last_name : "", + photo: result.model.deleted_at == null ? result.model.photo : null, + }, + booking: { + id: booking_id === null ? null : booking_id, + }, + }; + + setRooms((prev) => [...prev, room]); + setVirtual(true) + setActiveRoom(room); + } catch (err) { + tokenExpireError(dispatch, err.message); + globalDispatch({ type: "SHOW_ERROR", payload: { heading: "Failed to fetch other user data", message: err.message } }); + } + } + + async function getBookingDetails() { + setFetchingExtra(true); + const room_id = searchParams.get("room_id"); + const booking_id = searchParams.get("booking"); + setActiveProperty({}); + setActiveBooking({}); + if (room_id === "temp" || room_id === null) return; + sdk.setTable("room") + const data = await sdk.callRestAPI({ id: Number(room_id) }, "GET") + + await sdk.setTable("booking") + const bookings = await sdk.callRestAPI({}, "GETALL"); + const fetched_booking = bookings.list.reverse().find((item) => + (item.host_id === data.model?.user_id || item.host_id === data.model?.other_user_id) && + (item.customer_id === data.model?.user_id || item.customer_id === data.model?.other_user_id) && + (item.property_space_id === data?.model?.property_id) && + (item.status === BOOKING_STATUS.UPCOMING || item.status === BOOKING_STATUS.ONGOING) + ) + if (data.model?.booking_id !== null || booking_id !== null || (fetched_booking !== undefined && fetched_booking !== null) ) { + if (fetched_booking) { + setActiveBooking(fetched_booking) + return; + } + await sdk.setTable("booking") + const payload = { id: data.model?.booking_id ?? (booking_id ?? fetched_booking?.id) } + const result = await sdk.callRestAPI(payload, "GET"); + setActiveBooking((result?.model?.status === BOOKING_STATUS.ONGOING || result?.model?.status === BOOKING_STATUS.UPCOMING) ? result.model : {}) + } else { + const user_id = localStorage.getItem("user"); + const where = [`ergo_property_spaces.id = ${Number(data.model?.property_id)} AND ergo_property_spaces.deleted_at IS NULL`]; + const result = await sdk.callRawAPI("/v2/api/custom/ergo/popular/PAGINATE", { page: 1, limit: 1, user_id: Number(user_id), where, all: true }, "POST", ctrl.signal); + if (Array.isArray(result.list) && result.list.length > 0) { + setActiveProperty(result.list[0]); + setActiveBooking({}) + } else setActiveProperty({}) + } + setTimeout(() => { + setFetchingExtra(false); + }, 500); + } + + useEffect(() => { + getBookingDetails() + }, [searchParams.get("room_id")]); + + useEffect(() => { + getRooms(); + }, []); + + useEffect(() => { + if (!roomsFetched) return; + const room_id = searchParams.get("room_id"); + const property_space_id = searchParams.get("space"); + setActiveProperty({}); + setActiveBooking({}); + let room = rooms.find((rm) => rm.id == room_id); + if (room) { + setActiveRoom(room); + return; + } + const other_user_id = searchParams.get("other_user_id"); + if (!other_user_id) return; + const booking_id = searchParams.get("booking"); + room = rooms.find((rm) => ((rm.booking_id === booking_id && rm.other_user_id === other_user_id) || rm.property_id === Number(property_space_id))); + if (room) { + setActiveRoom(room); + } else { + getPropertyDetails(property_space_id) + createVirtualRoom(other_user_id, booking_id, property_space_id) + + } + }, [roomsFetched]); + + useEffect(() => { + + const controller = new AbortController(); + const pollMessages = async () => { + const abortController = new AbortController(); + try { + const poll = await sdk.startPolling(localStorage.getItem("user"), abortController.signal) + if (poll.message) { + // Do whatever you want here + let room = searchParams.get("room_id"); + if (room === "temp") return; + getRooms() + if (Number(searchParams.get("room_id"))) { + getMessages(Number(searchParams.get("room_id"))).then((res) => { + const unread = res.filter((msg) => msg.unread === 1 && msg.chat.user_id !== state.user).map((msg) => msg.id); + markMessagesAsRead(Number(searchParams.get("room_id")), unread); + } + ); + } + } + } catch (error) { + console.log(error) + } + finally { + if (!abortController.signal.aborted) { + pollMessages() + } + } + } + + return () => { + clearInterval(5000); + controller.abort(); + }; + }, []); + + function showImageModal(){ + setShowImagePreviewModal(true) + } + + + useEffect(() => { + setFetchingExtra(true); + globalDispatch({ type: "START_LOADING" }); + setActiveProperty({}); + setActiveBooking({}); + if (!activeRoom.id) return; + searchParams.set("room_id", activeRoom.id); + searchParams.set("booking", activeRoom.booking_id); + if (!searchParams.get("booking")) { + searchParams.delete("booking"); + } + searchParams.delete("space"); + searchParams.delete("other_user_id"); + setSearchParams(searchParams); + setMessage(""); + getMessages(activeRoom.id).then((res) => { + const unread = res.filter((msg) => msg.unread === 1 && msg.chat.user_id !== state.user).map((msg) => msg.id); + markMessagesAsRead(activeRoom.id, unread); + }); + if (activeRoom.booking_id !== null) { + fetchExtraBookingDetails(); + } + if (activeRoom.id) { + getPropertyDetails(activeRoom.property_id) + } + setTimeout(() => { + setFetchingExtra(false); + }, 500); + globalDispatch({ type: "STOP_LOADING" }); + + }, [activeRoom.id, render]); + + function sortByUpdateAt(a, b) { + return new Date(a.update_at) - new Date(b.update_at); + } + + return ( + <> +
    setShowEmoji(false)} + > +
    +
    +
    + + +
    + {roomsFetched && +
    + {searchParams.get("message_tab") != "archive" && + rooms && + rooms + .filter((rm) => rm.is_archive == ARCHIVE_STATUS.NOT_ARCHIVE) + .sort((a, b) => new Date(b.update_at) - new Date(a.update_at)) + .map((room, idx) => { + return ( + markMessagesAsRead} + messages={messages} + deleteRoom={deleteRoom} + archiveRoom={archiveRoom} + unArchiveRoom={unArchiveRoom} + /> + ); + })} + {searchParams.get("message_tab") == "archive" && + rooms && + rooms + .filter((rm) => rm.is_archive == ARCHIVE_STATUS.IS_ARCHIVE) + .sort((a, b) => new Date(b.update_at) - new Date(a.update_at)) + .map((room, idx) => { + return ( + + ); + })} +
    + } +
    +
    + +
    0 && !fetchingExtra) ? "block" : "hidden"} absolute top-0 right-0 -left-0 overflow-y-hidden bg-white md:static md:block md:max-h-[unset] md:w-[48%]`}> +
    + {activeRoom?.id ? ( + <> +
    + +
    +
    +

    Chat with {activeRoom?.first_name === undefined ? rooms[0]?.user?.first_name : activeRoom?.first_name + " " + activeRoom?.last_name === undefined ? rooms[0]?.user?.last_name : activeRoom?.last_name}

    + {mobileChatSection && activeRoom.booking_id && ( + + )} +
    + +
    +
    + +
    + +
    +
    + + + +
    + +
    { + e.preventDefault(); + sendMessage(); + }} + > + setMessage(e.target.value)} + autoComplete="off" + /> + +
    +
    +
    + + + ) : ( +
    Select a chat to view
    + )} +
    +
    + +
    0 && !fetchingExtra ) ? "block" : "hidden"} lg:block absolute top-0 right-0 -left-0 overflow-y-auto max-h-[var(--messages-page-height)] bg-white md:static md:block md:max-h-[unset] w-full md:w-[26%]`}> +
    +
    + + {(activeRoom?.booking_id && activeRoom?.booking?.id) && +

    Booking Preview

    + } + + {(activeRoom?.booking_id && !activeRoom?.booking?.id) && +

    Property Preview

    + } + {mobilePreviewOpen && ( + + )} +
    +
    +
    + {activeRoom.id && !activeRoom?.booking?.id ? ( +
    +
    + + {activeProperty?.category || "N/A"} +
    +
    +
    +

    {activeProperty?.name}

    +

    {activeProperty?.city}

    +

    {activeProperty?.country}

    +
    +

    + from: ${activeProperty?.rate}/hour +

    +
    + + {activeProperty?.max_capacity} +
    +
    +
    +
    +
    +

    + + + {(Number(activeProperty?.average_space_rating) || 0).toFixed(1)} + ({activeProperty?.space_rating_count}) + +

    + +
    +
    + {formatAmenities(activeProperty?.amenities).slice(0, 3).map((am, idx) => ( + + {am} + + ))} + {formatAmenities(activeProperty?.amenities).length > 3 ? +{formatAmenities(activeProperty?.amenities).length - 3} more : null} +
    +
    +
    +
    +
    + ) : null} + + {(activeRoom?.booking_id && activeRoom?.booking?.id) ? ( + <> +
    + {activeRoom?.booking?.space_category ? activeRoom?.booking?.space_category : "N/A"} +
    +
    +
    +

    Date

    +

    + {" "} + {monthsMapping[new Date(activeRoom?.booking?.booking_start_time).getMonth()] + + " " + + new Date(activeRoom?.booking?.booking_start_time).getDate() + + "/" + + new Date(activeRoom?.booking?.booking_start_time).getFullYear()} +

    +
    +
    +

    Time

    +

    + {formatAMPM(activeRoom.booking?.booking_start_time)} - {formatAMPM(activeRoom?.booking?.booking_end_time)} +

    +
    +
    +

    Duration

    +

    {activeRoom?.booking?.duration / 3600} hours

    +
    +
    +
    +

    Add-ons:

    +
    + {activeRoom?.booking?.add_ons?.map((addon, idx) => ( +
    + +

    {addon.name}

    +
    + ))} +
    +
    + + ) : null} +
    + {(activeRoom?.booking_id && activeRoom?.booking?.id) && + + View booking + } + + {activeRoom.id && !activeRoom?.booking?.id && + + View property + + } +
    +
    +
    +
    +
    + + {showEmoji && ( +
    +
    + { + setMessage((prev) => prev + em.emoji); + setShowEmoji(false); + }} + searchDisabled + /> +
    +
    + )} + + {showImagePreviewModal && + getRooms()} state={state} setMessages={setMessages} setActiveRoom={setActiveRoom} setRooms={setRooms} spaceId={spaceId} activeBooking={activeBooking} setShowImagePreviewModal={setShowImagePreviewModal}/> + } + + {activeRoom?.booking_id === null && + setShowMap(false)} + /> + } +
    + + ); +}; + +export default MessagesPage; \ No newline at end of file diff --git a/src/pages/Common/Messages/badWords.json b/src/pages/Common/Messages/badWords.json new file mode 100644 index 0000000..b886761 --- /dev/null +++ b/src/pages/Common/Messages/badWords.json @@ -0,0 +1,405 @@ +[ + "2g1c", + "2 girls 1 cup", + "acrotomophilia", + "alabama hot pocket", + "alaskan pipeline", + "anal", + "anilingus", + "anus", + "apeshit", + "arsehole", + "ass", + "asshole", + "assmunch", + "auto erotic", + "autoerotic", + "babeland", + "baby batter", + "baby juice", + "ball gag", + "ball gravy", + "ball kicking", + "ball licking", + "ball sack", + "ball sucking", + "bangbros", + "bangbus", + "bareback", + "barely legal", + "barenaked", + "bastard", + "bastardo", + "bastinado", + "bbw", + "bdsm", + "beaner", + "beaners", + "beaver cleaver", + "beaver lips", + "beastiality", + "bestiality", + "big black", + "big breasts", + "big knockers", + "big tits", + "bimbos", + "birdlock", + "bitch", + "bitches", + "black cock", + "blonde action", + "blonde on blonde action", + "blowjob", + "blow job", + "blow your load", + "blue waffle", + "blumpkin", + "bollocks", + "bondage", + "boner", + "boob", + "boobs", + "booty call", + "brown showers", + "brunette action", + "bukkake", + "bulldyke", + "bullet vibe", + "bullshit", + "bung hole", + "bunghole", + "busty", + "butt", + "buttcheeks", + "butthole", + "camel toe", + "camgirl", + "camslut", + "camwhore", + "carpet muncher", + "carpetmuncher", + "chocolate rosebuds", + "cialis", + "circlejerk", + "cleveland steamer", + "clit", + "clitoris", + "clover clamps", + "clusterfuck", + "cock", + "cocks", + "coprolagnia", + "coprophilia", + "cornhole", + "coon", + "coons", + "creampie", + "cum", + "cumming", + "cumshot", + "cumshots", + "cunnilingus", + "cunt", + "darkie", + "date rape", + "daterape", + "deep throat", + "deepthroat", + "dendrophilia", + "dick", + "dildo", + "dingleberry", + "dingleberries", + "dirty pillows", + "dirty sanchez", + "doggie style", + "doggiestyle", + "doggy style", + "doggystyle", + "dog style", + "dolcett", + "domination", + "dominatrix", + "dommes", + "donkey punch", + "double dong", + "double penetration", + "dp action", + "dry hump", + "dvda", + "eat my ass", + "ecchi", + "ejaculation", + "erotic", + "erotism", + "escort", + "eunuch", + "fag", + "faggot", + "fecal", + "felch", + "fellatio", + "feltch", + "female squirting", + "femdom", + "figging", + "fingerbang", + "fingering", + "fisting", + "foot fetish", + "footjob", + "frotting", + "fuck", + "fuck buttons", + "fuckin", + "fucking", + "fucktards", + "fudge packer", + "fudgepacker", + "futanari", + "gangbang", + "gang bang", + "gay sex", + "genitals", + "giant cock", + "girl on", + "girl on top", + "girls gone wild", + "goatcx", + "goatse", + "god damn", + "gokkun", + "golden shower", + "goodpoop", + "goo girl", + "goregasm", + "grope", + "group sex", + "g-spot", + "guro", + "hand job", + "handjob", + "hard core", + "hardcore", + "hentai", + "homoerotic", + "honkey", + "hooker", + "horny", + "hot carl", + "hot chick", + "how to kill", + "how to murder", + "huge fat", + "humping", + "incest", + "intercourse", + "jack off", + "jail bait", + "jailbait", + "jelly donut", + "jerk off", + "jigaboo", + "jiggaboo", + "jiggerboo", + "jizz", + "juggs", + "kike", + "kinbaku", + "kinkster", + "kinky", + "knobbing", + "leather restraint", + "leather straight jacket", + "lemon party", + "livesex", + "lolita", + "lovemaking", + "make me come", + "male squirting", + "masturbate", + "masturbating", + "masturbation", + "menage a trois", + "milf", + "missionary position", + "mong", + "motherfucker", + "mound of venus", + "mr hands", + "muff diver", + "muffdiving", + "nambla", + "nawashi", + "negro", + "neonazi", + "nigga", + "nigger", + "nig nog", + "nimphomania", + "nipple", + "nipples", + "nsfw", + "nsfw images", + "nude", + "nudity", + "nutten", + "nympho", + "nymphomania", + "octopussy", + "omorashi", + "one cup two girls", + "one guy one jar", + "orgasm", + "orgy", + "paedophile", + "paki", + "panties", + "panty", + "pedobear", + "pedophile", + "pegging", + "penis", + "phone sex", + "piece of shit", + "pikey", + "pissing", + "piss pig", + "pisspig", + "playboy", + "pleasure chest", + "pole smoker", + "ponyplay", + "poof", + "poon", + "poontang", + "punany", + "poop chute", + "poopchute", + "porn", + "porno", + "pornography", + "prince albert piercing", + "pthc", + "pubes", + "pussy", + "queaf", + "queef", + "quim", + "raghead", + "raging boner", + "rape", + "raping", + "rapist", + "rectum", + "reverse cowgirl", + "rimjob", + "rimming", + "rosy palm", + "rosy palm and her 5 sisters", + "rusty trombone", + "sadism", + "santorum", + "scat", + "schlong", + "scissoring", + "semen", + "sex", + "sexcam", + "sexo", + "sexy", + "sexual", + "sexually", + "sexuality", + "shaved beaver", + "shaved pussy", + "shemale", + "shibari", + "shit", + "shitblimp", + "shitty", + "shota", + "shrimping", + "skeet", + "slanteye", + "slut", + "s&m", + "smut", + "snatch", + "snowballing", + "sodomize", + "sodomy", + "spastic", + "spic", + "splooge", + "splooge moose", + "spooge", + "spread legs", + "spunk", + "strap on", + "strapon", + "strappado", + "strip club", + "style doggy", + "suck", + "sucks", + "suicide girls", + "sultry women", + "swastika", + "swinger", + "tainted love", + "taste my", + "tea bagging", + "threesome", + "throating", + "thumbzilla", + "tied up", + "tight white", + "tit", + "tits", + "titties", + "titty", + "tongue in a", + "topless", + "tosser", + "towelhead", + "tranny", + "tribadism", + "tub girl", + "tubgirl", + "tushy", + "twat", + "twink", + "twinkie", + "two girls one cup", + "undressing", + "upskirt", + "urethra play", + "urophilia", + "vagina", + "venus mound", + "viagra", + "vibrator", + "violet wand", + "vorarephilia", + "voyeur", + "voyeurweb", + "voyuer", + "vulva", + "wank", + "wetback", + "wet dream", + "white power", + "whore", + "worldsex", + "wrapping men", + "wrinkled starfish", + "xx", + "xxx", + "yaoi", + "yellow showers", + "yiffy", + "zoophilia", + "đŸ–•" +] \ No newline at end of file diff --git a/src/pages/Common/PrivacyPolicyPage.jsx b/src/pages/Common/PrivacyPolicyPage.jsx new file mode 100644 index 0000000..aee9a8c --- /dev/null +++ b/src/pages/Common/PrivacyPolicyPage.jsx @@ -0,0 +1,48 @@ +import { GlobalContext } from "@/globalContext"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import MkdSDK from "@/utils/MkdSDK"; +import React, { useState } from "react"; +import { useContext } from "react"; +import { useEffect } from "react"; + +export default function PrivacyPolicyPage() { + const [content, setContent] = useState(""); + const { dispatch: globalDispatch } = useContext(GlobalContext); + + async function fetchPrivacyPolicy() { + globalDispatch({ type: "START_LOADING" }); + const sdk = new MkdSDK(); + sdk.setTable("cms"); + try { + const result = await callCustomAPI("cms", "post", { payload: { content_key: "privacy_policy" }, limit: 1000, page: 1 }, "PAGINATE"); + + if (Array.isArray(result.list) && result.list.length > 0) { + setContent(result.list.find((stg) => stg.content_key == "privacy_policy")?.content_value); + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Cannot get Privacy policy", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + useEffect(() => { + fetchPrivacyPolicy(); + }, []); + + return ( +
    +
    +
    +
    +
    + ); +} diff --git a/src/pages/Common/SearchPage.jsx b/src/pages/Common/SearchPage.jsx new file mode 100644 index 0000000..6d62474 --- /dev/null +++ b/src/pages/Common/SearchPage.jsx @@ -0,0 +1,476 @@ +import React from "react"; +import { useEffect } from "react"; +import { useState } from "react"; +import { useSearchParams } from "react-router-dom"; +import PropertySpaceTile from "@/components/frontend/PropertySpaceTile"; +import "react-calendar/dist/Calendar.css"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import PeopleIcon from "@/components/frontend/icons/PeopleIcon"; +import { GlobalContext } from "@/globalContext"; +import { useContext } from "react"; +import CustomSelect from "@/components/frontend/CustomSelect"; +import FilterIcon from "@/components/frontend/icons/FilterIcon"; +import { Tooltip } from "react-tooltip"; +import NoteIcon from "@/components/frontend/icons/NoteIcon"; +import { formatDate } from "@/utils/date-time-utils"; +import { DRAFT_STATUS, SPACE_STATUS } from "@/utils/constants"; +import { useForm } from "react-hook-form"; +import CustomLocationAutoCompleteV2 from "@/components/CustomLocationAutoCompleteV2"; +import DatePickerV3 from "@/components/DatePickerV3"; +import { isValidDate, parseSearchParams } from "@/utils/utils"; +import FilterCheckBoxesV2 from "@/components/FilterCheckBoxesV2"; +import MkdSDK from "@/utils/MkdSDK"; +import useAmenityCategories from "@/hooks/api/useAmenityCategories"; +import CustomStaticLocationAutoCompleteV2 from "@/components/CustomStaticLocationAutoCompleteV2 "; + +const prices = [ + { name: "$0 - $30", id: 0 }, + { name: "$31 - $60", id: 1 }, + { name: "$60 - $90", id: 2 }, + { name: "$90 - $120", id: 3 }, + { name: "$120 - $150", id: 4 }, + { name: "$150 - $180", id: 5 }, +]; +const capacity = [ + { name: "0 - 4", id: 0 }, + { name: "5 - 9", id: 1 }, + { name: "10 - 14", id: 2 }, + { name: "15 - 19", id: 3 }, + { name: "20 - 24", id: 4 }, + { name: "25 - 30", id: 5 }, + { name: "Greater Than 30", id: 6 }, +]; + +const reviews = [ + { name: "4", id: 0 }, + { name: "3", id: 1 }, + { name: "2", id: 2 }, + { name: "1", id: 3 }, +]; + +const sdk = new MkdSDK(); +const ctrl = new AbortController(); + +const SearchPage = () => { + const [searchParams, setSearchParams] = useSearchParams(); + + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + + const [filterPopup, setFilterPopup] = useState(false); + + const spaceCategories = globalState.spaceCategories; + const amenityCategories = useAmenityCategories(); + const [propertySpaces, setPropertySpaces] = useState([]); + const [render, forceRender] = useState(false); + + const { handleSubmit, control, setValue, resetField, register } = useForm({ + defaultValues: (() => { + const params = parseSearchParams(searchParams); + return { + ...params, + location: params.location ?? "", + booking_start_time: "", + category: params.category?.split(",") || [], + capacity: params.capacity?.split(",") || [], + price: params.price?.split(",") || [], + amenity: params.amenity?.split(",") || [], + review: params.review?.split(",") || [], + }; + })(), + }); + const [sortAsc, setSortAsc] = useState(false); + + async function fetchSpaces() { + const params = parseSearchParams(searchParams); + const location = (params.location?.split(",")) + const d = new Date(params.booking_start_time || undefined); + + const filter = { + ...params, + booking_start_time: isNaN(d) ? undefined : d, + category: params.category?.split(",") || [], + price: params.price?.split(",") || [], + capacity: params.capacity?.split(",") || [], + amenity: params.amenity?.split(",") || [], + review: params.review?.split(",") || [], + }; + globalDispatch({ type: "START_LOADING" }); + + // make sure only approved and non-draft spaces + var where = [`ergo_property_spaces.space_status = ${SPACE_STATUS.APPROVED} AND schedule_template_id IS NOT NULL AND ergo_property_spaces_images.is_approved = 1 AND ergo_property_spaces.draft_status = ${DRAFT_STATUS.COMPLETED} AND ergo_property_spaces.deleted_at IS NULL`]; + + // use data.location to search address, city, country and zip + if (filter.location) { + where.push( + `(ergo_property.address_line_1 LIKE '%${filter.location}%' OR ergo_property.address_line_2 LIKE '%${filter.location}%' OR ergo_property.city LIKE '%${location[0] && location[0]}%' OR ergo_property.country LIKE '%${location.length === 1 ? location[0] : location.length === 2 ? location[1] : location[2]}%' OR ergo_property.zip LIKE '%${filter.location}%' OR ergo_property.name LIKE '%${filter.location}%')`, + ); + } + + if (filter.size) { + where.push(`ergo_property_spaces.size = ${filter.size}`); + } + + if (filter.capacity.length > 0) { + if (filter.capacity[filter.capacity.length-1] !== "Greater Than 30") { + const str = filter.capacity[filter.capacity.length-1]; // Get the first (and only) element from the array + const numbers = str.split('-').map(num => num.trim()); // Split the string and trim spaces + const [num1, num2] = numbers; // Destructure the resulting array to get the numbers + where.pop() + where.push( + `ergo_property_spaces.max_capacity BETWEEN ${num1} AND ${num2}`, + ); + } else { + where.push( + `ergo_property_spaces.max_capacity > 30`, + ); + } + } + + if (filter.category.length > 0) { + where.push(`(${filter.category.map((cg) => `ergo_spaces.category LIKE '%${cg}%'`).join(" OR ")})`); + } + + if (filter.amenity.length > 0) { + where.push(`(${filter.amenity.map((am) => `ergo_amenity.name LIKE '%${am}%'`).join(" OR ")})`); + } + + if (filter.review.length > 0) { + where.push(`(${filter.review.map((rv) => `ER.average_space_rating >= ${rv.replace("+", "")}`).join(" OR ")})`); + } + + if (filter.price.length > 0) { + where.push( + `(${filter.price + .filter((pr) => pr.trim() != "") + .map((pr) => pr.split("-")) + .map(([from, to]) => `ergo_property_spaces.rate BETWEEN ${from.trim().slice(1)} AND ${to.trim().slice(1)} `) + .join(" OR ")})`, + ); + } + + try { + const user_id = Number(localStorage.getItem("user")); + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/popular/PAGINATE", + { page: 1, limit: 10000, user_id: Number(user_id), where, booking_start_time: isValidDate(filter.booking_start_time || "") ? new Date(filter.booking_start_time).toISOString() : undefined }, + "POST", + ctrl.signal, + ); + setPropertySpaces(result.list); + } catch (err) { + console.log("err", err); + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + useEffect(() => { + if (isValidDate(searchParams.get("booking_start_time"))) { + setValue("booking_start_time", new Date(searchParams.get("booking_start_time")), { shouldDirty: true }); + } + }, []); + + useEffect(() => { + if (render) { + fetchSpaces(); + } + }, [render]); + + const removeFilter = (searchField, arrEl) => { + if (!arrEl) { + setValue(searchField, ""); + searchParams.set(searchField, ""); + setSearchParams(searchParams); + } else { + const prev = searchParams.get(searchField) ?? ""; + const arr = prev?.split(",") || []; + const removeElIndex = arr.indexOf(arrEl); + if (removeElIndex > -1) { + arr.splice(removeElIndex, 1); + setValue(searchField, arr); + searchParams.set(searchField, arr.join(",")); + } + } + setSearchParams(searchParams); + }; + + async function onSubmit(data) { + if (globalState.location && globalState.location.includes("undefined")) { + const parts = globalState.location.split(","); + const result = parts[0].trim(); + globalState.location = result; + } + searchParams.set("location", globalState.location); + searchParams.set("booking_start_time", isValidDate(data.booking_start_time) ? data.booking_start_time.toISOString() : ""); + searchParams.set("category", data.category.join(",")); + searchParams.set("price", data.price.join(",")); + searchParams.set("amenity", data.amenity.join(",")); + searchParams.set("review", data.review.join(",")); + if (data.max_capacity !== "NaN") { + searchParams.set("max_capacity", Number(data.max_capacity)); + } + searchParams.set("capacity", data.capacity); + setSearchParams(searchParams); + } + + useEffect(() => { + fetchSpaces(); + }, [searchParams]); + + const sortRating = (a, b) => { + if (sortAsc == 1) { + return (a.average_space_rating ?? 0) - (b.average_space_rating ?? 0); + } + return (b.average_space_rating ?? 0) - (a.average_space_rating ?? 0); + }; + + return ( +
    +
    +
    +
    + +
    +
    +
    + {propertySpaces.length == 0 ? ( + "No results Found" + ) : ( + <> + {" "} + Results Found ({propertySpaces.length}) + + )} +
    + +
    +
    + {propertySpaces.length == 0 && ( +
    +

    + No results found +

    +
    + )} + {propertySpaces.sort(sortRating).map((sp) => ( + + ))} +
    +
    +
    + +
    + ); +}; + +export default SearchPage; diff --git a/src/pages/Common/SignUp/BecomeAHostPage.jsx b/src/pages/Common/SignUp/BecomeAHostPage.jsx new file mode 100644 index 0000000..3c0c396 --- /dev/null +++ b/src/pages/Common/SignUp/BecomeAHostPage.jsx @@ -0,0 +1,464 @@ +import { AuthContext } from "@/authContext"; +import { LoadingButton } from "@/components/frontend"; +import DatePickerV2 from "@/components/frontend/DatePickerV2"; +import { GlobalContext } from "@/globalContext"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import { NOTIFICATION_STATUS, NOTIFICATION_TYPE } from "@/utils/constants"; +import MkdSDK from "@/utils/MkdSDK"; +import { yupResolver } from "@hookform/resolvers/yup"; +import moment from "moment/moment"; +import React, { useContext, useEffect, useState, useRef } from "react"; +import { FileUploader } from "react-drag-drop-files"; +import { useForm } from "react-hook-form"; +import { Navigate, useNavigate } from "react-router"; +import { Link } from "react-router-dom"; +import countries from "@/utils/countries.json"; +import * as yup from "yup"; +import CustomLocationAutoCompleteV2 from "@/components/CustomLocationAutoCompleteV2"; +import CustomComboBox from "@/components/CustomComboBox"; + +const readImage = (file, previewEl) => { + const reader = new FileReader(); + reader.onload = (event) => { + document.getElementById(previewEl).src = event.target.result; + }; + + reader.readAsDataURL(file); +}; + +async function getFileFromUrl(url) { + if (!url) return null; + try { + let response = await fetch(url); + let data = await response.blob(); + let metadata = { + type: "image/jpeg", + }; + return new File([data], url.split("/").pop(), metadata); + } catch (err) { + return null; + } +} + +export default function BecomeAHostPage() { + const initialDate = useRef(new Date()); + const { state: globalState, dispatch: globalDispatch } = useContext(GlobalContext); + const { dispatch: authDispatch } = useContext(AuthContext); + const [frontImage, setFrontImage] = useState(null); + const [backImage, setBackImage] = useState(null); + const [passport, setPassport] = useState(null); + const [loading, setLoading] = useState(false); + const [imageErr, setImageErr] = useState(""); + + const navigate = useNavigate(); + + const schema = yup.object({ + // dob: yup + // .string() + // .required("This field is required") + // .test("is-not-in-future", "Not a valid date", (val) => { + // if (val == "") return true; + // const date = new Date(val); + // return date.setDate(date.getDate() + 1) < new Date(); + // }), + expiry_date: yup + .string() + .required("This field is required") + .test("is-not-in-past", "Invalid expiry date", (val) => { + const date = new Date(val); + return date.setDate(date.getDate() - 1) > new Date(); + }), + city: yup.string().required("This field is required"), + country: yup.string().required("This field is required"), + selectedType: yup.string().required("This field is required"), + about: yup.string().required("This field is required"), + }); + + const { + handleSubmit, + register, + setValue, + control, + watch, + formState: { errors }, + } = useForm({ + defaultValues: { + dob: globalState.user.dob ? moment(globalState.user.dob).format("yyyy-MM-DD") : "", + expiry_date: globalState.user.verificationExpiry ? moment(globalState.user.verificationExpiry).format("yyyy-MM-DD") : "", + city: globalState.user.city || "", + country: globalState.user.country || "", + selectedType: globalState.user.verificationType || "Driver's License", + about: globalState.user.about || "", + }, + resolver: yupResolver(schema), + }); + const sdk = new MkdSDK(); + + const selectedType = watch("selectedType"); + + const handleImageUpload = async (file) => { + const formData = new FormData(); + formData.append("file", file); + try { + const upload = await sdk.uploadImage(formData); + return upload.url; + } catch (err) { + console.log("err", err); + return ""; + } + }; + + async function onSubmit(data) { + // check if images are uploaded + if (selectedType == "Driver's License" && (!frontImage || !backImage)) { + setImageErr("Please upload required documents"); + return; + } + + if (selectedType == "Passport" && !passport) { + setImageErr("Please upload required documents"); + return; + } + + console.log("submitting", data); + setLoading(true); + try { + // edit user + await callCustomAPI( + "edit-self", + "post", + { + user: { role: ["superadmin", "admin"].includes(globalState.user.role) ? undefined : "host" }, + profile: { + city: data.city, + country: data.country, + // dob: isSameDay(data.dob, initialDate.current) ? undefined : moment(data.dob).format("yyyy-MM-DD"), + about: data.about, + getting_started: 0, + }, + }, + "", + ); + // submit id verification + if (selectedType == "Driver's License") { + data.image_front = await handleImageUpload(frontImage); + data.image_back = await handleImageUpload(backImage); + } else { + data.image_front = await handleImageUpload(passport); + } + + sdk.setTable("id_verification"); + const result = await sdk.callRestAPI( + { + id: globalState.user.verificationId, + type: selectedType, + expiry_date: data.expiry_date, + status: 0, + image_front: data.image_front, + image_back: data.image_back, + user_id: Number(localStorage.getItem("user")), + }, + globalState.user.verificationId ? "PUT" : "POST", + ); + + // create notification + sdk.setTable("notification"); + await sdk.callRestAPI( + { + user_id: Number(localStorage.getItem("user")), + actor_id: null, + action_id: result.message, + notification_time: new Date().toISOString().split(".")[0], + message: "New ID Verification submitted", + type: NOTIFICATION_TYPE.NEW_ID_VERIFICATION, + status: NOTIFICATION_STATUS.NOT_ADDRESSED, + }, + "POST", + ); + + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Success", + message: `Host account created, please re login to your account`, + btn: "Ok got it", + onClose: () => { + sdk.logout(); + authDispatch({ type: "LOGOUT" }); + navigate("/login"); + }, + }, + }); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + setLoading(false); + } + + useEffect(() => { + (async () => { + const front = await getFileFromUrl(globalState.user.verificationImageFront); + const back = await getFileFromUrl(globalState.user.verificationImageBack); + if (globalState.user.verificationType == "Passport") { + setPassport(front); + } else { + setFrontImage(front); + setBackImage(back); + } + })(); + }, []); + + if (!globalState.user.id) return ; + + return ( +
    +
    +

    Become A Host

    +

    Gain the ability to rent your spaces by giving us some additional information

    + +

    Location

    +
    +
    + + setValue("city", val)} + name="city" + className={`w-full rounded border py-2 px-3 leading-tight text-gray-700 ${errors.city?.message ? "border-red-500 focus:outline-red-500" : "focus-within:outline-primary"}`} + placeholder="" + hideIcons + suggestionType={["(cities)"]} + /> + + {/*
    + +
    */} +

    {errors.city?.message}

    +
    +
    + + setValue("country", val)} + items={countries} + containerClassName="relative w-full" + className={`w-full truncate border py-2 px-3 text-black ${errors.country?.message ? "border-red-500 focus:outline-red-500" : "focus-within:outline-primary"}`} + placeholder="" + /> + {/*
    + +
    */} +

    {errors.country?.message}

    +
    +
    +

    Profile Information

    +
    + + setValue("dob", v)} + /> +
    +
    + + +

    {errors.about?.message}

    +
    + +

    Identity Verification

    +

    Explain what document(s) are allowed.

    +
    + + +
    +

    {imageErr}

    +
    + {selectedType == "Driver's License" ? ( +
    + { + setFrontImage(file); + }} + types={["SVG", "JPEG", "PNG", "GIF", "JPG"]} + > +
    + {frontImage?.name ? ( + + ) : ( + <> +

    Front

    +

    + Click to upload or drag and drop SVG, PNG, JPG or GIF (max. 800x400px) +

    + + )} +
    +
    + { + setBackImage(file); + }} + types={["SVG", "JPEG", "PNG", "GIF", "JPG"]} + > +
    + {backImage?.name ? ( + + ) : ( + <> +

    Back

    +

    + Click to upload or drag and drop SVG, PNG, JPG or GIF (max. 800x400px) +

    + + )} +
    +
    +
    + ) : ( + { + setPassport(file); + }} + types={["SVG", "JPEG", "PNG", "GIF", "JPG"]} + > +
    + {passport?.name ? ( + + ) : ( + <> +

    Passport page with photo

    +

    + Click to upload or drag and drop SVG, PNG, JPG or GIF (max. 800x400px) +

    + + )} +
    +
    + )} +
    +
    + + setValue("expiry_date", v)} + /> +
    +
    + + Cancel + + + Continue + +
    +
    +
    + ); +} diff --git a/src/pages/Common/SignUp/CheckVerificationPage.jsx b/src/pages/Common/SignUp/CheckVerificationPage.jsx new file mode 100644 index 0000000..0d084af --- /dev/null +++ b/src/pages/Common/SignUp/CheckVerificationPage.jsx @@ -0,0 +1,28 @@ +import { AuthContext } from "@/authContext"; +import React, { useContext, useEffect } from "react"; +import { Navigate } from "react-router"; + +export default function CheckVerificationPage() { + const { state: authState, dispatch: authDispatch } = useContext(AuthContext); + + useEffect(() => { + let timeout; + + timeout = setTimeout(() => { + authDispatch({ type: "DISALLOW_CHECK_VERIFICATION" }); + }, 10000); + + return () => clearTimeout(timeout); + }, []); + + if (!authState.allowCheckVerification) return ; + + return ( +
    +
    +

    Account Created successfully. Please check your email to verify your account

    +

    You'll be redirected to login page shortly

    +
    +
    + ); +} diff --git a/src/pages/Common/SignUp/PageWrapper.jsx b/src/pages/Common/SignUp/PageWrapper.jsx new file mode 100644 index 0000000..de269f1 --- /dev/null +++ b/src/pages/Common/SignUp/PageWrapper.jsx @@ -0,0 +1,27 @@ +import React from "react"; +import { Outlet } from "react-router"; +import { Link, useSearchParams } from "react-router-dom"; +import Icon from "@/components/Icons"; +import { SignUpContextProvider } from "./signUpContext"; + +const PageWrapper = () => { + return ( + +
    +
    + + + +
    +
    + +
    +
    +
    + ); +}; + +export default PageWrapper; diff --git a/src/pages/Common/SignUp/PrivacyAndPolicyModal.jsx b/src/pages/Common/SignUp/PrivacyAndPolicyModal.jsx new file mode 100644 index 0000000..2989c6d --- /dev/null +++ b/src/pages/Common/SignUp/PrivacyAndPolicyModal.jsx @@ -0,0 +1,109 @@ +import React from 'react' +import { GlobalContext } from "@/globalContext"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import { Dialog, Transition } from "@headlessui/react"; +import { useEffect } from "react"; +import { useContext } from "react"; +import { useState } from "react"; +import { Fragment } from "react"; +import MkdSDK from "@/utils/MkdSDK"; + +const PrivacyAndPolicyModal = ({ isOpen, closeModal }) => { + const [privacy, setPrivacy] = useState(""); + const { dispatch: globalDispatch } = useContext(GlobalContext); + + async function fetchPrivacyPolicy() { + globalDispatch({ type: "START_LOADING" }); + const sdk = new MkdSDK(); + sdk.setTable("cms"); + try { + const result = await callCustomAPI("cms", "post", { payload: { content_key: "privacy_policy" }, limit: 1000, page: 1 }, "PAGINATE"); + + if (Array.isArray(result.list) && result.list.length > 0) { + setPrivacy(result.list.find((stg) => stg.content_key == "privacy_policy")?.content_value); + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Cannot get Privacy policy", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + useEffect(() => { + fetchPrivacyPolicy(); + }, []); + + return ( + <> +
    + + + + +
    + + +
    +
    + + + + {" "} + {" "} + + +
    +
    +
    +
    +
    +
    +
    +
    +
    + + ) +} + +export default PrivacyAndPolicyModal \ No newline at end of file diff --git a/src/pages/Common/SignUp/SignUpDetailsForm.jsx b/src/pages/Common/SignUp/SignUpDetailsForm.jsx new file mode 100644 index 0000000..5b264c9 --- /dev/null +++ b/src/pages/Common/SignUp/SignUpDetailsForm.jsx @@ -0,0 +1,247 @@ +import React from "react"; +import { Navigate, useNavigate } from "react-router"; +import { useSignUpContext } from "./signUpContext"; + +import { yupResolver } from "@hookform/resolvers/yup"; +import { useForm } from "react-hook-form"; +import * as yup from "yup"; +import { AuthContext } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { Link } from "react-router-dom"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import { useRef } from "react"; +import { isSameDay } from "@/utils/date-time-utils"; +import moment from "moment/moment"; +import TermsAndConditionsModal from "./TermsAndConditionsModal"; +import DatePickerV2 from "@/components/frontend/DatePickerV2"; +import { LoadingButton } from "@/components/frontend"; +import PrivacyAndPolicyModal from "./PrivacyAndPolicyModal"; + +export default function SignUpDetailsForm() { + const navigate = useNavigate(); + const { signUpData } = useSignUpContext(); + const role = signUpData.role; + const { dispatch: authDispatch } = React.useContext(AuthContext); + const [showPassword, setShowPassword] = React.useState(false); + const [loading, setLoading] = React.useState(false); + const sdk = new MkdSDK(); + const [modalOpen, setModalOpen] = React.useState(false); + const [privacyOpen, setPrivacyModalOpen] = React.useState(false); + const initialDate = useRef(new Date()); + + function closeModal() { + setModalOpen(false); + } + function closePrivacyModal() { + setPrivacyModalOpen(false); + } + + const schema = yup.object({ + firstName: yup.string(), + lastName: yup.string(), + dob: yup.date(), + password: yup.string() + }); + + const { + register, + setError, + handleSubmit, + trigger, + watch, + setValue, + control, + formState: { errors, dirtyFields }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + firstName: signUpData.firstName, + lastName: signUpData.lastName, + dob: initialDate.current, + password: signUpData.password, + }, + criteriaMode: "all", + }); + + const data = watch(); + + async function onSubmit() { + setLoading(true); + try { + const result = await sdk.register(signUpData.email, data.password, role); + if (!result.error) { + localStorage.setItem("token", result.token); + + // register device + sdk.setTable("device"); + await sdk.callRestAPI({ active: 1, user_id: result.user_id, last_login_time: new Date().toISOString().split("T")[0], uid: localStorage.getItem("device-uid") }, "POST"); + + await callCustomAPI( + "edit-self", + "post", + { + user: { + first_name: data.firstName, + last_name: data.lastName, + }, + profile: { + dob: isSameDay(data.dob, initialDate.current) ? undefined : moment(data.dob).format("yyyy-MM-DD"), + }, + }, + "", + result.token, + ); + + localStorage.removeItem("token"); + + authDispatch({ type: "ALLOW_CHECK_VERIFICATION" }); + navigate("/check-verification"); + localStorage.setItem("first_login", result.user_id); + setLoading(false); + } else { + setLoading(false); + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + + } + } + } + + } catch (err) { + setLoading(false); + setError("firstName", { + type: "manual", + message: err.message, + }); + } + } + + if (!signUpData.email) return ; + + return ( + <> +
    +
    +

    Finish Signing Up

    +
    + +

    {errors.firstName?.message}

    +
    + +
    + +

    {errors.lastName?.message}

    +
    + + setValue("dob", v)} + /> +
    + { + trigger("password"); + }, + })} + className="flex-grow rounded-md border p-2 px-4 focus:outline-none active:outline-none " + placeholder="Password" + />{" "} + +
    + + +

    + Select and agree to {" "} + + {" "} + to continue. + {" "} + {" "} + +

    + + Continue + + +
    +
    +
    + + + + ); +} diff --git a/src/pages/Common/SignUp/SignUpForm.jsx b/src/pages/Common/SignUp/SignUpForm.jsx new file mode 100644 index 0000000..c4f64c3 --- /dev/null +++ b/src/pages/Common/SignUp/SignUpForm.jsx @@ -0,0 +1,150 @@ +import React, { useState } from "react"; +import { Link, Navigate, useNavigate } from "react-router-dom"; + +import { yupResolver } from "@hookform/resolvers/yup"; +import { useForm } from "react-hook-form"; +import * as yup from "yup"; +import { useSignUpContext } from "./signUpContext"; +import { callCustomAPI, oauthLoginApi } from "@/utils/callCustomAPI"; +import { LoadingButton } from "@/components/frontend"; +import TLDs from "@/assets/json/email-tlds.json"; + +const SignUpForm = () => { + const navigate = useNavigate(); + const { signUpData, dispatch } = useSignUpContext(); + const role = signUpData.role; + const schema = yup.object({ + email: yup + .string(), + }); + const [loading, setLoading] = useState(false); + + const { + register, + handleSubmit, + setError, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + email: signUpData.email, + }, + }); + + const onSubmit = async (data) => { + setLoading(true); + try { + const result = await callCustomAPI("email-exist", "post", { email: data.email }, ""); + + if (result.error || result.exist) throw new Error("User already exists"); + + dispatch({ type: "SET_EMAIL", payload: data.email }); + navigate("/signup/details" + "?role=" + role); + } catch (err) { + setError("email", { type: "manual", message: err.message }); + } + setLoading(false); + }; + + const handleGoogleLogin = async () => { + const result = await oauthLoginApi("google", role); + window.open(result.data, "_self"); + }; + + const handleFacebookLogin = async () => { + const result = await oauthLoginApi("facebook", role); + window.open(result.data, "_self"); + }; + + const handleAppleLogin = async () => { + const result = await oauthLoginApi("apple", role); + window.open(result.data, "_self"); + }; + + + if (!signUpData.role) return ; + + return ( + <> +
    +
    +

    {role == "host" ? "Become a host" : "Sign up"}

    + + {Object.entries(errors).length > 0 ? ( +

    {Object.values(errors)[0].message}

    + ) : ( + <> + )} + + Continue + + +
    +
    OR
    +
    + + + +
    +

    + Already have an account?{" "} + + Log In + {" "} +

    +
    +
    +
    +
    + + ); +}; + +export default SignUpForm; diff --git a/src/pages/Common/SignUp/SignUpSelectRole.jsx b/src/pages/Common/SignUp/SignUpSelectRole.jsx new file mode 100644 index 0000000..09d0e0c --- /dev/null +++ b/src/pages/Common/SignUp/SignUpSelectRole.jsx @@ -0,0 +1,57 @@ +import NextIcon from "@/components/frontend/icons/NextIcon"; +import React from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { useSignUpContext } from "./signUpContext"; + +export default function SignUpSelectRole() { + const { dispatch } = useSignUpContext(); + const navigate = useNavigate(); + + function selectHost() { + dispatch({ type: "SET_ROLE", payload: "host" }); + navigate("/signup"); + } + + function selectCustomer() { + dispatch({ type: "SET_ROLE", payload: "customer" }); + navigate("/signup"); + } + + return ( + <> +
    +
    +

    Sign Up

    +

    Select an option below

    +
    +
    +
    +
    + + +
    +
    +
    + + ); +} diff --git a/src/pages/Common/SignUp/TermsAndConditionsModal.jsx b/src/pages/Common/SignUp/TermsAndConditionsModal.jsx new file mode 100644 index 0000000..72363ef --- /dev/null +++ b/src/pages/Common/SignUp/TermsAndConditionsModal.jsx @@ -0,0 +1,117 @@ +import { LoadingButton } from "@/components/frontend"; +import { GlobalContext } from "@/globalContext"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import { Dialog, Transition } from "@headlessui/react"; +import { useEffect } from "react"; +import { useContext } from "react"; +import { useState } from "react"; +import { Fragment } from "react"; + +export default function TermsAndConditionsModal({ isOpen, closeModal, setIsAgreed }) { + const [termsAndConditions, setTermsAndCondition] = useState(""); + const [agreed, setAgreed] = useState(false); + const { dispatch: globalDispatch } = useContext(GlobalContext); + + async function fetchTermsAndConditions() { + try { + const result = await callCustomAPI("cms", "post", { where: [`content_key = 'terms_and_conditions'`], limit: 1, page: 1 }, "PAGINATE"); + + if (Array.isArray(result.list) && result.list.length > 0) { + setTermsAndCondition(result.list[0].content_value); + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Cannot get Terms and Conditions", + message: err.message, + }, + }); + } + } + + useEffect(() => { + fetchTermsAndConditions(); + }, []); + return ( + <> +
    + + + + +
    + + +
    +
    + + + + {" "} + {" "} + + +
    +
    +
    +
    + {setAgreed((prev) => !prev); setIsAgreed((prev) => !prev); closeModal()}} + /> + +
    +
    +
    +
    +
    +
    +
    + + ); +} diff --git a/src/pages/Common/SignUp/VerifyEmailPage.jsx b/src/pages/Common/SignUp/VerifyEmailPage.jsx new file mode 100644 index 0000000..05e7a07 --- /dev/null +++ b/src/pages/Common/SignUp/VerifyEmailPage.jsx @@ -0,0 +1,53 @@ +import React, { useContext, useState } from "react"; +import { useEffect } from "react"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import MkdSDK from "@/utils/MkdSDK"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import { GlobalContext, showToast } from "@/globalContext"; + +const VerifyEmailPage = () => { + const [searchParams] = useSearchParams(); + const [pageText, setPageText] = useState("Verifying your email..."); + const navigate = useNavigate(); + const { dispatch: globalDispatch } = useContext(GlobalContext); + + let sdk = new MkdSDK(); + + async function verifyEmail() { + const token = searchParams.get("token"); + try { + const result = await sdk.verifyEmail(token); + + if (searchParams.get("is_manual") != "true") { + // only send signup confirmation email if email verification link was not triggered manually + const user = await callCustomAPI("get-user", "post", { id: result.user_id }, ""); + const tmpl = await sdk.getEmailTemplate("signup-confirmation"); + const body = tmpl.html?.replace(new RegExp("{{{first_name}}}", "g"), user.first_name).replace(new RegExp("{{{last_name}}}", "g"), user.last_name); + + await sdk.sendEmail(user.email, tmpl.subject, body); + } + + if (!result.error) { + showToast(globalDispatch, "Email verified", 3000, "success"); + } + + setPageText("Your account has been verified, You will be redirected to login page shortly"); + + setTimeout(() => { + navigate(`/login`); + }, 4000); + } catch (err) {} + } + + useEffect(() => { + verifyEmail(); + }, []); + + return ( +
    +

    {pageText}

    +
    + ); +}; + +export default VerifyEmailPage; diff --git a/src/pages/Common/SignUp/common-passwords.json b/src/pages/Common/SignUp/common-passwords.json new file mode 100644 index 0000000..8d8c7be --- /dev/null +++ b/src/pages/Common/SignUp/common-passwords.json @@ -0,0 +1,10003 @@ +[ + "password", + "123456", + "12345678", + "1234", + "qwerty", + "12345", + "dragon", + "pussy", + "baseball", + "football", + "letmein", + "monkey", + "696969", + "abc123", + "mustang", + "michael", + "shadow", + "master", + "jennifer", + "111111", + "2000", + "jordan", + "superman", + "harley", + "1234567", + "fuckme", + "hunter", + "fuckyou", + "trustno1", + "ranger", + "buster", + "thomas", + "tigger", + "robert", + "soccer", + "fuck", + "batman", + "test", + "pass", + "killer", + "hockey", + "george", + "charlie", + "andrew", + "michelle", + "love", + "sunshine", + "jessica", + "asshole", + "6969", + "pepper", + "daniel", + "access", + "123456789", + "654321", + "joshua", + "maggie", + "starwars", + "silver", + "william", + "dallas", + "yankees", + "123123", + "ashley", + "666666", + "hello", + "amanda", + "orange", + "biteme", + "freedom", + "computer", + "sexy", + "thunder", + "nicole", + "ginger", + "heather", + "hammer", + "summer", + "corvette", + "taylor", + "fucker", + "austin", + "1111", + "merlin", + "matthew", + "121212", + "golfer", + "cheese", + "princess", + "martin", + "chelsea", + "patrick", + "richard", + "diamond", + "yellow", + "bigdog", + "secret", + "asdfgh", + "sparky", + "cowboy", + "camaro", + "anthony", + "matrix", + "falcon", + "iloveyou", + "bailey", + "guitar", + "jackson", + "purple", + "scooter", + "phoenix", + "aaaaaa", + "morgan", + "tigers", + "porsche", + "mickey", + "maverick", + "cookie", + "nascar", + "peanut", + "justin", + "131313", + "money", + "horny", + "samantha", + "panties", + "steelers", + "joseph", + "snoopy", + "boomer", + "whatever", + "iceman", + "smokey", + "gateway", + "dakota", + "cowboys", + "eagles", + "chicken", + "dick", + "black", + "zxcvbn", + "please", + "andrea", + "ferrari", + "knight", + "hardcore", + "melissa", + "compaq", + "coffee", + "booboo", + "bitch", + "johnny", + "bulldog", + "xxxxxx", + "welcome", + "james", + "player", + "ncc1701", + "wizard", + "scooby", + "charles", + "junior", + "internet", + "bigdick", + "mike", + "brandy", + "tennis", + "blowjob", + "banana", + "monster", + "spider", + "lakers", + "miller", + "rabbit", + "enter", + "mercedes", + "brandon", + "steven", + "fender", + "john", + "yamaha", + "diablo", + "chris", + "boston", + "tiger", + "marine", + "chicago", + "rangers", + "gandalf", + "winter", + "bigtits", + "barney", + "edward", + "raiders", + "porn", + "badboy", + "blowme", + "spanky", + "bigdaddy", + "johnson", + "chester", + "london", + "midnight", + "blue", + "fishing", + "000000", + "hannah", + "slayer", + "11111111", + "rachel", + "sexsex", + "redsox", + "thx1138", + "asdf", + "marlboro", + "panther", + "zxcvbnm", + "arsenal", + "oliver", + "qazwsx", + "mother", + "victoria", + "7777777", + "jasper", + "angel", + "david", + "winner", + "crystal", + "golden", + "butthead", + "viking", + "jack", + "iwantu", + "shannon", + "murphy", + "angels", + "prince", + "cameron", + "girls", + "madison", + "wilson", + "carlos", + "hooters", + "willie", + "startrek", + "captain", + "maddog", + "jasmine", + "butter", + "booger", + "angela", + "golf", + "lauren", + "rocket", + "tiffany", + "theman", + "dennis", + "liverpoo", + "flower", + "forever", + "green", + "jackie", + "muffin", + "turtle", + "sophie", + "danielle", + "redskins", + "toyota", + "jason", + "sierra", + "winston", + "debbie", + "giants", + "packers", + "newyork", + "jeremy", + "casper", + "bubba", + "112233", + "sandra", + "lovers", + "mountain", + "united", + "cooper", + "driver", + "tucker", + "helpme", + "fucking", + "pookie", + "lucky", + "maxwell", + "8675309", + "bear", + "suckit", + "gators", + "5150", + "222222", + "shithead", + "fuckoff", + "jaguar", + "monica", + "fred", + "happy", + "hotdog", + "tits", + "gemini", + "lover", + "xxxxxxxx", + "777777", + "canada", + "nathan", + "victor", + "florida", + "88888888", + "nicholas", + "rosebud", + "metallic", + "doctor", + "trouble", + "success", + "stupid", + "tomcat", + "warrior", + "peaches", + "apples", + "fish", + "qwertyui", + "magic", + "buddy", + "dolphins", + "rainbow", + "gunner", + "987654", + "freddy", + "alexis", + "braves", + "cock", + "2112", + "1212", + "cocacola", + "xavier", + "dolphin", + "testing", + "bond007", + "member", + "calvin", + "voodoo", + "7777", + "samson", + "alex", + "apollo", + "fire", + "tester", + "walter", + "beavis", + "voyager", + "peter", + "porno", + "bonnie", + "rush2112", + "beer", + "apple", + "scorpio", + "jonathan", + "skippy", + "sydney", + "scott", + "red123", + "power", + "gordon", + "travis", + "beaver", + "star", + "jackass", + "flyers", + "boobs", + "232323", + "zzzzzz", + "steve", + "rebecca", + "scorpion", + "doggie", + "legend", + "ou812", + "yankee", + "blazer", + "bill", + "runner", + "birdie", + "bitches", + "555555", + "parker", + "topgun", + "asdfasdf", + "heaven", + "viper", + "animal", + "2222", + "bigboy", + "4444", + "arthur", + "baby", + "private", + "godzilla", + "donald", + "williams", + "lifehack", + "phantom", + "dave", + "rock", + "august", + "sammy", + "cool", + "brian", + "platinum", + "jake", + "bronco", + "paul", + "mark", + "frank", + "heka6w2", + "copper", + "billy", + "cumshot", + "garfield", + "willow", + "cunt", + "little", + "carter", + "slut", + "albert", + "69696969", + "kitten", + "super", + "jordan23", + "eagle1", + "shelby", + "america", + "11111", + "jessie", + "house", + "free", + "123321", + "chevy", + "bullshit", + "white", + "broncos", + "horney", + "surfer", + "nissan", + "999999", + "saturn", + "airborne", + "elephant", + "marvin", + "shit", + "action", + "adidas", + "qwert", + "kevin", + "1313", + "explorer", + "walker", + "police", + "christin", + "december", + "benjamin", + "wolf", + "sweet", + "therock", + "king", + "online", + "dickhead", + "brooklyn", + "teresa", + "cricket", + "sharon", + "dexter", + "racing", + "penis", + "gregory", + "0000", + "teens", + "redwings", + "dreams", + "michigan", + "hentai", + "magnum", + "87654321", + "nothing", + "donkey", + "trinity", + "digital", + "333333", + "stella", + "cartman", + "guinness", + "123abc", + "speedy", + "buffalo", + "kitty", + "pimpin", + "eagle", + "einstein", + "kelly", + "nelson", + "nirvana", + "vampire", + "xxxx", + "playboy", + "louise", + "pumpkin", + "snowball", + "test123", + "girl", + "sucker", + "mexico", + "beatles", + "fantasy", + "ford", + "gibson", + "celtic", + "marcus", + "cherry", + "cassie", + "888888", + "natasha", + "sniper", + "chance", + "genesis", + "hotrod", + "reddog", + "alexande", + "college", + "jester", + "passw0rd", + "bigcock", + "smith", + "lasvegas", + "carmen", + "slipknot", + "3333", + "death", + "kimberly", + "1q2w3e", + "eclipse", + "1q2w3e4r", + "stanley", + "samuel", + "drummer", + "homer", + "montana", + "music", + "aaaa", + "spencer", + "jimmy", + "carolina", + "colorado", + "creative", + "hello1", + "rocky", + "goober", + "friday", + "bollocks", + "scotty", + "abcdef", + "bubbles", + "hawaii", + "fluffy", + "mine", + "stephen", + "horses", + "thumper", + "5555", + "pussies", + "darkness", + "asdfghjk", + "pamela", + "boobies", + "buddha", + "vanessa", + "sandman", + "naughty", + "douglas", + "honda", + "matt", + "azerty", + "6666", + "shorty", + "money1", + "beach", + "loveme", + "4321", + "simple", + "poohbear", + "444444", + "badass", + "destiny", + "sarah", + "denise", + "vikings", + "lizard", + "melanie", + "assman", + "sabrina", + "nintendo", + "water", + "good", + "howard", + "time", + "123qwe", + "november", + "xxxxx", + "october", + "leather", + "bastard", + "young", + "101010", + "extreme", + "hard", + "password1", + "vincent", + "pussy1", + "lacrosse", + "hotmail", + "spooky", + "amateur", + "alaska", + "badger", + "paradise", + "maryjane", + "poop", + "crazy", + "mozart", + "video", + "russell", + "vagina", + "spitfire", + "anderson", + "norman", + "eric", + "cherokee", + "cougar", + "barbara", + "long", + "420420", + "family", + "horse", + "enigma", + "allison", + "raider", + "brazil", + "blonde", + "jones", + "55555", + "dude", + "drowssap", + "jeff", + "school", + "marshall", + "lovely", + "1qaz2wsx", + "jeffrey", + "caroline", + "franklin", + "booty", + "molly", + "snickers", + "leslie", + "nipples", + "courtney", + "diesel", + "rocks", + "eminem", + "westside", + "suzuki", + "daddy", + "passion", + "hummer", + "ladies", + "zachary", + "frankie", + "elvis", + "reggie", + "alpha", + "suckme", + "simpson", + "patricia", + "147147", + "pirate", + "tommy", + "semperfi", + "jupiter", + "redrum", + "freeuser", + "wanker", + "stinky", + "ducati", + "paris", + "natalie", + "babygirl", + "bishop", + "windows", + "spirit", + "pantera", + "monday", + "patches", + "brutus", + "houston", + "smooth", + "penguin", + "marley", + "forest", + "cream", + "212121", + "flash", + "maximus", + "nipple", + "bobby", + "bradley", + "vision", + "pokemon", + "champion", + "fireman", + "indian", + "softball", + "picard", + "system", + "clinton", + "cobra", + "enjoy", + "lucky1", + "claire", + "claudia", + "boogie", + "timothy", + "marines", + "security", + "dirty", + "admin", + "wildcats", + "pimp", + "dancer", + "hardon", + "veronica", + "fucked", + "abcd1234", + "abcdefg", + "ironman", + "wolverin", + "remember", + "great", + "freepass", + "bigred", + "squirt", + "justice", + "francis", + "hobbes", + "kermit", + "pearljam", + "mercury", + "domino", + "9999", + "denver", + "brooke", + "rascal", + "hitman", + "mistress", + "simon", + "tony", + "bbbbbb", + "friend", + "peekaboo", + "naked", + "budlight", + "electric", + "sluts", + "stargate", + "saints", + "bondage", + "brittany", + "bigman", + "zombie", + "swimming", + "duke", + "qwerty1", + "babes", + "scotland", + "disney", + "rooster", + "brenda", + "mookie", + "swordfis", + "candy", + "duncan", + "olivia", + "hunting", + "blink182", + "alicia", + "8888", + "samsung", + "bubba1", + "whore", + "virginia", + "general", + "passport", + "aaaaaaaa", + "erotic", + "liberty", + "arizona", + "jesus", + "abcd", + "newport", + "skipper", + "rolltide", + "balls", + "happy1", + "galore", + "christ", + "weasel", + "242424", + "wombat", + "digger", + "classic", + "bulldogs", + "poopoo", + "accord", + "popcorn", + "turkey", + "jenny", + "amber", + "bunny", + "mouse", + "007007", + "titanic", + "liverpool", + "dreamer", + "everton", + "friends", + "chevelle", + "carrie", + "gabriel", + "psycho", + "nemesis", + "burton", + "pontiac", + "connor", + "eatme", + "lickme", + "roland", + "cumming", + "mitchell", + "ireland", + "lincoln", + "arnold", + "spiderma", + "patriots", + "goblue", + "devils", + "eugene", + "empire", + "asdfg", + "cardinal", + "brown", + "shaggy", + "froggy", + "qwer", + "kawasaki", + "kodiak", + "people", + "phpbb", + "light", + "54321", + "kramer", + "chopper", + "hooker", + "honey", + "whynot", + "lesbian", + "lisa", + "baxter", + "adam", + "snake", + "teen", + "ncc1701d", + "qqqqqq", + "airplane", + "britney", + "avalon", + "sandy", + "sugar", + "sublime", + "stewart", + "wildcat", + "raven", + "scarface", + "elizabet", + "123654", + "trucks", + "wolfpack", + "pervert", + "lawrence", + "raymond", + "redhead", + "american", + "alyssa", + "bambam", + "movie", + "woody", + "shaved", + "snowman", + "tiger1", + "chicks", + "raptor", + "1969", + "stingray", + "shooter", + "france", + "stars", + "madmax", + "kristen", + "sports", + "jerry", + "789456", + "garcia", + "simpsons", + "lights", + "ryan", + "looking", + "chronic", + "alison", + "hahaha", + "packard", + "hendrix", + "perfect", + "service", + "spring", + "srinivas", + "spike", + "katie", + "252525", + "oscar", + "brother", + "bigmac", + "suck", + "single", + "cannon", + "georgia", + "popeye", + "tattoo", + "texas", + "party", + "bullet", + "taurus", + "sailor", + "wolves", + "panthers", + "japan", + "strike", + "flowers", + "pussycat", + "chris1", + "loverboy", + "berlin", + "sticky", + "marina", + "tarheels", + "fisher", + "russia", + "connie", + "wolfgang", + "testtest", + "mature", + "bass", + "catch22", + "juice", + "michael1", + "nigger", + "159753", + "women", + "alpha1", + "trooper", + "hawkeye", + "head", + "freaky", + "dodgers", + "pakistan", + "machine", + "pyramid", + "vegeta", + "katana", + "moose", + "tinker", + "coyote", + "infinity", + "inside", + "pepsi", + "letmein1", + "bang", + "control", + "hercules", + "morris", + "james1", + "tickle", + "outlaw", + "browns", + "billybob", + "pickle", + "test1", + "michele", + "antonio", + "sucks", + "pavilion", + "changeme", + "caesar", + "prelude", + "tanner", + "adrian", + "darkside", + "bowling", + "wutang", + "sunset", + "robbie", + "alabama", + "danger", + "zeppelin", + "juan", + "rusty", + "pppppp", + "nick", + "2001", + "ping", + "darkstar", + "madonna", + "qwe123", + "bigone", + "casino", + "cheryl", + "charlie1", + "mmmmmm", + "integra", + "wrangler", + "apache", + "tweety", + "qwerty12", + "bobafett", + "simone", + "none", + "business", + "sterling", + "trevor", + "transam", + "dustin", + "harvey", + "england", + "2323", + "seattle", + "ssssss", + "rose", + "harry", + "openup", + "pandora", + "pussys", + "trucker", + "wallace", + "indigo", + "storm", + "malibu", + "weed", + "review", + "babydoll", + "doggy", + "dilbert", + "pegasus", + "joker", + "catfish", + "flipper", + "valerie", + "herman", + "fuckit", + "detroit", + "kenneth", + "cheyenne", + "bruins", + "stacey", + "smoke", + "joey", + "seven", + "marino", + "fetish", + "xfiles", + "wonder", + "stinger", + "pizza", + "babe", + "pretty", + "stealth", + "manutd", + "gracie", + "gundam", + "cessna", + "longhorn", + "presario", + "mnbvcxz", + "wicked", + "mustang1", + "victory", + "21122112", + "shelly", + "awesome", + "athena", + "q1w2e3r4", + "help", + "holiday", + "knicks", + "street", + "redneck", + "12341234", + "casey", + "gizmo", + "scully", + "dragon1", + "devildog", + "triumph", + "eddie", + "bluebird", + "shotgun", + "peewee", + "ronnie", + "angel1", + "daisy", + "special", + "metallica", + "madman", + "country", + "impala", + "lennon", + "roscoe", + "omega", + "access14", + "enterpri", + "miranda", + "search", + "smitty", + "blizzard", + "unicorn", + "tight", + "rick", + "ronald", + "asdf1234", + "harrison", + "trigger", + "truck", + "danny", + "home", + "winnie", + "beauty", + "thailand", + "1234567890", + "cadillac", + "castle", + "tyler", + "bobcat", + "buddy1", + "sunny", + "stones", + "asian", + "freddie", + "chuck", + "butt", + "loveyou", + "norton", + "hellfire", + "hotsex", + "indiana", + "short", + "panzer", + "lonewolf", + "trumpet", + "colors", + "blaster", + "12121212", + "fireball", + "logan", + "precious", + "aaron", + "elaine", + "jungle", + "atlanta", + "gold", + "corona", + "curtis", + "nikki", + "polaris", + "timber", + "theone", + "baller", + "chipper", + "orlando", + "island", + "skyline", + "dragons", + "dogs", + "benson", + "licker", + "goldie", + "engineer", + "kong", + "pencil", + "basketba", + "open", + "hornet", + "world", + "linda", + "barbie", + "chan", + "farmer", + "valentin", + "wetpussy", + "indians", + "larry", + "redman", + "foobar", + "travel", + "morpheus", + "bernie", + "target", + "141414", + "hotstuff", + "photos", + "laura", + "savage", + "holly", + "rocky1", + "fuck_inside", + "dollar", + "turbo", + "design", + "newton", + "hottie", + "moon", + "202020", + "blondes", + "4128", + "lestat", + "avatar", + "future", + "goforit", + "random", + "abgrtyu", + "jjjjjj", + "cancer", + "q1w2e3", + "smiley", + "goldberg", + "express", + "virgin", + "zipper", + "wrinkle1", + "stone", + "andy", + "babylon", + "dong", + "powers", + "consumer", + "dudley", + "monkey1", + "serenity", + "samurai", + "99999999", + "bigboobs", + "skeeter", + "lindsay", + "joejoe", + "master1", + "aaaaa", + "chocolat", + "christia", + "birthday", + "stephani", + "tang", + "1234qwer", + "alfred", + "ball", + "98765432", + "maria", + "sexual", + "maxima", + "77777777", + "sampson", + "buckeye", + "highland", + "kristin", + "seminole", + "reaper", + "bassman", + "nugget", + "lucifer", + "airforce", + "nasty", + "watson", + "warlock", + "2121", + "philip", + "always", + "dodge", + "chrissy", + "burger", + "bird", + "snatch", + "missy", + "pink", + "gang", + "maddie", + "holmes", + "huskers", + "piglet", + "photo", + "joanne", + "hamilton", + "dodger", + "paladin", + "christy", + "chubby", + "buckeyes", + "hamlet", + "abcdefgh", + "bigfoot", + "sunday", + "manson", + "goldfish", + "garden", + "deftones", + "icecream", + "blondie", + "spartan", + "julie", + "harold", + "charger", + "brandi", + "stormy", + "sherry", + "pleasure", + "juventus", + "rodney", + "galaxy", + "holland", + "escort", + "zxcvb", + "planet", + "jerome", + "wesley", + "blues", + "song", + "peace", + "david1", + "ncc1701e", + "1966", + "51505150", + "cavalier", + "gambit", + "karen", + "sidney", + "ripper", + "oicu812", + "jamie", + "sister", + "marie", + "martha", + "nylons", + "aardvark", + "nadine", + "minnie", + "whiskey", + "bing", + "plastic", + "anal", + "babylon5", + "chang", + "savannah", + "loser", + "racecar", + "insane", + "yankees1", + "mememe", + "hansolo", + "chiefs", + "fredfred", + "freak", + "frog", + "salmon", + "concrete", + "yvonne", + "zxcv", + "shamrock", + "atlantis", + "warren", + "wordpass", + "julian", + "mariah", + "rommel", + "1010", + "harris", + "predator", + "sylvia", + "massive", + "cats", + "sammy1", + "mister", + "stud", + "marathon", + "rubber", + "ding", + "trunks", + "desire", + "montreal", + "justme", + "faster", + "kathleen", + "irish", + "1999", + "bertha", + "jessica1", + "alpine", + "sammie", + "diamonds", + "tristan", + "00000", + "swinger", + "shan", + "stallion", + "pitbull", + "letmein2", + "roberto", + "ready", + "april", + "palmer", + "ming", + "shadow1", + "audrey", + "chong", + "clitoris", + "wang", + "shirley", + "fuckers", + "jackoff", + "bluesky", + "sundance", + "renegade", + "hollywoo", + "151515", + "bernard", + "wolfman", + "soldier", + "picture", + "pierre", + "ling", + "goddess", + "manager", + "nikita", + "sweety", + "titans", + "hang", + "fang", + "ficken", + "niners", + "bottom", + "bubble", + "hello123", + "ibanez", + "webster", + "sweetpea", + "stocking", + "323232", + "tornado", + "lindsey", + "content", + "bruce", + "buck", + "aragorn", + "griffin", + "chen", + "campbell", + "trojan", + "christop", + "newman", + "wayne", + "tina", + "rockstar", + "father", + "geronimo", + "pascal", + "crimson", + "brooks", + "hector", + "penny", + "anna", + "google", + "camera", + "chandler", + "fatcat", + "lovelove", + "cody", + "cunts", + "waters", + "stimpy", + "finger", + "cindy", + "wheels", + "viper1", + "latin", + "robin", + "greenday", + "987654321", + "creampie", + "brendan", + "hiphop", + "willy", + "snapper", + "funtime", + "duck", + "trombone", + "adult", + "cotton", + "cookies", + "kaiser", + "mulder", + "westham", + "latino", + "jeep", + "ravens", + "aurora", + "drizzt", + "madness", + "energy", + "kinky", + "314159", + "sophia", + "stefan", + "slick", + "rocker", + "55555555", + "freeman", + "french", + "mongoose", + "speed", + "dddddd", + "hong", + "henry", + "hungry", + "yang", + "catdog", + "cheng", + "ghost", + "gogogo", + "randy", + "tottenha", + "curious", + "butterfl", + "mission", + "january", + "singer", + "sherman", + "shark", + "techno", + "lancer", + "lalala", + "autumn", + "chichi", + "orion", + "trixie", + "clifford", + "delta", + "bobbob", + "bomber", + "holden", + "kang", + "kiss", + "1968", + "spunky", + "liquid", + "mary", + "beagle", + "granny", + "network", + "bond", + "kkkkkk", + "millie", + "1973", + "biggie", + "beetle", + "teacher", + "susan", + "toronto", + "anakin", + "genius", + "dream", + "cocks", + "dang", + "bush", + "karate", + "snakes", + "bangkok", + "callie", + "fuckyou2", + "pacific", + "daytona", + "kelsey", + "infantry", + "skywalke", + "foster", + "felix", + "sailing", + "raistlin", + "vanhalen", + "huang", + "herbert", + "jacob", + "blackie", + "tarzan", + "strider", + "sherlock", + "lang", + "gong", + "sang", + "dietcoke", + "ultimate", + "tree", + "shai", + "sprite", + "ting", + "artist", + "chai", + "chao", + "devil", + "python", + "ninja", + "misty", + "ytrewq", + "sweetie", + "superfly", + "456789", + "tian", + "jing", + "jesus1", + "freedom1", + "dian", + "drpepper", + "potter", + "chou", + "darren", + "hobbit", + "violet", + "yong", + "shen", + "phillip", + "maurice", + "gloria", + "nolimit", + "mylove", + "biscuit", + "yahoo", + "shasta", + "sex4me", + "smoker", + "smile", + "pebbles", + "pics", + "philly", + "tong", + "tintin", + "lesbians", + "marlin", + "cactus", + "frank1", + "tttttt", + "chun", + "danni", + "emerald", + "showme", + "pirates", + "lian", + "dogg", + "colleen", + "xiao", + "xian", + "tazman", + "tanker", + "patton", + "toshiba", + "richie", + "alberto", + "gotcha", + "graham", + "dillon", + "rang", + "emily", + "keng", + "jazz", + "bigguy", + "yuan", + "woman", + "tomtom", + "marion", + "greg", + "chaos", + "fossil", + "flight", + "racerx", + "tuan", + "creamy", + "boss", + "bobo", + "musicman", + "warcraft", + "window", + "blade", + "shuang", + "sheila", + "shun", + "lick", + "jian", + "microsoft", + "rong", + "allen", + "feng", + "getsome", + "sally", + "quality", + "kennedy", + "morrison", + "1977", + "beng", + "wwwwww", + "yoyoyo", + "zhang", + "seng", + "teddy", + "joanna", + "andreas", + "harder", + "luke", + "qazxsw", + "qian", + "cong", + "chuan", + "deng", + "nang", + "boeing", + "keeper", + "western", + "isabelle", + "1963", + "subaru", + "sheng", + "thuglife", + "teng", + "jiong", + "miao", + "martina", + "mang", + "maniac", + "pussie", + "tracey", + "a1b2c3", + "clayton", + "zhou", + "zhuang", + "xing", + "stonecol", + "snow", + "spyder", + "liang", + "jiang", + "memphis", + "regina", + "ceng", + "magic1", + "logitech", + "chuang", + "dark", + "million", + "blow", + "sesame", + "shao", + "poison", + "titty", + "terry", + "kuan", + "kuai", + "kyle", + "mian", + "guan", + "hamster", + "guai", + "ferret", + "florence", + "geng", + "duan", + "pang", + "maiden", + "quan", + "velvet", + "nong", + "neng", + "nookie", + "buttons", + "bian", + "bingo", + "biao", + "zhong", + "zeng", + "xiong", + "zhun", + "ying", + "zong", + "xuan", + "zang", + "0.0.000", + "suan", + "shei", + "shui", + "sharks", + "shang", + "shua", + "small", + "peng", + "pian", + "piao", + "liao", + "meng", + "miami", + "reng", + "guang", + "cang", + "change", + "ruan", + "diao", + "luan", + "lucas", + "qing", + "chui", + "chuo", + "cuan", + "nuan", + "ning", + "heng", + "huan", + "kansas", + "muscle", + "monroe", + "weng", + "whitney", + "1passwor", + "bluemoon", + "zhui", + "zhua", + "xiang", + "zheng", + "zhen", + "zhei", + "zhao", + "zhan", + "yomama", + "zhai", + "zhuo", + "zuan", + "tarheel", + "shou", + "shuo", + "tiao", + "lady", + "leonard", + "leng", + "kuang", + "jiao", + "13579", + "basket", + "qiao", + "qiong", + "qiang", + "chuai", + "nian", + "niao", + "niang", + "huai", + "22222222", + "bianca", + "zhuan", + "zhuai", + "shuan", + "shuai", + "stardust", + "jumper", + "margaret", + "archie", + "66666666", + "charlott", + "forget", + "qwertz", + "bones", + "history", + "milton", + "waterloo", + "2002", + "stuff", + "11223344", + "office", + "oldman", + "preston", + "trains", + "murray", + "vertigo", + "246810", + "black1", + "swallow", + "smiles", + "standard", + "alexandr", + "parrot", + "luther", + "user", + "nicolas", + "1976", + "surfing", + "pioneer", + "pete", + "masters", + "apple1", + "asdasd", + "auburn", + "hannibal", + "frontier", + "panama", + "lucy", + "buffy", + "brianna", + "welcome1", + "vette", + "blue22", + "shemale", + "111222", + "baggins", + "groovy", + "global", + "turner", + "181818", + "1979", + "blades", + "spanking", + "life", + "byteme", + "lobster", + "collins", + "dawg", + "hilton", + "japanese", + "1970", + "1964", + "2424", + "polo", + "markus", + "coco", + "deedee", + "mikey", + "1972", + "171717", + "1701", + "strip", + "jersey", + "green1", + "capital", + "sasha", + "sadie", + "putter", + "vader", + "seven7", + "lester", + "marcel", + "banshee", + "grendel", + "gilbert", + "dicks", + "dead", + "hidden", + "iloveu", + "1980", + "sound", + "ledzep", + "michel", + "147258", + "female", + "bugger", + "buffett", + "bryan", + "hell", + "kristina", + "molson", + "2020", + "wookie", + "sprint", + "thanks", + "jericho", + "102030", + "grace", + "fuckin", + "mandy", + "ranger1", + "trebor", + "deepthroat", + "bonehead", + "molly1", + "mirage", + "models", + "1984", + "2468", + "stuart", + "showtime", + "squirrel", + "pentium", + "mario", + "anime", + "gator", + "powder", + "twister", + "connect", + "neptune", + "bruno", + "butts", + "engine", + "eatshit", + "mustangs", + "woody1", + "shogun", + "septembe", + "pooh", + "jimbo", + "roger", + "annie", + "bacon", + "center", + "russian", + "sabine", + "damien", + "mollie", + "voyeur", + "2525", + "363636", + "leonardo", + "camel", + "chair", + "germany", + "giant", + "qqqq", + "nudist", + "bone", + "sleepy", + "tequila", + "megan", + "fighter", + "garrett", + "dominic", + "obiwan", + "makaveli", + "vacation", + "walnut", + "1974", + "ladybug", + "cantona", + "ccbill", + "satan", + "rusty1", + "passwor1", + "columbia", + "napoleon", + "dusty", + "kissme", + "motorola", + "william1", + "1967", + "zzzz", + "skater", + "smut", + "play", + "matthew1", + "robinson", + "valley", + "coolio", + "dagger", + "boner", + "bull", + "horndog", + "jason1", + "blake", + "penguins", + "rescue", + "griffey", + "8j4ye3uz", + "californ", + "champs", + "qwertyuiop", + "portland", + "queen", + "colt45", + "boat", + "xxxxxxx", + "xanadu", + "tacoma", + "mason", + "carpet", + "gggggg", + "safety", + "palace", + "italia", + "stevie", + "picturs", + "picasso", + "thongs", + "tempest", + "ricardo", + "roberts", + "asd123", + "hairy", + "foxtrot", + "gary", + "nimrod", + "hotboy", + "343434", + "1111111", + "asdfghjkl", + "goose", + "overlord", + "blood", + "wood", + "stranger", + "454545", + "shaolin", + "sooners", + "socrates", + "spiderman", + "peanuts", + "maxine", + "rogers", + "13131313", + "andrew1", + "filthy", + "donnie", + "ohyeah", + "africa", + "national", + "kenny", + "keith", + "monique", + "intrepid", + "jasmin", + "pickles", + "assass", + "fright", + "potato", + "darwin", + "hhhhhh", + "kingdom", + "weezer", + "424242", + "pepsi1", + "throat", + "romeo", + "gerard", + "looker", + "puppy", + "butch", + "monika", + "suzanne", + "sweets", + "temple", + "laurie", + "josh", + "megadeth", + "analsex", + "nymets", + "ddddddd", + "bigballs", + "support", + "stick", + "today", + "down", + "oakland", + "oooooo", + "qweasd", + "chucky", + "bridge", + "carrot", + "chargers", + "discover", + "dookie", + "condor", + "night", + "butler", + "hoover", + "horny1", + "isabella", + "sunrise", + "sinner", + "jojo", + "megapass", + "martini", + "assfuck", + "grateful", + "ffffff", + "abigail", + "esther", + "mushroom", + "janice", + "jamaica", + "wright", + "sims", + "space", + "there", + "timmy", + "7654321", + "77777", + "cccccc", + "gizmodo", + "roxanne", + "ralph", + "tractor", + "cristina", + "dance", + "mypass", + "hongkong", + "helena", + "1975", + "blue123", + "pissing", + "thomas1", + "redred", + "rich", + "basketball", + "attack", + "cash", + "satan666", + "drunk", + "dixie", + "dublin", + "bollox", + "kingkong", + "katrina", + "miles", + "1971", + "22222", + "272727", + "sexx", + "penelope", + "thompson", + "anything", + "bbbb", + "battle", + "grizzly", + "passat", + "porter", + "tracy", + "defiant", + "bowler", + "knickers", + "monitor", + "wisdom", + "wild", + "slappy", + "thor", + "letsgo", + "robert1", + "feet", + "rush", + "brownie", + "hudson", + "098765", + "playing", + "playtime", + "lightnin", + "melvin", + "atomic", + "bart", + "hawk", + "goku", + "glory", + "llllll", + "qwaszx", + "cosmos", + "bosco", + "knights", + "bentley", + "beast", + "slapshot", + "lewis", + "assword", + "frosty", + "gillian", + "sara", + "dumbass", + "mallard", + "dddd", + "deanna", + "elwood", + "wally", + "159357", + "titleist", + "angelo", + "aussie", + "guest", + "golfing", + "doobie", + "loveit", + "chloe", + "elliott", + "werewolf", + "vipers", + "janine", + "1965", + "blabla", + "surf", + "sucking", + "tardis", + "serena", + "shelley", + "thegame", + "legion", + "rebels", + "fernando", + "fast", + "gerald", + "sarah1", + "double", + "onelove", + "loulou", + "toto", + "crash", + "blackcat", + "0007", + "tacobell", + "soccer1", + "jedi", + "manuel", + "method", + "river", + "chase", + "ludwig", + "poopie", + "derrick", + "boob", + "breast", + "kittycat", + "isabel", + "belly", + "pikachu", + "thunder1", + "thankyou", + "jose", + "celeste", + "celtics", + "frances", + "frogger", + "scoobydo", + "sabbath", + "coltrane", + "budman", + "willis", + "jackal", + "bigger", + "zzzzz", + "silvia", + "sooner", + "licking", + "gopher", + "geheim", + "lonestar", + "primus", + "pooper", + "newpass", + "brasil", + "heather1", + "husker", + "element", + "moomoo", + "beefcake", + "zzzzzzzz", + "tammy", + "shitty", + "smokin", + "personal", + "jjjj", + "anthony1", + "anubis", + "backup", + "gorilla", + "fuckface", + "painter", + "lowrider", + "punkrock", + "traffic", + "claude", + "daniela", + "dale", + "delta1", + "nancy", + "boys", + "easy", + "kissing", + "kelley", + "wendy", + "theresa", + "amazon", + "alan", + "fatass", + "dodgeram", + "dingdong", + "malcolm", + "qqqqqqqq", + "breasts", + "boots", + "honda1", + "spidey", + "poker", + "temp", + "johnjohn", + "miguel", + "147852", + "archer", + "asshole1", + "dogdog", + "tricky", + "crusader", + "weather", + "syracuse", + "spankme", + "speaker", + "meridian", + "amadeus", + "back", + "harley1", + "falcons", + "dorothy", + "turkey50", + "kenwood", + "keyboard", + "ilovesex", + "1978", + "blackman", + "shazam", + "shalom", + "lickit", + "jimbob", + "richmond", + "roller", + "carson", + "check", + "fatman", + "funny", + "garbage", + "sandiego", + "loving", + "magnus", + "cooldude", + "clover", + "mobile", + "bell", + "payton", + "plumber", + "texas1", + "tool", + "topper", + "jenna", + "mariners", + "rebel", + "harmony", + "caliente", + "celica", + "fletcher", + "german", + "diana", + "oxford", + "osiris", + "orgasm", + "punkin", + "porsche9", + "tuesday", + "close", + "breeze", + "bossman", + "kangaroo", + "billie", + "latinas", + "judith", + "astros", + "scruffy", + "donna", + "qwertyu", + "davis", + "hearts", + "kathy", + "jammer", + "java", + "springer", + "rhonda", + "ricky", + "1122", + "goodtime", + "chelsea1", + "freckles", + "flyboy", + "doodle", + "city", + "nebraska", + "bootie", + "kicker", + "webmaster", + "vulcan", + "iverson", + "191919", + "blueeyes", + "stoner", + "321321", + "farside", + "rugby", + "director", + "pussy69", + "power1", + "bobbie", + "hershey", + "hermes", + "monopoly", + "west", + "birdman", + "blessed", + "blackjac", + "southern", + "peterpan", + "thumbs", + "lawyer", + "melinda", + "fingers", + "fuckyou1", + "rrrrrr", + "a1b2c3d4", + "coke", + "nicola", + "bohica", + "heart", + "elvis1", + "kids", + "blacky", + "stories", + "sentinel", + "snake1", + "phoebe", + "jesse", + "richard1", + "1234abcd", + "guardian", + "candyman", + "fisting", + "scarlet", + "dildo", + "pancho", + "mandingo", + "lucky7", + "condom", + "munchkin", + "billyboy", + "summer1", + "student", + "sword", + "skiing", + "sergio", + "site", + "sony", + "thong", + "rootbeer", + "assassin", + "cassidy", + "frederic", + "fffff", + "fitness", + "giovanni", + "scarlett", + "durango", + "postal", + "achilles", + "dawn", + "dylan", + "kisses", + "warriors", + "imagine", + "plymouth", + "topdog", + "asterix", + "hallo", + "cameltoe", + "fuckfuck", + "bridget", + "eeeeee", + "mouth", + "weird", + "will", + "sithlord", + "sommer", + "toby", + "theking", + "juliet", + "avenger", + "backdoor", + "goodbye", + "chevrole", + "faith", + "lorraine", + "trance", + "cosworth", + "brad", + "houses", + "homers", + "eternity", + "kingpin", + "verbatim", + "incubus", + "1961", + "blond", + "zaphod", + "shiloh", + "spurs", + "station", + "jennie", + "maynard", + "mighty", + "aliens", + "hank", + "charly", + "running", + "dogman", + "omega1", + "printer", + "aggies", + "chocolate", + "deadhead", + "hope", + "javier", + "bitch1", + "stone55", + "pineappl", + "thekid", + "lizzie", + "rockets", + "ashton", + "camels", + "formula", + "forrest", + "rosemary", + "oracle", + "rain", + "pussey", + "porkchop", + "abcde", + "clancy", + "nellie", + "mystic", + "inferno", + "blackdog", + "steve1", + "pauline", + "alexander", + "alice", + "alfa", + "grumpy", + "flames", + "scream", + "lonely", + "puffy", + "proxy", + "valhalla", + "unreal", + "cynthia", + "herbie", + "engage", + "yyyyyy", + "010101", + "solomon", + "pistol", + "melody", + "celeb", + "flying", + "gggg", + "santiago", + "scottie", + "oakley", + "portugal", + "a12345", + "newbie", + "mmmm", + "venus", + "1qazxsw2", + "beverly", + "zorro", + "work", + "writer", + "stripper", + "sebastia", + "spread", + "phil", + "tobias", + "links", + "members", + "metal", + "1221", + "andre", + "565656", + "funfun", + "trojans", + "again", + "cyber", + "hurrican", + "moneys", + "1x2zkg8w", + "zeus", + "thing", + "tomato", + "lion", + "atlantic", + "celine", + "usa123", + "trans", + "account", + "aaaaaaa", + "homerun", + "hyperion", + "kevin1", + "blacks", + "44444444", + "skittles", + "sean", + "hastings", + "fart", + "gangbang", + "fubar", + "sailboat", + "older", + "oilers", + "craig", + "conrad", + "church", + "damian", + "dean", + "broken", + "buster1", + "hithere", + "immortal", + "sticks", + "pilot", + "peters", + "lexmark", + "jerkoff", + "maryland", + "anders", + "cheers", + "possum", + "columbus", + "cutter", + "muppet", + "beautiful", + "stolen", + "swordfish", + "sport", + "sonic", + "peter1", + "jethro", + "rockon", + "asdfghj", + "pass123", + "paper", + "pornos", + "ncc1701a", + "bootys", + "buttman", + "bonjour", + "escape", + "1960", + "becky", + "bears", + "362436", + "spartans", + "tinman", + "threesom", + "lemons", + "maxmax", + "1414", + "bbbbb", + "camelot", + "chad", + "chewie", + "gogo", + "fusion", + "saint", + "dilligaf", + "nopass", + "myself", + "hustler", + "hunter1", + "whitey", + "beast1", + "yesyes", + "spank", + "smudge", + "pinkfloy", + "patriot", + "lespaul", + "annette", + "hammers", + "catalina", + "finish", + "formula1", + "sausage", + "scooter1", + "orioles", + "oscar1", + "over", + "colombia", + "cramps", + "natural", + "eating", + "exotic", + "iguana", + "bella", + "suckers", + "strong", + "sheena", + "start", + "slave", + "pearl", + "topcat", + "lancelot", + "angelica", + "magelan", + "racer", + "ramona", + "crunch", + "british", + "button", + "eileen", + "steph", + "456123", + "skinny", + "seeking", + "rockhard", + "chief", + "filter", + "first", + "freaks", + "sakura", + "pacman", + "poontang", + "dalton", + "newlife", + "homer1", + "klingon", + "watcher", + "walleye", + "tasha", + "tasty", + "sinatra", + "starship", + "steel", + "starbuck", + "poncho", + "amber1", + "gonzo", + "grover", + "catherin", + "carol", + "candle", + "firefly", + "goblin", + "scotch", + "diver", + "usmc", + "huskies", + "eleven", + "kentucky", + "kitkat", + "israel", + "beckham", + "bicycle", + "yourmom", + "studio", + "tara", + "33333333", + "shane", + "splash", + "jimmy1", + "reality", + "12344321", + "caitlin", + "focus", + "sapphire", + "mailman", + "raiders1", + "clark", + "ddddd", + "hopper", + "excalibu", + "more", + "wilbur", + "illini", + "imperial", + "phillips", + "lansing", + "maxx", + "gothic", + "golfball", + "carlton", + "camille", + "facial", + "front242", + "macdaddy", + "qwer1234", + "vectra", + "cowboys1", + "crazy1", + "dannyboy", + "jane", + "betty", + "benny", + "bennett", + "leader", + "martinez", + "aquarius", + "barkley", + "hayden", + "caught", + "franky", + "ffff", + "floyd", + "sassy", + "pppp", + "pppppppp", + "prodigy", + "clarence", + "noodle", + "eatpussy", + "vortex", + "wanking", + "beatrice", + "billy1", + "siemens", + "pedro", + "phillies", + "research", + "groups", + "carolyn", + "chevy1", + "cccc", + "fritz", + "gggggggg", + "doughboy", + "dracula", + "nurses", + "loco", + "madrid", + "lollipop", + "trout", + "utopia", + "chrono", + "cooler", + "conner", + "nevada", + "wibble", + "werner", + "summit", + "marco", + "marilyn", + "1225", + "babies", + "capone", + "fugazi", + "panda", + "mama", + "qazwsxed", + "puppies", + "triton", + "9876", + "command", + "nnnnnn", + "ernest", + "momoney", + "iforgot", + "wolfie", + "studly", + "shawn", + "renee", + "alien", + "hamburg", + "81fukkc", + "741852", + "catman", + "china", + "forgot", + "gagging", + "scott1", + "drew", + "oregon", + "qweqwe", + "train", + "crazybab", + "daniel1", + "cutlass", + "brothers", + "holes", + "heidi", + "mothers", + "music1", + "what", + "walrus", + "1957", + "bigtime", + "bike", + "xtreme", + "simba", + "ssss", + "rookie", + "angie", + "bathing", + "fresh", + "sanchez", + "rotten", + "maestro", + "luis", + "look", + "turbo1", + "99999", + "butthole", + "hhhh", + "elijah", + "monty", + "bender", + "yoda", + "shania", + "shock", + "phish", + "thecat", + "rightnow", + "reagan", + "baddog", + "asia", + "greatone", + "gateway1", + "randall", + "abstr", + "napster", + "brian1", + "bogart", + "high", + "hitler", + "emma", + "kill", + "weaver", + "wildfire", + "jackson1", + "isaiah", + "1981", + "belinda", + "beaner", + "yoyo", + "0.0.0.000", + "super1", + "select", + "snuggles", + "slutty", + "some", + "phoenix1", + "technics", + "toon", + "raven1", + "rayray", + "123789", + "1066", + "albion", + "greens", + "fashion", + "gesperrt", + "santana", + "paint", + "powell", + "credit", + "darling", + "mystery", + "bowser", + "bottle", + "brucelee", + "hehehe", + "kelly1", + "mojo", + "1998", + "bikini", + "woofwoof", + "yyyy", + "strap", + "sites", + "spears", + "theodore", + "julius", + "richards", + "amelia", + "central", + "f**k", + "nyjets", + "punisher", + "username", + "vanilla", + "twisted", + "bryant", + "brent", + "bunghole", + "here", + "elizabeth", + "erica", + "kimber", + "viagra", + "veritas", + "pony", + "pool", + "titts", + "labtec", + "lifetime", + "jenny1", + "masterbate", + "mayhem", + "redbull", + "govols", + "gremlin", + "505050", + "gmoney", + "rupert", + "rovers", + "diamond1", + "lorenzo", + "trident", + "abnormal", + "davidson", + "deskjet", + "cuddles", + "nice", + "bristol", + "karina", + "milano", + "vh5150", + "jarhead", + "1982", + "bigbird", + "bizkit", + "sixers", + "slider", + "star69", + "starfish", + "penetration", + "tommy1", + "john316", + "meghan", + "michaela", + "market", + "grant", + "caligula", + "carl", + "flicks", + "films", + "madden", + "railroad", + "cosmo", + "cthulhu", + "bradford", + "br0d3r", + "military", + "bearbear", + "swedish", + "spawn", + "patrick1", + "polly", + "these", + "todd", + "reds", + "anarchy", + "groove", + "franco", + "fuckher", + "oooo", + "tyrone", + "vegas", + "airbus", + "cobra1", + "christine", + "clips", + "delete", + "duster", + "kitty1", + "mouse1", + "monkeys", + "jazzman", + "1919", + "262626", + "swinging", + "stroke", + "stocks", + "sting", + "pippen", + "labrador", + "jordan1", + "justdoit", + "meatball", + "females", + "saturday", + "park", + "vector", + "cooter", + "defender", + "desert", + "demon", + "nike", + "bubbas", + "bonkers", + "english", + "kahuna", + "wildman", + "4121", + "sirius", + "static", + "piercing", + "terror", + "teenage", + "leelee", + "marissa", + "microsof", + "mechanic", + "robotech", + "rated", + "hailey", + "chaser", + "sanders", + "salsero", + "nuts", + "macross", + "quantum", + "rachael", + "tsunami", + "universe", + "daddy1", + "cruise", + "nguyen", + "newpass6", + "nudes", + "hellyeah", + "vernon", + "1959", + "zaq12wsx", + "striker", + "sixty", + "steele", + "spice", + "spectrum", + "smegma", + "thumb", + "jjjjjjjj", + "mellow", + "astrid", + "cancun", + "cartoon", + "sabres", + "samiam", + "pants", + "oranges", + "oklahoma", + "lust", + "coleman", + "denali", + "nude", + "noodles", + "buzz", + "brest", + "hooter", + "mmmmmmmm", + "warthog", + "bloody", + "blueblue", + "zappa", + "wolverine", + "sniffing", + "lance", + "jean", + "jjjjj", + "harper", + "calico", + "freee", + "rover", + "door", + "pooter", + "closeup", + "bonsai", + "evelyn", + "emily1", + "kathryn", + "keystone", + "iiii", + "1955", + "yzerman", + "theboss", + "tolkien", + "jill", + "megaman", + "rasta", + "bbbbbbbb", + "bean", + "handsome", + "hal9000", + "goofy", + "gringo", + "gofish", + "gizmo1", + "samsam", + "scuba", + "onlyme", + "tttttttt", + "corrado", + "clown", + "clapton", + "deborah", + "boris", + "bulls", + "vivian", + "jayhawk", + "bethany", + "wwww", + "sharky", + "seeker", + "ssssssss", + "somethin", + "pillow", + "thesims", + "lighter", + "lkjhgf", + "melissa1", + "marcius2", + "barry", + "guiness", + "gymnast", + "casey1", + "goalie", + "godsmack", + "doug", + "lolo", + "rangers1", + "poppy", + "abby", + "clemson", + "clipper", + "deeznuts", + "nobody", + "holly1", + "elliot", + "eeee", + "kingston", + "miriam", + "belle", + "yosemite", + "sucked", + "sex123", + "sexy69", + "pic's", + "tommyboy", + "lamont", + "meat", + "masterbating", + "marianne", + "marc", + "gretzky", + "happyday", + "frisco", + "scratch", + "orchid", + "orange1", + "manchest", + "quincy", + "unbelievable", + "aberdeen", + "dawson", + "nathalie", + "ne1469", + "boxing", + "hill", + "korn", + "intercourse", + "161616", + "1985", + "ziggy", + "supersta", + "stoney", + "senior", + "amature", + "barber", + "babyboy", + "bcfields", + "goliath", + "hack", + "hardrock", + "children", + "frodo", + "scout", + "scrappy", + "rosie", + "qazqaz", + "tracker", + "active", + "craving", + "commando", + "cohiba", + "deep", + "cyclone", + "dana", + "bubba69", + "katie1", + "mpegs", + "vsegda", + "jade", + "irish1", + "better", + "sexy1", + "sinclair", + "smelly", + "squerting", + "lions", + "jokers", + "jeanette", + "julia", + "jojojo", + "meathead", + "ashley1", + "groucho", + "cheetah", + "champ", + "firefox", + "gandalf1", + "packer", + "magnolia", + "love69", + "tyler1", + "typhoon", + "tundra", + "bobby1", + "kenworth", + "village", + "volley", + "beth", + "wolf359", + "0420", + "000007", + "swimmer", + "skydive", + "smokes", + "patty", + "peugeot", + "pompey", + "legolas", + "kristy", + "redhot", + "rodman", + "redalert", + "having", + "grapes", + "4runner", + "carrera", + "floppy", + "dollars", + "ou8122", + "quattro", + "adams", + "cloud9", + "davids", + "nofear", + "busty", + "homemade", + "mmmmm", + "whisper", + "vermont", + "webmaste", + "wives", + "insertion", + "jayjay", + "philips", + "phone", + "topher", + "tongue", + "temptress", + "midget", + "ripken", + "havefun", + "gretchen", + "canon", + "celebrity", + "five", + "getting", + "ghetto", + "direct", + "otto", + "ragnarok", + "trinidad", + "usnavy", + "conover", + "cruiser", + "dalshe", + "nicole1", + "buzzard", + "hottest", + "kingfish", + "misfit", + "moore", + "milfnew", + "warlord", + "wassup", + "bigsexy", + "blackhaw", + "zippy", + "shearer", + "tights", + "thursday", + "kungfu", + "labia", + "journey", + "meatloaf", + "marlene", + "rider", + "area51", + "batman1", + "bananas", + "636363", + "cancel", + "ggggg", + "paradox", + "mack", + "lynn", + "queens", + "adults", + "aikido", + "cigars", + "nova", + "hoosier", + "eeyore", + "moose1", + "warez", + "interacial", + "streaming", + "313131", + "pertinant", + "pool6123", + "mayday", + "rivers", + "revenge", + "animated", + "banker", + "baddest", + "gordon24", + "ccccc", + "fortune", + "fantasies", + "touching", + "aisan", + "deadman", + "homepage", + "ejaculation", + "whocares", + "iscool", + "jamesbon", + "1956", + "1pussy", + "womam", + "sweden", + "skidoo", + "spock", + "sssss", + "petra", + "pepper1", + "pinhead", + "micron", + "allsop", + "amsterda", + "army", + "aside", + "gunnar", + "666999", + "chip", + "foot", + "fowler", + "february", + "face", + "fletch", + "george1", + "sapper", + "science", + "sasha1", + "luckydog", + "lover1", + "magick", + "popopo", + "public", + "ultima", + "derek", + "cypress", + "booker", + "businessbabe", + "brandon1", + "edwards", + "experience", + "vulva", + "vvvv", + "jabroni", + "bigbear", + "yummy", + "010203", + "searay", + "secret1", + "showing", + "sinbad", + "sexxxx", + "soleil", + "software", + "piccolo", + "thirteen", + "leopard", + "legacy", + "jensen", + "justine", + "memorex", + "marisa", + "mathew", + "redwing", + "rasputin", + "134679", + "anfield", + "greenbay", + "gore", + "catcat", + "feather", + "scanner", + "pa55word", + "contortionist", + "danzig", + "daisy1", + "hores", + "erik", + "exodus", + "vinnie", + "iiiiii", + "zero", + "1001", + "subway", + "tank", + "second", + "snapple", + "sneakers", + "sonyfuck", + "picks", + "poodle", + "test1234", + "their", + "llll", + "junebug", + "june", + "marker", + "mellon", + "ronaldo", + "roadkill", + "amanda1", + "asdfjkl", + "beaches", + "greene", + "great1", + "cheerleaers", + "force", + "doitnow", + "ozzy", + "madeline", + "radio", + "tyson", + "christian", + "daphne", + "boxster", + "brighton", + "housewifes", + "emmanuel", + "emerson", + "kkkk", + "mnbvcx", + "moocow", + "vides", + "wagner", + "janet", + "1717", + "bigmoney", + "blonds", + "1000", + "storys", + "stereo", + "4545", + "420247", + "seductive", + "sexygirl", + "lesbean", + "live", + "justin1", + "124578", + "animals", + "balance", + "hansen", + "cabbage", + "canadian", + "gangbanged", + "dodge1", + "dimas", + "lori", + "loud", + "malaka", + "puss", + "probes", + "adriana", + "coolman", + "crawford", + "dante", + "nacked", + "hotpussy", + "erotica", + "kool", + "mirror", + "wearing", + "implants", + "intruder", + "bigass", + "zenith", + "woohoo", + "womans", + "tanya", + "tango", + "stacy", + "pisces", + "laguna", + "krystal", + "maxell", + "andyod22", + "barcelon", + "chainsaw", + "chickens", + "flash1", + "downtown", + "orgasms", + "magicman", + "profit", + "pusyy", + "pothead", + "coconut", + "chuckie", + "contact", + "clevelan", + "designer", + "builder", + "budweise", + "hotshot", + "horizon", + "hole", + "experienced", + "mondeo", + "wifes", + "1962", + "strange", + "stumpy", + "smiths", + "sparks", + "slacker", + "piper", + "pitchers", + "passwords", + "laptop", + "jeremiah", + "allmine", + "alliance", + "bbbbbbb", + "asscock", + "halflife", + "grandma", + "hayley", + "88888", + "cecilia", + "chacha", + "saratoga", + "sandy1", + "santos", + "doogie", + "number", + "positive", + "qwert40", + "transexual", + "crow", + "close-up", + "darrell", + "bonita", + "ib6ub9", + "volvo", + "jacob1", + "iiiii", + "beastie", + "sunnyday", + "stoned", + "sonics", + "starfire", + "snapon", + "pictuers", + "pepe", + "testing1", + "tiberius", + "lisalisa", + "lesbain", + "litle", + "retard", + "ripple", + "austin1", + "badgirl", + "golfgolf", + "flounder", + "garage", + "royals", + "dragoon", + "dickie", + "passwor", + "ocean", + "majestic", + "poppop", + "trailers", + "dammit", + "nokia", + "bobobo", + "br549", + "emmitt", + "knock", + "minime", + "mikemike", + "whitesox", + "1954", + "3232", + "353535", + "seamus", + "solo", + "sparkle", + "sluttey", + "pictere", + "titten", + "lback", + "1024", + "angelina", + "goodluck", + "charlton", + "fingerig", + "gallaries", + "goat", + "ruby", + "passme", + "oasis", + "lockerroom", + "logan1", + "rainman", + "twins", + "treasure", + "absolutely", + "club", + "custom", + "cyclops", + "nipper", + "bucket", + "homepage-", + "hhhhh", + "momsuck", + "indain", + "2345", + "beerbeer", + "bimmer", + "susanne", + "stunner", + "stevens", + "456456", + "shell", + "sheba", + "tootsie", + "tiny", + "testerer", + "reefer", + "really", + "1012", + "harcore", + "gollum", + "545454", + "chico", + "caveman", + "carole", + "fordf150", + "fishes", + "gaymen", + "saleen", + "doodoo", + "pa55w0rd", + "looney", + "presto", + "qqqqq", + "cigar", + "bogey", + "brewer", + "helloo", + "dutch", + "kamikaze", + "monte", + "wasser", + "vietnam", + "visa", + "japanees", + "0123", + "swords", + "slapper", + "peach", + "jump", + "marvel", + "masterbaiting", + "march", + "redwood", + "rolling", + "1005", + "ametuer", + "chiks", + "cathy", + "callaway", + "fucing", + "sadie1", + "panasoni", + "mamas", + "race", + "rambo", + "unknown", + "absolut", + "deacon", + "dallas1", + "housewife", + "kristi", + "keywest", + "kirsten", + "kipper", + "morning", + "wings", + "idiot", + "18436572", + "1515", + "beating", + "zxczxc", + "sullivan", + "303030", + "shaman", + "sparrow", + "terrapin", + "jeffery", + "masturbation", + "mick", + "redfish", + "1492", + "angus", + "barrett", + "goirish", + "hardcock", + "felicia", + "forfun", + "galary", + "freeporn", + "duchess", + "olivier", + "lotus", + "pornographic", + "ramses", + "purdue", + "traveler", + "crave", + "brando", + "enter1", + "killme", + "moneyman", + "welder", + "windsor", + "wifey", + "indon", + "yyyyy", + "stretch", + "taylor1", + "4417", + "shopping", + "picher", + "pickup", + "thumbnils", + "johnboy", + "jets", + "jess", + "maureen", + "anne", + "ameteur", + "amateurs", + "apollo13", + "hambone", + "goldwing", + "5050", + "charley", + "sally1", + "doghouse", + "padres", + "pounding", + "quest", + "truelove", + "underdog", + "trader", + "crack", + "climber", + "bolitas", + "bravo", + "hohoho", + "model", + "italian", + "beanie", + "beretta", + "wrestlin", + "stroker", + "tabitha", + "sherwood", + "sexyman", + "jewels", + "johannes", + "mets", + "marcos", + "rhino", + "bdsm", + "balloons", + "goodman", + "grils", + "happy123", + "flamingo", + "games", + "route66", + "devo", + "dino", + "outkast", + "paintbal", + "magpie", + "llllllll", + "twilight", + "critter", + "christie", + "cupcake", + "nickel", + "bullseye", + "krista", + "knickerless", + "mimi", + "murder", + "videoes", + "binladen", + "xerxes", + "slim", + "slinky", + "pinky", + "peterson", + "thanatos", + "meister", + "menace", + "ripley", + "retired", + "albatros", + "balloon", + "bank", + "goten", + "5551212", + "getsdown", + "donuts", + "divorce", + "nwo4life", + "lord", + "lost", + "underwear", + "tttt", + "comet", + "deer", + "damnit", + "dddddddd", + "deeznutz", + "nasty1", + "nonono", + "nina", + "enterprise", + "eeeee", + "misfit99", + "milkman", + "vvvvvv", + "isaac", + "1818", + "blueboy", + "beans", + "bigbutt", + "wyatt", + "tech", + "solution", + "poetry", + "toolman", + "laurel", + "juggalo", + "jetski", + "meredith", + "barefoot", + "50spanks", + "gobears", + "scandinavian", + "original", + "truman", + "cubbies", + "nitram", + "briana", + "ebony", + "kings", + "warner", + "bilbo", + "yumyum", + "zzzzzzz", + "stylus", + "321654", + "shannon1", + "server", + "secure", + "silly", + "squash", + "starman", + "steeler", + "staples", + "phrases", + "techniques", + "laser", + "135790", + "allan", + "barker", + "athens", + "cbr600", + "chemical", + "fester", + "gangsta", + "fucku2", + "freeze", + "game", + "salvador", + "droopy", + "objects", + "passwd", + "lllll", + "loaded", + "louis", + "manchester", + "losers", + "vedder", + "clit", + "chunky", + "darkman", + "damage", + "buckshot", + "buddah", + "boobed", + "henti", + "hillary", + "webber", + "winter1", + "ingrid", + "bigmike", + "beta", + "zidane", + "talon", + "slave1", + "pissoff", + "person", + "thegreat", + "living", + "lexus", + "matador", + "readers", + "riley", + "roberta", + "armani", + "ashlee", + "goldstar", + "5656", + "cards", + "fmale", + "ferris", + "fuking", + "gaston", + "fucku", + "ggggggg", + "sauron", + "diggler", + "pacers", + "looser", + "pounded", + "premier", + "pulled", + "town", + "trisha", + "triangle", + "cornell", + "collin", + "cosmic", + "deeper", + "depeche", + "norway", + "bright", + "helmet", + "kristine", + "kendall", + "mustard", + "misty1", + "watch", + "jagger", + "bertie", + "berger", + "word", + "3x7pxr", + "silver1", + "smoking", + "snowboar", + "sonny", + "paula", + "penetrating", + "photoes", + "lesbens", + "lambert", + "lindros", + "lillian", + "roadking", + "rockford", + "1357", + "143143", + "asasas", + "goodboy", + "898989", + "chicago1", + "card", + "ferrari1", + "galeries", + "godfathe", + "gawker", + "gargoyle", + "gangster", + "rubble", + "rrrr", + "onetime", + "pussyman", + "pooppoop", + "trapper", + "twenty", + "abraham", + "cinder", + "company", + "newcastl", + "boricua", + "bunny1", + "boxer", + "hotred", + "hockey1", + "hooper", + "edward1", + "evan", + "kris", + "misery", + "moscow", + "milk", + "mortgage", + "bigtit", + "show", + "snoopdog", + "three", + "lionel", + "leanne", + "joshua1", + "july", + "1230", + "assholes", + "cedric", + "fallen", + "farley", + "gene", + "frisky", + "sanity", + "script", + "divine", + "dharma", + "lucky13", + "property", + "tricia", + "akira", + "desiree", + "broadway", + "butterfly", + "hunt", + "hotbox", + "hootie", + "heat", + "howdy", + "earthlink", + "karma", + "kiteboy", + "motley", + "westwood", + "1988", + "bert", + "blackbir", + "biggles", + "wrench", + "working", + "wrestle", + "slippery", + "pheonix", + "penny1", + "pianoman", + "tomorrow", + "thedude", + "jenn", + "jonjon", + "jones1", + "mattie", + "memory", + "micheal", + "roadrunn", + "arrow", + "attitude", + "azzer", + "seahawks", + "diehard", + "dotcom", + "lola", + "tunafish", + "chivas", + "cinnamon", + "clouds", + "deluxe", + "northern", + "nuclear", + "north", + "boom", + "boobie", + "hurley", + "krishna", + "momomo", + "modles", + "volume", + "23232323", + "bluedog", + "wwwwwww", + "zerocool", + "yousuck", + "pluto", + "limewire", + "link", + "joung", + "marcia", + "awnyce", + "gonavy", + "haha", + "films+pic+galeries", + "fabian", + "francois", + "girsl", + "fuckthis", + "girfriend", + "rufus", + "drive", + "uncencored", + "a123456", + "airport", + "clay", + "chrisbln", + "combat", + "cygnus", + "cupoi", + "never", + "netscape", + "brett", + "hhhhhhhh", + "eagles1", + "elite", + "knockers", + "kendra", + "mommy", + "1958", + "tazmania", + "shonuf", + "piano", + "pharmacy", + "thedog", + "lips", + "jillian", + "jenkins", + "midway", + "arsenal1", + "anaconda", + "australi", + "gromit", + "gotohell", + "787878", + "66666", + "carmex2", + "camber", + "gator1", + "ginger1", + "fuzzy", + "seadoo", + "dorian", + "lovesex", + "rancid", + "uuuuuu", + "911911", + "nature", + "bulldog1", + "helen", + "health", + "heater", + "higgins", + "kirk", + "monalisa", + "mmmmmmm", + "whiteout", + "virtual", + "ventura", + "jamie1", + "japanes", + "james007", + "2727", + "2469", + "blam", + "bitchass", + "believe", + "zephyr", + "stiffy", + "sweet1", + "silent", + "southpar", + "spectre", + "tigger1", + "tekken", + "lenny", + "lakota", + "lionking", + "jjjjjjj", + "medical", + "megatron", + "1369", + "hawaiian", + "gymnastic", + "golfer1", + "gunners", + "7779311", + "515151", + "famous", + "glass", + "screen", + "rudy", + "royal", + "sanfran", + "drake", + "optimus", + "panther1", + "love1", + "mail", + "maggie1", + "pudding", + "venice", + "aaron1", + "delphi", + "niceass", + "bounce", + "busted", + "house1", + "killer1", + "miracle", + "momo", + "musashi", + "jammin", + "2003", + "234567", + "wp2003wp", + "submit", + "silence", + "sssssss", + "state", + "spikes", + "sleeper", + "passwort", + "toledo", + "kume", + "media", + "meme", + "medusa", + "mantis", + "remote", + "reading", + "reebok", + "1017", + "artemis", + "hampton", + "harry1", + "cafc91", + "fettish", + "friendly", + "oceans", + "oooooooo", + "mango", + "ppppp", + "trainer", + "troy", + "uuuu", + "909090", + "cross", + "death1", + "news", + "bullfrog", + "hokies", + "holyshit", + "eeeeeee", + "mitch", + "jasmine1", + "&", + "&", + "sergeant", + "spinner", + "leon", + "jockey", + "records", + "right", + "babyblue", + "hans", + "gooner", + "474747", + "cheeks", + "cars", + "candice", + "fight", + "glow", + "pass1234", + "parola", + "okokok", + "pablo", + "magical", + "major", + "ramsey", + "poseidon", + "989898", + "confused", + "circle", + "crusher", + "cubswin", + "nnnn", + "hollywood", + "erin", + "kotaku", + "milo", + "mittens", + "whatsup", + "vvvvv", + "iomega", + "insertions", + "bengals", + "bermuda", + "biit", + "yellow1", + "012345", + "spike1", + "south", + "sowhat", + "pitures", + "peacock", + "pecker", + "theend", + "juliette", + "jimmie", + "romance", + "augusta", + "hayabusa", + "hawkeyes", + "castro", + "florian", + "geoffrey", + "dolly", + "lulu", + "qaz123", + "usarmy", + "twinkle", + "cloud", + "chuckles", + "cold", + "hounddog", + "hover", + "hothot", + "europa", + "ernie", + "kenshin", + "kojak", + "mikey1", + "water1", + "196969", + "because", + "wraith", + "zebra", + "wwwww", + "33333", + "simon1", + "spider1", + "snuffy", + "philippe", + "thunderb", + "teddy1", + "lesley", + "marino13", + "maria1", + "redline", + "renault", + "aloha", + "antoine", + "handyman", + "cerberus", + "gamecock", + "gobucks", + "freesex", + "duffman", + "ooooo", + "papa", + "nuggets", + "magician", + "longbow", + "preacher", + "porno1", + "county", + "chrysler", + "contains", + "dalejr", + "darius", + "darlene", + "dell", + "navy", + "buffy1", + "hedgehog", + "hoosiers", + "honey1", + "hott", + "heyhey", + "europe", + "dutchess", + "everest", + "wareagle", + "ihateyou", + "sunflowe", + "3434", + "senators", + "shag", + "spoon", + "sonoma", + "stalker", + "poochie", + "terminal", + "terefon", + "laurence", + "maradona", + "maryann", + "marty", + "roman", + "1007", + "142536", + "alibaba", + "america1", + "bartman", + "astro", + "goth", + "century", + "chicken1", + "cheater", + "four", + "ghost1", + "passpass", + "oral", + "r2d2c3po", + "civic", + "cicero", + "myxworld", + "kkkkk", + "missouri", + "wishbone", + "infiniti", + "jameson", + "1a2b3c", + "1qwerty", + "wonderboy", + "skip", + "shojou", + "stanford", + "sparky1", + "smeghead", + "poiuy", + "titanium", + "torres", + "lantern", + "jelly", + "jeanne", + "meier", + "1213", + "bayern", + "basset", + "gsxr750", + "cattle", + "charlene", + "fishing1", + "fullmoon", + "gilles", + "dima", + "obelix", + "popo", + "prissy", + "ramrod", + "unique", + "absolute", + "bummer", + "hotone", + "dynasty", + "entry", + "konyor", + "missy1", + "moses", + "282828", + "yeah", + "xyz123", + "stop", + "426hemi", + "404040", + "seinfeld", + "simmons", + "pingpong", + "lazarus", + "matthews", + "marine1", + "manning", + "recovery", + "12345a", + "beamer", + "babyface", + "greece", + "gustav", + "7007", + "charity", + "camilla", + "ccccccc", + "faggot", + "foxy", + "frozen", + "gladiato", + "duckie", + "dogfood", + "paranoid", + "packers1", + "longjohn", + "radical", + "tuna", + "clarinet", + "claudio", + "circus", + "danny1", + "novell", + "nights", + "bonbon", + "kashmir", + "kiki", + "mortimer", + "modelsne", + "moondog", + "monaco", + "vladimir", + "insert", + "1953", + "zxc123", + "supreme", + "3131", + "sexxx", + "selena", + "softail", + "poipoi", + "pong", + "together", + "mars", + "martin1", + "rogue", + "alone", + "avalanch", + "audia4", + "55bgates", + "cccccccc", + "chick", + "came11", + "figaro", + "geneva", + "dogboy", + "dnsadm", + "dipshit", + "paradigm", + "othello", + "operator", + "officer", + "malone", + "post", + "rafael", + "valencia", + "tripod", + "choice", + "chopin", + "coucou", + "coach", + "cocksuck", + "common", + "creature", + "borussia", + "book", + "browning", + "heritage", + "hiziad", + "homerj", + "eight", + "earth", + "millions", + "mullet", + "whisky", + "jacques", + "store", + "4242", + "speedo", + "starcraf", + "skylar", + "spaceman", + "piggy", + "pierce", + "tiger2", + "legos", + "lala", + "jezebel", + "judy", + "joker1", + "mazda", + "barton", + "baker", + "727272", + "chester1", + "fishman", + "food", + "rrrrrrrr", + "sandwich", + "dundee", + "lumber", + "magazine", + "radar", + "ppppppp", + "tranny", + "aaliyah", + "admiral", + "comics", + "cleo", + "delight", + "buttfuck", + "homeboy", + "eternal", + "kilroy", + "kellie", + "khan", + "violin", + "wingman", + "walmart", + "bigblue", + "blaze", + "beemer", + "beowulf", + "bigfish", + "yyyyyyy", + "woodie", + "yeahbaby", + "0123456", + "tbone", + "style", + "syzygy", + "starter", + "lemon", + "linda1", + "merlot", + "mexican", + "11235813", + "anita", + "banner", + "bangbang", + "badman", + "barfly", + "grease", + "carla", + "charles1", + "ffffffff", + "screw", + "doberman", + "diane", + "dogshit", + "overkill", + "counter", + "coolguy", + "claymore", + "demons", + "demo", + "nomore", + "normal", + "brewster", + "hhhhhhh", + "hondas", + "iamgod", + "enterme", + "everett", + "electron", + "eastside", + "kayla", + "minimoni", + "mybaby", + "wildbill", + "wildcard", + "ipswich", + "200000", + "bearcat", + "zigzag", + "yyyyyyyy", + "xander", + "sweetnes", + "369369", + "skyler", + "skywalker", + "pigeon", + "peyton", + "tipper", + "lilly", + "asdf123", + "alphabet", + "asdzxc", + "babybaby", + "banane", + "barnes", + "guyver", + "graphics", + "grand", + "chinook", + "florida1", + "flexible", + "fuckinside", + "otis", + "ursitesux", + "tototo", + "trust", + "tower", + "adam12", + "christma", + "corey", + "chrome", + "buddie", + "bombers", + "bunker", + "hippie", + "keegan", + "misfits", + "vickie", + "292929", + "woofer", + "wwwwwwww", + "stubby", + "sheep", + "secrets", + "sparta", + "stang", + "spud", + "sporty", + "pinball", + "jorge", + "just4fun", + "johanna", + "maxxxx", + "rebecca1", + "gunther", + "fatima", + "fffffff", + "freeway", + "garion", + "score", + "rrrrr", + "sancho", + "outback", + "maggot", + "puddin", + "trial", + "adrienne", + "987456", + "colton", + "clyde", + "brain", + "brains", + "hoops", + "eleanor", + "dwayne", + "kirby", + "mydick", + "villa", + "19691969", + "bigcat", + "becker", + "shiner", + "silverad", + "spanish", + "templar", + "lamer", + "juicy", + "marsha", + "mike1", + "maximum", + "rhiannon", + "real", + "1223", + "10101010", + "arrows", + "andres", + "alucard", + "baldwin", + "baron", + "avenue", + "ashleigh", + "haggis", + "channel", + "cheech", + "safari", + "ross", + "dog123", + "orion1", + "paloma", + "qwerasdf", + "presiden", + "vegitto", + "trees", + "969696", + "adonis", + "colonel", + "cookie1", + "newyork1", + "brigitte", + "buddyboy", + "hellos", + "heineken", + "dwight", + "eraser", + "kerstin", + "motion", + "moritz", + "millwall", + "visual", + "jaybird", + "1983", + "beautifu", + "bitter", + "yvette", + "zodiac", + "steven1", + "sinister", + "slammer", + "smashing", + "slick1", + "sponge", + "teddybea", + "theater", + "this", + "ticklish", + "lipstick", + "jonny", + "massage", + "mann", + "reynolds", + "ring", + "1211", + "amazing", + "aptiva", + "applepie", + "bailey1", + "guitar1", + "chanel", + "canyon", + "gagged", + "fuckme1", + "rough", + "digital1", + "dinosaur", + "punk", + "98765", + "90210", + "clowns", + "cubs", + "daniels", + "deejay", + "nigga", + "naruto", + "boxcar", + "icehouse", + "hotties", + "electra", + "kent", + "widget", + "india", + "insanity", + "1986", + "2004", + "best", + "bluefish", + "bingo1", + "*****", + "stratus", + "strength", + "sultan", + "storm1", + "44444", + "4200", + "sentnece", + "season", + "sexyboy", + "sigma", + "smokie", + "spam", + "point", + "pippo", + "ticket", + "temppass", + "joel", + "manman", + "medicine", + "1022", + "anton", + "almond", + "bacchus", + "aztnm", + "axio", + "awful", + "bamboo", + "hakr", + "gregor", + "hahahaha", + "5678", + "casanova", + "caprice", + "camero1", + "fellow", + "fountain", + "dupont", + "dolphin1", + "dianne", + "paddle", + "magnet", + "qwert1", + "pyon", + "porsche1", + "tripper", + "vampires", + "coming", + "noway", + "burrito", + "bozo", + "highheel", + "hughes", + "hookem", + "eddie1", + "ellie", + "entropy", + "kkkkkkkk", + "kkkkkkk", + "illinois", + "jacobs", + "1945", + "1951", + "24680", + "21212121", + "100000", + "stonecold", + "taco", + "subzero", + "sharp", + "sexxxy", + "skolko", + "shanna", + "skyhawk", + "spurs1", + "sputnik", + "piazza", + "testpass", + "letter", + "lane", + "kurt", + "jiggaman", + "matilda", + "1224", + "harvard", + "hannah1", + "525252", + "4ever", + "carbon", + "chef", + "federico", + "ghosts", + "gina", + "scorpio1", + "rt6ytere", + "madison1", + "loki", + "raquel", + "promise", + "coolness", + "christina", + "coldbeer", + "citadel", + "brittney", + "highway", + "evil", + "monarch", + "morgan1", + "washingt", + "1997", + "bella1", + "berry", + "yaya", + "yolanda", + "superb", + "taxman", + "studman", + "stephanie", + "3636", + "sherri", + "sheriff", + "shepherd", + "poland", + "pizzas", + "tiffany1", + "toilet", + "latina", + "lassie", + "larry1", + "joseph1", + "mephisto", + "meagan", + "marian", + "reptile", + "rico", + "razor", + "1013", + "barron", + "hammer1", + "gypsy", + "grande", + "carroll", + "camper", + "chippy", + "cat123", + "call", + "chimera", + "fiesta", + "glock", + "glenn", + "domain", + "dieter", + "dragonba", + "onetwo", + "nygiants", + "odessa", + "password2", + "louie", + "quartz", + "prowler", + "prophet", + "towers", + "ultra", + "cocker", + "corleone", + "dakota1", + "cumm", + "nnnnnnn", + "natalia", + "boxers", + "hugo", + "heynow", + "hollow", + "iceberg", + "elvira", + "kittykat", + "kate", + "kitchen", + "wasabi", + "vikings1", + "impact", + "beerman", + "string", + "sleep", + "splinter", + "snoopy1", + "pipeline", + "pocket", + "legs", + "maple", + "mickey1", + "manuela", + "mermaid", + "micro", + "meowmeow", + "redbird", + "alisha", + "baura", + "battery", + "grass", + "chevys", + "chestnut", + "caravan", + "carina", + "charmed", + "fraser", + "frogman", + "diving", + "dogger", + "draven", + "drifter", + "oatmeal", + "paris1", + "longdong", + "quant4307s", + "rachel1", + "vegitta", + "cole", + "cobras", + "corsair", + "dadada", + "noelle", + "mylife", + "nine", + "bowwow", + "body", + "hotrats", + "eastwood", + "moonligh", + "modena", + "wave", + "illusion", + "iiiiiii", + "jayhawks", + "birgit", + "zone", + "sutton", + "susana", + "swingers", + "shocker", + "shrimp", + "sexgod", + "squall", + "stefanie", + "squeeze", + "soul", + "patrice", + "poiu", + "players", + "tigers1", + "toejam", + "tickler", + "line", + "julie1", + "jimbo1", + "jefferso", + "juanita", + "michael2", + "rodeo", + "robot", + "1023", + "annie1", + "bball", + "guess", + "happy2", + "charter", + "farm", + "flasher", + "falcon1", + "fiction", + "fastball", + "gadget", + "scrabble", + "diaper", + "dirtbike", + "dinner", + "oliver1", + "partner", + "paco", + "lucille", + "macman", + "poopy", + "popper", + "postman", + "ttttttt", + "ursula", + "acura", + "cowboy1", + "conan", + "daewoo", + "cyrus", + "customer", + "nation", + "nemrac58", + "nnnnn", + "nextel", + "bolton", + "bobdylan", + "hopeless", + "eureka", + "extra", + "kimmie", + "kcj9wx5n", + "killbill", + "musica", + "volkswag", + "wage", + "windmill", + "wert", + "vintage", + "iloveyou1", + "itsme", + "bessie", + "zippo", + "311311", + "starligh", + "smokey1", + "spot", + "snappy", + "soulmate", + "plasma", + "thelma", + "tonight", + "krusty", + "just4me", + "mcdonald", + "marius", + "rochelle", + "rebel1", + "1123", + "alfredo", + "aubrey", + "audi", + "chantal", + "fick", + "goaway", + "roses", + "sales", + "rusty2", + "dirt", + "dogbone", + "doofus", + "ooooooo", + "oblivion", + "mankind", + "luck", + "mahler", + "lllllll", + "pumper", + "puck", + "pulsar", + "valkyrie", + "tupac", + "compass", + "concorde", + "costello", + "cougars", + "delaware", + "niceguy", + "nocturne", + "bob123", + "boating", + "bronze", + "hopkins", + "herewego", + "hewlett", + "houhou", + "hubert", + "earnhard", + "eeeeeeee", + "keller", + "mingus", + "mobydick", + "venture", + "verizon", + "imation", + "1950", + "1948", + "1949", + "223344", + "bigbig", + "blossom", + "zack", + "wowwow", + "sissy", + "skinner", + "spiker", + "square", + "snooker", + "sluggo", + "player1", + "junk", + "jeannie", + "jsbach", + "jumbo", + "jewel", + "medic", + "robins", + "reddevil", + "reckless", + "123456a", + "1125", + "1031", + "beacon", + "astra", + "gumby", + "hammond", + "hassan", + "757575", + "585858", + "chillin", + "fuck1", + "sander", + "lowell", + "radiohea", + "upyours", + "trek", + "courage", + "coolcool", + "classics", + "choochoo", + "darryl", + "nikki1", + "nitro", + "bugs", + "boytoy", + "ellen", + "excite", + "kirsty", + "kane", + "wingnut", + "wireless", + "icu812", + "1master", + "beatle", + "bigblock", + "blanca", + "wolfen", + "summer99", + "sugar1", + "tartar", + "sexysexy", + "senna", + "sexman", + "sick", + "someone", + "soprano", + "pippin", + "platypus", + "pixies", + "telephon", + "land", + "laura1", + "laurent", + "rimmer", + "road", + "report", + "1020", + "12qwaszx", + "arturo", + "around", + "hamish", + "halifax", + "fishhead", + "forum", + "dododo", + "doit", + "outside", + "paramedi", + "lonesome", + "mandy1", + "twist", + "uuuuu", + "uranus", + "ttttt", + "butcher", + "bruce1", + "helper", + "hopeful", + "eduard", + "dusty1", + "kathy1", + "katherin", + "moonbeam", + "muscles", + "monster1", + "monkeybo", + "morton", + "windsurf", + "vvvvvvv", + "vivid", + "install", + "1947", + "187187", + "1941", + "1952", + "tatiana", + "susan1", + "31415926", + "sinned", + "sexxy", + "senator", + "sebastian", + "shadows", + "smoothie", + "snowflak", + "playstat", + "playa", + "playboy1", + "toaster", + "jerry1", + "marie1", + "mason1", + "merlin1", + "roger1", + "roadster", + "112358", + "1121", + "andrea1", + "bacardi", + "auto", + "hardware", + "hardy", + "789789", + "5555555", + "captain1", + "flores", + "fergus", + "sascha", + "rrrrrrr", + "dome", + "onion", + "nutter", + "lololo", + "qqqqqqq", + "quick", + "undertak", + "uuuuuuuu", + "uuuuuuu", + "criminal", + "cobain", + "cindy1", + "coors", + "dani", + "descent", + "nimbus", + "nomad", + "nanook", + "norwich", + "bomb", + "bombay", + "broker", + "hookup", + "kiwi", + "winners", + "jackpot", + "1a2b3c4d", + "1776", + "beardog", + "bighead", + "blast", + "bird33", + "0987", + "stress", + "shot", + "spooge", + "pelican", + "peepee", + "perry", + "pointer", + "titan", + "thedoors", + "jeremy1", + "annabell", + "altima", + "baba", + "hallie", + "hate", + "hardone", + "5454", + "candace", + "catwoman", + "flip", + "faithful", + "finance", + "farmboy", + "farscape", + "genesis1", + "salomon", + "destroy", + "papers", + "option", + "page", + "loser1", + "lopez", + "r2d2", + "pumpkins", + "training", + "chriss", + "cumcum", + "ninjas", + "ninja1", + "hung", + "erika", + "eduardo", + "killers", + "miller1", + "islander", + "jamesbond", + "intel", + "jarvis", + "19841984", + "2626", + "bizzare", + "blue12", + "biker", + "yoyoma", + "sushi", + "styles", + "shitface", + "series", + "shanti", + "spanker", + "steffi", + "smart", + "sphinx", + "please1", + "paulie", + "pistons", + "tiburon", + "limited", + "maxwell1", + "mdogg", + "rockies", + "armstron", + "alexia", + "arlene", + "alejandr", + "arctic", + "banger", + "audio", + "asimov", + "augustus", + "grandpa", + "753951", + "4you", + "chilly", + "care1839", + "chapman", + "flyfish", + "fantasia", + "freefall", + "santa", + "sandrine", + "oreo", + "ohshit", + "macbeth", + "madcat", + "loveya", + "mallory", + "rage", + "quentin", + "qwerqwer", + "project", + "ramirez", + "colnago", + "citizen", + "chocha", + "cobalt", + "crystal1", + "dabears", + "nevets", + "nineinch", + "broncos1", + "helene", + "huge", + "edgar", + "epsilon", + "easter", + "kestrel", + "moron", + "virgil", + "winston1", + "warrior1", + "iiiiiiii", + "iloveyou2", + "1616", + "beat", + "bettina", + "woowoo", + "zander", + "straight", + "shower", + "sloppy", + "specialk", + "tinkerbe", + "jellybea", + "reader", + "romero", + "redsox1", + "ride", + "1215", + "1112", + "annika", + "arcadia", + "answer", + "baggio", + "base", + "guido", + "555666", + "carmel", + "cayman", + "cbr900rr", + "chips", + "gabriell", + "gertrude", + "glennwei", + "roxy", + "sausages", + "disco", + "pass1", + "luna", + "lovebug", + "macmac", + "queenie", + "puffin", + "vanguard", + "trip", + "trinitro", + "airwolf", + "abbott", + "aaa111", + "cocaine", + "cisco", + "cottage", + "dayton", + "deadly", + "datsun", + "bricks", + "bumper", + "eldorado", + "kidrock", + "wizard1", + "whiskers", + "wind", + "wildwood", + "istheman", + "interest", + "italy", + "25802580", + "benoit", + "bigones", + "woodland", + "wolfpac", + "strawber", + "suicide", + "3030", + "sheba1", + "sixpack", + "peace1", + "physics", + "pearson", + "tigger2", + "toad", + "megan1", + "meow", + "ringo", + "roll", + "amsterdam", + "717171", + "686868", + "5424", + "catherine", + "canuck", + "football1", + "footjob", + "fulham", + "seagull", + "orgy", + "lobo", + "mancity", + "truth", + "trace", + "vancouve", + "vauxhall", + "acidburn", + "derf", + "myspace1", + "boozer", + "buttercu", + "howell", + "hola", + "easton", + "minemine", + "munch", + "jared", + "1dragon", + "biology", + "bestbuy", + "bigpoppa", + "blackout", + "blowfish", + "bmw325", + "bigbob", + "stream", + "talisman", + "tazz", + "sundevil", + "3333333", + "skate", + "shutup", + "shanghai", + "shop", + "spencer1", + "slowhand", + "polish", + "pinky1", + "tootie", + "thecrow", + "leroy", + "jonathon", + "jubilee", + "jingle", + "martine", + "matrix1", + "manowar", + "michaels", + "messiah", + "mclaren", + "resident", + "reilly", + "redbaron", + "rollins", + "romans", + "return", + "rivera", + "andromed", + "athlon", + "beach1", + "badgers", + "guitars", + "harald", + "harddick", + "gotribe", + "6996", + "7grout", + "5wr2i7h8", + "635241", + "chase1", + "carver", + "charlotte", + "fallout", + "fiddle", + "fredrick", + "fenris", + "francesc", + "fortuna", + "ferguson", + "fairlane", + "felipe", + "felix1", + "forward", + "gasman", + "frost", + "fucks", + "sahara", + "sassy1", + "dogpound", + "dogbert", + "divx1", + "manila", + "loretta", + "priest", + "pornporn", + "quasar", + "venom", + "987987", + "access1", + "clippers", + "daylight", + "decker", + "daman", + "data", + "dentist", + "crusty", + "nathan1", + "nnnnnnnn", + "bruno1", + "bucks", + "brodie", + "budapest", + "kittens", + "kerouac", + "mother1", + "waldo1", + "wedding", + "whistler", + "whatwhat", + "wanderer", + "idontkno", + "1942", + "1946", + "bigdawg", + "bigpimp", + "zaqwsx", + "414141", + "3000gt", + "434343", + "shoes", + "serpent", + "starr", + "smurf", + "pasword", + "tommie", + "thisisit", + "lake", + "john1", + "robotics", + "redeye", + "rebelz", + "1011", + "alatam", + "asses", + "asians", + "bama", + "banzai", + "harvest", + "gonzalez", + "hair", + "hanson", + "575757", + "5329", + "cascade", + "chinese", + "fatty", + "fender1", + "flower2", + "funky", + "sambo", + "drummer1", + "dogcat", + "dottie", + "oedipus", + "osama", + "macleod", + "prozac", + "private1", + "rampage", + "punch", + "presley", + "concord", + "cook", + "cinema", + "cornwall", + "cleaner", + "christopher", + "ciccio", + "corinne", + "clutch", + "corvet07", + "daemon", + "bruiser", + "boiler", + "hjkl", + "eyes", + "egghead", + "expert", + "ethan", + "kasper", + "mordor", + "wasted", + "jamess", + "iverson3", + "bluesman", + "zouzou", + "090909", + "1002", + "switch", + "stone1", + "4040", + "sisters", + "sexo", + "shawna", + "smith1", + "sperma", + "sneaky", + "polska", + "thewho", + "terminat", + "krypton", + "lawson", + "library", + "lekker", + "jules", + "johnson1", + "johann", + "justus", + "rockie", + "romano", + "aspire", + "bastards", + "goodie", + "cheese1", + "fenway", + "fishon", + "fishin", + "fuckoff1", + "girls1", + "sawyer", + "dolores", + "desmond", + "duane", + "doomsday", + "pornking", + "ramones", + "rabbits", + "transit", + "aaaaa1", + "clock", + "delilah", + "noel", + "boyz", + "bookworm", + "bongo", + "bunnies", + "brady", + "buceta", + "highbury", + "henry1", + "heels", + "eastern", + "krissy", + "mischief", + "mopar", + "ministry", + "vienna", + "weston", + "wildone", + "vodka", + "jayson", + "bigbooty", + "beavis1", + "betsy", + "xxxxxx1", + "yogibear", + "000001", + "0815", + "zulu", + "420000", + "september", + "sigmar", + "sprout", + "stalin", + "peggy", + "patch", + "lkjhgfds", + "lagnaf", + "rolex", + "redfox", + "referee", + "123123123", + "1231", + "angus1", + "ariana", + "ballin", + "attila", + "hall", + "greedy", + "grunt", + "747474", + "carpedie", + "cecile", + "caramel", + "foxylady", + "field", + "gatorade", + "gidget", + "futbol", + "frosch", + "saiyan", + "schmidt", + "drums", + "donner", + "doggy1", + "drum", + "doudou", + "pack", + "pain", + "nutmeg", + "quebec", + "valdepen", + "trash", + "triple", + "tosser", + "tuscl", + "track", + "comfort", + "choke", + "comein", + "cola", + "deputy", + "deadpool", + "bremen", + "borders", + "bronson", + "break", + "hotass", + "hotmail1", + "eskimo", + "eggman", + "koko", + "kieran", + "katrin", + "kordell1", + "komodo", + "mone", + "munich", + "vvvvvvvv", + "winger", + "jaeger", + "ivan", + "jackson5", + "2222222", + "bergkamp", + "bennie", + "bigben", + "zanzibar", + "worm", + "xxx123", + "sunny1", + "373737", + "services", + "sheridan", + "slater", + "slayer1", + "snoop", + "stacie", + "peachy", + "thecure", + "times", + "little1", + "jennaj", + "marquis", + "middle", + "rasta69", + "1114", + "aries", + "havana", + "gratis", + "calgary", + "checkers", + "flanker", + "salope", + "dirty1", + "draco", + "dogface", + "luv2epus", + "rainbow6", + "qwerty123", + "umpire", + "turnip", + "vbnm", + "tucson", + "troll", + "aileen", + "codered", + "commande", + "damon", + "nana", + "neon", + "nico", + "nightwin", + "neil", + "boomer1", + "bushido", + "hotmail0", + "horace", + "enternow", + "kaitlyn", + "keepout", + "karen1", + "mindy", + "mnbv", + "viewsoni", + "volcom", + "wizards", + "wine", + "1995", + "berkeley", + "bite", + "zach", + "woodstoc", + "tarpon", + "shinobi", + "starstar", + "phat", + "patience", + "patrol", + "toolbox", + "julien", + "johnny1", + "joebob", + "marble", + "riders", + "reflex", + "120676", + "1235", + "angelus", + "anthrax", + "atlas", + "hawks", + "grandam", + "harlem", + "hawaii50", + "gorgeous", + "655321", + "cabron", + "challeng", + "callisto", + "firewall", + "firefire", + "fischer", + "flyer", + "flower1", + "factory", + "federal", + "gambler", + "frodo1", + "funk", + "sand", + "sam123", + "scania", + "dingo", + "papito", + "passmast", + "olive", + "palermo", + "ou8123", + "lock", + "ranch", + "pride", + "randy1", + "twiggy", + "travis1", + "transfer", + "treetop", + "addict", + "admin1", + "963852", + "aceace", + "clarissa", + "cliff", + "cirrus", + "clifton", + "colin", + "bobdole", + "bonner", + "bogus", + "bonjovi", + "bootsy", + "boater", + "elway7", + "edison", + "kelvin", + "kenny1", + "moonshin", + "montag", + "moreno", + "wayne1", + "white1", + "jazzy", + "jakejake", + "1994", + "1991", + "2828", + "blunt", + "bluejays", + "beau", + "belmont", + "worthy", + "systems", + "sensei", + "southpark", + "stan", + "peeper", + "pharao", + "pigpen", + "tomahawk", + "teensex", + "leedsutd", + "larkin", + "jermaine", + "jeepster", + "jimjim", + "josephin", + "melons", + "marlon", + "matthias", + "marriage", + "robocop", + "1003", + "1027", + "antelope", + "azsxdc", + "gordo", + "hazard", + "granada", + "8989", + "7894", + "ceasar", + "cabernet", + "cheshire", + "california", + "chelle", + "candy1", + "fergie", + "fanny", + "fidelio", + "giorgio", + "fuckhead", + "ruth", + "sanford", + "diego", + "dominion", + "devon", + "panic", + "longer", + "mackie", + "qawsed", + "trucking", + "twelve", + "chloe1", + "coral", + "daddyo", + "nostromo", + "boyboy", + "booster", + "bucky", + "honolulu", + "esquire", + "dynamite", + "motor", + "mollydog", + "wilder", + "windows1", + "waffle", + "wallet", + "warning", + "virus", + "washburn", + "wealth", + "vincent1", + "jabber", + "jaguars", + "javelin", + "irishman", + "idefix", + "bigdog1", + "blue42", + "blanked", + "blue32", + "biteme1", + "bearcats", + "blaine", + "yessir", + "sylveste", + "team", + "stephan", + "sunfire", + "tbird", + "stryker", + "3ip76k2", + "sevens", + "sheldon", + "pilgrim", + "tenchi", + "titman", + "leeds", + "lithium", + "lander", + "linkin", + "landon", + "marijuan", + "mariner", + "markie", + "midnite", + "reddwarf", + "1129", + "123asd", + "12312312", + "allstar", + "albany", + "asdf12", + "antonia", + "aspen", + "hardball", + "goldfing", + "7734", + "49ers", + "carlo", + "chambers", + "cable", + "carnage", + "callum", + "carlos1", + "fitter", + "fandango", + "festival", + "flame", + "gofast", + "gamma", + "fucmy69", + "scrapper", + "dogwood", + "django", + "magneto", + "loose", + "premium", + "addison", + "9999999", + "abc1234", + "cromwell", + "newyear", + "nichole", + "bookie", + "burns", + "bounty", + "brown1", + "bologna", + "earl", + "entrance", + "elway", + "killjoy", + "kerry", + "keenan", + "kick", + "klondike", + "mini", + "mouser", + "mohammed", + "wayer", + "impreza", + "irene", + "insomnia", + "24682468", + "2580", + "24242424", + "billbill", + "bellaco", + "blessing", + "blues1", + "bedford", + "blanco", + "blunts", + "stinks", + "teaser", + "streets", + "sf49ers", + "shovel", + "solitude", + "spikey", + "sonia", + "pimpdadd", + "timeout", + "toffee", + "lefty", + "johndoe", + "johndeer", + "mega", + "manolo", + "mentor", + "margie", + "ratman", + "ridge", + "record", + "rhodes", + "robin1", + "1124", + "1210", + "1028", + "1226", + "another", + "babylove", + "barbados", + "harbor", + "gramma", + "646464", + "carpente", + "chaos1", + "fishbone", + "fireblad", + "glasgow", + "frogs", + "scissors", + "screamer", + "salem", + "scuba1", + "ducks", + "driven", + "doggies", + "dicky", + "donovan", + "obsidian", + "rams", + "progress", + "tottenham", + "aikman", + "comanche", + "corolla", + "clarke", + "conway", + "cumslut", + "cyborg", + "dancing", + "boston1", + "bong", + "houdini", + "helmut", + "elvisp", + "edge", + "keksa12", + "misha", + "monty1", + "monsters", + "wetter", + "watford", + "wiseguy", + "veronika", + "visitor", + "janelle", + "1989", + "1987", + "20202020", + "biatch", + "beezer", + "bigguns", + "blueball", + "bitchy", + "wyoming", + "yankees2", + "wrestler", + "stupid1", + "sealteam", + "sidekick", + "simple1", + "smackdow", + "sporting", + "spiral", + "smeller", + "sperm", + "plato", + "tophat", + "test2", + "theatre", + "thick", + "toomuch", + "leigh", + "jello", + "jewish", + "junkie", + "maxim", + "maxime", + "meadow", + "remingto", + "roofer", + "124038", + "1018", + "1269", + "1227", + "123457", + "arkansas", + "alberta", + "aramis", + "andersen", + "beaker", + "barcelona", + "baltimor", + "googoo", + "goochi", + "852456", + "4711", + "catcher", + "carman", + "champ1", + "chess", + "fortress", + "fishfish", + "firefigh", + "geezer", + "rsalinas", + "samuel1", + "saigon", + "scooby1", + "doors", + "dick1", + "devin", + "doom", + "dirk", + "doris", + "dontknow", + "load", + "magpies", + "manfred", + "raleigh", + "vader1", + "universa", + "tulips", + "defense", + "mygirl", + "burn", + "bowtie", + "bowman", + "holycow", + "heinrich", + "honeys", + "enforcer", + "katherine", + "minerva", + "wheeler", + "witch", + "waterboy", + "jaime", + "irving", + "1992", + "23skidoo", + "bimbo", + "blue11", + "birddog", + "woodman", + "womble", + "zildjian", + "030303", + "stinker", + "stoppedby", + "sexybabe", + "speakers", + "slugger", + "spotty", + "smoke1", + "polopolo", + "perfect1", + "things", + "torpedo", + "tender", + "thrasher", + "lakeside", + "lilith", + "jimmys", + "jerk", + "junior1", + "marsh", + "masamune", + "rice", + "root", + "1214", + "april1", + "allgood", + "bambi", + "grinch", + "767676", + "5252", + "cherries", + "chipmunk", + "cezer121", + "carnival", + "capecod", + "finder", + "flint", + "fearless", + "goats", + "funstuff", + "gideon", + "savior", + "seabee", + "sandro", + "schalke", + "salasana", + "disney1", + "duckman", + "options", + "pancake", + "pantera1", + "malice", + "lookin", + "love123", + "lloyd", + "qwert123", + "puppet", + "prayers", + "union", + "tracer", + "crap", + "creation", + "cwoui", + "nascar24", + "hookers", + "hollie", + "hewitt", + "estrella", + "erection", + "ernesto", + "ericsson", + "edthom", + "kaylee", + "kokoko", + "kokomo", + "kimball", + "morales", + "mooses", + "monk", + "walton", + "weekend", + "inter", + "internal", + "1michael", + "1993", + "19781978", + "25252525", + "worker", + "summers", + "surgery", + "shibby", + "shamus", + "skibum", + "sheepdog", + "sex69", + "spliff", + "slipper", + "spoons", + "spanner", + "snowbird", + "slow", + "toriamos", + "temp123", + "tennesse", + "lakers1", + "jomama", + "julio", + "mazdarx7", + "rosario", + "recon", + "riddle", + "room", + "revolver", + "1025", + "1101", + "barney1", + "babycake", + "baylor", + "gotham", + "gravity", + "hallowee", + "hancock", + "616161", + "515000", + "caca", + "cannabis", + "castor", + "chilli", + "fdsa", + "getout", + "fuck69", + "gators1", + "sail", + "sable", + "rumble", + "dolemite", + "dork", + "dickens", + "duffer", + "dodgers1", + "painting", + "onions", + "logger", + "lorena", + "lookout", + "magic32", + "port", + "poon", + "prime", + "twat", + "coventry", + "citroen", + "christmas", + "civicsi", + "cocksucker", + "coochie", + "compaq1", + "nancy1", + "buzzer", + "boulder", + "butkus", + "bungle", + "hogtied", + "honor", + "hero", + "hotgirls", + "hilary", + "heidi1", + "eggplant", + "mustang6", + "mortal", + "monkey12", + "wapapapa", + "wendy1", + "volleyba", + "vibrate", + "vicky", + "bledsoe", + "blink", + "birthday4", + "woof", + "xxxxx1", + "talk", + "stephen1", + "suburban", + "stock", + "tabatha", + "sheeba", + "start1", + "soccer10", + "something", + "starcraft", + "soccer12", + "peanut1", + "plastics", + "penthous", + "peterbil", + "tools", + "tetsuo", + "torino", + "tennis1", + "termite", + "ladder", + "last", + "lemmein", + "lakewood", + "jughead", + "melrose", + "megane", + "reginald", + "redone", + "request", + "angela1", + "alive", + "alissa", + "goodgirl", + "gonzo1", + "golden1", + "gotyoass", + "656565", + "626262", + "capricor", + "chains", + "calvin1", + "foolish", + "fallon", + "getmoney", + "godfather", + "gabber", + "gilligan", + "runaway", + "salami", + "dummy", + "dungeon", + "dudedude", + "dumb", + "dope", + "opus", + "paragon", + "oxygen", + "panhead", + "pasadena", + "opendoor", + "odyssey", + "magellan", + "lottie", + "printing", + "pressure", + "prince1", + "trustme", + "christa", + "court", + "davies", + "neville", + "nono", + "bread", + "buffet", + "hound", + "kajak", + "killkill", + "mona", + "moto", + "mildred", + "winner1", + "vixen", + "whiteboy", + "versace", + "winona", + "voyager1", + "instant", + "indy", + "jackjack", + "bigal", + "beech", + "biggun", + "blake1", + "blue99", + "big1", + "woods", + "synergy", + "success1", + "336699", + "sixty9", + "shark1", + "skin", + "simba1", + "sharpe", + "sebring", + "spongebo", + "spunk", + "springs", + "sliver", + "phialpha", + "password9", + "pizza1", + "plane", + "perkins", + "pookey", + "tickling", + "lexingky", + "lawman", + "joe123", + "jolly", + "mike123", + "romeo1", + "redheads", + "reserve", + "apple123", + "alanis", + "ariane", + "antony", + "backbone", + "aviation", + "band", + "hand", + "green123", + "haley", + "carlitos", + "byebye", + "cartman1", + "camden", + "chewy", + "camaross", + "favorite6", + "forumwp", + "franks", + "ginscoot", + "fruity", + "sabrina1", + "devil666", + "doughnut", + "pantie", + "oldone", + "paintball", + "lumina", + "rainbow1", + "prosper", + "total", + "true", + "umbrella", + "ajax", + "951753", + "achtung", + "abc12345", + "compact", + "color", + "corn", + "complete", + "christi", + "closer", + "corndog", + "deerhunt", + "darklord", + "dank", + "nimitz", + "brandy1", + "bowl", + "breanna", + "holidays", + "hetfield", + "holein1", + "hillbill", + "hugetits", + "east", + "evolutio", + "kenobi", + "whiplash", + "waldo", + "wg8e3wjf", + "wing", + "istanbul", + "invis", + "1996", + "benton", + "bigjohn", + "bluebell", + "beef", + "beater", + "benji", + "bluejay", + "xyzzy", + "wrestling", + "storage", + "superior", + "suckdick", + "taichi", + "stellar", + "stephane", + "shaker", + "skirt", + "seymour", + "semper", + "splurge", + "squeak", + "pearls", + "playball", + "pitch", + "phyllis", + "pooky", + "piss", + "tomas", + "titfuck", + "joemama", + "johnny5", + "marcello", + "marjorie", + "married", + "maxi", + "rhubarb", + "rockwell", + "ratboy", + "reload", + "rooney", + "redd", + "1029", + "1030", + "1220", + "anchor", + "bbking", + "baritone", + "gryphon", + "gone", + "57chevy", + "494949", + "celeron", + "fishy", + "gladiator", + "fucker1", + "roswell", + "dougie", + "downer", + "dicker", + "diva", + "domingo", + "donjuan", + "nympho", + "omar", + "praise", + "racers", + "trick", + "trauma", + "truck1", + "trample", + "acer", + "corwin", + "cricket1", + "clemente", + "climax", + "denmark", + "cuervo", + "notnow", + "nittany", + "neutron", + "native", + "bosco1", + "buffa", + "breaker", + "hello2", + "hydro", + "estelle", + "exchange", + "explore", + "kisskiss", + "kittys", + "kristian", + "montecar", + "modem", + "mississi", + "mooney", + "weiner", + "washington", + "20012001", + "bigdick1", + "bibi", + "benfica", + "yahoo1", + "striper", + "tabasco", + "supra", + "383838", + "456654", + "seneca", + "serious", + "shuttle", + "socks", + "stanton", + "penguin1", + "pathfind", + "testibil", + "thethe", + "listen", + "lightning", + "lighting", + "jeter2", + "marma", + "mark1", + "metoo", + "republic", + "rollin", + "redleg", + "redbone", + "redskin", + "rocco", + "1245", + "armand", + "anthony7", + "altoids", + "andrews", + "barley", + "away", + "asswipe", + "bauhaus", + "bbbbbb1", + "gohome", + "harrier", + "golfpro", + "goldeney", + "818181", + "6666666", + "5000", + "5rxypn", + "cameron1", + "calling", + "checker", + "calibra", + "fields", + "freefree", + "faith1", + "fist", + "fdm7ed", + "finally", + "giraffe", + "glasses", + "giggles", + "fringe", + "gate", + "georgie", + "scamper", + "rrpass1", + "screwyou", + "duffy", + "deville", + "dimples", + "pacino", + "ontario", + "passthie", + "oberon", + "quest1", + "postov1000", + "puppydog", + "puffer", + "raining", + "protect", + "qwerty7", + "trey", + "tribe", + "ulysses", + "tribal", + "adam25", + "a1234567", + "compton", + "collie", + "cleopatr", + "contract", + "davide", + "norris", + "namaste", + "myrtle", + "buffalo1", + "bonovox", + "buckley", + "bukkake", + "burning", + "burner", + "bordeaux", + "burly", + "hun999", + "emilie", + "elmo", + "enters", + "enrique", + "keisha", + "mohawk", + "willard", + "vgirl", + "whale", + "vince", + "jayden", + "jarrett", + "1812", + "1943", + "222333", + "bigjim", + "bigd", + "zoom", + "wordup", + "ziggy1", + "yahooo", + "workout", + "young1", + "written", + "xmas", + "zzzzzz1", + "surfer1", + "strife", + "sunlight", + "tasha1", + "skunk", + "shauna", + "seth", + "soft", + "sprinter", + "peaches1", + "planes", + "pinetree", + "plum", + "pimping", + "theforce", + "thedon", + "toocool", + "leeann", + "laddie", + "list", + "lkjh", + "lara", + "joke", + "jupiter1", + "mckenzie", + "matty", + "rene", + "redrose", + "1200", + "102938", + "annmarie", + "alexa", + "antares", + "austin31", + "ground", + "goose1", + "737373", + "78945612", + "789987", + "6464", + "calimero", + "caster", + "casper1", + "cement", + "chevrolet", + "chessie", + "caddy", + "chill", + "child", + "canucks", + "feeling", + "favorite", + "fellatio", + "f00tball", + "francine", + "gateway2", + "gigi", + "gamecube", + "giovanna", + "rugby1", + "scheisse", + "dshade", + "dudes", + "dixie1", + "owen", + "offshore", + "olympia", + "lucas1", + "macaroni", + "manga", + "pringles", + "puff", + "tribble", + "trouble1", + "ussy", + "core", + "clint", + "coolhand", + "colonial", + "colt", + "debra", + "darthvad", + "dealer", + "cygnusx1", + "natalie1", + "newark", + "husband", + "hiking", + "errors", + "eighteen", + "elcamino", + "emmett", + "emilia", + "koolaid", + "knight1", + "murphy1", + "volcano", + "idunno", + "2005", + "2233", + "block", + "benito", + "blueberr", + "biguns", + "yamahar1", + "zapper", + "zorro1", + "0911", + "3006", + "sixsix", + "shopper", + "siobhan", + "sextoy", + "stafford", + "snowboard", + "speedway", + "sounds", + "pokey", + "peabody", + "playboy2", + "titi", + "think", + "toast", + "toonarmy", + "lister", + "lambda", + "joecool", + "jonas", + "joyce", + "juniper", + "mercer", + "max123", + "manny", + "massimo", + "mariposa", + "met2002", + "reggae", + "ricky1", + "1236", + "1228", + "1016", + "all4one", + "arianna", + "baberuth", + "asgard", + "gonzales", + "484848", + "5683", + "6669", + "catnip", + "chiquita", + "charisma", + "capslock", + "cashmone", + "chat", + "figure", + "galant", + "frenchy", + "gizmodo1", + "girlies", + "gabby", + "garner", + "screwy", + "doubled", + "divers", + "dte4uw", + "done", + "dragonfl", + "maker", + "locks", + "rachelle", + "treble", + "twinkie", + "trailer", + "tropical", + "acid", + "crescent", + "cooking", + "cococo", + "cory", + "dabomb", + "daffy", + "dandfa", + "cyrano", + "nathanie", + "briggs", + "boners", + "helium", + "horton", + "hoffman", + "hellas", + "espresso", + "emperor", + "killa", + "kikimora", + "wanda", + "w4g8at", + "verona", + "ilikeit", + "iforget", + "1944", + "20002000", + "birthday1", + "beatles1", + "blue1", + "bigdicks", + "beethove", + "blacklab", + "blazers", + "benny1", + "woodwork", + "0069", + "0101", + "taffy", + "susie", + "survivor", + "swim", + "stokes", + "4567", + "shodan", + "spoiled", + "steffen", + "pissed", + "pavlov", + "pinnacle", + "place", + "petunia", + "terrell", + "thirty", + "toni", + "tito", + "teenie", + "lemonade", + "lily", + "lillie", + "lalakers", + "lebowski", + "lalalala", + "ladyboy", + "jeeper", + "joyjoy", + "mercury1", + "mantle", + "mannn", + "rocknrol", + "riversid", + "reeves", + "123aaa", + "11112222", + "121314", + "1021", + "1004", + "1120", + "allen1", + "ambers", + "amstel", + "ambrose", + "alice1", + "alleycat", + "allegro", + "ambrosia", + "alley", + "australia", + "hatred", + "gspot", + "graves", + "goodsex", + "hattrick", + "harpoon", + "878787", + "8inches", + "4wwvte", + "cassandr", + "charlie123", + "case", + "chavez", + "fighting", + "gabriela", + "gatsby", + "fudge", + "gerry", + "generic", + "gareth", + "fuckme2", + "samm", + "sage", + "seadog", + "satchmo", + "scxakv", + "santafe", + "dipper", + "dingle", + "dizzy", + "outoutout", + "madmad", + "london1", + "qbg26i", + "pussy123", + "randolph", + "vaughn", + "tzpvaw", + "vamp", + "comedy", + "comp", + "cowgirl", + "coldplay", + "dawgs", + "delaney", + "nt5d27", + "novifarm", + "needles", + "notredam", + "newness", + "mykids", + "bryan1", + "bouncer", + "hihihi", + "honeybee", + "iceman1", + "herring", + "horn", + "hook", + "hotlips", + "dynamo", + "klaus", + "kittie", + "kappa", + "kahlua", + "muffy", + "mizzou", + "mohamed", + "musical", + "wannabe", + "wednesda", + "whatup", + "weller", + "waterfal", + "willy1", + "invest", + "blanche", + "bear1", + "billabon", + "youknow", + "zelda", + "yyyyyy1", + "zachary1", + "01234567", + "070462", + "zurich", + "superstar", + "storms", + "tail", + "stiletto", + "strat", + "427900", + "sigmachi", + "shelter", + "shells", + "sexy123", + "smile1", + "sophie1", + "stefano", + "stayout", + "somerset", + "smithers", + "playmate", + "pinkfloyd", + "phish1", + "payday", + "thebear", + "telefon", + "laetitia", + "kswbdu", + "larson", + "jetta", + "jerky", + "melina", + "metro", + "revoluti", + "retire", + "respect", + "1216", + "1201", + "1204", + "1222", + "1115", + "archange", + "barry1", + "handball", + "676767", + "chandra", + "chewbacc", + "flesh", + "furball", + "gocubs", + "fruit", + "fullback", + "gman", + "gentle", + "dunbar", + "dewalt", + "dominiqu", + "diver1", + "dhip6a", + "olemiss", + "ollie", + "mandrake", + "mangos", + "pretzel", + "pusssy", + "tripleh", + "valdez", + "vagabond", + "clean", + "comment", + "crew", + "clovis", + "deaths", + "dandan", + "csfbr5yy", + "deadspin", + "darrel", + "ninguna", + "noah", + "ncc74656", + "bootsie", + "bp2002", + "bourbon", + "brennan", + "bumble", + "books", + "hose", + "heyyou", + "houston1", + "hemlock", + "hippo", + "hornets", + "hurricane", + "horseman", + "hogan", + "excess", + "extensa", + "muffin1", + "virginie", + "werdna", + "idontknow", + "info", + "iron", + "jack1", + "1bitch", + "151nxjmt", + "bendover", + "bmwbmw", + "bills", + "zaq123", + "wxcvbn", + "surprise", + "supernov", + "tahoe", + "talbot", + "simona", + "shakur", + "sexyone", + "seviyi", + "sonja", + "smart1", + "speed1", + "pepito", + "phantom1", + "playoffs", + "terry1", + "terrier", + "laser1", + "lite", + "lancia", + "johngalt", + "jenjen", + "jolene", + "midori", + "message", + "maserati", + "matteo", + "mental", + "miami1", + "riffraff", + "ronald1", + "reason", + "rhythm", + "1218", + "1026", + "123987", + "1015", + "1103", + "armada", + "architec", + "austria", + "gotmilk", + "hawkins", + "gray", + "camila", + "camp", + "cambridg", + "charge", + "camero", + "flex", + "foreplay", + "getoff", + "glacier", + "glotest", + "froggie", + "gerbil", + "rugger", + "sanity72", + "salesman", + "donna1", + "dreaming", + "deutsch", + "orchard", + "oyster", + "palmtree", + "ophelia", + "pajero", + "m5wkqf", + "magenta", + "luckyone", + "treefrog", + "vantage", + "usmarine", + "tyvugq", + "uptown", + "abacab", + "aaaaaa1", + "advance", + "chuck1", + "delmar", + "darkange", + "cyclones", + "nate", + "navajo", + "nope", + "border", + "bubba123", + "building", + "iawgk2", + "hrfzlz", + "dylan1", + "enrico", + "encore", + "emilio", + "eclipse1", + "killian", + "kayleigh", + "mutant", + "mizuno", + "mustang2", + "video1", + "viewer", + "weed420", + "whales", + "jaguar1", + "insight", + "1990", + "159159", + "1love", + "bliss", + "bears1", + "bigtruck", + "binder", + "bigboss", + "blitz", + "xqgann", + "yeahyeah", + "zeke", + "zardoz", + "stickman", + "table", + "3825", + "signal", + "sentra", + "side", + "shiva", + "skipper1", + "singapor", + "southpaw", + "sonora", + "squid", + "slamdunk", + "slimjim", + "placid", + "photon", + "placebo", + "pearl1", + "test12", + "therock1", + "tiger123", + "leinad", + "legman", + "jeepers", + "joeblow", + "mccarthy", + "mike23", + "redcar", + "rhinos", + "rjw7x4", + "1102", + "13576479", + "112211", + "alcohol", + "gwju3g", + "greywolf", + "7bgiqk", + "7878", + "535353", + "4snz9g", + "candyass", + "cccccc1", + "carola", + "catfight", + "cali", + "fister", + "fosters", + "finland", + "frankie1", + "gizzmo", + "fuller", + "royalty", + "rugrat", + "sandie", + "rudolf", + "dooley", + "dive", + "doreen", + "dodo", + "drop", + "oemdlg", + "out3xf", + "paddy", + "opennow", + "puppy1", + "qazwsxedc", + "pregnant", + "quinn", + "ramjet", + "under", + "uncle", + "abraxas", + "corner", + "creed", + "cocoa", + "crown", + "cows", + "cn42qj", + "dancer1", + "death666", + "damned", + "nudity", + "negative", + "nimda2k", + "buick", + "bobb", + "braves1", + "brook", + "henrik", + "higher", + "hooligan", + "dust", + "everlast", + "karachi", + "mortis", + "mulligan", + "monies", + "motocros", + "wally1", + "weapon", + "waterman", + "view", + "willie1", + "vicki", + "inspiron", + "1test", + "2929", + "bigblack", + "xytfu7", + "yackwin", + "zaq1xsw2", + "yy5rbfsc", + "100100", + "0660", + "tahiti", + "takehana", + "talks", + "332211", + "3535", + "sedona", + "seawolf", + "skydiver", + "shine", + "spleen", + "slash", + "spjfet", + "special1", + "spooner", + "slimshad", + "sopranos", + "spock1", + "penis1", + "patches1", + "terri", + "thierry", + "thething", + "toohot", + "large", + "limpone", + "johnnie", + "mash4077", + "matchbox", + "masterp", + "maxdog", + "ribbit", + "reed", + "rita", + "rockin", + "redhat", + "rising", + "1113", + "14789632", + "1331", + "allday", + "aladin", + "andrey", + "amethyst", + "ariel", + "anytime", + "baseball1", + "athome", + "basil", + "goofy1", + "greenman", + "gustavo", + "goofball", + "ha8fyp", + "goodday", + "778899", + "charon", + "chappy", + "castillo", + "caracas", + "cardiff", + "capitals", + "canada1", + "cajun", + "catter", + "freddy1", + "favorite2", + "frazier", + "forme", + "follow", + "forsaken", + "feelgood", + "gavin", + "gfxqx686", + "garlic", + "sarge", + "saskia", + "sanjose", + "russ", + "salsa", + "dilbert1", + "dukeduke", + "downhill", + "longhair", + "loop", + "locutus", + "lockdown", + "malachi", + "mamacita", + "lolipop", + "rainyday", + "pumpkin1", + "punker", + "prospect", + "rambo1", + "rainbows", + "quake", + "twin", + "trinity1", + "trooper1", + "aimee", + "citation", + "coolcat", + "crappy", + "default", + "dental", + "deniro", + "d9ungl", + "daddys", + "napoli", + "nautica", + "nermal", + "bukowski", + "brick", + "bubbles1", + "bogota", + "board", + "branch", + "breath", + "buds", + "hulk", + "humphrey", + "hitachi", + "evans", + "ender", + "export", + "kikiki", + "kcchiefs", + "kram", + "morticia", + "montrose", + "mongo", + "waqw3p", + "wizzard", + "visited", + "whdbtp", + "whkzyc", + "image", + "154ugeiu", + "1fuck", + "binky", + "blind", + "bigred1", + "blubber", + "benz", + "becky1", + "year2005", + "wonderfu", + "wooden", + "xrated", + "0001", + "tampabay", + "survey", + "tammy1", + "stuffer", + "3mpz4r", + "3000", + "3some", + "selina", + "sierra1", + "shampoo", + "silk", + "shyshy", + "slapnuts", + "standby", + "spartan1", + "sprocket", + "sometime", + "stanley1", + "poker1", + "plus", + "thought", + "theshit", + "torture", + "thinking", + "lavalamp", + "light1", + "laserjet", + "jediknig", + "jjjjj1", + "jocelyn", + "mazda626", + "menthol", + "maximo", + "margaux", + "medic1", + "release", + "richter", + "rhino1", + "roach", + "renate", + "repair", + "reveal", + "1209", + "1234321", + "amigos", + "apricot", + "alexandra", + "asdfgh1", + "hairball", + "hatter", + "graduate", + "grimace", + "7xm5rq", + "6789", + "cartoons", + "capcom", + "cheesy", + "cashflow", + "carrots", + "camping", + "fanatic", + "fool", + "format", + "fleming", + "girlie", + "glover", + "gilmore", + "gardner", + "safeway", + "ruthie", + "dogfart", + "dondon", + "diapers", + "outsider", + "odin", + "opiate", + "lollol", + "love12", + "loomis", + "mallrats", + "prague", + "primetime21", + "pugsley", + "program", + "r29hqq", + "touch", + "valleywa", + "airman", + "abcdefg1", + "darkone", + "cummer", + "dempsey", + "damn", + "nadia", + "natedogg", + "nineball", + "ndeyl5", + "natchez", + "newone", + "normandy", + "nicetits", + "buddy123", + "buddys", + "homely", + "husky", + "iceland", + "hr3ytm", + "highlife", + "holla", + "earthlin", + "exeter", + "eatmenow", + "kimkim", + "karine", + "k2trix", + "kernel", + "kirkland", + "money123", + "moonman", + "miles1", + "mufasa", + "mousey", + "wilma", + "wilhelm", + "whites", + "warhamme", + "instinct", + "jackass1", + "2277", + "20spanks", + "blobby", + "blair", + "blinky", + "bikers", + "blackjack", + "becca", + "blue23", + "xman", + "wyvern", + "085tzzqi", + "zxzxzx", + "zsmj2v", + "suede", + "t26gn4", + "sugars", + "sylvie", + "tantra", + "swoosh", + "swiss", + "4226", + "4271", + "321123", + "383pdjvl", + "shoe", + "shane1", + "shelby1", + "spades", + "spain", + "smother", + "soup", + "sparhawk", + "pisser", + "photo1", + "pebble", + "phones", + "peavey", + "picnic", + "pavement", + "terra", + "thistle", + "tokyo", + "therapy", + "lives", + "linden", + "kronos", + "lilbit", + "linux", + "johnston", + "material", + "melanie1", + "marbles", + "redlight", + "reno", + "recall", + "1208", + "1138", + "1008", + "alchemy", + "aolsucks", + "alexalex", + "atticus", + "auditt", + "ballet", + "b929ezzh", + "goodyear", + "hanna", + "griffith", + "gubber", + "863abgsg", + "7474", + "797979", + "464646", + "543210", + "4zqauf", + "4949", + "ch5nmk", + "carlito", + "chewey", + "carebear", + "caleb", + "checkmat", + "cheddar", + "chachi", + "fever", + "forgetit", + "fine", + "forlife", + "giants1", + "gates", + "getit", + "gamble", + "gerhard", + "galileo", + "g3ujwg", + "ganja", + "rufus1", + "rushmore", + "scouts", + "discus", + "dudeman", + "olympus", + "oscars", + "osprey", + "madcow", + "locust", + "loyola", + "mammoth", + "proton", + "rabbit1", + "question", + "ptfe3xxp", + "pwxd5x", + "purple1", + "punkass", + "prophecy", + "uyxnyd", + "tyson1", + "aircraft", + "access99", + "abcabc", + "cocktail", + "colts", + "civilwar", + "cleveland", + "claudia1", + "contour", + "clement", + "dddddd1", + "cypher", + "denied", + "dapzu455", + "dagmar", + "daisydog", + "name", + "noles", + "butters", + "buford", + "hoochie", + "hotel", + "hoser", + "eddy", + "ellis", + "eldiablo", + "kingrich", + "mudvayne", + "motown", + "mp8o6d", + "wife", + "vipergts", + "italiano", + "innocent", + "2055", + "2211", + "beavers", + "bloke", + "blade1", + "yamato", + "zooropa", + "yqlgr667", + "050505", + "zxcvbnm1", + "zw6syj", + "suckcock", + "tango1", + "swing", + "stern", + "stephens", + "swampy", + "susanna", + "tammie", + "445566", + "333666", + "380zliki", + "sexpot", + "sexylady", + "sixtynin", + "sickboy", + "spiffy", + "sleeping", + "skylark", + "sparkles", + "slam", + "pintail", + "phreak", + "places", + "teller", + "timtim", + "tires", + "thighs", + "left", + "latex", + "llamas", + "letsdoit", + "lkjhg", + "landmark", + "letters", + "lizzard", + "marlins", + "marauder", + "metal1", + "manu", + "register", + "righton", + "1127", + "alain", + "alcat", + "amigo", + "basebal1", + "azertyui", + "attract", + "azrael", + "hamper", + "gotenks", + "golfgti", + "gutter", + "hawkwind", + "h2slca", + "harman", + "grace1", + "6chid8", + "789654", + "canine", + "casio", + "cazzo", + "chamber", + "cbr900", + "cabrio", + "calypso", + "capetown", + "feline", + "flathead", + "fisherma", + "flipmode", + "fungus", + "goal", + "g9zns4", + "full", + "giggle", + "gabriel1", + "fuck123", + "saffron", + "dogmeat", + "dreamcas", + "dirtydog", + "dunlop", + "douche", + "dresden", + "dickdick", + "destiny1", + "pappy", + "oaktree", + "lydia", + "luft4", + "puta", + "prayer", + "ramada", + "trumpet1", + "vcradq", + "tulip", + "tracy71", + "tycoon", + "aaaaaaa1", + "conquest", + "click", + "chitown", + "corps", + "creepers", + "constant", + "couples", + "code", + "cornhole", + "danman", + "dada", + "density", + "d9ebk7", + "cummins", + "darth", + "cute", + "nash", + "nirvana1", + "nixon", + "norbert", + "nestle", + "brenda1", + "bonanza", + "bundy", + "buddies", + "hotspur", + "heavy", + "horror", + "hufmqw", + "electro", + "erasure", + "enough", + "elisabet", + "etvww4", + "ewyuza", + "eric1", + "kinder", + "kenken", + "kismet", + "klaatu", + "musician", + "milamber", + "willi", + "waiting", + "isacs155", + "igor", + "1million", + "1letmein", + "x35v8l", + "yogi", + "ywvxpz", + "xngwoj", + "zippy1", + "020202", + "****", + "stonewal", + "sweeney", + "story", + "sentry", + "sexsexsex", + "spence", + "sonysony", + "smirnoff", + "star12", + "solace", + "sledge", + "states", + "snyder", + "star1", + "paxton", + "pentagon", + "pkxe62", + "pilot1", + "pommes", + "paulpaul", + "plants", + "tical", + "tictac", + "toes", + "lighthou", + "lemans", + "kubrick", + "letmein22", + "letmesee", + "jys6wz", + "jonesy", + "jjjjjj1", + "jigga", + "joelle", + "mate", + "merchant", + "redstorm", + "riley1", + "rosa", + "relief", + "14141414", + "1126", + "allison1", + "badboy1", + "asthma", + "auggie", + "basement", + "hartley", + "hartford", + "hardwood", + "gumbo", + "616913", + "57np39", + "56qhxs", + "4mnveh", + "cake", + "forbes", + "fatluvr69", + "fqkw5m", + "fidelity", + "feathers", + "fresno", + "godiva", + "gecko", + "gladys", + "gibson1", + "gogators", + "fridge", + "general1", + "saxman", + "rowing", + "sammys", + "scotts", + "scout1", + "sasasa", + "samoht", + "dragon69", + "ducky", + "dragonball", + "driller", + "p3wqaw", + "nurse", + "papillon", + "oneone", + "openit", + "optimist", + "longshot", + "portia", + "rapier", + "pussy2", + "ralphie", + "tuxedo", + "ulrike", + "undertow", + "trenton", + "copenhag", + "come", + "delldell", + "culinary", + "deltas", + "mytime", + "nicky", + "nickie", + "noname", + "noles1", + "bucker", + "bopper", + "bullock", + "burnout", + "bryce", + "hedges", + "ibilltes", + "hihje863", + "hitter", + "ekim", + "espana", + "eatme69", + "elpaso", + "envelope", + "express1", + "eeeeee1", + "eatme1", + "karaoke", + "kara", + "mustang5", + "misses", + "wellingt", + "willem", + "waterski", + "webcam", + "jasons", + "infinite", + "iloveyou!", + "jakarta", + "belair", + "bigdad", + "beerme", + "yoshi", + "yinyang", + "zimmer", + "x24ik3", + "063dyjuy", + "0000007", + "ztmfcq", + "stopit", + "stooges", + "survival", + "stockton", + "symow8", + "strato", + "2hot4u", + "ship", + "simons", + "skins", + "shakes", + "sex1", + "shield", + "snacks", + "softtail", + "slimed123", + "pizzaman", + "pipe", + "pitt", + "pathetic", + "pinto", + "tigercat", + "tonton", + "lager", + "lizzy", + "juju", + "john123", + "jennings", + "josiah", + "jesse1", + "jordon", + "jingles", + "martian", + "mario1", + "rootedit", + "rochard", + "redwine", + "requiem", + "riverrat", + "rats", + "1117", + "1014", + "1205", + "althea", + "allie", + "amor", + "amiga", + "alpina", + "alert", + "atreides", + "banana1", + "bahamut", + "hart", + "golfman", + "happines", + "7uftyx", + "5432", + "5353", + "5151", + "4747", + "byron", + "chatham", + "chadwick", + "cherie", + "foxfire", + "ffvdj474", + "freaked", + "foreskin", + "gayboy", + "gggggg1", + "glenda", + "gameover", + "glitter", + "funny1", + "scoobydoo", + "scroll", + "rudolph", + "saddle", + "saxophon", + "dingbat", + "digimon", + "omicron", + "parsons", + "ohio", + "panda1", + "loloxx", + "macintos", + "lululu", + "lollypop", + "racer1", + "queen1", + "qwertzui", + "prick", + "upnfmc", + "tyrant", + "trout1", + "9skw5g", + "aceman", + "adelaide", + "acls2h", + "aaabbb", + "acapulco", + "aggie", + "comcast", + "craft", + "crissy", + "cloudy", + "cq2kph", + "custer", + "d6o8pm", + "cybersex", + "davecole", + "darian", + "crumbs", + "daisey", + "davedave", + "dasani", + "needle", + "mzepab", + "myporn", + "narnia", + "nineteen", + "booger1", + "bravo1", + "budgie", + "btnjey", + "highlander", + "hotel6", + "humbug", + "edwin", + "ewtosi", + "kristin1", + "kobe", + "knuckles", + "keith1", + "katarina", + "muff", + "muschi", + "montana1", + "wingchun", + "wiggle", + "whatthe", + "walking", + "watching", + "vette1", + "vols", + "virago", + "intj3a", + "ishmael", + "intern", + "jachin", + "illmatic", + "199999", + "2010", + "beck", + "blender", + "bigpenis", + "bengal", + "blue1234", + "your", + "zaqxsw", + "xray", + "xxxxxxx1", + "zebras", + "yanks", + "worlds", + "tadpole", + "stripes", + "svetlana", + "3737", + "4343", + "3728", + "4444444", + "368ejhih", + "solar", + "sonne", + "smalls", + "sniffer", + "sonata", + "squirts", + "pitcher", + "playstation", + "pktmxr", + "pescator", + "points", + "texaco", + "lesbos", + "lilian", + "l8v53x", + "jo9k2jw2", + "jimbeam", + "josie", + "jimi", + "jupiter2", + "jurassic", + "marines1", + "maya", + "rocket1", + "ringer", + "14725836", + "12345679", + "1219", + "123098", + "1233", + "alessand", + "althor", + "angelika", + "arch", + "armando", + "alpha123", + "basher", + "barefeet", + "balboa", + "bbbbb1", + "banks", + "badabing", + "harriet", + "gopack", + "golfnut", + "gsxr1000", + "gregory1", + "766rglqy", + "8520", + "753159", + "8dihc6", + "69camaro", + "666777", + "cheeba", + "chino", + "calendar", + "cheeky", + "camel1", + "fishcake", + "falling", + "flubber", + "giuseppe", + "gianni", + "gloves", + "gnasher23", + "frisbee", + "fuzzy1", + "fuzzball", + "sauce", + "save13tx", + "schatz", + "russell1", + "sandra1", + "scrotum", + "scumbag", + "sabre", + "samdog", + "dripping", + "dragon12", + "dragster", + "paige", + "orwell", + "mainland", + "lunatic", + "lonnie", + "lotion", + "maine", + "maddux", + "qn632o", + "poophead", + "rapper", + "porn4life", + "producer", + "rapunzel", + "tracks", + "velocity", + "vanessa1", + "ulrich", + "trueblue", + "vampire1", + "abacus", + "902100", + "crispy", + "corky", + "crane", + "chooch", + "d6wnro", + "cutie", + "deal", + "dabulls", + "dehpye", + "navyseal", + "njqcw4", + "nownow", + "nigger1", + "nightowl", + "nonenone", + "nightmar", + "bustle", + "buddy2", + "boingo", + "bugman", + "bulletin", + "bosshog", + "bowie", + "hybrid", + "hillside", + "hilltop", + "hotlegs", + "honesty", + "hzze929b", + "hhhhh1", + "hellohel", + "eloise", + "evilone", + "edgewise", + "e5pftu", + "eded", + "embalmer", + "excalibur", + "elefant", + "kenzie", + "karl", + "karin", + "killah", + "kleenex", + "mouses", + "mounta1n", + "motors", + "mutley", + "muffdive", + "vivitron", + "winfield", + "wednesday", + "w00t88", + "iloveit", + "jarjar", + "incest", + "indycar", + "17171717", + "1664", + "17011701", + "222777", + "2663", + "beelch", + "benben", + "yitbos", + "yyyyy1", + "yasmin", + "zapata", + "zzzzz1", + "stooge", + "tangerin", + "taztaz", + "stewart1", + "summer69", + "sweetness", + "system1", + "surveyor", + "stirling", + "3qvqod", + "3way", + "456321", + "sizzle", + "simhrq", + "shrink", + "shawnee", + "someday", + "sparty", + "ssptx452", + "sphere", + "spark", + "slammed", + "sober", + "persian", + "peppers", + "ploppy", + "pn5jvw", + "poobear", + "pianos", + "plaster", + "testme", + "tiff", + "thriller", + "larissa", + "lennox", + "jewell", + "master12", + "messier", + "rockey", + "1229", + "1217", + "1478", + "1009", + "anastasi", + "almighty", + "amonra", + "aragon", + "argentin", + "albino", + "azazel", + "grinder", + "6uldv8", + "83y6pv", + "8888888", + "4tlved", + "515051", + "carsten", + "changes", + "flanders", + "flyers88", + "ffffff1", + "firehawk", + "foreman", + "firedog", + "flashman", + "ggggg1", + "gerber", + "godspeed", + "galway", + "giveitup", + "funtimes", + "gohan", + "giveme", + "geryfe", + "frenchie", + "sayang", + "rudeboy", + "savanna", + "sandals", + "devine", + "dougal", + "drag0n", + "dga9la", + "disaster", + "desktop", + "only", + "onlyone", + "otter", + "pandas", + "mafia", + "lombard", + "luckys", + "lovejoy", + "lovelife", + "manders", + "product", + "qqh92r", + "qcmfd454", + "pork", + "radar1", + "punani", + "ptbdhw", + "turtles", + "undertaker", + "trs8f7", + "tramp", + "ugejvp", + "abba", + "911turbo", + "acdc", + "abcd123", + "clever", + "corina", + "cristian", + "create", + "crash1", + "colony", + "crosby", + "delboy", + "daniele", + "davinci", + "daughter", + "notebook", + "niki", + "nitrox", + "borabora", + "bonzai", + "budd", + "brisbane", + "hotter", + "heeled", + "heroes", + "hooyah", + "hotgirl", + "i62gbq", + "horse1", + "hills", + "hpk2qc", + "epvjb6", + "echo", + "korean", + "kristie", + "mnbvc", + "mohammad", + "mind", + "mommy1", + "munster", + "wade", + "wiccan", + "wanted", + "jacket", + "2369", + "bettyboo", + "blondy", + "bismark", + "beanbag", + "bjhgfi", + "blackice", + "yvtte545", + "ynot", + "yess", + "zlzfrh", + "wolvie", + "007bond", + "******", + "tailgate", + "tanya1", + "sxhq65", + "stinky1", + "3234412", + "3ki42x", + "seville", + "shimmer", + "sheryl", + "sienna", + "shitshit", + "skillet", + "seaman", + "sooners1", + "solaris", + "smartass", + "pastor", + "pasta", + "pedros", + "pennywis", + "pfloyd", + "tobydog", + "thetruth", + "lethal", + "letme1n", + "leland", + "jenifer", + "mario66", + "micky", + "rocky2", + "rewq", + "ripped", + "reindeer", + "1128", + "1207", + "1104", + "1432", + "aprilia", + "allstate", + "alyson", + "bagels", + "basic", + "baggies", + "barb", + "barrage", + "greatest", + "gomez", + "guru", + "guard", + "72d5tn", + "606060", + "4wcqjn", + "caldwell", + "chance1", + "catalog", + "faust", + "film", + "flange", + "fran", + "fartman", + "geil", + "gbhcf2", + "fussball", + "glen", + "fuaqz4", + "gameboy", + "garnet", + "geneviev", + "rotary", + "seahawk", + "russel", + "saab", + "seal", + "samadams", + "devlt4", + "ditto", + "drevil", + "drinker", + "deuce", + "dipstick", + "donut", + "octopus", + "ottawa", + "losangel", + "loverman", + "porky", + "q9umoz", + "rapture", + "pump", + "pussy4me", + "university", + "triplex", + "ue8fpw", + "trent", + "trophy", + "turbos", + "troubles", + "agent", + "aaa340", + "churchil", + "crazyman", + "consult", + "creepy", + "craven", + "class", + "cutiepie", + "ddddd1", + "dejavu", + "cuxldv", + "nettie", + "nbvibt", + "nikon", + "niko", + "norwood", + "nascar1", + "nolan", + "bubba2", + "boobear", + "boogers", + "buff", + "bullwink", + "bully", + "bulldawg", + "horsemen", + "escalade", + "editor", + "eagle2", + "dynamic", + "ella", + "efyreg", + "edition", + "kidney", + "minnesot", + "mogwai", + "morrow", + "msnxbi", + "moonlight", + "mwq6qlzo", + "wars", + "werder", + "verygood", + "voodoo1", + "wheel", + "iiiiii1", + "159951", + "1624", + "1911a1", + "2244", + "bellagio", + "bedlam", + "belkin", + "bill1", + "woodrow", + "xirt2k", + "worship", + "??????", + "tanaka", + "swift", + "susieq", + "sundown", + "sukebe", + "tales", + "swifty", + "2fast4u", + "senate", + "sexe", + "sickness", + "shroom", + "shaun", + "seaweed", + "skeeter1", + "status", + "snicker", + "sorrow", + "spanky1", + "spook", + "patti", + "phaedrus", + "pilots", + "pinch", + "peddler", + "theo", + "thumper1", + "tessie", + "tiger7", + "tmjxn151", + "thematri", + "l2g7k3", + "letmeinn", + "lazy", + "jeffjeff", + "joan", + "johnmish", + "mantra", + "mariana", + "mike69", + "marshal", + "mart", + "mazda6", + "riptide", + "robots", + "rental", + "1107", + "1130", + "142857", + "11001001", + "1134", + "armored", + "alvin", + "alec", + "allnight", + "alright", + "amatuers", + "bartok", + "attorney", + "astral", + "baboon", + "bahamas", + "balls1", + "bassoon", + "hcleeb", + "happyman", + "granite", + "graywolf", + "golf1", + "gomets", + "8vjzus", + "7890", + "789123", + "8uiazp", + "5757", + "474jdvff", + "551scasi", + "50cent", + "camaro1", + "cherry1", + "chemist", + "final", + "firenze", + "fishtank", + "farrell", + "freewill", + "glendale", + "frogfrog", + "gerhardt", + "ganesh", + "same", + "scirocco", + "devilman", + "doodles", + "dinger", + "okinawa", + "olympic", + "nursing", + "orpheus", + "ohmygod", + "paisley", + "pallmall", + "null", + "lounge", + "lunchbox", + "manhatta", + "mahalo", + "mandarin", + "qwqwqw", + "qguvyt", + "pxx3eftp", + "president", + "rambler", + "puzzle", + "poppy1", + "turk182", + "trotter", + "vdlxuc", + "trish", + "tugboat", + "valiant", + "tracie", + "uwrl7c", + "chris123", + "coaster", + "cmfnpu", + "decimal", + "debbie1", + "dandy", + "daedalus", + "dede", + "natasha1", + "nissan1", + "nancy123", + "nevermin", + "napalm", + "newcastle", + "boats", + "branden", + "britt", + "bonghit", + "hester", + "ibxnsm", + "hhhhhh1", + "holger", + "durham", + "edmonton", + "erwin", + "equinox", + "dvader", + "kimmy", + "knulla", + "mustafa", + "monsoon", + "mistral", + "morgana", + "monica1", + "mojave", + "month", + "monterey", + "mrbill", + "vkaxcs", + "victor1", + "wacker", + "wendell", + "violator", + "vfdhif", + "wilson1", + "wavpzt", + "verena", + "wildstar", + "winter99", + "iqzzt580", + "jarrod", + "imback", + "1914", + "19741974", + "1monkey", + "1q2w3e4r5t", + "2500", + "2255", + "blank", + "bigshow", + "bigbucks", + "blackcoc", + "zoomer", + "wtcacq", + "wobble", + "xmen", + "xjznq5", + "yesterda", + "yhwnqc", + "zzzxxx", + "streak", + "393939", + "2fchbg", + "skinhead", + "skilled", + "shakira", + "shaft", + "shadow12", + "seaside", + "sigrid", + "sinful", + "silicon", + "smk7366", + "snapshot", + "sniper1", + "soccer11", + "staff", + "slap", + "smutty", + "peepers", + "pleasant", + "plokij", + "pdiddy", + "pimpdaddy", + "thrust", + "terran", + "topaz", + "today1", + "lionhear", + "littlema", + "lauren1", + "lincoln1", + "lgnu9d", + "laughing", + "juneau", + "methos", + "medina", + "merlyn", + "rogue1", + "romulus", + "redshift", + "1202", + "1469", + "12locked", + "arizona1", + "alfarome", + "al9agd", + "aol123", + "altec", + "apollo1", + "arse", + "baker1", + "bbb747", + "bach", + "axeman", + "astro1", + "hawthorn", + "goodfell", + "hawks1", + "gstring", + "hannes", + "8543852", + "868686", + "4ng62t", + "554uzpad", + "5401", + "567890", + "5232", + "catfood", + "frame", + "flow", + "fire1", + "flipflop", + "fffff1", + "fozzie", + "fluff", + "garrison", + "fzappa", + "furious", + "round", + "rustydog", + "sandberg", + "scarab", + "satin", + "ruger", + "samsung1", + "destin", + "diablo2", + "dreamer1", + "detectiv", + "dominick", + "doqvq3", + "drywall", + "paladin1", + "papabear", + "offroad", + "panasonic", + "nyyankee", + "luetdi", + "qcfmtz", + "pyf8ah", + "puddles", + "privacy", + "rainer", + "pussyeat", + "ralph1", + "princeto", + "trivia", + "trewq", + "tri5a3", + "advent", + "9898", + "agyvorc", + "clarkie", + "coach1", + "courier", + "contest", + "christo", + "corinna", + "chowder", + "concept", + "climbing", + "cyzkhw", + "davidb", + "dad2ownu", + "days", + "daredevi", + "de7mdf", + "nose", + "necklace", + "nazgul", + "booboo1", + "broad", + "bonzo", + "brenna", + "boot", + "butch1", + "huskers1", + "hgfdsa", + "hornyman", + "elmer", + "elektra", + "england1", + "elodie", + "kermit1", + "knife", + "kaboom", + "minute", + "modern", + "motherfucker", + "morten", + "mocha", + "monday1", + "morgoth", + "ward", + "weewee", + "weenie", + "walters", + "vorlon", + "website", + "wahoo", + "ilovegod", + "insider", + "jayman", + "1911", + "1dallas", + "1900", + "1ranger", + "201jedlz", + "2501", + "1qaz", + "bertram", + "bignuts", + "bigbad", + "beebee", + "billows", + "belize", + "bebe", + "wvj5np", + "wu4etd", + "yamaha1", + "wrinkle5", + "zebra1", + "yankee1", + "zoomzoom", + "09876543", + "0311", + "?????", + "stjabn", + "tainted", + "3tmnej", + "shoot", + "skooter", + "skelter", + "sixteen", + "starlite", + "smack", + "spice1", + "stacey1", + "smithy", + "perrin", + "pollux", + "peternorth", + "pixie", + "paulina", + "piston", + "pick", + "poets", + "pine", + "toons", + "tooth", + "topspin", + "kugm7b", + "legends", + "jeepjeep", + "juliana", + "joystick", + "junkmail", + "jojojojo", + "jonboy", + "judge", + "midland", + "meteor", + "mccabe", + "matter", + "mayfair", + "meeting", + "merrill", + "raul", + "riches", + "reznor", + "rockrock", + "reboot", + "reject", + "robyn", + "renee1", + "roadway", + "rasta220", + "1411", + "1478963", + "1019", + "archery", + "allman", + "andyandy", + "barks", + "bagpuss", + "auckland", + "gooseman", + "hazmat", + "gucci", + "guns", + "grammy", + "happydog", + "greek", + "7kbe9d", + "7676", + "6bjvpe", + "5lyedn", + "5858", + "5291", + "charlie2", + "chas", + "c7lrwu", + "candys", + "chateau", + "ccccc1", + "cardinals", + "fear", + "fihdfv", + "fortune12", + "gocats", + "gaelic", + "fwsadn", + "godboy", + "gldmeo", + "fx3tuo", + "fubar1", + "garland", + "generals", + "gforce", + "rxmtkp", + "rulz", + "sairam", + "dunhill", + "division", + "dogggg", + "detect", + "details", + "doll", + "drinks", + "ozlq6qwm", + "ov3ajy", + "lockout", + "makayla", + "macgyver", + "mallorca", + "loves", + "prima", + "pvjegu", + "qhxbij", + "raphael", + "prelude1", + "totoro", + "tusymo", + "trousers", + "tunnel", + "valeria", + "tulane", + "turtle1", + "tracy1", + "aerosmit", + "abbey1", + "address", + "clticic", + "clueless", + "cooper1", + "comets", + "collect", + "corbin", + "delpiero", + "derick", + "cyprus", + "dante1", + "dave1", + "nounours", + "neal", + "nexus6", + "nero", + "nogard", + "norfolk", + "brent1", + "booyah", + "bootleg", + "buckaroo", + "bulls23", + "bulls1", + "booper", + "heretic", + "icecube", + "hellno", + "hounds", + "honeydew", + "hooters1", + "hoes", + "howie", + "hevnm4", + "hugohugo", + "eighty", + "epson", + "evangeli", + "eeeee1", + "eyphed", + "tiwaribachjayega" +] \ No newline at end of file diff --git a/src/pages/Common/SignUp/email-tlds.json b/src/pages/Common/SignUp/email-tlds.json new file mode 100644 index 0000000..73b5df1 --- /dev/null +++ b/src/pages/Common/SignUp/email-tlds.json @@ -0,0 +1,1481 @@ +[ + "aaa", + "aarp", + "abarth", + "abb", + "abbott", + "abbvie", + "abc", + "able", + "abogado", + "abudhabi", + "ac", + "academy", + "accenture", + "accountant", + "accountants", + "aco", + "actor", + "ad", + "ads", + "adult", + "ae", + "aeg", + "aero", + "aetna", + "af", + "afl", + "africa", + "ag", + "agakhan", + "agency", + "ai", + "aig", + "airbus", + "airforce", + "airtel", + "akdn", + "al", + "alfaromeo", + "alibaba", + "alipay", + "allfinanz", + "allstate", + "ally", + "alsace", + "alstom", + "am", + "amazon", + "americanexpress", + "americanfamily", + "amex", + "amfam", + "amica", + "amsterdam", + "analytics", + "android", + "anquan", + "anz", + "ao", + "aol", + "apartments", + "app", + "apple", + "aq", + "aquarelle", + "ar", + "arab", + "aramco", + "archi", + "army", + "arpa", + "art", + "arte", + "as", + "asda", + "asia", + "associates", + "at", + "athleta", + "attorney", + "au", + "auction", + "audi", + "audible", + "audio", + "auspost", + "author", + "auto", + "autos", + "avianca", + "aw", + "aws", + "ax", + "axa", + "az", + "azure", + "ba", + "baby", + "baidu", + "banamex", + "bananarepublic", + "band", + "bank", + "bar", + "barcelona", + "barclaycard", + "barclays", + "barefoot", + "bargains", + "baseball", + "basketball", + "bauhaus", + "bayern", + "bb", + "bbc", + "bbt", + "bbva", + "bcg", + "bcn", + "bd", + "be", + "beats", + "beauty", + "beer", + "bentley", + "berlin", + "best", + "bestbuy", + "bet", + "bf", + "bg", + "bh", + "bharti", + "bi", + "bible", + "bid", + "bike", + "bing", + "bingo", + "bio", + "biz", + "bj", + "black", + "blackfriday", + "blockbuster", + "blog", + "bloomberg", + "blue", + "bm", + "bms", + "bmw", + "bn", + "bnpparibas", + "bo", + "boats", + "boehringer", + "bofa", + "bom", + "bond", + "boo", + "book", + "booking", + "bosch", + "bostik", + "boston", + "bot", + "boutique", + "box", + "br", + "bradesco", + "bridgestone", + "broadway", + "broker", + "brother", + "brussels", + "bs", + "bt", + "build", + "builders", + "business", + "buy", + "buzz", + "bv", + "bw", + "by", + "bz", + "bzh", + "ca", + "cab", + "cafe", + "cal", + "call", + "calvinklein", + "cam", + "camera", + "camp", + "canon", + "capetown", + "capital", + "capitalone", + "car", + "caravan", + "cards", + "care", + "career", + "careers", + "cars", + "casa", + "case", + "cash", + "casino", + "cat", + "catering", + "catholic", + "cba", + "cbn", + "cbre", + "cbs", + "cc", + "cd", + "center", + "ceo", + "cern", + "cf", + "cfa", + "cfd", + "cg", + "ch", + "chanel", + "channel", + "charity", + "chase", + "chat", + "cheap", + "chintai", + "christmas", + "chrome", + "church", + "ci", + "cipriani", + "circle", + "cisco", + "citadel", + "citi", + "citic", + "city", + "cityeats", + "ck", + "cl", + "claims", + "cleaning", + "click", + "clinic", + "clinique", + "clothing", + "cloud", + "club", + "clubmed", + "cm", + "cn", + "co", + "coach", + "codes", + "coffee", + "college", + "cologne", + "com", + "comcast", + "commbank", + "community", + "company", + "compare", + "computer", + "comsec", + "condos", + "construction", + "consulting", + "contact", + "contractors", + "cooking", + "cookingchannel", + "cool", + "coop", + "corsica", + "country", + "coupon", + "coupons", + "courses", + "cpa", + "cr", + "credit", + "creditcard", + "creditunion", + "cricket", + "crown", + "crs", + "cruise", + "cruises", + "cu", + "cuisinella", + "cv", + "cw", + "cx", + "cy", + "cymru", + "cyou", + "cz", + "dabur", + "dad", + "dance", + "data", + "date", + "dating", + "datsun", + "day", + "dclk", + "dds", + "de", + "deal", + "dealer", + "deals", + "degree", + "delivery", + "dell", + "deloitte", + "delta", + "democrat", + "dental", + "dentist", + "desi", + "design", + "dev", + "dhl", + "diamonds", + "diet", + "digital", + "direct", + "directory", + "discount", + "discover", + "dish", + "diy", + "dj", + "dk", + "dm", + "dnp", + "do", + "docs", + "doctor", + "dog", + "domains", + "dot", + "download", + "drive", + "dtv", + "dubai", + "dunlop", + "dupont", + "durban", + "dvag", + "dvr", + "dz", + "earth", + "eat", + "ec", + "eco", + "edeka", + "edu", + "education", + "ee", + "eg", + "email", + "emerck", + "energy", + "engineer", + "engineering", + "enterprises", + "epson", + "equipment", + "er", + "ericsson", + "erni", + "es", + "esq", + "estate", + "et", + "etisalat", + "eu", + "eurovision", + "eus", + "events", + "exchange", + "expert", + "exposed", + "express", + "extraspace", + "fage", + "fail", + "fairwinds", + "faith", + "family", + "fan", + "fans", + "farm", + "farmers", + "fashion", + "fast", + "fedex", + "feedback", + "ferrari", + "ferrero", + "fi", + "fiat", + "fidelity", + "fido", + "film", + "final", + "finance", + "financial", + "fire", + "firestone", + "firmdale", + "fish", + "fishing", + "fit", + "fitness", + "fj", + "fk", + "flickr", + "flights", + "flir", + "florist", + "flowers", + "fly", + "fm", + "fo", + "foo", + "food", + "foodnetwork", + "football", + "ford", + "forex", + "forsale", + "forum", + "foundation", + "fox", + "fr", + "free", + "fresenius", + "frl", + "frogans", + "frontdoor", + "frontier", + "ftr", + "fujitsu", + "fun", + "fund", + "furniture", + "futbol", + "fyi", + "ga", + "gal", + "gallery", + "gallo", + "gallup", + "game", + "games", + "gap", + "garden", + "gay", + "gb", + "gbiz", + "gd", + "gdn", + "ge", + "gea", + "gent", + "genting", + "george", + "gf", + "gg", + "ggee", + "gh", + "gi", + "gift", + "gifts", + "gives", + "giving", + "gl", + "glass", + "gle", + "global", + "globo", + "gm", + "gmail", + "gmbh", + "gmo", + "gmx", + "gn", + "godaddy", + "gold", + "goldpoint", + "golf", + "goo", + "goodyear", + "goog", + "google", + "gop", + "got", + "gov", + "gp", + "gq", + "gr", + "grainger", + "graphics", + "gratis", + "green", + "gripe", + "grocery", + "group", + "gs", + "gt", + "gu", + "guardian", + "gucci", + "guge", + "guide", + "guitars", + "guru", + "gw", + "gy", + "hair", + "hamburg", + "hangout", + "haus", + "hbo", + "hdfc", + "hdfcbank", + "health", + "healthcare", + "help", + "helsinki", + "here", + "hermes", + "hgtv", + "hiphop", + "hisamitsu", + "hitachi", + "hiv", + "hk", + "hkt", + "hm", + "hn", + "hockey", + "holdings", + "holiday", + "homedepot", + "homegoods", + "homes", + "homesense", + "honda", + "horse", + "hospital", + "host", + "hosting", + "hot", + "hoteles", + "hotels", + "hotmail", + "house", + "how", + "hr", + "hsbc", + "ht", + "hu", + "hughes", + "hyatt", + "hyundai", + "ibm", + "icbc", + "ice", + "icu", + "id", + "ie", + "ieee", + "ifm", + "ikano", + "il", + "im", + "imamat", + "imdb", + "immo", + "immobilien", + "in", + "inc", + "industries", + "infiniti", + "info", + "ing", + "ink", + "institute", + "insurance", + "insure", + "int", + "international", + "intuit", + "investments", + "io", + "ipiranga", + "iq", + "ir", + "irish", + "is", + "ismaili", + "ist", + "istanbul", + "it", + "itau", + "itv", + "jaguar", + "java", + "jcb", + "je", + "jeep", + "jetzt", + "jewelry", + "jio", + "jll", + "jm", + "jmp", + "jnj", + "jo", + "jobs", + "joburg", + "jot", + "joy", + "jp", + "jpmorgan", + "jprs", + "juegos", + "juniper", + "kaufen", + "kddi", + "ke", + "kerryhotels", + "kerrylogistics", + "kerryproperties", + "kfh", + "kg", + "kh", + "ki", + "kia", + "kids", + "kim", + "kinder", + "kindle", + "kitchen", + "kiwi", + "km", + "kn", + "koeln", + "komatsu", + "kosher", + "kp", + "kpmg", + "kpn", + "kr", + "krd", + "kred", + "kuokgroup", + "kw", + "ky", + "kyoto", + "kz", + "la", + "lacaixa", + "lamborghini", + "lamer", + "lancaster", + "lancia", + "land", + "landrover", + "lanxess", + "lasalle", + "lat", + "latino", + "latrobe", + "law", + "lawyer", + "lb", + "lc", + "lds", + "lease", + "leclerc", + "lefrak", + "legal", + "lego", + "lexus", + "lgbt", + "li", + "lidl", + "life", + "lifeinsurance", + "lifestyle", + "lighting", + "like", + "lilly", + "limited", + "limo", + "lincoln", + "link", + "lipsy", + "live", + "living", + "lk", + "llc", + "llp", + "loan", + "loans", + "locker", + "locus", + "lol", + "london", + "lotte", + "lotto", + "love", + "lpl", + "lplfinancial", + "lr", + "ls", + "lt", + "ltd", + "ltda", + "lu", + "lundbeck", + "luxe", + "luxury", + "lv", + "ly", + "ma", + "madrid", + "maif", + "maison", + "makeup", + "man", + "management", + "mango", + "map", + "market", + "marketing", + "markets", + "marriott", + "marshalls", + "maserati", + "mattel", + "mba", + "mc", + "mckinsey", + "md", + "me", + "med", + "media", + "meet", + "melbourne", + "meme", + "memorial", + "men", + "menu", + "merckmsd", + "mg", + "mh", + "miami", + "microsoft", + "mil", + "mini", + "mint", + "mit", + "mitsubishi", + "mk", + "ml", + "mlb", + "mls", + "mm", + "mma", + "mn", + "mo", + "mobi", + "mobile", + "moda", + "moe", + "moi", + "mom", + "monash", + "money", + "monster", + "mormon", + "mortgage", + "moscow", + "moto", + "motorcycles", + "mov", + "movie", + "mp", + "mq", + "mr", + "ms", + "msd", + "mt", + "mtn", + "mtr", + "mu", + "museum", + "music", + "mutual", + "mv", + "mw", + "mx", + "my", + "mz", + "na", + "nab", + "nagoya", + "name", + "natura", + "navy", + "nba", + "nc", + "ne", + "nec", + "net", + "netbank", + "netflix", + "network", + "neustar", + "new", + "news", + "next", + "nextdirect", + "nexus", + "nf", + "nfl", + "ng", + "ngo", + "nhk", + "ni", + "nico", + "nike", + "nikon", + "ninja", + "nissan", + "nissay", + "nl", + "no", + "nokia", + "northwesternmutual", + "norton", + "now", + "nowruz", + "nowtv", + "np", + "nr", + "nra", + "nrw", + "ntt", + "nu", + "nyc", + "nz", + "obi", + "observer", + "office", + "okinawa", + "olayan", + "olayangroup", + "oldnavy", + "ollo", + "om", + "omega", + "one", + "ong", + "onl", + "online", + "ooo", + "open", + "oracle", + "orange", + "org", + "organic", + "origins", + "osaka", + "otsuka", + "ott", + "ovh", + "pa", + "page", + "panasonic", + "paris", + "pars", + "partners", + "parts", + "party", + "passagens", + "pay", + "pccw", + "pe", + "pet", + "pf", + "pfizer", + "pg", + "ph", + "pharmacy", + "phd", + "philips", + "phone", + "photo", + "photography", + "photos", + "physio", + "pics", + "pictet", + "pictures", + "pid", + "pin", + "ping", + "pink", + "pioneer", + "pizza", + "pk", + "pl", + "place", + "play", + "playstation", + "plumbing", + "plus", + "pm", + "pn", + "pnc", + "pohl", + "poker", + "politie", + "porn", + "post", + "pr", + "pramerica", + "praxi", + "press", + "prime", + "pro", + "prod", + "productions", + "prof", + "progressive", + "promo", + "properties", + "property", + "protection", + "pru", + "prudential", + "ps", + "pt", + "pub", + "pw", + "pwc", + "py", + "qa", + "qpon", + "quebec", + "quest", + "racing", + "radio", + "re", + "read", + "realestate", + "realtor", + "realty", + "recipes", + "red", + "redstone", + "redumbrella", + "rehab", + "reise", + "reisen", + "reit", + "reliance", + "ren", + "rent", + "rentals", + "repair", + "report", + "republican", + "rest", + "restaurant", + "review", + "reviews", + "rexroth", + "rich", + "richardli", + "ricoh", + "ril", + "rio", + "rip", + "ro", + "rocher", + "rocks", + "rodeo", + "rogers", + "room", + "rs", + "rsvp", + "ru", + "rugby", + "ruhr", + "run", + "rw", + "rwe", + "ryukyu", + "sa", + "saarland", + "safe", + "safety", + "sakura", + "sale", + "salon", + "samsclub", + "samsung", + "sandvik", + "sandvikcoromant", + "sanofi", + "sap", + "sarl", + "sas", + "save", + "saxo", + "sb", + "sbi", + "sbs", + "sc", + "sca", + "scb", + "schaeffler", + "schmidt", + "scholarships", + "school", + "schule", + "schwarz", + "science", + "scot", + "sd", + "se", + "search", + "seat", + "secure", + "security", + "seek", + "select", + "sener", + "services", + "seven", + "sew", + "sex", + "sexy", + "sfr", + "sg", + "sh", + "shangrila", + "sharp", + "shaw", + "shell", + "shia", + "shiksha", + "shoes", + "shop", + "shopping", + "shouji", + "show", + "showtime", + "si", + "silk", + "sina", + "singles", + "site", + "sj", + "sk", + "ski", + "skin", + "sky", + "skype", + "sl", + "sling", + "sm", + "smart", + "smile", + "sn", + "sncf", + "so", + "soccer", + "social", + "softbank", + "software", + "sohu", + "solar", + "solutions", + "song", + "sony", + "soy", + "spa", + "space", + "sport", + "spot", + "sr", + "srl", + "ss", + "st", + "stada", + "staples", + "star", + "statebank", + "statefarm", + "stc", + "stcgroup", + "stockholm", + "storage", + "store", + "stream", + "studio", + "study", + "style", + "su", + "sucks", + "supplies", + "supply", + "support", + "surf", + "surgery", + "suzuki", + "sv", + "swatch", + "swiss", + "sx", + "sy", + "sydney", + "systems", + "sz", + "tab", + "taipei", + "talk", + "taobao", + "target", + "tatamotors", + "tatar", + "tattoo", + "tax", + "taxi", + "tc", + "tci", + "td", + "tdk", + "team", + "tech", + "technology", + "tel", + "temasek", + "tennis", + "teva", + "tf", + "tg", + "th", + "thd", + "theater", + "theatre", + "tiaa", + "tickets", + "tienda", + "tiffany", + "tips", + "tires", + "tirol", + "tj", + "tjmaxx", + "tjx", + "tk", + "tkmaxx", + "tl", + "tm", + "tmall", + "tn", + "to", + "today", + "tokyo", + "tools", + "top", + "toray", + "toshiba", + "total", + "tours", + "town", + "toyota", + "toys", + "tr", + "trade", + "trading", + "training", + "travel", + "travelchannel", + "travelers", + "travelersinsurance", + "trust", + "trv", + "tt", + "tube", + "tui", + "tunes", + "tushu", + "tv", + "tvs", + "tw", + "tz", + "ua", + "ubank", + "ubs", + "ug", + "uk", + "unicom", + "university", + "uno", + "uol", + "ups", + "us", + "uy", + "uz", + "va", + "vacations", + "vana", + "vanguard", + "vc", + "ve", + "vegas", + "ventures", + "verisign", + "versicherung", + "vet", + "vg", + "vi", + "viajes", + "video", + "vig", + "viking", + "villas", + "vin", + "vip", + "virgin", + "visa", + "vision", + "viva", + "vivo", + "vlaanderen", + "vn", + "vodka", + "volkswagen", + "volvo", + "vote", + "voting", + "voto", + "voyage", + "vu", + "vuelos", + "wales", + "walmart", + "walter", + "wang", + "wanggou", + "watch", + "watches", + "weather", + "weatherchannel", + "webcam", + "weber", + "website", + "wed", + "wedding", + "weibo", + "weir", + "wf", + "whoswho", + "wien", + "wiki", + "williamhill", + "win", + "windows", + "wine", + "winners", + "wme", + "wolterskluwer", + "woodside", + "work", + "works", + "world", + "wow", + "ws", + "wtc", + "wtf", + "xbox", + "xerox", + "xfinity", + "xihuan", + "xin", + "xn--11b4c3d", + "xn--1ck2e1b", + "xn--1qqw23a", + "xn--2scrj9c", + "xn--30rr7y", + "xn--3bst00m", + "xn--3ds443g", + "xn--3e0b707e", + "xn--3hcrj9c", + "xn--3pxu8k", + "xn--42c2d9a", + "xn--45br5cyl", + "xn--45brj9c", + "xn--45q11c", + "xn--4dbrk0ce", + "xn--4gbrim", + "xn--54b7fta0cc", + "xn--55qw42g", + "xn--55qx5d", + "xn--5su34j936bgsg", + "xn--5tzm5g", + "xn--6frz82g", + "xn--6qq986b3xl", + "xn--80adxhks", + "xn--80ao21a", + "xn--80aqecdr1a", + "xn--80asehdb", + "xn--80aswg", + "xn--8y0a063a", + "xn--90a3ac", + "xn--90ae", + "xn--90ais", + "xn--9dbq2a", + "xn--9et52u", + "xn--9krt00a", + "xn--b4w605ferd", + "xn--bck1b9a5dre4c", + "xn--c1avg", + "xn--c2br7g", + "xn--cck2b3b", + "xn--cckwcxetd", + "xn--cg4bki", + "xn--clchc0ea0b2g2a9gcd", + "xn--czr694b", + "xn--czrs0t", + "xn--czru2d", + "xn--d1acj3b", + "xn--d1alf", + "xn--e1a4c", + "xn--eckvdtc9d", + "xn--efvy88h", + "xn--fct429k", + "xn--fhbei", + "xn--fiq228c5hs", + "xn--fiq64b", + "xn--fiqs8s", + "xn--fiqz9s", + "xn--fjq720a", + "xn--flw351e", + "xn--fpcrj9c3d", + "xn--fzc2c9e2c", + "xn--fzys8d69uvgm", + "xn--g2xx48c", + "xn--gckr3f0f", + "xn--gecrj9c", + "xn--gk3at1e", + "xn--h2breg3eve", + "xn--h2brj9c", + "xn--h2brj9c8c", + "xn--hxt814e", + "xn--i1b6b1a6a2e", + "xn--imr513n", + "xn--io0a7i", + "xn--j1aef", + "xn--j1amh", + "xn--j6w193g", + "xn--jlq480n2rg", + "xn--jvr189m", + "xn--kcrx77d1x4a", + "xn--kprw13d", + "xn--kpry57d", + "xn--kput3i", + "xn--l1acc", + "xn--lgbbat1ad8j", + "xn--mgb9awbf", + "xn--mgba3a3ejt", + "xn--mgba3a4f16a", + "xn--mgba7c0bbn0a", + "xn--mgbaakc7dvf", + "xn--mgbaam7a8h", + "xn--mgbab2bd", + "xn--mgbah1a3hjkrd", + "xn--mgbai9azgqp6j", + "xn--mgbayh7gpa", + "xn--mgbbh1a", + "xn--mgbbh1a71e", + "xn--mgbc0a9azcg", + "xn--mgbca7dzdo", + "xn--mgbcpq6gpa1a", + "xn--mgberp4a5d4ar", + "xn--mgbgu82a", + "xn--mgbi4ecexp", + "xn--mgbpl2fh", + "xn--mgbt3dhd", + "xn--mgbtx2b", + "xn--mgbx4cd0ab", + "xn--mix891f", + "xn--mk1bu44c", + "xn--mxtq1m", + "xn--ngbc5azd", + "xn--ngbe9e0a", + "xn--ngbrx", + "xn--node", + "xn--nqv7f", + "xn--nqv7fs00ema", + "xn--nyqy26a", + "xn--o3cw4h", + "xn--ogbpf8fl", + "xn--otu796d", + "xn--p1acf", + "xn--p1ai", + "xn--pgbs0dh", + "xn--pssy2u", + "xn--q7ce6a", + "xn--q9jyb4c", + "xn--qcka1pmc", + "xn--qxa6a", + "xn--qxam", + "xn--rhqv96g", + "xn--rovu88b", + "xn--rvc1e0am3e", + "xn--s9brj9c", + "xn--ses554g", + "xn--t60b56a", + "xn--tckwe", + "xn--tiq49xqyj", + "xn--unup4y", + "xn--vermgensberater-ctb", + "xn--vermgensberatung-pwb", + "xn--vhquv", + "xn--vuq861b", + "xn--w4r85el8fhu5dnra", + "xn--w4rs40l", + "xn--wgbh1c", + "xn--wgbl6a", + "xn--xhq521b", + "xn--xkc2al3hye2a", + "xn--xkc2dl3a5ee0h", + "xn--y9a3aq", + "xn--yfro4i67o", + "xn--ygbi2ammx", + "xn--zfr164b", + "xxx", + "xyz", + "yachts", + "yahoo", + "yamaxun", + "yandex", + "ye", + "yodobashi", + "yoga", + "yokohama", + "you", + "youtube", + "yt", + "yun", + "za", + "zappos", + "zara", + "zero", + "zip", + "zm", + "zone", + "zuerich", + "zw" +] \ No newline at end of file diff --git a/src/pages/Common/SignUp/signUpContext.jsx b/src/pages/Common/SignUp/signUpContext.jsx new file mode 100644 index 0000000..dbad0bd --- /dev/null +++ b/src/pages/Common/SignUp/signUpContext.jsx @@ -0,0 +1,35 @@ +import React, { createContext, useContext, useReducer } from "react"; + +const initialSignUpData = { + email: "", + firstName: "", + lastName: "", + dob: "", + password: "", + role: "", +}; + +const reducer = (state, action) => { + switch (action.type) { + case "SET_EMAIL": + return { ...state, email: action.payload }; + case "SET_ROLE": + return { ...state, role: action.payload }; + default: + return state; + } +}; + +// create context here +const signUpContext = createContext({}); + +// wrap this component around App.tsx to get access to userData in all components +const SignUpContextProvider = ({ children }) => { + const [signUpData, dispatch] = useReducer(reducer, initialSignUpData); + + return {children}; +}; + +// use this custom hook to get the data in any component in component tree +const useSignUpContext = () => useContext(signUpContext); +export { useSignUpContext, SignUpContextProvider }; diff --git a/src/pages/Common/TermsAndConditionsPage.jsx b/src/pages/Common/TermsAndConditionsPage.jsx new file mode 100644 index 0000000..fed2b85 --- /dev/null +++ b/src/pages/Common/TermsAndConditionsPage.jsx @@ -0,0 +1,48 @@ +import { GlobalContext } from "@/globalContext"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import MkdSDK from "@/utils/MkdSDK"; +import React, { useState } from "react"; +import { useContext } from "react"; +import { useEffect } from "react"; + +export default function TermsAndConditionsPage() { + const [content, setContent] = useState(""); + const { dispatch: globalDispatch } = useContext(GlobalContext); + + async function fetchTermsAndConditions() { + globalDispatch({ type: "START_LOADING" }); + const sdk = new MkdSDK(); + sdk.setTable("cms"); + try { + const result = await callCustomAPI("cms", "post", { where: [`content_key = 'terms_and_conditions'`], limit: 1, page: 1 }, "PAGINATE"); + + if (Array.isArray(result.list) && result.list.length > 0) { + setContent(result.list[0].content_value); + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Cannot get Cancellation policy", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + useEffect(() => { + fetchTermsAndConditions(); + }, []); + + return ( +
    +
    +
    +
    +
    + ); +} diff --git a/src/pages/Customer/Billings/CustomerBillingsPage.jsx b/src/pages/Customer/Billings/CustomerBillingsPage.jsx new file mode 100644 index 0000000..836d18a --- /dev/null +++ b/src/pages/Customer/Billings/CustomerBillingsPage.jsx @@ -0,0 +1,136 @@ +import React, { Fragment, useState } from "react"; +import { Menu, Transition } from "@headlessui/react"; +import { useCards } from "@/hooks/api"; +import { ExclamationCircleIcon, TrashIcon } from "@heroicons/react/24/outline"; +import AddCardMethodModal from "@/components/Billing/AddCardMethodModal"; +import DeleteCardMethodModal from "@/components/Billing/DeleteCardMethodModal"; +import { EllipsisVerticalIcon } from "@heroicons/react/24/solid"; + +const cardIcons = { + MasterCard: "/mastercard.jpg", + Visa: "/visa.jpg", + "American Express": "/american-express.png", + Discover: "/discover.png", +}; + +export default function CustomerBillingsPage() { + const [addMethodPopup, setAddMethodPopup] = useState(false); + + const [deleteMethodPopup, setDeleteMethodPopup] = useState(false); + const [selectedCard, setSelectedCard] = useState({}); + const { cards, changeDefaultCard, fetchCards } = useCards({ loader: true, onCardDelete: () => setDeleteMethodPopup(false) }); + + return ( + <> +
    +
    +
    +

    Payment Method

    + +
    + {cards.map((card) => ( +
    +
    + +
    +
    +
    + +
    +
    +

    Credit card

    + + Expires: {card.exp_month}/{card.exp_year} + +
    +
  • {card.last4}
  • + +
    + + + +
    + + +
    + + + +
    +
    +
    +
    +
    +
    + ))} +
    + {cards.length == 0 && ( +
    +

    + No cards yet +

    +
    + )} +
    + setAddMethodPopup(false)} + onSuccess={() => fetchCards()} + /> + { + setSelectedCard({}); + setDeleteMethodPopup(false); + }} + onSuccess={() => fetchCards()} + card={selectedCard} + /> + + ); +} diff --git a/src/pages/Customer/Bookings/CustomerBookingCard.jsx b/src/pages/Customer/Bookings/CustomerBookingCard.jsx new file mode 100644 index 0000000..dfa4cb2 --- /dev/null +++ b/src/pages/Customer/Bookings/CustomerBookingCard.jsx @@ -0,0 +1,253 @@ +import React, { useEffect, useState } from "react"; +import Skeleton from "react-loading-skeleton"; +import { Link } from "react-router-dom"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import MkdSDK from "@/utils/MkdSDK"; +import { parseJsonSafely, secondsToHour } from "@/utils/utils"; +import { formatDiff, monthsMapping } from "@/utils/date-time-utils"; +import { useContext } from "react"; +import { GlobalContext } from "@/globalContext"; +import { BOOKING_STATUS } from "@/utils/constants"; +import moment from "moment"; +import { FavoriteButton } from "@/components/frontend"; +import { ClockIcon } from "@heroicons/react/24/outline"; + +let sdk = new MkdSDK(); + +export default function CustomerBookingCard({ data, forceRender, favoriteId }) { + const statusMapping = ["Pending", "Upcoming", "Ongoing", "Completed", "Declined", "Canceled", "Expired"]; + const statusColorMapping = ["text-white", "my-text-gradient", "text-[yellow]", "text-[#667085]", "text-[#D92D20]", "text-[#DC6803]", "text-[#D92D20] !bg-[#F2F4F7]"]; + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const [imageLoaded, setImageLoaded] = useState(false); + const bookingExpired = data.booking_start_time && data.status < 2 ? new Date(data.booking_start_time) < Date.now() : false; + const [countdown, setCountdown] = useState({}); + + async function cancelBooking(id) { + const payload = { + id, + booked_unit: 1, + status: BOOKING_STATUS.CANCELLED, + }; + try { + await callCustomAPI("booking", "post", payload, "PUT"); + if (data.status === BOOKING_STATUS.UPCOMING) { + await sdk.callRawAPI("/v2/api/custom/ergo/refund", { booking_id: data.id, stripe_payment_intent_id: data.stripe_payment_intent_id }, "POST"); + } else { + await sdk.callRawAPI("/v2/api/custom/ergo/capture", { booking_id: data.id, status: 5, stripe_payment_intent_id: data.stripe_payment_intent_id }, "POST"); + } + sendCancelEmail(data.host_id, data.property_name, `from ${moment(data.booking_start_time).format("MM/DD/YYYY")} to ${moment(data.booking_end_time).format("MM/DD/YYYY")}`); + if (forceRender) { + forceRender(new Date()); + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function sendCancelEmail(id, space_name, time) { + try { + // get user email and preferences + const result = await callCustomAPI("get-user", "post", { id }, ""); + const tmpl = await sdk.getEmailTemplate("booking-cancelled"); + + if (parseJsonSafely(result.settings, {}).email_on_booking_cancelled == true) { + const body = tmpl.html?.replace(new RegExp("{{{space_name}}}", "g"), space_name).replace(new RegExp("{{{time}}}", "g"), time); + await sdk.sendEmail(result.email, tmpl.subject, body); + } + + if (parseJsonSafely(globalState.user.settings, {}).email_on_booking_cancelled == true) { + const body = tmpl.html?.replace(new RegExp("{{{space_name}}}", "g"), space_name).replace(new RegExp("{{{time}}}", "g"), time); + await sdk.sendEmail(globalState.user.email, tmpl.subject, body); + } + } catch (err) { } + } + + useEffect(() => { + if (data.status != BOOKING_STATUS.UPCOMING && !bookingExpired) return; + let interval = null; + if (!data.booking_start_time) { + return () => clearInterval(interval); + } + interval = setInterval(() => { + const diff = formatDiff(data.booking_start_time); + setCountdown(diff); + }, 1000); + }, [data.booking_start_time]); + + return ( + <> +
    +
    + setImageLoaded(true)} + alt="" + className="absolute top-0 left-0 h-full w-full object-cover" + /> + {data.status == BOOKING_STATUS.UPCOMING && !bookingExpired && ( +
    +
    + {countdown.timeLeft} + {countdown.format} +
    +
    + )} + +
    + +
    + {!imageLoaded && } +
    +
    +
    +

    {data.property_name || }

    +

    {!data.property_name ? : ([1, 2].includes(data.status) && !bookingExpired ? data.address_line_1 : data.property_city) ?? "N/A"}

    +

    + {" "} + {!data.property_name ? : ([1, 2].includes(data.status) && !bookingExpired ? data.address_line_2 : data.property_country) ?? "N/A"} +

    + {data.property_name ? ( +

    + Host: {data.host_first_name} {data.host_last_name} +

    + ) : ( + + )} +
    +
    + {data.booking_start_time ? ( +
    +

    Date

    + + {monthsMapping[new Date(data.booking_start_time).getMonth()] + " " + new Date(data.booking_start_time).getDate() + "/" + new Date(data.booking_start_time).getFullYear()} + +
    + ) : ( + + )} + {data.duration ? ( +
    +

    Duration

    + {secondsToHour(data.duration)} +
    + ) : ( + + )} + {data.duration ? ( +
    +

    Total Price

    + ${((data?.total ?? 0) + (data?.addon_cost ?? 0)).toFixed(2)} + {/* ${((data?.total ?? 0) + (data?.addon_cost ?? 0)).toFixed(2)} */} +
    + ) : ( + + )} +
    +
    + + {" "} + {statusMapping[bookingExpired ? 6 : data.status ?? 0]} + + {data.id && ( + + View details + + )} + + {!bookingExpired && ( + + )} +
    +
    +
    +
    +
    +
    + setImageLoaded(true)} + alt="" + className="absolute top-0 left-0 h-full w-full object-cover" + /> + {!imageLoaded && } + {data.status == BOOKING_STATUS.UPCOMING && !bookingExpired && ( +
    +
    + {countdown.timeLeft} + {countdown.format} +
    +
    + )} +
    +
    +

    {data.property_name || }

    +

    + {" "} + {!data.property_name ? : ([1, 2].includes(data.status) && !bookingExpired ? data.address_line_1 : data.property_city) ?? "N/A"} +

    +

    + {" "} + {!data.property_name ? : ([1, 2].includes(data.status) && !bookingExpired ? data.address_line_2 : data.property_country) ?? "N/A"} +

    + {data.property_name ? ( +

    + Host: {data.host_first_name} {data.host_last_name} +

    + ) : ( + + )} +
    +
    +
    + + {" "} + {statusMapping[bookingExpired ? 6 : data.status ?? 0]} + + {data.id && ( + + View details + + )} + + {!bookingExpired && ( + + )} +
    +
    + + ); +} diff --git a/src/pages/Customer/Bookings/CustomerBookingDetailsPage.jsx b/src/pages/Customer/Bookings/CustomerBookingDetailsPage.jsx new file mode 100644 index 0000000..48eea94 --- /dev/null +++ b/src/pages/Customer/Bookings/CustomerBookingDetailsPage.jsx @@ -0,0 +1,779 @@ +import React from "react"; +import { useEffect } from "react"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { Link, useNavigate, useParams } from "react-router-dom"; +import CircleCheckIcon from "@/components/frontend/icons/CircleCheckIcon"; +import DateTimeIcon from "@/components/frontend/icons/DateTimeIcon"; +import PersonIcon from "@/components/frontend/icons/PersonIcon"; +import StarIcon from "@/components/frontend/icons/StarIcon"; +import ThreeDotsMenu from "@/components/frontend/ThreeDotsMenu"; +import Icon from "@/components/Icons"; +import useDelayUnmount from "@/hooks/useDelayUnmount"; +import MkdSDK from "@/utils/MkdSDK"; +import { useContext } from "react"; +import { GlobalContext, showToast } from "@/globalContext"; +import { daysMapping, monthsMapping, formatAMPM } from "@/utils/date-time-utils"; +import NoteIcon from "@/components/frontend/icons/NoteIcon"; +import FavoriteButton from "@/components/frontend/FavoriteButton"; +import { BOOKING_STATUS, NOTIFICATION_STATUS, NOTIFICATION_TYPE, PAYMENT_STATUS } from "@/utils/constants"; +import { LoadingButton } from "@/components/frontend"; +import { useCards, usePropertySpace, usePublicUserData } from "@/hooks/api"; +import useUserCurrentLocation from "@/hooks/api/useUserCurrentLocation"; +import PropertySpaceMapImage from "@/components/frontend/PropertySpaceMapImage"; +import { parseJsonSafely } from "@/utils/utils"; +import moment from "moment"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import PayBookingModal from "./PayBookingModal"; +import { Elements, PaymentRequestButtonElement, useStripe } from "@stripe/react-stripe-js"; +import { loadStripe } from "@stripe/stripe-js"; +import SelectExistingCardsModal from "./SelectExistingCardsModal"; + +const statusMapping = ["Pending", "Upcoming", "Ongoing", "Completed", "Declined", "Canceled", "Expired"]; +const statusColorMapping = ["text-white", "my-text-gradient", "text-[#667085]", "text-[#667085]", "text-[#D92D20]", "text-[#DC6803]", "text-[#D92D20] !bg-[#F2F4F7]"]; + +let sdk = new MkdSDK(); +let ctrl = new AbortController(); + +export default function CustomerBookingDetailsPage() { + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const { dispatch, state: authState } = useContext(AuthContext); + + const navigate = useNavigate(); + const { id } = useParams(); + const [booking, setBooking] = useState({}); + const [paymentMethod, setPaymentMethod] = useState(); + const [confirmPayment, setConfirmPayment] = useState(false); + const [addReviewPopup, setAddReviewPopup] = useState(false); + const showAddReviewPopup = useDelayUnmount(addReviewPopup, 100); + const [checkedCount, setCheckedCount] = useState(0); + const [availableHashtags, setAvailableHashtags] = useState([{ name: "Test", id: 12 }]); + const [render, forceRender] = useState(false); + const [loading, setLoading] = useState(false); + const [showMap, setShowMap] = useState(false); + const [newCardPaymentModal, setNewCardPaymentModal] = useState(false); + const [clientSecret, setClientSecret] = useState(undefined); + const [paymentOptions, setPaymentOptions] = useState(false); + const [existingCardsModal, setExistingCardsModal] = useState(false); + const { cards } = useCards({ loader: false }); + + const stripePromise = loadStripe(import.meta.env.VITE_REACT_STRIPE_PUBLIC_KEY); + + const bookingExpired = booking.booking_start_time && booking.status < BOOKING_STATUS.ONGOING ? new Date(booking.booking_start_time) < Date.now() : false; + + const { register, handleSubmit, watch, reset } = useForm(); + const ratingVal = watch("rating"); + const hostRatingVal = watch("host_rating"); + const hashtags = watch("hashtags", []); + + const [declinePopup, setDeclinePopup] = useState(false); + const showDeclinePopup = useDelayUnmount(declinePopup, 300); + const [reason, setReason] = useState(""); + + useEffect(() => { + if (Array.isArray(hashtags)) { + setCheckedCount(hashtags?.filter(Boolean).length); + } + }, [hashtags]); + + const otherUserData = usePublicUserData(booking.host_id); + const { propertySpace } = usePropertySpace(booking.property_space_id, render); + + async function addHashTagToReview(hashtags, reviewId) { + try { + sdk.setTable("review_hashtag"); + hashtags.map((hashtag) => + sdk.callRestAPI( + { + hashtag_id: hashtag, + review_id: reviewId, + }, + "POST", + ), + ); + await Promise.all(hashtags); + } catch (error) { + console.log("Error", error); + } + } + + const onSubmit = async (data) => { + console.log("submitting", data); + setLoading(true); + let newReview = { + customer_id: booking.customer_id, + host_id: booking.host_id, + property_spaces_id: booking.property_space_id, + property_name: booking.property_name, + booking_id: booking.id, + comment: data.comment, + customer_rating: null, + host_rating: data.host_rating, + space_rating: data.rating, + post_date: new Date().toISOString(), + given_by: "customer", + received_by: "host", + }; + try { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/review/POST", newReview, "POST", ctrl.signal); + await addHashTagToReview(data.hashtags, result.message); + + // create notification + sdk.setTable("notification"); + await sdk.callRestAPI( + { + user_id: Number(localStorage.getItem("user")), + actor_id: null, + action_id: result.message, + notification_time: new Date().toISOString().split(".")[0], + message: "New Review Added", + type: NOTIFICATION_TYPE.ADD_REVIEW, + status: NOTIFICATION_STATUS.NOT_ADDRESSED, + }, + "POST", + ); + setLoading(false); + + setAddReviewPopup(false); + + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Success", + message: "Review added successful", + btn: "Ok got it", + }, + }); + reset(); + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") { + setLoading(false); + return; + } + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + setLoading(false); + }; + + async function fetchBooking(booking_id) { + globalDispatch({ type: "START_LOADING" }); + const where = [`ergo_booking.id = ${booking_id} AND ergo_booking.deleted_at IS NULL`]; + try { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/booking/details", { where }, "POST", ctrl.signal); + setBooking(result.list ?? {}); + + return result.list ?? {} + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") { + globalDispatch({ type: "STOP_LOADING" }); + return; + } + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + finally { + globalDispatch({ type: "STOP_LOADING" }); + } + } + + async function fetchHashtags() { + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/hashtag/PAGINATE", + { + page: 1, + limit: 1000, + where: [`ergo_hashtag.deleted_at IS NULL`], + }, + "POST", + ctrl.signal, + ); + if (Array.isArray(result.list)) { + setAvailableHashtags(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function cancelBooking(id) { + const payload = { + id, + booked_unit: 1, + status: BOOKING_STATUS.CANCELLED, + }; + try { + await sdk.callRawAPI("/v2/api/custom/ergo/booking/PUT", payload, "POST", ctrl.signal); + if (booking.status === BOOKING_STATUS.UPCOMING) { + await sdk.callRawAPI("/v2/api/custom/ergo/refund", { booking_id: booking.id, stripe_payment_intent_id: booking.stripe_payment_intent_id }, "POST"); + } else { + await sdk.callRawAPI("/v2/api/custom/ergo/capture", { booking_id: booking.id, status: 5, stripe_payment_intent_id: booking.stripe_payment_intent_id }, "POST"); + } + sendCancelEmail(booking.host_id, booking.property_name, `from ${moment(booking.booking_start_time).format("MM/DD/YYYY")} to ${moment(booking.booking_end_time).format("MM/DD/YYYY")}`, reason); + fetchBooking(id); + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + async function deleteBooking(id) { + const payload = { + id, + }; + try { + sdk.setTable("booking") + const result = await sdk.callRestAPI(payload, "DELETE", ctrl.signal); + showToast(globalDispatch, result.message, 5000) + navigate("/account/my-bookings") + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function sendCancelEmail(id, space_name, time) { + try { + // get user email and preferences + const result = await sdk.callRawAPI("/v2/api/custom/ergo/get-user", { id }, "POST", ctrl.signal); + const tmpl = await sdk.getEmailTemplate("booking-cancelled"); + + if (parseJsonSafely(result.settings, {}).email_on_booking_cancelled == true) { + const body = tmpl.html?.replace(new RegExp("{{{space_name}}}", "g"), space_name).replace(new RegExp("{{{time}}}", "g"), time); + await sdk.sendEmail(result.email, tmpl.subject, body); + } + + if (parseJsonSafely(globalState.user.settings, {}).email_on_booking_cancelled == true) { + const body = tmpl.html?.replace(new RegExp("{{{space_name}}}", "g"), space_name).replace(new RegExp("{{{time}}}", "g"), time); + await sdk.sendEmail(globalState.user.email, tmpl.subject, body); + } + } catch (err) { } + } + + async function remindHost() { + try { + const tmpl = await sdk.getEmailTemplate("pending-booking-reminder"); + let time = `from ${moment(booking.booking_start_time).format("MM/DD/YYYY")} to ${moment(booking.booking_end_time).format("MM/DD/YYYY")}`; + + const body = tmpl.html + ?.replace(new RegExp("{{{space_name}}}", "g"), booking.property_name) + .replace(new RegExp("{{{time}}}", "g"), time) + .replace(new RegExp("{{{booking_id}}}", "g"), booking.id) + .replace(new RegExp("{{{customer_name}}}", "g"), `${booking.customer_first_name} ${booking.customer_last_name}`); + await sdk.sendEmail(booking.host_email, tmpl.subject, body); + + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Email sent", + message: "Email sent to host", + btn: "Ok got it", + }, + }); + } catch (err) { + tokenExpireError(dispatch, err.message); + } + } + + useEffect(() => { + fetchBooking(id) + }, []) + + useEffect(() => { + (async () => { + fetchHashtags() + })(); + }, []) + + const { latitude, longitude, done } = useUserCurrentLocation(); + + return ( +
    +
    + +
    +
    +

    Booking - {booking.property_name}

    +
    +

    Status

    + + {" "} + {statusMapping[bookingExpired ? 6 : booking.status ?? 0]} + +
    +
    + , + onClick: () => deleteBooking(booking.id), + notShow: booking.status == BOOKING_STATUS.ONGOING || booking.status == BOOKING_STATUS.UPCOMING, + }, + { + label: "Cancel booking", + icon: <>, + onClick: () => cancelBooking(booking.id), + notShow: booking.status == BOOKING_STATUS.COMPLETED || booking.status == BOOKING_STATUS.ONGOING || bookingExpired || booking.status == BOOKING_STATUS.CANCELLED, + }, + { + label: "Edit booking", + icon: <>, + onClick: () => navigate("/account/my-bookings/edit/" + booking.id), + notShow: booking.status > BOOKING_STATUS.UPCOMING || bookingExpired, + }, + { + label: "Add a review", + icon: <>, + onClick: () => setAddReviewPopup(true), + notShow: booking.status != BOOKING_STATUS.COMPLETED || bookingExpired, + }, + ]} + /> +
    +
    +
    +
    +
    +
    + {(!bookingExpired && booking.status != BOOKING_STATUS.CANCELLED) && +
    +
    + +
    +

    What's next

    +

    + You booking has been sent to the host and will be reviewed within 2 hours. If you don’t hear form the host withing 2h you can cancel your booking or reach out to them via{" "} + + Messages + {" "} +

    +
    +
    +
    + } +
  • Booking Time
  • +
    +
    +

    {monthsMapping[new Date(booking.booking_start_time).getMonth()] ?? "N/A"}

    + {new Date(booking.booking_start_time).getDate() || "N/A"} +

    {daysMapping[new Date(booking.booking_start_time).getDay()] ?? "N/A"}

    +
    +
    +
    + +

    From

    + {formatAMPM(booking.booking_start_time ?? "01/01/01")}{" "} +
    +
    + +

    Until

    + {formatAMPM(booking.booking_end_time ?? "01/01/01")} +
    +
    +
    +
    +
  • Space:
  • + {booking.status == BOOKING_STATUS.COMPLETED ? ( + + ) : null} +
    +
    +
    + +
    +
    +
    +

    {booking.property_name}

    +

    + {[BOOKING_STATUS.UPCOMING, BOOKING_STATUS.ONGOING].includes(booking.status) ? propertySpace.address_line_1 : propertySpace.city} +

    +

    + {[BOOKING_STATUS.UPCOMING, BOOKING_STATUS.ONGOING].includes(booking.status) ? propertySpace.address_line_2 : propertySpace.country} +

    +
    +

    + from: ${(Number(booking.hourly_rate) || 0).toFixed(2)}/day +

    +
    + + {propertySpace.max_capacity} +
    +
    +
    +
    +

    + + + {(Number(propertySpace.average_space_rating) || 0).toFixed(1)} + ({propertySpace.space_rating_count}) + +

    + {[BOOKING_STATUS.COMPLETED, BOOKING_STATUS.ONGOING, BOOKING_STATUS.UPCOMING].includes(booking.status) ? ( + + (view on map) + + ) : ( + + )} +
    +
    +
    + {booking.add_ons !== undefined && booking?.add_ons?.length > 0 && +
  • Add-ons:
  • + } +
    + {(booking.add_ons ?? []).map((addon, idx) => ( +
    + +

    {addon.name}

    +
    + ))} +
    +
    +
    +
    +

    Your Host

    +
    +
    + +

    {(booking.host_first_name ?? "") + " " + (booking.host_last_name ?? "")}

    +
    + + Chat with Host + +
    + + {booking.status == BOOKING_STATUS.PENDING && ( +
    +

    + {" "} + host via email to review your booking +

    +
    + )} +
    +
    +
    +

    Charges

    + {booking.payment_status === PAYMENT_STATUS.SUCCESSFUL && + Paid + } +
    +

    (funds will be put on hold, pending when host accepts/rejects your booking)

    +
    +

    Rate

    +

    ${(booking.hourly_rate || 0).toFixed(2)}

    +
    +
    +

    Price

    +

    ${(booking.hourly_rate * (booking.duration / 3600) || 0).toFixed(2)}

    +
    + {(booking.add_ons ?? []).map((addon) => ( +
    +

    {addon.name}

    +

    ${(addon.cost || 0).toFixed(2)}

    +
    + ))} + +
    +

    Tax

    +

    ${(booking.tax || 0).toFixed(2)}

    +
    +
    +

    Total

    +

    {booking.payment_status == PAYMENT_STATUS.SUCCESSFUL && (Paid)} ${(booking.total || 0).toFixed(2)}

    +
    +
    + + Cancellation policy + +
    +
    + {showAddReviewPopup && ( +
    setAddReviewPopup(false)} + > +
    e.stopPropagation()} + onSubmit={handleSubmit(onSubmit)} + > +
    +

    Leave a review

    + +
    +

    You have completed your booking! Leave a review to let others know about your experience with the space and the host.

    +
    +

    Select rating

    +
    + {[1, 2, 3, 4, 5].map((val) => ( + + ))} +
    + <> +

    Host rating

    +
    + {[1, 2, 3, 4, 5].map((val) => ( + + ))} +
    + + +

    + Select Hashtags (max 3) +

    +
    + {availableHashtags.map((hash, i) => ( +
    + = 3 && !hashtags.includes(hash.id.toString())} + value={hash.id} + /> + +
    + ))} +
    +

    Comment

    + + + Submit + +
    +
    +
    + )} + setShowMap(false)} + /> + {showDeclinePopup && ( +
    setDeclinePopup(false)} + > +
    e.stopPropagation()} + > +
    +

    Are you sure?

    + +
    +
    +

    + You are about to decline a booking from{" "} + + {booking.customer_first_name} {booking.customer_last_name} + {" "} + for {booking.property_name} on{" "} + {monthsMapping[new Date(booking.booking_start_time).getMonth()] + " " + new Date(booking.booking_start_time).getDate() + "/" + new Date(booking.booking_start_time).getFullYear()}. +

    +

    Reason

    + +
    + + +
    +
    +
    + )} +
    + ); +} diff --git a/src/pages/Customer/Bookings/CustomerBookingFiltersModal.jsx b/src/pages/Customer/Bookings/CustomerBookingFiltersModal.jsx new file mode 100644 index 0000000..6cf0f5c --- /dev/null +++ b/src/pages/Customer/Bookings/CustomerBookingFiltersModal.jsx @@ -0,0 +1,198 @@ +import DatePickerV3 from "@/components/DatePickerV3"; +import { isValidDate, parseSearchParams } from "@/utils/utils"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment } from "react"; +import { useForm } from "react-hook-form"; +import { useSearchParams } from "react-router-dom"; + +const statuses = [ + { label: "Pending", value: 0 }, + { label: "Upcoming", value: 1 }, + { label: "Ongoing", value: 2 }, + { label: "Completed", value: 3 }, + { label: "Declined", value: 4 }, + { label: "Expired", value: "expired" }, +]; + +export default function CustomerBookingFiltersModal({ modalOpen, closeModal }) { + const [searchParams, setSearchParams] = useSearchParams(); + const { handleSubmit, register, watch, reset, setValue, control, formState, resetField } = useForm({ + defaultValues: (() => { + const params = parseSearchParams(searchParams); + return { + host_name: params.host_name ?? "", + from: isValidDate(params.from ?? "") ? new Date(params.from) : new Date(), + to: isValidDate(params.to ?? "") ? new Date(params.to) : new Date(), + space_name: params.space_name ?? "", + status: params.status ?? "", + id: params.id ?? "", + direction: "DESC", + }; + })(), + }); + + const { dirtyFields } = formState; + + const fromDate = watch("from"); + + const onSubmit = async (data) => { + console.log("submitting ", data); + searchParams.set("id", data.id); + searchParams.set("host_name", data.host_name); + searchParams.set("space_name", data.space_name); + searchParams.set("status", data.status); + searchParams.set("from", dirtyFields?.from ? data.from.toISOString().split("T")[0] : ""); + searchParams.set("to", dirtyFields?.to ? data.to.toISOString().split("T")[0] : ""); + setSearchParams(searchParams); + closeModal(); + }; + + return ( + + + +
    + + +
    +
    + + +
    +
    + + Filters + + +
    + {" "} +
    +
    +
    + +
    +
    + resetField("from", { keepDirty: false, keepTouched: false })} + setValue={(val) => setValue("from", val, { shouldDirty: true })} + control={control} + name="from" + labelClassName="justify-between flex-grow flex-row-reverse" + placeholder="From" + min={new Date("2001-01-01")} + /> +
    +
    + resetField("to", { keepDirty: false, keepTouched: false })} + setValue={(val) => setValue("to", val, { shouldDirty: true })} + control={control} + name="to" + labelClassName="justify-between flex-grow flex-row-reverse" + placeholder="To" + min={fromDate} + /> +
    +
    + + + +
    + +
    +
    +
    +
    +
    +
    + ); +} diff --git a/src/pages/Customer/Bookings/CustomerBookingListPage.jsx b/src/pages/Customer/Bookings/CustomerBookingListPage.jsx new file mode 100644 index 0000000..60b2ea6 --- /dev/null +++ b/src/pages/Customer/Bookings/CustomerBookingListPage.jsx @@ -0,0 +1,308 @@ +import React, { useContext, useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { GlobalContext } from "@/globalContext"; +import { useSearchParams } from "react-router-dom"; +import InfiniteScroll from "react-infinite-scroll-component"; +import NoteIcon from "@/components/frontend/icons/NoteIcon"; +import { isValidDate, parseSearchParams } from "@/utils/utils"; +import MkdSDK from "@/utils/MkdSDK"; +import CustomerBookingCard from "./CustomerBookingCard"; +import { AdjustmentsHorizontalIcon } from "@heroicons/react/24/solid"; +import CustomSelectV2 from "@/components/CustomSelectV2"; +import DatePickerV3 from "@/components/DatePickerV3"; +import CustomerBookingFiltersModal from "./CustomerBookingFiltersModal"; +import { AuthContext, tokenExpireError } from "@/authContext"; + +const sdk = new MkdSDK(); +const ctrl = new AbortController(); + +export default function CustomerBookingListPage() { + const FETCH_PER_SCROLL = 12; + + const [showFilter, setShowFilter] = useState(false); + const [bookings, setBookings] = useState([]); + const [render, forceRender] = useState(false); + const [searchParams, setSearchParams] = useSearchParams(); + + const [bookingsTotal, setBookingsTotal] = useState(100); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const { dispatch } = useContext(AuthContext); + const [favoriteStatuses, setFavoriteStatuses] = useState([]); + const user_id = +localStorage.getItem("user") || 0; + + const { handleSubmit, register, watch, reset, setValue, control, formState, resetField } = useForm({ + defaultValues: (() => { + const params = parseSearchParams(searchParams); + return { + host_name: params.host_name ?? "", + from: isValidDate(params.from ?? "") ? new Date(params.from) : new Date(), + to: isValidDate(params.to ?? "") ? new Date(params.to) : new Date(), + space_name: params.space_name ?? "", + status: params.status ?? "", + id: params.id ?? "", + direction: "DESC", + }; + })(), + }); + + const { dirtyFields } = formState; + + const direction = watch("direction"); + const fromDate = watch("from"); + + const onSubmit = async (data) => { + if (window.innerWidth < 700) { + setShowFilter(false); + } + console.log("submitting", data); + setBookings([]); + searchParams.set("id", data.id); + searchParams.set("host_name", data.host_name); + searchParams.set("status", data.status); + searchParams.set("from", dirtyFields?.from ? data.from.toISOString().split("T")[0] : ""); + searchParams.set("to", dirtyFields?.to ? data.to.toISOString().split("T")[0] : ""); + setSearchParams(searchParams); + }; + + async function fetchMyBookings(page) { + // only add empty spaces if there's no empty card i.e we are not currently fetching + if (bookings.every((bk) => Object.keys(bk).length > 0)) { + setBookings((prev) => { + const amountToFetch = bookingsTotal - prev.length > FETCH_PER_SCROLL ? FETCH_PER_SCROLL : Math.abs(bookingsTotal - prev.length - FETCH_PER_SCROLL); + return [...prev, ...Array(amountToFetch).fill({})]; + }); + } + const filters = parseSearchParams(searchParams); + + var where = [`ergo_booking.customer_id = ${user_id} AND ergo_booking.deleted_at IS NULL`]; + + if (filters.host_name) { + where.push(`(ergo_user.first_name LIKE '%${filters.host_name}%' OR ergo_user.last_name LIKE '%${filters.host_name}%'`); + } + + if (filters.from) { + where.push(`ergo_booking.booking_start_time >= date('${filters.from}')`); + } + + if (filters.to) { + where.push(`ergo_booking.booking_end_time <= date('${filters.to}')`); + } + + if (filters.space_name) { + where.push(`ergo_property.name LIKE '%${filters.space_name}%'`); + } + + if (filters.status) { + if (filters.status == "expired") { + where.push(`ergo_booking.booking_start_time < date('${new Date().toISOString()}')`); + } else { + where.push(`ergo_booking.status = ${filters.status} AND ergo_booking.booking_start_time > date('${new Date().toISOString()}')`); + } + } + + if (filters.id) { + where = [`ergo_booking.customer_id = ${user_id} AND ergo_booking.id = ${filters.id}`]; + } + + try { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/booking/PAGINATE", { page: page ?? 1, limit: FETCH_PER_SCROLL, where, sortId: "update_at", direction: "DESC" }, "POST", ctrl.signal); + if (Array.isArray(result.list)) { + setBookings((prev) => { + return [...prev.filter((item) => Object.keys(item).length > 0), ...result.list].filter((v, i, a) => a.findIndex((v2) => v2.id === v.id) === i); + }); + setBookingsTotal(result.total); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function fetchFavoriteStatuses() { + const payload = { user_id: `${user_id}` }; + sdk.setTable("user_property_spaces"); + try { + const result = await sdk.callRestAPI({ payload }, "GETALL"); + if (Array.isArray(result.list)) { + setFavoriteStatuses(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") { + globalDispatch({ type: "STOP_LOADING" }); + return; + } + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + useEffect(() => { + fetchMyBookings(); + }, [searchParams]); + + useEffect(() => { + if (render) { + fetchFavoriteStatuses(); + setBookings([]); + fetchMyBookings(1); + } + }, [render]); + + useEffect(() => { + fetchFavoriteStatuses(); + }, []); + + const sortByDate = (a, b) => { + if (direction == "DESC") { + return new Date(b.update_at) - new Date(a.update_at); + } + return new Date(a.update_at) - new Date(b.update_at); + }; + + return ( +
    +
    +
    +
    + + +
    + +
    +
    + {bookings.length == 0 && ( +
    +

    + You have no bookings +

    +
    + )} + { + console.log("calling next", bookings.length / FETCH_PER_SCROLL + 1); + fetchMyBookings(Math.round(bookings.length / FETCH_PER_SCROLL + 1)); + }} + scrollThreshold={0.9} + hasMore={bookings.length < bookingsTotal} + loader={<>} + endMessage={ + bookings.length > 10 && ( +

    + +

    + ) + } + > + {bookings.sort(sortByDate).map((book, i) => ( + fav.property_spaces_id == book.property_space_id)?.id ?? null} + /> + ))} +
    + setShowFilter(false)} + /> +
    + ); +} diff --git a/src/pages/Customer/Bookings/EditBookingPage.jsx b/src/pages/Customer/Bookings/EditBookingPage.jsx new file mode 100644 index 0000000..56895bc --- /dev/null +++ b/src/pages/Customer/Bookings/EditBookingPage.jsx @@ -0,0 +1,354 @@ +import React from "react"; +import { useState } from "react"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { useNavigate, useParams } from "react-router"; +import DateTimeIcon from "@/components/frontend/icons/DateTimeIcon"; +import Icon from "@/components/Icons"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import { formatAMPM, fullMonthsMapping, getDuration } from "@/utils/date-time-utils"; +import { useContext } from "react"; +import { GlobalContext } from "@/globalContext"; +import FavoriteButton from "@/components/frontend/FavoriteButton"; +import useSchedulingData from "@/hooks/api/useSchedulingData"; +import Counter from "@/components/frontend/Counter"; +import DateTimePicker from "@/components/frontend/DateTimePicker"; +import useTaxAndCommission from "@/hooks/api/useTaxAndCommission"; +import LoadingButton from "@/components/frontend/LoadingButton"; +import AddIcon from "@/components/frontend/icons/AddIcon"; +import usePropertyAddons from "@/hooks/api/usePropertyAddons"; +import AddonCounterV2 from "@/components/frontend/AddonCounterV2"; +import MkdSDK from "@/utils/MkdSDK"; +import moment from "moment"; +import { BOOKING_STATUS } from "@/utils/constants"; +import { AuthContext, tokenExpireError } from "@/authContext"; + +const sdk = new MkdSDK(); +const ctrl = new AbortController(); + +export default function EditBookingPage() { + const { id: booking_id } = useParams(); + const navigate = useNavigate(); + + const { register, watch, handleSubmit, setValue } = useForm({ defaultValues: { selectedAddons: [] } }); + const formValues = watch(); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const { dispatch } = useContext(AuthContext); + const [booking, setBooking] = useState([]); + const [showCalendar, setShowCalendar] = useState(false); + const [loading, setLoading] = useState(false); + + const { tax, commission } = useTaxAndCommission(); + const { bookedSlots, scheduleTemplate } = useSchedulingData({ property_space_id: booking.property_space_id }); + const addons = usePropertyAddons(booking.property_id); + + const [showCharges, setShowCharges] = useState(false); + + async function fetchBooking() { + globalDispatch({ type: "START_LOADING" }); + const where = [`ergo_booking.id = ${booking_id} AND ergo_booking.deleted_at IS NULL`]; + try { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/booking/details", { where }, "POST", ctrl.signal); + setBooking(result.list ?? {}); + setValue("from", formatAMPM(result.list.booking_start_time)); + setValue("to", formatAMPM(result.list.booking_end_time)); + setValue("selectedDate", new Date(result.list.booking_start_time)); + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") { + globalDispatch({ type: "STOP_LOADING" }); + return; + } + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + useEffect(() => { + fetchBooking(); + }, []); + + useEffect(() => { + if (addons.length > 0 && booking.property_space_id) { + setValue( + "selectedAddons", + booking.add_ons.map((addon) => addon.name), + ); + } + }, [addons.length, booking]); + + async function onSubmit(data) { + console.log("submitting ", data); + + setLoading(true); + const dateFormat = moment(data.selectedDate).format("MM/DD/YY"); + const user_id = localStorage.getItem("user"); + try { + await sdk.callRawAPI( + "/v2/api/custom/ergo/booking/PUT", + { + id: booking.id, + booked_unit: 1, + booking_start_time: new Date(dateFormat + " " + data.from).toISOString(), + booking_end_time: new Date(dateFormat + " " + data.to).toISOString(), + commission_rate: Number(commission), + customer_id: Number(user_id), + duration: getDuration(data.from, data.to) * 3600, + host_id: booking.host_id, + payment_status: 0, + property_space_id: Number(booking.property_space_id), + status: 0, + num_guests: data.num_guests - 1, + tax_rate: Number(tax ?? booking?.tax), + }, + "POST", + ctrl.signal, + ); + + // get addons to delete and addons to create + let addons_to_delete = booking.add_ons.filter((addon) => !data.selectedAddons.includes(addon.name)).map((addon) => addon.booking_addons_id); + let addons_to_create = addons + .filter((addon) => data.selectedAddons.includes(addon.add_on_name) && !booking.add_ons.map((addon) => addon.name).includes(addon.add_on_name)) + .map((addon) => addon.id); + + sdk.setTable("booking_addons"); + for (const delete_id of addons_to_delete) { + const deleteResult = await sdk.callRestAPI({ id: delete_id }, "DELETE"); + console.log("deleteResult", deleteResult); + } + + for (const property_add_on_id of addons_to_create) { + await sdk.callRestAPI({ booking_id: booking.id, property_add_on_id }, "POST"); + } + navigate(`/account/my-bookings/${booking.id}`); + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Edit Booking Failed", + message: err.message, + }, + }); + } + + // notify host + if (booking.status == BOOKING_STATUS.UPCOMING) { + const r = await sdk.sendEmail(booking.host_email, "Booking Changed", `The structure for this email will be changed shortly`); + } + } + const bookingStartDate = new Date(formValues.selectedDate); + + if (!booking.id) return null; + + return ( +
    setShowCalendar(false)} + > + +

    Edit Booking

    +
    +
    +
    +
    + +
    +
    +

    {booking.property_name}

    +

    {booking.address_line_1}

    +

    {booking.city + ", " + booking.address_line_2}

    +
    +
    +
    +
    + +

    Date & time

    +
    +
    +
    +

    Date

    +

    + {" "} + {(bookingStartDate instanceof Date ? fullMonthsMapping[bookingStartDate.getMonth()] : "") + " " + bookingStartDate.getDate() + "/" + bookingStartDate.getFullYear()} +

    +
    +
    +

    Time

    +

    + {formValues.from} - {formValues.to} +

    +
    +
    +

    Duration

    +

    {getDuration(formValues.from, formValues.to)} hour(s)

    +
    +
    + +

    Add Ons

    +
    + {addons.map((addon) => { + return ( + + ); + })} +
    + +
    +

    Price and availability

    +
    + Max capacity + + {" "} + {booking.max_capacity} people + +
    +
    + Pricing from + + from: ${booking.rate}/h + +
    + +
    +
    + Number of guests + +
    +
    +
    + ({ fromTime: new Date(slot.start_time), toTime: new Date(slot.end_time) }))} + scheduleTemplate={scheduleTemplate} + defaultDate={bookingStartDate} + /> +
    + +
    +
    +
    +
    +
    +

    Charges

    +
    +
    +
    +

    Rate

    +

    ${booking.rate.toFixed(2)}/h

    +
    +
    +

    Price

    +

    ${(booking.rate * getDuration(formValues.from, formValues.to)).toFixed(2)}

    +
    + {formValues.selectedAddons.map((addon_name, idx) => { + let price = addons.find((addon) => addon.add_on_name == addon_name)?.cost; + if (!price) return null; + return ( +
    +

    {addon_name}

    +

    ${Number(price).toFixed(2)}

    +
    + ); + })} +
    +

    Tax

    +

    ${Number((booking.rate * getDuration(formValues.from, formValues.to) * tax) / 100).toFixed(2)}

    +
    +
    +

    Total

    +

    + {" "} + $ + {( + Number( + addons.reduce((acc, curr) => { + if (!formValues.selectedAddons.includes(curr.add_on_name)) return acc; + return Number(acc) + (Number(curr.cost) ?? 0); + }, 0), + ) + + Number((booking.rate * getDuration(formValues.from, formValues.to) * tax) / 100) + + Number(booking.rate * getDuration(formValues.from, formValues.to)) + ).toFixed(2)} +

    +
    +
    + + Save + +

    (Note: this will affect the rates)

    +
    + + Cancellation Policy + +
    +
    +
    + ); +} diff --git a/src/pages/Customer/Bookings/PayBookingModal.jsx b/src/pages/Customer/Bookings/PayBookingModal.jsx new file mode 100644 index 0000000..d4e1689 --- /dev/null +++ b/src/pages/Customer/Bookings/PayBookingModal.jsx @@ -0,0 +1,129 @@ +import { AuthContext } from "@/authContext"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment, useEffect, useState } from "react"; +import { useContext } from "react"; +import { GlobalContext } from "@/globalContext"; +import { LoadingButton } from "@/components/frontend"; +import { CardElement, PaymentElement, useElements, useStripe } from "@stripe/react-stripe-js"; +import { useParams } from "react-router"; +import MkdSDK from "@/utils/MkdSDK"; +let sdk = new MkdSDK(); + + +export default function PayBookingModal({ setConfirmPayment, modalOpen, closeModal, onSuccess, clientSecret, booking_id, paymentMethod }) { + const stripe = useStripe(); + const elements = useElements(); + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const [loading, setLoading] = useState(false); + const { id } = useParams(); + + async function onSubmit(e) { + e.preventDefault(); + setLoading(true); + console.log("submitting"); + try { + if (!stripe || !elements) { + return; + } + const resultData = await stripe.createPaymentMethod({ + type: 'card', + card: elements.getElement(CardElement), + }) + const result = await sdk.callRawAPI(`/v2/api/custom/ergo/pay-hold`, { booking_id, user_id: Number(localStorage.getItem("user")), payment_method: resultData?.paymentMethod?.id }, "POST"); + if (result.error) { + throw new Error(error.message); + } + setConfirmPayment(true) + closeModal(); + globalDispatch({ + type: "SHOW_CONFIRMATION", payload: { + heading: "Payment Successful", message: "Booking successful", btn: "OK", onClose: async () => { + await onSuccess(id); + } + } + }); + } catch (err) { + globalDispatch({ type: "SHOW_ERROR", payload: { heading: "Payment failed", message: err.message } }); + } + setLoading(false); + } + + useEffect(() => { + if (!clientSecret) { + return; + } + }, []) + + + return ( + + + +
    + + +
    +
    + + + + Payment + +
    + +
    + + + Proceed + +
    +
    +
    +
    +
    +
    +
    + ); +} diff --git a/src/pages/Customer/Bookings/SelectExistingCardsModal.jsx b/src/pages/Customer/Bookings/SelectExistingCardsModal.jsx new file mode 100644 index 0000000..39cbdfb --- /dev/null +++ b/src/pages/Customer/Bookings/SelectExistingCardsModal.jsx @@ -0,0 +1,302 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment, useEffect, useState } from "react"; +import { useContext } from "react"; +import { GlobalContext } from "@/globalContext"; +import { LoadingButton } from "@/components/frontend"; +import MkdSDK from "@/utils/MkdSDK"; +import { useLocation, useNavigate, useParams } from "react-router"; +import moment from "moment"; +import { showToast } from "@/globalContext"; +import { addOneHour } from "@/utils/utils"; +import axios from "axios"; + +const stripeKey = import.meta.env.VITE_REACT_STRIPE_PUBLIC_KEY; + +const cardIcons = { + MasterCard: "/mastercard.jpg", + Visa: "/visa.jpg", + "American Express": "/american-express.png", + Discover: "/discover.png", +}; + +export default function SelectExistingCardsModal({ setPaying, setConfirmPayment, setloadingBtn, selectedAddons, bookingData, modalOpen, closeModal, onSuccess, cards, booking_id }) { + const { dispatch: authDispatch, state: authState } = useContext(AuthContext); + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const [loading, setLoading] = useState(false); + const [selectedCard, setSelectedCard] = useState({}); + const [cardSelected, setCardSelected] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [redirectData, setRedirectData] = useState(null); + const location = useLocation(); + const { id } = useParams(); + const user_id = localStorage.getItem("user"); + const navigate = useNavigate() + const sdk = new MkdSDK(); + + async function payBooking() { + localStorage.setItem("card", JSON.stringify(selectedCard)) + try { + const dateFormat = moment(bookingData.selectedDate).format("MM/DD/YY"); + let addons = [] + if (!cardSelected) { + showToast(globalDispatch, "Please select a card to proceed", 5000, "ERROR") + setLoading(false); + } else { + setLoading(true); + localStorage.setItem("paying", true); + for (const [k, v] of Object.entries(selectedAddons)) { + const property_add_on_id = document.querySelector(`input[name='${k}']`)?.getAttribute("id").replace("cb", ""); + if (typeof v === "string") { + addons.push(Number(property_add_on_id)) + } + } + localStorage.setItem("addons", JSON.stringify(addons)) + const result = await sdk.callRawAPI(`/v2/api/custom/ergo/pay-hold`, { + "payment_method": selectedCard?.id, + "stripe_uid": selectedCard?.customer.id, + "booking_start_time": new Date(dateFormat + " " + bookingData.from).toISOString().slice(0, 19).replace("T", " "), + "booking_end_time": new Date(dateFormat + " " + bookingData.to).toISOString().slice(0, 19).replace("T", " "), + "customer_id": Number(user_id), + "host_id": bookingData.host_id, + "property_space_id": Number(id), + "num_guests": bookingData.num_guests, + "booking_addons": addons + + }, "POST"); + + // Create the Axios request configuration + const axiosConfig = { + headers: { + 'Authorization': `Bearer ${stripeKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + } + }; + + // Construct the request data + const requestData = new URLSearchParams(); + requestData.append('payment_method', selectedCard?.id); + requestData.append('client_secret', result?.payment_intent?.client_secret); + requestData.append('return_url', `https://ergo.mkdlabs.com/property/${id}/booking-preview`); + + if (result.message === "Action required!") { + await axios.post(`https://api.stripe.com/v1/payment_intents/${result?.payment_intent?.id}/confirm`, requestData, axiosConfig).then(async (response) => { + if (response.error) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + message: response.error?.message ? response.error?.message : response?.trace?.raw?.message, + }, + }) + setLoading(false); + } else { + if (response?.data?.next_action !== null) { + window.location.replace(response?.data?.next_action?.redirect_to_url?.url) + } else { + const second_result = await sdk.callRawAPI(`/v2/api/custom/ergo/pay-hold`, { + "payment_method": selectedCard?.id, + "stripe_uid": selectedCard?.customer.id, + "booking_start_time": new Date(dateFormat + " " + bookingData.from).toISOString().slice(0, 19).replace("T", " "), + "booking_end_time": new Date(dateFormat + " " + bookingData.to).toISOString().slice(0, 19).replace("T", " "), + "customer_id": Number(user_id), + "host_id": bookingData.host_id, + "property_space_id": Number(id), + "num_guests": bookingData.num_guests, + "booking_addons": addons, + "payment_intent": result?.payment_intent?.id + + }, "POST"); + if (!second_result.error) { + // Show your customer that the payment has succeeded + closeModal(); + globalDispatch({ + type: "SHOW_CONFIRMATION", payload: { + heading: "Payment, awaiting Host Approval", message: "Booking successful", btn: "OK", onClose: () => navigate("/account/my-bookings") + } + }); + setLoading(false); + setloadingBtn(false); + } + } + } + }) + } + } + } catch (error) { + setLoading(false); + tokenExpireError(authDispatch, error.message); + showToast(globalDispatch, error?.response?.data ? error?.response?.data?.error?.message : error?.message, "5000", "ERROR") + setLoading(false); + localStorage.removeItem("paying"); + } + } + + useEffect(() => { + // Function to parse query + const getQueryParams = (search) => { + return search + .slice(1) + .split('&') + .reduce((acc, current) => { + const [key, value] = current.split('='); + acc[key] = decodeURIComponent(value); + return acc; + }, {}); + }; + // Check if there are query parameters in the URL + if (location.search) { + const queryParams = getQueryParams(location.search); + + // Check if the query parameters contain Stripe redirect data + if (queryParams.payment_intent) { + setloadingBtn(true) + // Create the Axios request configuration + const axiosConfig = { + headers: { + 'Authorization': `Bearer ${stripeKey}`, + 'Content-Type': 'application/x-www-form-urlencoded', + } + }; + + const requestData = new URLSearchParams(); + requestData.append('client_secret', queryParams?.payment_intent_client_secret); + + // Parse and set the redirect data + axios.get(`https://api.stripe.com/v1/payment_intents/${queryParams?.payment_intent}?client_secret=${queryParams?.payment_intent_client_secret}`, axiosConfig) + .then(async (response) => { + if (response.status !== 200) { + // PaymentIntent client secret was invalid + showToast(globalDispatch, "Payment Unsuccessful", "5000", "ERROR") + setLoading(false); + } else { + const dateFormat = moment(bookingData.selectedDate).format("MM/DD/YY"); + const cardDetails = JSON.parse(localStorage.getItem("card")); + const addons = JSON.parse(localStorage.getItem("addons")); + if (response.data.status === 'requires_capture') { + const result = await sdk.callRawAPI(`/v2/api/custom/ergo/pay-hold`, { + "payment_method": cardDetails?.id, + "stripe_uid": cardDetails?.customer.id, + "booking_start_time": new Date(dateFormat + " " + bookingData.from).toISOString().slice(0, 19).replace("T", " "), + "booking_end_time": new Date(dateFormat + " " + bookingData.to).toISOString().slice(0, 19).replace("T", " "), + "customer_id": Number(user_id), + "host_id": bookingData.host_id, + "property_space_id": Number(id), + "num_guests": bookingData.num_guests, + "booking_addons": addons, + "payment_intent": queryParams?.payment_intent + + }, "POST"); + console.log(result) + if (!result.error) { + // Show your customer that the payment has succeeded + closeModal(); + globalDispatch({ + type: "SHOW_CONFIRMATION", payload: { + heading: "Payment, awaiting Host Approval", message: "Booking successful", btn: "OK", onClose: () => navigate("/account/my-bookings") + } + }); + setLoading(false); + setloadingBtn(false); + } + } + } + }) + const redirectData = queryParams; + setRedirectData(redirectData); + setIsLoading(false); + } + } else { + setIsLoading(false); + } + }, [location.search]); + + + return ( + + + +
    + + +
    +
    + + + + Select Card + +
    + {cards.map((card) => ( +
    { setSelectedCard(card); setCardSelected(true) }} + > +
    +
    + +
    +
    +

    Credit card

    + + Expires: {card.exp_month}/{card.exp_year} + +
    +
  • {card.last4}
  • +
    +
    + ))} +
    + + + Pay + +
    +
    +
    +
    +
    +
    +
    + ); +} diff --git a/src/pages/Customer/CustomerAccountHeader.jsx b/src/pages/Customer/CustomerAccountHeader.jsx new file mode 100644 index 0000000..3ed6e52 --- /dev/null +++ b/src/pages/Customer/CustomerAccountHeader.jsx @@ -0,0 +1,15 @@ +import React from "react"; +import { Outlet, useLocation } from "react-router"; +import NavBarSlider from "@/components/frontend/NavBarSlider"; + +export default function CustomerAccountHeader() { + const { pathname } = useLocation(); + const menuBlacklist = ["/account/verification", "/account/my-bookings/", "/account/my-spaces/"]; + + return ( +
    +
    {menuBlacklist.every((path) => !pathname.startsWith(path)) && }
    + +
    + ); +} diff --git a/src/pages/Customer/Payments/CustomerPaymentsPage.jsx b/src/pages/Customer/Payments/CustomerPaymentsPage.jsx new file mode 100644 index 0000000..b6c6fe9 --- /dev/null +++ b/src/pages/Customer/Payments/CustomerPaymentsPage.jsx @@ -0,0 +1,353 @@ +import React, { useEffect } from "react"; +import { useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import PaginationBar from "@/components/PaginationBar"; +import PaginationHeader from "@/components/PaginationHeader"; +import useDelayUnmount from "@/hooks/useDelayUnmount"; +import CustomSelect from "@/components/frontend/CustomSelect"; +import { useContext } from "react"; +import { GlobalContext } from "@/globalContext"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import { formatDate, monthsMapping } from "@/utils/date-time-utils"; + +const columns = [ + { + header: "BOOKING DATE", + accessor: "booking_start_time", + }, + { + header: "SPACE", + accessor: "space", + }, + { + header: "PAYMENT METHOD", + accessor: "payment_method", + }, + { + header: "AMOUNT", + accessor: "amount", + }, + { + header: "RECEIPT", + accessor: "receipt", + }, + { + header: "ACTION", + accessor: "", + }, +]; +export default function CustomerPaymentsPage() { + const [viewPaymentPopup, setViewPaymentPopup] = useState(false); + const showViewPaymentPopup = useDelayUnmount(viewPaymentPopup, 200); + + const [pageSize, setPageSize] = useState(10); + const [pageCount, setPageCount] = useState(0); + const [dataTotal, setDataTotal] = useState(0); + const [currentPage, setPage] = useState(0); + const [canPreviousPage, setCanPreviousPage] = useState(false); + const [canNextPage, setCanNextPage] = useState(false); + const [direction, setDirection] = useState("DESC"); + const [rows, setRows] = useState([]); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const [selectedPayment, setSelectedPayment] = useState({}); + + const navigate = useNavigate(); + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, pageSize) { + globalDispatch({ type: "START_LOADING" }); + + const user_id = localStorage.getItem("user"); + const where = [`ergo_booking.customer_id = ${user_id} AND ergo_booking.status = 3`]; + try { + const result = await callCustomAPI( + "booking", + "post", + { + where, + page: pageNum, + limit: pageSize, + sortId: "update_at", + direction: "DESC", + }, + "PAGINATE", + ); + + const { list, total, limit, num_pages, page } = result; + setRows(list); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + useEffect(() => { + getData(1, pageSize); + }, []); + + const sortByDate = (a, b) => { + if (direction == "DESC") { + return new Date(b.update_at) - new Date(a.update_at); + } + return new Date(a.update_at) - new Date(b.update_at); + }; + + function handlePrint() { + console.log("printing"); + var myWindow = window.open("", "PRINT", "height=400,width=600"); + + myWindow.document.write("" + document.title + ""); + myWindow.document.write(""); + myWindow.document.write("

    " + document.title + "

    "); + myWindow.document.write(document.getElementById("receipt").innerHTML); + myWindow.document.write(""); + + myWindow.document.close(); // necessary for IE >= 10 + myWindow.focus(); // necessary for IE >= 10*/ + + myWindow.print(); + // myWindow.close(); + } + + return ( +
    +
    + +
    + +
    + + + + {columns.map((column, index) => ( + + ))} + + + + {rows.sort(sortByDate).map((row, i) => { + return ( + + {columns.map((cell, index) => { + if (cell.accessor === "") { + return ( + + ); + } + if (cell.accessor == "booking_start_time") { + var date = new Date(row[cell.accessor]); + return ( + + ); + } + if (cell.accessor == "space") { + var date = new Date(row[cell.accessor]); + return ( + + ); + } + if (cell.accessor == "amount") { + var date = new Date(row[cell.accessor]); + return ( + + ); + } + if (cell.accessor == "receipt") { + return ( + + ); + } + if (cell.accessor == "payment_method") { + return ( + + ); + } + return ( + + ); + })} + + ); + })} + +
    + {column.header} +
    + + + {monthsMapping[date.getMonth()] + " " + date.getDate() + "/" + date.getFullYear()} + + {row.property_name + " " + row.space_category} + + {"$" + (row.total + row.addon_cost).toFixed(2)} + + {row.id} + + Credit Card + + {row[cell.accessor]} +
    +
    + + + {showViewPaymentPopup && ( +
    setViewPaymentPopup(false)} + > +
    e.stopPropagation()} + id="receipt" + > +
    +

    Payment details

    + +
    +
    +

    + Booking Started on: {formatDate(selectedPayment.booking_start_time)} +

    +

    + Space name: {selectedPayment.property_name} +

    +

    + Booking: #{selectedPayment.id}{" "} + + (View booking details) + +

    +

    + Space: #{selectedPayment.property_space_id}{" "} + + (View booking details) + +

    +

    + Total cost: ${selectedPayment.total?.toFixed(2)} +

    +

    + Total addon cost: ${selectedPayment.addon_cost?.toFixed(2)} +

    + + +
    +
    + )} +
    + ); +} diff --git a/src/pages/Customer/Profile/CustomerProfilePage.jsx b/src/pages/Customer/Profile/CustomerProfilePage.jsx new file mode 100644 index 0000000..6619806 --- /dev/null +++ b/src/pages/Customer/Profile/CustomerProfilePage.jsx @@ -0,0 +1,289 @@ +import React, { useContext } from "react"; +import { useState } from "react"; +import { Link } from "react-router-dom"; +import NotVerifiedIcon from "@/components/frontend/icons/NotVerifiedIcon"; +import PencilIcon from "@/components/frontend/icons/PencilIcon"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import Skeleton from "react-loading-skeleton"; +import { formatDate } from "@/utils/date-time-utils"; +import { IMAGE_STATUS, NOTIFICATION_STATUS, NOTIFICATION_TYPE } from "@/utils/constants"; +import SwitchBulkMode from "@/components/SwitchBulkMode"; +import TwoFaDialog from "@/components/Profile/TwoFaDialog"; +import EditProfileModal from "@/components/Profile/EditProfileModal"; +import EditLocationModal from "@/components/Profile/EditLocationModal"; +import EditPasswordModal from "@/components/Profile/EditPasswordModal"; +import EditAboutModal from "@/components/Profile/EditAboutModal"; +import { parseJsonSafely } from "@/utils/utils"; +import EnableEmailDialog from "@/components/Profile/EnableEmailDialog"; +import DeleteAccountModal from "@/components/Profile/DeleteAccountModal"; + +function getProfilePhotoMessage(image_status) { + switch (image_status) { + case IMAGE_STATUS.IN_REVIEW: + return "We are currently reviewing your profile picture"; + case IMAGE_STATUS.APPROVED: + return "This will be displayed on your profile"; + case IMAGE_STATUS.NOT_APPROVED: + return "The image you uploaded was rejected after reviewing, please change it"; + default: + return "Please upload a profile picture"; + } +} + +export default function CustomerProfilePage() { + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const [loading, setLoading] = useState(false); + const [twoFa, setTwoFa] = useState(false); + const [twoFaDialog, setTwoFaDialog] = useState(false); + const [enableEmailDialog, setEnableEmailDialog] = useState(false); + + const [updatePassword, setUpdatePassword] = useState(false); + + const [updateName, setUpdateName] = useState(false); + + const [updateAbout, setUpdateAbout] = useState(false); + + const [updateLocation, setUpdateLocation] = useState(false); + + const [deleteAccountModal, setDeleteAccountModal] = useState(false); + + let sdk = new MkdSDK(); + + const changeProfilePic = async (e) => { + globalDispatch({ type: "START_LOADING" }); + const file = e.target.files; + const formData = new FormData(); + for (let i = 0; i < file.length; i++) { + formData.append("file", file[i]); + } + try { + const upload = await sdk.uploadImage(formData); + console.log("upload", upload); + sdk.setTable("user"); + const result = await callCustomAPI( + "edit-self", + "post", + { + user: { + photo: upload.url, + is_photo_approved: IMAGE_STATUS.IN_REVIEW, + }, + }, + "", + ); + globalDispatch({ type: "SET_USER_DATA", payload: { ...globalState.user, photo: upload.url, is_photo_approved: IMAGE_STATUS.IN_REVIEW } }); + // create notification + sdk.setTable("notification"); + await sdk.callRestAPI( + { + user_id: globalState.user.id, + actor_id: null, + action_id: globalState.user.id, + notification_time: new Date().toISOString().split(".")[0], + message: "Profile Picture Edited", + type: NOTIFICATION_TYPE.EDIT_USER_PICTURE, + status: NOTIFICATION_STATUS.NOT_ADDRESSED, + }, + "POST", + ); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + }; + + const removeProfilePic = async (e) => { + try { + sdk.setTable("user"); + await callCustomAPI( + "edit-self", + "post", + { + user: { + photo: null, + is_photo_approved: null, + }, + }, + "", + ); + globalDispatch({ type: "SET_USER_DATA", payload: { ...globalState.user, photo: null, is_photo_approved: null } }); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + }; + + async function changeTwoFa() { + setLoading(true); + try { + await callCustomAPI( + "edit-self", + "post", + { + user: { + two_factor_authentication: twoFa != 1 ? 1 : 0, + }, + }, + "", + ); + setTwoFaDialog(false); + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Success", + message: `Two factor Authentication ${twoFa == 1 ? "disabled" : "enabled"}`, + btn: "Ok got it", + }, + }); + setTwoFa((prev) => (prev == 1 ? 0 : 1)); + } catch (err) { + setTwoFaDialog(false); + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + setLoading(false); + } + + return ( +
    +
    +
    +
    +

    Your photo

    + {getProfilePhotoMessage(globalState.user.is_photo_approved)} +
    +
    + +
    + + +
    +
    +
    +
    +

    Profile status

    +
    + {![0, 1].includes(globalState.user.verificationStatus) && ( + + Get verified + + )} + + +
    +
    +
    + +
    + setUpdateName(false)} + modalOpen={updateName} + /> + setUpdatePassword(false)} + modalOpen={updatePassword} + /> + setUpdateAbout(false)} + modalOpen={updateAbout} + /> + setUpdateLocation(false)} + modalOpen={updateLocation} + /> + + setTwoFaDialog(false)} + isEnabled={twoFa} + onProceed={changeTwoFa} + loading={loading} + /> + setEnableEmailDialog(false)} + /> +
    + ); +} diff --git a/src/pages/Customer/Reviews/CustomerReviewsPage.jsx b/src/pages/Customer/Reviews/CustomerReviewsPage.jsx new file mode 100644 index 0000000..2a43ca1 --- /dev/null +++ b/src/pages/Customer/Reviews/CustomerReviewsPage.jsx @@ -0,0 +1,390 @@ +import moment from "moment"; +import React from "react"; +import { useContext } from "react"; +import { useEffect } from "react"; +import { useState } from "react"; +import { Link } from "react-router-dom"; +import StarIcon from "@/components/frontend/icons/StarIcon"; +import PaginationBar from "@/components/PaginationBar"; +import PaginationHeader from "@/components/PaginationHeader"; +import { GlobalContext } from "@/globalContext"; +import useDelayUnmount from "@/hooks/useDelayUnmount"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import CustomSelect from "@/components/frontend/CustomSelect"; + +const columns = [ + { + header: "ID", + accessor: "id", + }, + { + header: "BOOKING DATE", + accessor: "booking_start_time", + }, + { + header: "SPACE", + accessor: "name", + }, + { + header: "HOST", + accessor: "host_full_name", + }, + { + header: "RATING", + accessor: "rating", + }, + { + header: "STATUS", + accessor: "status", + mapping: ["Under Review", "Posted", "Declined"], + }, + { + header: "ACTION", + accessor: "", + }, +]; + +export default function CustomerReviewsPage() { + const [type, setType] = useState(0); + const [viewReviewPopup, setViewReviewPopup] = useState(false); + const showViewReviewPopup = useDelayUnmount(viewReviewPopup, 100); + + const [viewReviewData, setViewReviewData] = useState({}); + + const [pageSize, setPageSize] = useState(10); + const [pageCount, setPageCount] = useState(0); + const [dataTotal, setDataTotal] = useState(0); + const [currentPage, setPage] = useState(0); + const [canPreviousPage, setCanPreviousPage] = useState(false); + const [canNextPage, setCanNextPage] = useState(false); + const [rows, setRows] = useState([]); + const [direction, setDirection] = useState("DESC"); + + const { dispatch: globalDispatch } = useContext(GlobalContext); + + async function getData(pageNum, pageSize) { + globalDispatch({ type: "START_LOADING" }); + + const user_id = localStorage.getItem("user"); + const where = [`ergo_review.customer_id = ${user_id}`, `ergo_review.given_by = ${type == 1 ? "'customer'" : "'host'"}`]; + try { + const result = await callCustomAPI( + "review-hashtag", + "post", + { + where, + page: pageNum, + limit: pageSize, + user: "customer", + sortId: "post_date", + direction: "DESC", + }, + "PAGINATE", + ); + + const { list, total, limit, num_pages, page } = result; + setRows(list); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + useEffect(() => { + getData(1, 10); + }, [type]); + + const sortByDate = (a, b) => { + if (direction == "DESC") { + return new Date(b.update_at) - new Date(a.update_at); + } + return new Date(a.update_at) - new Date(b.update_at); + }; + + return ( +
    +
    +
    + + +
    + +
    +
    + +
    +
    + + + + {columns.map((column, index) => ( + + ))} + + + + {rows.sort(sortByDate).map((row, i) => { + return ( + + {columns.map((cell, index) => { + if (cell.accessor === "") { + return ( + + ); + } + if (cell.accessor.includes("rating")) { + return ( + + ); + } + if (cell.accessor == "booking_start_time") { + return ( + + ); + } + if (cell.accessor == "host_full_name") { + return ( + + ); + } + if (cell.mapping) { + return ( + + ); + } + return ( + + ); + })} + + ); + })} + +
    + {column.header} +
    + + + + + {(Number(type == 0 ? row.customer_rating : row.space_rating) || 0).toFixed(1)} + + + {moment(row[cell.accessor]).format("MM/DD/YY")} + + {row[`host_first_name`] + " " + row[`host_last_name`]} + + + {" "} + {cell.mapping[row[cell.accessor]]} + + + {row[cell.accessor]} +
    +
    +
    + +
    + + {showViewReviewPopup && ( +
    setViewReviewPopup(false)} + > +
    e.stopPropagation()} + > +
    +

    Review details

    + {" "} +
    +
    +

    + Review posted on: {moment(viewReviewData.booking_start_time ?? "11/11/11").format("MM/DD/YY")} +

    +

    + Space: {viewReviewData.name ?? ""} +

    +

    + Booking: #{viewReviewData.booking_id ?? ""}{" "} + + (View details) + +

    +

    Rating

    +
    + {[1, 2, 3, 4, 5].map((val) => ( + = val ? "#FEC84B" : "none"} + xmlns="http://www.w3.org/2000/svg" + > + = val ? "#FEC84B" : "#98A2B3"} + /> + + ))} +
    + {viewReviewData.host_rating && ( + <> +

    Host rating

    +
    + {[1, 2, 3, 4, 5].map((val) => ( + = val ? "#FEC84B" : "none"} + xmlns="http://www.w3.org/2000/svg" + > + = val ? "#FEC84B" : "#98A2B3"} + /> + + ))} +
    + + )} +

    Hashtags

    +
    + {viewReviewData.hashtags ? ( + viewReviewData.hashtags.split(",").map((tag, idx) => ( + + {tag} + + )) + ) : ( + <> + )} +
    +

    Comments

    +

    {viewReviewData.comment ?? ""}

    + +
    +
    + )} +
    + ); +} diff --git a/src/pages/Customer/Verification/CustomerVerificationPage.jsx b/src/pages/Customer/Verification/CustomerVerificationPage.jsx new file mode 100644 index 0000000..d2672e3 --- /dev/null +++ b/src/pages/Customer/Verification/CustomerVerificationPage.jsx @@ -0,0 +1,317 @@ +import React, { useContext } from "react"; +import { useForm } from "react-hook-form"; +import { useNavigate } from "react-router"; +import Icon from "@/components/Icons"; +import { FileUploader } from "react-drag-drop-files"; +import { useState } from "react"; +import useDelayUnmount from "@/hooks/useDelayUnmount"; +import GreenCheckIcon from "@/components/frontend/icons/GreenCheckIcon"; +import SecurityIcon from "@/components/frontend/icons/SecurityIcon"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { LoadingButton } from "@/components/frontend"; +import { NOTIFICATION_STATUS, NOTIFICATION_TYPE } from "@/utils/constants"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import { useSearchParams } from "react-router-dom"; + +let sdk = new MkdSDK(); + +export default function CustomerVerificationPage() { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + + const schema = yup.object({ + expiry_date: yup.string().test("is-not-in-past", "Not a valid date", (val) => { + if (val == "") return false; + const date = new Date(val); + return date.setDate(date.getDate() - 1) > new Date(); + }), + }); + + const { + handleSubmit, + register, + watch, + formState: { errors }, + } = useForm({ defaultValues: { selectedType: "Driver's License" }, resolver: yupResolver(schema) }); + + const [frontImage, setFrontImage] = useState(null); + const [backImage, setBackImage] = useState(null); + const [passport, setPassport] = useState(null); + const [loading, setLoading] = useState(false); + + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const { dispatch: authDispatch } = useContext(AuthContext); + + const [verified, setVerified] = useState(false); + const showVerified = useDelayUnmount(verified, 300); + + const selectedType = watch("selectedType"); + + const isDisabled = () => { + if (selectedType == "Driver's License" && frontImage && backImage) return false; + if (selectedType == "Passport" && passport) return false; + return true; + }; + + const handleImageUpload = async (file) => { + const formData = new FormData(); + formData.append("file", file); + try { + const upload = await sdk.uploadImage(formData); + return upload.url; + } catch (err) { + console.log("err", err); + return ""; + } + }; + + const onSubmit = async (data) => { + try { + setLoading(true); + if (selectedType == "Driver's License") { + data.frontImage = await handleImageUpload(frontImage); + data.backImage = await handleImageUpload(backImage); + } else { + data.frontImage = await handleImageUpload(passport); + } + sdk.setTable("id_verification"); + const result = await sdk.callRestAPI( + { + id: globalState.user.verificationId, + type: selectedType, + expiry_date: data.expiry_date, + status: 0, + image_front: data.frontImage, + image_back: data.backImage ?? null, + user_id: Number(localStorage.getItem("user")), + }, + globalState.user.verificationId ? "PUT" : "POST", + ); + + // create notification + sdk.setTable("notification"); + await sdk.callRestAPI( + { + user_id: Number(localStorage.getItem("user")), + actor_id: null, + action_id: result.message, + notification_time: new Date().toISOString().split(".")[0], + message: "New ID Verification submitted", + type: NOTIFICATION_TYPE.NEW_ID_VERIFICATION, + status: NOTIFICATION_STATUS.NOT_ADDRESSED, + }, + "POST", + ); + + setVerified(true); + } catch (err) { + tokenExpireError(authDispatch, err.message); + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + setLoading(false); + }; + + const readImage = (file, previewEl) => { + const reader = new FileReader(); + reader.onload = (event) => { + document.getElementById(previewEl).src = event.target.result; + }; + + reader.readAsDataURL(file); + }; + + return ( +
    +
    + +
    +

    Identity Verification

    +
    +

    + + Safety is our priority +

    +

    + To establish trust for all parties we verify both hosts and guests. Your personal information is secure. We will never share your information with third parties. +

    +
    +
    +

    Verification Documents.

    +
    + + +
    +
    + {selectedType == "Driver's License" ? ( +
    + { + setFrontImage(file); + }} + types={["SVG", "JPEG", "PNG", "GIF", "JPG"]} + > +
    + {frontImage?.name ? ( + + ) : ( + <> +

    Front

    +

    + Click to upload or drag and drop SVG, PNG, JPG or GIF (max. 800x400px) +

    + + )} +
    +
    + { + setBackImage(file); + }} + types={["SVG", "JPEG", "PNG", "GIF", "JPG"]} + > +
    + {backImage?.name ? ( + + ) : ( + <> +

    Back

    +

    + Click to upload or drag and drop SVG, PNG, JPG or GIF (max. 800x400px) +

    + + )} +
    +
    +
    + ) : ( + { + setPassport(file); + }} + types={["SVG", "JPEG", "PNG", "GIF", "JPG"]} + > +
    + {passport?.name ? ( + + ) : ( + <> +

    Passport page with photo

    +

    + Click to upload or drag and drop SVG, PNG, JPG or GIF (max. 800x400px) +

    + + )} +
    +
    + )} +
    +
    + + + {errors.expiry_date?.message &&

    {errors.expiry_date?.message}

    } +
    + + Submit Document + +
    +
    +
    e.stopPropagation()} + > +

    + + Document received +

    +

    Once we verify your document you will receive an email. It usually takes up to 24 hours.

    + +
    +
    +
    + ); +} diff --git a/src/pages/Host/Addons/EditHostAddons.jsx b/src/pages/Host/Addons/EditHostAddons.jsx new file mode 100644 index 0000000..d820024 --- /dev/null +++ b/src/pages/Host/Addons/EditHostAddons.jsx @@ -0,0 +1,184 @@ +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import { useNavigate, useParams } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import EditAdminPageLayout from "@/layouts/EditAdminPageLayout"; +import { LoadingButton } from "@/components/frontend"; + +let sdk = new MkdSDK(); + +const EditHostAddOnPage = () => { + const [spaceCategories, setSpaceCategories] = React.useState([]); + const { dispatch } = React.useContext(AuthContext); + const [loading, setLoading] = React.useState(); + + const schema = yup + .object({ + name: yup.string().required("Name is required"), + cost: yup.number().required().typeError("Cost must be a number"), + }) + .required(); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const navigate = useNavigate(); + const [id, setId] = useState(0); + const { + register, + handleSubmit, + setError, + setValue, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const params = useParams(); + + useEffect(function () { + (async function () { + try { + sdk.setTable("add_on"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + if (!result.error) { + setValue("name", result.model.name); + setValue("cost", result.model.cost); + setId(result.model.id); + } + } catch (error) { + console.log("error", error); + tokenExpireError(dispatch, error.message); + } + })(); + }, []); + + async function fetchSpaceCategories() { + try { + sdk.setTable("spaces"); + const result = await sdk.callRestAPI({}, "GETALL"); + if (Array.isArray(result.list)) { + setSpaceCategories(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + const onSubmit = async (data) => { + setLoading(true) + + sdk.setTable("add_on"); + try { + const result = await sdk.callRestAPI( + { + id: id, + name: data.name, + cost: data.cost, + }, + "PUT", + ); + + if (!result.error) { + showToast(globalDispatch, "Updated"); + setLoading(false) + navigate("/account/my-addons"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + setLoading(false) + console.log("Error", error); + setError("name", { + type: "manual", + message: error.message, + }); + } + }; + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "add_on", + }, + }); + fetchSpaceCategories(); + }, []); + + return ( +
    + +
    +
    + + +

    {errors.name?.message}

    +
    + +
    + + +

    {errors.cost?.message}

    +
    + +
    + + + + Save + +
    +
    +
    +
    + ); +}; + +export default EditHostAddOnPage; diff --git a/src/pages/Host/Addons/HostAddOnListPage.jsx b/src/pages/Host/Addons/HostAddOnListPage.jsx new file mode 100644 index 0000000..12c1860 --- /dev/null +++ b/src/pages/Host/Addons/HostAddOnListPage.jsx @@ -0,0 +1,267 @@ +import React, { useState } from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { Link, useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import AddButton from "@/components/AddButton"; +import Button from "@/components/Button"; +import Table from "@/components/Table"; +import PaginationHeader from "@/components/PaginationHeader"; +import ReactHtmlTableToExcel from "react-html-table-to-excel"; +import { ID_PREFIX } from "@/utils/constants"; +import { adminColumns, applySetting } from "@/utils/adminPortalColumns"; +import TreeSDK from "@/utils/TreeSDK"; +import { PlusCircleIcon } from "@heroicons/react/24/outline"; +import HostAddAddonsModal from "@/components/HostAddAddonsModal"; + +let sdk = new MkdSDK(); +let treeSdk = new TreeSDK(); + +const HostAddOnListPage = () => { + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch, state } = React.useContext(GlobalContext); + const [tableColumns, setTableColumns] = React.useState([]); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_addon_filter") ?? ""); + const [addOnModal, setAddOnModal] = useState(false); + const [editAddOnModal, setEditAddOnModal] = useState(false); + + const [spaceCategories, setSpaceCategories] = React.useState([]); + + const schema = yup.object({ + name: yup.string(), + }); + const { + register, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: parseSearchParams(searchParams), + }); + + function onSort(accessor) { + const columns = tableColumns; + const index = columns.findIndex((column) => column.accessor === accessor); + const column = columns[index]; + column.isSortedDesc = !column.isSortedDesc; + columns.splice(index, 1, column); + setTableColumns(() => [...columns]); + const sortedList = selector(data, column.isSortedDesc, accessor); + setCurrentTableData(sortedList); + } + + function selector(users, isSortedDesc, accessor) { + if (accessor?.split(",").length > 1) { + accessor = accessor.split(",")[0]; + } + + return users.sort((a, b) => { + if (isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1; + } else { + return a[accessor] < b[accessor] ? 1 : -1; + } + } + if (!isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1; + } else { + return a[accessor] < b[accessor] ? -1 : 1; + } + } + }); + } + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum) { + + const data = parseSearchParams(searchParams); + data.id = data.id?.replace(ID_PREFIX.ADDON_CATEGORY, ""); + + try { + let filter = ["ergo_add_on.deleted_at,is"]; + if (data.id) { + filter.push(`ergo_add_on.id,eq,${data.id}`); + } + if (data.name) { + filter.push(`name,cs,${data.name}`); + } + if (data.space_id) { + filter.push(`space_id,eq,${data.space_id}`); + } + filter.push(`creator_id,eq,${localStorage.getItem('user')}`) + + let result = await treeSdk.getPaginate("add_on", { + filter, + join: ["spaces|space_id"], + page: pageNum || 1, + size: limitNum, + order: "update_at", + }); + + const { list, total, limit, num_pages, page } = result; + + const sortedList = selector(list, false); + setCurrentTableData(sortedList); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + async function fetchSpaceCategories() { + try { + let filter = ["deleted_at,is"]; + const result = await treeSdk.getList("spaces", { + filter, + join: [], + }); + if (Array.isArray(result.list)) { + setSpaceCategories(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "add_on", + }, + }); + + (async function () { + await fetchColumnOrder(); + await fetchSpaceCategories(); + getData(1, pageSize); + })(); + }, []); + + React.useEffect(() => { + if (state.deleted) { + globalDispatch({ + type: "DELETED", + payload: { + deleted: false, + }, + }); + getData(currentPage, pageSize); + } + }, [state.deleted]); + + async function fetchColumnOrder() { + globalDispatch({ type: "START_LOADING" }); + sdk.setTable("settings"); + const payload = { key_name: "host_addon_categories_column_order" }; + try { + const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE"); + if (Array.isArray(result.list) && result.list.length > 0) { + setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.host_addon_categories)); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + return ( +
    +
    +

    Host Customized Add-ons

    + +
    + + + + +
    +
    + + + + + +{addOnModal && + + } + + ); +}; + +export default HostAddOnListPage; diff --git a/src/pages/Host/Amenities/EditHostAmenities.jsx b/src/pages/Host/Amenities/EditHostAmenities.jsx new file mode 100644 index 0000000..4aab18b --- /dev/null +++ b/src/pages/Host/Amenities/EditHostAmenities.jsx @@ -0,0 +1,155 @@ +import React, { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { GlobalContext, showToast } from "@/globalContext"; +import { useNavigate, useParams } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import EditAdminPageLayout from "@/layouts/EditAdminPageLayout"; +import { LoadingButton } from "@/components/frontend"; + +let sdk = new MkdSDK(); + +const EditHostAmenitiesPage = () => { + const [spaceCategories, setSpaceCategories] = React.useState([]); + const { dispatch } = React.useContext(AuthContext); + const [loading, setLoading] = React.useState(); + + const schema = yup + .object({ + name: yup.string().required("Name is required"), + }) + .required(); + const { dispatch: globalDispatch } = React.useContext(GlobalContext); + const navigate = useNavigate(); + const [id, setId] = useState(0); + const { + register, + handleSubmit, + setError, + setValue, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + const params = useParams(); + + useEffect(function () { + (async function () { + try { + sdk.setTable("amenity"); + const result = await sdk.callRestAPI({ id: Number(params?.id) }, "GET"); + if (!result.error) { + setValue("name", result.model.name); + setId(result.model.id); + } + } catch (error) { + console.log("error", error); + tokenExpireError(dispatch, error.message); + } + })(); + }, []); + + async function fetchSpaceCategories() { + try { + sdk.setTable("spaces"); + const result = await sdk.callRestAPI({}, "GETALL"); + if (Array.isArray(result.list)) { + setSpaceCategories(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + const onSubmit = async (data) => { + setLoading(true) + + sdk.setTable("amenity"); + try { + const result = await sdk.callRestAPI( + { + id: id, + name: data.name, + cost: data.cost, + space_id: data.space_id, + }, + "PUT", + ); + + if (!result.error) { + showToast(globalDispatch, "Updated"); + setLoading(false) + navigate("/account/my-amenities"); + } else { + if (result.validation) { + const keys = Object.keys(result.validation); + for (let i = 0; i < keys.length; i++) { + const field = keys[i]; + setError(field, { + type: "manual", + message: result.validation[field], + }); + } + } + } + } catch (error) { + setLoading(false) + console.log("Error", error); + setError("name", { + type: "manual", + message: error.message, + }); + } + }; + React.useEffect(() => { + fetchSpaceCategories(); + }, []); + + return ( +
    +
    +
    + + +

    {errors.name?.message}

    +
    + + +
    + + + + Save + +
    + +
    + ); +}; + +export default EditHostAmenitiesPage; diff --git a/src/pages/Host/Amenities/HostAmenitiesListPage.jsx b/src/pages/Host/Amenities/HostAmenitiesListPage.jsx new file mode 100644 index 0000000..7367890 --- /dev/null +++ b/src/pages/Host/Amenities/HostAmenitiesListPage.jsx @@ -0,0 +1,269 @@ +import React, { useState } from "react"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { Link, useSearchParams } from "react-router-dom"; +import { GlobalContext, showToast } from "@/globalContext"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { clearSearchParams, parseSearchParams } from "@/utils/utils"; +import PaginationBar from "@/components/PaginationBar"; +import AddButton from "@/components/AddButton"; +import Button from "@/components/Button"; +import Table from "@/components/Table"; +import PaginationHeader from "@/components/PaginationHeader"; +import ReactHtmlTableToExcel from "react-html-table-to-excel"; +import { ID_PREFIX } from "@/utils/constants"; +import { adminColumns, applySetting } from "@/utils/adminPortalColumns"; +import TreeSDK from "@/utils/TreeSDK"; +import { PlusCircleIcon } from "@heroicons/react/24/outline"; +import HostAddAddonsModal from "@/components/HostAddAddonsModal"; +import HostAddAmenityModal from "@/components/HostAddAmenityModal"; + +let sdk = new MkdSDK(); +let treeSdk = new TreeSDK(); + +const HostAmenitiesListPage = () => { + const { dispatch } = React.useContext(AuthContext); + const { dispatch: globalDispatch, state } = React.useContext(GlobalContext); + const [tableColumns, setTableColumns] = React.useState([]); + const [data, setCurrentTableData] = React.useState([]); + const [pageSize, setPageSize] = React.useState(10); + const [pageCount, setPageCount] = React.useState(0); + const [dataTotal, setDataTotal] = React.useState(0); + const [currentPage, setPage] = React.useState(0); + const [canPreviousPage, setCanPreviousPage] = React.useState(false); + const [canNextPage, setCanNextPage] = React.useState(false); + const [searchParams, setSearchParams] = useSearchParams(localStorage.getItem("admin_addon_filter") ?? ""); + const [amenityModal, setAmenityModal] = useState(false); + + const [spaceCategories, setSpaceCategories] = React.useState([]); + + const schema = yup.object({ + name: yup.string(), + }); + const { + register, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: parseSearchParams(searchParams), + }); + + function onSort(accessor) { + const columns = tableColumns; + const index = columns.findIndex((column) => column.accessor === accessor); + const column = columns[index]; + column.isSortedDesc = !column.isSortedDesc; + columns.splice(index, 1, column); + console.log(columns) + setTableColumns(() => [...columns]); + const sortedList = selector(data, column.isSortedDesc, accessor); + setCurrentTableData(sortedList); + } + + function selector(users, isSortedDesc, accessor) { + if (accessor?.split(",").length > 1) { + accessor = accessor.split(",")[0]; + } + + return users.sort((a, b) => { + if (isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? 1 : -1; + } else { + return a[accessor] < b[accessor] ? 1 : -1; + } + } + if (!isSortedDesc) { + if (isNaN(a[accessor])) { + return a[accessor]?.toLowerCase() < b[accessor]?.toLowerCase() ? -1 : 1; + } else { + return a[accessor] < b[accessor] ? -1 : 1; + } + } + }); + } + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, limitNum) { + + const data = parseSearchParams(searchParams); + data.id = data.id?.replace(ID_PREFIX.ADDON_CATEGORY, ""); + + try { + let filter = ["ergo_amenity.deleted_at,is"]; + if (data.id) { + filter.push(`ergo_amenity.id,eq,${data.id}`); + } + if (data.name) { + filter.push(`name,cs,${data.name}`); + } + if (data.space_id) { + filter.push(`space_id,eq,${data.space_id}`); + } + filter.push(`creator_id,eq,${localStorage.getItem('user')}`) + + let result = await treeSdk.getPaginate("amenity", { + filter, + join: ["spaces|space_id"], + page: pageNum || 1, + size: limitNum, + order: "update_at", + }); + + const { list, total, limit, num_pages, page } = result; + + const sortedList = selector(list, false); + setCurrentTableData(sortedList); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (error) { + tokenExpireError(dispatch, error.message); + showToast(globalDispatch, error.message, 4000, "ERROR"); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + async function fetchSpaceCategories() { + try { + let filter = ["deleted_at,is"]; + const result = await treeSdk.getList("spaces", { + filter, + join: [], + }); + if (Array.isArray(result.list)) { + setSpaceCategories(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + + React.useEffect(() => { + globalDispatch({ + type: "SETPATH", + payload: { + path: "add_on", + }, + }); + + (async function () { + await fetchColumnOrder(); + await fetchSpaceCategories(); + getData(1, pageSize); + })(); + }, []); + + React.useEffect(() => { + if (state.deleted) { + globalDispatch({ + type: "DELETED", + payload: { + deleted: false, + }, + }); + getData(currentPage, pageSize); + } + }, [state.deleted]); + + async function fetchColumnOrder() { + globalDispatch({ type: "START_LOADING" }); + sdk.setTable("settings"); + const payload = { key_name: "host_addon_categories_column_order" }; + try { + const result = await sdk.callRestAPI({ limit: 1, page: 1, payload }, "PAGINATE"); + if (Array.isArray(result.list) && result.list.length > 0) { + setTableColumns(applySetting(result.list[0].optional_data ?? [], adminColumns.host_amenity_categories)); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + showToast(globalDispatch, err.message, 4000, "ERROR"); + } + } + + return ( +
    +
    +

    Host Customized Amenties

    + + +
    + + + + +
    +
    +
    + + + + +{amenityModal && + getData()}/> + } + + ); +}; + +export default HostAmenitiesListPage; diff --git a/src/pages/Host/Billings/AddPayoutMethodModal.jsx b/src/pages/Host/Billings/AddPayoutMethodModal.jsx new file mode 100644 index 0000000..9ced96d --- /dev/null +++ b/src/pages/Host/Billings/AddPayoutMethodModal.jsx @@ -0,0 +1,181 @@ +import { GlobalContext } from "@/globalContext"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment, useState } from "react"; +import { useContext } from "react"; +import { LoadingButton } from "@/components/frontend"; +import { useForm } from "react-hook-form"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; + +export default function AddPayoutMethodModal({ modalOpen, closeModal, onSuccess }) { + const schema = yup.object({ + account_number: yup.string().required("Enter Account Number").label("Account Number"), + account_number2: yup.string() + .oneOf([yup.ref("account_number"), null], "Account Numbers Must Match"), + routing_number: yup.string().required("Enter Routing Number").label("Routing Number"), + routing_number2: yup.string() + .oneOf([yup.ref("routing_number"), null], "Routing Numbers Must Match"), + }); + + const { formState, handleSubmit, register, reset, formState: { errors }, } = useForm({ resolver: yupResolver(schema), defaultValues: { routing_number: "", account_number: "", account_number2: "", account_name: "" } }); + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const { dispatch, state } = useContext(AuthContext); + const [ctrl] = useState(new AbortController()); + + const onSubmit = async (data) => { + try { + const sdk = new MkdSDK(); + sdk.setTable("payout_method"); + await sdk.callRestAPI({ host_id: state.user, account_name: data?.account_name, account_number: data?.account_number, routing_number: data?.routing_number }, "POST", ctrl.signal); + closeModal(); + onSuccess(); + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + reset(); + closeModal(); + globalDispatch({ + type: "SHOW_ERROR", + payload: { + message: err.message, + }, + }); + } + }; + + + return ( + + + +
    + + +
    +
    + + +
    + + Add Payout Method + + +
    +
    + + +
    +
    + + +

    + {errors.routing_number2?.message} +

    +
    +
    + + +
    +
    + + +

    + {errors.account_number2?.message} +

    +
    +
    + + +
    + +
    + + + Add payout method + +
    +
    +
    +
    +
    +
    +
    + ); +} diff --git a/src/pages/Host/Billings/HostBillingsPage.jsx b/src/pages/Host/Billings/HostBillingsPage.jsx new file mode 100644 index 0000000..2261182 --- /dev/null +++ b/src/pages/Host/Billings/HostBillingsPage.jsx @@ -0,0 +1,269 @@ +import React, { Fragment, useContext, useEffect, useState } from "react"; +import { Menu, Transition } from "@headlessui/react"; +import { useCards } from "@/hooks/api"; +import { ExclamationCircleIcon, TrashIcon } from "@heroicons/react/24/outline"; +import AddCardMethodModal from "@/components/Billing/AddCardMethodModal"; +import DeleteCardMethodModal from "@/components/Billing/DeleteCardMethodModal"; +import { EllipsisVerticalIcon } from "@heroicons/react/24/solid"; +import AddPayoutMethodModal from "./AddPayoutMethodModal"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import TreeSDK from "@/utils/TreeSDK"; + +const cardIcons = { + MasterCard: "/mastercard.jpg", + Visa: "/visa.jpg", + "American Express": "/american-express.png", + Discover: "/discover.png", +}; + +export default function HostBillingsPage() { + const [addMethodPopup, setAddMethodPopup] = useState(false); + + const [deleteMethodPopup, setDeleteMethodPopup] = useState(false); + const [selectedCard, setSelectedCard] = useState({}); + const { cards, changeDefaultCard, fetchCards } = useCards({ loader: true, onCardDelete: () => setDeleteMethodPopup(false) }); + + const [addPayoutModal, setAddPayoutModal] = useState(false); + const { dispatch, state } = useContext(AuthContext); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const [payoutMethods, setPayoutMethods] = useState([]); + + async function fetchPayoutMethods() { + globalDispatch({ type: "START_LOADING" }); + try { + const treeSdk = new TreeSDK(); + const result = await treeSdk.getList("payout_method", { join: [], filter: [`deleted_at,is`, `host_id,eq,${state.user}`] }); + if (Array.isArray(result.list)) { + setPayoutMethods(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + async function deletePayoutMethod(id) { + globalDispatch({ type: "START_LOADING" }); + try { + const treeSdk = new TreeSDK(); + await treeSdk.delete("payout_method", id); + fetchPayoutMethods(); + } catch (err) { + tokenExpireError(dispatch, err.message); + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + useEffect(() => { + fetchPayoutMethods(); + }, []); + + return ( + <> +
    +
    +
    +
    +

    Payment Method

    + +
    + {cards.map((card) => ( +
    +
    + +
    +
    +
    + +
    +
    +

    Credit card

    + + Expires: {card.exp_month}/{card.exp_year} + +
    +
  • {card.last4}
  • + +
    + + + +
    + + +
    + + + +
    +
    +
    +
    +
    +
    + ))} +
    + {cards.length == 0 && ( +
    +

    + No cards yet +

    +
    + )} +
    +
    +
    +
    +

    Payout Method

    + +
    +
    Let us know where you'd like us to send your money
    + {payoutMethods.map((card) => ( +
    +
    +
    +
    +

    Routing number

    +

    {card.routing_number}

    +
    +
    +

    Account number

    +

    {card.account_number}

    +
    +
    +

    Account holder name

    +

    {card.account_name}

    +
    +
    +
    + +
    + + + +
    + + +
    + + + +
    +
    +
    +
    +
    +
    + ))} +
    +
    +
    + setAddMethodPopup(false)} + onSuccess={() => fetchCards()} + /> + setAddPayoutModal(false)} + onSuccess={() => fetchPayoutMethods()} + /> + { + setSelectedCard({}); + setDeleteMethodPopup(false); + }} + onSuccess={() => fetchCards()} + card={selectedCard} + /> + + ); +} diff --git a/src/pages/Host/Bookings/AcceptBookingModal.jsx b/src/pages/Host/Bookings/AcceptBookingModal.jsx new file mode 100644 index 0000000..6a8ae2c --- /dev/null +++ b/src/pages/Host/Bookings/AcceptBookingModal.jsx @@ -0,0 +1,148 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment, useState } from "react"; +import { useContext } from "react"; +import moment from "moment"; +import { BOOKING_STATUS } from "@/utils/constants"; +import { LoadingButton } from "@/components/frontend"; +import { monthsMapping } from "@/utils/date-time-utils"; +import { parseJsonSafely } from "@/utils/utils"; +import { GlobalContext } from "@/globalContext"; + +const sdk = new MkdSDK(); +const ctrl = new AbortController(); + +export default function AcceptBookingModal({ modalOpen, closeModal, booking, onSuccess }) { + const { dispatch: authDispatch } = useContext(AuthContext); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const [loading, setLoading] = useState(false); + + async function acceptBooking() { + setLoading(true); + try { + await sdk.callRawAPI("/v2/api/custom/ergo/capture", { booking_id: booking.id, status: 1, stripe_payment_intent_id: booking.stripe_payment_intent_id }, "POST"); + sendAcceptEmailToCustomer( + booking.customer_id, + booking.property_name, + `from ${moment(booking.booking_start_time).format("MM/DD/YYYY")} to ${moment(booking.booking_end_time).format("MM/DD/YYYY")}`, + ); + closeModal(); + onSuccess(); + } catch (err) { + tokenExpireError(authDispatch, err.message); + if (err.name == "AbortError") { + setLoading(false); + return; + } + closeModal(); + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + + setLoading(false); + } + + async function sendAcceptEmailToCustomer(id, space_name, time) { + try { + // get user email and preferences + const result = await sdk.callRawAPI("/v2/api/custom/ergo/get-user", { id }, "POST", ctrl.signal); + + if (parseJsonSafely(result.settings, {}).email_on_booking_accepted == true) { + const tmpl = await sdk.getEmailTemplate("booking-accepted"); + const body = tmpl.html?.replace(new RegExp("{{{space_name}}}", "g"), space_name).replace(new RegExp("{{{time}}}", "g"), time); + + await sdk.sendEmail(result.email, tmpl.subject, body); + } + } catch (err) { } + } + + return ( + + + +
    + + +
    +
    + + + + Are you sure + +
    +

    + You are about to accept a booking from{" "} + + {booking.customer_first_name} {booking.customer_last_name} + {" "} + for {booking.property_name} on{" "} + + {monthsMapping[new Date(booking.booking_start_time).getMonth()] + " " + new Date(booking.booking_start_time).getDate() + "/" + new Date(booking.booking_start_time).getFullYear()} + + . +

    {" "} +
    + +

    + Note: all other bookings within this time slot will be automatically declined +

    + +
    + + + Accept + +
    +
    +
    +
    +
    +
    +
    + ); +} diff --git a/src/pages/Host/Bookings/BookingDeclineModal.jsx b/src/pages/Host/Bookings/BookingDeclineModal.jsx new file mode 100644 index 0000000..8dfaeab --- /dev/null +++ b/src/pages/Host/Bookings/BookingDeclineModal.jsx @@ -0,0 +1,158 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment, useState } from "react"; +import { useContext } from "react"; +import { LoadingButton } from "@/components/frontend"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import moment from "moment"; +import { monthsMapping } from "@/utils/date-time-utils"; +import { BOOKING_STATUS } from "@/utils/constants"; +import { parseJsonSafely } from "@/utils/utils"; + +const sdk = new MkdSDK(); +const ctrl = new AbortController(); + +export default function BookingDeclineModal({ modalOpen, closeModal, onSuccess, booking }) { + const { dispatch: authDispatch } = useContext(AuthContext); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const [loading, setLoading] = useState(false); + + async function onSubmit(e) { + e.preventDefault(); + setLoading(true); + const formData = new FormData(e.target); + const reason = formData.get("reason"); + try { + await sdk.callRawAPI("/v2/api/custom/ergo/capture", { booking_id: booking.id, status: 4, stripe_payment_intent_id: booking.stripe_payment_intent_id }, "POST"); + if (reason) { + sendDeclineEmailToCustomer( + booking.customer_id, + booking.property_name, + `from ${moment(booking.booking_start_time).format("MM/DD/YYYY")} to ${moment(booking.booking_end_time).format("MM/DD/YYYY")}`, + reason, + ); + } + closeModal(); + onSuccess(); + } catch (err) { + tokenExpireError(authDispatch, err.message); + if (err.name == "AbortError") { + setLoading(false); + return; + } + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + + setLoading(false); + } + + async function sendDeclineEmailToCustomer(id, space_name, time, reason) { + try { + // get user email and preferences + const result = await sdk.callRawAPI("/v2/api/custom/ergo/get-user", { id }, "POST", ctrl.signal); + + if (parseJsonSafely(result.settings, {}).email_on_booking_declined == true) { + const tmpl = await sdk.getEmailTemplate("booking-decline"); + const body = tmpl.html?.replace(new RegExp("{{{reason}}}", "g"), reason).replace(new RegExp("{{{space_name}}}", "g"), space_name).replace(new RegExp("{{{time}}}", "g"), time); + + await sdk.sendEmail(result.email, tmpl.subject, body); + } + } catch (err) { } + } + + return ( + + + +
    + + +
    +
    + + + + Are you sure? + +
    +

    + You are about to decline a booking from{" "} + + {booking.customer_first_name} {booking.customer_last_name} + {" "} + for {booking.property_name} on{" "} + + {monthsMapping[new Date(booking.booking_start_time).getMonth()] + " " + new Date(booking.booking_start_time).getDate() + "/" + new Date(booking.booking_start_time).getFullYear()} + + . +

    {" "} +
    + + +
    + + + Proceed + +
    +
    +
    +
    +
    +
    +
    + ); +} diff --git a/src/pages/Host/Bookings/HostBookingCard.jsx b/src/pages/Host/Bookings/HostBookingCard.jsx new file mode 100644 index 0000000..ce7301a --- /dev/null +++ b/src/pages/Host/Bookings/HostBookingCard.jsx @@ -0,0 +1,310 @@ +import React, { useEffect, useState } from "react"; +import Skeleton from "react-loading-skeleton"; +import { Link } from "react-router-dom"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import MkdSDK from "@/utils/MkdSDK"; +import { parseJsonSafely, secondsToHour } from "@/utils/utils"; +import { formatDiff, monthsMapping } from "@/utils/date-time-utils"; +import { useContext } from "react"; +import { GlobalContext } from "@/globalContext"; +import { BOOKING_STATUS, PAYMENT_STATUS } from "@/utils/constants"; +import moment from "moment"; +import { FavoriteButton } from "@/components/frontend"; +import { ClockIcon } from "@heroicons/react/24/outline"; +import BookingDeclineModal from "./BookingDeclineModal"; +import AcceptBookingModal from "./AcceptBookingModal"; + +let sdk = new MkdSDK(); + +export default function HostBookingCard({ tourReview, data, forceRender, favoriteId }) { + const statusMapping = ["Pending", "Upcoming", "Ongoing", "Completed", "Declined", "Canceled", "Expired"]; + const statusColorMapping = ["text-white", "my-text-gradient", "text-[yellow]", "text-[#667085]", "text-[#D92D20]", "text-[#DC6803]", "text-[#D92D20] !bg-[#F2F4F7]"]; + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const [declinePopup, setDeclinePopup] = useState(false); + const [acceptPopup, setAcceptPopup] = useState(false); + const [countdown, setCountdown] = useState({}); + const [imageLoaded, setImageLoaded] = useState(false); + const bookingExpired = data.booking_start_time && data.status < 2 ? new Date(data.booking_end_time) < Date.now() : false; + + async function cancelBooking(id) { + const payload = { + id, + booked_unit: 1, + status: BOOKING_STATUS.CANCELLED, + }; + try { + await callCustomAPI("booking", "post", payload, "PUT"); + if (data.status === BOOKING_STATUS.UPCOMING) { + await sdk.callRawAPI("/v2/api/custom/ergo/refund", { booking_id: data.id, stripe_payment_intent_id: data.stripe_payment_intent_id }, "POST"); + } + sendCancelEmail(data.customer_id, data.property_name, `from ${moment(data.booking_start_time).format("MM/DD/YYYY")} to ${moment(data.booking_end_time).format("MM/DD/YYYY")}`, reason); + if (forceRender) { + forceRender(new Date()); + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function sendCancelEmail(id, space_name, time) { + try { + // get user email and preferences + const result = await callCustomAPI("get-user", "post", { id }, ""); + const tmpl = await sdk.getEmailTemplate("booking-cancelled"); + + if (parseJsonSafely(result.settings, {}).email_on_booking_cancelled == true) { + const body = tmpl.html?.replace(new RegExp("{{{space_name}}}", "g"), space_name).replace(new RegExp("{{{time}}}", "g"), time); + await sdk.sendEmail(result.email, tmpl.subject, body); + } + + if (parseJsonSafely(globalState.user.settings, {}).email_on_booking_cancelled == true) { + const body = tmpl.html?.replace(new RegExp("{{{space_name}}}", "g"), space_name).replace(new RegExp("{{{time}}}", "g"), time); + await sdk.sendEmail(globalState.user.email, tmpl.subject, body); + } + } catch (err) { } + } + + useEffect(() => { + if (data.status != BOOKING_STATUS.UPCOMING && !bookingExpired) return; + let interval = null; + if (!data.booking_start_time) { + return () => clearInterval(interval); + } + interval = setInterval(() => { + const diff = formatDiff(data.booking_start_time); + setCountdown(diff); + }, 1000); + }, [data.booking_start_time]); + + return ( + <> +
    +
    + setImageLoaded(true)} + alt="" + className="absolute top-0 left-0 h-full w-full object-cover" + /> + {data.status == BOOKING_STATUS.UPCOMING && !bookingExpired && ( +
    +
    + : {countdown.timeLeft} + {countdown.format} +
    +
    + )} + +
    + +
    + {!imageLoaded && } +
    +
    +
    +

    {data.property_name || }

    +

    {!data.property_name ? : data.address_line_1}

    +

    {!data.property_name ? : data.address_line_2}

    + {data.property_name ? ( +

    + Guest: {data.customer_first_name} {data.customer_last_name} +

    + ) : ( + + )} +
    +
    + {data.booking_start_time ? ( +
    +

    Date

    + + {monthsMapping[new Date(data.booking_start_time).getMonth()] + " " + new Date(data.booking_start_time).getDate() + "/" + new Date(data.booking_start_time).getFullYear()} + +
    + ) : ( + + )} + {data.duration ? ( +
    +

    Duration

    + {secondsToHour(data.duration)} +
    + ) : ( + + )} + {data.duration ? ( +
    +

    Total Price

    + ${((data?.total ?? 0) + (data?.addon_cost ?? 0)).toFixed(2)} +
    + ) : ( + + )} +
    +
    + + {" "} + {statusMapping[bookingExpired ? 6 : data.status ?? 0]} + + {data.id && ( + + View details + + )} + + {(() => { + if (!bookingExpired && data.status == BOOKING_STATUS.PENDING) { + return ( +
    + {data.status === BOOKING_STATUS.PENDING && + + + }{" "} + |{" "} + +
    + ); + } else if (!bookingExpired && data.payment_status !== PAYMENT_STATUS.SUCCESSFUL) { + return ( + + ); + } + })()} +
    +
    +
    +
    +
    +
    + setImageLoaded(true)} + alt="" + className="absolute top-0 left-0 h-full w-full object-cover" + /> + {!imageLoaded && } + {data.status == BOOKING_STATUS.UPCOMING && !bookingExpired && ( +
    +
    + : {countdown.timeLeft} + {countdown.format} +
    +
    + )} +
    +
    +

    {data.property_name || }

    +

    + {" "} + {!data.property_name ? : ([1, 2].includes(data.status) && !bookingExpired ? data.address_line_1 : data.property_city) ?? "N/A"} +

    +

    + {" "} + {!data.property_name ? : ([1, 2].includes(data.status) && !bookingExpired ? data.address_line_2 : data.property_country) ?? "N/A"} +

    + {data.property_name ? ( +

    + Host: {data.host_first_name} {data.host_last_name} +

    + ) : ( + + )} +
    +
    +
    + + {" "} + {statusMapping[bookingExpired ? 6 : data.status ?? 0]} + + {data.id && ( + + View detail + + )} + + {!bookingExpired && data.status == 0 ? ( +
    + {data.payment_status === PAYMENT_STATUS.PENDING && + + + }{" "} + |{" "} + +
    + ) : ( + (!bookingExpired && data.payment_status !== PAYMENT_STATUS.SUCCESSFUL) && ( + + + ) + )} +
    +
    + setDeclinePopup(false)} + onSuccess={() => forceRender(true)} + booking={data} + /> + setAcceptPopup(false)} + onSuccess={() => forceRender(true)} + booking={data} + /> + + ); +} diff --git a/src/pages/Host/Bookings/HostBookingDetailsPage.jsx b/src/pages/Host/Bookings/HostBookingDetailsPage.jsx new file mode 100644 index 0000000..ca17464 --- /dev/null +++ b/src/pages/Host/Bookings/HostBookingDetailsPage.jsx @@ -0,0 +1,613 @@ +import React from "react"; +import { useEffect } from "react"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { Link, Navigate, useNavigate, useParams } from "react-router-dom"; +import CircleCheckIcon from "@/components/frontend/icons/CircleCheckIcon"; +import DateTimeIcon from "@/components/frontend/icons/DateTimeIcon"; +import PersonIcon from "@/components/frontend/icons/PersonIcon"; +import StarIcon from "@/components/frontend/icons/StarIcon"; +import ThreeDotsMenu from "@/components/frontend/ThreeDotsMenu"; +import Icon from "@/components/Icons"; +import useDelayUnmount from "@/hooks/useDelayUnmount"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import MkdSDK from "@/utils/MkdSDK"; +import { useContext } from "react"; +import { GlobalContext } from "@/globalContext"; +import { daysMapping, monthsMapping, formatAMPM } from "@/utils/date-time-utils"; +import NoteIcon from "@/components/frontend/icons/NoteIcon"; +import FavoriteButton from "@/components/frontend/FavoriteButton"; +import { BOOKING_STATUS, NOTIFICATION_STATUS, NOTIFICATION_TYPE } from "@/utils/constants"; +import { LoadingButton } from "@/components/frontend"; +import { usePropertySpace, usePublicUserData } from "@/hooks/api"; +import useUserCurrentLocation from "@/hooks/api/useUserCurrentLocation"; +import PropertySpaceMapImage from "@/components/frontend/PropertySpaceMapImage"; +import { parseJsonSafely } from "@/utils/utils"; +import moment from "moment"; +import BookingDeclineModal from "./BookingDeclineModal"; +import AcceptBookingModal from "./AcceptBookingModal"; +import { AuthContext } from "@/authContext"; + +const statusMapping = ["Pending", "Upcoming", "Ongoing", "Completed", "Declined", "Canceled", "Expired"]; +const statusColorMapping = ["text-white", "my-text-gradient", "text-[#667085]", "text-[#667085]", "text-[#D92D20]", "text-[#DC6803]", "text-[#D92D20] !bg-[#F2F4F7]"]; + +let sdk = new MkdSDK(); + +export default function HostBookingDetailsPage() { + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const { state } = useContext(AuthContext); + + const navigate = useNavigate(); + const { id } = useParams(); + const [booking, setBooking] = useState({}); + const [addReviewPopup, setAddReviewPopup] = useState(false); + const showAddReviewPopup = useDelayUnmount(addReviewPopup, 100); + const [checkedCount, setCheckedCount] = useState(0); + const [availableHashtags, setAvailableHashtags] = useState([{ name: "Test", id: 12 }]); + const [render, forceRender] = useState(false); + const [loading, setLoading] = useState(false); + const [showMap, setShowMap] = useState(false); + + const bookingExpired = booking.booking_start_time && booking.status < BOOKING_STATUS.ONGOING ? new Date(booking.booking_start_time) < Date.now() : false; + + const { register, handleSubmit, watch, reset } = useForm(); + const ratingVal = watch("rating"); + const hostRatingVal = watch("host_rating"); + const hashtags = watch("hashtags", []); + + const [declinePopup, setDeclinePopup] = useState(false); + const [acceptPopup, setAcceptPopup] = useState(false); + + useEffect(() => { + if (Array.isArray(hashtags)) { + setCheckedCount(hashtags?.filter(Boolean).length); + } + }, [hashtags]); + + const otherUserData = usePublicUserData(booking.customer_id); + const { propertySpace } = usePropertySpace(booking.property_space_id, render); + + async function addHashTagToReview(hashtags, reviewId) { + try { + sdk.setTable("review_hashtag"); + hashtags.map((hashtag) => + sdk.callRestAPI( + { + hashtag_id: hashtag, + review_id: reviewId, + }, + "POST", + ), + ); + await Promise.all(hashtags); + } catch (error) { + console.log("Error", error); + } + } + + const onSubmit = async (data) => { + console.log("submitting", data); + setLoading(true); + let newReview = { + customer_id: booking.customer_id, + host_id: booking.host_id, + property_spaces_id: booking.property_space_id, + booking_id: booking.id, + comment: data.comment, + customer_rating: data.rating, + host_rating: null, + space_rating: null, + post_date: new Date().toISOString(), + given_by: "host", + received_by: "customer", + }; + try { + const result = await callCustomAPI("review", "post", newReview, "POST"); + await addHashTagToReview(data.hashtags, result.message); + + // create notification + sdk.setTable("notification"); + await sdk.callRestAPI( + { + user_id: Number(localStorage.getItem("user")), + actor_id: null, + action_id: result.message, + notification_time: new Date().toISOString().split(".")[0], + message: "New Review Added", + type: NOTIFICATION_TYPE.ADD_REVIEW, + status: NOTIFICATION_STATUS.NOT_ADDRESSED, + }, + "POST", + ); + setLoading(false); + + setAddReviewPopup(false); + + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Success", + message: "Review added successful", + btn: "Ok got it", + }, + }); + reset(); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + setLoading(false); + }; + + async function fetchBooking(booking_id) { + globalDispatch({ type: "START_LOADING" }); + const where = [`ergo_booking.id = ${booking_id}`]; + try { + const result = await callCustomAPI("booking/details", "post", { where }, ""); + setBooking(result.list ?? {}); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + async function fetchHashtags() { + try { + const result = await callCustomAPI( + "hashtag", + "post", + { + page: 1, + limit: 1000, + }, + "PAGINATE", + ); + if (Array.isArray(result.list)) { + setAvailableHashtags(result.list); + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function cancelBooking(id) { + const payload = { + id, + booked_unit: 1, + status: BOOKING_STATUS.CANCELLED, + }; + try { + await callCustomAPI("booking", "post", payload, "PUT"); + if (data.status === BOOKING_STATUS.UPCOMING) { + await sdk.callRawAPI("/v2/api/custom/ergo/refund", { booking_id: data.id, stripe_payment_intent_id: data.stripe_payment_intent_id }, "POST"); + } + sendCancelEmail( + booking.customer_id, + booking.property_name, + `from ${moment(booking.booking_start_time).format("MM/DD/YYYY")} to ${moment(booking.booking_end_time).format("MM/DD/YYYY")}`, + reason, + ); + fetchBooking(id); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function sendCancelEmail(id, space_name, time) { + try { + // get user email and preferences + const result = await callCustomAPI("get-user", "post", { id }, ""); + const tmpl = await sdk.getEmailTemplate("booking-cancelled"); + + if (parseJsonSafely(result.settings, {}).email_on_booking_cancelled == true) { + const body = tmpl.html?.replace(new RegExp("{{{space_name}}}", "g"), space_name).replace(new RegExp("{{{time}}}", "g"), time); + await sdk.sendEmail(result.email, tmpl.subject, body); + } + + if (parseJsonSafely(globalState.user.settings, {}).email_on_booking_cancelled == true) { + const body = tmpl.html?.replace(new RegExp("{{{space_name}}}", "g"), space_name).replace(new RegExp("{{{time}}}", "g"), time); + await sdk.sendEmail(globalState.user.email, tmpl.subject, body); + } + } catch (err) { } + } + + useEffect(() => { + (async () => { + await fetchBooking(id); + fetchHashtags(); + })(); + }, []); + + const { latitude, longitude, done } = useUserCurrentLocation(); + + return ( +
    +
    + +
    +
    +

    Booking - {booking.property_name}

    +
    +

    Status

    + + {" "} + {statusMapping[bookingExpired ? 6 : booking.status ?? 0]} + +
    +
    + , + onClick: () => cancelBooking(booking.id), + notShow: booking.status == BOOKING_STATUS.COMPLETED || booking.status == BOOKING_STATUS.PENDING || booking.status == BOOKING_STATUS.CANCELLED || bookingExpired, + }, + { + label: "Accept booking", + icon: <>, + onClick: () => setAcceptPopup(true), + notShow: booking.status !== BOOKING_STATUS.PENDING || bookingExpired, + }, + { + label: "Decline booking", + icon: <>, + onClick: () => setDeclinePopup(true), + notShow: booking.status !== BOOKING_STATUS.PENDING || bookingExpired, + }, + { + label: "Review customer", + icon: <>, + onClick: () => setAddReviewPopup(true), + notShow: booking.status != BOOKING_STATUS.COMPLETED || bookingExpired, + }, + ]} + /> +
    +
    +
    +
    +
    +
    +
    +
    + +
    +

    What's next

    +

    + You booking has been sent to the host and will be reviewed within 2 hours. If you don’t hear form the host withing 2h you can cancel your booking or reach out to them via{" "} + + Messages + {" "} +

    +
    +
    +
    +
  • Booking Time
  • +
    +
    +

    {monthsMapping[new Date(booking.booking_start_time).getMonth()] ?? "N/A"}

    + {new Date(booking.booking_start_time).getDate() || "N/A"} +

    {daysMapping[new Date(booking.booking_start_time).getDay()] ?? "N/A"}

    +
    +
    +
    + +

    From

    + {formatAMPM(booking.booking_start_time ?? "01/01/01")}{" "} +
    +
    + +

    Until

    + {formatAMPM(booking.booking_end_time ?? "01/01/01")} +
    +
    +
    +
    +
  • Space:
  • + {booking.status == BOOKING_STATUS.COMPLETED ? ( + + ) : null} +
    +
    +
    + +
    +
    +
    +

    {booking.property_name}

    +

    + {[BOOKING_STATUS.UPCOMING, BOOKING_STATUS.ONGOING].includes(booking.status) ? propertySpace.address_line_1 : propertySpace.city} +

    +

    + {[BOOKING_STATUS.UPCOMING, BOOKING_STATUS.ONGOING].includes(booking.status) ? propertySpace.address_line_2 : propertySpace.country} +

    +
    +

    + from: ${(Number(booking.hourly_rate) || 0).toFixed(2)}/day +

    +
    + + {propertySpace.max_capacity} +
    +
    +
    +
    +

    + + + {(Number(propertySpace.average_space_rating) || 0).toFixed(1)} + ({propertySpace.space_rating_count}) + +

    + {[BOOKING_STATUS.COMPLETED, BOOKING_STATUS.ONGOING, BOOKING_STATUS.UPCOMING].includes(booking.status) ? ( + + (view on map) + + ) : ( + + )} +
    +
    +
    + {booking.add_ons !== undefined && booking?.add_ons?.length > 0 && +
  • Add-ons:
  • + } +
    + {(booking.add_ons ?? []).map((addon, idx) => ( +
    + +

    {addon.name}

    +
    + ))} +
    +
    +
    +
    +

    Your Guest

    +
    +
    + +

    {(booking.customer_first_name ?? "") + " " + (booking.customer_last_name ?? "")}

    +
    + + Chat with customer + +
    +
    + + {state.role == "customer" && +
    +

    Charges

    +

    (You will not be charged until the host accepts your booking)

    +
    +

    Rate

    +

    ${(booking.hourly_rate || 0).toFixed(2)}

    +
    +
    +

    Price

    +

    ${(booking.hourly_rate * (booking.duration / 3600) || 0).toFixed(2)}

    +
    + {(booking.add_ons ?? []).map((addon) => ( +
    +

    {addon.name}

    +

    ${(addon.cost || 0).toFixed(2)}

    +
    + ))} + +
    +

    Tax

    +

    ${(booking.tax || 0).toFixed(2)}

    +
    +
    +

    Total

    +

    ${(booking.total || 0).toFixed(2)}

    +
    +
    + } + + Cancellation policy + +
    +
    + {showAddReviewPopup && ( +
    setAddReviewPopup(false)} + > +
    e.stopPropagation()} + onSubmit={handleSubmit(onSubmit)} + > +
    +

    Leave a review

    + +
    +

    Leave a review to let others know about your experience with the space and the customer.

    +
    +

    Select rating

    +
    + {[1, 2, 3, 4, 5].map((val) => ( + + ))} +
    +

    + Select Hashtags (max 3) +

    +
    + {availableHashtags.map((hash, i) => ( +
    + = 3 && !hashtags.includes(hash.id.toString())} + value={hash.id} + /> + +
    + ))} +
    +

    Comment

    + + + Submit + +
    + +
    + )} + setShowMap(false)} + /> + setDeclinePopup(false)} + onSuccess={() => fetchBooking(id)} + booking={booking} + /> + setAcceptPopup(false)} + onSuccess={() => fetchBooking(id)} + booking={booking} + /> +
    + ); +} diff --git a/src/pages/Host/Bookings/HostBookingFiltersModal.jsx b/src/pages/Host/Bookings/HostBookingFiltersModal.jsx new file mode 100644 index 0000000..309cff4 --- /dev/null +++ b/src/pages/Host/Bookings/HostBookingFiltersModal.jsx @@ -0,0 +1,198 @@ +import DatePickerV3 from "@/components/DatePickerV3"; +import { isValidDate, parseSearchParams } from "@/utils/utils"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment } from "react"; +import { useForm } from "react-hook-form"; +import { useSearchParams } from "react-router-dom"; + +const statuses = [ + { label: "Pending", value: 0 }, + { label: "Upcoming", value: 1 }, + { label: "Ongoing", value: 2 }, + { label: "Completed", value: 3 }, + { label: "Declined", value: 4 }, + { label: "Expired", value: "expired" }, +]; + +export default function HostBookingFiltersModal({ modalOpen, closeModal }) { + const [searchParams, setSearchParams] = useSearchParams(); + const { handleSubmit, register, watch, reset, setValue, control, formState, resetField } = useForm({ + defaultValues: (() => { + const params = parseSearchParams(searchParams); + return { + guest_name: params.guest_name ?? "", + from: isValidDate(params.from ?? "") ? new Date(params.from) : new Date(), + to: isValidDate(params.to ?? "") ? new Date(params.to) : new Date(), + space_name: params.space_name ?? "", + status: params.status ?? "", + id: params.id ?? "", + direction: "DESC", + }; + })(), + }); + + const { dirtyFields } = formState; + + const fromDate = watch("from"); + + const onSubmit = async (data) => { + console.log("submitting ", data); + searchParams.set("id", data.id); + searchParams.set("guest_name", data.guest_name); + searchParams.set("space_name", data.space_name); + searchParams.set("status", data.status); + searchParams.set("from", dirtyFields?.from ? data.from.toISOString().split("T")[0] : ""); + searchParams.set("to", dirtyFields?.to ? data.to.toISOString().split("T")[0] : ""); + setSearchParams(searchParams); + closeModal(); + }; + + return ( + + + +
    + + +
    +
    + + +
    +
    + + Filters + + +
    + {" "} +
    +
    +
    + +
    +
    + resetField("from", { keepDirty: false, keepTouched: false })} + setValue={(val) => setValue("from", val, { shouldDirty: true })} + control={control} + name="from" + labelClassName="justify-between flex-grow flex-row-reverse" + placeholder="From" + min={new Date("2001-01-01")} + /> +
    +
    + resetField("to", { keepDirty: false, keepTouched: false })} + setValue={(val) => setValue("to", val, { shouldDirty: true })} + control={control} + name="to" + labelClassName="justify-between flex-grow flex-row-reverse" + placeholder="To" + min={fromDate} + /> +
    +
    + + + +
    + +
    +
    +
    +
    +
    +
    + ); +} diff --git a/src/pages/Host/Bookings/HostBookingListPage.jsx b/src/pages/Host/Bookings/HostBookingListPage.jsx new file mode 100644 index 0000000..bd04cfe --- /dev/null +++ b/src/pages/Host/Bookings/HostBookingListPage.jsx @@ -0,0 +1,315 @@ +import React, { useContext, useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { GlobalContext } from "@/globalContext"; +import { useSearchParams } from "react-router-dom"; +import InfiniteScroll from "react-infinite-scroll-component"; +import NoteIcon from "@/components/frontend/icons/NoteIcon"; +import { isValidDate, parseSearchParams } from "@/utils/utils"; +import MkdSDK from "@/utils/MkdSDK"; +import HostBookingCard from "./HostBookingCard"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import { AdjustmentsHorizontalIcon } from "@heroicons/react/24/solid"; +import CustomSelectV2 from "@/components/CustomSelectV2"; +import DatePickerV3 from "@/components/DatePickerV3"; +import HostBookingFiltersModal from "./HostBookingFiltersModal"; + +const sdk = new MkdSDK(); +const ctrl = new AbortController(); + +export default function HostBookingListPage() { + const FETCH_PER_SCROLL = 12; + + const [showFilter, setShowFilter] = useState(false); + const [bookings, setBookings] = useState([]); + const [render, forceRender] = useState(false); + const [searchParams, setSearchParams] = useSearchParams(); + + const [bookingsTotal, setBookingsTotal] = useState(100); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const { dispatch } = useContext(AuthContext); + const [favoriteStatuses, setFavoriteStatuses] = useState([]); + const user_id = +localStorage.getItem("user") || 0; + + const { handleSubmit, register, watch, reset, setValue, control, formState, resetField } = useForm({ + defaultValues: (() => { + const params = parseSearchParams(searchParams); + return { + guest_name: params.guest_name ?? "", + from: isValidDate(params.from ?? "") ? new Date(params.from) : new Date(), + to: isValidDate(params.to ?? "") ? new Date(params.to) : new Date(), + space_name: params.space_name ?? "", + status: params.status ?? "", + id: params.id ?? "", + direction: "DESC", + }; + })(), + }); + + const { dirtyFields } = formState; + + const direction = watch("direction"); + const fromDate = watch("from"); + + const onSubmit = async (data) => { + if (window.innerWidth < 700) { + setShowFilter(false); + } + console.log("submitting", data); + setBookings([]); + searchParams.set("id", data.id); + searchParams.set("guest_name", data.guest_name); + searchParams.set("status", data.status); + searchParams.set("from", dirtyFields?.from ? data.from.toISOString().split("T")[0] : ""); + searchParams.set("to", dirtyFields?.to ? data.to.toISOString().split("T")[0] : ""); + setSearchParams(searchParams); + }; + + async function fetchMyBookings(page) { + // only add empty spaces if there's no empty card i.e we are not currently fetching + if (bookings.every((bk) => Object.keys(bk).length > 0)) { + setBookings((prev) => { + const amountToFetch = bookingsTotal - prev.length > FETCH_PER_SCROLL ? FETCH_PER_SCROLL : Math.abs(bookingsTotal - prev.length - FETCH_PER_SCROLL); + return [...prev, ...Array(amountToFetch).fill({})]; + }); + } + const filters = parseSearchParams(searchParams); + + var where = [`ergo_booking.host_id = ${user_id}`]; + + if (filters.guest_name) { + where.push(`(ergo_user.first_name LIKE '%${filters.guest_name}%' OR ergo_user.last_name LIKE '%${filters.guest_name}%'`); + } + + if (filters.from) { + where.push(`ergo_booking.booking_start_time >= date('${filters.from}')`); + } + + if (filters.to) { + where.push(`ergo_booking.booking_end_time <= date('${filters.to}')`); + } + + if (filters.space_name) { + where.push(`ergo_property.name LIKE '%${filters.space_name}%'`); + } + + if (filters.status) { + if (filters.status == "expired") { + where.push(`ergo_booking.booking_start_time < date('${new Date().toISOString()}')`); + } else { + where.push(`ergo_booking.status = ${filters.status}`); + } + } + + if (filters.id) { + where = [`ergo_booking.host_id = ${user_id} AND ergo_booking.id = ${filters.id}`]; + } + + try { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/booking/PAGINATE", { page: page ?? 1, limit: FETCH_PER_SCROLL, where, sortId: "update_at", direction: "DESC" }, "POST", ctrl.signal); + if (Array.isArray(result.list)) { + setBookings((prev) => { + return [...prev.filter((item) => Object.keys(item).length > 0), ...result.list].filter((v, i, a) => a.findIndex((v2) => v2.id === v.id) === i); + }); + setBookingsTotal(result.total); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function fetchFavoriteStatuses() { + const payload = { user_id: `${user_id}` }; + sdk.setTable("user_property_spaces"); + try { + const result = await sdk.callRestAPI({ payload }, "GETALL", ctrl.signal); + if (Array.isArray(result.list)) { + setFavoriteStatuses(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") { + globalDispatch({ type: "STOP_LOADING" }); + return; + } + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + useEffect(() => { + fetchMyBookings(); + }, [searchParams]); + + useEffect(() => { + if (render) { + fetchFavoriteStatuses(); + setBookings([]); + fetchMyBookings(1); + } + }, [render]); + + + useEffect(() => { + fetchFavoriteStatuses(); + }, []); + + const sortByDate = (a, b) => { + if (direction == "DESC") { + return new Date(b.id) - new Date(a.id); + } + return new Date(a.id) - new Date(b.id); + }; + + return ( +
    +
    +
    +
    + + +
    + + +
    + {bookings.length == 0 && ( +
    +

    + You have no bookings +

    +
    + )} + { + fetchMyBookings(Math.round(bookings.length / FETCH_PER_SCROLL + 1)); + }} + scrollThreshold={0.9} + hasMore={bookings.length < bookingsTotal} + loader={<>} + endMessage={ + bookings.length > 10 && ( +

    + +

    + ) + } + > + {bookings.sort(sortByDate).map((book, i) => ( + fav.property_spaces_id == book.property_space_id)?.id ?? null} + /> + ))} +
    + setShowFilter(false)} + /> +
    + ); +} diff --git a/src/pages/Host/HostAccountHeader.jsx b/src/pages/Host/HostAccountHeader.jsx new file mode 100644 index 0000000..922a7e4 --- /dev/null +++ b/src/pages/Host/HostAccountHeader.jsx @@ -0,0 +1,71 @@ +import React from "react"; +import { Outlet, useLocation } from "react-router"; +import { Link } from "react-router-dom"; +import WelcomeIcon from "@/components/frontend/icons/WelcomeIcon"; +import { useContext } from "react"; +import { GlobalContext } from "@/globalContext"; +import NavBarSlider from "@/components/frontend/NavBarSlider"; +import { ID_VERIFICATION_STATUSES } from "@/utils/constants"; +import { useState } from "react"; +import { useEffect } from "react"; + +export default function HostAccountHeader() { + const { pathname } = useLocation(); + const { state: globalState } = useContext(GlobalContext); + + const toastBlacklist = ["/account/verification", "/account/my-bookings/", "/account/my-spaces/"]; + const menuBlacklist = ["/account/verification", "/account/my-bookings/", "/account/my-spaces/"]; + + const [toastOpen, setToastOpen] = useState(false); + + useEffect(() => { + // TODO: fix this so it only shows toast on first login + setToastOpen(globalState.user.getting_started == 0); + }, [globalState.user.getting_started]); + + return ( +
    +
    +
    +
    +

    + + Welcome to Ergo +

    + +
    +

    + {true ? ( + <> + {" "} + This is your host panel, where you can handle all things related to your spaces and bookings. NOTE: Before publishing space(s) you need to complete your{" "} + + ’Profile’ + {" "} + and get verified. + + ) : globalState.user.verificationStatus != ID_VERIFICATION_STATUSES.VERIFIED ? ( + <> + Before publishing space(s) you need to complete your{" "} + + ’Profile’ + {" "} + and get verified. + + ) : ( + <> + )} +

    +
    + {menuBlacklist.every((path) => !pathname.startsWith(path)) && } +
    + +
    + ); +} diff --git a/src/pages/Host/Payments/HostPaymentsPage.jsx b/src/pages/Host/Payments/HostPaymentsPage.jsx new file mode 100644 index 0000000..0eb5fe6 --- /dev/null +++ b/src/pages/Host/Payments/HostPaymentsPage.jsx @@ -0,0 +1,485 @@ +import React, { useEffect, useRef } from "react"; +import { useState } from "react"; +import { createSearchParams, Link, useNavigate, useOutletContext, useSearchParams } from "react-router-dom"; +import PaginationBar from "@/components/PaginationBar"; +import PaginationHeader from "@/components/PaginationHeader"; +import useDelayUnmount from "@/hooks/useDelayUnmount"; +import CustomSelect from "@/components/frontend/CustomSelect"; +import { useContext } from "react"; +import { GlobalContext } from "@/globalContext"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import { formatDate, isSameDay, monthsMapping } from "@/utils/date-time-utils"; +import { useForm } from "react-hook-form"; +import { formatDate2, isValidDate, parseSearchParams } from "@/utils/utils"; +import DatePicker from "@/components/frontend/DatePicker"; +import DownloadIcon from "@/components/frontend/icons/DownloadIcon"; +import moment from "moment"; +import CsvDownloadButton from "react-json-to-csv"; +import DatePickerV2 from "@/components/frontend/DatePickerV2"; + +const columns = [ + { + header: "BOOKING DATE", + accessor: "booking_start_time", + }, + { + header: "SPACE", + accessor: "space", + }, + { + header: "PAYMENT METHOD", + accessor: "payment_method", + }, + { + header: "AMOUNT", + accessor: "amount", + }, + { + header: "RECEIPT", + accessor: "receipt", + }, + { + header: "ACTION", + accessor: "", + }, +]; +const HostPaymentsPage = () => { + const [viewPaymentPopup, setViewPaymentPopup] = useState(false); + const showViewPaymentPopup = useDelayUnmount(viewPaymentPopup, 300); + const role = localStorage.getItem("role"); + const [searchParams, setSearchParams] = useSearchParams(); + const [pageSize, setPageSize] = useState(10); + const [pageCount, setPageCount] = useState(0); + const [dataTotal, setDataTotal] = useState(0); + const [currentPage, setPage] = useState(0); + const [canPreviousPage, setCanPreviousPage] = useState(false); + const [canNextPage, setCanNextPage] = useState(false); + const [direction, setDirection] = useState("DESC"); + const [rows, setRows] = useState([]); + const [mySpaces, setMySpaces] = useState([]); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const [selectedPayment, setSelectedPayment] = useState({}); + + const initialSearchDate = useRef(new Date()); + const [fromDate, setFromDate] = useState(initialSearchDate.current); + const [toDate, setToDate] = useState(initialSearchDate.current); + + const { handleSubmit, register, reset, setValue, control, dirtyFields } = useForm({ + defaultValues: (() => { + const params = parseSearchParams(searchParams); + return { + from: "", + to: "", + } + })(), + }); + + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + async function getData(pageNum, pageSize) { + globalDispatch({ type: "START_LOADING" }); + const data = parseSearchParams(searchParams); + const user_id = localStorage.getItem("user"); + + var start = data?.from; + var end = data?.to; + + const where = [ + `${role == "host" ? `ergo_booking.host_id = ${user_id}` : `ergo_booking.customer_id = ${user_id}`} + AND ${data.space_id ? `ergo_property_spaces.id = ${data.space_id}` : "1"} + AND ergo_booking.status = 3 ${start ? `AND ergo_booking.booking_start_time BETWEEN '${start}' AND '${end}'` : ""}`, + ]; + try { + const result = await callCustomAPI( + "booking", + "post", + { + where, + page: pageNum, + limit: pageSize, + sortId: "update_at", + direction: "DESC", + }, + "PAGINATE", + ); + + const { list, total, limit, num_pages, page } = result; + setRows(list); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + async function fetchMySpaces() { + let user_id = localStorage.getItem("user"); + let where = [`ergo_property.host_id = ${user_id}`]; + try { + const result = await callCustomAPI("popular", "post", { page: 1, limit: 1000, user_id, where, all: true, sortId: "update_at", direction: "DESC" }, "PAGINATE"); + if (Array.isArray(result.list)) { + setMySpaces(result.list); + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + const onSubmit = async (data) => { + console.log(data) + const formatFrom = formatDate2(data.from) + const formatTo = formatDate2(data.to) + + searchParams.set("from", data.from ? formatFrom : ""); + searchParams.set("to", data.to ? formatTo : new Date().toISOString().split("T")[0]); + searchParams.set("status", data.status); + searchParams.set("space_id", data.space_id); + setSearchParams(searchParams); + getData(1, pageSize); + }; + + useEffect(() => { + getData(1, pageSize); + fetchMySpaces(); + }, []); + + const sortByDate = (a, b) => { + if (direction == "DESC") { + return new Date(b.update_at) - new Date(a.update_at); + } + return new Date(a.update_at) - new Date(b.update_at); + }; + + function handlePrint() { + var myWindow = window.open("", "PRINT", "height=400,width=600"); + + myWindow.document.write("" + document.title + ""); + myWindow.document.write(""); + myWindow.document.write("

    " + document.title + "

    "); + myWindow.document.write(document.getElementById("receipt").innerHTML); + myWindow.document.write(""); + + myWindow.document.close(); // necessary for IE >= 10 + myWindow.focus(); // necessary for IE >= 10*/ + + myWindow.print(); + // myWindow.close(); + } + + return ( +
    +
    +
    +
    + +
    + resetField("from", { keepDirty: false, keepTouched: false })} + setValue={(val) => setValue("from", val, { shouldDirty: true })} + control={control} + name="from" + labelClassName="justify-between flex-grow flex-row-reverse" + placeholder="Start" + type="space" + min={new Date("2001-01-01")} + /> +
    + +
    + resetField("to", { keepDirty: false, keepTouched: false })} + setValue={(val) => setValue("to", val, { shouldDirty: true })} + control={control} + name="to" + labelClassName="justify-between flex-grow flex-row-reverse" + placeholder="End" + type="space" + min={new Date("2001-01-01")} + /> +
    + + +
    + +
    + + Export CSV + +
    +
    + + +
    +
    +

    + Total Paid Out: ${rows.reduce((acc, curr) => acc + (curr.total + curr.addon_cost), 0).toFixed(2)} +

    + +
    +
    + +
    +
    + + + {columns.map((column, index) => ( + + ))} + + + + {rows.sort(sortByDate).map((row, i) => { + return ( + + {columns.map((cell, index) => { + if (cell.accessor === "") { + return ( + + ); + } + if (cell.accessor == "booking_start_time") { + var date = new Date(row[cell.accessor]); + return ( + + ); + } + if (cell.accessor == "space") { + var date = new Date(row[cell.accessor]); + return ( + + ); + } + if (cell.accessor == "amount") { + var date = new Date(row[cell.accessor]); + return ( + + ); + } + if (cell.accessor == "receipt") { + return ( + + ); + } + if (cell.accessor == "payment_method") { + return ( + + ); + } + return ( + + ); + })} + + ); + })} + +
    + {column.header} +
    + + + {monthsMapping[date.getMonth()] + " " + date.getDate() + "/" + date.getFullYear()} + + {row.property_name + " " + row.space_category} + + {"$" + (row.total + row.addon_cost).toFixed(2)} + + {row.id} + + Credit Card + + {row[cell.accessor]} +
    +
    + + + {showViewPaymentPopup && ( +
    setViewPaymentPopup(false)} + > +
    e.stopPropagation()} + id="receipt" + > +
    +

    Payment details

    + +
    +
    +

    + Booking Started on: {formatDate(selectedPayment.booking_start_time)} +

    +

    + Space name: {selectedPayment.property_name} +

    +

    + Booking: #{selectedPayment.id}{" "} + + (View booking details) + +

    +

    + Space: #{selectedPayment.property_space_id}{" "} + + (View booking details) + +

    +

    + Total cost: ${selectedPayment.total?.toFixed(2)} +

    +

    + Total addon cost: ${selectedPayment.addon_cost?.toFixed(2)} +

    + + +
    +
    + )} +
    + ); +}; + +export default HostPaymentsPage; diff --git a/src/pages/Host/Profile/HostProfilePage.jsx b/src/pages/Host/Profile/HostProfilePage.jsx new file mode 100644 index 0000000..470ddc3 --- /dev/null +++ b/src/pages/Host/Profile/HostProfilePage.jsx @@ -0,0 +1,304 @@ +import React, { useContext } from "react"; +import { useState } from "react"; +import { Link } from "react-router-dom"; +import NotVerifiedIcon from "@/components/frontend/icons/NotVerifiedIcon"; +import PencilIcon from "@/components/frontend/icons/PencilIcon"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import Skeleton from "react-loading-skeleton"; +import { formatDate } from "@/utils/date-time-utils"; +import { IMAGE_STATUS, NOTIFICATION_STATUS, NOTIFICATION_TYPE } from "@/utils/constants"; +import SwitchBulkMode from "@/components/SwitchBulkMode"; +import TwoFaDialog from "@/components/Profile/TwoFaDialog"; +import EditProfileModal from "@/components/Profile/EditProfileModal"; +import EditLocationModal from "@/components/Profile/EditLocationModal"; +import EditPasswordModal from "@/components/Profile/EditPasswordModal"; +import EditAboutModal from "@/components/Profile/EditAboutModal"; +import { parseJsonSafely } from "@/utils/utils"; +import EnableEmailDialog from "@/components/Profile/EnableEmailDialog"; +import DeleteAccountModal from "@/components/Profile/DeleteAccountModal"; + +function getProfilePhotoMessage(image_status) { + switch (image_status) { + case IMAGE_STATUS.IN_REVIEW: + return "We are currently reviewing your profile picture"; + case IMAGE_STATUS.APPROVED: + return "This will be displayed on your profile"; + case IMAGE_STATUS.NOT_APPROVED: + return "The image you uploaded was rejected after reviewing, please change it"; + default: + return "Please upload a profile picture"; + } +} + +export default function HostProfilePage() { + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const [loading, setLoading] = useState(false); + const [twoFa, setTwoFa] = useState(false); + const [twoFaDialog, setTwoFaDialog] = useState(false); + const [enableEmailDialog, setEnableEmailDialog] = useState(false); + + const [updatePassword, setUpdatePassword] = useState(false); + + const [updateName, setUpdateName] = useState(false); + + const [updateAbout, setUpdateAbout] = useState(false); + + const [updateLocation, setUpdateLocation] = useState(false); + + const [deleteAccountModal, setDeleteAccountModal] = useState(false); + + let sdk = new MkdSDK(); + + const changeProfilePic = async (e) => { + globalDispatch({ type: "START_LOADING" }); + const file = e.target.files; + const formData = new FormData(); + for (let i = 0; i < file.length; i++) { + formData.append("file", file[i]); + } + try { + const upload = await sdk.uploadImage(formData); + console.log("upload", upload); + sdk.setTable("user"); + const result = await callCustomAPI( + "edit-self", + "post", + { + user: { + photo: upload.url, + is_photo_approved: IMAGE_STATUS.IN_REVIEW, + }, + }, + "", + ); + globalDispatch({ type: "SET_USER_DATA", payload: { ...globalState.user, photo: upload.url, is_photo_approved: IMAGE_STATUS.IN_REVIEW } }); + // create notification + sdk.setTable("notification"); + await sdk.callRestAPI( + { + user_id: globalState.user.id, + actor_id: null, + action_id: globalState.user.id, + notification_time: new Date().toISOString().split(".")[0], + message: "Profile Picture Edited", + type: NOTIFICATION_TYPE.EDIT_USER_PICTURE, + status: NOTIFICATION_STATUS.NOT_ADDRESSED, + }, + "POST", + ); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + }; + + const removeProfilePic = async (e) => { + try { + sdk.setTable("user"); + await callCustomAPI( + "edit-self", + "post", + { + user: { + photo: null, + is_photo_approved: null, + }, + }, + "", + ); + globalDispatch({ type: "SET_USER_DATA", payload: { ...globalState.user, photo: null, is_photo_approved: null } }); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + }; + + async function changeTwoFa() { + setLoading(true); + try { + await callCustomAPI( + "edit-self", + "post", + { + user: { + two_factor_authentication: twoFa != 1 ? 1 : 0, + }, + }, + "", + ); + setTwoFaDialog(false); + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { + heading: "Success", + message: `Two factor Authentication ${twoFa == 1 ? "disabled" : "enabled"}`, + btn: "Ok got it", + }, + }); + setTwoFa((prev) => (prev == 1 ? 0 : 1)); + } catch (err) { + setTwoFaDialog(false); + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + setLoading(false); + } + + return ( +
    +
    +
    +
    +

    Your photo

    + {getProfilePhotoMessage(globalState.user.is_photo_approved)} +
    +
    + +
    + + +
    +
    +
    +
    +

    Profile status

    +
    + {![0, 1].includes(globalState.user.verificationStatus) && ( + + Get verified + + )} + + +
    +
    +
    + +
    +
    + + Manage Property Rules Template + + + Add Property Rules Template + +
    + setUpdateName(false)} + modalOpen={updateName} + /> + setUpdatePassword(false)} + modalOpen={updatePassword} + /> + setUpdateAbout(false)} + modalOpen={updateAbout} + /> + setUpdateLocation(false)} + modalOpen={updateLocation} + /> + + setTwoFaDialog(false)} + isEnabled={twoFa} + onProceed={changeTwoFa} + loading={loading} + /> + setEnableEmailDialog(false)} + /> +
    + ); +} diff --git a/src/pages/Host/PropertyRulesTemplate/CreatePropertyRulesTemplatePage.jsx b/src/pages/Host/PropertyRulesTemplate/CreatePropertyRulesTemplatePage.jsx new file mode 100644 index 0000000..7477a8e --- /dev/null +++ b/src/pages/Host/PropertyRulesTemplate/CreatePropertyRulesTemplatePage.jsx @@ -0,0 +1,102 @@ +import React, { useContext } from "react"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { useForm } from "react-hook-form"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import { ArrowLeftIcon } from "@heroicons/react/24/outline"; +import { Link, useNavigate } from "react-router-dom"; +import MkdSDK from "@/utils/MkdSDK"; + +const sdk = new MkdSDK(); + +export default function CreatePropertyRuleTemplatePage() { + const { dispatch, state } = useContext(AuthContext); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const navigate = useNavigate(); + + const schema = yup.object({ + template_name: yup.string().required("This field is required"), + template: yup.string().required("This field is required"), + }); + const { + register, + handleSubmit, + + formState: { errors, isSubmitting, isValidating }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { template: "", template_name: "" }, + mode: "all", + }); + + async function onSubmit(data) { + console.log("submitting", data); + sdk.setTable("property_space_rule_template"); + try { + await sdk.callRestAPI({ template_name: data.template_name, template: JSON.stringify({ paragraph: data.template }), host_id: state.user }, "POST"); + globalDispatch({ type: "SHOW_CONFIRMATION", payload: { heading: "Successful", message: "Template created successfully", btn: "Back to profile", onClose: () => navigate("/account/profile") } }); + } catch (err) { + tokenExpireError(dispatch, err.message); + globalDispatch({ type: "SHOW_ERROR", payload: { heading: "Failed to create template", message: err.message } }); + } + } + + return ( +
    +
    + + + Back + +
    +
    +

    Create Rules Template

    +
    + + +
    + +
    + + +
    +
    + +
    +
    + ); +} diff --git a/src/pages/Host/PropertyRulesTemplate/EditPropertyRulesTemplatePage.jsx b/src/pages/Host/PropertyRulesTemplate/EditPropertyRulesTemplatePage.jsx new file mode 100644 index 0000000..1fd4d85 --- /dev/null +++ b/src/pages/Host/PropertyRulesTemplate/EditPropertyRulesTemplatePage.jsx @@ -0,0 +1,124 @@ +import React, { useContext, useEffect } from "react"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { useForm } from "react-hook-form"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import { ArrowLeftIcon } from "@heroicons/react/24/outline"; +import { Link, useNavigate, useParams } from "react-router-dom"; +import MkdSDK from "@/utils/MkdSDK"; +import TreeSDK from "@/utils/TreeSDK"; +import { parseJsonSafely } from "@/utils/utils"; + +const sdk = new MkdSDK(); + +export default function EditPropertyRuleTemplatePage() { + const { dispatch, state } = useContext(AuthContext); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const navigate = useNavigate(); + const { id } = useParams(); + + const schema = yup.object({ + template_name: yup.string().required("This field is required"), + template: yup.string().required("This field is required"), + }); + const { + register, + handleSubmit, + setValue, + formState: { errors, isSubmitting, isValidating }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { template: "", template_name: "" }, + mode: "all", + }); + + async function onSubmit(data) { + console.log("submitting", data); + sdk.setTable("property_space_rule_template"); + try { + await sdk.callRestAPI({ id, template_name: data.template_name, template: JSON.stringify({ paragraph: data.template }), host_id: state.user }, "PUT"); + globalDispatch({ + type: "SHOW_CONFIRMATION", + payload: { heading: "Successful", message: "Template edited successfully", btn: "Go back", onClose: () => navigate("/account/profile/rules-templates") }, + }); + } catch (err) { + tokenExpireError(dispatch, err.message); + globalDispatch({ type: "SHOW_ERROR", payload: { heading: "Failed to edit template", message: err.message } }); + } + } + + async function fetchTemplate() { + try { + const treeSdk = new TreeSDK(); + const result = await treeSdk.getOne("property_space_rule_template", id, { join: [] }); + setValue("template_name", result.model.template_name); + setValue("template", parseJsonSafely(result.model.template, {}).paragraph); + } catch (err) { + tokenExpireError(dispatch, err.message); + globalDispatch({ type: "SHOW_ERROR", payload: { heading: "Error fetching template", message: err.message } }); + } + } + + useEffect(() => { + fetchTemplate(); + }, []); + + return ( +
    +
    + + + Back + +
    +
    +

    Edit Rules Template

    +
    + + +
    + +
    + + +
    +
    + +
    +
    + ); +} diff --git a/src/pages/Host/PropertyRulesTemplate/HostPropertyRulesTemplatePage.jsx b/src/pages/Host/PropertyRulesTemplate/HostPropertyRulesTemplatePage.jsx new file mode 100644 index 0000000..4b305e8 --- /dev/null +++ b/src/pages/Host/PropertyRulesTemplate/HostPropertyRulesTemplatePage.jsx @@ -0,0 +1,188 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import { LoadingButton } from "@/components/frontend"; +import ThreeDotsMenu from "@/components/frontend/ThreeDotsMenu"; +import { GlobalContext } from "@/globalContext"; +import TreeSDK from "@/utils/TreeSDK"; +import { parseJsonSafely } from "@/utils/utils"; +import { Dialog, Disclosure, Transition } from "@headlessui/react"; +import { ArrowLeftIcon, ChevronDownIcon, PencilIcon, PlusCircleIcon, TrashIcon } from "@heroicons/react/24/outline"; +import React, { Fragment, useContext, useEffect, useState } from "react"; +import { Link, Navigate, useNavigate } from "react-router-dom"; + +export default function HostPropertyRulesTemplatePage() { + const [templates, setTemplates] = useState([]); + const [fetching, setFetching] = useState(false); + const { dispatch: authDispatch, state: authState } = useContext(AuthContext); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const navigate = useNavigate(); + const [deleteTemplate, setDeleteTemplate] = useState({}); + const [loading, setLoading] = useState(false); + + async function fetchTemplates() { + setLoading(true); + globalDispatch({ type: "START_LOADING" }); + try { + const treeSdk = new TreeSDK(); + const result = await treeSdk.getList("property_space_rule_template", { join: [], filter: [`deleted_at,is`, `host_id,eq,${authState.user}`] }); + if (Array.isArray(result.list)) { + setTemplates(result.list); + } + setLoading(false); + } catch (err) { + setLoading(false); + tokenExpireError(authDispatch, err.message); + globalDispatch({ type: "SHOW_ERROR", payload: { heading: "Operation failed", message: err.message } }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + async function handleDeleteTemplate() { + setLoading(true); + try { + const treeSdk = new TreeSDK(); + await treeSdk.delete("property_space_rule_template", deleteTemplate.id); + setDeleteTemplate({}); + fetchTemplates(); + } catch (err) { + tokenExpireError(authDispatch, err.message); + globalDispatch({ type: "SHOW_ERROR", payload: { heading: "Operation failed", message: err.message } }); + } + setLoading(false); + } + + useEffect(() => { + fetchTemplates(); + }, []); + + return ( +
    +
    + + + Back + +
    + + + {(!loading && templates.length == 0) && + No Rules Templates Yet + } + +
    + {templates.map((t) => ( + + +
    + + {t.template_name} +
    + , + onClick: () => navigate(`/account/profile/edit-rules-templates/${t.id}`), + }, + { + label: "Delete", + icon: , + onClick: () => setDeleteTemplate(t), + }, + ]} + /> +
    + + + {parseJsonSafely(t.template, {}).paragraph} + +
    + ))} +
    + + setDeleteTemplate({})} + > + +
    + + +
    +
    + + + + Are you sure + +
    +

    + Are you sure you want to delete {deleteTemplate.template_name}? +

    +
    + +
    + + + Proceed + +
    +
    +
    +
    +
    +
    +
    +
    + ); +} diff --git a/src/pages/Host/Reviews/HostReviewsPage.jsx b/src/pages/Host/Reviews/HostReviewsPage.jsx new file mode 100644 index 0000000..b25751a --- /dev/null +++ b/src/pages/Host/Reviews/HostReviewsPage.jsx @@ -0,0 +1,391 @@ +import moment from "moment"; +import React from "react"; +import { useContext } from "react"; +import { useEffect } from "react"; +import { useState } from "react"; + +import { Link } from "react-router-dom"; +import StarIcon from "@/components/frontend/icons/StarIcon"; +import PaginationBar from "@/components/PaginationBar"; +import PaginationHeader from "@/components/PaginationHeader"; +import { GlobalContext } from "@/globalContext"; +import useDelayUnmount from "@/hooks/useDelayUnmount"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import CustomSelect from "@/components/frontend/CustomSelect"; + +const columns = [ + { + header: "ID", + accessor: "id", + }, + { + header: "BOOKING DATE", + accessor: "booking_start_time", + }, + { + header: "SPACE", + accessor: "name", + }, + { + header: "GUEST", + accessor: "host_full_name", + }, + { + header: "RATING", + accessor: "rating", + }, + { + header: "STATUS", + accessor: "status", + mapping: ["Under Review", "Posted", "Declined"], + }, + { + header: "ACTION", + accessor: "", + }, +]; + +export default function HostReviewsPage() { + const [type, setType] = useState(0); + const [viewReviewPopup, setViewReviewPopup] = useState(false); + const showViewReviewPopup = useDelayUnmount(viewReviewPopup, 100); + + const [viewReviewData, setViewReviewData] = useState({}); + + const [pageSize, setPageSize] = useState(10); + const [pageCount, setPageCount] = useState(0); + const [dataTotal, setDataTotal] = useState(0); + const [currentPage, setPage] = useState(0); + const [canPreviousPage, setCanPreviousPage] = useState(false); + const [canNextPage, setCanNextPage] = useState(false); + const [rows, setRows] = useState([]); + const [direction, setDirection] = useState("DESC"); + + const { dispatch: globalDispatch } = useContext(GlobalContext); + + async function getData(pageNum, pageSize) { + globalDispatch({ type: "START_LOADING" }); + + const user_id = localStorage.getItem("user"); + const where = [`ergo_booking.host_id = ${user_id}`, `ergo_review.given_by = ${type == 1 ? "'host'" : "'customer'"}`]; + try { + const result = await callCustomAPI( + "review-hashtag", + "post", + { + where, + page: pageNum, + limit: pageSize, + user: "host", + sortId: "post_date", + direction: "DESC", + }, + "PAGINATE", + ); + + const { list, total, limit, num_pages, page } = result; + setRows(list); + setPageSize(limit); + setPageCount(num_pages); + setPage(page); + setDataTotal(total); + setCanPreviousPage(page > 1); + setCanNextPage(page + 1 <= num_pages); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + function updatePageSize(limit) { + (async function () { + setPageSize(limit); + await getData(0, limit); + })(); + } + function previousPage() { + (async function () { + await getData(currentPage - 1 > 0 ? currentPage - 1 : 0, pageSize); + })(); + } + + function nextPage() { + (async function () { + await getData(currentPage + 1 <= pageCount ? currentPage + 1 : 0, pageSize); + })(); + } + + useEffect(() => { + getData(1, 10); + }, [type]); + + const sortByDate = (a, b) => { + if (direction == "DESC") { + return new Date(b.update_at) - new Date(a.update_at); + } + return new Date(a.update_at) - new Date(b.update_at); + }; + + return ( +
    +
    +
    + + +
    + +
    +
    + +
    +
    + + + + {columns.map((column, index) => ( + + ))} + + + + {rows.sort(sortByDate).map((row, i) => { + return ( + + {columns.map((cell, index) => { + if (cell.accessor === "") { + return ( + + ); + } + if (cell.accessor.includes("rating")) { + return ( + + ); + } + if (cell.accessor == "booking_start_time") { + return ( + + ); + } + if (cell.accessor == "host_full_name") { + return ( + + ); + } + if (cell.mapping) { + return ( + + ); + } + return ( + + ); + })} + + ); + })} + +
    + {column.header} +
    + + + + + {(Number(type == 0 ? row.space_rating : row.customer_rating) || 0).toFixed(1)} + + + {moment(row[cell.accessor]).format("MM/DD/YY")} + + {row[`customer_first_name`] + " " + row[`customer_last_name`]} + + + {" "} + {cell.mapping[row[cell.accessor]]} + + + {row[cell.accessor]} +
    +
    +
    + +
    + + {showViewReviewPopup && ( +
    setViewReviewPopup(false)} + > +
    e.stopPropagation()} + > +
    +

    Review details

    + {" "} +
    +
    +

    + Review posted on: {moment(viewReviewData.booking_start_time ?? "11/11/11").format("MM/DD/YY")} +

    +

    + Space: {viewReviewData.name ?? ""} +

    +

    + Booking: #{viewReviewData.booking_id ?? ""}{" "} + + (View details) + +

    +

    Rating

    +
    + {[1, 2, 3, 4, 5].map((val) => ( + = val ? "#FEC84B" : "none"} + xmlns="http://www.w3.org/2000/svg" + > + = val ? "#FEC84B" : "#98A2B3"} + /> + + ))} +
    + {viewReviewData.host_rating && ( + <> +

    Host rating

    +
    + {[1, 2, 3, 4, 5].map((val) => ( + = val ? "#FEC84B" : "none"} + xmlns="http://www.w3.org/2000/svg" + > + = val ? "#FEC84B" : "#98A2B3"} + /> + + ))} +
    + + )} +

    Hashtags

    +
    + {viewReviewData.hashtags ? ( + viewReviewData.hashtags.split(",").map((tag, idx) => ( + + {tag} + + )) + ) : ( + <> + )} +
    +

    Comments

    +

    {viewReviewData.comment ?? ""}

    + +
    +
    + )} +
    + ); +} diff --git a/src/pages/Host/Spaces/Add/AccountNotVerifiedModal.jsx b/src/pages/Host/Spaces/Add/AccountNotVerifiedModal.jsx new file mode 100644 index 0000000..ac3f8cc --- /dev/null +++ b/src/pages/Host/Spaces/Add/AccountNotVerifiedModal.jsx @@ -0,0 +1,76 @@ +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment } from "react"; + +export default function AccountNotVerifiedModal({ modalOpen, closeModal, onSubmit }) { + return ( + + + +
    + + +
    +
    + + + + Host account not verified yet. + +
    +

    The host account needs to be verified for this space to be approved.

    +
    + +
    + + +
    +
    +
    +
    +
    +
    +
    + ); +} diff --git a/src/pages/Host/Spaces/Add/CreateTemplateModal.jsx b/src/pages/Host/Spaces/Add/CreateTemplateModal.jsx new file mode 100644 index 0000000..b92ac44 --- /dev/null +++ b/src/pages/Host/Spaces/Add/CreateTemplateModal.jsx @@ -0,0 +1,246 @@ +import { LoadingButton } from "@/components/frontend"; +import { daysMapping, hourlySlots } from "@/utils/date-time-utils"; +import MkdSDK from "@/utils/MkdSDK"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment, useState } from "react"; +import { Controller, useFieldArray, useForm } from "react-hook-form"; + +export default function CreateTemplateModal({ modalOpen, closeModal, onSuccess }) { + const [loading, setLoading] = useState(false); + + const { handleSubmit, control, register, reset, formState, watch, setValue } = useForm({ + defaultValues: { template_time: [{ from: "12:00 am", to: "01:00 am" }], template_name: "", selectedDays: [] }, + }); + + const { fields, append, remove } = useFieldArray({ + control, + name: "template_time", + }); + + const times = watch("template_time"); + + async function onSubmit(data) { + const sdk = new MkdSDK(); + setLoading(true); + const host_id = localStorage.getItem("user"); + + const body = { + host_id, + template_name: data.template_name, + }; + + if (Array.isArray(data.selectedDays)) { + daysMapping.forEach((day) => { + body[day] = data.selectedDays.includes(day) ? 1 : 0; + }); + } else { + daysMapping.forEach((day) => { + body[day] = 0; + }); + } + + body["slots"] = JSON.stringify(data.template_time.map((time) => ({ start: new Date(`01/01/2001 ${time.from}`).toISOString(), end: new Date(`01/01/2001 ${time.to}`).toISOString() }))); + sdk.setTable("schedule_template"); + try { + await sdk.callRestAPI(body, "POST"); + onSuccess(); + reset(); + closeModal(); + } catch (err) { + console.log("er", err); + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + setLoading(false); + } + + return ( + + + +
    + + +
    +
    + + +
    + Create new template + +
    +
    +

    Set hours for:

    +
    + {daysMapping.map((day) => ( +
    + ( + { + let copy = [...field.value]; + if (field.value.includes(day)) { + copy = field.value.filter((v) => v != day); + } else { + copy.push(day); + } + field.onChange(copy); + }} + onBlur={field.onBlur} + /> + )} + /> + + +
    + ))} +
    +
    + {fields.map((field, index) => ( +
    +
    + + +
    + +
    + ))} +
    + + +
    + + + {Object.entries(formState.errors).length > 0 ? ( +

    + {Object.values(formState.errors)[0].message} +

    + ) : null} +
    +
    + + Create template + +
    +
    +
    +
    +
    +
    + ); +} diff --git a/src/pages/Host/Spaces/Add/EditAddonsModal.jsx b/src/pages/Host/Spaces/Add/EditAddonsModal.jsx new file mode 100644 index 0000000..2d1faf4 --- /dev/null +++ b/src/pages/Host/Spaces/Add/EditAddonsModal.jsx @@ -0,0 +1,127 @@ +import { Dialog, Transition } from "@headlessui/react"; +import { Fragment } from "react"; +import { useOutletContext } from "react-router"; +import { useSpaceContext } from "./spaceContext"; + +export default function EditAddonsModal({ modalOpen, closeModal }) { + const { spaceData, dispatch } = useSpaceContext(); + const { spaceCategories, addons } = useOutletContext(); + + function isCatOthers(){ + const cat = spaceCategories.find((cat) => Number(cat.id) == Number(spaceData.category)) + if (cat?.category === "Others") { + return true + } else return false + } + + function addOrRemoveAmenity(addonId) { + let copy = [...spaceData.addons]; + if (copy.includes(addonId)) { + const index = copy.indexOf(addonId); + if (index > -1) { + copy.splice(index, 1); + } + } else { + copy.push(addonId); + } + dispatch({ type: "SET_ADDONS", payload: copy }); + } + + return ( + <> + + + +
    + + +
    +
    + + + + {" "} + {" "} + Addons + + + {isCatOthers() ? + addons.sort((a, b) => (a.creator_id !== 1 ? -1 : 1) - (b.creator_id !== 1 ? -1 : 1)).map((ad) => ( +
    + addOrRemoveAmenity(String(ad.id))} + /> + +
    + )) + : + addons.filter((ad) => (ad.space_id === Number(spaceData.category)) || ad.creator_id === Number(localStorage.getItem("user"))).sort((a, b) => (a.creator_id !== 1 ? -1 : 1) - (b.creator_id !== 1 ? -1 : 1)).map((ad) => ( +
    + addOrRemoveAmenity(String(ad.id))} + /> + +
    + )) + } +
    +
    +
    +
    +
    +
    + + ); +} diff --git a/src/pages/Host/Spaces/Add/EditAmenitiesModal.jsx b/src/pages/Host/Spaces/Add/EditAmenitiesModal.jsx new file mode 100644 index 0000000..31a8ce3 --- /dev/null +++ b/src/pages/Host/Spaces/Add/EditAmenitiesModal.jsx @@ -0,0 +1,147 @@ +import { Dialog, Transition } from "@headlessui/react"; +import { Fragment } from "react"; +import { useOutletContext } from "react-router"; +import { useSpaceContext } from "./spaceContext"; + +export default function EditAmenitiesModal({ modalOpen, closeModal }) { + const { spaceData, dispatch } = useSpaceContext(); + const { spaceCategories, amenities } = useOutletContext(); + + function isCatOthers(){ + const cat = spaceCategories.find((cat) => Number(cat.id) == Number(spaceData.category)) + if (cat?.category === "Others") { + return true + } else return false + } + + function addOrRemoveAmenity(amenityId) { + let copy = [...spaceData.amenities]; + if (copy.includes(amenityId)) { + const index = copy.indexOf(amenityId); + if (index > -1) { + copy.splice(index, 1); + } + } else { + copy.push(amenityId); + } + dispatch({ type: "SET_AMENITIES", payload: copy }); + } + + return ( + <> + + + +
    + + +
    +
    + + + + {" "} + {" "} + Amenities + + + {isCatOthers() ? + amenities.sort((a, b) => (a.creator_id !== 1 ? -1 : 1) - (b.creator_id !== 1 ? -1 : 1)).map((am) => ( +
    + addOrRemoveAmenity(String(am.id))} + /> + +
    + )) + : + amenities.filter((am) => (am.space_id === Number(spaceData.category)) || am.creator_id === Number(localStorage.getItem("user"))).sort((a, b) => (a.creator_id !== 1 ? -1 : 1) - (b.creator_id !== 1 ? -1 : 1)).map((am) => ( +
    + addOrRemoveAmenity(String(am.id))} + /> + +
    + )) + } + + + {/* {amenities + ?.filter((am) => (am.space_id == Number(spaceData.category)) || am.creator_id === localStorage.getItem("user")) + .map((am) => ( +
    + addOrRemoveAmenity(String(am.id))} + /> + +
    + ))} */} +
    +
    +
    +
    +
    +
    + + ); +} diff --git a/src/pages/Host/Spaces/Add/EditDescriptionModal.jsx b/src/pages/Host/Spaces/Add/EditDescriptionModal.jsx new file mode 100644 index 0000000..ea81354 --- /dev/null +++ b/src/pages/Host/Spaces/Add/EditDescriptionModal.jsx @@ -0,0 +1,94 @@ +import { Dialog, Transition } from "@headlessui/react"; +import { Fragment } from "react"; +import { useSpaceContext } from "./spaceContext"; + +export default function EditDescriptionModal({ modalOpen, closeModal }) { + const { spaceData, dispatch } = useSpaceContext(); + + async function onSubmit(e) { + e.preventDefault(); + const formData = new FormData(e.target); + const description = formData.get("description"); + dispatch({ type: "SET_DESCRIPTION", payload: description }); + closeModal(); + } + + return ( + <> + + + +
    + + +
    +
    + + + + {" "} + {" "} + Description + + + +
    + +
    +
    +
    +
    +
    +
    +
    + + ); +} diff --git a/src/pages/Host/Spaces/Add/EditPropertyNameModal.jsx b/src/pages/Host/Spaces/Add/EditPropertyNameModal.jsx new file mode 100644 index 0000000..cda9416 --- /dev/null +++ b/src/pages/Host/Spaces/Add/EditPropertyNameModal.jsx @@ -0,0 +1,96 @@ +import MkdSDK from "@/utils/MkdSDK"; +import { Dialog, Transition } from "@headlessui/react"; +import { Fragment } from "react"; +import { useSpaceContext } from "./spaceContext"; + +export default function EditPropertyNameModal({ modalOpen, closeModal }) { + const { spaceData, dispatch } = useSpaceContext(); + + async function onSubmit(e) { + e.preventDefault(); + const formData = new FormData(e.target); + const name = formData.get("name"); + dispatch({ type: "SET_PROPERTY_NAME", payload: name }); + closeModal(); + } + + return ( + <> + + + +
    + + +
    +
    + + + + {" "} + {" "} + Space name + + + + +
    + +
    +
    +
    +
    +
    +
    +
    + + ); +} diff --git a/src/pages/Host/Spaces/Add/EditPropertyRulesModal.jsx b/src/pages/Host/Spaces/Add/EditPropertyRulesModal.jsx new file mode 100644 index 0000000..6791795 --- /dev/null +++ b/src/pages/Host/Spaces/Add/EditPropertyRulesModal.jsx @@ -0,0 +1,95 @@ +import { Dialog, Transition } from "@headlessui/react"; +import { Fragment } from "react"; +import { useSpaceContext } from "./spaceContext"; + +export default function EditPropertyRulesModal({ modalOpen, closeModal, rules }) { + const { spaceData, dispatch } = useSpaceContext(); + + async function onSubmit(e) { + e.preventDefault(); + const formData = new FormData(e.target); + const rule = formData.get("rule"); + dispatch({ type: "SET_RULE", payload: rule }); + closeModal(); + } + + return ( + <> + + + +
    + + +
    +
    + + + + {" "} + {" "} + Rule + + + + +
    + +
    +
    +
    +
    +
    +
    +
    + + ); +} diff --git a/src/pages/Host/Spaces/Add/EditTemplateModal.jsx b/src/pages/Host/Spaces/Add/EditTemplateModal.jsx new file mode 100644 index 0000000..a0d1cbd --- /dev/null +++ b/src/pages/Host/Spaces/Add/EditTemplateModal.jsx @@ -0,0 +1,269 @@ +import { LoadingButton } from "@/components/frontend"; +import { GlobalContext } from "@/globalContext"; +import { daysMapping, formatAMPM, hourlySlots } from "@/utils/date-time-utils"; +import MkdSDK from "@/utils/MkdSDK"; +import { parseJsonSafely } from "@/utils/utils"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment, useContext, useState } from "react"; +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { useNavigate } from "react-router"; + +export default function EditTemplateModal({ forceRender, selectedTemplate, setSelectedTemplate, data, modalOpen, closeModal, onSuccess }) { + const [loading, setLoading] = useState(false); + const { dispatch: globalDispatch } = useContext(GlobalContext); + + const parsedSlots = parseJsonSafely(data.slots, []); + const { handleSubmit, control, register, reset, formState, watch } = useForm({ + defaultValues: { + template_time: parsedSlots.map((slot) => ({ from: formatAMPM(slot.start), to: formatAMPM(slot.end) })), + template_name: data.template_name, + selectedDays: daysMapping.filter((day) => data[day] == 1).map((day) => day), + }, + }); + + const navigate = useNavigate(); + + const { fields, append, remove } = useFieldArray({ + control, + name: "template_time", + }); + + const times = watch("template_time"); + + async function onSubmit(formData) { + setLoading(true); + const sdk = new MkdSDK(); + const body = { + id: data.id, + template_name: formData.template_name, + }; + if (Array.isArray(formData.selectedDays)) { + daysMapping.forEach((day) => { + body[day] = formData.selectedDays.includes(day) ? 1 : 0; + }); + } else { + daysMapping.forEach((day) => { + body[day] = 0; + }); + } + body["slots"] = JSON.stringify( + formData.template_time + .filter((time) => time.from != "" && time.to != "") + .map((time, idx) => ({ + start: new Date(`01/01/2001 ${time.from}`).toISOString(), + end: new Date(`01/01/2001 ${time.to}`).toISOString(), + })), + ); + console.log("result") + sdk.setTable("schedule_template"); + try { + const result = await sdk.callRestAPI(body, "PUT"); + console.log(result) + if ((data?.template_name === selectedTemplate?.template_name)) { + setSelectedTemplate(body) + } + onSuccess(); + closeModal(); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation faile", + message: err.message, + }, + }); + } + setLoading(false); + } + + return ( + + + +
    + + +
    +
    + + +
    + Edit template + +
    +
    +

    Set hours for:

    +
    + {daysMapping.map((day) => ( +
    + ( + { + let copy = [...field.value]; + if (field.value.includes(day)) { + copy = field.value.filter((v) => v != day); + } else { + copy.push(day); + } + field.onChange(copy); + }} + onBlur={field.onBlur} + /> + )} + /> + + +
    + ))} +
    +
    + {fields.map((field, index) => ( +
    +
    + + +
    + +
    + ))} +
    + + +
    + + +

    {formState.errors.template_name?.message}

    +
    +
    + + Edit template + +
    +
    +
    +
    +
    +
    + ); +} diff --git a/src/pages/Host/Spaces/Add/PageWrapper.jsx b/src/pages/Host/Spaces/Add/PageWrapper.jsx new file mode 100644 index 0000000..a94b56f --- /dev/null +++ b/src/pages/Host/Spaces/Add/PageWrapper.jsx @@ -0,0 +1,56 @@ +import React from "react"; +import { Outlet, useLocation, useNavigate } from "react-router"; +import Icon from "@/components/Icons"; +import { SpaceContextProvider } from "./spaceContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useContext } from "react"; +import { GlobalContext } from "@/globalContext"; +import useAddonCategories from "@/hooks/api/useAddonCategories"; +import useAmenityCategories from "@/hooks/api/useAmenityCategories"; +import useRuleTemplates from "@/hooks/api/useRuleTemplates"; +const sdk = new MkdSDK(); + +const PageWrapper = () => { + const navigate = useNavigate(); + const { pathname } = useLocation(); + const arr = pathname.split("/"); + const currentStep = Number(arr[arr.length - 1]) || 1; + const { state: globalState } = useContext(GlobalContext); + const amenities = useAmenityCategories(); + const addons = useAddonCategories(); + const ruleTemplates = useRuleTemplates(globalState.user.id); + const spaceCategories = globalState.spaceCategories; + + return ( + +
    + {currentStep < 5 && ( +
    + +
    +
    1 ? "bg-my-gradient" : "bg-[#F2F4F7]"} w-[80px] rounded-lg border py-1 md:w-24`}>
    +
    2 ? "bg-my-gradient" : "bg-[#F2F4F7]"} w-[80px] rounded-lg border py-1 md:w-24`}>
    +
    3 ? "bg-my-gradient" : "bg-[#F2F4F7]"} w-[80px] rounded-lg border py-1 md:w-24`}>
    +
    4 ? "bg-my-gradient" : "bg-[#F2F4F7]"} w-[80px] rounded-lg border py-1 md:w-24`}>
    +
    +
    + )} + + +
    +
    + ); +}; + +export default PageWrapper; diff --git a/src/pages/Host/Spaces/Add/ScheduleDay.jsx b/src/pages/Host/Spaces/Add/ScheduleDay.jsx new file mode 100644 index 0000000..a56735f --- /dev/null +++ b/src/pages/Host/Spaces/Add/ScheduleDay.jsx @@ -0,0 +1,211 @@ +import React, { useState } from "react"; +import CopyIcon from "@/components/frontend/icons/CopyIcon"; +import PencilIcon from "@/components/frontend/icons/PencilIcon"; +import RecurringIcon from "@/components/frontend/icons/RecurringIcon"; +import ResetIcon from "@/components/frontend/icons/ResetIcon"; +import { useSpaceContext } from "./spaceContext"; +import moment from "moment"; +import { useFieldArray, useForm } from "react-hook-form"; +import useDelayUnmount from "@/hooks/useDelayUnmount"; +import CustomizedIcon from "@/components/frontend/icons/CustomizedIcon"; +import { daysMapping, fullDaysMapping, hourlySlots, formatAMPM } from "@/utils/date-time-utils"; +import { useEffect } from "react"; +import Icon from "@/components/Icons"; +import { parseJsonSafely } from "@/utils/utils"; + +const ScheduleDay = ({ selectedDate, date, view, activeStartDate, isDirty, setIsDirty, selectedTemplate }) => { + const { spaceData, dispatch } = useSpaceContext(); + // const showOptions = selectedDate.getDate() == date.getDate(); + const [showOptions, setShowOptions] = useState(false); + const [editPopup, setEditPopup] = useState(false); + const showEditPopup = useDelayUnmount(editPopup, 300); + const dayFormatted = moment(date).format("MM/DD/YY"); + + // get slots from context or template + let slots = Array.isArray(spaceData?.customSlots[dayFormatted]) + ? spaceData?.customSlots[dayFormatted] + : selectedTemplate[daysMapping[date.getDay()]] == 1 && Array.isArray(parseJsonSafely(selectedTemplate.slots)) + ? parseJsonSafely(selectedTemplate.slots) + : []; + + let isCustom = Array.isArray(spaceData?.customSlots[dayFormatted]); + + const { handleSubmit, register, control, getValues, setValue } = useForm({ + defaultValues: { + custom_slot: slots.map((slot) => ({ start: formatAMPM(slot.start), end: formatAMPM(slot.end) })), + }, + }); + const { fields, append, prepend, remove, swap, move, insert } = useFieldArray({ + control, + name: "custom_slot", + }); + + const onSubmit = (data) => { + console.log("editing", data); + dispatch({ + type: "SET_DAY_SLOT", + payload: { + day: dayFormatted, + slots: data.custom_slot.map((time) => ({ + start: new Date(`${dayFormatted} ${time.start}`).toISOString(), + end: new Date(`${dayFormatted} ${time.end}`).toISOString() })), + }, + }); + setEditPopup(false); + }; + + const resetToTemplate = (e) => { + e.stopPropagation(); + dispatch({ type: "CLEAR_DAY_SLOT", payload: dayFormatted }); + }; + + const copyFromPreviousWeek = (e) => { + e.stopPropagation(); + dispatch({ type: "INHERIT_DAY_SLOT", payload: dayFormatted }); + }; + + useEffect(() => { + setShowOptions(false); + }, [selectedDate]); + + return ( +
    +
    +
    {date.getDate()}
    + {isCustom ? : } +
    +
    + {slots.slice(0, 3).map((sl, idx) => ( +

    {formatAMPM(sl.start) + " - " + formatAMPM(sl.end)}

    + ))} + {slots.length > 3 && + {slots.length - 3} More} +
    + {dayFormatted == moment(selectedDate).format("MM/DD/YY") && ( + + )} + + {showOptions && ( +
    + + {selectedTemplate.template_name && ( + + )} + + +
    + )} +
    setEditPopup(false)} + > +
    e.stopPropagation()} + onSubmit={handleSubmit(onSubmit)} + > +
    +

    Edit Day

    + +
    +
    +
    + {fields.map((field, index) => ( +
    +
    + + +
    + +
    + ))} +
    + + +
    +
    + +
    +
    +
    + ); +}; + +export default ScheduleDay; diff --git a/src/pages/Host/Spaces/Add/SelectRuleTemplate.jsx b/src/pages/Host/Spaces/Add/SelectRuleTemplate.jsx new file mode 100644 index 0000000..5a1c773 --- /dev/null +++ b/src/pages/Host/Spaces/Add/SelectRuleTemplate.jsx @@ -0,0 +1,79 @@ +import NoteIcon from "@/components/frontend/icons/NoteIcon"; +import { parseJsonSafely } from "@/utils/utils"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment } from "react"; + +export default function SelectRuleTemplate({ isOpen, closeModal, templates, onSelect }) { + return ( + + + +
    + + +
    +
    + + +
    + Select Rules template + +
    +
    + {templates.map((tmp) => ( + + ))} + {templates.length == 0 && ( +

    + + No templates yet +

    + )} +
    +
    +
    +
    +
    +
    +
    + ); +} diff --git a/src/pages/Host/Spaces/Add/SelectTemplatesModal.jsx b/src/pages/Host/Spaces/Add/SelectTemplatesModal.jsx new file mode 100644 index 0000000..ff637cc --- /dev/null +++ b/src/pages/Host/Spaces/Add/SelectTemplatesModal.jsx @@ -0,0 +1,79 @@ +import NoteIcon from "@/components/frontend/icons/NoteIcon"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment } from "react"; + +export default function SelectTemplatesModal({ modalOpen, closeModal, templates, clearAll, selectedTemplate, setSelectedTemplate }) { + return ( + + + +
    + + +
    +
    + + +
    + Select Schedule template + +
    +
    + {templates.map((tmp) => ( + + ))} + {templates.length == 0 && ( +

    + + No templates yet +

    + )} +
    +
    +
    +
    +
    +
    +
    + ); +} diff --git a/src/pages/Host/Spaces/Add/SpaceDetailsFour.jsx b/src/pages/Host/Spaces/Add/SpaceDetailsFour.jsx new file mode 100644 index 0000000..ac63722 --- /dev/null +++ b/src/pages/Host/Spaces/Add/SpaceDetailsFour.jsx @@ -0,0 +1,542 @@ +import React from "react"; +import { useState } from "react"; +import { Link, useNavigate, useOutletContext } from "react-router-dom"; +import FaqAccordion from "@/components/frontend/FaqAccordion"; +import { useSpaceContext } from "./spaceContext"; +import { useEffect } from "react"; +import MkdSDK from "@/utils/MkdSDK"; +import { useContext } from "react"; +import { GlobalContext } from "@/globalContext"; +import DateTimePicker from "@/components/frontend/DateTimePicker"; +import { useForm } from "react-hook-form"; +import CustomSelect from "@/components/frontend/CustomSelect"; +import { DRAFT_STATUS, ID_VERIFICATION_STATUSES, IMAGE_STATUS, NOTIFICATION_STATUS, NOTIFICATION_TYPE, SPACE_STATUS, SPACE_VISIBILITY } from "@/utils/constants"; +import ReCAPTCHA from "react-google-recaptcha"; +import PropertyImageSlider from "@/components/frontend/PropertyImageSlider"; +import { parseJsonSafely } from "@/utils/utils"; +import PropertySpaceMapImage from "@/components/frontend/PropertySpaceMapImage"; +import CircleCheckIcon from "@/components/frontend/icons/CircleCheckIcon"; +import PencilIcon from "@/components/frontend/icons/PencilIcon"; +import EditDescriptionModal from "./EditDescriptionModal"; +import EditAmenitiesModal from "./EditAmenitiesModal"; +import EditAddonsModal from "./EditAddonsModal"; +import EditPropertyRulesModal from "./EditPropertyRulesModal"; +import EditPropertyNameModal from "./EditPropertyNameModal"; +import AccountNotVerifiedModal from "./AccountNotVerifiedModal"; +import PropertyImageSliderAdd from "@/components/frontend/PropertyImageSliderAdd"; + +const SpaceDetailsFour = () => { + const [galleryOpen, setGalleryOpen] = useState(false); + const { amenities, addons } = useOutletContext(); + const [recaptchaValidated, setRecaptchaValidated] = useState(false); + const [showMap, setShowMap] = useState(false); + const { spaceData, dispatch } = useSpaceContext(); + const sdk = new MkdSDK(); + + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const navigate = useNavigate(); + + const { register, setValue } = useForm(); + const [showCalendar, setShowCalendar] = useState(false); + const [editDescription, setEditDescription] = useState(false); + const [editPropertyName, setEditPropertyName] = useState(false); + const [editAmenities, setEditAmenities] = useState(false); + const [editAddons, setEditAddons] = useState(false); + const [editPropertyRules, setEditPropertyRules] = useState(false); + + const [accountNotVerified, setAccountNotVerified] = useState(false); + + // useEffect(() => { + // if (!spaceData.name) { + // navigate("/spaces/add"); + // return; + // } + // }, []); + + const onSubmit = async (e) => { + if (e) e.preventDefault(); + if (globalState.user.verificationStatus != ID_VERIFICATION_STATUSES.VERIFIED && !accountNotVerified) { + setAccountNotVerified(true); + return; + } + globalDispatch({ type: "START_LOADING" }); + const host_id = Number(localStorage.getItem("user")); + + try { + // create property + sdk.setTable("property"); + const propertyResult = await sdk.callRestAPI( + { + address_line_1: spaceData.address_line_1, + address_line_2: spaceData.address_line_2, + city: spaceData.city, + country: spaceData.country, + zip: spaceData.zip, + status: 1, + verified: 1, + host_id, + name: spaceData.name, + rule: spaceData.rule, + }, + "POST", + ); + // create property add ons + sdk.setTable("property_add_on"); + for (let i = 0; i < spaceData.addons.length; i++) { + const addon_id = spaceData.addons[i]; + await sdk.callRestAPI( + { + property_id: propertyResult.message, + add_on_id: addon_id, + }, + "POST", + ); + } + // create property space + sdk.setTable("property_spaces"); + const propertySpaceResult = await sdk.callRestAPI( + { + property_id: propertyResult.message, + space_id: spaceData.category, + max_capacity: spaceData.max_capacity, + description: spaceData.description, + rate: spaceData.rate, + availability: SPACE_VISIBILITY.VISIBLE, + draft_status: DRAFT_STATUS.COMPLETED, + space_status: SPACE_STATUS.UNDER_REVIEW, + additional_guest_rate: spaceData.additional_guest_rate || undefined, + size: spaceData.size || undefined, + }, + "POST", + ); + // create property space images + for (let i = 0; i < spaceData.pictureIds.length; i++) { + sdk.setTable("property_spaces_images"); + const pictureId = spaceData.pictureIds[i]; + if (pictureId) { + await sdk.callRestAPI( + { + property_id: propertyResult.message, + property_spaces_id: propertySpaceResult.message, + photo_id: pictureId, + is_approved: IMAGE_STATUS.IN_REVIEW, + }, + "POST", + ); + } + if (pictureId && pictureId == spaceData.thumbnail) { + sdk.setTable("property_spaces"); + await sdk.callRestAPI( + { + id: propertySpaceResult.message, + default_image_id: spaceData.thumbnail, + }, + "PUT", + ); + } + } + + // create property space faqs + sdk.setTable("property_space_faq"); + for (let i = 0; i < spaceData.faqs.length; i++) { + const faq = spaceData.faqs[i]; + await sdk.callRestAPI( + { + property_space_id: propertySpaceResult.message, + question: faq.question, + answer: faq.answer, + }, + "POST", + ); + } + // create property space amenities + sdk.setTable("property_spaces_amenitites"); + for (let i = 0; i < spaceData.amenities.length; i++) { + const amenity_id = spaceData.amenities[i]; + await sdk.callRestAPI( + { + property_spaces_id: propertySpaceResult.message, + amenity_id, + }, + "POST", + ); + } + + // create scheduling + sdk.setTable("property_spaces_schedule_template"); + await sdk.callRestAPI( + { + property_spaces_id: propertySpaceResult.message, + schedule_template_id: spaceData.schedule_template.id, + custom_slots: JSON.stringify(spaceData.customSlots), + }, + "POST", + ); + + // create notification + sdk.setTable("notification"); + await sdk.callRestAPI( + { + user_id: host_id, + actor_id: null, + action_id: propertySpaceResult.message, + notification_time: new Date().toISOString().split(".")[0], + message: "New Space Created", + type: NOTIFICATION_TYPE.CREATE_SPACE, + status: NOTIFICATION_STATUS.NOT_ADDRESSED, + }, + "POST", + ); + navigate("/spaces/add/5"); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + window.scrollTo({ top: 0, left: 0 }); + }; + + const onChange = () => { + setRecaptchaValidated(true); + }; + + + return ( +
    { + setShowCalendar(false); + }} + > +

    Review

    +

    Below is how people will see your listing:

    +
    +
    +

    {spaceData.name}

    + +
    + +
    +
    +
    +
    + + +
    + + +
    + {spaceData.pictures.filter((v) => (v != null && v != "")).length > 0 && + + } +
    +
    +
    +
    +
    +

    Description

    +

    {spaceData.description}

    +
    +
    + +
    +
    +
    +
    +
    +

    Amenities

    +
      + {amenities.sort((a, b) => (a.creator_id !== 1 ? -1 : 1) - (b.creator_id !== 1 ? -1 : 1)) + ?.filter((am) => spaceData.amenities.includes(String(am.id))) + .map((am) => ( +
    • + + {am.name} +
    • + ))} +
    +
    +
    + +
    +
    +
    +
    +
    +

    Add ons

    +
      + {addons?.sort((a, b) => (a.creator_id !== 1 ? -1 : 1) - (b.creator_id !== 1 ? -1 : 1)) + ?.filter((addon) => spaceData.addons.includes(String(addon.id))) + .map((addon) => ( +
    • + + {" "} +
      + {addon.name} + ${addon.cost}/h +
      {" "} +
      {" "} +
    • + ))} +
    +
    +
    + +
    +
    +
    +
    +
    +

    About the host

    +
    + +
    + +
    +
    +

    {globalState.user.first_name}

    +

    {globalState.user.last_name}

    +
    +

    {globalState.user.about}

    + +
    +
    + +
    +
    +
    +
    +

    Reviews

    + +
    +
    +
    +
    +

    FAQs

    + {spaceData.faqs.map((faq, idx) => ( + + ))} + {/*
    + +
    */} +
    +
    +
    +
    +

    Property rules

    +

    {spaceData.rule}

    +
    +
    + +
    +
    +
    +
    +
    +

    Price and availability

    +
    + Max capacity + + {" "} + {spaceData.max_capacity} people + +
    +
    + Pricing from + + from: ${spaceData?.rate}/h + +
    + {spaceData.additional_guest_rate && spaceData.max_capacity > 1 ? ( +
    + Additional guests + + from: ${spaceData.additional_guest_rate}/h + +
    + ) : null} +
    +
    +
    + +
    + +
    +
    +
    +
    +

    Price and availability

    +
    + Max capacity + + {" "} + {spaceData.max_capacity ?? spaceData?.max_capacity} people + +
    +
    + Pricing from + + from: ${spaceData.rate}/h + +
    + +
    +
    + +
    + +
    +
    +
    + (v != null && v != ""))} + modalOpen={galleryOpen} + closeModal={() => setGalleryOpen(false)} + /> + +
    + {/* */} + +
    + setShowMap(false)} + /> + setEditDescription(false)} + /> + setEditPropertyName(false)} + /> + setEditAmenities(false)} + /> + setEditAddons(false)} + /> + {/* */} + setEditPropertyRules(false)} + /> + setAccountNotVerified(false)} + onSubmit={onSubmit} + /> +
    + ); +}; + +export default SpaceDetailsFour; diff --git a/src/pages/Host/Spaces/Add/SpaceDetailsOne.jsx b/src/pages/Host/Spaces/Add/SpaceDetailsOne.jsx new file mode 100644 index 0000000..70d6ddd --- /dev/null +++ b/src/pages/Host/Spaces/Add/SpaceDetailsOne.jsx @@ -0,0 +1,384 @@ +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import React, { useState } from "react"; +import { useNavigate, useOutletContext } from "react-router"; +import { useForm } from "react-hook-form"; +import { useSpaceContext } from "./spaceContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { useContext } from "react"; +import { GlobalContext } from "@/globalContext"; +import { SPACE_CATEGORY_SIZES, DRAFT_STATUS, SPACE_STATUS, SPACE_VISIBILITY } from "@/utils/constants"; +import CustomLocationAutoCompleteV2 from "@/components/CustomLocationAutoCompleteV2"; +import CustomComboBox from "@/components/CustomComboBox"; +import countries from "@/utils/countries.json"; +import CustomSelectV2 from "@/components/CustomSelectV2"; +import CounterV2 from "@/components/CounterV2"; +import SelectRuleTemplate from "./SelectRuleTemplate"; +import { extractLocationInfo } from "@/utils/utils"; +const sdk = new MkdSDK(); + +const AddSpacePage = () => { + const { spaceData, dispatch } = useSpaceContext(); + const { ruleTemplates } = useOutletContext(); + const { state: globalState, dispatch: globalDispatch } = useContext(GlobalContext); + const [selectRuleTemplateModal, setSelectRuleTemplateModal] = useState(false); + const schema = yup.object({ + category: yup.string().required("This field is required"), + name: yup.string().required("This field is required"), + address_line_1: yup.string().required("This field is required"), + address_line_2: yup.string(), + city: yup.string().required("This field is required"), + zip: yup.string(), + rate: yup.number().typeError("Must be a number").positive().integer(), + description: yup.string().required("This field is required"), + rule: yup.string(), + max_capacity: yup.number().required("This field is required").min(1, "Max capacity must be greater than 0").typeError("This field is required"), + additional_guest_rate: yup.number().typeError("Must be a number").positive().integer(), + }); + + const navigate = useNavigate(); + const { + register, + handleSubmit, + setValue, + watch, + trigger, + control, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + ...spaceData, + category: Number(spaceData.category) + }, + mode: "all", + }); + + const formValues = watch(); + + const onSubmit = async (data) => { + const result = extractLocationInfo(data?.city) + data.city = (result && result[0]); + data.country = (result && result[1]); + console.log("submitting", data); + dispatch({ type: "SET_DETAILS_ONE", payload: { ...data, amenities: [], addons: [] } }); + navigate("/spaces/add/2"); + }; + + const onSaveDraft = async () => { + const host_id = localStorage.getItem("user"); + + const result = extractLocationInfo(formValues.city) + formValues.city = (result[0]); + formValues.country = (result[1]); + + const formIsValid = await trigger(); + if (!formIsValid) return; + + globalDispatch({ type: "START_LOADING" }); + var propertyResult; + try { + // create property if needed + if (!spaceData.property_id) { + sdk.setTable("property"); + propertyResult = await sdk.callRestAPI( + { + address_line_1: formValues.address_line_1, + address_line_2: formValues.address_line_2, + city: formValues.city, + country: formValues.country, + zip: formValues.zip, + status: 1, + verified: 1, + host_id, + name: formValues.name, + rule: formValues.rule, + }, + "POST", + ); + dispatch({ type: "SET_PROPERTY_ID", payload: propertyResult?.message }); + } + + // create space + sdk.setTable("property_spaces"); + await sdk.callRestAPI( + { + property_id: propertyResult?.message ?? spaceData.property_id, + space_id: formValues.category, + max_capacity: formValues.max_capacity, + description: formValues.description, + rate: formValues.rate, + space_status: SPACE_STATUS.UNDER_REVIEW, + availability: SPACE_VISIBILITY.VISIBLE, + draft_status: DRAFT_STATUS.PROPERTY_SPACE, + size: hasSizes ? formValues.size : SPACE_CATEGORY_SIZES.UNSET, + additional_guest_rate: formValues.additional_guest_rate || undefined, + }, + "POST", + ); + + navigate("/account/my-spaces"); + } catch (err) { + if (err.message == "Validation Error") { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed!!", + message: `Space category "${globalState.spaceCategories.find((cat) => cat.id == formValues.category)?.category}" already exists for property "${formValues.name}"`, + }, + }); + } + } + globalDispatch({ type: "STOP_LOADING" }); + }; + + const hasSizes = globalState.spaceCategories.find((ctg) => ctg.id == Number(formValues.category))?.has_sizes == 1; + const SIZES = [ + { label: "All", value: SPACE_CATEGORY_SIZES.UNSET }, + { label: "Small", value: SPACE_CATEGORY_SIZES.SMALL }, + { label: "Medium", value: SPACE_CATEGORY_SIZES.MEDIUM }, + { label: "Large", value: SPACE_CATEGORY_SIZES.LARGE }, + { label: "X-Large", value: SPACE_CATEGORY_SIZES.X_LARGE }, + ]; + + return ( +
    +
    +

    Space Details

    +

    Fields marked with an asterisk are required

    +
    + + +
    +
    + + setValue("address_line_1", val)} + name="address_line_1" + className={`w-full rounded border py-2 px-3 leading-tight text-gray-700 ${errors.address_line_1?.message ? "border-red-500 focus:outline-red-500" : "focus-within:outline-primary"}`} + placeholder="" + hideIcons + suggestionType={["address"]} + /> +
    +
    + + setValue("address_line_2", val)} + name="address_line_2" + className={`w-full rounded border py-2 px-3 leading-tight text-gray-700 ${errors.address_line_2?.message ? "border-red-500 focus:outline-red-500" : "focus-within:outline-primary"}`} + placeholder="" + hideIcons + suggestionType={["address"]} + /> +
    +
    + + setValue("city", val)} + name="city" + className={`w-full rounded border py-2 px-3 leading-tight text-gray-700 ${errors.city?.message ? "border-red-500 focus:outline-red-500" : "focus-within:outline-primary"}`} + placeholder="" + hideIcons + suggestionType={["(regions)"]} + /> +
    +
    + + +
    +
    + + +
    +
    + +
    + $ + +
    +
    +
    + + +
    +
    + + +
    +
    +

    * Max number of guests * {errors.max_capacity?.message ? {errors.max_capacity?.message} : ""}

    + setValue("max_capacity", val)} + /> +
    + {hasSizes && ( +
    + + +
    + )} + +
    + +
    + $ + +
    +
    +
    + +
    + +
    + setSelectRuleTemplateModal(false)} + templates={ruleTemplates} + onSelect={(val) => setValue("rule", val)} + /> +
    + ); +}; + +export default AddSpacePage; diff --git a/src/pages/Host/Spaces/Add/SpaceDetailsThree.jsx b/src/pages/Host/Spaces/Add/SpaceDetailsThree.jsx new file mode 100644 index 0000000..3b34dc1 --- /dev/null +++ b/src/pages/Host/Spaces/Add/SpaceDetailsThree.jsx @@ -0,0 +1,405 @@ +import React, { useState } from "react"; +import { useContext } from "react"; +import { Calendar } from "react-calendar"; +import { useNavigate } from "react-router"; +import NextIcon from "@/components/frontend/icons/NextIcon"; +import PrevIcon from "@/components/frontend/icons/PrevIcon"; +import ScheduleDay from "@/pages/Host/Spaces/Add/ScheduleDay"; +import { GlobalContext } from "@/globalContext"; +import { useEffect } from "react"; +import { useSpaceContext } from "./spaceContext"; +import MkdSdk from "@/utils/MkdSDK"; + +import NoteIcon from "@/components/frontend/icons/NoteIcon"; +import { DRAFT_STATUS, IMAGE_STATUS, SPACE_STATUS, SPACE_VISIBILITY } from "@/utils/constants"; +import { Tab } from "@headlessui/react"; +import SelectTemplatesModal from "./SelectTemplatesModal"; +import CreateTemplateModal from "./CreateTemplateModal"; +import TemplateCard from "./TemplateCard"; + +let sdk = new MkdSdk(); + +const SpaceDetailsThree = () => { + const [addTemplatePopup, setAddTemplatePopup] = useState(false); + const [selectTemplatePopup, setSelectTemplatePopup] = useState(false); + const [selectedDate, setSelectedDate] = useState(new Date()); + const { state: globalState, dispatch: globalDispatch } = useContext(GlobalContext); + const [templates, setTemplates] = useState([]); + const [render, forceRender] = useState(new Date()); + const [selectedTemplate, setSelectedTemplate] = useState({}); + const { spaceData, dispatch } = useSpaceContext(); + const [templatesFetched, setTemplatesFetched] = useState(false); + + const navigate = useNavigate(); + + async function fetchTemplates() { + const host_id = localStorage.getItem("user"); + const payload = { host_id }; + sdk.setTable("schedule_template"); + try { + const result = await sdk.callRestAPI({ payload }, "GETALL"); + if (Array.isArray(result.list)) { + setTemplates(result.list); + setTemplatesFetched(true); + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + useEffect(() => { + fetchTemplates(); + }, [render]); + + useEffect(() => { + if (templatesFetched && spaceData?.schedule_template?.id) { + setSelectedTemplate(templates.find((tmp) => tmp.id == spaceData.schedule_template.id)); + } + }, [templatesFetched]); + + async function submitSchedule() { + if (selectedTemplate?.slots?.length < 1 || selectedTemplate?.slots === "[]") { + globalDispatch({ + type: "SHOW_ERROR", + payload: { heading: "Template selected doesn't have a slot", message: "Click on Use Template and Add Slot Time" }, + }); + return; + } + if (!selectedTemplate.id) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { heading: "Template was not selected", message: "Click on Use Template and Select a Schedule Template" }, + }); + return; + } + console.log(selectedTemplate) + dispatch({ type: "SET_SCHEDULE_TEMPLATE", payload: selectedTemplate }); + navigate("/spaces/add/4"); + } + + const onSaveDraft = async () => { + if (selectedTemplate?.slots && selectedTemplate?.slots?.length < 1 || selectedTemplate?.slots === "[]") { + globalDispatch({ + type: "SHOW_ERROR", + payload: { heading: "Template selected doesn't have a slot", message: "Click on Use Template and Add Slot Time" }, + }); + return; + } + + if (!selectedTemplate.id && selectedTemplate?.slots) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { heading: "Template was not selected", message: "Click on Use Template and Select a Schedule Template" }, + }); + return; + } + + const host_id = localStorage.getItem("user"); + globalDispatch({ type: "START_LOADING" }); + var propertyResult, propertySpaceResult; + if (!spaceData.property_id) { + sdk.setTable("property"); + propertyResult = await sdk.callRestAPI( + { + address_line_1: spaceData.address_line_1, + address_line_2: spaceData.address_line_2, + city: spaceData.city, + country: spaceData.country, + zip: spaceData.zip, + status: 1, + verified: 1, + host_id, + name: spaceData.name, + rule: spaceData.rule, + }, + "POST", + ); + dispatch({ type: "SET_PROPERTY_ID", payload: propertyResult?.message }); + } + + // create space + if (!spaceData.property_space_id) { + sdk.setTable("property_spaces"); + propertySpaceResult = await sdk.callRestAPI( + { + property_id: propertyResult?.message ?? spaceData.property_id, + space_id: spaceData.category, + max_capacity: spaceData.max_capacity, + description: spaceData.description, + rate: spaceData.rate, + space_status: SPACE_STATUS.UNDER_REVIEW, + availability: SPACE_VISIBILITY.VISIBLE, + draft_status: DRAFT_STATUS.SCHEDULING, + additional_guest_rate: spaceData.additional_guest_rate || undefined, + size: spaceData.size || undefined, + }, + "POST", + ); + } + try { + // create property add ons + sdk.setTable("property_add_on"); + for (let i = 0; i < spaceData.addons.length; i++) { + const addon_id = spaceData.addons[i]; + await sdk.callRestAPI( + { + property_id: propertyResult?.message, + add_on_id: addon_id, + }, + "POST", + ); + } + + // create property space images + for (let i = 0; i < spaceData.pictureIds.length; i++) { + sdk.setTable("property_spaces_images"); + const pictureId = spaceData.pictureIds[i]; + if (pictureId) { + await sdk.callRestAPI( + { + property_id: propertyResult.message, + property_spaces_id: propertySpaceResult.message, + photo_id: pictureId, + is_approved: IMAGE_STATUS.IN_REVIEW, + }, + "POST", + ); + } + if (pictureId && pictureId == spaceData.thumbnail) { + sdk.setTable("property_spaces"); + await sdk.callRestAPI( + { + id: propertySpaceResult.message, + default_image_id: spaceData.thumbnail, + }, + "PUT", + ); + } + } + + // create property space faqs + sdk.setTable("property_space_faq"); + for (let i = 0; i < spaceData.faqs.length; i++) { + const faq = spaceData.faqs[i]; + await sdk.callRestAPI( + { + property_space_id: propertySpaceResult.message, + question: faq.question, + answer: faq.answer, + }, + "POST", + ); + } + + // create property space amenities + sdk.setTable("property_spaces_amenitites"); + for (let i = 0; i < spaceData.amenities.length; i++) { + const amenity_id = spaceData.amenities[i]; + await sdk.callRestAPI( + { + property_spaces_id: propertySpaceResult.message, + amenity_id, + }, + "POST", + ); + } + + // create scheduling + sdk.setTable("property_spaces_schedule_template"); + await sdk.callRestAPI( + { + property_spaces_id: propertySpaceResult.message, + schedule_template_id: selectedTemplate.id, + custom_slots: JSON.stringify(spaceData.customSlots), + }, + "POST", + ); + navigate("/account/my-spaces"); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + }; + + // useEffect(() => { + // if (!spaceData.name) { + // navigate("/spaces/add"); + // } + // }, []); + + return ( +
    +

    Space Details

    +
    +

    + + How it works +

    +

    + You can predefine each day of the week in ’Templates’ - those hours will be applied to each day of the week. On top of that you can customize each day according to your needs. +
    +
    You will be able to edit and change the space availability anytime from in ‘Spaces/space/edit availability’. +

    +
    + + + + Calendar + Templates +
    +
    + + +
    e.stopPropagation()} + > + +
    +
    + +
    + +
    + + } + prevLabel={} + next2Label={ + <> + + + } + prev2Label={<>} + navigationLabel={({ date, label, locale, view }) => ( + <> +
    e.stopPropagation()} + > + {label} +
    + +
    +
    + + )} + tileContent={({ activeStartDate, date, view }) => { + return ( + + ); + }} + minDate={new Date()} + maxDetail="month" + /> +
    + setSelectTemplatePopup(false)} + clearAll={() => dispatch({ type: "CLEAR_ALL_SLOTS" })} + templates={templates} + selectedTemplate={selectedTemplate} + setSelectedTemplate={setSelectedTemplate} + /> +
    + +
    +
    +

    Template name & day(s)

    +

    Time slots

    +
    + +
    + {templates.map((tmp) => ( + + ))} + {templates.length == 0 && ( +

    + + No templates yet +

    + )} + setAddTemplatePopup(false)} + onSuccess={() => fetchTemplates()} + /> +
    +
    +
    + +

    + Keep in mind it usually takes us 2 days to review new space. Once we approve it it wil be posted with the first date/time available +

    +
    + +
    + +
    + ); +}; + +export default SpaceDetailsThree; diff --git a/src/pages/Host/Spaces/Add/SpaceDetailsTwo.jsx b/src/pages/Host/Spaces/Add/SpaceDetailsTwo.jsx new file mode 100644 index 0000000..4ed54a1 --- /dev/null +++ b/src/pages/Host/Spaces/Add/SpaceDetailsTwo.jsx @@ -0,0 +1,576 @@ +import { yupResolver } from "@hookform/resolvers/yup"; +import React from "react"; +import { useState } from "react"; +import { FileUploader } from "react-drag-drop-files"; +import { useFieldArray, useForm } from "react-hook-form"; +import * as yup from "yup"; +import SunEditor from "suneditor-react"; +import "suneditor/dist/css/suneditor.min.css"; +import { useNavigate, useOutletContext } from "react-router"; +import { useSpaceContext } from "./spaceContext"; +import { useEffect } from "react"; +import MkdSDK from "@/utils/MkdSDK"; +import useDelayUnmount from "@/hooks/useDelayUnmount"; +import { useContext } from "react"; +import { GlobalContext, showToast } from "@/globalContext"; +import { DRAFT_STATUS, IMAGE_STATUS, SPACE_STATUS, SPACE_VISIBILITY } from "@/utils/constants"; +import { Link } from "react-router-dom"; +import CustomSelectV2 from "@/components/CustomSelectV2"; +import useCancellation from "@/hooks/api/useCancellation"; +import { sanitizeAndTruncate } from "@/utils/utils"; +import CircleCheckIcon from "@/components/frontend/icons/CircleCheckIcon"; +import HostAddAddonsModal from "@/components/HostAddAddonsModal"; +import TreeSDK from "@/utils/TreeSDK"; + +const treeSdk = new TreeSDK(); + +async function getFileFromUrl(url) { + if (!url) return null; + try { + let response = await fetch(url); + let data = await response.blob(); + let metadata = { + type: "image/jpeg", + }; + return new File([data], url.split("/").pop(), metadata); + } catch (err) { + return null; + } +} + +const readImage = (file, previewEl) => { + const reader = new FileReader(); + reader.onload = (event) => { + document.getElementById(previewEl).src = event.target.result; + }; + + reader.readAsDataURL(file); +}; + +const SpaceDetailsTwo = () => { + const { spaceData, dispatch } = useSpaceContext(); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const [pictures, setPictures] = useState([]); + const [addOnModal, setAddOnModal] = useState(false); + + const [addAmenitiesPopup, setAddAmenitiesPopup] = useState(false); + const showAddAmenitiesPopup = useDelayUnmount(addAmenitiesPopup, 100); + const cancellationPolicy = useCancellation(); + const [addAddonsPopup, setAddAddonsPopup] = useState(false); + const showAddAddonsPopup = useDelayUnmount(addAddonsPopup, 100); + const { addons, amenities } = useOutletContext(); + const sdk = new MkdSDK(); + const { spaceCategories, ruleTemplates } = useOutletContext(); + + const navigate = useNavigate(); + const schema = yup.object({ + question: yup.string(), + }); + + const { + register, + handleSubmit, + getValues, + setValue, + watch, + control, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: spaceData, + }); + + const formValues = watch(); + + const selectedAmenities = watch("amenities"); + const selectedAddons = watch("addons"); + + const { fields, append, remove } = useFieldArray({ + control, + name: "faqs", + }); + + const handleImageUpload = async (file) => { + if (!file) return { url: "", id: null }; + const formData = new FormData(); + formData.append("file", file); + + try { + const upload = await sdk.uploadImage(formData); + return upload; + } catch (error) { + return { url: "", id: null }; + } + }; + + const onSubmit = async (data) => { + const uploadedImages = []; + const uploadedIds = []; + globalDispatch({ type: "START_LOADING" }); + + for (let i = 0; i < pictures.length; i++) { + const file = pictures[i]; + const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/svg+xml']; + if (file?.type && !allowedTypes.includes(file?.type)) { + showToast(globalDispatch, 'Invalid file type. Only JPEG, PNG, WEBP, and SVG are allowed.', 4000, "ERROR"); + return; + } + + if (file?.size && file?.size > 5 * 1024 * 1024) { // 5 MB limit + showToast(globalDispatch, 'One of the image is too large. Max size is 5 MB.', 4000, "ERROR"); + return; + } + + const upload = await handleImageUpload(file); + uploadedImages[i] = upload.url; + uploadedIds[i] = upload.id; + if (file?.name == data.thumbnail) { + dispatch({ type: "SET_THUMBNAIL", payload: upload.id }); + } + } + dispatch({ type: "SET_DETAILS_TWO", payload: { faqs: data.faqs, amenities: data.amenities, addons: data.addons, pictures: uploadedImages, pictureIds: uploadedIds } }); + globalDispatch({ type: "STOP_LOADING" }); + + navigate("/spaces/add/3"); + window.scrollTo({ top: 0, left: 0 }); + }; + + const onSaveDraft = async () => { + const host_id = localStorage.getItem("user"); + globalDispatch({ type: "START_LOADING" }); + var propertyResult, propertySpaceResult; + + try { + if (!spaceData.property_id) { + sdk.setTable("property"); + propertyResult = await sdk.callRestAPI( + { + address_line_1: spaceData.address_line_1, + address_line_2: spaceData.address_line_2, + city: spaceData.city, + country: spaceData.country, + zip: spaceData.zip, + status: 1, + verified: 1, + host_id, + name: spaceData.name, + rule: spaceData.rule, + }, + "POST", + ); + dispatch({ type: "SET_PROPERTY_ID", payload: propertyResult.message }); + } + + // create space + if (!spaceData.property_space_id) { + sdk.setTable("property_spaces"); + propertySpaceResult = await sdk.callRestAPI( + { + property_id: propertyResult?.message ?? spaceData.property_id, + space_id: spaceData.category, + max_capacity: spaceData.max_capacity, + description: spaceData.description, + rate: spaceData.rate, + space_status: SPACE_STATUS.UNDER_REVIEW, + availability: SPACE_VISIBILITY.VISIBLE, + draft_status: DRAFT_STATUS.IMAGES, + additional_guest_rate: spaceData.additional_guest_rate || undefined, + size: spaceData.size || undefined, + }, + "POST", + ); + } + + // create property add ons + sdk.setTable("property_add_on"); + for (let i = 0; i < formValues.addons.length; i++) { + const addon_id = formValues.addons[i]; + const propertyAddonResult = await sdk.callRestAPI( + { + property_id: propertyResult?.message ?? spaceData.property_id, + add_on_id: addon_id, + }, + "POST", + ); + } + + // create property space images + for (let i = 0; i < pictures.length; i++) { + sdk.setTable("property_spaces_images"); + const file = pictures[i]; + + const upload = await handleImageUpload(file); + if (upload.id) { + const propertySpaceImagesResult = await sdk.callRestAPI( + { + property_id: propertyResult?.message ?? spaceData.property_id, + property_spaces_id: propertySpaceResult.message, + photo_id: upload.id, + is_approved: IMAGE_STATUS.IN_REVIEW, + }, + "POST", + ); + } + if (file?.name == formValues.thumbnail) { + sdk.setTable("property_spaces"); + const defaultImageResult = await sdk.callRestAPI( + { + id: propertySpaceResult.message, + default_image_id: upload.id, + // is_approved: IMAGE_STATUS.APPROVED, + }, + "PUT", + ); + } + } + + // create property space faqs + sdk.setTable("property_space_faq"); + for (let i = 0; i < formValues.faqs.length; i++) { + const faq = formValues.faqs[i]; + const propertySpaceFaqResult = await sdk.callRestAPI( + { + property_space_id: propertySpaceResult.message, + question: faq.question, + answer: faq.answer, + }, + "POST", + ); + } + + // create property space amenities + sdk.setTable("property_spaces_amenitites"); + for (let i = 0; i < formValues.amenities.length; i++) { + const amenity_id = formValues.amenities[i]; + const propertySpaceAmenityResult = await sdk.callRestAPI( + { + property_spaces_id: propertySpaceResult.message, + amenity_id, + }, + "POST", + ); + } + navigate("/account/my-spaces"); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + }; + + useEffect(() => { + setValue("amenities", spaceData.amenities); + }, [amenities]); + + useEffect(() => { + setValue("addons", spaceData.addons); + }, [addons]); + + useEffect(() => { + for (let i = 0; i < spaceData.pictures.length; i++) { + const url = spaceData.pictures[i]; + getFileFromUrl(url).then((picFile) => { + setPictures((prev) => { + var copy = [...prev]; + copy[i] = picFile; + return copy; + }); + }); + } + }, []); + + function isCatOthers(){ + const cat = spaceCategories.find((cat) => Number(cat.id) == Number(spaceData.category)) + if (cat?.category === "Others") { + return true + } else return false + } + + return ( +
    +
    +

    Space Details

    +
    +

    * Photographs of the space

    +

    file type (jpeg/png/svg), max size (5MB), suggest resolution (640*480)

    +
    + {pictures.map((file, idx) => { + // add FileUploader logic here + })} +
    +

    * Select thumbnail image

    + pic?.name)} + labelField="name" + valueField="name" + containerClassName="mb-12" + className="w-full border py-2 px-3 focus:outline-primary" + openClassName="ring-primary ring-2" + placeholder={"Select thumbnail"} + control={control} + name="thumbnail" + /> +

    + What do you offer with the space (optional) +

    +
    + + +
    +
    + {amenities + ?.filter((am) => { + if (Array.isArray(selectedAmenities)) { + return selectedAmenities?.includes(String(am.id)); + } + return false; + }).sort((a, b) => (a.space_id === null ? -1 : 1) - (b.space_id === null ? -1 : 1)) + .map((am) => ( +
  • + + {am.name} +
  • + ))} +
    +

    + Add-ons (optional) +

    +
    + + +
    + +
    + {addons + ?.filter((addon) => { + if (Array.isArray(selectedAddons)) { + return selectedAddons?.includes(String(addon.id)); + } + return false; + }).sort((a, b) => (a.space_id === null ? -1 : 1) - (b.space_id === null ? -1 : 1)) + .map((addon) => ( +
  • + + {addon.name}
  • + ))} +
    +

    + Frequently asked question (optional) +

    +

    These FAQs will show as part of your space listing.

    +
    + {fields.map((field, index) => ( +
    +
    + + +
    + +
    + + setValue(`faqs.${index}.answer`, content)} + placeholder="" + hideToolbar={true} + setOptions={{ resizingBar: false }} + defaultValue={getValues().faqs[index].answer} + /> +
    + ))} + + +
    +
    +
    +

    +

    +
    + + View More + +
    +
    + +
    +
    setAddAmenitiesPopup(false)} + > +
    e.stopPropagation()} + > +
    +

    Select Amenities

    + +
    +
    + {isCatOthers() ? + amenities.sort((a, b) => (a.creator_id !== 1 ? -1 : 1) - (b.creator_id !== 1 ? -1 : 1)).map((am) => ( +
    + + +
    + )) + : + amenities.filter((am) => (am.space_id === Number(spaceData.category)) || am.creator_id === Number(localStorage.getItem("user"))).sort((a, b) => (a.creator_id !== 1 ? -1 : 1) - (b.creator_id !== 1 ? -1 : 1)).map((am) => ( +
    + + +
    + )) + } +
    +
    +
    +
    setAddAddonsPopup(false)} + > +
    e.stopPropagation()} + > +
    +

    Select Addons

    + +
    +
    + {isCatOthers() ? + addons.sort((a, b) => (a.creator_id !== 1 ? -1 : 1) - (b.creator_id !== 1 ? -1 : 1)).map((addon) => ( +
    + + +
    + )) + : + addons.sort((a, b) => (a.creator_id !== 1 ? -1 : 1) - (b.creator_id !== 1 ? -1 : 1)).filter((ad) => ad.space_id === Number(spaceData.category) || ad.creator_id === Number(localStorage.getItem("user"))) + .map((addon) => ( +
    + + +
    + )) + + } +
    +
    +
    + +
    + + {addOnModal && + + } +
    + ); +}; + +export default SpaceDetailsTwo; diff --git a/src/pages/Host/Spaces/Add/SpaceSubmitted.jsx b/src/pages/Host/Spaces/Add/SpaceSubmitted.jsx new file mode 100644 index 0000000..6395bc3 --- /dev/null +++ b/src/pages/Host/Spaces/Add/SpaceSubmitted.jsx @@ -0,0 +1,43 @@ +import React from "react"; +import { Link } from "react-router-dom"; +import GreenCheckIcon from "@/components/frontend/icons/GreenCheckIcon"; + +const SpaceSubmitted = () => ( +
    +
    +
    + +

    Space successfully submitted

    +
    + + Go to my spaces + +
    +
    +

    + + + + What's next? +

    +

    Our team will review the space and get back to you shortly. It usually takes up to 24 hrs. We will email you when there’s an update..

    +
    +
    +); + +export default SpaceSubmitted; diff --git a/src/pages/Host/Spaces/Add/TemplateCard.jsx b/src/pages/Host/Spaces/Add/TemplateCard.jsx new file mode 100644 index 0000000..b099c25 --- /dev/null +++ b/src/pages/Host/Spaces/Add/TemplateCard.jsx @@ -0,0 +1,112 @@ +import React, { useState } from "react"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import { parseJsonSafely } from "@/utils/utils"; +import { formatAMPM, daysMapping } from "@/utils/date-time-utils"; +import { useContext } from "react"; +import { GlobalContext } from "@/globalContext"; +import EditTemplateModal from "./EditTemplateModal"; +import TrashIcon from "@/components/frontend/icons/TrashIcon"; +import PencilIcon from "@/components/frontend/icons/PencilIcon"; +import ThreeDotsMenu from "@/components/frontend/ThreeDotsMenu"; + +export default function TemplateCard({ data, forceRender }) { + const [editPopup, setEditPopup] = useState(false); + const { dispatch: globalDispatch } = useContext(GlobalContext); + + const parsedSlots = parseJsonSafely(data.slots, []); + + async function deleteTemplate(id) { + globalDispatch({ type: "START_LOADING" }); + try { + await callCustomAPI("host/schedule-slot/template", "delete", { id }, ""); + if (forceRender) forceRender(new Date()); + } catch (err) { + globalDispatch({ type: "STOP_LOADING" }); + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + return ( +
    +
    +
    +
    +

    {data.template_name}

    +

    + ( + {daysMapping + .filter((day) => data[day] == 1) + .map((day, i, arr) => { + return day + (i == arr.length - 1 ? "" : ", "); + })} + ) +

    +
    +
    + , + onClick: () => setEditPopup(true), + }, + { + label: "Delete", + icon: , + onClick: () => deleteTemplate(data.id), + }, + ]} + menuClassName="right-[unset] left-0 origin-top-left" + /> +
    +
    +
    + {Array.isArray(parsedSlots) && + parsedSlots.slice(0, 2).map((slot, idx) => ( +
    +

    Slot {idx + 1}:

    +

    + {formatAMPM(slot.start)} - {formatAMPM(slot.end)} +

    +
    + ))} +
    +
    +
    +
    + + +
    + + { + if (forceRender) forceRender(); + }} + modalOpen={editPopup} + closeModal={() => setEditPopup(false)} + /> +
    + ); +} diff --git a/src/pages/Host/Spaces/Add/spaceContext.jsx b/src/pages/Host/Spaces/Add/spaceContext.jsx new file mode 100644 index 0000000..320fb55 --- /dev/null +++ b/src/pages/Host/Spaces/Add/spaceContext.jsx @@ -0,0 +1,84 @@ +import moment from "moment"; +import React, { createContext, useContext, useReducer } from "react"; + +const initialSpaceData = { + category: "", + name: "", + rate: "", + max_capacity: 0, + description: "", + rule: "", + zip: "", + country: "", + city: "", + address_line_1: "", + address_line_2: "", + additional_guest_rate: "", + size: 0, + pictures: [null, null, null, null, null, null], + pictureIds: [], + faqs: [{ question: "", answer: "" }], + thumbnail: "", + addons: [], + amenities: [], + customSlots: {}, + schedule_template: {} +}; + +function lastWeek(today) { + return moment(today).subtract(1, "week").format("MM/DD/YY"); +} + +function addWeekToDate(slots) { + if (!Array.isArray(slots)) return []; + return slots.map((slot) => ({ start: moment(slot.start).add(1, "week").toISOString(), end: moment(slot.end).add(1, "week").toISOString() })); +} + +const reducer = (state, action) => { + switch (action.type) { + case "SET_PROPERTY_ID": + return { ...state, property_id: action.payload }; + case "SET_DETAILS_ONE": + return { ...state, ...action.payload }; + case "SET_DETAILS_TWO": + return { ...state, ...action.payload }; + case "SET_THUMBNAIL": + return { ...state, thumbnail: action.payload }; + case "SET_DAY_SLOT": + return { ...state, customSlots: { ...state.customSlots, [action.payload.day]: action.payload.slots } }; + case "CLEAR_ALL_SLOTS": + return { ...state, customSlots: {} }; + case "CLEAR_DAY_SLOT": + return { ...state, customSlots: { ...state.customSlots, [action.payload]: undefined } }; + case "INHERIT_DAY_SLOT": + return { ...state, customSlots: { ...state.customSlots, [action.payload]: addWeekToDate(state.customSlots[lastWeek(action.payload)]) } }; + case "SET_SCHEDULE_TEMPLATE": + return { ...state, schedule_template: action.payload }; + case "SET_DESCRIPTION": + return { ...state, description: action.payload }; + case "SET_PROPERTY_NAME": + return { ...state, name: action.payload }; + case "SET_AMENITIES": + return { ...state, amenities: action.payload }; + case "SET_ADDONS": + return { ...state, addons: action.payload }; + case "SET_RULE": + return { ...state, rule: action.payload }; + default: + return state; + } +}; + +// create context here +const spaceContext = createContext({}); + +// wrap this component around App.tsx to get access to userData in all components +const SpaceContextProvider = ({ children }) => { + const [spaceData, dispatch] = useReducer(reducer, initialSpaceData); + + return {children}; +}; + +// use this custom hook to get the data in any component in component tree +const useSpaceContext = () => useContext(spaceContext); +export { useSpaceContext, SpaceContextProvider }; diff --git a/src/pages/Host/Spaces/DeleteSpaceConfirmation.jsx b/src/pages/Host/Spaces/DeleteSpaceConfirmation.jsx new file mode 100644 index 0000000..9013d33 --- /dev/null +++ b/src/pages/Host/Spaces/DeleteSpaceConfirmation.jsx @@ -0,0 +1,108 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment, useState } from "react"; +import { useContext } from "react"; +import { useNavigate } from "react-router"; +import { LoadingButton } from "@/components/frontend"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; + +export default function DeleteSpaceConfirmation({ modalOpen, closeModal, propertySpace }) { + const { dispatch: authDispatch, state: authState } = useContext(AuthContext); + const { state: globalState, dispatch: globalDispatch } = useContext(GlobalContext); + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + const [ctrl] = useState(new AbortController()); + + async function deleteSpace() { + setLoading(true); + try { + const sdk = new MkdSDK(); + sdk.setTable("property_spaces"); + await sdk.callRestAPI({ id: propertySpace.id }, "DELETE", ctrl.signal); + closeModal(); + navigate("/account/my-spaces"); + } catch (err) { + if (err.name == "AbortError") { + setLoading(false); + return; + } + tokenExpireError(authDispatch, err.message); + globalDispatch({ type: "SHOW_ERROR", payload: { heading: "Delete Failed", message: err.message } }); + } + setLoading(false); + } + + return ( + + + +
    + + +
    +
    + + + + Are you sure? + +
    +

    Are you sure you want to delete this space?. This change is not reversible

    +
    + +
    + + + Yes I'm sure + +
    +
    +
    +
    +
    +
    +
    + ); +} diff --git a/src/pages/Host/Spaces/Edit/EditAddonsModal.jsx b/src/pages/Host/Spaces/Edit/EditAddonsModal.jsx new file mode 100644 index 0000000..94e4c7e --- /dev/null +++ b/src/pages/Host/Spaces/Edit/EditAddonsModal.jsx @@ -0,0 +1,260 @@ +import useAddonCategories from "@/hooks/api/useAddonCategories"; +import { Dialog, Transition } from "@headlessui/react"; +import { Fragment, useState } from "react"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { LoadingButton } from "@/components/frontend"; + +export default function EditAddonsModal({ modalOpen, category, closeModal, propertyAddons, id, property_id, forceRender }) { + const addons = useAddonCategories(id, category == "Others"); + const [loading, setLoading] = useState(false) + const schema = yup + .object({ + name: yup.string() + }) + .required(); + + const { + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + let prop_addons = propertyAddons; + + let ids = prop_addons.map(obj => Number(obj.add_on_id)) + const sdk = new MkdSDK(); + + async function addOrRemoveAddon(event) { + const checkbox = event.target; + const id = parseInt(checkbox.value); + + if (checkbox.checked) { + // If the checkbox is checked, add the ID to the array if it doesn't exist + if (!ids.includes(id)) { + ids.push(id); + } + } else { + // If the checkbox is unchecked, remove the ID from the array if it exists + const index = ids.indexOf(id); + if (index !== -1) { + ids.splice(index, 1); + } + } + } + + const fetchSelectedAddons = () => { + const selectedCheckboxes = document.querySelectorAll('.addon-checkbox:checked'); + const selectedIds = Array.from(selectedCheckboxes).map(checkbox => parseInt(checkbox.value)); + return selectedIds; + }; + + function arraysHaveSameContent(arr1, arr2) { + // If the arrays have different lengths, they can't be the same + if (arr1.length !== arr2.length) { + return false; + } + + // Sort both arrays + const sortedArr1 = arr1.slice().sort(); + const sortedArr2 = arr2.slice().sort(); + + // Compare the sorted arrays as strings + return JSON.stringify(sortedArr1) === JSON.stringify(sortedArr2); + } + + function findArrayDifferences(arr1, arr2) { + // Find elements in arr2 that are not in arr1 + const toAdd = arr2.filter(item => !arr1.includes(item)); + // Find elements in arr1 that are not in arr2 + const toRemove = arr1.filter(item => !arr2.includes(item)); + + return { toAdd, toRemove }; + } + + async function updateAddons() { + setLoading(true); + // edit property + sdk.setTable("property_add_on"); + const data = prop_addons.map(obj => Number(obj.add_on_id)) + const selectedAddonsIds = fetchSelectedAddons(); + + if (arraysHaveSameContent(data, selectedAddonsIds)) { + setLoading(false); + closeModal() + return; + } + else { + const { toAdd, toRemove } = findArrayDifferences(data, selectedAddonsIds); + + for (const addonId of toRemove) { + const addon = prop_addons.find((p_am) => p_am.add_on_id === addonId); + if (addon) { + await sdk.callRestAPI({ id: Number(addon.id) }, "DELETE"); + } + } + + for (const addonId of toAdd) { + await sdk.callRestAPI( + { + property_id, + add_on_id: Number(addonId), + }, + "POST" + ); + } + + setLoading(false); + closeModal(); + forceRender(); // Force re-render to reflect changes + + // findArrayDifferences(data, ids) + // const diff = findArrayDifferences(data, ids) + // const diff2 = findArrayReverseDifferences(ids, data) + + // if (diff.length > 0) { + // for (let i = 0; i < diff.length; i++) { + // const am_ = diff[i]; + // const am_id = prop_addons?.find((p_am) => p_am.add_on_id === am_) + // if (am_id !== undefined) { + // await sdk.callRestAPI( + // { + // id: Number(am_id?.id), + // }, + // "DELETE", + // ); + + // prop_addons.filter((ad) => Number(ad.add_on_id) !== Number(am_)) + // } + // for (let i = 0; i < prop_addons.length; i++) { + // if (prop_addons[i].add_on_id === Number(am_)) { + // prop_addons.splice(i, 1); + // break; // Assuming there's only one object with add_on_id 37 + // } + // } + + // } + // } + + // if (diff2.length > 0) { + // for (let i = 0; i < diff2.length; i++) { + // const am_ = diff2[i]; + // const added_add_on = addons.find((add_on) => Number(add_on.id) == Number(am_)) + // prop_addons.push(added_add_on) + // await sdk.callRestAPI( + // { + // property_id, + // add_on_id: Number(am_), + // }, + // "POST", + // ); + // } + // } + + // setLoading(false); + // closeModal() + } + } + + return ( + <> + + + +
    + + +
    +
    + + + + {" "} + {" "} + Addons + + + {addons.sort((a, b) => (a.creator_id !== 1 ? -1 : 1) - (b.creator_id !== 1 ? -1 : 1)).map((ad, id) => ( +
    + e.add_on_id === ad.id)} + onChange={(e) => addOrRemoveAddon(e)} + /> + +
    + ))} +
    + + + + Update + +
    +
    +
    +
    +
    +
    +
    + + ); +} diff --git a/src/pages/Host/Spaces/Edit/EditAmenitiesModal.jsx b/src/pages/Host/Spaces/Edit/EditAmenitiesModal.jsx new file mode 100644 index 0000000..5daa0f3 --- /dev/null +++ b/src/pages/Host/Spaces/Edit/EditAmenitiesModal.jsx @@ -0,0 +1,211 @@ +import { Dialog, Transition } from "@headlessui/react"; +import { Fragment, useState } from "react"; +import useAmenityCategories from "@/hooks/api/useAmenityCategories"; +import MkdSDK from "@/utils/MkdSDK"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import { LoadingButton } from "@/components/frontend"; + +export default function EditAmenitiesModal({ modalOpen, category, closeModal, propertyAmenities, id, oldAm, p_id, idAm, forceRender }) { + const amenities = useAmenityCategories(id, category == "Others"); + const [loading, setLoading] = useState(false); + const schema = yup + .object({ + name: yup.string() + }) + .required(); + + const { + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + }); + + let propAmenities = propertyAmenities; + let ids = idAm; + + const sdk = new MkdSDK(); + + async function addOrRemoveAmenity(event) { + const checkbox = event.target; + const id = parseInt(checkbox.value); + if (checkbox.checked) { + + if (!ids.includes(id)) { + ids.push(id); + } + } else { + // If the checkbox is unchecked, remove the ID from the array if it exists + const index = ids.indexOf(id); + if (index !== -1) { + ids.splice(index, 1); + } + } + } + + const fetchSelectedAmenities = () => { + const selectedCheckboxes = document.querySelectorAll('.amenity-checkbox:checked'); + const selectedIds = Array.from(selectedCheckboxes).map(checkbox => parseInt(checkbox.value)); + return selectedIds; + }; + + function arraysHaveSameContent(arr1, arr2) { + // If the arrays have different lengths, they can't be the same + if (arr1.length !== arr2.length) { + return false; + } + + // Compare the sorted arrays as strings + return JSON.stringify(arr1.slice().sort()) === JSON.stringify(arr2.slice().sort()); + } + + function findArrayDifferences(arr1, arr2) { + // Find elements in arr2 that are not in arr1 + const toAdd = arr2.filter(item => !arr1.includes(item)); + // Find elements in arr1 that are not in arr2 + const toRemove = arr1.filter(item => !arr2.includes(item)); + + return { toAdd, toRemove }; + } + + async function updateAmenities() { + setLoading(true); + // edit property + sdk.setTable("property_spaces_amenitites"); + const data = propAmenities.map(obj => Number(obj.amenity_id)); + const selectedAmenityIds = fetchSelectedAmenities(); + + if (arraysHaveSameContent(data, selectedAmenityIds)) { + setLoading(false); + closeModal(); + return; + } else { + const { toAdd, toRemove } = findArrayDifferences(data, selectedAmenityIds); + + for (const amenityId of toRemove) { + const amenity = propAmenities.find((p_am) => p_am.amenity_id === amenityId); + if (amenity) { + await sdk.callRestAPI({ id: Number(amenity.id) }, "DELETE"); + } + } + + for (const amenityId of toAdd) { + await sdk.callRestAPI( + { + property_spaces_id: p_id, + amenity_id: Number(amenityId), + }, + "POST" + ); + } + + setLoading(false); + closeModal(); + forceRender(); // Force re-render to reflect changes + } + } + + return ( + <> + + + +
    + + +
    +
    + + + + {" "} + Amenities + + + {amenities.sort((a, b) => (a.creator_id !== 1 ? -1 : 1) - (b.creator_id !== 1 ? -1 : 1)).map((am) => ( +
    + e.amenity_id === am.id)} + onChange={(e) => addOrRemoveAmenity(e)} + /> + +
    + ))} + +
    + + + + Update + +
    + +
    +
    +
    +
    +
    +
    + + ); +} diff --git a/src/pages/Host/Spaces/Edit/EditDescriptionModal.jsx b/src/pages/Host/Spaces/Edit/EditDescriptionModal.jsx new file mode 100644 index 0000000..4727162 --- /dev/null +++ b/src/pages/Host/Spaces/Edit/EditDescriptionModal.jsx @@ -0,0 +1,95 @@ +import { Dialog, Transition } from "@headlessui/react"; +import { Fragment } from "react"; +import { useSpaceContext } from "./spaceContext"; + + +export default function EditDescriptionModal({ modalOpen, closeModal, propertyDescription }) { + const { state, dispatch } = useSpaceContext(); + + async function onSubmit(e) { + e.preventDefault(); + const formData = new FormData(e.target); + const description = formData.get("description"); + dispatch({ type: "SET_DESCRIPTION", payload: description }); + closeModal(); + } + + return ( + <> + + + +
    + + +
    +
    + + + + {" "} + {" "} + Description + + + +
    + +
    +
    +
    +
    +
    +
    +
    + + ); +} diff --git a/src/pages/Host/Spaces/Edit/EditFaqsModal.jsx b/src/pages/Host/Spaces/Edit/EditFaqsModal.jsx new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/Host/Spaces/Edit/EditPropertNameModal.jsx b/src/pages/Host/Spaces/Edit/EditPropertNameModal.jsx new file mode 100644 index 0000000..76aef1c --- /dev/null +++ b/src/pages/Host/Spaces/Edit/EditPropertNameModal.jsx @@ -0,0 +1,96 @@ +import MkdSDK from "@/utils/MkdSDK"; +import { Dialog, Transition } from "@headlessui/react"; +import { Fragment } from "react"; +import { useSpaceContext } from "./spaceContext"; + +export default function EditPropertyNameModal({ modalOpen, closeModal, name }) { + const { spaceData, dispatch } = useSpaceContext(); + + async function onSubmit(e) { + e.preventDefault(); + const formData = new FormData(e.target); + const name = formData.get("name"); + dispatch({ type: "SET_PROPERTY_NAME", payload: name }); + closeModal(); + } + + return ( + <> + + + +
    + + +
    +
    + + + + {" "} + {" "} + Space name + + + + +
    + +
    +
    +
    +
    +
    +
    +
    + + ); +} diff --git a/src/pages/Host/Spaces/Edit/EditPropertyDetails.jsx b/src/pages/Host/Spaces/Edit/EditPropertyDetails.jsx new file mode 100644 index 0000000..eaa524f --- /dev/null +++ b/src/pages/Host/Spaces/Edit/EditPropertyDetails.jsx @@ -0,0 +1,503 @@ +import React from "react"; +import { useState } from "react"; +import { Link, useLocation, useNavigate, useOutletContext, useParams } from "react-router-dom"; +import FaqAccordion from "@/components/frontend/FaqAccordion"; +import { useSpaceContext } from "./spaceContext"; +import { useEffect } from "react"; +import MkdSDK from "@/utils/MkdSDK"; +import { useContext } from "react"; +import { GlobalContext } from "@/globalContext"; +import DateTimePicker from "@/components/frontend/DateTimePicker"; +import { useForm } from "react-hook-form"; +import CustomSelect from "@/components/frontend/CustomSelect"; +import { DRAFT_STATUS, ID_VERIFICATION_STATUSES, IMAGE_STATUS, NOTIFICATION_STATUS, NOTIFICATION_TYPE, SPACE_STATUS, SPACE_VISIBILITY } from "@/utils/constants"; +import ReCAPTCHA from "react-google-recaptcha"; +import PropertyImageSlider from "@/components/frontend/PropertyImageSlider"; +import { parseJsonSafely } from "@/utils/utils"; +import PropertySpaceMapImage from "@/components/frontend/PropertySpaceMapImage"; +import CircleCheckIcon from "@/components/frontend/icons/CircleCheckIcon"; +import PencilIcon from "@/components/frontend/icons/PencilIcon"; +import AccountNotVerifiedModal from "../Add/AccountNotVerifiedModal"; +import { usePropertyAddons, usePropertySpace, usePropertySpaceAmenities, usePropertySpaceFaqs, usePropertySpaceImages, usePropertySpaceReviews, usePublicUserData } from "@/hooks/api"; +import PropertyEditImageSlider from "@/components/frontend/PropertyEditImageSlider"; +import EditDescriptionModal from "./EditDescriptionModal"; +import EditPropertyNameModal from "./EditPropertNameModal"; +import EditAddonsModal from "./EditAddonsModal"; +import EditPropertyRulesModal from "./EditPropertyRulesModal"; +import axios from "axios"; +import usePropertySpaceImagesV2 from "@/hooks/api/usePropertySpaceImagesV2"; +import EditAmenitiesModal from "./EditAmenitiesModal"; +import useAmenityCategories from "@/hooks/api/useAmenityCategories"; +import useAddonCategories from "@/hooks/api/useAddonCategories"; + +const EditPropertyDetails = () => { + const { id } = useParams(); + const [galleryOpen, setGalleryOpen] = useState(false); + const [render, forceRender] = useState(false); + + const { propertySpace, notFound } = usePropertySpace(id, render); + const spaceImages = usePropertySpaceImagesV2(propertySpace.id, false); + const { state: scheduleTemplate } = useLocation(); + const [editAmenities, setEditAmenities] = useState(false); + const [editAddons, setEditAddons] = useState(false); + const spaceAddons = usePropertyAddons(propertySpace.property_id, editAddons); + const spaceAmenities = usePropertySpaceAmenities(propertySpace.id, editAmenities); + + const faqs = usePropertySpaceFaqs(propertySpace.id); + const reviews = usePropertySpaceReviews(propertySpace.id); + const [recaptchaValidated, setRecaptchaValidated] = useState(false); + const [showMap, setShowMap] = useState(false); + const { spaceData, dispatch } = useSpaceContext(); + + const amenities = useAmenityCategories(propertySpace?.space_id, propertySpace?.category == "Others" ? true : false); + + let newAm = amenities.map(obj => Number(obj.id)); + + const addons = useAddonCategories(propertySpace?.space_id, propertySpace?.category == "Others" ? true : false); + + let newAdd = addons.map(obj => Number(obj.id)); + + // const { amenities, addons } = useOutletContext(); + + const sdk = new MkdSDK(); + + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + + const navigate = useNavigate(); + + const { register, setValue } = useForm(); + const [showCalendar, setShowCalendar] = useState(false); + const [editDescription, setEditDescription] = useState(false); + const [editPropertyName, setEditPropertyName] = useState(false); + const [editPropertyRules, setEditPropertyRules] = useState(false); + + const [accountNotVerified, setAccountNotVerified] = useState(false); + + useEffect(() => { + dispatch({ type: "SET_DESCRIPTION", payload: propertySpace.description }) + dispatch({ type: "SET_ADDONS", payload: spaceAddons }) + dispatch({ type: "SET_AMENITIES", payload: spaceAmenities }) + dispatch({ type: "SET_RULE", payload: propertySpace.rule }) + dispatch({ type: "SET_PROPERTY_NAME", payload: propertySpace.name }) + }, []); + + // Read values passed on state + + + const onSubmit = async (e) => { + if (e) e.preventDefault(); + console.log("submitting"); + if (globalState.user.verificationStatus != ID_VERIFICATION_STATUSES.VERIFIED && !accountNotVerified) { + setAccountNotVerified(true); + return; + } + globalDispatch({ type: "START_LOADING" }); + const host_id = Number(localStorage.getItem("user")); + + try { + // edit property + sdk.setTable("property"); + const propertyResult = await sdk.callRestAPI( + { + id: id, + name: spaceData?.name ? spaceData?.name : propertySpace?.name, + rule: spaceData?.rule ? spaceData?.rule : propertySpace?.rule, + }, + "PUT", + ); + + // approve property space + await axios.post("https://ergo.mkdlabs.com/rest/property_spaces/PUT", + { id: Number(id), draft_status: DRAFT_STATUS.COMPLETED }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + "x-project": "ZXJnbzprNWdvNGw1NDhjaDRxazU5MTh4MnVsanV2OHJxcXAyYXM", + }, + }, + ); + + // create notification + sdk.setTable("notification"); + await sdk.callRestAPI( + { + user_id: host_id, + actor_id: null, + action_id: id, + notification_time: new Date().toISOString().split(".")[0], + message: "Space Draft Status Completed", + type: NOTIFICATION_TYPE.CREATE_SPACE, + status: NOTIFICATION_STATUS.NOT_ADDRESSED, + }, + "POST", + ); + navigate("/spaces/add/5"); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + window.scrollTo({ top: 0, left: 0 }); + }; + + const onChange = () => { + setRecaptchaValidated(true); + }; + + return ( +
    { + setShowCalendar(false); + }} + > +

    Review

    +

    Below is how people will see your listing:

    +
    +
    +

    + {spaceData?.name ? spaceData?.name : propertySpace?.name} +

    + +
    + +
    +
    +
    +
    + + +
    + + + +
    + +
    +
    +
    +
    +
    +

    Description

    +

    {spaceData?.description ? spaceData?.description : propertySpace?.description}

    +
    +
    + +
    +
    +
    +
    +
    +
    +

    Amenities

    +
    + +
    +
    + +
      + {(spaceAmenities)?.map((am) => ( +
    • + + {am.amenity_name} +
    • + ))} +
    +
    + +
    +
    +
    +
    +
    +

    Add ons

    +
    + +
    +
    +
      + {(spaceAddons)?.map((addon) => ( +
    • + +
      + {addon.add_on_name ?? addon.name} +
      +
      + ${addon.cost}/h +
    • + ))} +
    +
    + +
    +
    +
    +
    +

    About the host

    +
    +
    + +
    +
    +

    {globalState.user.first_name}

    +

    {globalState.user.last_name}

    +
    +

    {globalState.user.about}

    +
    + +
    +
    +
    +
    +
    +

    Reviews

    + +
    +
    +
    +
    +

    FAQs

    + {faqs.map((faq, idx) => ( + + ))} +
    +
    +
    +
    +

    Property rules

    +

    {spaceData?.rule ? spaceData?.rule : propertySpace?.rule}

    + +
    +
    + +
    +
    +
    + {/*
    +
    +

    Price and availability

    +
    + Max capacity + + {" "} + {propertySpace.max_capacity} people + +
    +
    + Pricing fro + + from: ${propertySpace?.rate}/h + +
    + {propertySpace.additional_guest_rate || (propertySpace.max_capacity > 1) ? ( +
    + Additional guests + + from: ${propertySpace.additional_guest_rate}/h + +
    + ) : null} +
    +
    +
    + +
    + +
    +
    +
    */} +
    +

    Price and availability

    +
    + Max capacity + + {" "} + {propertySpace.max_capacity ?? propertySpace?.max_capacity} people + +
    +
    + Pricing from + + from: ${propertySpace.rate}/h + +
    + {(propertySpace.additional_guest_rate && (propertySpace.max_capacity > 1)) ? ( +
    + Additional guests + + from: ${propertySpace.additional_guest_rate}/h + +
    + ) : null} + +
    +
    + +
    + +
    +
    +
    + v != null)} + modalOpen={galleryOpen} + closeModal={() => setGalleryOpen(false)} + /> + +
    + +
    + setShowMap(false)} + /> + setEditDescription(false)} + /> + setEditPropertyName(false)} + /> + setEditAmenities(false)} + idAm={newAm} + p_id={id} + forceRender={forceRender} + /> + setEditAddons(false)} + idAm={newAdd} + property_id={propertySpace?.property_id} + p_id={id} + forceRender={forceRender} + /> + setEditPropertyRules(false)} + /> + setAccountNotVerified(false)} + onSubmit={onSubmit} + /> +
    + ); +}; + +export default EditPropertyDetails; diff --git a/src/pages/Host/Spaces/Edit/EditPropertyImagesPage.jsx b/src/pages/Host/Spaces/Edit/EditPropertyImagesPage.jsx new file mode 100644 index 0000000..3765dd0 --- /dev/null +++ b/src/pages/Host/Spaces/Edit/EditPropertyImagesPage.jsx @@ -0,0 +1,647 @@ +import { yupResolver } from "@hookform/resolvers/yup"; +import React from "react"; +import { useState } from "react"; +import { FileUploader } from "react-drag-drop-files"; +import { useFieldArray, useForm } from "react-hook-form"; +import * as yup from "yup"; +import SunEditor from "suneditor-react"; +import "suneditor/dist/css/suneditor.min.css"; +import { Navigate, useNavigate, useOutletContext, useParams } from "react-router"; +import { useEffect } from "react"; +import MkdSDK from "@/utils/MkdSDK"; +import useDelayUnmount from "@/hooks/useDelayUnmount"; +import { useContext } from "react"; +import { GlobalContext } from "@/globalContext"; +import { Link, useSearchParams } from "react-router-dom"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import axios from "axios"; +import { DRAFT_STATUS, IMAGE_STATUS, NOTIFICATION_STATUS, NOTIFICATION_TYPE } from "@/utils/constants"; +import CustomSelectV2 from "@/components/CustomSelectV2"; +import { usePropertyAddons, usePropertySpace, usePropertySpaceAmenities, usePropertySpaceFaqs } from "@/hooks/api"; +import useAmenityCategories from "@/hooks/api/useAmenityCategories"; +import useAddonCategories from "@/hooks/api/useAddonCategories"; +import { useSpaceContext } from "./spaceContext"; +import useCancellation from "@/hooks/api/useCancellation"; +import { sanitizeAndTruncate } from "@/utils/utils"; +import CircleCheckIcon from "@/components/frontend/icons/CircleCheckIcon"; + + +const EditPropertyImagesPage = () => { + const [searchParams] = useSearchParams(); + const mode = searchParams.get("mode") ?? "edit"; + const [draftType, setDraftType] = useState(""); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const { spaceData, dispatch } = useSpaceContext(); + const [pictures, setPictures] = useState([null, null, null, null, null, null]); + const { id } = useParams(); + const { propertySpace, notFound } = usePropertySpace(id); + + const [addAmenitiesPopup, setAddAmenitiesPopup] = useState(false); + const showAddAmenitiesPopup = useDelayUnmount(addAmenitiesPopup, 100); + const [addAddonsPopup, setAddAddonsPopup] = useState(false); + const showAddAddonsPopup = useDelayUnmount(addAddonsPopup, 100); + const [scheduleTemplate, setScheduleTemplate] = useState({}); + const cancellationPolicy = useCancellation(); + + const amenities = useAmenityCategories(propertySpace.space_id, propertySpace.category === "Others" ? true : false); + const addons = useAddonCategories(propertySpace.space_id, propertySpace.category === "Others" ? true : false); + + const propertyAmenities = usePropertySpaceAmenities(id); + const propertyAddons = usePropertyAddons(propertySpace.property_id); + const propertyFaqs = usePropertySpaceFaqs(id); + + const [pictureIds, setPictureIds] = useState([]); + + const sdk = new MkdSDK(); + + const navigate = useNavigate(); + const schema = yup.object({ + question: yup.string(), + }); + + const { + register, + handleSubmit, + getValues, + setValue, + watch, + control, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + amenities: [], + addons: [], + faqs: [], + thumbnail: "", + }, + }); + + const selectedAmenities = watch("amenities"); + const selectedAddons = watch("addons"); + + const { fields, append, remove } = useFieldArray({ + control, + name: "faqs", + }); + + async function getFileFromUrl(url) { + if (url.photo_url === undefined) return null; + const filename = url.photo_url.substring(url.photo_url.lastIndexOf("/") + 1); + let response = await sdk.fetchImage(filename) + let data = await response.blob(); + let metadata = { + type: url.default_image, + }; + return new File([data], url.photo_url.split("/").pop(), metadata); + } + + const readImage = (file, previewEl) => { + const reader = new FileReader(); + reader.onload = (event) => { + document.getElementById(previewEl).src = event.target.result; + }; + reader.readAsDataURL(file); + }; + + const handleImageUpload = async (file) => { + if (!file) return { url: "", id: null }; + const formData = new FormData(); + formData.append("file", file); + + try { + const upload = await sdk.uploadImage(formData); + return upload; + } catch (error) { + return { url: "", id: null }; + } + }; + + async function changeDraftStatus(space_id, newStatus) { + try { + const result = await axios.post( + "https://ergo.mkdlabs.com/rest/property_spaces/PUT", + { id: Number(space_id), draft_status: newStatus }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + "x-project": "ZXJnbzprNWdvNGw1NDhjaDRxazU5MTh4MnVsanV2OHJxcXAyYXM", + }, + }, + ); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function fetchScheduleTemplate(id) { + try { + const result = await callCustomAPI( + "property_spaces_schedule_template", + "post", + { + page: 1, + limit: 1, + where: [`property_spaces_id = ${id}`], + }, + "PAGINATE", + ); + if (Array.isArray(result.list) && result.list.length > 0) { + setScheduleTemplate({ custom_slots: result.list[0].custom_slots, schedule_id: result.list[0].id }); + } + if (result.list[0]?.schedule_template_id) { + const templateResult = await callCustomAPI( + "schedule_template", + "post", + { + page: 1, + limit: 1, + where: [`id = ${result.list[0].schedule_template_id}`], + }, + "PAGINATE", + ); + if (Array.isArray(templateResult.list) && (templateResult.list[0] ?? {})) { + setScheduleTemplate((prev) => { + let updated = { ...prev, ...templateResult.list[0] }; + return updated; + }); + } + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + const onSubmit = async (data) => { + globalDispatch({ type: "START_LOADING" }); + var addonsToCreate = data.addons.filter((addon) => !propertyAddons.map((addon) => String(addon.add_on_id)).includes(addon)); + var addonsToDelete = propertyAddons.filter((addon) => !data.addons.includes(String(addon.add_on_id))).map((addon) => addon.id); + + var amenitiesToCreate = data.amenities.filter((am) => !propertyAmenities.map((am) => String(am.amenity_id)).includes(am)); + var amenitiesToDelete = propertyAmenities.filter((am) => !data.amenities.includes(String(am.amenity_id))).map((am) => am.id); + + var faqsToCreate = data.faqs.filter((faq) => !propertyFaqs.map((faq) => JSON.stringify(faq)).includes(JSON.stringify(faq))); + var faqsToDelete = propertyFaqs.filter((faq) => !data.faqs.map((faq) => JSON.stringify(faq)).includes(JSON.stringify(faq))).map((faq) => faq.id); + + try { + globalDispatch({ type: "SET_DETAILS_TWO", payload: { faqs: data.faqs, amenities: data.amenities, addons: data.addons } }); + + // create property add ons + sdk.setTable("property_add_on"); + for (let i = 0; i < addonsToCreate.length; i++) { + const add_on_id = addonsToCreate[i]; + await sdk.callRestAPI( + { + property_id: propertySpace?.property_id, + add_on_id, + }, + "POST", + ); + } + // delete addons + sdk.setTable("property_add_on"); + for (let i = 0; i < addonsToDelete.length; i++) { + const id = addonsToDelete[i]; + await sdk.callRestAPI({ id }, "DELETE"); + } + + // create property space amenities + sdk.setTable("property_spaces_amenitites"); + for (let i = 0; i < amenitiesToCreate.length; i++) { + const amenity_id = amenitiesToCreate[i]; + await sdk.callRestAPI( + { + property_spaces_id: id, + amenity_id, + }, + "POST", + ); + } + + // delete property space amenities + sdk.setTable("property_spaces_amenitites"); + for (let i = 0; i < amenitiesToDelete.length; i++) { + const id = amenitiesToDelete[i]; + await sdk.callRestAPI({ id }, "DELETE"); + } + + // create property space faqs + sdk.setTable("property_space_faq"); + for (let i = 0; i < faqsToCreate.length; i++) { + const faq = faqsToCreate[i]; + await sdk.callRestAPI( + { + property_space_id: id, + question: faq.question, + answer: faq.answer, + }, + "POST", + ); + } + + // delete property space faqs + sdk.setTable("property_space_faq"); + for (let i = 0; i < faqsToDelete.length; i++) { + const id = faqsToDelete[i]; + await sdk.callRestAPI({ id }, "DELETE"); + } + + let pictureAddedCount = 0; + + // create property space images + for (let i = 0; i < pictures.length; i++) { + sdk.setTable("property_spaces_images"); + const file = pictures[i]; + + const upload = await handleImageUpload(file); + if (upload.id) { + await sdk.callRestAPI( + { + property_id: propertySpace?.property_id, + property_spaces_id: id, + photo_id: upload.id, + is_approved: file?.name == data.thumbnail ? IMAGE_STATUS.APPROVED : IMAGE_STATUS.NOT_APPROVED, + }, + "POST", + ); + pictureAddedCount++; + } + if (file?.name == data.thumbnail) { + sdk.setTable("property_spaces"); + await sdk.callRestAPI( + { + id, + default_image_id: upload.id, + }, + "PUT", + ); + } + } + + if (pictureAddedCount > 0) { + // create notification + sdk.setTable("notification"); + await sdk.callRestAPI( + { + user_id: localStorage.getItem("user"), + action_id: id, + actor_id: null, + notification_time: new Date().toISOString().split(".")[0], + message: "New Property Space Images Added", + type: NOTIFICATION_TYPE.CREATE_PROPERTY_SPACE_IMAGE, + status: NOTIFICATION_STATUS.NOT_ADDRESSED, + }, + "POST", + ); + } + + // delete previous space images + sdk.setTable("property_spaces_images"); + for (let i = 0; i < pictureIds.length; i++) { + const id = pictureIds[i]; + sdk.callRestAPI({ id }, "DELETE"); + } + if (mode == "create") { + await changeDraftStatus(id, DRAFT_STATUS.IMAGES); + } + if (draftType === "continue") { + navigate(`/account/my-spaces/${id}/edit-scheduling?mode=edit`, { state: scheduleTemplate }); + } else { + navigate(`/account/my-spaces/${id}`); + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + }; + + async function fetchPropertySpaceImages() { + const ctrl = new AbortController(); + const where = [`property_spaces_id = ${id} AND ergo_property_spaces_images.deleted_at IS NULL`]; + try { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/property-space-images/PAGINATE", { page: 1, limit: 7, where }, "POST", ctrl.signal); + if (Array.isArray(result.list)) { + setPictureIds(result.list.map((pic) => pic.id)); + for (let i = 0; i < result.list.length; i++) { + const url = result.list[i]; + const picFile = await getFileFromUrl(url); + setPictures((prev) => { + var copy = [...prev]; + copy[i] = picFile; + return copy; + }); + } + } + } catch (err) { + console.log(err) + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + useEffect(() => { + fetchPropertySpaceImages() + fetchScheduleTemplate(id); + }, []) + + useEffect(() => { + if (amenities.length && propertyAmenities.length) { + setValue( + "amenities", + propertyAmenities.map((am) => String(am.amenity_id)), + ); + } + }, [amenities, propertyAmenities]); + + useEffect(() => { + if (addons.length && propertyAddons.length) { + setValue( + "addons", + propertyAddons.map((addon) => String(addon.add_on_id)), + ); + } + }, [addons, propertyAddons]); + + useEffect(() => { + if (propertyFaqs.length) { + setValue("faqs", propertyFaqs); + } + }, [propertyFaqs]); + + if (notFound) return ; + + return ( +
    +
    +

    Edit Space Images, Faqs, Addons & Amenities

    +
    +

    * Photographs of the space

    +

    Provide additional information on the files. file type, max size, suggest resolution etc.

    +
    + {pictures.map((file, idx) => { + return ( + { + setPictures((prev) => { + const copy = [...prev]; + copy[idx] = file; + return copy; + }); + }} + types={["JPEG", "PNG", "JPG"]} + fileOrFiles={file} + key={idx} + > +
    + {pictures[idx]?.name ? ( + + ) : ( +

    + Add Image

    + )} +
    +
    + ); + })} +
    +

    * Select thumbnail image

    + pic?.name)} + labelField="name" + valueField="name" + containerClassName="mb-12" + className="w-full border py-2 px-3 focus:outline-primary" + openClassName="ring-primary ring-2" + placeholder={"Select thumbnail"} + control={control} + name="thumbnail" + /> +

    + What do you offer with the space (optional) +

    + +
    + {amenities + .filter((am) => { + if (Array.isArray(selectedAmenities)) { + return selectedAmenities?.includes(String(am.id)); + } + return false; + }) + .map((am) => ( +
  • + {am.name}
  • + ))} +
    +

    + Add-ons (optional) +

    + +
    + {addons + .filter((addon) => { + if (Array.isArray(selectedAddons)) { + return selectedAddons?.includes(String(addon.id)); + } + return false; + }) + .map((addon) => ( +
  • + {addon.name} +
  • + ))} +
    +

    + Frequently asked question (optional) +

    +

    These FAQs will show as part of your space listing.

    +
    + {fields.map((field, index) => ( +
    +
    + + +
    + +
    + +
    + setValue(`faqs.${index}.answer`, content)} + placeholder="" + hideToolbar={true} + setOptions={{ resizingBar: false, fontSize: 16 }} + defaultValue={getValues().faqs[index].answer} + /> +
    + ))} + + +
    +
    +
    +

    +

    +
    + + View More + +
    +
    + +
    + +
    +
    setAddAmenitiesPopup(false)} + > +
    e.stopPropagation()} + > +
    +

    Select Amenities

    + +
    +
    + {amenities.sort((a, b) => (a.creator_id !== 1 ? -1 : 1) - (b.creator_id !== 1 ? -1 : 1)).map((am) => ( +
    + + +
    + ))} +
    +
    +
    +
    setAddAddonsPopup(false)} + > +
    e.stopPropagation()} + > +
    +

    Select Addons

    + +
    +
    + {addons.sort((a, b) => (a.creator_id !== 1 ? -1 : 1) - (b.creator_id !== 1 ? -1 : 1)).map((addon) => ( +
    + + +
    + ))} +
    +
    +
    +
    +
    + ); +}; + +export default EditPropertyImagesPage; diff --git a/src/pages/Host/Spaces/Edit/EditPropertyRulesModal.jsx b/src/pages/Host/Spaces/Edit/EditPropertyRulesModal.jsx new file mode 100644 index 0000000..04e4fc1 --- /dev/null +++ b/src/pages/Host/Spaces/Edit/EditPropertyRulesModal.jsx @@ -0,0 +1,95 @@ +import { Dialog, Transition } from "@headlessui/react"; +import { Fragment } from "react"; +import { useSpaceContext } from "./spaceContext"; + +export default function EditPropertyRulesModal({ modalOpen, closeModal, rules }) { + const { spaceData, dispatch } = useSpaceContext(); + + async function onSubmit(e) { + e.preventDefault(); + const formData = new FormData(e.target); + const rule = formData.get("rule"); + dispatch({ type: "SET_RULE", payload: rule }); + closeModal(); + } + + return ( + <> + + + +
    + + +
    +
    + + + + {" "} + {" "} + Rule + + + + +
    + +
    +
    +
    +
    +
    +
    +
    + + ); +} diff --git a/src/pages/Host/Spaces/Edit/EditPropertySpacePage.jsx b/src/pages/Host/Spaces/Edit/EditPropertySpacePage.jsx new file mode 100644 index 0000000..d2f7ca1 --- /dev/null +++ b/src/pages/Host/Spaces/Edit/EditPropertySpacePage.jsx @@ -0,0 +1,411 @@ +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import React from "react"; +import { useNavigate, useParams } from "react-router"; +import { useForm } from "react-hook-form"; +import { useEffect } from "react"; +import { useState } from "react"; +import MkdSDK from "@/utils/MkdSDK"; +import { useContext } from "react"; +import { GlobalContext } from "@/globalContext"; +import { useSearchParams } from "react-router-dom"; +import { SPACE_CATEGORY_SIZES, NOTIFICATION_STATUS, NOTIFICATION_TYPE, SPACE_STATUS } from "@/utils/constants"; +import CustomLocationAutoCompleteV2 from "@/components/CustomLocationAutoCompleteV2"; +import CustomSelectV2 from "@/components/CustomSelectV2"; +import CounterV2 from "@/components/CounterV2"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import CustomComboBox from "@/components/CustomComboBox"; +import countries from "@/utils/countries.json"; +import { useSpaceContext } from "./spaceContext"; +import { extractLocationInfo } from "@/utils/utils"; +const sdk = new MkdSDK(); +const ctrl = new AbortController(); + +const EditPropertySpacePage = () => { + const { dispatch: authDispatch } = useContext(AuthContext); + const { spaceData, dispatch } = useSpaceContext(); + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const [searchParams] = useSearchParams(); + const mode = searchParams.get("mode"); + const { id } = useParams(); + const [currSpace, setCurrSpace] = useState({}); + const [draftType, setDraftType] = useState(""); + + const schema = yup.object({ + category: yup.string(), + name: yup.string().required("This field is required"), + address_line_1: yup.string().required("This field is required"), + address_line_2: yup.string(), + city: yup.string().required("This field is required"), + zip: yup.string(), + rate: yup.number().typeError("Must be a number").positive().integer(), + description: yup.string().required("This field is required"), + rule: yup.string(), + max_capacity: yup.number().required("This field is required").min(1).typeError("This field is required"), + additional_guest_rate: yup.string(), + }); + + const navigate = useNavigate(); + const { + register, + handleSubmit, + control, + watch, + getValues, + setValue, + formState: { errors }, + } = useForm({ + resolver: yupResolver(schema), + defaultValues: { + category: "", + id: "", + name: "", + rate: "", + max_capacity: 0, + description: "", + rule: "", + zip: "", + country: "", + city: "", + address_line_1: "", + address_line_2: "", + additional_guest_rate: "", + size: 0, + }, + mode: "all", + }); + + async function fetchCurrSpace() { + const where = [`ergo_property_spaces.id = ${id}`]; + const user_id = localStorage.getItem("user"); + try { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/popular/PAGINATE", { page: 1, limit: 1, user_id: Number(user_id), where, all: true }, "POST", ctrl.signal); + if (Array.isArray(result.list) && result.list.length > 0) { + setCurrSpace(result.list[0]); + } + } catch (err) { + tokenExpireError(authDispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + useEffect(() => { + if (mode == "edit") { + fetchCurrSpace(); + } + }, []); + + const onSubmit = async (data) => { + const result = extractLocationInfo(data?.city) + data.city = (result[0]); + data.country = (result[1]); + console.log("submitting", data); + const host_id = localStorage.getItem("user"); + globalDispatch({ type: "START_LOADING" }); + + try { + if (mode == "edit") { + sdk.setTable("property"); + await sdk.callRestAPI( + { + id: currSpace.property_id, + address_line_1: data.address_line_1, + address_line_2: data.address_line_2, + city: data.city, + country: data.country, + zip: data.zip, + status: 0, + verified: 0, + host_id, + name: data.name, + rule: data.rule, + }, + "PUT", + ); + + sdk.setTable("property_spaces"); + const propertySpaceResult = await sdk.callRestAPI( + { + id, + property_id: currSpace.property_id, + space_id: data.category, + max_capacity: data.max_capacity, + description: data.description, + rate: data.rate, + space_status: SPACE_STATUS.UNDER_REVIEW, + additional_guest_rate: data.additional_guest_rate, + size: hasSizes ? data.size : SPACE_CATEGORY_SIZES.UNSET, + }, + "PUT", + ); + + // create notification + sdk.setTable("notification"); + await sdk.callRestAPI( + { + user_id: host_id, + actor_id: null, + action_id: propertySpaceResult.message, + notification_time: new Date().toISOString().split(".")[0], + message: "Property Space Edited", + type: NOTIFICATION_TYPE.EDIT_PROPERTY_SPACE, + status: NOTIFICATION_STATUS.NOT_ADDRESSED, + }, + "POST", + ); + } + if (draftType === "continue") { + navigate(`/account/my-spaces/${getValues("id")}/edit-images?mode=edit`); + } else { + navigate(-1); + } + } catch (err) { + tokenExpireError(authDispatch, err.message); + globalDispatch({ type: "SHOW_ERROR", payload: { heading: "Edit Space Failed", message: err.message } }); + } + globalDispatch({ type: "STOP_LOADING" }); + }; + + const SIZES = [ + { label: "All", value: SPACE_CATEGORY_SIZES.UNSET }, + { label: "Small", value: SPACE_CATEGORY_SIZES.SMALL }, + { label: "Medium", value: SPACE_CATEGORY_SIZES.MEDIUM }, + { label: "Large", value: SPACE_CATEGORY_SIZES.LARGE }, + { label: "X-Large", value: SPACE_CATEGORY_SIZES.X_LARGE }, + ]; + const category = watch("category"); + const hasSizes = globalState.spaceCategories.find((ctg) => ctg.id == Number(category))?.has_sizes == 1; + + return ( +
    +
    +

    Space Details

    +
    + + +
    +
    + + setValue("address_line_1", val)} + name="address_line_1" + className={`w-full rounded border py-2 px-3 leading-tight text-gray-700 ${errors.address_line_1?.message ? "border-red-500 focus:outline-red-500" : "focus-within:outline-primary"}`} + placeholder="" + hideIcons + suggestionType={["(cities)"]} + /> +
    +
    + + setValue("address_line_2", val)} + name="address_line_2" + className={`w-full rounded border py-2 px-3 leading-tight text-gray-700 ${errors.address_line_2?.message ? "border-red-500 focus:outline-red-500" : "focus-within:outline-primary"}`} + placeholder="" + hideIcons + suggestionType={["(cities)"]} + /> +
    +
    + + setValue("city", val)} + name="city" + className={`w-full rounded border py-2 px-3 leading-tight text-gray-700 ${errors.city?.message ? "border-red-500 focus:outline-red-500" : "focus-within:outline-primary"}`} + placeholder="" + hideIcons + suggestionType={["(cities)"]} + /> +
    +
    + + +
    + +
    + + +
    +
    + +
    + $ + +
    +
    +
    + + +
    +
    + + +
    +
    +

    * Max number of guests {errors.max_capacity?.message ? {errors.max_capacity?.message} : ""}

    + setValue("max_capacity", val)} + /> +
    + {hasSizes && ( +
    + + +
    + )} + +
    + +
    + $ + +
    +
    +
    + +
    + +
    +
    + ); +}; + +export default EditPropertySpacePage; diff --git a/src/pages/Host/Spaces/Edit/EditScheduleDay.jsx b/src/pages/Host/Spaces/Edit/EditScheduleDay.jsx new file mode 100644 index 0000000..7368c04 --- /dev/null +++ b/src/pages/Host/Spaces/Edit/EditScheduleDay.jsx @@ -0,0 +1,235 @@ +import React, { useState } from "react"; +import CopyIcon from "@/components/frontend/icons/CopyIcon"; +import PencilIcon from "@/components/frontend/icons/PencilIcon"; +import RecurringIcon from "@/components/frontend/icons/RecurringIcon"; +import ResetIcon from "@/components/frontend/icons/ResetIcon"; +import { useSpaceContext } from "./spaceContext"; +import moment from "moment"; +import { useFieldArray, useForm } from "react-hook-form"; +import useDelayUnmount from "@/hooks/useDelayUnmount"; +import CustomizedIcon from "@/components/frontend/icons/CustomizedIcon"; +import { daysMapping, fullDaysMapping, hourlySlots, formatAMPM } from "@/utils/date-time-utils"; +import Icon from "@/components/Icons"; +import { useEffect } from "react"; +import { parseJsonSafely } from "@/utils/utils"; +import { usePropertySpace } from "@/hooks/api"; +import MkdSDK from "@/utils/MkdSDK"; + +let sdk = new MkdSDK(); + +const EditScheduleDay = ({ selectedDate, date, id, schedule_id, selected_id, isDirty, setIsDirty, selectedTemplate, activeStartDate }) => { + + const { spaceData, dispatch } = useSpaceContext(); + // const showOptions = selectedDate.getDate() == date.getDate(); + const [showOptions, setShowOptions] = useState(false); + + const [editPopup, setEditPopup] = useState(false); + const [slotDates, setSlotDates] = useState([]); + const showEditPopup = useDelayUnmount(editPopup, 300); + const dayFormatted = moment(date).format("MM/DD/YY"); + + async function returnSpaceDetails() { + sdk.setTable("property_spaces_schedule_template") + const data = await sdk.callRestAPI({}, "GETALL") + const mainData = data?.list.find((space) => space.property_spaces_id === Number(id)) + if (mainData?.customSlots) { + setSlotDates(JSON.parse(mainData?.custom_slots)) + } + + } + + useEffect(() => { + returnSpaceDetails() + }, [editPopup]) + + // get slots from context or template + let slots = Array.isArray(spaceData?.customSlots[dayFormatted] ?? slotDates[dayFormatted]) + ? spaceData?.customSlots[dayFormatted] ?? slotDates[dayFormatted] + : selectedTemplate[daysMapping[date.getDay()]] == 1 && Array.isArray(parseJsonSafely(selectedTemplate.slots)) + ? parseJsonSafely(selectedTemplate.slots) + : []; + + let isCustom = Array.isArray(spaceData?.customSlots[dayFormatted] ?? slotDates[dayFormatted]); + + const { handleSubmit, register, control, getValues, setValue } = useForm({ + defaultValues: { + custom_slot: slots.map((slot) => ({ start: formatAMPM(slot.start), end: formatAMPM(slot.end) })), + }, + }); + const { fields, append, prepend, remove, swap, move, insert } = useFieldArray({ + control, + name: "custom_slot", + }); + useEffect(() => { + setShowOptions(false); + }, [selectedDate]); + + const onSubmit = async (data) => { + dispatch({ + type: "SET_DAY_SLOT", + payload: { + day: dayFormatted, + slots: data.custom_slot.map((time) => ({ start: new Date(`${dayFormatted} ${time.start}`).toISOString(), end: new Date(`${dayFormatted} ${time.end}`).toISOString() })), + }, + }); + sdk.setTable("property_spaces_schedule_template"); + await sdk.callRestAPI( + { + id: schedule_id && schedule_id, + schedule_template_id: selected_id && selected_id, + custom_slots: JSON.stringify(spaceData?.customSlots), + }, + "PUT", + ); + setEditPopup(false); + }; + + const resetToTemplate = (e) => { + e.stopPropagation(); + dispatch({ type: "CLEAR_DAY_SLOT", payload: dayFormatted }); + }; + + const copyFromPreviousWeek = (e) => { + e.stopPropagation(); + dispatch({ type: "INHERIT_DAY_SLOT", payload: dayFormatted }); + }; + + return ( +
    +
    +
    {date.getDate()}
    + {isCustom ? : } +
    +
    + {slots.slice(0, 3).map((sl, idx) => ( +

    {formatAMPM(sl.start) + " - " + formatAMPM(sl.end)}

    + ))} + {slots.length > 3 && + {slots.length - 3} More} +
    + {dayFormatted == moment(selectedDate).format("MM/DD/YY") && ( + + )} + + {showOptions && ( +
    + + {selectedTemplate.template_name && ( + + )} + + +
    + )} +
    setEditPopup(false)} + > +
    e.stopPropagation()} + onSubmit={handleSubmit(onSubmit)} + > +
    +

    Edit Day

    + +
    +
    +
    + {fields.map((field, index) => ( +
    +
    + + +
    + +
    + ))} +
    + + +
    +
    + +
    +
    +
    + ); +}; + +export default EditScheduleDay; diff --git a/src/pages/Host/Spaces/Edit/EditScheduleWrapper.jsx b/src/pages/Host/Spaces/Edit/EditScheduleWrapper.jsx new file mode 100644 index 0000000..e1bd3e9 --- /dev/null +++ b/src/pages/Host/Spaces/Edit/EditScheduleWrapper.jsx @@ -0,0 +1,10 @@ +import React from "react"; +import EditSpaceSchedulingPage from "./EditSpaceSchedulingPage"; +import { SpaceContextProvider } from "./spaceContext"; + +const EditScheduleWrapper = () => ( + + + +); +export default EditScheduleWrapper; diff --git a/src/pages/Host/Spaces/Edit/EditSpaceSchedulingPage.jsx b/src/pages/Host/Spaces/Edit/EditSpaceSchedulingPage.jsx new file mode 100644 index 0000000..245bb85 --- /dev/null +++ b/src/pages/Host/Spaces/Edit/EditSpaceSchedulingPage.jsx @@ -0,0 +1,336 @@ +import React, { useState } from "react"; +import { useContext } from "react"; +import { Calendar } from "react-calendar"; +import { useLocation, useNavigate, useParams } from "react-router"; +import AvailabilityTemplate from "@/components/frontend/AvailabilityTemplate"; +import NextIcon from "@/components/frontend/icons/NextIcon"; +import PrevIcon from "@/components/frontend/icons/PrevIcon"; +import { GlobalContext } from "@/globalContext"; +import { useEffect } from "react"; +import { useSpaceContext } from "./spaceContext"; +import MkdSdk from "@/utils/MkdSDK"; +import EditScheduleDay from "./EditScheduleDay"; +import { useSearchParams } from "react-router-dom"; +import axios from "axios"; +import { DRAFT_STATUS } from "@/utils/constants"; +import NoteIcon from "@/components/frontend/icons/NoteIcon"; +import { Tab } from "@headlessui/react"; +import SelectTemplatesModal from "@/pages/Host/Spaces/Add/SelectTemplatesModal"; +import CreateTemplateModal from "@/pages/Host/Spaces/Add/CreateTemplateModal"; +import { usePropertyAddons, usePropertySpace, usePropertySpaceAmenities } from "@/hooks/api"; +import moment from "moment"; + +let sdk = new MkdSdk(); + +const EditSpaceSchedulingPage = () => { + const { id } = useParams(); + const { state: scheduleTemplate } = useLocation(); + const [render, forceRender] = useState(new Date()); + // const dayFormatted = moment(date).format("MM/DD/YY"); + + const { propertySpace, notFound } = usePropertySpace(id, render); +console.log(scheduleTemplate) + + + const [searchParams] = useSearchParams(); + const [addTemplatePopup, setAddTemplatePopup] = useState(false); + const [selectTemplatePopup, setSelectTemplatePopup] = useState(false); + const [selectedDate, setSelectedDate] = useState(new Date()); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const [templates, setTemplates] = useState([]); + const [selectedTemplate, setSelectedTemplate] = useState(scheduleTemplate); + const { spaceData, dispatch } = useSpaceContext(); + const spaceAddons = usePropertyAddons(propertySpace.property_id); + const spaceAmenities = usePropertySpaceAmenities(propertySpace.property_id); + + useEffect(() => { + dispatch({ type: "SET_ADDONS", payload: spaceAddons }) + dispatch({ type: "SET_AMENITIES", payload: spaceAmenities }) + if (scheduleTemplate?.custom_slots) { + for (let index = 0; index < Object?.keys(JSON.parse(scheduleTemplate?.custom_slots)).length; index++) { + const elementDay = Object.keys(JSON.parse(scheduleTemplate?.custom_slots))[index]; + const elementTime = Object.values(JSON.parse(scheduleTemplate?.custom_slots))[index]; + dispatch({ + type: "SET_DAY_SLOT", + payload: { + day: elementDay, + slots: elementTime, + }, + }); + } + } + }, []); + + + const navigate = useNavigate(); + + + async function fetchTemplates() { + const host_id = Number(localStorage.getItem("user")); + sdk.setTable("schedule_template"); + try { + const result = await sdk.callRestAPI({ payload: { host_id } }, "GETALL"); + if (Array.isArray(result.list)) { + setTemplates(result.list); + } + + } catch (err) { + console.log("error", err) + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function changeDraftStatus(space_id, newStatus) { + try { + const result = await axios.post("https://ergo.mkdlabs.com/rest/property_spaces/PUT", + { id: Number(space_id), draft_status: newStatus }, + { + headers: { + Authorization: `Bearer ${localStorage.getItem("token")}`, + "x-project": "ZXJnbzprNWdvNGw1NDhjaDRxazU5MTh4MnVsanV2OHJxcXAyYXM", + }, + }, + ); + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + useEffect(() => { + fetchTemplates(); + }, [render]); + + useEffect(() => { + dispatch({ type: "SET_INITIAL_SCHEDULING", payload: scheduleTemplate }); + }, []); + + async function submitSchedule(draftType) { + if (selectedTemplate?.slots?.length < 1 || selectedTemplate?.slots === "[]") { + globalDispatch({ + type: "SHOW_ERROR", + payload: { heading: "Template selected doesn't have a slot", message: "Click on Use Template and Add Slot Time" }, + }); + return; + } + if (!selectedTemplate.id) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { heading: "Template was not selected", message: "Click on Use Template and Select a Schedule Template" }, + }); + return; + } + globalDispatch({ type: "START_LOADING" }); + + const mode = searchParams.get("mode"); + + try { + // create/edit scheduling + sdk.setTable("property_spaces_schedule_template"); + if (mode == "create") { + await sdk.callRestAPI( + { + property_spaces_id: Number(id), + schedule_template_id: selectedTemplate.id, + custom_slots: JSON.stringify(spaceData?.customSlots), + }, + "POST", + ); + await changeDraftStatus(id, DRAFT_STATUS.COMPLETED); + } + // console.log(JSON.stringify(spaceData?.customSlots)) + else { + sdk.setTable("property_spaces_schedule_template"); + + await sdk.callRestAPI( + { + id: scheduleTemplate.schedule_id, + schedule_template_id: selectedTemplate.id, + custom_slots: JSON.stringify(spaceData?.customSlots), + }, + "PUT", + ); + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + dispatch({ type: "SET_SCHEDULE_TEMPLATE", payload: selectedTemplate }); + if (draftType === "continue") { + navigate(`/account/my-spaces/${id}/edit-review`, { state: { selectedTemplate, spaceAmenities, spaceAddons } }); + } else { + navigate(`/account/my-spaces/${id}`); + } + } + + return ( +
    +

    Edit Scheduling

    +
    +

    + + How it works +

    +

    + You can predefine each day of the week in ’Templates’ - those hours will be applied to each day of the week. On top of that you can customize each day according to your needs. +
    +
    You will be able to edit and change the space availability anytime from in ‘Spaces/space/edit availability’. +

    +
    + + + Calendar + Templates +
    +
    + + +
    e.stopPropagation()} + > +
    + +
    + { + setSelectedDate(v); + }} + value={selectedDate} + defaultValue={selectedDate} + nextLabel={ + <> +
    +
    + +
    +
    + + } + prevLabel={} + navigationLabel={({ date, label, locale, view }) => ( + <> +
    e.stopPropagation()} + > + {label} +
    + {/* */} +
    +
    + + )} + tileContent={({ activeStartDate, date, view }) => { + return ( + + ); + }} + minDate={new Date()} + /> +
    + setSelectTemplatePopup(false)} + clearAll={() => dispatch({ type: "CLEAR_ALL_SLOTS" })} + templates={templates} + selectedTemplate={selectedTemplate} + setSelectedTemplate={setSelectedTemplate} + /> +
    + +
    +
    +

    Template name & day(s)

    +

    Time slots

    +
    + +
    + {templates.map((tmp) => ( + + ))} + {templates.length == 0 && ( +

    + + No templates yet +

    + )} + setAddTemplatePopup(false)} + onSuccess={() => fetchTemplates()} + /> +
    +
    +
    + +

    + Keep in mind it usually takes us 2 days to review new space. Once we approve it it wil be posted with the first date/time available +

    +
    + +
    + +
    + ); +}; + +export default EditSpaceSchedulingPage; diff --git a/src/pages/Host/Spaces/Edit/PageWrapper.jsx b/src/pages/Host/Spaces/Edit/PageWrapper.jsx new file mode 100644 index 0000000..d1f5c08 --- /dev/null +++ b/src/pages/Host/Spaces/Edit/PageWrapper.jsx @@ -0,0 +1,16 @@ +import React from "react"; +import { Outlet } from "react-router"; +import { SpaceContextProvider } from "./spaceContext"; + + +const PageWrapper = () => { + return ( +
    + + + +
    + ); +}; + +export default PageWrapper; diff --git a/src/pages/Host/Spaces/Edit/spaceContext.jsx b/src/pages/Host/Spaces/Edit/spaceContext.jsx new file mode 100644 index 0000000..320fb55 --- /dev/null +++ b/src/pages/Host/Spaces/Edit/spaceContext.jsx @@ -0,0 +1,84 @@ +import moment from "moment"; +import React, { createContext, useContext, useReducer } from "react"; + +const initialSpaceData = { + category: "", + name: "", + rate: "", + max_capacity: 0, + description: "", + rule: "", + zip: "", + country: "", + city: "", + address_line_1: "", + address_line_2: "", + additional_guest_rate: "", + size: 0, + pictures: [null, null, null, null, null, null], + pictureIds: [], + faqs: [{ question: "", answer: "" }], + thumbnail: "", + addons: [], + amenities: [], + customSlots: {}, + schedule_template: {} +}; + +function lastWeek(today) { + return moment(today).subtract(1, "week").format("MM/DD/YY"); +} + +function addWeekToDate(slots) { + if (!Array.isArray(slots)) return []; + return slots.map((slot) => ({ start: moment(slot.start).add(1, "week").toISOString(), end: moment(slot.end).add(1, "week").toISOString() })); +} + +const reducer = (state, action) => { + switch (action.type) { + case "SET_PROPERTY_ID": + return { ...state, property_id: action.payload }; + case "SET_DETAILS_ONE": + return { ...state, ...action.payload }; + case "SET_DETAILS_TWO": + return { ...state, ...action.payload }; + case "SET_THUMBNAIL": + return { ...state, thumbnail: action.payload }; + case "SET_DAY_SLOT": + return { ...state, customSlots: { ...state.customSlots, [action.payload.day]: action.payload.slots } }; + case "CLEAR_ALL_SLOTS": + return { ...state, customSlots: {} }; + case "CLEAR_DAY_SLOT": + return { ...state, customSlots: { ...state.customSlots, [action.payload]: undefined } }; + case "INHERIT_DAY_SLOT": + return { ...state, customSlots: { ...state.customSlots, [action.payload]: addWeekToDate(state.customSlots[lastWeek(action.payload)]) } }; + case "SET_SCHEDULE_TEMPLATE": + return { ...state, schedule_template: action.payload }; + case "SET_DESCRIPTION": + return { ...state, description: action.payload }; + case "SET_PROPERTY_NAME": + return { ...state, name: action.payload }; + case "SET_AMENITIES": + return { ...state, amenities: action.payload }; + case "SET_ADDONS": + return { ...state, addons: action.payload }; + case "SET_RULE": + return { ...state, rule: action.payload }; + default: + return state; + } +}; + +// create context here +const spaceContext = createContext({}); + +// wrap this component around App.tsx to get access to userData in all components +const SpaceContextProvider = ({ children }) => { + const [spaceData, dispatch] = useReducer(reducer, initialSpaceData); + + return {children}; +}; + +// use this custom hook to get the data in any component in component tree +const useSpaceContext = () => useContext(spaceContext); +export { useSpaceContext, SpaceContextProvider }; diff --git a/src/pages/Host/Spaces/MySpaceBookingHistoryPage.jsx b/src/pages/Host/Spaces/MySpaceBookingHistoryPage.jsx new file mode 100644 index 0000000..0c40369 --- /dev/null +++ b/src/pages/Host/Spaces/MySpaceBookingHistoryPage.jsx @@ -0,0 +1,288 @@ +import React from "react"; +import { useContext } from "react"; +import { useEffect } from "react"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { useParams } from "react-router"; +import { GlobalContext } from "@/globalContext"; +import NoteIcon from "@/components/frontend/icons/NoteIcon"; +import MkdSDK from "@/utils/MkdSDK"; +import HostBookingCard from "@/pages/Host/Bookings/HostBookingCard"; +import { isValidDate, parseSearchParams } from "@/utils/utils"; +import { useSearchParams } from "react-router-dom"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import DatePickerV3 from "@/components/DatePickerV3"; +import CustomSelectV2 from "@/components/CustomSelectV2"; +import { AdjustmentsHorizontalIcon } from "@heroicons/react/24/solid"; + +const sdk = new MkdSDK(); +const ctrl = new AbortController(); + +const MySpaceBookingHistoryPage = ({ myBookings }) => { + const { id } = useParams(); + const [showFilter, setShowFilter] = useState(false); + const [bookings, setBookings] = useState(myBookings || Array(4).fill({})); + const [render, forceRender] = useState(false); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const { dispatch } = useContext(AuthContext); + + const [searchParams, setSearchParams] = useSearchParams(); + + const [favoriteStatuses, setFavoriteStatuses] = useState([]); + const { handleSubmit, register, watch, setValue, control, formState, resetField } = useForm({ + defaultValues: (() => { + const params = parseSearchParams(searchParams); + return { + id: params.id ?? "", + guest_name: params.guest_name ?? "", + from: isValidDate(params.from ?? "") ? new Date(params.from) : new Date(), + to: isValidDate(params.to ?? "") ? new Date(params.to) : new Date(), + space_name: params.space_name ?? "", + status: params.status ?? "", + direction: "DESC", + }; + })(), + }); + + const { dirtyFields } = formState; + + const direction = watch("direction"); + const fromDate = watch("from"); + + const onSubmit = async (data) => { + if (window.innerWidth < 700) { + setShowFilter(false); + } + console.log("submitting", data); + setBookings([]); + searchParams.set("id", data.id); + searchParams.set("guest_name", data.guest_name); + searchParams.set("status", data.status); + searchParams.set("from", dirtyFields?.from ? data.from.toISOString().split("T")[0] : ""); + searchParams.set("to", dirtyFields?.to ? data.to.toISOString().split("T")[0] : ""); + setSearchParams(searchParams); + }; + + async function fetchFavoriteStatuses() { + const user_id = Number(localStorage.getItem("user") ?? 0); + const payload = { user_id }; + sdk.setTable("user_property_spaces"); + try { + const result = await sdk.callRestAPI({ payload }, "GETALL"); + if (Array.isArray(result.list)) { + setFavoriteStatuses(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") { + globalDispatch({ type: "STOP_LOADING" }); + return; + } + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + async function fetchMySpaceBookings() { + setBookings(Array(10).fill({})); + const user_id = localStorage.getItem("user"); + var where = [`ergo_booking.host_id = ${user_id} AND ergo_booking.property_space_id = ${id} AND ergo_booking.deleted_at IS NULL`]; + const filters = parseSearchParams(searchParams); + if (filters.guest_name) { + where.push(`(ergo_user.first_name LIKE '%${filters.guest_name}%' OR ergo_user.last_name LIKE '%${filters.guest_name}%'`); + } + + if (filters.from) { + where.push(`ergo_booking.booking_start_time >= date('${filters.from}')`); + } + + if (filters.to) { + where.push(`ergo_booking.booking_end_time <= date('${filters.to}')`); + } + + if (filters.space_name) { + where.push(`ergo_property.name LIKE '%${filters.space_name}%'`); + } + + if (filters.status) { + if (filters.status == "expired") { + where.push(`ergo_booking.booking_start_time < date('${new Date().toISOString()}')`); + } else { + where.push(`ergo_booking.status = ${filters.status}`); + } + } + + if (filters.id) { + where = [`ergo_booking.host_id = ${user_id} AND ergo_booking.id = ${filters.id} AND ergo_booking.property_space_id = ${id} AND ergo_booking.deleted_at IS NULL`]; + } + + console.log("where", where); + try { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/booking/PAGINATE", { page: 1, limit: 1000, where, sortId: "update_at", direction: "DESC" }, "POST", ctrl.signal); + if (Array.isArray(result.list)) { + setBookings(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + useEffect(() => { + fetchFavoriteStatuses(); + }, []); + + useEffect(() => { + setBookings(myBookings); + }, [myBookings]); + + useEffect(() => { + fetchMySpaceBookings(); + }, [searchParams]); + + useEffect(() => { + if (render) { + fetchFavoriteStatuses(); + } + }, [render]); + + const sortByDate = (a, b) => { + if (direction == "DESC") { + return new Date(b.id) - new Date(a.id); + } + return new Date(a.id) - new Date(b.id); + }; + + return ( +
    +
    +
    +
    + + +
    + +
    +
    +
    + {bookings.length == 0 && ( +
    +

    + You have no bookings +

    +
    + )} + {bookings.sort(sortByDate).map((book, i) => ( + fav.property_spaces_id == book.property_space_id)?.id ?? null} + /> + ))} +
    +
    + ); +}; + +export default MySpaceBookingHistoryPage; diff --git a/src/pages/Host/Spaces/MySpaceCard.jsx b/src/pages/Host/Spaces/MySpaceCard.jsx new file mode 100644 index 0000000..47a36e8 --- /dev/null +++ b/src/pages/Host/Spaces/MySpaceCard.jsx @@ -0,0 +1,205 @@ +import { monthsMapping } from "@/utils/date-time-utils"; +import React, { useState } from "react"; +import { useContext } from "react"; +import Skeleton from "react-loading-skeleton"; +import { Link, useNavigate } from "react-router-dom"; +import { GlobalContext } from "@/globalContext"; +import { DRAFT_STATUS, SPACE_VISIBILITY } from "@/utils/constants"; +import MkdSDK from "@/utils/MkdSDK"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import { FavoriteButton } from "@/components/frontend"; +import ThreeDotsMenu from "@/components/frontend/ThreeDotsMenu"; +import StarIcon from "@/components/frontend/icons/StarIcon"; +import PersonIcon from "@/components/frontend/icons/PersonIcon"; + +export default function MySpaceCard({ data, forceRender, reset }) { + const navigate = useNavigate(); + const statusMapping = ["Under review", "Active", "Rejected"]; + const statusColorMapping = ["text-[#667085]", "my-text-gradient", "text-[#DC6803]"]; + const { dispatch: globalDispatch } = useContext(GlobalContext); + const { dispatch } = useContext(AuthContext); + const [imageLoaded, setImageLoaded] = useState(false); + const sdk = new MkdSDK(); + + async function hidePropertySpace(id) { + try { + await sdk.callRawAPI("/rest/property_spaces/PUT", { id, availability: SPACE_VISIBILITY.HIDDEN }, "POST"); + forceRender(new Date()); + if (reset) { + reset(); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function showPropertySpace(id) { + try { + await sdk.callRawAPI("/rest/property_spaces/PUT", { id, availability: SPACE_VISIBILITY.VISIBLE }, "POST"); + forceRender(new Date()); + if (reset) { + reset(); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + return ( + <> +
    +
    + {!!data.url && + <> setImageLoaded(true)} + alt="" + className="absolute top-0 left-0 h-full w-full object-cover" />
    + +
    + } + {!data.url && } +
    +
    +
    +

    {data.name || }

    +

    {data.address_line_1 || }

    + {data.id ? ( +
    +

    + from: ${data.rate}/h +

    +
    +
    + + {data.max_capacity} +
    +

    + + {(Number(data.average_space_rating) || 0).toFixed(1)} +

    +
    +
    + ) : ( + + )} +
    +
    +
    + {data.id ? ( +
    +

    Created

    + + {monthsMapping[new Date(data.create_at).getMonth()] + " " + new Date(data.create_at).getDate() + "/" + new Date(data.create_at).getFullYear()} + +
    + ) : ( + + )} + {data.id ? ( +
    +

    Total Bookings

    + {data.booking_count} +
    + ) : ( + + )} + {data.id ? ( +
    +

    Reviews

    + {data.space_rating_count ?? 0} +
    + ) : ( + + )} +
    +
    +
    + + {" "} + {(data.draft_status < DRAFT_STATUS.COMPLETED ? "DRAFT" : statusMapping[data.space_status]) || } + + {data.id && ( + + View details + + )} + + {(() => { + if (data.space_status == 1 && data.availability == 1 && data.draft_status >= DRAFT_STATUS.COMPLETED) { + return ( + + ); + } + if (data.space_status == 1 && data.availability == 0 && data.draft_status >= DRAFT_STATUS.COMPLETED) { + return ( + + ); + } + })()} +
    + , + onClick: () => showPropertySpace(data.id), + notShow: !(data.space_status == 1 && data.availability == 0), + }, + { + label: "Deactivate", + icon: <>, + onClick: () => hidePropertySpace(data.id), + notShow: !(data.space_status == 1 && data.availability == 1), + }, + { + label: "View Details", + icon: <>, + onClick: () => { + navigate("/account/my-spaces/" + data.id); + }, + }, + ]} + /> +
    +
    +
    +
    + + ); +} diff --git a/src/pages/Host/Spaces/MySpaceDetailsPage.jsx b/src/pages/Host/Spaces/MySpaceDetailsPage.jsx new file mode 100644 index 0000000..1fb9d12 --- /dev/null +++ b/src/pages/Host/Spaces/MySpaceDetailsPage.jsx @@ -0,0 +1,619 @@ +import React, { Fragment, useEffect } from "react"; +import { useState } from "react"; +import { Navigate, useLocation, useNavigate, useParams } from "react-router-dom"; +import FaqAccordion from "@/components/frontend/FaqAccordion"; +import ReviewCard from "@/components/frontend/ReviewCard"; +import StarIcon from "@/components/frontend/icons/StarIcon"; +import MkdSDK from "@/utils/MkdSDK"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import DateTimePicker from "@/components/frontend/DateTimePicker"; +import { useForm } from "react-hook-form"; +import Icon from "@/components/Icons"; +import ThreeDotsMenu from "@/components/frontend/ThreeDotsMenu"; +import MySpaceBookingHistoryPage from "./MySpaceBookingHistoryPage"; +import CustomSelect from "@/components/frontend/CustomSelect"; +import DraftProgress from "@/components/frontend/DraftProgress"; +import { DRAFT_STATUS, IMAGE_STATUS, SPACE_VISIBILITY } from "@/utils/constants"; +import { useContext } from "react"; +import { GlobalContext } from "@/globalContext"; +import FavoriteButton from "@/components/frontend/FavoriteButton"; +import PropertyImageSlider from "@/components/frontend/PropertyImageSlider"; +import { usePropertyAddons, usePropertySpace, usePropertySpaceAmenities, usePropertySpaceFaqs, usePropertySpaceImages, usePropertySpaceReviews, usePublicUserData } from "@/hooks/api"; +import PropertySpaceMapImage from "@/components/frontend/PropertySpaceMapImage"; +import { Tab } from "@headlessui/react"; +import AllReviewsModal from "@/components/frontend/AllReviewsModal"; +import CircleCheckIcon from "@/components/frontend/icons/CircleCheckIcon"; +import DeleteSpaceConfirmation from "./DeleteSpaceConfirmation"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import useAmenityCategories from "@/hooks/api/useAmenityCategories"; +import usePropertySpaceImagesV2 from "@/hooks/api/usePropertySpaceImagesV2"; + +let sdk = new MkdSDK(); +let ctrl = new AbortController(); + +const statusMapping = ["Under Review", "Active", "Rejected"]; +const statusColorMapping = ["text-[#DC6803]", "my-text-gradient", "text-[#D92D20]"]; + +const MySpaceDetailsPage = () => { + const { state: spaceData } = useLocation(); + const { dispatch: globalDispatch } = useContext(GlobalContext); + const { dispatch } = useContext(AuthContext); + const [galleryOpen, setGalleryOpen] = useState(false); + const [reviewsPopup, setReviewsPopup] = useState(false); + const navigate = useNavigate(); + const { id } = useParams(); + const [reviewDirection, setReviewDirection] = useState("DESC"); + const [bookedSlots, setBookedSlots] = useState([]); + const [scheduleTemplate, setScheduleTemplate] = useState({}); + const [myBookings, setMyBookings] = useState([]); + + const { handleSubmit, register, setValue } = useForm(); + + const [showCalendar, setShowCalendar] = useState(false); + const [render, forceRender] = useState(false); + const [showMap, setShowMap] = useState(false); + + const [fetching, setFetching] = useState(true); + + + const { propertySpace, notFound } = usePropertySpace(id, render); + const hostData = usePublicUserData(propertySpace.host_id); + const spaceImages = usePropertySpaceImagesV2(propertySpace.id, false,); + const spaceAddons = usePropertyAddons(propertySpace.property_id); + const spaceAmenities = usePropertySpaceAmenities(propertySpace.id); + + const faqs = usePropertySpaceFaqs(propertySpace.id); + const reviews = usePropertySpaceReviews(propertySpace.id); + const [deleteSpace, setDeleteSpace] = useState(false); + + + async function fetchBookedSlots(id) { + try { + const result = await callCustomAPI("customer/schedule", "post", { property_spaces_id: id }, "", null, "v3"); + if (Array.isArray(result.list)) { + setBookedSlots(result.list); + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function fetchScheduleTemplate(id) { + try { + const result = await callCustomAPI( + "property_spaces_schedule_template", + "post", + { + page: 1, + limit: 1, + where: [`property_spaces_id = ${id}`], + }, + "PAGINATE", + ); + + if (Array.isArray(result.list) && result.list.length > 0) { + setScheduleTemplate({ custom_slots: result.list[0].custom_slots, schedule_id: result.list[0].id }); + } + if (result.list[0]?.schedule_template_id) { + const templateResult = await callCustomAPI( + "schedule_template", + "post", + { + page: 1, + limit: 1, + where: [`id = ${result.list[0].schedule_template_id}`], + }, + "PAGINATE", + ); + if (Array.isArray(templateResult.list) && (templateResult.list[0] ?? {})) { + setScheduleTemplate((prev) => { + let updated = { ...prev, ...templateResult.list[0] }; + return updated; + }); + } + } + } catch (err) { + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function fetchMySpaceBookings() { + const user_id = localStorage.getItem("user"); + var where = [`ergo_booking.host_id = ${user_id} AND ergo_booking.property_space_id = ${id} AND ergo_booking.deleted_at IS NULL`]; + try { + const result = await sdk.callRawAPI("/v2/api/custom/ergo/booking/PAGINATE", { page: 1, limit: 10000, where }, "POST", ctrl.signal); + if (Array.isArray(result.list)) { + setMyBookings(result.list); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + async function hidePropertySpace(id) { + globalDispatch({ type: "START_LOADING" }); + try { + await sdk.callRawAPI("/rest/property_spaces/PUT", { id, availability: SPACE_VISIBILITY.HIDDEN }, "POST", ctrl.signal); + forceRender(new Date()); + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") { + globalDispatch({ type: "STOP_LOADING" }); + return; + } + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + async function showPropertySpace(id) { + globalDispatch({ type: "START_LOADING" }); + try { + await sdk.callRawAPI("/rest/property_spaces/PUT", { id, availability: SPACE_VISIBILITY.VISIBLE }, "POST", ctrl.signal); + forceRender(new Date()); + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") { + globalDispatch({ type: "STOP_LOADING" }); + return; + } + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + globalDispatch({ type: "STOP_LOADING" }); + } + + const onSubmit = async (data) => { + console.log("submitting ", data); + }; + + useEffect(() => { + fetchBookedSlots(id); + fetchScheduleTemplate(id); + fetchMySpaceBookings(); + }, []); + + const sortByPostDate = (a, b) => { + if (reviewDirection == "DESC") { + return new Date(b.post_date) - new Date(a.post_date); + } + return new Date(a.post_date) - new Date(b.post_date); + }; + + if (notFound) return ; + + return ( +
    { + setShowCalendar(false); + }} + > +
    + +
    +
    +

    Space Details

    +
    + + {" "} + {(propertySpace.draft_status ?? spaceData?.draft_status) < DRAFT_STATUS.COMPLETED ? "DRAFT" : statusMapping[propertySpace.space_status ?? spaceData?.space_status ?? 0]} + + +
    +
    + + + + + {" "} + + + {" "} +
    +
    + + +
    + {propertySpace.id ?? spaceData?.id ? ( + <> + {" "} + {(propertySpace.draft_status ?? spaceData?.draft_status) < DRAFT_STATUS.COMPLETED ? ( + <> +

    Finish creating your space

    + + + ) : ( +
    +
    +

    Space ID

    +

    {id}

    +
    +
    +

    Total Revenue

    +

    + ${" "} + {myBookings + .reduce((acc, curr) => { + return acc + (curr.total ?? 0) + (curr.addon_cost ?? 0); + }, 0) + .toFixed(2)} +

    +
    +
    +

    Total Bookings

    +

    {myBookings.length}

    +
    +
    + )} + + ) : null} +
    +
    +

    {propertySpace.name ?? spaceData?.name}

    +

    {propertySpace.address_line_1 ?? spaceData?.address_line_1}

    + +
    +
    +

    + + + {(Number(propertySpace.average_space_rating ?? spaceData?.average_space_rating) || 0).toFixed(1)} + ({propertySpace.space_rating_count ?? spaceData?.space_rating_count}) + +

    +
    + + Save +
    +
    +
    +
    + + {spaceImages[0]?.photo_url && + + } + {spaceImages[1]?.photo_url && + + } +
    + {spaceImages[2]?.photo_url && + + } + {spaceImages[3]?.photo_url && + + } +
    + {spaceImages[4]?.photo_url && + + } + +
    + +
    +
    +

    Description

    +

    {propertySpace.description ?? spaceData?.description}

    +
    +

    Amenities

    +
      + {spaceAmenities.map((am, idx) => ( +
    • + + {am.amenity_name} +
    • + ))} +
    +
    +

    Add ons

    +
      + {spaceAddons.map((addon) => ( +
    • + + {" "} +
      + {addon.add_on_name} +
      {" "} +
      {" "} + ${addon.cost}/h +
    • + ))} +
    +
    +
    +

    About the host

    +
    +
    +
    + +
    + +
    +
    +

    {hostData.first_name}

    +

    {hostData.last_name}

    +
    +

    {propertySpace.about ?? spaceData?.about}

    +
    +
    + +

    {propertySpace.about ?? spaceData?.about}

    +
    +
    +

    Reviews

    + +
    +
    + {reviews.length == 0 &&

    No reviews yet

    } + {reviews + .sort(sortByPostDate) + .slice(0, 10) + .map((rw) => ( + + ))} +
    + {reviews.length > 10 ? ( + + ) : null} +
    +
    +
    +

    FAQs

    + {faqs.map((faq) => ( + + ))} +
    +

    Property rules

    +

    {propertySpace.rule ?? spaceData?.rule}

    +
    + +
    +
    +

    Price and availability

    +
    + Max capacity + + {" "} + {propertySpace.max_capacity ?? spaceData?.max_capacity} people + +
    +
    + Pricing from + + from: ${propertySpace.rate ?? spaceData?.rate}/h + +
    + {propertySpace.additional_guest_rate && propertySpace.max_capacity > 1 ? ( +
    + Additional guest + + from: ${propertySpace.additional_guest_rate}/h + +
    + ) : null} +
    +
    +
    + ({ fromTime: new Date(slot.start_time), toTime: new Date(slot.end_time) }))} + scheduleTemplate={scheduleTemplate} + defaultMessage="Check Availability" + /> +
    + +
    +
    +
    +
    + setGalleryOpen(false)} + /> + setReviewsPopup(false)} + reviews={reviews} + onDirectionChange={setReviewDirection} + /> +
    +
    + + {" "} +
    + +
    +
    +
    +
    + + setShowMap(false)} + /> + setDeleteSpace(false)} + propertySpace={propertySpace} + /> +
    + ); +}; + +export default MySpaceDetailsPage; diff --git a/src/pages/Host/Spaces/MySpacesFiltersModal.jsx b/src/pages/Host/Spaces/MySpacesFiltersModal.jsx new file mode 100644 index 0000000..25a1e2f --- /dev/null +++ b/src/pages/Host/Spaces/MySpacesFiltersModal.jsx @@ -0,0 +1,294 @@ +import { AuthContext, tokenExpireError } from "@/authContext"; +import DatePickerV3 from "@/components/DatePickerV3"; +import DatePickerV2 from "@/components/frontend/DatePickerV2"; +import { GlobalContext } from "@/globalContext"; +import MkdSDK from "@/utils/MkdSDK"; +import { formatDate, isValidDate, parseSearchParams } from "@/utils/utils"; +import { Dialog, Transition } from "@headlessui/react"; +import React, { Fragment, useContext } from "react"; +import { useForm } from "react-hook-form"; +import { useSearchParams } from "react-router-dom"; + +const sdk = new MkdSDK(); +const ctrl = new AbortController(); + +const statuses = [ + { label: "Under Review", value: 0 }, + { label: "Approved", value: 1 }, + { label: "Declined", value: 2 }, +]; + +const visibilityStatuses = [ + { label: "Hidden", value: 0 }, + { label: "Visible", value: 1 }, +]; + +export default function MySpacesFiltersModal({ modalOpen, closeModal, setSpaces, FETCH_PER_SCROLL, spacesTotal, setSpacesTotal, forceRender }) { + const { dispatch } = useContext(AuthContext); + const { dispatch: globalDispatch } = useContext(GlobalContext); + + const [searchParams, setSearchParams] = useSearchParams(); + const { handleSubmit, register, watch, reset, setValue, control, formState, resetField } = useForm({ + defaultValues: (() => { + const params = parseSearchParams(searchParams); + return { + id: params.id ?? "", + space_name: params.space_name ?? "", + from: isValidDate(params.from ?? "") ? new Date(params.from) : new Date(), + to: isValidDate(params.to ?? "") ? new Date(params.to) : new Date(), + space_status: params.space_status ?? "", + availability: params.availability ?? "", + direction: "DESC", + }; + })(), + }); + + const { dirtyFields } = formState; + + const fromDate = watch("from"); + + const onSubmit = async (data) => { + const formatTo = formatDate(data.to) + const formatFrom = formatDate(data.from) + + searchParams.set("id", data.id); + searchParams.set("space_name", data.space_name); + searchParams.set("space_status", data.space_status); + searchParams.set("from", dirtyFields?.from ? formatFrom : ""); + searchParams.set("to", dirtyFields?.to ? formatTo : new Date().toISOString().split("T")[0]); + searchParams.set("availability", data.availability); + setSearchParams(searchParams); + fetchMySpaces(); + closeModal(); + }; + + async function fetchMySpaces(page) { + const host_id = +localStorage.getItem("user"); + setSpaces((prev) => { + const amountToFetch = spacesTotal - prev.length > FETCH_PER_SCROLL ? FETCH_PER_SCROLL : Math.abs(spacesTotal - prev.length - FETCH_PER_SCROLL); + return [...prev, ...Array(amountToFetch).fill({})]; + }); + + const filters = parseSearchParams(searchParams); + + var where = [`ergo_property.host_id = ${host_id} AND ergo_property_spaces.deleted_at IS NULL`]; + + if (filters.space_name) { + where.push(`ergo_property.name LIKE '%${filters.space_name}%'`); + } + + if (filters.from && filters.to === undefined) { + where.push(`ergo_property_spaces.create_at = ${filters.from}`) + } + + if (filters.to && filters.from === undefined) { + where.push(`ergo_property_spaces.create_at = ${filters.to}`) + } + if (filters.to && filters.from) { + where.push(`ergo_property_spaces.create_at BETWEEN '${filters.from}' AND '${filters.to}'`) + } + + if (filters.space_status) { + where.push(`ergo_property_spaces.space_status = ${filters.space_status} AND ergo_property_spaces.draft_status > 2`); + } + + if (filters.availability === "1") { + where.push(`ergo_property_spaces.availability = ${filters.availability} AND ergo_property_spaces.draft_status > 2 AND ergo_property_spaces.space_status = 1`); + } + if (filters.availability === "0") { + where.push(`ergo_property_spaces.availability = ${filters.availability} AND ergo_property_spaces.draft_status > 2 `); + } + + if (filters.id) { + where = [`ergo_property.host_id = ${host_id} AND ergo_property_spaces.id = ${filters.id} AND ergo_property_spaces.deleted_at IS NULL`]; + } + + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/popular/PAGINATE", + { page: page ?? 1, limit: FETCH_PER_SCROLL, user_id: host_id, where, all: true, sortId: "update_at", direction: "DESC" }, + "POST", + ctrl.signal, + ); + + if (Array.isArray(result.list)) { + // setSpaces(result.list); + setSpaces((prev) => { + return [...prev.filter((item) => Object.keys(item).length > 0), ...result.list].filter((v, i, a) => a.findIndex((v2) => v2.id === v.id) === i); + }); + setSpacesTotal(result.total); + } + forceRender(true) + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + return ( + + + +
    + + +
    +
    + + +
    +
    + + Filters + + +
    + {" "} +
    +
    +
    + +
    + resetField("from", { keepDirty: false, keepTouched: false })} + setValue={(val) => setValue("from", val, { shouldDirty: true })} + control={control} + name="from" + labelClassName="justify-between flex-grow flex-row-reverse" + placeholder="From" + type="space" + min={new Date("2001-01-01")} + /> +
    +
    + resetField("to", { keepDirty: false, keepTouched: false })} + setValue={(val) => setValue("to", val, { shouldDirty: true })} + control={control} + name="to" + labelClassName="justify-between flex-grow flex-row-reverse" + placeholder="To" + type="space" + min={fromDate} + /> +
    + + + + + +
    + +
    +
    +
    +
    +
    +
    + ); +} diff --git a/src/pages/Host/Spaces/MySpacesListPage.jsx b/src/pages/Host/Spaces/MySpacesListPage.jsx new file mode 100644 index 0000000..d42bc66 --- /dev/null +++ b/src/pages/Host/Spaces/MySpacesListPage.jsx @@ -0,0 +1,320 @@ +import React, { useRef } from "react"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import NextIcon from "@/components/frontend/icons/NextIcon"; +import { Link, useNavigate, useSearchParams } from "react-router-dom"; +import { useEffect } from "react"; +import { callCustomAPI } from "@/utils/callCustomAPI"; +import DatePicker from "@/components/frontend/DatePicker"; +import { useContext } from "react"; +import { GlobalContext } from "@/globalContext"; +import CustomSelect from "@/components/frontend/CustomSelect"; +import InfiniteScroll from "react-infinite-scroll-component"; +import NoteIcon from "@/components/frontend/icons/NoteIcon"; +import useDelayUnmount from "@/hooks/useDelayUnmount"; +import { ID_VERIFICATION_STATUSES } from "@/utils/constants"; +import MySpaceCard from "./MySpaceCard"; +import CustomSelectV2 from "@/components/CustomSelectV2"; +import { AdjustmentsHorizontalIcon } from "@heroicons/react/24/solid"; +import DatePickerV3 from "@/components/DatePickerV3"; +import { formatDate, increaseDate, isValidDate, parseSearchParams } from "@/utils/utils"; +import MkdSDK from "@/utils/MkdSDK"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import MySpacesFiltersModal from "./MySpacesFiltersModal"; +import DatePickerV2 from "@/components/frontend/DatePickerV2"; + +const sdk = new MkdSDK(); +const ctrl = new AbortController(); + +export default function MySpacesListPage() { + const FETCH_PER_SCROLL = 12; + const [searchParams, setSearchParams] = useSearchParams(); + const [spaces, setSpaces] = useState([]); + const [showFilter, setShowFilter] = useState(false); + const [render, forceRender] = useState(false); + const navigate = useNavigate(); + + const { dispatch: globalDispatch } = useContext(GlobalContext); + const { dispatch } = useContext(AuthContext); + const [spacesTotal, setSpacesTotal] = useState(100); + + const { handleSubmit, register, watch, reset, setValue, control, formState, resetField } = useForm({ + defaultValues: (() => { + const params = parseSearchParams(searchParams); + return { + id: params.id ?? "", + space_name: params.guest_name ?? "", + from: isValidDate(params.from ?? "") ? new Date(params.from) : new Date(), + to: isValidDate(params.to ?? "") ? new Date(params.to) : new Date(), + availability: params.availability ?? "", + space_status: params.space_status ?? "", + direction: "DESC", + }; + })(), + }); + + const { dirtyFields } = formState; + + const direction = watch("direction"); + const fromDate = watch("from"); + + const onSubmit = async (data) => { + if (window.innerWidth < 700) { + setShowFilter(false); + } + setSpaces([]); + const formatTo = formatDate(data.to) + const formatFrom = formatDate(data.from) + + searchParams.set("id", data.id); + searchParams.set("space_name", data.space_name); + searchParams.set("space_status", data.space_status); + searchParams.set("from", dirtyFields?.from ? formatFrom : ""); + searchParams.set("to", dirtyFields?.to ? formatTo : new Date().toISOString().split("T")[0]); + searchParams.set("availability", data.availability); + setSearchParams(searchParams); + fetchMySpaces() + }; + async function fetchMySpaces(page) { + const host_id = +localStorage.getItem("user"); + setSpaces((prev) => { + const amountToFetch = spacesTotal - prev.length > FETCH_PER_SCROLL ? FETCH_PER_SCROLL : Math.abs(spacesTotal - prev.length - FETCH_PER_SCROLL); + return [...prev, ...Array(amountToFetch).fill({})]; + }); + + const filters = parseSearchParams(searchParams); + + let where = [`ergo_property.host_id = ${host_id} AND ergo_property_spaces.deleted_at IS NULL`]; + + if (filters.space_name) { + where.push(`ergo_property.name LIKE '%${filters.space_name}%'`); + } + + if (filters.from !== undefined && filters.to === undefined) { + where.push(`ergo_property_spaces.create_at = ${filters.from}`) + } + + if (filters.to === undefined && filters.from !== undefined) { + where.push(`ergo_property_spaces.create_at = ${filters.to}`) + } + if (filters.to && filters.from) { + where.push(`ergo_property_spaces.create_at BETWEEN '${filters.from}' AND '${filters.to}'`) + } + + if (Number(filters.space_status) < 3) { + where.push(`ergo_property_spaces.space_status = ${filters.space_status} AND ergo_property_spaces.draft_status > 2`); + } + if (Number(filters.space_status) === 3) { + where.push(`ergo_property_spaces.draft_status < 3`); + } + + if (filters.availability === "1") { + where.push(`ergo_property_spaces.availability = ${filters.availability} AND ergo_property_spaces.draft_status > 2 AND ergo_property_spaces.space_status = 1`); + } + if (filters.availability === "0") { + where.push(`ergo_property_spaces.availability = ${filters.availability} AND ergo_property_spaces.draft_status > 2 AND ergo_property_spaces.space_status = 0`); + } + + if (filters.id) { + where = [`ergo_property.host_id = ${host_id} AND ergo_property_spaces.id = ${filters.id} AND ergo_property_spaces.deleted_at IS NULL`]; + } + + try { + const result = await sdk.callRawAPI( + "/v2/api/custom/ergo/popular/PAGINATE", + { page: page ?? 1, limit: FETCH_PER_SCROLL, user_id: host_id, where, all: true, sortId: "update_at", direction: "DESC" }, + "POST", + ctrl.signal, + ); + + if (Array.isArray(result.list)) { + // setSpaces(result.list); + setSpaces((prev) => { + return [...prev.filter((item) => Object.keys(item).length > 0), ...result.list].filter((v, i, a) => a.findIndex((v2) => v2.id === v.id) === i); + }); + setSpacesTotal(result.total); + } + } catch (err) { + tokenExpireError(dispatch, err.message); + if (err.name == "AbortError") return; + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + } + + useEffect(() => { + fetchMySpaces(); + }, []); + + useEffect(() => { + if (render) { + setSpaces([]); + fetchMySpaces(); + } + }, [render]); + + const sortByDate = (a, b) => { + if (direction == "DESC") { + return new Date(b.id) - new Date(a.id); + } + return new Date(a.id) - new Date(b.id); + }; + + return ( +
    +
    + +
    +
    +
    +
    + + +
    + +
    +
    + {spaces.length == 0 && ( +
    +

    + You have no spaces +

    +
    + )} + { + fetchMySpaces(Math.round(spaces.length / FETCH_PER_SCROLL + 1)); + }} + scrollThreshold={0.9} + hasMore={spaces.length < spacesTotal} + loader={<>} + endMessage={ + spaces.length > 10 && ( +

    + +

    + ) + } + className="pb-20" + > + {spaces.sort(sortByDate).map((space, i) => ( + + ))} +
    + setShowFilter(false)} + setSpaces={setSpaces} + forceRender={forceRender} + spacesTotal={spacesTotal} + FETCH_PER_SCROLL={FETCH_PER_SCROLL} + setSpacesTotal={setSpacesTotal} + /> +
    + ); +} diff --git a/src/pages/Host/Verification/HostVerificationPage.jsx b/src/pages/Host/Verification/HostVerificationPage.jsx new file mode 100644 index 0000000..96b6662 --- /dev/null +++ b/src/pages/Host/Verification/HostVerificationPage.jsx @@ -0,0 +1,317 @@ +import React, { useContext } from "react"; +import { useForm } from "react-hook-form"; +import { useNavigate } from "react-router"; +import Icon from "@/components/Icons"; +import { FileUploader } from "react-drag-drop-files"; +import { useState } from "react"; +import useDelayUnmount from "@/hooks/useDelayUnmount"; +import GreenCheckIcon from "@/components/frontend/icons/GreenCheckIcon"; +import SecurityIcon from "@/components/frontend/icons/SecurityIcon"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; +import MkdSDK from "@/utils/MkdSDK"; +import { LoadingButton } from "@/components/frontend"; +import { NOTIFICATION_STATUS, NOTIFICATION_TYPE } from "@/utils/constants"; +import { AuthContext, tokenExpireError } from "@/authContext"; +import { GlobalContext } from "@/globalContext"; +import { useSearchParams } from "react-router-dom"; + +let sdk = new MkdSDK(); + +export default function HostVerificationPage() { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + + const schema = yup.object({ + expiry_date: yup.string().test("is-not-in-past", "Not a valid date", (val) => { + if (val == "") return false; + const date = new Date(val); + return date.setDate(date.getDate() - 1) > new Date(); + }), + }); + + const { + handleSubmit, + register, + watch, + formState: { errors }, + } = useForm({ defaultValues: { selectedType: "Driver's License" }, resolver: yupResolver(schema) }); + + const [frontImage, setFrontImage] = useState(null); + const [backImage, setBackImage] = useState(null); + const [passport, setPassport] = useState(null); + const [loading, setLoading] = useState(false); + + const { dispatch: globalDispatch, state: globalState } = useContext(GlobalContext); + const { dispatch: authDispatch } = useContext(AuthContext); + + const [verified, setVerified] = useState(false); + const showVerified = useDelayUnmount(verified, 300); + + const selectedType = watch("selectedType"); + + const isDisabled = () => { + if (selectedType == "Driver's License" && frontImage && backImage) return false; + if (selectedType == "Passport" && passport) return false; + return true; + }; + + const handleImageUpload = async (file) => { + const formData = new FormData(); + formData.append("file", file); + try { + const upload = await sdk.uploadImage(formData); + return upload.url; + } catch (err) { + console.log("err", err); + return ""; + } + }; + + const onSubmit = async (data) => { + try { + setLoading(true); + if (selectedType == "Driver's License") { + data.frontImage = await handleImageUpload(frontImage); + data.backImage = await handleImageUpload(backImage); + } else { + data.frontImage = await handleImageUpload(passport); + } + sdk.setTable("id_verification"); + const result = await sdk.callRestAPI( + { + id: globalState.user.verificationId, + type: selectedType, + expiry_date: data.expiry_date, + status: 0, + image_front: data.frontImage, + image_back: data.backImage ?? null, + user_id: Number(localStorage.getItem("user")), + }, + globalState.user.verificationId ? "PUT" : "POST", + ); + + // create notification + sdk.setTable("notification"); + await sdk.callRestAPI( + { + user_id: Number(localStorage.getItem("user")), + actor_id: null, + action_id: result.message, + notification_time: new Date().toISOString().split(".")[0], + message: "New ID Verification submitted", + type: NOTIFICATION_TYPE.NEW_ID_VERIFICATION, + status: NOTIFICATION_STATUS.NOT_ADDRESSED, + }, + "POST", + ); + + setVerified(true); + } catch (err) { + tokenExpireError(authDispatch, err.message); + globalDispatch({ + type: "SHOW_ERROR", + payload: { + heading: "Operation failed", + message: err.message, + }, + }); + } + setLoading(false); + }; + + const readImage = (file, previewEl) => { + const reader = new FileReader(); + reader.onload = (event) => { + document.getElementById(previewEl).src = event.target.result; + }; + + reader.readAsDataURL(file); + }; + + return ( +
    +
    + +
    +

    Identity Verification

    +
    +

    + + Safety is our priority +

    +

    + To establish trust for all parties we verify both hosts and guests. Your personal information is secure. We will never share your information with third parties. +

    +
    +
    +

    Explain what document(s) are allowed.

    +
    + + +
    +
    + {selectedType == "Driver's License" ? ( +
    + { + setFrontImage(file); + }} + types={["SVG", "JPEG", "PNG", "GIF", "JPG"]} + > +
    + {frontImage?.name ? ( + + ) : ( + <> +

    Front

    +

    + Click to upload or drag and drop SVG, PNG, JPG or GIF (max. 800x400px) +

    + + )} +
    +
    + { + setBackImage(file); + }} + types={["SVG", "JPEG", "PNG", "GIF", "JPG"]} + > +
    + {backImage?.name ? ( + + ) : ( + <> +

    Back

    +

    + Click to upload or drag and drop SVG, PNG, JPG or GIF (max. 800x400px) +

    + + )} +
    +
    +
    + ) : ( + { + setPassport(file); + }} + types={["SVG", "JPEG", "PNG", "GIF", "JPG"]} + > +
    + {passport?.name ? ( + + ) : ( + <> +

    Passport page with photo

    +

    + Click to upload or drag and drop SVG, PNG, JPG or GIF (max. 800x400px) +

    + + )} +
    +
    + )} +
    +
    + + + {errors.expiry_date?.message &&

    {errors.expiry_date?.message}

    } +
    + + Submit Document + +
    +
    +
    e.stopPropagation()} + > +

    + + Document received +

    +

    Once we verify your document you will receive an email. It usually takes up to 24 hours.

    + +
    +
    +
    + ); +} diff --git a/src/service-worker.js b/src/service-worker.js new file mode 100644 index 0000000..576111d --- /dev/null +++ b/src/service-worker.js @@ -0,0 +1,44 @@ + +const CACHE_NAME = 'my-cache-v1'; + +// List of files to be cached +const urlsToCache = [ + '/', + './index.jsx', +]; + +// Install the service worker +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => { + return cache.addAll(urlsToCache); + }) + ); +}); + +// Activate the service worker +self.addEventListener('activate', (event) => { + const cacheWhitelist = [CACHE_NAME]; + + event.waitUntil( + caches.keys().then((cacheNames) => { + return Promise.all( + cacheNames.map((name) => { + if (cacheWhitelist.indexOf(name) === -1) { + return caches.delete(name); + } + return null; + }) + ); + }) + ); +}); + +// Fetch event +self.addEventListener('fetch', (event) => { + event.respondWith( + caches.match(event.request).then((response) => { + return response || fetch(event.request); + }) + ); +}); diff --git a/src/serviceWorkerRegistration.js b/src/serviceWorkerRegistration.js new file mode 100644 index 0000000..abd9b79 --- /dev/null +++ b/src/serviceWorkerRegistration.js @@ -0,0 +1,13 @@ +export function register() { + if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('./src/service-worker.js', { type: 'module' }) + .then((registration) => { + console.log('ServiceWorker registration successful with scope: ', registration.scope); + }) + .catch((error) => { + console.error('ServiceWorker registration failed: ', error); + }); + }); + } +} \ No newline at end of file diff --git a/src/utils/MkdSDK.jsx b/src/utils/MkdSDK.jsx new file mode 100644 index 0000000..f4b556d --- /dev/null +++ b/src/utils/MkdSDK.jsx @@ -0,0 +1,1868 @@ +// import { DeviceUUID } from "device-uuid"; +import { v4 as uuidv4 } from 'uuid'; + +export default function MkdSDK() { + this._baseurl = "https://ergo.mkdlabs.com"; + this._project_id = "ergo"; + this._secret = "k5go4l548ch4qk5918x2uljuv8rqqp2as"; + + this._table = ""; + this._custom = ""; + this._method = ""; + + const raw = this._project_id + ":" + this._secret; + let base64Encode = btoa(raw); + + this.login = async function (email, password, role) { + if (!localStorage.getItem("device-uid")) { + getUniqueUID(); + } + const result = await fetch(this._baseurl + "/v2/api/lambda/login", { + method: "post", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + }, + body: JSON.stringify({ + email, + password, + role, + }), + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + this.oauthLoginApi = async function (type, role) { + if (!localStorage.getItem("device-uid")) { + getUniqueUID(); + } + localStorage.setItem("originalRole", "customer"); + + const socialLogin = await fetch(`${this._baseurl}/v2/api/lambda/${type}/login?role=${role}`, { + method: 'GET', + headers: { + "x-project": base64Encode + } + }); + const socialLink = await socialLogin.text(); + + if (socialLogin.status === 401) { + throw new Error(socialLink.message); + } + + if (socialLogin.status === 403) { + throw new Error(socialLink.message); + } + + return socialLink; + }; + + this.setUUId = async function () { + await fetch(this._baseurl + "/v3/api/custom/ergo/device", { + method: "post", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + body: JSON.stringify({ uid: localStorage.getItem("device-uid") ?? getUniqueUID() }), + }); + } + + this.customLogin = async function (payload) { + const result = await fetch(this._baseurl + "/v3/api/ergo/login", { + method: "post", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + }, + body: JSON.stringify({ ...payload, is_refresh: true, uid: getUniqueUID() }), + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + this.loginTwoFa = async function (email, password, role) { + const result = await fetch(this._baseurl + "/v2/api/lambda/2fa/login", { + method: "post", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + }, + body: JSON.stringify({ + email, + password, + role, + }), + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + this.getHeader = function () { + return { + Authorization: "Bearer " + localStorage.getItem("token"), + "x-project": base64Encode, + }; + }; + + this.baseUrl = function () { + return this._baseurl; + }; + this.uploadUrl = function () { + return this._baseurl + "/v2/api/lambda/upload"; + }; + + this.upload = function (payload) { }; + + this.getProfile = async function () { + const result = await fetch(this._baseurl + "/v2/api/lambda/profile", { + method: "get", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + this.getProfileCustom = async function () { + const profileResult = await this.fetchJoinTwoTables( + "user", + "profile", + "id", + "user_id", + "user.*, profile.dob, profile.about, profile.address_line_1, profile.address_line_2, profile.city, profile.country, profile.settings, profile.getting_started", + [`user.id = ${localStorage.getItem("user")}`], + ); + + if (!Array.isArray(profileResult.list) && profileResult.list.length > 0) throw new Error("Failed to get user"); + + // get verification status + this.setTable("id_verification"); + const verificationResult = await this.callRestAPI({ payload: { user_id: localStorage.getItem("user") }, limit: 1, page: 1, sortId: "id", direction: "DESC" }, "PAGINATE"); + + let verificationData = {}; + + if (Array.isArray(verificationResult.list) && verificationResult.list.length > 0) { + verificationData.verificationStatus = verificationResult.list[0].status; + verificationData.verificationType = verificationResult.list[0].type; + verificationData.verificationImageFront = verificationResult.list[0].image_front; + verificationData.verificationImageBack = verificationResult.list[0].image_back; + verificationData.verificationExpiry = verificationResult.list[0].expiry_date; + verificationData.verificationId = verificationResult.list[0].id; + } + + // TODO: get user preferences + + return { ...profileResult.list[0], ...verificationData }; + }; + + this.editProfile = async function (first_name, last_name) { + const result = await fetch(this._baseurl + "/v2/api/lambda/profile", { + method: "post", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + body: JSON.stringify({ payload: { first_name, last_name } }), + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + this.check = async function (role) { + const result = await fetch(this._baseurl + "/v2/api/lambda/check", { + method: "post", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + body: JSON.stringify({ + role, + }), + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + return json; + }; + + this.getProfilePreference = async function () { + const result = await fetch(this._baseurl + "/v2/api/lambda/preference", { + method: "get", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + // update email + this.updateEmail = async function (email) { + const result = await fetch(this._baseurl + "/v2/api/lambda/update/email", { + method: "post", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + body: JSON.stringify({ + email, + }), + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + // update password + this.updatePassword = async function (body) { + const result = await fetch(this._baseurl + "/v2/api/lambda/update/password", { + method: "post", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + body: JSON.stringify(body), + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + // update email + this.updateEmailByAdmin = async function (email, id) { + const result = await fetch(this._baseurl + "/v2/api/lambda/admin/update/email", { + method: "post", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + body: JSON.stringify({ + email, + id, + }), + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + // update password + this.updatePasswordByAdmin = async function (password, id) { + const result = await fetch(this._baseurl + "/v2/api/lambda/admin/update/password", { + method: "post", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + body: JSON.stringify({ + password, + id, + }), + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + this.sendEmail = async function (to, subject, body) { + const result = await fetch(this._baseurl + "/v2/api/lambda/mail/send", { + method: "post", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + }, + body: JSON.stringify({ + to, + from: "info@mkd.com", + subject, + body, + }), + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + this.sendEmailVerification = function () { }; + this.updateEmailVerification = function () { }; + + this.setTable = function (table) { + this._table = table; + }; + + this.getProjectId = function () { + return this._project_id; + }; + + this.logout = async function () { + const result = await fetch(this._baseurl + "/v3/api/ergo/logout", { + method: "post", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + uid: localStorage.getItem("device-uid"), + }, + body: JSON.stringify({ uid: localStorage.getItem("device-uid") }), + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + this.register = async function (email, password, role) { + if (!localStorage.getItem("device-uid")) { + getUniqueUID(); + } + const result = await fetch(this._baseurl + "/v2/api/lambda/register-email", { + method: "post", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + }, + body: JSON.stringify({ + email, + password, + role, + }), + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + this.verifyEmail = async function (token) { + const result = await fetch(this._baseurl + "/v2/api/lambda/verify-email?token=" + token, { + method: "get", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + }, + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + this.forgot = async function (email, role) { + const result = await fetch(this._baseurl + "/v2/api/lambda/forgot", { + method: "post", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + }, + body: JSON.stringify({ + email, + role, + }), + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + this.reset = async function (token, code, password) { + const result = await fetch(this._baseurl + "/v2/api/lambda/reset", { + method: "post", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + }, + body: JSON.stringify({ + token, + code, + password, + }), + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + this.fetchImage = async function (url) { + const result = await fetch(this._baseurl + `/v2/api/custom/ergo/s3proxy/${url}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + }); + // const json = await result.json(); + + // if (result.status === 401) { + // throw new Error(json.message); + // } + + // if (result.status === 403) { + // throw new Error(json.message); + // } + return result; + } + + this.callRestAPI = async function (payload, method, signal) { + const header = { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + + switch (method) { + case "GET": + const getResult = await fetch(this._baseurl + `/v1/api/rest/${this._table}/GET`, { + method: "post", + headers: header, + body: JSON.stringify(payload), + signal, + }); + const jsonGet = await getResult.json(); + + if (getResult.status === 401) { + throw new Error(jsonGet.message); + } + + if (getResult.status === 403) { + throw new Error(jsonGet.message); + } + return jsonGet; + case "POST": + const insertResult = await fetch(this._baseurl + `/v1/api/rest/${this._table}/${method}`, { + method: "post", + headers: header, + body: JSON.stringify(payload), + }); + const jsonInsert = await insertResult.json(); + + if (insertResult.status === 401) { + throw new Error(jsonInsert.message); + } + + if (insertResult.status === 403) { + throw new Error(jsonInsert.message); + } + return jsonInsert; + case "PUT": + const updateResult = await fetch(this._baseurl + `/v1/api/rest/${this._table}/${method}`, { + method: "post", + headers: header, + body: JSON.stringify(payload), + }); + const jsonUpdate = await updateResult.json(); + + if (updateResult.status === 401) { + throw new Error(jsonUpdate.message); + } + + if (updateResult.status === 403) { + throw new Error(jsonUpdate.message); + } + return jsonUpdate; + + // Part: Update Table Without Using ID + case "PUTWHERE": + const updateWhereRes = await fetch(this._baseurl + `/v1/api/rest/${this._table}/${method}`, { + method: "post", + headers: header, + body: JSON.stringify(payload), // Note: payload: {set: {[string]: any}, where: {[string]: any}} + }); + const jsonUpdateWhereRes = await updateWhereRes.json(); + + if (updateWhereRes.status === 401) { + throw new Error(jsonUpdateWhereRes.message); + } + + return jsonUpdateWhereRes; + + case "DELETE": + const deleteResult = await fetch(this._baseurl + `/v1/api/rest/${this._table}/${method}`, { + method: "post", + headers: header, + body: JSON.stringify(payload), + }); + const jsonDelete = await deleteResult.json(); + + if (deleteResult.status === 401) { + throw new Error(jsonDelete.message); + } + + if (deleteResult.status === 403) { + throw new Error(jsonDelete.message); + } + return jsonDelete; + case "DELETEALL": + const deleteAllResult = await fetch(this._baseurl + `/v1/api/rest/${this._table}/${method}`, { + method: "post", + headers: header, + body: JSON.stringify(payload), + }); + const jsonDeleteAll = await deleteAllResult.json(); + + if (deleteAllResult.status === 401) { + throw new Error(jsonDeleteAll.message); + } + + if (deleteAllResult.status === 403) { + throw new Error(jsonDeleteAll.message); + } + return jsonDeleteAll; + case "GETALL": + const getAllResult = await fetch(this._baseurl + `/v1/api/rest/${this._table}/${method}`, { + method: "post", + headers: header, + body: JSON.stringify(payload), + }); + const jsonGetAll = await getAllResult.json(); + + if (getAllResult.status === 401) { + throw new Error(jsonGetAll.message); + } + + if (getAllResult.status === 403) { + throw new Error(jsonGetAll.message); + } + return jsonGetAll; + case "PAGINATE": + if (!payload.page) { + payload.page = 1; + } + if (!payload.limit) { + payload.limit = 10; + } + const paginateResult = await fetch(this._baseurl + `/v1/api/rest/${this._table}/${method}`, { + method: "post", + headers: header, + body: JSON.stringify(payload), + }); + const jsonPaginate = await paginateResult.json(); + + if (paginateResult.status === 401) { + throw new Error(jsonPaginate.message); + } + + if (paginateResult.status === 403) { + throw new Error(jsonPaginate.message); + } + return jsonPaginate; + case "AUTOCOMPLETE": + const autocompleteResult = await fetch(this._baseurl + `/v1/api/rest/${this._table}/${method}`, { + method: "post", + headers: header, + body: JSON.stringify(payload), + }); + const jsonAutocomplete = await autocompleteResult.json(); + + if (autocompleteResult.status === 401) { + throw new Error(jsonAutocomplete.message); + } + + if (autocompleteResult.status === 403) { + throw new Error(jsonAutocomplete.message); + } + return jsonAutocomplete; + default: + break; + } + }; + + this.callRawAPI = async function (uri, payload, method, signal) { + const header = { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + uid: localStorage.getItem("device-uid"), + }; + + const result = await fetch(this._baseurl + uri, { + method: method, + headers: header, + body: JSON.stringify(payload), + signal, + }); + const jsonResult = await result.json(); + + if (result.status === 401) { + throw new Error(jsonResult.message); + } + + if (result.status === 403) { + throw new Error(jsonResult.message); + } + return jsonResult; + }; + + // Part: Get All Data by Joining Two Columns + this.fetchJoinTwoTables = async function (table_1, table_2, join_id_1, join_id_2, select = "", where = [1], method = "GETALL", page = 1, limit = 10000) { + const header = { + "content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + const payload = { + tables: [table_1, table_2], + join_id_1, + join_id_2, + select, + where, + page, + limit, + }; + const paginateResult = await fetch(this._baseurl + `/v1/api/join/${table_1}/${table_2}/${method}`, { method: "post", headers: header, body: JSON.stringify(payload) }); + + const jsonPaginate = await paginateResult.json(); + + if (paginateResult.status === 401) { + throw new Error(jsonPaginate.message); + } + + if (paginateResult.status === 403) { + throw new Error(jsonPaginate.message); + } + return jsonPaginate; + }; + + // Part: Get Data by Joining Multiple Columns with Pagination + this.callMultiJoinRestAPI = async function (tables, joinIds, selectStr, where, page, limit, method) { + const header = { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + + if (!page) { + page = 1; + } + if (!limit) { + limit = 10; + } + const paginateResult = await fetch(this._baseurl + `/v1/api/multi-join/${method}`, { + method: "post", + headers: header, + body: JSON.stringify({ + tables, // ["tableName1", "tableName2"] + joinIds, // ["tableName1.id", "tableName2.id"] + selectStr, // "tableName1.field1, tableName2.field2" + where, // ["status=2424", "id=1"] + page, + limit, + }), + }); + const jsonPaginate = await paginateResult.json(); + + if (paginateResult.status === 401) { + throw new Error(jsonPaginate.message); + } + + if (paginateResult.status === 403) { + throw new Error(jsonPaginate.message); + } + return jsonPaginate; + }; + + this.subscribe = function (payload) { }; + this.subscribeChannel = function (channel, payload) { }; + this.subscribeListen = function (channel) { }; + this.unSubscribeChannel = function (channel, payload) { }; + this.broadcast = function (payload) { }; + + this.exportCSV = async function () { + const header = { + "content-type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + const getResult = await fetch(this._baseurl + `/v1/api/rest/${this._table}/EXPORT`, { + method: "post", + headers: header, + }); + const res = await getResult.text(); + let hiddenElement = document.createElement("a"); + hiddenElement.href = "data:text/csv;charset=utf-8," + encodeURI(res); + hiddenElement.target = "_blank"; + + hiddenElement.download = this._table + ".csv"; + hiddenElement.click(); + + if (getResult.status === 401) { + throw new Error(res.message); + } + + if (getResult.status === 403) { + throw new Error(res.message); + } + }; + + this.cmsAdd = async function (page, key, type, value) { + const header = { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + + const insertResult = await fetch(this._baseurl + `/v2/api/lambda/cms`, { + method: "post", + headers: header, + body: JSON.stringify({ + page, + key, + type, + value, + }), + }); + const jsonInsert = await insertResult.json(); + + if (insertResult.status === 401) { + throw new Error(jsonInsert.message); + } + + if (insertResult.status === 403) { + throw new Error(jsonInsert.message); + } + return jsonInsert; + }; + + this.cmsEdit = async function (id, page, key, type, value) { + const header = { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + + const updateResult = await fetch(this._baseurl + `/v2/api/lambda/cms/` + id, { + method: "put", + headers: header, + body: JSON.stringify({ + page, + key, + type, + value, + }), + }); + const jsonInsert = await updateResult.json(); + + if (updateResult.status === 401) { + throw new Error(jsonInsert.message); + } + + if (updateResult.status === 403) { + throw new Error(jsonInsert.message); + } + return jsonInsert; + }; + + this.getToken = function () { + return window.localStorage.getItem("token"); + }; + + // get chat id + this.getChatId = async function (room_id) { + const result = await fetch(this._baseurl + `/v2/api/lambda/room?room_id=${room_id}`, { + method: "get", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + // post chat + this.getChats = async function (room_id, chat_id, date) { + const result = await fetch(this._baseurl + `/v2/api/lambda/chat`, { + method: "post", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + body: JSON.stringify({ + room_id, + chat_id, + date, + }), + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + this.restoreChat = async function (room_id) { + await fetch(this._baseurl + `/v2/api/lambda/v2/api/lambda/room/poll?room=${room_id}`, { + method: "get", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + }); + }; + + // post a new message + this.postMessage = async function (messageDetails) { + const result = await fetch(this._baseurl + `/v3/api/lambda/realtime/send`, { + method: "post", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + body: JSON.stringify(messageDetails), + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + this.uploadImage = async function (file) { + const result = await fetch(this._baseurl + `/v2/api/lambda/s3/upload`, { + method: "post", + headers: { + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + body: file, + }); + + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + this.createRoom = async function (roomDetails) { + const result = await fetch(this._baseurl + `/v2/api/lambda/room`, { + method: "post", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + body: JSON.stringify(roomDetails), + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + this.getAllUsers = async function () { + const result = await fetch(this._baseurl + `/v1/api/rest/user/GETALL`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + // start pooling + this.startPooling = async function (user_id, signal) { + const result = await fetch(this._baseurl + `/v3/api/lambda/realtime/room/poll?user_id=${user_id}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + signal, + }); + + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + /** + * start stripe functions + */ + + this.addStripeProduct = async function (data) { + const result = await fetch(this._baseurl + "/v2/api/lambda/stripe/product", { + method: "post", + headers: { + "content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + body: JSON.stringify(data), + }); + + const json = await result.json(); + if ([401, 403, 500].includes(result.status)) { + throw new Error(json.message); + } + return json; + }; + + this.getStripeProducts = async function (paginationParams, filterParams) { + const header = { + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + const paginationQuery = new URLSearchParams(paginationParams); + const filterQuery = new URLSearchParams(filterParams); + const getResult = await fetch(this._baseurl + `/v2/api/lambda/stripe/products?${paginationQuery}&${filterQuery}`, { + method: "get", + headers: header, + }); + const jsonGet = await getResult.json(); + + if ([401, 403, 500].includes(getResult.status)) { + throw new Error(jsonGet.message); + } + + return jsonGet; + }; + + this.getStripeProduct = async function (id) { + const header = { + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + const getResult = await fetch(this._baseurl + `/v2/api/lambda/stripe/product/${id}`, { + method: "get", + headers: header, + }); + const jsonGet = await getResult.json(); + + if ([401, 403, 500].includes(getResult.status)) { + throw new Error(jsonGet.message); + } + + return jsonGet; + }; + + this.updateStripeProduct = async function (id, payload) { + const header = { + "content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + const getResult = await fetch(this._baseurl + `/v2/api/lambda/stripe/product/${id}`, { + method: "put", + headers: header, + body: JSON.stringify(payload), + }); + const jsonGet = await getResult.json(); + + if ([401, 403, 500].includes(getResult.status)) { + throw new Error(jsonGet.message); + } + + return jsonGet; + }; + + this.addStripePrice = async function (data) { + const result = await fetch(this._baseurl + "/v2/api/lambda/stripe/price", { + method: "post", + headers: { + "content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + body: JSON.stringify(data), + }); + + const json = await result.json(); + if ([401, 403, 500].includes(result.status)) { + throw new Error(json.message); + } + return json; + }; + + this.getStripePrices = async function (paginationParams, filterParams) { + const header = { + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + const paginationQuery = new URLSearchParams(paginationParams); + const filterQuery = new URLSearchParams(filterParams); + const getResult = await fetch(this._baseurl + `/v2/api/lambda/stripe/prices?${paginationQuery}&${filterQuery}`, { + method: "get", + headers: header, + }); + const jsonGet = await getResult.json(); + + if ([401, 403, 500].includes(getResult.status)) { + throw new Error(jsonGet.message); + } + + return jsonGet; + }; + + this.getStripePrice = async function (id) { + const header = { + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + const getResult = await fetch(this._baseurl + `/v2/api/lambda/stripe/price/${id}`, { + method: "get", + headers: header, + }); + const jsonGet = await getResult.json(); + + if ([401, 403, 500].includes(getResult.status)) { + throw new Error(jsonGet.message); + } + + return jsonGet; + }; + + this.updateStripePrice = async function (id, payload) { + const header = { + "content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + const getResult = await fetch(this._baseurl + `/v2/api/lambda/stripe/price/${id}`, { + method: "put", + headers: header, + body: JSON.stringify(payload), + }); + const jsonGet = await getResult.json(); + + if ([401, 403, 500].includes(getResult.status)) { + throw new Error(jsonGet.message); + } + + return jsonGet; + }; + + this.getStripeSubscriptions = async function (paginationParams, filterParams) { + const header = { + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + const paginationQuery = new URLSearchParams(paginationParams); + const filterQuery = new URLSearchParams(filterParams); + const getResult = await fetch(this._baseurl + `/v2/api/lambda/stripe/subscriptions?${paginationQuery}&${filterQuery}`, { + method: "get", + headers: header, + }); + const jsonGet = await getResult.json(); + + if ([401, 403, 500].includes(getResult.status)) { + throw new Error(jsonGet.message); + } + + return jsonGet; + }; + + this.adminCancelStripeSubscription = async function (subId, data) { + const result = await fetch(this._baseurl + `/v2/api/lambda/stripe/subscription/${subId}`, { + method: "delete", + headers: { + "content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + body: JSON.stringify(data), + }); + + const json = await result.json(); + if ([401, 403, 500].includes(result.status)) { + throw new Error(json.message); + } + + return json; + }; + + this.adminCreateUsageCharge = async function (subId, quantity) { + const result = await fetch(this._baseurl + `/v2/api/lambda/stripe/subscription/usage-charge`, { + method: "post", + headers: { + "content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + body: JSON.stringify({ + subId, + quantity, + }), + }); + + const json = await result.json(); + if ([401, 403, 500].includes(result.status)) { + throw new Error(json.message); + } + + return json; + }; + + this.getStripeInvoices = async function (paginationParams, filterParams) { + const header = { + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + const paginationQuery = new URLSearchParams(paginationParams); + const filterQuery = new URLSearchParams(filterParams); + const getResult = await fetch(this._baseurl + `/v2/api/lambda/stripe/invoices?${paginationQuery}`, { + method: "get", + headers: header, + }); + + const jsonGet = await getResult.json(); + + if ([401, 403, 500].includes(getResult.status)) { + throw new Error(jsonGet.message); + } + + return jsonGet; + }; + this.getStripeInvoicesV2 = async function (paginationParams, filterParams) { + const header = { + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + const paginationQuery = new URLSearchParams(paginationParams); + const filterQuery = new URLSearchParams(filterParams); + const getResult = await fetch(this._baseurl + `/v2/api/lambda/stripe/invoices-v2?${paginationQuery}`, { + method: "get", + headers: header, + }); + + const jsonGet = await getResult.json(); + + if ([401, 403, 500].includes(getResult.status)) { + throw new Error(jsonGet.message); + } + + return jsonGet; + }; + + this.getStripeOrders = async function (paginationParams, filterParams) { + const header = { + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + const paginationQuery = new URLSearchParams(paginationParams); + const filterQuery = new URLSearchParams(filterParams); + const getResult = await fetch(this._baseurl + `/v2/api/lambda/stripe/orders?${paginationQuery}&${filterQuery}`, { + method: "get", + headers: header, + }); + + const jsonGet = await getResult.json(); + + if ([401, 403, 500].includes(getResult.status)) { + throw new Error(jsonGet.message); + } + + return jsonGet; + }; + + /** + * ------------------------------------------------------- + */ + + this.initCheckoutSession = async function (data) { + const result = await fetch(this._baseurl + "/v2/api/lambda/stripe/checkout", { + method: "post", + headers: { + "content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + body: JSON.stringify(data), + }); + + const json = await result.json(); + if ([401, 403, 500].includes(result.status)) { + throw new Error(json.message); + } + return json; + }; + + this.registerAndSubscribe = async function (data) { + /** + * + * @param {object} data {email, password, cardToken, planId} + * @returns + */ + const result = await fetch(this._baseurl + "/v2/api/lambda/stripe/customer/register-subscribe", { + method: "post", + headers: { + "content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + body: JSON.stringify(data), + }); + + const json = await result.json(); + if ([401, 403, 500].includes(result.status)) { + throw new Error(json.message); + } + return json; + }; + + this.createStripeCustomer = async function (payload) { + const header = { + "content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + + const getResult = await fetch(this._baseurl + `/v2/api/lambda/stripe/customer`, { + method: "post", + headers: header, + body: JSON.stringify(payload), + }); + const jsonGet = await getResult.json(); + + if ([401, 403, 500].includes(getResult.status)) { + throw new Error(jsonGet.message); + } + + return jsonGet; + }; + + this.createCustomerStripeCard = async function (payload, signal) { + const header = { + "content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + + const getResult = await fetch(this._baseurl + `/v2/api/lambda/stripe/customer/card`, { + method: "post", + headers: header, + body: JSON.stringify(payload), + signal, + }); + + const jsonGet = await getResult.json(); + + if ([401, 403, 500].includes(getResult.status)) { + throw new Error(jsonGet.message); + } + + return jsonGet; + }; + + this.createStripeSubscription = async function (data) { + const result = await fetch(this._baseurl + "/v2/api/lambda/stripe/customer/subscription", { + method: "post", + headers: { + "content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + body: JSON.stringify(data), + }); + + const json = await result.json(); + if ([401, 403, 500].includes(result.status)) { + throw new Error(json.message); + } + return json; + }; + + this.getCustomerStripeSubscription = async function (userId) { + const header = { + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + + const getResult = await fetch(this._baseurl + `/v2/api/lambda/stripe/customer/subscription`, { + method: "get", + headers: header, + }); + const jsonGet = await getResult.json(); + + if ([401, 403, 500].includes(getResult.status)) { + throw new Error(jsonGet.message); + } + + return jsonGet; + }; + + this.getCustomerStripeSubscriptions = async function (paginationParams, filterParams) { + const header = { + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + const paginationQuery = new URLSearchParams(paginationParams); + const filterQuery = new URLSearchParams(filterParams); + const getResult = await fetch(this._baseurl + `/v2/api/lambda/stripe/customer/subscriptions?${paginationQuery}&${filterQuery}`, { + method: "get", + headers: header, + }); + const jsonGet = await getResult.json(); + + if ([401, 403, 500].includes(getResult.status)) { + throw new Error(jsonGet.message); + } + + return jsonGet; + }; + + this.changeStripeSubscription = async function (data) { + const result = await fetch(this._baseurl + "/v2/api/lambda/stripe/customer/subscription", { + method: "put", + headers: { + "content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + body: JSON.stringify(data), + }); + + const json = await result.json(); + if ([401, 403, 500].includes(result.status)) { + throw new Error(json.message); + } + return json; + }; + + this.cancelStripeSubscription = async function (subId, data) { + const result = await fetch(this._baseurl + `/v2/api/lambda/stripe/customer/subscription/${subId}`, { + method: "delete", + headers: { + "content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + body: JSON.stringify(data), + }); + + const json = await result.json(); + if ([401, 403, 500].includes(result.status)) { + throw new Error(json.message); + } + return json; + }; + + this.getCustomerStripeDetails = async function () { + const header = { + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + + const getResult = await fetch(this._baseurl + `/v2/api/lambda/stripe/customer`, { + method: "get", + headers: header, + }); + + const jsonGet = await getResult.json(); + + if ([401, 403, 500].includes(getResult.status)) { + throw new Error(jsonGet.message); + } + + return jsonGet; + }; + + this.getCustomerStripeCards = async function (paginationParams) { + const header = { + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + const paginationQuery = new URLSearchParams(paginationParams); + const getResult = await fetch(this._baseurl + `/v2/api/lambda/stripe/customer/cards?${paginationQuery}`, { + method: "get", + headers: header, + }); + + const jsonGet = await getResult.json(); + + if ([401, 403, 500].includes(getResult.status)) { + throw new Error(jsonGet.message); + } + + return jsonGet; + }; + + this.getCustomerStripeInvoices = async function (paginationParams) { + const header = { + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + const paginationQuery = new URLSearchParams(paginationParams); + const getResult = await fetch(this._baseurl + `/v2/api/lambda/stripe/customer/invoices?${paginationQuery}`, { + method: "get", + headers: header, + }); + + const jsonGet = await getResult.json(); + + if ([401, 403, 500].includes(getResult.status)) { + throw new Error(jsonGet.message); + } + + return jsonGet; + }; + + this.getCustomerStripeCharges = async function (paginationParams) { + const header = { + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + const paginationQuery = new URLSearchParams(paginationParams); + const getResult = await fetch(this._baseurl + `/v2/api/lambda/stripe/customer/charges?${paginationQuery}`, { + method: "get", + headers: header, + }); + + const jsonGet = await getResult.json(); + + if ([401, 403, 500].includes(getResult.status)) { + throw new Error(jsonGet.message); + } + + return jsonGet; + }; + + this.getCustomerStripeOrders = async function (paginationParams) { + const header = { + Authorization: "Bearer " + localStorage.getItem("token"), + "x-project": base64Encode, + }; + const paginationQuery = new URLSearchParams(paginationParams); + const getResult = await fetch(this._baseurl + `/v2/api/lambda/stripe/customer/orders?${paginationQuery}`, { + method: "get", + headers: header, + }); + + const jsonGet = await getResult.json(); + + if ([401, 403, 500].includes(getResult.status)) { + throw new Error(jsonGet.message); + } + + return jsonGet; + }; + + this.setStripeCustomerDefaultCard = async function (cardId) { + const header = { + "content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + const getResult = await fetch(this._baseurl + `/v2/api/lambda/stripe/customer/card/${cardId}/set-default`, { + method: "put", + headers: header, + }); + const jsonGet = await getResult.json(); + + if ([401, 403, 500].includes(getResult.status)) { + throw new Error(jsonGet.message); + } + + return jsonGet; + }; + + this.deleteCustomerStripeCard = async function (cardId) { + const header = { + "content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + const getResult = await fetch(this._baseurl + `/v2/api/lambda/stripe/customer/card/${cardId}`, { + method: "delete", + headers: header, + }); + const jsonGet = await getResult.json(); + + if ([401, 403, 500].includes(getResult.status)) { + throw new Error(jsonGet.message); + } + + return jsonGet; + }; + + /** end stripe functions */ + // FOR CHAT COMPONENT + + // get chat room + this.getMyRoom = async function () { + const result = await fetch(this._baseurl + "/v3/api/lambda/realtime/room/my", { + method: "GET", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + }); + + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + // get chat id + this.getChatId = async function (room_id) { + const result = await fetch(this._baseurl + `/v2/api/lambda/room?room_id=${room_id}`, { + method: "get", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + // post chat + this.getChats = async function (room_id, date) { + const result = await fetch(this._baseurl + `/v3/api/lambda/realtime/chat`, { + method: "post", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + body: JSON.stringify({ + room_id, + + date, + }), + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + this.restoreChat = async function (room_id) { + await fetch(this._baseurl + `/v2/api/lambda/v2/api/lambda/room/poll?room=${room_id}`, { + method: "get", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + }); + }; + + // post a new message + this.postMessage = async function (messageDetails) { + const result = await fetch(this._baseurl + `/v3/api/lambda/realtime/send`, { + method: "post", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + body: JSON.stringify(messageDetails), + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + this.createRoom = async function (roomDetails) { + const result = await fetch(this._baseurl + `/v3/api/lambda/realtime/room`, { + method: "post", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + body: JSON.stringify(roomDetails), + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + this.getAllUsers = async function () { + const result = await fetch(this._baseurl + `/v1/api/rest/user/GETALL`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + return json; + }; + + this.getEmailTemplate = async function (slug) { + const result = await fetch(this._baseurl + `/v2/api/custom/ergo/email/PAGINATE`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }, + body: JSON.stringify({ page: 1, limit: 1, where: [`slug LIKE '%${slug}%'`] }), + }); + const json = await result.json(); + + if (result.status === 401) { + throw new Error(json.message); + } + + if (result.status === 403) { + throw new Error(json.message); + } + if (Array.isArray(json.list) && json.list.length > 0) { + return json.list[0]; + } + + return {}; + }; + + return this; +} + +export async function fetchProfile(user_id) { + try { + const sdk = new MkdSDK(); + const result = await sdk.fetchJoinTwoTables( + "user", + "profile", + "id", + "user_id", + "user.*, profile.dob, profile.about, profile.address_line_1, profile.address_line_2, profile.city, profile.country", + [`user.id = ${user_id ?? localStorage.getItem("user")}`], + ); + console.log(result); + if (Array.isArray(result.list) && result.list.length > 0) { + console.log("CUSTOM API(fetchProfile): ", result.list[0]); + return result.list[0]; + } + throw new Error("profile not found"); + } catch (err) { + console.log("CUSTOM ERROR: ", err); + throw err; + } +} + +function getUniqueUID() { + const uid = uuidv4(); + localStorage.setItem("device-uid", uid); + return uid; +} diff --git a/src/utils/ScrollToTop.jsx b/src/utils/ScrollToTop.jsx new file mode 100644 index 0000000..ed3dd21 --- /dev/null +++ b/src/utils/ScrollToTop.jsx @@ -0,0 +1,13 @@ +import { useEffect } from "react"; +import { useLocation } from "react-router-dom"; + +function ScrollToTop() { + const { pathname } = useLocation(); + useEffect(() => { + window.scrollTo(0, 0); + }, [pathname]); + + return null; +} + +export default ScrollToTop; diff --git a/src/utils/TreeSDK.jsx b/src/utils/TreeSDK.jsx new file mode 100644 index 0000000..bf8b00e --- /dev/null +++ b/src/utils/TreeSDK.jsx @@ -0,0 +1,396 @@ +import { empty } from "./utils"; + +export default function TreeSDK() { + this._baseurl = "https://ergo.mkdlabs.com"; + this._project_id = "ergo"; + this._secret = "k5go4l548ch4qk5918x2uljuv8rqqp2as"; + this._table = ""; + + const raw = this._project_id + ":" + this._secret; + let base64Encode = btoa(raw); + + this.getHeader = function () { + return { + Authorization: "Bearer " + localStorage.getItem("token"), + "x-project": base64Encode, + }; + }; + + this.baseUrl = function () { + return this._baseurl; + }; + + this.getProjectId = function () { + return this._project_id; + }; + + this.treeBaseUrl = function () { + return this._baseurl + "/v4/api/records"; + }; + + function getJoins(options = {}) { + let hasJoin = options.hasOwnProperty("join"); + let joins = options.join; + if (hasJoin && typeof joins == "string") joins = joins.split(","); + + let joinQuery = ""; + joins.forEach((join) => { + joinQuery += `join=${join}&`; + }); + + return [hasJoin, joins, joinQuery]; + } + + function getOrdering(options) { + let order = options.order ?? "id"; + let direction = options.direction ?? "desc"; + + return `order=${order},${direction}&`; + } + + function getFilters(options) { + let hasFilter = options.hasOwnProperty("filter"); + let filters = options.filter; + + let filterQuery = ""; + if (hasFilter && Array.isArray(filters)) { + filters.forEach((filter) => { + filterQuery += `filter=${filter}&`; + }); + } + + return [hasFilter, filters, filterQuery]; + } + + /* + Returns one entry + @params table : string - name of table to fetch + @params id : number - id to fetch + @params options : object - optional parameters + options.join - Array or comma separated list of tables to join + + let res = await (new TreeSDK).getOne('author', 1 { + join: ['book'], + }); + + */ + + this.getOne = async function (table, id, options = {}) { + if (empty(table) || empty(id)) throw new Error("table and id is required."); + + let [hasJoin, joins, joinQuery] = getJoins(options); + const header = { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + + const getResult = await fetch(this.treeBaseUrl() + `/${table}/${id}?${joinQuery}`, { + method: "get", + headers: header, + }); + const json = await getResult.json(); + + if (getResult.status === 401) { + throw new Error(json.message); + } + + if (getResult.status === 403) { + throw new Error(json.message); + } + return json; + }; + + /* + Returns one or more entries + @params table : string - name of table to fetch + @params ids : Array|string|number - array, comma separated list of ids or just a single id to fetch + @params options : object - optional parameters + options.join - Array or comma separated list of tables to join + + let res = await (new TreeSDK).getMany('author', [1,2] { + join: ['book'], + }); + + */ + this.getMany = async function (table, ids, options = {}) { + if (empty(table) || empty(ids)) throw new Error("table and id is required."); + + let [hasJoin, joins, joinQuery] = getJoins(options); + let id = Array.isArray(ids) ? ids.join(",") : ids; + const header = { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + + const getResult = await fetch(this.treeBaseUrl() + `/${table}/${id}?${joinQuery}`, { + method: "get", + headers: header, + }); + const json = await getResult.json(); + + if (getResult.status === 401) { + throw new Error(json.message); + } + + if (getResult.status === 403) { + throw new Error(json.message); + } + return json; + }; + + /* + Returns one or more entries with ordering and filters + @params table : string - name of table to fetch + @params options : object - optional parameters + options.join - Array or comma separated list of tables to join + options.filter - Array of filters + options.order - field used to sort the result + options.direction - direction of result asc|desc + options.size - max number of entries + Filter Options + cs contains string, sw starts with, ew ends with, eq equal, lt less than + le less or equal, ge greater or equal, gt greater than, bt between + is is null + NB: Add n to negate - ngt --> Nit greater than + let res = await (new TreeSDK).getList('author', { + filter: ['id,gt,2'], + join: ['book'] + }); + + */ + this.getList = async function (table, options = {}) { + if (empty(table)) throw new Error("table and id is required."); + let [hasJoin, joins, joinQuery] = getJoins(options); + let [hasFilter, filters, filterQuery] = getFilters(options); + let orderQuery = getOrdering(options); + let sizeQuery = options.hasOwnProperty("size") ? `size=${options.size}&` : ""; + const header = { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + uid: localStorage.getItem("device-uid"), + }; + + const getResult = await fetch(this.treeBaseUrl() + `/${table}?${joinQuery}${orderQuery}${sizeQuery}${filterQuery}`, { + method: "get", + headers: header, + }); + const json = await getResult.json(); + + if (getResult.status === 401) { + throw new Error(json.message); + } + + if (getResult.status === 403) { + throw new Error(json.message); + } + return json; + }; + + /* + Returns a paginated list of entries + @params table : string - name of table to fetch + @params options : object - optional parameters + options.join - Array or comma separated list of tables to join + options.filter - + options.order - field used to sort the result + options.direction - direction of result asc|desc + options.page - page number + options.size - max number of entries + + Filter Options + cs contains string, sw starts with, ew ends with, eq equal, lt less than + le less or equal, ge greater or equal, gt greater than, bt between + is is null + NB: Add n to negate - ngt --> Nit greater than + + let res = await (new TreeSDK).getPaginate('author', { + filter: ['id,gt,2'], + join: ['book'] + }); + + */ + this.getPaginate = async function (table, options = {}) { + if (empty(table)) throw new Error("table and id is required."); + + let [hasJoin, joins, joinQuery] = getJoins(options); + let [hasFilter, filters, filterQuery] = getFilters(options); + let orderQuery = getOrdering(options); + let size = options.size ?? 20; + let pageQuery = options.hasOwnProperty("page") ? `page=${options.page},${size}&` : `page=1&`; + const header = { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + uid: localStorage.getItem("device-uid"), + }; + const getResult = await fetch(this.treeBaseUrl() + `/${table}?${joinQuery}${orderQuery}${pageQuery}${filterQuery}`, { + method: "get", + headers: header, + }); + const json = await getResult.json(); + + if (getResult.status === 401) { + throw new Error(json.message); + } + + if (getResult.status === 403) { + throw new Error(json.message); + } + return json; + }; + + /* + Returns Creates a new entry + @params table : string - name of table to fetch + @params options : object - optional parameters + + + let res = await (new TreeSDK).create('author', { + name: 'author name', + age: 23 + }); + + + */ + this.create = async function (table, payload, options = {}) { + if (empty(table)) throw new Error("table and id is required."); + + const header = { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + const getResult = await fetch(this.treeBaseUrl() + `/${table}}`, { + method: "post", + headers: header, + body: JSON.stringify(payload), + }); + const json = await getResult.json(); + + if (getResult.status === 401) { + throw new Error(json.message); + } + + if (getResult.status === 403) { + throw new Error(json.message); + } + return json; + }; + + /* + Returns Updates an entry + @params table : string - name of table to update + @params id : number - id of table entry to update + @params payload : object - key value pair for values to update + + let res = await (new TreeSDK).update('author', 2 { + name: 'updated author name', + }); + + */ + this.update = async function (table, id, payload) { + if (empty(table) || empty(id)) throw new Error("table and id is required."); + + const header = { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + const getResult = await fetch(this.treeBaseUrl() + `/${table}/${id}`, { + method: "put", + headers: header, + body: JSON.stringify(payload), + }); + const json = await getResult.json(); + + if (getResult.status === 401) { + throw new Error(json.message); + } + + if (getResult.status === 403) { + throw new Error(json.message); + } + return json; + }; + + /* + Returns Delete an entry + @params table : string - name of table to delete from + @params id : number - id of table entry to delete + + let res = await (new TreeSDK).delete('author', 2); + */ + this.delete = async function (table, id, payload) { + if (empty(table) || empty(id)) throw new Error("table and id is required."); + + const header = { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + }; + const getResult = await fetch(this.treeBaseUrl() + `/${table}/${id}`, { + method: "delete", + headers: header, + body: JSON.stringify(payload), + }); + const json = await getResult.json(); + + if (getResult.status === 401) { + throw new Error(json.message); + } + + if (getResult.status === 403) { + throw new Error(json.message); + } + return json; + }; + + this.callRawAPI = async function (uri, payload, method, signal) { + const header = { + "Content-Type": "application/json", + "x-project": base64Encode, + Authorization: "Bearer " + localStorage.getItem("token"), + uid: localStorage.getItem("device-uid"), + }; + + const result = await fetch(this._baseurl + uri, { + method: method, + headers: header, + body: JSON.stringify(payload), + signal, + }); + const jsonResult = await result.json(); + + if (result.status === 401) { + throw new Error(jsonResult.message); + } + + if (result.status === 403) { + throw new Error(jsonResult.message); + } + return jsonResult; + }; + + return this; +} +/* + +cs contains string +sw starts with +ew ends with +eq + equal +Default when no operator is provided +lt less than +le less or equal +ge greater or equal +gt greater than +bt between +in in list +is is null + + +*/ diff --git a/src/utils/adminNavigationItems.jsx b/src/utils/adminNavigationItems.jsx new file mode 100644 index 0000000..ac69b81 --- /dev/null +++ b/src/utils/adminNavigationItems.jsx @@ -0,0 +1,295 @@ +import AddIcon from "@/components/frontend/icons/AddIcon"; +import PictureIcon from "@/components/frontend/icons/PictureIcon"; +import { + ArchiveBoxXMarkIcon, + BanknotesIcon, + BellAlertIcon, + BookOpenIcon, + BookmarkIcon, + BuildingLibraryIcon, + BuildingOffice2Icon, + BuildingOfficeIcon, + ChartBarIcon, + ClipboardDocumentCheckIcon, + ClipboardDocumentIcon, + ClipboardDocumentListIcon, + Cog8ToothIcon, + CreditCardIcon, + DeviceTabletIcon, + EnvelopeIcon, + HashtagIcon, + HomeIcon, + IdentificationIcon, + PhotoIcon, + PlusCircleIcon, + PuzzlePieceIcon, + QuestionMarkCircleIcon, + QueueListIcon, + SparklesIcon, + Square3Stack3DIcon, + Squares2X2Icon, + StarIcon, + TrashIcon, + UserCircleIcon, + UserGroupIcon, + UserIcon, +} from "@heroicons/react/24/outline"; +import { AdjustmentsHorizontalIcon, BookmarkSlashIcon } from "@heroicons/react/24/solid"; + +const adminNavigationItems = [ + { + title: "Dashboard", + path: "dashboard", + icon: , + }, + { + title: "Users", + path: "user", + icon: , + sub_categories: [ + { + title: "Hosts", + path: "host", + icon: , + }, + { + title: "Customers", + path: "customer", + icon: , + }, + ], + }, + { + title: "Devices", + path: "device", + icon: , + }, + { + title: "ID Verification", + path: "id_verification", + icon: , + }, + { + title: "Properties", + path: "property", + icon: , + sub_categories: [ + { + title: "Add-on", + path: "property_add_on", + icon: , + }, + ], + }, + { + title: "Categories", + path: "spaces", + icon: , + sub_categories: [ + { + title: "Space", + path: "spaces", + icon: , + }, + { + title: "Add-on", + path: "add_on", + icon: , + }, + { + title: "Amenity", + path: "amenity", + icon: , + }, + ], + }, + + { + title: "Property Spaces", + path: "property_spaces", + icon: , + sub_categories: [ + { + title: "Images", + path: "property_spaces_images", + icon: , + }, + { + title: "Amenities", + path: "property_spaces_amenitites", + icon: , + }, + { + title: "Faqs", + path: "property_spaces_faq", + icon: , + }, + ], + }, + + { + title: "Bookings", + path: "booking", + icon: , + sub_categories: [ + { + title: "Add-ons", + path: "booking_addons", + icon: , + }, + ], + }, + { + title: "Payout", + path: "payout", + icon: , + }, + { + title: "Payout methods", + path: "payout_method", + icon: , + }, + { + title: "Hashtags", + path: "hashtag", + icon: , + }, + { + title: "Review", + path: "review", + icon: , + }, + { + title: "FAQ", + path: "faq", + icon: , + }, + { + title: "Reports", + path: "reports", + icon: , + }, + { + title: "Email", + path: "email", + icon: , + }, + { + title: "Notifications", + path: "notification", + icon: , + }, + { + title: "CMS", + path: "privacy", + icon: , + sub_categories: [ + { + title: "Privacy policy", + path: "privacy", + icon: , + }, + { + id: 24, + title: "Terms & Conditions", + path: "terms_and_conditions", + icon: , + }, + { + id: 25, + title: "Cancellation Policy", + path: "cancellation_policy", + icon: , + }, + ], + }, + { + title: "Settings", + path: "settings", + icon: , + }, + { + title: "Profile", + path: "profile", + icon: , + }, + { + title: "Recycle bin", + path: "recycle_bin_users", + icon: , + sub_categories: [ + { + title: "Users", + path: "recycle_bin_users", + icon: , + }, + { + title: "Devices", + path: "recycle_bin_devices", + icon: , + }, + { + title: "Properties", + path: "recycle_bin_properties", + icon: , + }, + { + title: "Property Addons", + path: "recycle_bin_properties_addon", + icon: , + }, + { + title: "Bookings", + path: "recycle_bin_booking", + icon: , + }, + { + title: "Booking Addons", + path: "recycle_bin_booking_addon", + icon: , + }, + { + title: "Property Spaces", + path: "recycle_bin_properties_spaces", + icon: , + }, + { + title: "Space Images", + path: "recycle_bin_properties_space_images", + icon: , + }, + { + title: "Space Amenities", + path: "recycle_bin_properties_space_amenities", + icon: , + }, + { + title: "Spaces Faqs", + path: "recycle_bin_properties_space_faq", + icon: , + }, + { + title: "Spaces", + path: "recycle_bin_spaces", + icon: , + }, + { + title: "Faqs", + path: "recycle_bin_faqs", + icon: , + }, + { + title: "Hashtags", + path: "recycle_bin_hashtag", + icon: , + }, + { + title: "Payout", + path: "recycle_bin_payout", + icon: , + }, + ], + }, +]; + +export default adminNavigationItems; diff --git a/src/utils/adminPortalColumns.js b/src/utils/adminPortalColumns.js new file mode 100644 index 0000000..2f785c6 --- /dev/null +++ b/src/utils/adminPortalColumns.js @@ -0,0 +1,1003 @@ +import { DRAFT_STATUS, ID_PREFIX } from "./constants"; +import { parseJsonSafely } from "./utils"; + +// applySetting assumes setting & columns are the same length +export function applySetting(setting, columns) { + setting = parseJsonSafely(setting); + setting = setting.sort((a, b) => a.defaultOrder - b.defaultOrder); + return columns.filter((_, idx) => setting[idx]?.shouldShow).sort((a, b) => setting[a.defaultOrder].orderNumber - setting[b.defaultOrder].orderNumber); +} + +export const adminColumns = { + admin_user: [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + defaultOrder: 0, + idPrefix: ID_PREFIX.USER, + }, + { + header: "First Name", + accessor: "first_name", + isSorted: true, + isSortedDesc: true, + defaultOrder: 1, + }, + { + header: "Last Name", + accessor: "last_name", + isSorted: true, + isSortedDesc: true, + defaultOrder: 2, + }, + { + header: "Email", + accessor: "email", + isSorted: true, + isSortedDesc: true, + defaultOrder: 3, + }, + { + header: "DOB", + accessor: "dob", + defaultOrder: 4, + }, + { + header: "Role", + accessor: "role", + isSorted: true, + isSortedDesc: true, + defaultOrder: 5, + }, + { + header: "Preferences", + accessor: "settings", + isSorted: true, + isSortedDesc: true, + defaultOrder: 6, + }, + { + header: "Status", + accessor: "status", + statusMapping: ["Inactive", "Active", "Suspend"], + isSorted: true, + isSortedDesc: true, + defaultOrder: 7, + }, + { + header: "Profile", + accessor: "", + defaultOrder: 8, + }, + { + header: "Photo Status", + accessor: "is_photo_approved", + mapping: ["IN REVIEW", "APPROVED", "REJECTED"], + default: "No Photo", + defaultOrder: 9, + }, + ], + admin_host: [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.HOST, + defaultOrder: 0, + }, + { + header: "Name", + accessor: "first_name,last_name", + isSorted: true, + isSortedDesc: true, + defaultOrder: 1, + }, + { + header: "Email", + accessor: "email", + isSorted: true, + isSortedDesc: true, + defaultOrder: 2, + }, + { + header: "Status", + accessor: "status", + statusMapping: ["Inactive", "Active", "Suspend"], + isSorted: true, + isSortedDesc: true, + defaultOrder: 3, + }, + { + header: "Total Payout", + accessor: "total_payout", + isSorted: true, + isSortedDesc: true, + defaultOrder: 4, + }, + { + header: "Properties", + accessor: "num_properties", + isSorted: true, + isSortedDesc: true, + defaultOrder: 5, + }, + { + header: "Profile", + accessor: "", + defaultOrder: 6, + }, + ], + admin_customer: [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.CUSTOMER, + defaultOrder: 0, + }, + { + header: "First Name", + accessor: "first_name", + isSorted: true, + isSortedDesc: true, + defaultOrder: 1, + }, + { + header: "Last Name", + accessor: "last_name", + isSorted: true, + isSortedDesc: true, + defaultOrder: 2, + }, + { + header: "Email", + accessor: "email", + isSorted: true, + isSortedDesc: true, + defaultOrder: 3, + }, + { + header: "Verified", + accessor: "verify", + mapping: ["No", "Yes"], + isSorted: true, + isSortedDesc: true, + defaultOrder: 4, + }, + { + header: "Payment Set", + accessor: "payment_method_set", + mapping: ["No", "Yes"], + isSorted: true, + isSortedDesc: true, + defaultOrder: 5, + }, + { + header: "Status", + accessor: "status", + statusMapping: ["Inactive", "Active", "Suspend"], + isSorted: true, + isSortedDesc: true, + defaultOrder: 6, + }, + { + header: "Profile", + accessor: "", + defaultOrder: 7, + }, + ], + admin_id_verification: [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.ID_VERIFICATION, + defaultOrder: 0, + }, + { + header: "User ID", + accessor: "user_id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.USER, + defaultOrder: 1, + }, + { + header: "Email", + accessor: "email", + isSorted: true, + isSortedDesc: true, + defaultOrder: 2, + }, + { + header: "Date of Birth", + accessor: "dob", + isSorted: true, + isSortedDesc: true, + defaultOrder: 3, + }, + { + header: "ID Photo Front", + accessor: "image_front", + defaultOrder: 4, + }, + { + header: "ID Photo Back", + accessor: "image_back", + defaultOrder: 5, + }, + { + header: "Expiry Date", + accessor: "expiry_date", + isSorted: true, + isSortedDesc: true, + defaultOrder: 6, + }, + { + header: "ID Type", + accessor: "type", + isSorted: false, + isSortedDesc: false, + defaultOrder: 7, + }, + { + header: "Status", + accessor: "status", + isSorted: true, + isSortedDesc: true, + mapping: ["Pending", "Verified", "Declined"], + defaultOrder: 8, + }, + { + header: "Actions", + accessor: "", + defaultOrder: 9, + }, + ], + admin_space_categories: [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.SPACE_CATEGORY, + defaultOrder: 0, + }, + { + header: "Category", + accessor: "category", + isSorted: true, + isSortedDesc: true, + defaultOrder: 1, + }, + { + header: "Image", + accessor: "image", + isSorted: true, + isSortedDesc: true, + defaultOrder: 2, + }, + { + header: "Icon", + accessor: "icon", + isSorted: true, + isSortedDesc: true, + defaultOrder: 2, + }, + { + header: "Has Sizes", + accessor: "has_sizes", + mapping: ["NO", "YES"], + isSorted: true, + isSortedDesc: true, + defaultOrder: 3, + }, + { + header: "Actions", + accessor: "", + defaultOrder: 4, + }, + ], + admin_addon_categories: [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.ADDON_CATEGORY, + defaultOrder: 0, + }, + { + header: "Space Category", + nested: "spaces", + accessor: "category", + isSorted: true, + isSortedDesc: true, + defaultOrder: 1, + }, + { + header: "Name", + accessor: "name", + isSorted: true, + isSortedDesc: true, + defaultOrder: 2, + }, + { + header: "Cost ($)", + accessor: "cost", + isSorted: true, + isSortedDesc: true, + defaultOrder: 3, + }, + { + header: "Actions", + accessor: "", + defaultOrder: 4, + }, + ], + host_addon_categories: [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.ADDON_CATEGORY, + defaultOrder: 0, + }, + { + header: "Name", + accessor: "name", + isSorted: true, + isSortedDesc: true, + defaultOrder: 1, + }, + { + header: "Cost ($)", + accessor: "cost", + isSorted: true, + isSortedDesc: true, + defaultOrder: 2, + }, + { + header: "Actions", + accessor: "", + defaultOrder: 3, + }, + ], + admin_amenity_categories: [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.AMENITY_CATEGORY, + defaultOrder: 0, + }, + { + header: "Space Category", + nested: "spaces", + accessor: "category", + isSorted: true, + isSortedDesc: true, + defaultOrder: 1, + }, + { + header: "Name", + accessor: "name", + isSorted: true, + isSortedDesc: true, + defaultOrder: 2, + }, + { + header: "Actions", + accessor: "", + defaultOrder: 3, + }, + ], + host_amenity_categories: [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.AMENITY_CATEGORY, + defaultOrder: 0, + }, + { + header: "Name", + accessor: "name", + isSorted: true, + isSortedDesc: true, + defaultOrder: 1, + }, + { + header: "Actions", + accessor: "", + defaultOrder: 2, + }, + ], + admin_property: [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.PROPERTY, + defaultOrder: 0, + }, + { + header: "Property Name", + accessor: "name", + isSorted: true, + isSortedDesc: true, + defaultOrder: 1, + }, + { + header: "Host's Email", + accessor: "email", + isSorted: true, + isSortedDesc: true, + defaultOrder: 2, + }, + { + header: "Address", + accessor: "address_line_1, address_line_2", + multiline: true, + defaultOrder: 3, + }, + { + header: "City", + accessor: "city", + isSorted: true, + isSortedDesc: true, + defaultOrder: 4, + }, + { + header: "Zip Code", + accessor: "zip", + isSorted: true, + isSortedDesc: true, + defaultOrder: 5, + }, + { + header: "Country", + accessor: "country", + isSorted: true, + isSortedDesc: true, + defaultOrder: 6, + }, + { + header: "Spaces", + accessor: "spaces", + isSorted: true, + isSortedDesc: true, + defaultOrder: 7, + }, + { + header: "Actions", + accessor: "", + defaultOrder: 8, + }, + ], + admin_property_space: [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.PROPERTY_SPACE, + defaultOrder: 0, + }, + { + header: "Property", + accessor: "property_name", + isSorted: true, + isSortedDesc: true, + defaultOrder: 1, + }, + { + header: "Space", + accessor: "space_category", + isSorted: true, + isSortedDesc: true, + defaultOrder: 2, + }, + { + header: "Max Capacity", + accessor: "max_capacity", + isSorted: true, + isSortedDesc: true, + defaultOrder: 3, + }, + { + header: "Rate ( Hourly )", + accessor: "rate", + isSorted: true, + isSortedDesc: true, + defaultOrder: 4, + }, + { + header: "Additional guest rate ( hourly )", + accessor: "additional_guest_rate", + isSorted: true, + isSortedDesc: true, + defaultOrder: 5, + }, + { + header: "Date Updated", + accessor: "update_at", + isSorted: true, + isSortedDesc: true, + formatDate: true, + defaultOrder: 6, + }, + { + header: "Is Draft", + accessor: "draft_status", + isSorted: true, + isSortedDesc: true, + format: (raw) => (raw === DRAFT_STATUS.COMPLETED ? "NO" : "YES"), + defaultOrder: 7, + }, + { + header: "Status", + accessor: "space_status", + isSorted: true, + isSortedDesc: true, + mapping: ["UNDER REVIEW", "APPROVED", "DECLINED"], + defaultOrder: 8, + }, + { + header: "Visibility", + accessor: "availability", + isSorted: true, + isSortedDesc: true, + mapping: ["HIDDEN", "VISIBLE"], + defaultOrder: 9, + }, + { + header: "Size", + accessor: "size", + isSorted: true, + isSortedDesc: true, + mapping: ["Small", "Medium", "Large", "X-Large"], + defaultOrder: 10, + }, + { + header: "Actions", + accessor: "", + viewProperty: true, + defaultOrder: 11, + }, + ], + admin_property_space_images: [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.PROPERTY_SPACE_IMAGES, + defaultOrder: 0, + }, + { + header: "Property", + accessor: "property_name", + isSorted: true, + isSortedDesc: true, + defaultOrder: 1, + }, + { + header: "Property Space", + accessor: "space_category", + isSorted: true, + isSortedDesc: true, + defaultOrder: 2, + }, + { + header: "Host Email", + accessor: "email", + isSorted: true, + isSortedDesc: true, + defaultOrder: 3, + }, + { + header: "Image", + accessor: "photo_url", + defaultOrder: 4, + }, + { + header: "Actions", + accessor: "", + defaultOrder: 5, + }, + { + header: "Status", + accessor: "is_approved", + defaultOrder: 6, + mapping: ["IN REVIEW", "APPROVED", "REJECTED"], + }, + ], + admin_property_space_amenities: [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.PROPERTY_SPACE_AMENITIES, + defaultOrder: 0, + }, + { + header: "Property Name", + accessor: "property_name", + isSorted: true, + isSortedDesc: true, + defaultOrder: 1, + }, + { + header: "Property Space", + accessor: "space_category", + isSorted: true, + isSortedDesc: true, + defaultOrder: 2, + }, + { + header: "Amenity", + accessor: "amenity_name", + isSorted: true, + isSortedDesc: true, + defaultOrder: 3, + }, + { + header: "Actions", + accessor: "", + defaultOrder: 4, + }, + ], + admin_property_space_faqs: [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.PROPERTY_SPACE_FAQS, + defaultOrder: 0, + }, + { + header: "Property Space ID", + accessor: "property_space_id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.PROPERTY_SPACE, + defaultOrder: 1, + }, + { + header: "Questions", + accessor: "question", + isSorted: true, + isSortedDesc: true, + defaultOrder: 2, + }, + { + header: "Actions", + accessor: "", + defaultOrder: 3, + }, + ], + admin_booking_addons: [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.BOOKING_ADDON, + defaultOrder: 0, + }, + { + header: "Booking Id", + accessor: "booking_id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.BOOKINGS, + defaultOrder: 1, + }, + { + header: "Property Add-on", + accessor: "add_on_name", + isSorted: true, + isSortedDesc: true, + defaultOrder: 2, + }, + { + header: "Actions", + accessor: "", + defaultOrder: 3, + }, + ], + admin_hashtag: [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.HASHTAGS, + defaultOrder: 0, + }, + { + header: "Name", + accessor: "name", + isSorted: true, + isSortedDesc: true, + defaultOrder: 1, + }, + { + header: "Actions", + accessor: "", + defaultOrder: 2, + }, + ], + admin_host_reviews: [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.REVIEWS, + defaultOrder: 0, + }, + { + header: "Host", + accessor: "host_first_name , host_last_name", + type: "single", + isSorted: true, + isSortedDesc: true, + defaultOrder: 1, + }, + { + header: "Space", + accessor: "name,category", + multiline: true, + isSorted: true, + isSortedDesc: true, + defaultOrder: 2, + }, + { + header: "Post Date", + accessor: "create_at", + defaultOrder: 3, + }, + { + header: "Rating", + accessor: "rating", + isSorted: true, + isSortedDesc: true, + defaultOrder: 4, + }, + { + header: "Type", + accessor: "type", + isSorted: true, + isSortedDesc: true, + defaultOrder: 5, + }, + { + header: "Status", + accessor: "status", + mapping: ["Under Review", "Posted", "Declined"], + defaultOrder: 6, + }, + { + header: "Action", + accessor: "", + defaultOrder: 7, + }, + ], + admin_customer_reviews: [ + { + header: "ID", + accessor: "id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.REVIEWS, + defaultOrder: 0, + }, + { + header: "Customer", + accessor: "customer_first_name , customer_last_name", + type: "single", + isSorted: true, + isSortedDesc: true, + defaultOrder: 1, + }, + { + header: "Space", + accessor: "name,category", + multiline: true, + isSorted: true, + isSortedDesc: true, + defaultOrder: 2, + }, + { + header: "Post Date", + accessor: "create_at", + defaultOrder: 3, + }, + { + header: "Rating", + accessor: "rating", + isSorted: true, + isSortedDesc: true, + defaultOrder: 4, + }, + { + header: "Type", + accessor: "type", + isSorted: true, + isSortedDesc: true, + defaultOrder: 5, + }, + { + header: "Status", + accessor: "status", + mapping: ["Under Review", "Posted", "Declined"], + defaultOrder: 6, + }, + { + header: "Action", + accessor: "", + defaultOrder: 7, + }, + ], + admin_booking_report: [ + { + header: "Booking ID", + accessor: "id", + idPrefix: ID_PREFIX.BOOKINGS, + defaultOrder: 0, + }, + { + header: "Host ID", + accessor: "host_id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.HOST, + defaultOrder: 1, + }, + { + header: "Customer ID", + accessor: "customer_id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.CUSTOMER, + defaultOrder: 2, + }, + { + header: "Property ID", + accessor: "property_id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.PROPERTY, + defaultOrder: 3, + }, + { + header: "Property Space ID", + accessor: "property_space_id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.PROPERTY_SPACE, + defaultOrder: 4, + }, + { + header: "Addon Cost", + accessor: "addon_cost", + isSorted: true, + isSortedDesc: true, + defaultOrder: 5, + }, + { + header: "Property name", + accessor: "property_name", + isSorted: true, + isSortedDesc: true, + defaultOrder: 6, + }, + { + header: "Space Category", + accessor: "space_category", + isSorted: true, + isSortedDesc: true, + defaultOrder: 7, + }, + { + header: "Hourly rate", + accessor: "rate", + isSorted: true, + isSortedDesc: true, + defaultOrder: 8, + }, + { + header: "Space Address", + accessor: "address_line_1, address_line_2", + joinFields: true, + isSorted: true, + isSortedDesc: true, + defaultOrder: 9, + }, + { + header: "Customer name", + accessor: "customer_first_name, customer_last_name", + joinFields: true, + isSorted: true, + isSortedDesc: true, + defaultOrder: 10, + }, + { + header: "Host name", + accessor: "host_first_name, host_last_name", + joinFields: true, + isSorted: true, + isSortedDesc: true, + defaultOrder: 11, + }, + { + header: "Booking date", + accessor: "booking_start_time", + isSorted: true, + isSortedDesc: true, + formatDate: true, + defaultOrder: 12, + }, + { + header: "Booking status", + accessor: "status", + isSorted: true, + isSortedDesc: true, + mapping: ["Pending", "Upcoming", "Ongoing", "Completed", "Declined", "Cancelled"], + defaultOrder: 13, + }, + ], + admin_analytics: [ + { + header: "User ID", + accessor: "user_id", + isSorted: true, + isSortedDesc: true, + idPrefix: ID_PREFIX.CUSTOMER, + defaultOrder: 0, + }, + { + header: "Url", + accessor: "path", + isSorted: true, + isSortedDesc: true, + defaultOrder: 1, + }, + { + header: "Country", + accessor: "country", + isSorted: true, + isSortedDesc: true, + isCountry: true, + defaultOrder: 2, + }, + { + header: "Role", + accessor: "role", + isSorted: true, + isSortedDesc: true, + defaultOrder: 3, + }, + { + header: "Date", + accessor: "create_at", + isSorted: true, + isSortedDesc: true, + formatDate: true, + defaultOrder: 4, + }, + ], +}; diff --git a/src/utils/callCustomAPI.jsx b/src/utils/callCustomAPI.jsx new file mode 100644 index 0000000..d170c85 --- /dev/null +++ b/src/utils/callCustomAPI.jsx @@ -0,0 +1,38 @@ +import axios from "axios"; + +export async function callCustomAPI(endpoint, method, payload, action = "NONE", token, version) { + try { + const result = await axios({ + method, + url: `https://ergo.mkdlabs.com/${version ?? "v2"}/api/custom/ergo/${endpoint}/${action}`, + data: payload, + headers: { + "X-Project": "ZXJnbzprNWdvNGw1NDhjaDRxazU5MTh4MnVsanV2OHJxcXAyYXM=", + Authorization: `Bearer ${token ?? localStorage.getItem("token")}`, + uid: localStorage.getItem("device-uid"), + }, + }); + if (result.data?.error) { + throw new Error(result.data.error || "An Error Occurred"); + } else { + switch (action) { + case "PAGINATE": + return result.data; + default: + return result.data; + } + } + } catch (err) { + console.log(`CUSTOM ERROR(${endpoint}): `, err); + if (err.response?.data?.message === "TOKEN_EXPIRED") { + localStorage.clear(); + location.href = "/login"; + } + // if (err.code == "ERR_NETWORK") throw new Error("Please make sure you have an active internet connection"); + throw new Error(err.response?.data?.message || "An Error Occurred"); + } +} + +export async function oauthLoginApi(type, role) { + return axios.get(`https://ergo.mkdlabs.com/v2/api/lambda/${type}/login?role=${role}`, { headers: { "x-project": "ZXJnbzprNWdvNGw1NDhjaDRxazU5MTh4MnVsanV2OHJxcXAyYXM" } }); +} diff --git a/src/utils/constants.js b/src/utils/constants.js new file mode 100644 index 0000000..da6f491 --- /dev/null +++ b/src/utils/constants.js @@ -0,0 +1,102 @@ +export const DRAFT_STATUS = { + PROPERTY_SPACE: 0, + IMAGES: 1, + SCHEDULING: 2, + COMPLETED: 3, + UNSET: null, +}; + +export const ID_PREFIX = { + USER: "U-", + HOST: "H-", + CUSTOMER: "C-", + PROPERTY: "P-", + PROPERTY_SPACE: "PS-", + SPACE_CATEGORY: "SC-", + ADDON_CATEGORY: "AOC-", + AMENITY_CATEGORY: "AMC-", + PROPERTY_SPACE_IMAGES: "PSI-", + PROPERTY_SPACE_AMENITIES: "PSA-", + PROPERTY_SPACE_FAQS: "PSF-", + PROPERTY_ADDON: "PA-", + BOOKINGS: "BK-", + BOOKING_ADDON: "BKA-", + PAYOUT: "PY-", + HASHTAGS: "H-", + REVIEWS: "R-", + FAQS: "F-", + EMAIL: "E-", + SETTING: "S-", + ID_VERIFICATION: "IDV-", + NOTIFICATION: "N-", + DEVICE: "D-", + PAYMENT_METHOD: "PM-", +}; + +export const BOOKING_STATUS = { + PENDING: 0, + UPCOMING: 1, + ONGOING: 2, + COMPLETED: 3, + DECLINED: 4, + CANCELLED: 5, + DELETED: 6, +}; + +export const PAYMENT_STATUS = { + NOT_PAID: 0, + SUCCESSFUL: 1, + PENDING: 2, + FAILED: 3 +} + +export const SPACE_STATUS = { + UNDER_REVIEW: 0, + APPROVED: 1, + DECLINED: 2, +}; + +export const SPACE_VISIBILITY = { + HIDDEN: 0, + VISIBLE: 1, +}; + +export const ARCHIVE_STATUS = { + NOT_ARCHIVE: 0, + IS_ARCHIVE: 1, +}; + +export const SPACE_CATEGORY_SIZES = { + SMALL: 0, + MEDIUM: 1, + LARGE: 2, + X_LARGE: 3, + UNSET: null, +}; + +export const IMAGE_STATUS = { + IN_REVIEW: 0, + APPROVED: 1, + NOT_APPROVED: 2, +}; + +export const NOTIFICATION_STATUS = { + NOT_ADDRESSED: 0, + ADDRESSED: 1, +}; + +export const NOTIFICATION_TYPE = { + CREATE_SPACE: 0, + CREATE_PROPERTY_SPACE_IMAGE: 1, + EDIT_USER_PICTURE: 2, + EDIT_PROPERTY_SPACE: 3, + ADD_REVIEW: 4, + ADD_PAYOUT: 5, + NEW_ID_VERIFICATION: 6, +}; + +export const ID_VERIFICATION_STATUSES = { + PENDING: 0, + VERIFIED: 1, + REJECTED: 2, +}; diff --git a/src/utils/countries.json b/src/utils/countries.json new file mode 100644 index 0000000..d3d4d2c --- /dev/null +++ b/src/utils/countries.json @@ -0,0 +1,245 @@ +[ + {"name": "Afghanistan", "code": "AF"}, + {"name": "Ă…land Islands", "code": "AX"}, + {"name": "Albania", "code": "AL"}, + {"name": "Algeria", "code": "DZ"}, + {"name": "American Samoa", "code": "AS"}, + {"name": "AndorrA", "code": "AD"}, + {"name": "Angola", "code": "AO"}, + {"name": "Anguilla", "code": "AI"}, + {"name": "Antarctica", "code": "AQ"}, + {"name": "Antigua and Barbuda", "code": "AG"}, + {"name": "Argentina", "code": "AR"}, + {"name": "Armenia", "code": "AM"}, + {"name": "Aruba", "code": "AW"}, + {"name": "Australia", "code": "AU"}, + {"name": "Austria", "code": "AT"}, + {"name": "Azerbaijan", "code": "AZ"}, + {"name": "Bahamas", "code": "BS"}, + {"name": "Bahrain", "code": "BH"}, + {"name": "Bangladesh", "code": "BD"}, + {"name": "Barbados", "code": "BB"}, + {"name": "Belarus", "code": "BY"}, + {"name": "Belgium", "code": "BE"}, + {"name": "Belize", "code": "BZ"}, + {"name": "Benin", "code": "BJ"}, + {"name": "Bermuda", "code": "BM"}, + {"name": "Bhutan", "code": "BT"}, + {"name": "Bolivia", "code": "BO"}, + {"name": "Bosnia and Herzegovina", "code": "BA"}, + {"name": "Botswana", "code": "BW"}, + {"name": "Bouvet Island", "code": "BV"}, + {"name": "Brazil", "code": "BR"}, + {"name": "British Indian Ocean Territory", "code": "IO"}, + {"name": "Brunei Darussalam", "code": "BN"}, + {"name": "Bulgaria", "code": "BG"}, + {"name": "Burkina Faso", "code": "BF"}, + {"name": "Burundi", "code": "BI"}, + {"name": "Cambodia", "code": "KH"}, + {"name": "Cameroon", "code": "CM"}, + {"name": "Canada", "code": "CA"}, + {"name": "Cape Verde", "code": "CV"}, + {"name": "Cayman Islands", "code": "KY"}, + {"name": "Central African Republic", "code": "CF"}, + {"name": "Chad", "code": "TD"}, + {"name": "Chile", "code": "CL"}, + {"name": "China", "code": "CN"}, + {"name": "Christmas Island", "code": "CX"}, + {"name": "Cocos (Keeling) Islands", "code": "CC"}, + {"name": "Colombia", "code": "CO"}, + {"name": "Comoros", "code": "KM"}, + {"name": "Congo", "code": "CG"}, + {"name": "Congo, The Democratic Republic of the", "code": "CD"}, + {"name": "Cook Islands", "code": "CK"}, + {"name": "Costa Rica", "code": "CR"}, + {"name": "Cote D\"Ivoire", "code": "CI"}, + {"name": "Croatia", "code": "HR"}, + {"name": "Cuba", "code": "CU"}, + {"name": "Cyprus", "code": "CY"}, + {"name": "Czech Republic", "code": "CZ"}, + {"name": "Denmark", "code": "DK"}, + {"name": "Djibouti", "code": "DJ"}, + {"name": "Dominica", "code": "DM"}, + {"name": "Dominican Republic", "code": "DO"}, + {"name": "Ecuador", "code": "EC"}, + {"name": "Egypt", "code": "EG"}, + {"name": "El Salvador", "code": "SV"}, + {"name": "Equatorial Guinea", "code": "GQ"}, + {"name": "Eritrea", "code": "ER"}, + {"name": "Estonia", "code": "EE"}, + {"name": "Ethiopia", "code": "ET"}, + {"name": "Falkland Islands (Malvinas)", "code": "FK"}, + {"name": "Faroe Islands", "code": "FO"}, + {"name": "Fiji", "code": "FJ"}, + {"name": "Finland", "code": "FI"}, + {"name": "France", "code": "FR"}, + {"name": "French Guiana", "code": "GF"}, + {"name": "French Polynesia", "code": "PF"}, + {"name": "French Southern Territories", "code": "TF"}, + {"name": "Gabon", "code": "GA"}, + {"name": "Gambia", "code": "GM"}, + {"name": "Georgia", "code": "GE"}, + {"name": "Germany", "code": "DE"}, + {"name": "Ghana", "code": "GH"}, + {"name": "Gibraltar", "code": "GI"}, + {"name": "Greece", "code": "GR"}, + {"name": "Greenland", "code": "GL"}, + {"name": "Grenada", "code": "GD"}, + {"name": "Guadeloupe", "code": "GP"}, + {"name": "Guam", "code": "GU"}, + {"name": "Guatemala", "code": "GT"}, + {"name": "Guernsey", "code": "GG"}, + {"name": "Guinea", "code": "GN"}, + {"name": "Guinea-Bissau", "code": "GW"}, + {"name": "Guyana", "code": "GY"}, + {"name": "Haiti", "code": "HT"}, + {"name": "Heard Island and Mcdonald Islands", "code": "HM"}, + {"name": "Holy See (Vatican City State)", "code": "VA"}, + {"name": "Honduras", "code": "HN"}, + {"name": "Hong Kong", "code": "HK"}, + {"name": "Hungary", "code": "HU"}, + {"name": "Iceland", "code": "IS"}, + {"name": "India", "code": "IN"}, + {"name": "Indonesia", "code": "ID"}, + {"name": "Iran, Islamic Republic Of", "code": "IR"}, + {"name": "Iraq", "code": "IQ"}, + {"name": "Ireland", "code": "IE"}, + {"name": "Isle of Man", "code": "IM"}, + {"name": "Israel", "code": "IL"}, + {"name": "Italy", "code": "IT"}, + {"name": "Jamaica", "code": "JM"}, + {"name": "Japan", "code": "JP"}, + {"name": "Jersey", "code": "JE"}, + {"name": "Jordan", "code": "JO"}, + {"name": "Kazakhstan", "code": "KZ"}, + {"name": "Kenya", "code": "KE"}, + {"name": "Kiribati", "code": "KI"}, + {"name": "Korea, Democratic People\"S Republic of", "code": "KP"}, + {"name": "Korea, Republic of", "code": "KR"}, + {"name": "Kuwait", "code": "KW"}, + {"name": "Kyrgyzstan", "code": "KG"}, + {"name": "Lao People\"S Democratic Republic", "code": "LA"}, + {"name": "Latvia", "code": "LV"}, + {"name": "Lebanon", "code": "LB"}, + {"name": "Lesotho", "code": "LS"}, + {"name": "Liberia", "code": "LR"}, + {"name": "Libyan Arab Jamahiriya", "code": "LY"}, + {"name": "Liechtenstein", "code": "LI"}, + {"name": "Lithuania", "code": "LT"}, + {"name": "Luxembourg", "code": "LU"}, + {"name": "Macao", "code": "MO"}, + {"name": "Macedonia, The Former Yugoslav Republic of", "code": "MK"}, + {"name": "Madagascar", "code": "MG"}, + {"name": "Malawi", "code": "MW"}, + {"name": "Malaysia", "code": "MY"}, + {"name": "Maldives", "code": "MV"}, + {"name": "Mali", "code": "ML"}, + {"name": "Malta", "code": "MT"}, + {"name": "Marshall Islands", "code": "MH"}, + {"name": "Martinique", "code": "MQ"}, + {"name": "Mauritania", "code": "MR"}, + {"name": "Mauritius", "code": "MU"}, + {"name": "Mayotte", "code": "YT"}, + {"name": "Mexico", "code": "MX"}, + {"name": "Micronesia, Federated States of", "code": "FM"}, + {"name": "Moldova, Republic of", "code": "MD"}, + {"name": "Monaco", "code": "MC"}, + {"name": "Mongolia", "code": "MN"}, + {"name": "Montserrat", "code": "MS"}, + {"name": "Morocco", "code": "MA"}, + {"name": "Mozambique", "code": "MZ"}, + {"name": "Myanmar", "code": "MM"}, + {"name": "Namibia", "code": "NA"}, + {"name": "Nauru", "code": "NR"}, + {"name": "Nepal", "code": "NP"}, + {"name": "Netherlands", "code": "NL"}, + {"name": "Netherlands Antilles", "code": "AN"}, + {"name": "New Caledonia", "code": "NC"}, + {"name": "New Zealand", "code": "NZ"}, + {"name": "Nicaragua", "code": "NI"}, + {"name": "Niger", "code": "NE"}, + {"name": "Nigeria", "code": "NG"}, + {"name": "Niue", "code": "NU"}, + {"name": "Norfolk Island", "code": "NF"}, + {"name": "Northern Mariana Islands", "code": "MP"}, + {"name": "Norway", "code": "NO"}, + {"name": "Oman", "code": "OM"}, + {"name": "Pakistan", "code": "PK"}, + {"name": "Palau", "code": "PW"}, + {"name": "Palestinian Territory, Occupied", "code": "PS"}, + {"name": "Panama", "code": "PA"}, + {"name": "Papua New Guinea", "code": "PG"}, + {"name": "Paraguay", "code": "PY"}, + {"name": "Peru", "code": "PE"}, + {"name": "Philippines", "code": "PH"}, + {"name": "Pitcairn", "code": "PN"}, + {"name": "Poland", "code": "PL"}, + {"name": "Portugal", "code": "PT"}, + {"name": "Puerto Rico", "code": "PR"}, + {"name": "Qatar", "code": "QA"}, + {"name": "Reunion", "code": "RE"}, + {"name": "Romania", "code": "RO"}, + {"name": "Russian Federation", "code": "RU"}, + {"name": "RWANDA", "code": "RW"}, + {"name": "Saint Helena", "code": "SH"}, + {"name": "Saint Kitts and Nevis", "code": "KN"}, + {"name": "Saint Lucia", "code": "LC"}, + {"name": "Saint Pierre and Miquelon", "code": "PM"}, + {"name": "Saint Vincent and the Grenadines", "code": "VC"}, + {"name": "Samoa", "code": "WS"}, + {"name": "San Marino", "code": "SM"}, + {"name": "Sao Tome and Principe", "code": "ST"}, + {"name": "Saudi Arabia", "code": "SA"}, + {"name": "Senegal", "code": "SN"}, + {"name": "Serbia and Montenegro", "code": "CS"}, + {"name": "Seychelles", "code": "SC"}, + {"name": "Sierra Leone", "code": "SL"}, + {"name": "Singapore", "code": "SG"}, + {"name": "Slovakia", "code": "SK"}, + {"name": "Slovenia", "code": "SI"}, + {"name": "Solomon Islands", "code": "SB"}, + {"name": "Somalia", "code": "SO"}, + {"name": "South Africa", "code": "ZA"}, + {"name": "South Georgia and the South Sandwich Islands", "code": "GS"}, + {"name": "Spain", "code": "ES"}, + {"name": "Sri Lanka", "code": "LK"}, + {"name": "Sudan", "code": "SD"}, + {"name": "Suriname", "code": "SR"}, + {"name": "Svalbard and Jan Mayen", "code": "SJ"}, + {"name": "Swaziland", "code": "SZ"}, + {"name": "Sweden", "code": "SE"}, + {"name": "Switzerland", "code": "CH"}, + {"name": "Syrian Arab Republic", "code": "SY"}, + {"name": "Taiwan", "code": "TW"}, + {"name": "Tajikistan", "code": "TJ"}, + {"name": "Tanzania, United Republic of", "code": "TZ"}, + {"name": "Thailand", "code": "TH"}, + {"name": "Timor-Leste", "code": "TL"}, + {"name": "Togo", "code": "TG"}, + {"name": "Tokelau", "code": "TK"}, + {"name": "Tonga", "code": "TO"}, + {"name": "Trinidad and Tobago", "code": "TT"}, + {"name": "Tunisia", "code": "TN"}, + {"name": "Turkey", "code": "TR"}, + {"name": "Turkmenistan", "code": "TM"}, + {"name": "Turks and Caicos Islands", "code": "TC"}, + {"name": "Tuvalu", "code": "TV"}, + {"name": "Uganda", "code": "UG"}, + {"name": "Ukraine", "code": "UA"}, + {"name": "United Arab Emirates", "code": "AE"}, + {"name": "United Kingdom", "code": "GB"}, + {"name": "United States", "code": "US"}, + {"name": "United States Minor Outlying Islands", "code": "UM"}, + {"name": "Uruguay", "code": "UY"}, + {"name": "Uzbekistan", "code": "UZ"}, + {"name": "Vanuatu", "code": "VU"}, + {"name": "Venezuela", "code": "VE"}, + {"name": "Viet Nam", "code": "VN"}, + {"name": "Virgin Islands, British", "code": "VG"}, + {"name": "Virgin Islands, U.S.", "code": "VI"}, + {"name": "Wallis and Futuna", "code": "WF"}, + {"name": "Western Sahara", "code": "EH"}, + {"name": "Yemen", "code": "YE"}, + {"name": "Zambia", "code": "ZM"}, + {"name": "Zimbabwe", "code": "ZW"} +] \ No newline at end of file diff --git a/src/utils/date-time-utils.js b/src/utils/date-time-utils.js new file mode 100644 index 0000000..f9d7b8c --- /dev/null +++ b/src/utils/date-time-utils.js @@ -0,0 +1,114 @@ +import moment from "moment"; + +function addHours(numOfHours, date) { + let dateToMilliseconds = date.getTime(); + let addedHours = dateToMilliseconds + 60 * 60 * 1000 * numOfHours; + return new Date(addedHours); +} + +function secondsToHour(d) { + d = Number(d); + let h = Math.floor(d / 3600); + + let hDisplay = h > 0 ? h + (h == 1 ? " hour" : " hours") : ""; + return hDisplay; +} + +// formatAMPM takes date: String | Date and returns time in AM/PM format e.g 12:00am +function formatAMPM(date) { + date = new Date(date); + if (isNaN(date)) return ""; + + var hours = date.getHours(); + var minutes = date.getMinutes(); + var am_pm = hours >= 12 ? "pm" : "am"; + hours = hours % 12; + hours = hours ? hours : 12; // the hour '0' should be '12' + minutes = minutes < 10 ? "0" + minutes : minutes; + var strTime = (hours < 10 ? "0" + hours : hours) + ":" + minutes + " " + am_pm; + return strTime; +} + +// getDuration can take as arguments (am/pm, date, valid date string) and returns hrs in Number +function getDuration(start, end) { + var start_date = new Date(start); + var end_date = new Date(end); + + if (!isNaN(start_date) && !isNaN(end_date)) { + return (end_date - start_date) / 3600000; + } + + start_date = new Date(`01/01/2001 ${start}`); + end_date = new Date(`01/01/2001 ${end}`); + + if (!isNaN(start_date) && !isNaN(end_date)) { + return (end_date - start_date) / 3600000; + } + return 0; +} + +const fullDaysMapping = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; +const monthsMapping = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; +const fullMonthsMapping = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; +const daysMapping = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"]; +const hourlySlots = [ + "12:00 am", + "01:00 am", + "02:00 am", + "03:00 am", + "04:00 am", + "05:00 am", + "06:00 am", + "07:00 am", + "08:00 am", + "09:00 am", + "10:00 am", + "11:00 am", + "12:00 pm", + "01:00 pm", + "02:00 pm", + "03:00 pm", + "04:00 pm", + "05:00 pm", + "06:00 pm", + "07:00 pm", + "08:00 pm", + "09:00 pm", + "10:00 pm", + "11:00 pm", +]; + +function formatDate(date) { + if (date == null) return ""; + date = new Date(date); + if (isNaN(date)) return ""; + return monthsMapping[date.getMonth()] + " " + date.getDate() + "/" + date.getFullYear(); +} + +function isSameDay(date1, date2) { + return moment(date1).format("MM/DD/YY") == moment(date2).format("MM/DD/YY"); +} + +function formatDiff(target) { + // if diff > 60seconds return min + const seconds = moment.duration(moment(target).diff(moment())).asSeconds(); + const minutes = moment.duration(moment(target).diff(moment())).asMinutes(); + const hours = moment.duration(moment(target).diff(moment())).asHours(); + const days = moment.duration(moment(target).diff(moment())).asDays(); + + if (days > 30) { + return { timeLeft: Math.floor(moment.duration(moment(target).diff(moment())).asMonths()), format: "mon" }; + } + if (hours > 24) { + return { timeLeft: Math.floor(days), format: Math.floor(days) > 1 ? "days" : "day" }; + } + if (minutes > 60) { + return { timeLeft: Math.floor(hours), format: Math.floor(hours) > 1 ? "hrs" : "hr" }; + } + if (seconds > 60) { + return { timeLeft: Math.floor(minutes), format: Math.floor(minutes) > 1 ? "mins" : "min" }; + } + return {}; +} + +export { formatAMPM, getDuration, formatDate, isSameDay, secondsToHour, formatDiff, fullDaysMapping, fullMonthsMapping, daysMapping, monthsMapping, hourlySlots }; diff --git a/src/utils/utils.jsx b/src/utils/utils.jsx new file mode 100644 index 0000000..f036356 --- /dev/null +++ b/src/utils/utils.jsx @@ -0,0 +1,229 @@ +let timerId; +import DOMPurify from "dompurify"; +import sanitizeHtml from 'sanitize-html'; + +export function classNames(...classes) { + return classes.filter(Boolean).join(" "); +} + +export const getNonNullValue = (value) => { + if (value != "") { + return value; + } else { + return undefined; + } +}; + +export function filterEmptyFields(object) { + Object.keys(object).forEach((key) => { + if (empty(object[key])) { + delete object[key]; + } + }); + return object; +} + +export function empty(value) { + return value === "" || value === null || value === undefined || value === "undefined"; +} + +export const debounce = (fn, delay = 500) => { + if (timerId) { + return; + } + timerId = setTimeout(() => { + fn(); + timerId = undefined; + }, delay); +}; + +export const addHours = (numOfHours, date) => { + let dateToMilliseconds = date.getTime(); + let addedHours = dateToMilliseconds + 60 * 60 * 1000 * numOfHours; + return new Date(addedHours); +}; + +export const secondsToHour = (d) => { + d = Number(d); + let h = Math.floor(d / 3600); + + let hDisplay = h > 0 ? h + (h == 1 ? " hr" : " hrs") : ""; + return hDisplay; +}; + +export function parseSearchParams(params) { + let obj = {}; + [...params].forEach(([k, v]) => { + if (typeof v === "string") { + obj[k] = v || undefined; + } else { + obj[k] = v; + } + }); + return obj; +} + +export function clearSearchParams(params, setParams) { + [...params].forEach(([k, v]) => { + params.delete(k); + }); + setParams(params); +} + +export function isNotInViewport(elementId) { + const element = document.getElementById(elementId); + if (!element) return true; + const rect = element.getBoundingClientRect(); + return !( + rect.top >= 0 && + rect.left >= 0 && + rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && + rect.right <= (window.innerWidth || document.documentElement.clientWidth) + ); +} + +export function parseJsonSafely(json, failReturn) { + if (typeof json === "object" || Array.isArray(json)) return json; + if (typeof json !== "string") return failReturn; + try { + const res = JSON.parse(json); + return res; + } catch (err) { + console.log("err", json, err); + return failReturn; + } +} + +export function sleep(time) { + return new Promise((resolve) => setTimeout(resolve, time)); +} + +export function isValidDate(dateStr) { + if (!dateStr) return false; + let d = new Date(dateStr); + return !isNaN(d); +} + +export const sanitizeAndTruncate = (html, maxLength) => { + // Sanitize the HTML content + const sanitizedHtml = DOMPurify.sanitize(html); + + // Truncate the content + const truncatedHtml = sanitizedHtml.substring(0, maxLength); + + // Render the truncated content with ellipsis + const truncatedWithEllipsis = truncatedHtml + (sanitizedHtml.length > maxLength ? '...' : ''); + + return truncatedWithEllipsis; +}; + +export const increaseDate = (newDate) => { + + let initialDate = new Date(newDate); + + // Increase the date by one day + initialDate.setDate(initialDate.getDate() + 1); + + // Convert the increased date back to a string + let increasedDate = initialDate.toISOString().split('T')[0]; + + return increasedDate; +} +export const formatDate = (newDate) => { + + // Original date string + let originalDateString = newDate; + + // Create a Date object from the original date string + const originalDate = new Date(originalDateString); + + // Extract year, month, and day from the Date object + const year = originalDate.getFullYear(); + // Months in JavaScript are 0-based, so add 1 to get the correct month + const month = originalDate.getMonth() + 1; + const day = originalDate.getDate(); + + // Create the desired formatted date string + const formattedDateString = `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`; + return formattedDateString; +} +export const formatDate2 = (newDate) => { + + // Original date string + let originalDateString = newDate; + + // Create a Date object from the original date string + const originalDate = new Date(originalDateString); + + // Extract year, month, and day from the Date object + const year = originalDate.getFullYear(); + // Months in JavaScript are 0-based, so add 1 to get the correct month + const month = originalDate.getMonth() + 1; + const day = originalDate.getDate(); + + // Create the desired formatted date string + const formattedDateString = `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`; + return formattedDateString; +} + +export const notificationTime = (_date) => { + const isoDateTime = _date; + + // Create a Date object from the ISO string + const date = new Date(isoDateTime); + + date?.setHours(date?.getHours() - 1); + + // Format the date in the desired format + const formattedDate = `${(date.getMonth() + 1).toString().padStart(2, '0')}/${date.getDate().toString().padStart(2, '0')}/${date.getFullYear()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')} ${date.getHours() >= 12 ? 'PM' : 'AM'}`; + return formattedDate; +} +export const addOneHour = (_date) => { + + // Parse the input time string into a Date object + const inputDate = new Date(_date); + + // Add one hour to the Date object + inputDate.setHours(inputDate.getHours() + 1); + + // Format the result as a string in the desired format + const formattedDate = `${inputDate.getFullYear()}-${(inputDate.getMonth() + 1).toString().padStart(2, '0')}-${inputDate.getDate().toString().padStart(2, '0')} ${inputDate.getHours().toString().padStart(2, '0')}:${inputDate.getMinutes().toString().padStart(2, '0')}:${inputDate.getSeconds().toString().padStart(2, '0')}`; + + return formattedDate; +} +export const formatScheduleDate = (_date) => { + const originalDate = new Date(_date); + + const year = originalDate.getFullYear().toString().slice(-2); // Get the last two digits of the year + const month = (originalDate.getMonth() + 1).toString().padStart(2, '0'); // Months are zero-based, so add 1 + const day = originalDate.getDate().toString().padStart(2, '0'); + + const formattedDate = `${month}/${day}/${year}`; + + return formattedDate; +} + +export function extractLocationInfo(input) { + // Define regular expressions to match the patterns + const cityStateRegex = /(.+),\s*(.+),\s*(.+)/; // Matches "City, State, Country" + const cityCountryRegex = /(.+),\s*(.+)/; // Matches "City, Country" + + // Try to match the input against both regex patterns + const cityStateMatch = input.match(cityStateRegex); + const cityCountryMatch = input.match(cityCountryRegex); + + if (cityStateMatch) { + // Extracted as "City, State" and "Country" + const cityState = cityStateMatch[1]; + const country = cityStateMatch[3]; + return [cityState, country]; + } else if (cityCountryMatch) { + // Extracted as "City" and "Country" + const city = cityCountryMatch[1]; + const country = cityCountryMatch[2]; + return [city, country]; + } else { + // No match found + return [input]; + } +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..e09ab5b --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,27 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./src/**/*.{js,jsx,ts,tsx}"], + theme: { + fontSize: { + xxs: "0.5rem", + xs: "0.75rem", + sm: "0.875rem", + base: "0.9rem", + xl: "1rem", + "2xl": "1.25rem", + "3xl": "1.5rem", + "4xl": "1.953rem", + "5xl": "2.441rem", + "6xl": "3.114rem", + "7xl": "4rem", + }, + extend: { + borderRadius: { + pill: "100vw", + circle: "50%", + }, + colors: { primary: "#33d4b7", "primary-dark": "#0D9895" }, + }, + }, + plugins: [require("@headlessui/tailwindcss", '@tailwindcss/custom-forms')], +}; diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..04ad10a --- /dev/null +++ b/vite.config.js @@ -0,0 +1,23 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import svgr from "vite-plugin-svgr"; +import { resolve } from "path"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react(), svgr()], + build: { + chunkSizeWarningLimit: 1600, + commonjsOptions: { + defaultIsModuleExports(id) { + if (/react-google-autocomplete/.test(id)) return false; + return "auto"; + } + } + }, + resolve: { + alias: { + "@": resolve(__dirname, "./src") + } + } +});