initial commit

This commit is contained in:
undefined
2025-01-24 20:05:48 +01:00
commit db55c10f43
484 changed files with 118165 additions and 0 deletions
+28
View File
@@ -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?
+202
View File
@@ -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 <Link to="/faq">FAQs</Link> 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.
+41
View File
@@ -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();
}
};
},
};
+43
View File
@@ -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;
+59
View File
@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Google tag (gtag.js) -->
<!-- <script
async
src="https://www.googletagmanager.com/gtag/js?id=G-ELX69J6Y4R"
></script>
<script>
window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'G-ELX69J6Y4R');
</script> -->
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta build-version="<%= __BUILDNUMBER__ %>" />
<title>Ergo App</title>
<meta name="description" content="{{{description}}}" />
<link rel="manifest" href="/manifest.json" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.ico" />
<meta name="theme-color" content="#FFFFFF">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Ergo">
<meta property="og:title" content="{{{title}}}" />
<meta property="og:description" content="{{{description}}}" />
<meta property="og:image" content="https://tools.techwithryanwong.com/facebook.png" />
<meta property="og:determiner" content="the" />
<meta property="og:locale" content="en_US" />
<meta property="og:site_name" content="Ergo App" />
<meta property="og:url" content="https://techwithryanwong.com" />
<meta property="og:type" content="website" />
<meta property="fb:app_id" content="866666398024478" />
<meta name="author" content="Ryan wong" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@ryanwongtech" />
<meta name="twitter:title" content="{{{title}}}" />
<meta name="twitter:description" content="{{{description}}}" />
<meta name="twitter:image" content="https://tools.techwithryanwong.com/facebook.png" />
<link href="https://api.fontshare.com/v2/css?f[]=general-sans@200,300,600,400&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.jsx"></script>
<!-- <script
id="tidio-script"
src="//code.tidio.co/h0tpq7blt8pa6septktw5zcdj85psftv.js"
async
></script> -->
<!-- <script
src="/node_modules/device-uuid/lib/device-uuid.js"
type="module"
></script> -->
</body>
</html>
+8
View File
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
}
}
}
+21
View File
@@ -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"
}
}
+8328
View File
File diff suppressed because it is too large Load Diff
+85
View File
@@ -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"
}
}
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="36px" height="36px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<path fill="none" stroke="#d0d5dd" stroke-width="10" stroke-dasharray="42.76482137044271 42.76482137044271" d="M24.3 30C11.4 30 5 43.3 5 50s6.4 20 19.3 20c19.3 0 32.1-40 51.4-40 C88.6 30 95 43.3 95 50s-6.4 20-19.3 20C56.4 70 43.6 30 24.3 30z" stroke-linecap="round" style="transform:scale(1);transform-origin:50px 50px">
<animate attributeName="stroke-dashoffset" repeatCount="indefinite" dur="1.6666666666666667s" keyTimes="0;1" values="0;256.58892822265625"></animate>
</path>
<!-- [ldio] generated by https://loading.io/ --></svg>

After

Width:  |  Height:  |  Size: 817 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 852 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

+39
View File
@@ -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"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

+2
View File
@@ -0,0 +1,2 @@
User-agent: *
Disallow: /
+13
View File
@@ -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 };
+9
View File
@@ -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 */
}
+45
View File
@@ -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 (
<MantineProvider
theme={{
primaryColor: "violet"
}}
>
<Notifications position="top-right" />
<AuthProvider>
<GlobalProvider>
<Router>
<Elements stripe={stripePromise}>
<Main />
</Elements>
</Router>
</GlobalProvider>
</AuthProvider>
</MantineProvider>
);
}
export default App;
+3
View File
@@ -0,0 +1,3 @@
<svg width="18" height="14" viewBox="0 0 18 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 7H1M1 7L7 13M1 7L7 1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 204 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="18" height="14" viewBox="0 0 18 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 7H17M17 7L11 1M17 7L11 13" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 208 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="22" height="18" viewBox="0 0 22 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 8V12M17 6V10M16 1C18.4487 1 19.7731 1.37476 20.4321 1.66544C20.5199 1.70415 20.5638 1.72351 20.6904 1.84437C20.7663 1.91682 20.9049 2.12939 20.9405 2.22809C21 2.39274 21 2.48274 21 2.66274V13.4111C21 14.3199 21 14.7743 20.8637 15.0079C20.7251 15.2454 20.5914 15.3559 20.3319 15.4472C20.0769 15.5369 19.562 15.438 18.5322 15.2401C17.8114 15.1017 16.9565 15 16 15C13 15 10 17 6 17C3.55129 17 2.22687 16.6252 1.56788 16.3346C1.48012 16.2958 1.43624 16.2765 1.3096 16.1556C1.23369 16.0832 1.09512 15.8706 1.05947 15.7719C1 15.6073 1 15.5173 1 15.3373L1 4.58885C1 3.68009 1 3.2257 1.13628 2.99214C1.2749 2.75456 1.40859 2.64412 1.66806 2.55281C1.92314 2.46305 2.43803 2.56198 3.46783 2.75985C4.18862 2.89834 5.04348 3 6 3C9 3 12 1 16 1ZM13.5 9C13.5 10.3807 12.3807 11.5 11 11.5C9.61929 11.5 8.5 10.3807 8.5 9C8.5 7.61929 9.61929 6.5 11 6.5C12.3807 6.5 13.5 7.61929 13.5 9Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="18" viewBox="0 0 16 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.33325 5.5C1.33325 4.09987 1.33325 3.3998 1.60574 2.86502C1.84542 2.39462 2.22787 2.01217 2.69828 1.77248C3.23306 1.5 3.93312 1.5 5.33325 1.5H10.6666C12.0667 1.5 12.7668 1.5 13.3016 1.77248C13.772 2.01217 14.1544 2.39462 14.3941 2.86502C14.6666 3.3998 14.6666 4.09987 14.6666 5.5V16.5L12.3749 14.8333L10.2916 16.5L7.99992 14.8333L5.70825 16.5L3.62492 14.8333L1.33325 16.5V5.5Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 559 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 19V13.6C13 13.0399 13 12.7599 12.891 12.546C12.7951 12.3578 12.6422 12.2049 12.454 12.109C12.2401 12 11.9601 12 11.4 12H8.6C8.03995 12 7.75992 12 7.54601 12.109C7.35785 12.2049 7.20487 12.3578 7.10899 12.546C7 12.7599 7 13.0399 7 13.6V19M17 19V4.2C17 3.0799 17 2.51984 16.782 2.09202C16.5903 1.71569 16.2843 1.40973 15.908 1.21799C15.4802 1 14.9201 1 13.8 1H6.2C5.07989 1 4.51984 1 4.09202 1.21799C3.71569 1.40973 3.40973 1.71569 3.21799 2.09202C3 2.51984 3 3.0799 3 4.2V19M19 19H1M7.5 6H7.51M12.5 6H12.51M8 6C8 6.27614 7.77614 6.5 7.5 6.5C7.22386 6.5 7 6.27614 7 6C7 5.72386 7.22386 5.5 7.5 5.5C7.77614 5.5 8 5.72386 8 6ZM13 6C13 6.27614 12.7761 6.5 12.5 6.5C12.2239 6.5 12 6.27614 12 6C12 5.72386 12.2239 5.5 12.5 5.5C12.7761 5.5 13 5.72386 13 6Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 932 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 9H1M14 1V5M6 1V5M5.8 21H14.2C15.8802 21 16.7202 21 17.362 20.673C17.9265 20.3854 18.3854 19.9265 18.673 19.362C19 18.7202 19 17.8802 19 16.2V7.8C19 6.11984 19 5.27976 18.673 4.63803C18.3854 4.07354 17.9265 3.6146 17.362 3.32698C16.7202 3 15.8802 3 14.2 3H5.8C4.11984 3 3.27976 3 2.63803 3.32698C2.07354 3.6146 1.6146 4.07354 1.32698 4.63803C1 5.27976 1 6.11984 1 7.8V16.2C1 17.8802 1 18.7202 1.32698 19.362C1.6146 19.9265 2.07354 20.3854 2.63803 20.673C3.27976 21 4.11984 21 5.8 21Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 666 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="14" height="8" viewBox="0 0 14 8" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1L7 7L13 1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 191 B

+5
View File
@@ -0,0 +1,5 @@
<svg width="19" height="4" viewBox="0 0 19 4" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.5 3C10.0523 3 10.5 2.55228 10.5 2C10.5 1.44772 10.0523 1 9.5 1C8.94772 1 8.5 1.44772 8.5 2C8.5 2.55228 8.94772 3 9.5 3Z" stroke="#98A2B3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.5 3C17.0523 3 17.5 2.55228 17.5 2C17.5 1.44772 17.0523 1 16.5 1C15.9477 1 15.5 1.44772 15.5 2C15.5 2.55228 15.9477 3 16.5 3Z" stroke="#98A2B3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2.5 3C3.05228 3 3.5 2.55228 3.5 2C3.5 1.44772 3.05228 1 2.5 1C1.94772 1 1.5 1.44772 1.5 2C1.5 2.55228 1.94772 3 2.5 3Z" stroke="#98A2B3" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 752 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="18" height="22" viewBox="0 0 18 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 1.26953V5.40007C11 5.96012 11 6.24015 11.109 6.45406C11.2049 6.64222 11.3578 6.7952 11.546 6.89108C11.7599 7.00007 12.0399 7.00007 12.6 7.00007H16.7305M6 15L8 17L12.5 12.5M11 1H5.8C4.11984 1 3.27976 1 2.63803 1.32698C2.07354 1.6146 1.6146 2.07354 1.32698 2.63803C1 3.27976 1 4.11984 1 5.8V16.2C1 17.8802 1 18.7202 1.32698 19.362C1.6146 19.9265 2.07354 20.3854 2.63803 20.673C3.27976 21 4.11984 21 5.8 21H12.2C13.8802 21 14.7202 21 15.362 20.673C15.9265 20.3854 16.3854 19.9265 16.673 19.362C17 18.7202 17 17.8802 17 16.2V7L11 1Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 712 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="18" height="22" viewBox="0 0 18 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 1.26953V5.40007C11 5.96012 11 6.24015 11.109 6.45406C11.2049 6.64222 11.3578 6.7952 11.546 6.89108C11.7599 7.00007 12.0399 7.00007 12.6 7.00007H16.7305M9 17V11M6 14H12M11 1H5.8C4.11984 1 3.27976 1 2.63803 1.32698C2.07354 1.6146 1.6146 2.07354 1.32698 2.63803C1 3.27976 1 4.11984 1 5.8V16.2C1 17.8802 1 18.7202 1.32698 19.362C1.6146 19.9265 2.07354 20.3854 2.63803 20.673C3.27976 21 4.11984 21 5.8 21H12.2C13.8802 21 14.7202 21 15.362 20.673C15.9265 20.3854 16.3854 19.9265 16.673 19.362C17 18.7202 17 17.8802 17 16.2V7L11 1Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 708 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="18" height="22" viewBox="0 0 18 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 1.26953V5.40007C11 5.96012 11 6.24015 11.109 6.45406C11.2049 6.64222 11.3578 6.7952 11.546 6.89108C11.7599 7.00007 12.0399 7.00007 12.6 7.00007H16.7305M6.84998 11.0022C7.02617 10.5014 7.37395 10.079 7.83171 9.80998C8.28947 9.54095 8.82767 9.4426 9.35099 9.53237C9.87431 9.62213 10.349 9.89421 10.6909 10.3004C11.0329 10.7066 11.22 11.2207 11.2192 11.7517C11.2192 13.2506 8.97089 14 8.97089 14M9 17H9.01M11 1H5.8C4.11984 1 3.27976 1 2.63803 1.32698C2.07354 1.6146 1.6146 2.07354 1.32698 2.63803C1 3.27976 1 4.11984 1 5.8V16.2C1 17.8802 1 18.7202 1.32698 19.362C1.6146 19.9265 2.07354 20.3854 2.63803 20.673C3.27976 21 4.11984 21 5.8 21H12.2C13.8802 21 14.7202 21 15.362 20.673C15.9265 20.3854 16.3854 19.9265 16.673 19.362C17 18.7202 17 17.8802 17 16.2V7L11 1Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 943 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="20" height="22" viewBox="0 0 20 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 9.5V5.8C17 4.11984 17 3.27976 16.673 2.63803C16.3854 2.07354 15.9265 1.6146 15.362 1.32698C14.7202 1 13.8802 1 12.2 1H5.8C4.11984 1 3.27976 1 2.63803 1.32698C2.07354 1.6146 1.6146 2.07354 1.32698 2.63803C1 3.27976 1 4.11984 1 5.8V16.2C1 17.8802 1 18.7202 1.32698 19.362C1.6146 19.9265 2.07354 20.3854 2.63803 20.673C3.27976 21 4.11984 21 5.8 21H8.5M19 21L17.5 19.5M18.5 17C18.5 18.933 16.933 20.5 15 20.5C13.067 20.5 11.5 18.933 11.5 17C11.5 15.067 13.067 13.5 15 13.5C16.933 13.5 18.5 15.067 18.5 17Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 685 B

+6
View File
@@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.4 3H4.6C4.03995 3 3.75992 3 3.54601 3.10899C3.35785 3.20487 3.20487 3.35785 3.10899 3.54601C3 3.75992 3 4.03995 3 4.6V8.4C3 8.96005 3 9.24008 3.10899 9.45399C3.20487 9.64215 3.35785 9.79513 3.54601 9.89101C3.75992 10 4.03995 10 4.6 10H8.4C8.96005 10 9.24008 10 9.45399 9.89101C9.64215 9.79513 9.79513 9.64215 9.89101 9.45399C10 9.24008 10 8.96005 10 8.4V4.6C10 4.03995 10 3.75992 9.89101 3.54601C9.79513 3.35785 9.64215 3.20487 9.45399 3.10899C9.24008 3 8.96005 3 8.4 3Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.4 3H15.6C15.0399 3 14.7599 3 14.546 3.10899C14.3578 3.20487 14.2049 3.35785 14.109 3.54601C14 3.75992 14 4.03995 14 4.6V8.4C14 8.96005 14 9.24008 14.109 9.45399C14.2049 9.64215 14.3578 9.79513 14.546 9.89101C14.7599 10 15.0399 10 15.6 10H19.4C19.9601 10 20.2401 10 20.454 9.89101C20.6422 9.79513 20.7951 9.64215 20.891 9.45399C21 9.24008 21 8.96005 21 8.4V4.6C21 4.03995 21 3.75992 20.891 3.54601C20.7951 3.35785 20.6422 3.20487 20.454 3.10899C20.2401 3 19.9601 3 19.4 3Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.4 14H15.6C15.0399 14 14.7599 14 14.546 14.109C14.3578 14.2049 14.2049 14.3578 14.109 14.546C14 14.7599 14 15.0399 14 15.6V19.4C14 19.9601 14 20.2401 14.109 20.454C14.2049 20.6422 14.3578 20.7951 14.546 20.891C14.7599 21 15.0399 21 15.6 21H19.4C19.9601 21 20.2401 21 20.454 20.891C20.6422 20.7951 20.7951 20.6422 20.891 20.454C21 20.2401 21 19.9601 21 19.4V15.6C21 15.0399 21 14.7599 20.891 14.546C20.7951 14.3578 20.6422 14.2049 20.454 14.109C20.2401 14 19.9601 14 19.4 14Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.4 14H4.6C4.03995 14 3.75992 14 3.54601 14.109C3.35785 14.2049 3.20487 14.3578 3.10899 14.546C3 14.7599 3 15.0399 3 15.6V19.4C3 19.9601 3 20.2401 3.10899 20.454C3.20487 20.6422 3.35785 20.7951 3.54601 20.891C3.75992 21 4.03995 21 4.6 21H8.4C8.96005 21 9.24008 21 9.45399 20.891C9.64215 20.7951 9.79513 20.6422 9.89101 20.454C10 20.2401 10 19.9601 10 19.4V15.6C10 15.0399 10 14.7599 9.89101 14.546C9.79513 14.3578 9.64215 14.2049 9.45399 14.109C9.24008 14 8.96005 14 8.4 14Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 16H14M9.0177 1.764L2.23539 7.03912C1.78202 7.39175 1.55534 7.56806 1.39203 7.78886C1.24737 7.98444 1.1396 8.20478 1.07403 8.43905C1 8.70352 1 8.9907 1 9.56505V16.8C1 17.9201 1 18.4801 1.21799 18.908C1.40973 19.2843 1.71569 19.5903 2.09202 19.782C2.51984 20 3.07989 20 4.2 20H15.8C16.9201 20 17.4802 20 17.908 19.782C18.2843 19.5903 18.5903 19.2843 18.782 18.908C19 18.4801 19 17.9201 19 16.8V9.56505C19 8.9907 19 8.70352 18.926 8.43905C18.8604 8.20478 18.7526 7.98444 18.608 7.78886C18.4447 7.56806 18.218 7.39175 17.7646 7.03913L10.9823 1.764C10.631 1.49075 10.4553 1.35412 10.2613 1.3016C10.0902 1.25526 9.9098 1.25526 9.73865 1.3016C9.54468 1.35412 9.36902 1.49075 9.0177 1.764Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 864 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="22" height="21" viewBox="0 0 22 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 20V12.6C8 12.0399 8 11.7599 8.109 11.546C8.20487 11.3578 8.35785 11.2049 8.54601 11.109C8.75993 11 9.03995 11 9.6 11H12.4C12.9601 11 13.2401 11 13.454 11.109C13.6422 11.2049 13.7951 11.3578 13.891 11.546C14 11.7599 14 12.0399 14 12.6V20M1 8.5L10.04 1.72C10.3843 1.46181 10.5564 1.33271 10.7454 1.28294C10.9123 1.23902 11.0877 1.23902 11.2546 1.28295C11.4436 1.33271 11.6157 1.46181 11.96 1.72L21 8.5M3 7V16.8C3 17.9201 3 18.4802 3.21799 18.908C3.40974 19.2843 3.7157 19.5903 4.09202 19.782C4.51985 20 5.0799 20 6.2 20H15.8C16.9201 20 17.4802 20 17.908 19.782C18.2843 19.5903 18.5903 19.2843 18.782 18.908C19 18.4802 19 17.9201 19 16.8V7L12.92 2.44C12.2315 1.92361 11.8872 1.66542 11.5091 1.56589C11.1754 1.47804 10.8246 1.47804 10.4909 1.56589C10.1128 1.66542 9.76852 1.92361 9.08 2.44L3 7Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 973 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.27209 18.7279L9.86863 12.1314C10.2646 11.7354 10.4627 11.5373 10.691 11.4632C10.8918 11.3979 11.1082 11.3979 11.309 11.4632C11.5373 11.5373 11.7354 11.7354 12.1314 12.1314L18.6839 18.6839M13 13L15.8686 10.1314C16.2646 9.73535 16.4627 9.53735 16.691 9.46316C16.8918 9.3979 17.1082 9.3979 17.309 9.46316C17.5373 9.53735 17.7354 9.73535 18.1314 10.1314L21 13M9 7C9 8.10457 8.10457 9 7 9C5.89543 9 5 8.10457 5 7C5 5.89543 5.89543 5 7 5C8.10457 5 9 5.89543 9 7ZM5.8 19H16.2C17.8802 19 18.7202 19 19.362 18.673C19.9265 18.3854 20.3854 17.9265 20.673 17.362C21 16.7202 21 15.8802 21 14.2V5.8C21 4.11984 21 3.27976 20.673 2.63803C20.3854 2.07354 19.9265 1.6146 19.362 1.32698C18.7202 1 17.8802 1 16.2 1H5.8C4.11984 1 3.27976 1 2.63803 1.32698C2.07354 1.6146 1.6146 2.07354 1.32698 2.63803C1 3.27976 1 4.11984 1 5.8V14.2C1 15.8802 1 16.7202 1.32698 17.362C1.6146 17.9265 2.07354 18.3854 2.63803 18.673C3.27976 19 4.11984 19 5.8 19Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+405
View File
@@ -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",
"🖕"
]
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
<svg width="69" height="25" viewBox="0 0 69 25" fill=" none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.95426 19.1764C6.3634 19.1764 4.96896 18.8828 3.77091 18.2957C2.57286 17.7086 1.63995 16.8476 0.972187 15.7125C0.324062 14.5775 0 13.2076 0 11.6029C0 10.0764 0.324062 8.74567 0.972187 7.61063C1.63995 6.47558 2.56304 5.59494 3.74145 4.9687C4.91986 4.3229 6.30448 4 7.89534 4C9.40763 4 10.7333 4.27398 11.8725 4.82193C13.0312 5.35032 13.9249 6.13311 14.5533 7.1703C15.2015 8.18793 15.5255 9.4404 15.5255 10.9277C15.5255 11.1821 15.5157 11.4267 15.4961 11.6616C15.4764 11.8768 15.447 12.0921 15.4077 12.3074H2.03275V10.5461H13.316L12.4911 11.6322C12.5108 11.4365 12.5206 11.2506 12.5206 11.0745C12.5206 10.8788 12.5206 10.6831 12.5206 10.4874C12.5206 9.13707 12.1278 8.13901 11.3422 7.49321C10.5762 6.82783 9.40763 6.49515 7.83642 6.49515C6.08844 6.49515 4.84129 6.89633 4.09497 7.69869C3.34864 8.50105 2.97548 9.65567 2.97548 11.1625V11.9258C2.97548 13.4522 3.34864 14.6166 4.09497 15.419C4.84129 16.2213 6.09826 16.6225 7.86588 16.6225C9.39781 16.6225 10.4977 16.3877 11.1654 15.918C11.8528 15.4288 12.1965 14.7536 12.1965 13.8925V13.6577H15.3782V13.9219C15.3782 14.9591 15.0542 15.8789 14.406 16.6812C13.7776 17.464 12.9036 18.0805 11.7841 18.5306C10.6842 18.9611 9.40763 19.1764 7.95426 19.1764Z"/>
<path d="M21.6795 18.8828H18.4978V4.29355H21.4143V8.43256L21.6795 8.57933V18.8828ZM21.6795 10.6929H20.9724V8.22707H21.6206C21.7581 7.42471 22.033 6.71042 22.4455 6.08418C22.8579 5.43838 23.4078 4.92957 24.0952 4.55774C24.8023 4.18591 25.6566 4 26.6583 4C27.7778 4 28.691 4.23484 29.3981 4.70451C30.1051 5.17419 30.6158 5.80042 30.93 6.58321C31.2639 7.366 31.4308 8.21729 31.4308 9.13707V11.0451H28.2786V9.75352C28.2786 8.69675 28.0429 7.92374 27.5715 7.4345C27.1002 6.94525 26.3146 6.70063 25.2147 6.70063C23.9577 6.70063 23.0543 7.0431 22.5044 7.72804C21.9545 8.41299 21.6795 9.40126 21.6795 10.6929Z"/>
<path d="M41.7076 24.1667C40.2542 24.1667 38.9776 23.9416 37.8778 23.4915C36.7779 23.061 35.9138 22.4347 35.2853 21.6128C34.6764 20.7909 34.372 19.7928 34.372 18.6186H37.5243C37.5243 19.3231 37.6716 19.8907 37.9662 20.3212C38.2608 20.7517 38.7223 21.0551 39.3508 21.2312C39.9989 21.4269 40.8434 21.5247 41.8844 21.5247C43.0038 21.5247 43.8877 21.3975 44.5358 21.1431C45.2035 20.9083 45.6847 20.4875 45.9793 19.8809C46.2739 19.2742 46.4212 18.4327 46.4212 17.3564V8.81417L46.6569 8.60868V4.29355H49.5735V17.1803C49.5735 18.8045 49.2494 20.1255 48.6013 21.1431C47.9532 22.1803 47.0399 22.9436 45.8615 23.4328C44.6831 23.922 43.2984 24.1667 41.7076 24.1667ZM40.3819 17.5619C38.9285 17.5619 37.6814 17.2781 36.6404 16.7106C35.6192 16.1431 34.8237 15.3505 34.2542 14.3328C33.7042 13.3152 33.4293 12.1312 33.4293 10.7809C33.4293 9.43062 33.7141 8.24664 34.2836 7.22901C34.8728 6.21139 35.6977 5.41881 36.7583 4.85129C37.8385 4.28376 39.1151 4 40.5881 4C42.12 4 43.4163 4.34247 44.4769 5.02741C45.5571 5.69279 46.2248 6.66149 46.4802 7.93353H47.1577L46.981 10.4874H46.4212C46.4212 9.66545 46.2248 8.98051 45.832 8.43256C45.4392 7.86503 44.8795 7.44428 44.1528 7.1703C43.4261 6.89633 42.5423 6.75934 41.5014 6.75934C40.4997 6.75934 39.6257 6.88654 38.8794 7.14095C38.1527 7.39536 37.593 7.81611 37.2002 8.4032C36.827 8.97073 36.6404 9.7633 36.6404 10.7809C36.6404 11.779 36.827 12.5716 37.2002 13.1587C37.5733 13.7458 38.1135 14.1763 38.8205 14.4503C39.5472 14.7047 40.4113 14.8319 41.413 14.8319C43.0235 14.8319 44.2608 14.4992 45.125 13.8338C45.9891 13.1684 46.4212 12.19 46.4212 10.8983H46.981V13.7751H46.215C45.9597 14.8906 45.341 15.8006 44.359 16.5051C43.377 17.2096 42.0513 17.5619 40.3819 17.5619Z"/>
<path d="M60.5895 19.1764C58.979 19.1764 57.5551 18.8633 56.3178 18.237C55.1001 17.5912 54.1476 16.7008 53.4601 15.5657C52.7924 14.4111 52.4585 13.0902 52.4585 11.6029C52.4585 10.0764 52.7924 8.74567 53.4601 7.61063C54.1476 6.47558 55.1001 5.59494 56.3178 4.9687C57.5551 4.3229 58.979 4 60.5895 4C62.2393 4 63.673 4.3229 64.8907 4.9687C66.1084 5.59494 67.0511 6.47558 67.7189 7.61063C68.4063 8.74567 68.75 10.0764 68.75 11.6029C68.75 13.0902 68.4063 14.4111 67.7189 15.5657C67.0511 16.7008 66.1084 17.5912 64.8907 18.237C63.673 18.8633 62.2393 19.1764 60.5895 19.1764ZM60.5895 16.3583C62.3768 16.3583 63.6632 15.9571 64.4488 15.1548C65.2344 14.3328 65.6272 13.1489 65.6272 11.6029C65.6272 10.0568 65.2344 8.87288 64.4488 8.05095C63.6632 7.20944 62.3768 6.78869 60.5895 6.78869C58.8219 6.78869 57.5453 7.20944 56.7597 8.05095C55.9741 8.87288 55.5813 10.0568 55.5813 11.6029C55.5813 13.1489 55.9741 14.3328 56.7597 15.1548C57.5453 15.9571 58.8219 16.3583 60.5895 16.3583Z"/>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.3333 13.1667L16.5 9M16.5 9L12.3333 4.83333M16.5 9H6.5M6.5 1.5H5.5C4.09987 1.5 3.3998 1.5 2.86502 1.77248C2.39462 2.01217 2.01217 2.39462 1.77248 2.86502C1.5 3.3998 1.5 4.09987 1.5 5.5V12.5C1.5 13.9001 1.5 14.6002 1.77248 15.135C2.01217 15.6054 2.39462 15.9878 2.86502 16.2275C3.3998 16.5 4.09987 16.5 5.5 16.5H6.5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 497 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="22" height="18" viewBox="0 0 22 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 4L9.16492 9.71544C9.82609 10.1783 10.1567 10.4097 10.5163 10.4993C10.8339 10.5785 11.1661 10.5785 11.4837 10.4993C11.8433 10.4097 12.1739 10.1783 12.8351 9.71544L21 4M5.8 17H16.2C17.8802 17 18.7202 17 19.362 16.673C19.9265 16.3854 20.3854 15.9265 20.673 15.362C21 14.7202 21 13.8802 21 12.2V5.8C21 4.11984 21 3.27976 20.673 2.63803C20.3854 2.07354 19.9265 1.6146 19.362 1.32698C18.7202 1 17.8802 1 16.2 1H5.8C4.11984 1 3.27976 1 2.63803 1.32698C2.07354 1.6146 1.6146 2.07354 1.32698 2.63803C1 3.27976 1 4.11984 1 5.8V12.2C1 13.8802 1 14.7202 1.32698 15.362C1.6146 15.9265 2.07354 16.3854 2.63803 16.673C3.27976 17 4.11984 17 5.8 17Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 815 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="17" height="2" viewBox="0 0 17 2" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.5 1H15.5" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 204 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.332 2.00004C11.5071 1.82494 11.715 1.68605 11.9438 1.59129C12.1725 1.49653 12.4177 1.44775 12.6654 1.44775C12.913 1.44775 13.1582 1.49653 13.387 1.59129C13.6157 1.68605 13.8236 1.82494 13.9987 2.00004C14.1738 2.17513 14.3127 2.383 14.4074 2.61178C14.5022 2.84055 14.551 3.08575 14.551 3.33337C14.551 3.58099 14.5022 3.82619 14.4074 4.05497C14.3127 4.28374 14.1738 4.49161 13.9987 4.66671L4.9987 13.6667L1.33203 14.6667L2.33203 11L11.332 2.00004Z" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 632 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5 1V15M1.5 8H15.5" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 215 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="21" height="22" viewBox="0 0 21 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.0005 14C12.6573 14 14.0005 12.6569 14.0005 11C14.0005 9.34315 12.6573 8 11.0005 8C9.34363 8 8.00049 9.34315 8.00049 11C8.00049 12.6569 9.34363 14 11.0005 14Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.28957 18.3711L8.87402 19.6856C9.04776 20.0768 9.3313 20.4093 9.69024 20.6426C10.0492 20.8759 10.4681 21.0001 10.8962 21C11.3244 21.0001 11.7433 20.8759 12.1022 20.6426C12.4612 20.4093 12.7447 20.0768 12.9185 19.6856L13.5029 18.3711C13.711 17.9047 14.0609 17.5159 14.5029 17.26C14.9477 17.0034 15.4622 16.8941 15.9729 16.9478L17.4029 17.1C17.8286 17.145 18.2582 17.0656 18.6396 16.8713C19.021 16.6771 19.3379 16.3763 19.5518 16.0056C19.766 15.635 19.868 15.2103 19.8455 14.7829C19.823 14.3555 19.677 13.9438 19.4251 13.5978L18.5785 12.4344C18.277 12.0171 18.1159 11.5148 18.1185 11C18.1184 10.4866 18.281 9.98635 18.5829 9.57111L19.4296 8.40778C19.6814 8.06175 19.8275 7.65007 19.85 7.22267C19.8725 6.79528 19.7704 6.37054 19.5562 6C19.3423 5.62923 19.0255 5.32849 18.644 5.13423C18.2626 4.93997 17.833 4.86053 17.4074 4.90556L15.9774 5.05778C15.4667 5.11141 14.9521 5.00212 14.5074 4.74556C14.0645 4.48825 13.7144 4.09736 13.5074 3.62889L12.9185 2.31444C12.7447 1.92317 12.4612 1.59072 12.1022 1.3574C11.7433 1.12408 11.3244 0.99993 10.8962 1C10.4681 0.99993 10.0492 1.12408 9.69024 1.3574C9.3313 1.59072 9.04776 1.92317 8.87402 2.31444L8.28957 3.62889C8.0825 4.09736 7.73245 4.48825 7.28957 4.74556C6.84479 5.00212 6.33024 5.11141 5.81957 5.05778L4.38513 4.90556C3.95946 4.86053 3.52987 4.93997 3.14844 5.13423C2.76702 5.32849 2.45014 5.62923 2.23624 6C2.02206 6.37054 1.92002 6.79528 1.94251 7.22267C1.96499 7.65007 2.11103 8.06175 2.36291 8.40778L3.20957 9.57111C3.51151 9.98635 3.67411 10.4866 3.67402 11C3.67411 11.5134 3.51151 12.0137 3.20957 12.4289L2.36291 13.5922C2.11103 13.9382 1.96499 14.3499 1.94251 14.7773C1.92002 15.2047 2.02206 15.6295 2.23624 16C2.45036 16.3706 2.76727 16.6712 3.14864 16.8654C3.53001 17.0596 3.95949 17.1392 4.38513 17.0944L5.81513 16.9422C6.3258 16.8886 6.84034 16.9979 7.28513 17.2544C7.72966 17.511 8.08134 17.902 8.28957 18.3711Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 10V14.2C19 15.8802 19 16.7202 18.673 17.362C18.3854 17.9265 17.9265 18.3854 17.362 18.673C16.7202 19 15.8802 19 14.2 19H5.8C4.11984 19 3.27976 19 2.63803 18.673C2.07354 18.3854 1.6146 17.9265 1.32698 17.362C1 16.7202 1 15.8802 1 14.2V10M14 5L10 1M10 1L6 5M10 1V13" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 447 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.5305 16.3677L14.0759 16.1595C14.0235 16.274 13.9536 16.3632 13.8657 16.4344C13.7965 16.4905 13.717 16.524 13.6123 16.5331C13.5358 16.5398 13.4554 16.5237 13.3567 16.4582L13.3568 16.4581L13.3463 16.4515L9.19625 13.8473L8.93049 13.6805L8.66473 13.8473L4.51473 16.4515L4.51464 16.4513L4.50423 16.4582C4.40555 16.5237 4.32514 16.5398 4.24873 16.5331C4.14394 16.524 4.06449 16.4905 3.99524 16.4344C3.90747 16.3633 3.83772 16.2743 3.7853 16.16C3.75378 16.0908 3.7385 15.9998 3.76856 15.8638C3.76859 15.8636 3.76862 15.8635 3.76865 15.8634L4.86845 10.9424L4.9322 10.6572L4.71496 10.4617L1.04419 7.15819C0.945727 7.06488 0.900968 6.97672 0.883143 6.89047L0.883147 6.89047L0.882745 6.88857C0.859403 6.7778 0.86615 6.67532 0.902553 6.57104C0.945215 6.44883 1.0025 6.3661 1.06731 6.30703C1.10669 6.27113 1.1849 6.22473 1.34132 6.19965L6.17594 5.75835L6.47965 5.73062L6.59401 5.44791L8.46901 0.812492L8.46973 0.810696C8.51654 0.693678 8.57769 0.628401 8.64887 0.586111L8.64959 0.585682C8.75552 0.522513 8.84694 0.5 8.93049 0.5C9.01398 0.5 9.10586 0.522488 9.21263 0.585824C9.28315 0.627946 9.34427 0.693252 9.39125 0.810696L9.39197 0.81249L11.267 5.44791L11.3813 5.73062L11.685 5.75835L16.5197 6.19965C16.6761 6.22473 16.7543 6.27113 16.7937 6.30703C16.8585 6.3661 16.9158 6.44883 16.9584 6.57105C16.995 6.67572 17.0019 6.7786 16.979 6.88955C16.9607 6.97634 16.9153 7.06483 16.8168 7.15818L13.146 10.4617L12.9288 10.6572L12.9925 10.9424L14.0923 15.8634C14.0924 15.8635 14.0924 15.8636 14.0924 15.8637C14.1225 15.9999 14.1072 16.0909 14.0756 16.1602L14.5305 16.3677ZM14.5305 16.3677C14.6138 16.1851 14.6305 15.9809 14.5805 15.7552L3.33049 16.3677C3.41382 16.5497 3.53049 16.7014 3.68049 16.8229C3.83049 16.9444 4.00549 17.0139 4.20549 17.0312C4.40549 17.0486 4.59716 16.9965 4.78049 16.875L8.93049 14.2708L13.0805 16.875C13.2638 16.9965 13.4555 17.0486 13.6555 17.0312C13.8555 17.0139 14.0305 16.9444 14.1805 16.8229C14.3305 16.7014 14.4472 16.5497 14.5305 16.3677Z" />
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.3333 5.00008V4.33341C12.3333 3.39999 12.3333 2.93328 12.1517 2.57676C11.9919 2.26316 11.7369 2.00819 11.4233 1.8484C11.0668 1.66675 10.6001 1.66675 9.66667 1.66675H8.33333C7.39991 1.66675 6.9332 1.66675 6.57668 1.8484C6.26308 2.00819 6.00811 2.26316 5.84832 2.57676C5.66667 2.93328 5.66667 3.39999 5.66667 4.33341V5.00008M1.5 5.00008H16.5M14.8333 5.00008V14.3334C14.8333 15.7335 14.8333 16.4336 14.5608 16.9684C14.3212 17.4388 13.9387 17.8212 13.4683 18.0609C12.9335 18.3334 12.2335 18.3334 10.8333 18.3334H7.16667C5.76654 18.3334 5.06647 18.3334 4.53169 18.0609C4.06129 17.8212 3.67883 17.4388 3.43915 16.9684C3.16667 16.4336 3.16667 15.7335 3.16667 14.3334V5.00008" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 850 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.3163 18.4384C4.92462 17.0052 6.34492 16 8 16H14C15.6551 16 17.0754 17.0052 17.6837 18.4384M15 8.5C15 10.7091 13.2091 12.5 11 12.5C8.79086 12.5 7 10.7091 7 8.5C7 6.29086 8.79086 4.5 11 4.5C13.2091 4.5 15 6.29086 15 8.5ZM21 11C21 16.5228 16.5228 21 11 21C5.47715 21 1 16.5228 1 11C1 5.47715 5.47715 1 11 1C16.5228 1 21 5.47715 21 11Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 514 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.00002 20.8174C3.6026 21 4.41649 21 5.8 21H16.2C17.5835 21 18.3974 21 19 20.8174M3.00002 20.8174C2.87082 20.7783 2.75133 20.7308 2.63803 20.673C2.07354 20.3854 1.6146 19.9265 1.32698 19.362C1 18.7202 1 17.8802 1 16.2V5.8C1 4.11984 1 3.27976 1.32698 2.63803C1.6146 2.07354 2.07354 1.6146 2.63803 1.32698C3.27976 1 4.11984 1 5.8 1H16.2C17.8802 1 18.7202 1 19.362 1.32698C19.9265 1.6146 20.3854 2.07354 20.673 2.63803C21 3.27976 21 4.11984 21 5.8V16.2C21 17.8802 21 18.7202 20.673 19.362C20.3854 19.9265 19.9265 20.3854 19.362 20.673C19.2487 20.7308 19.1292 20.7783 19 20.8174M3.00002 20.8174C3.00035 20.0081 3.00521 19.5799 3.07686 19.2196C3.39249 17.6329 4.63288 16.3925 6.21964 16.0769C6.60603 16 7.07069 16 8 16H14C14.9293 16 15.394 16 15.7804 16.0769C17.3671 16.3925 18.6075 17.6329 18.9231 19.2196C18.9948 19.5799 18.9996 20.0081 19 20.8174M15 8.5C15 10.7091 13.2091 12.5 11 12.5C8.79086 12.5 7 10.7091 7 8.5C7 6.29086 8.79086 4.5 11 4.5C13.2091 4.5 15 6.29086 15 8.5Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 19V17C21 15.1362 19.7252 13.5701 18 13.126M14.5 1.29076C15.9659 1.88415 17 3.32131 17 5C17 6.67869 15.9659 8.11585 14.5 8.70924M16 19C16 17.1362 16 16.2044 15.6955 15.4693C15.2895 14.4892 14.5108 13.7105 13.5307 13.3045C12.7956 13 11.8638 13 10 13H7C5.13623 13 4.20435 13 3.46927 13.3045C2.48915 13.7105 1.71046 14.4892 1.30448 15.4693C1 16.2044 1 17.1362 1 19M12.5 5C12.5 7.20914 10.7091 9 8.5 9C6.29086 9 4.5 7.20914 4.5 5C4.5 2.79086 6.29086 1 8.5 1C10.7091 1 12.5 2.79086 12.5 5Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 667 B

+177
View File
@@ -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 <div className="popup-container flex items-center justify-center">
<div className="">
<svg
style={{ margin: "auto", background: "transparent", display: " block", shapeRendering: "auto" }}
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid"
className="md:w-[100px] md:h-[100px] w-[80px] h-[80px]"
>
<path
fill="none"
stroke="#d0d5dd"
strokeWidth="6"
strokeDasharray="42.76482137044271 42.76482137044271"
d="M24.3 30C11.4 30 5 43.3 5 50s6.4 20 19.3 20c19.3 0 32.1-40 51.4-40 C88.6 30 95 43.3 95 50s-6.4 20-19.3 20C56.4 70 43.6 30 24.3 30z"
strokeLinecap="round"
style={{ transform: "scale(0.8)", transformOrigin: "50px 50px" }}
>
<animate
attributeName="stroke-dashoffset"
repeatCount="indefinite"
dur="1.882051282051282s"
keyTimes="0;1"
values="0;256.58892822265625"
></animate>
</path>
</svg>
</div>
</div>;
return (
<AuthContext.Provider
value={{
state,
dispatch,
}}
>
{children}
</AuthContext.Provider>
);
};
export default AuthProvider;
+18
View File
@@ -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 (
<>
<NavLink
to={link}
className="ml-5 mb-1 flex items-center rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none"
>
<PlusCircleIcon className="h-6 w-6" />
<span className="ml-2">{text ? text : ""}</span>
</NavLink>
</>
);
};
export default AddButton;
+207
View File
@@ -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 (
<>
<div className={`sidebar-holder overflow-y-auto border-r-4 border-gray-100 ${!state.isOpen ? "open-nav" : ""}`}>
<div className="sticky top-0 h-fit pb-8">
<div className="mt-4 w-full p-4">
<div className="mx-auto w-10/12 text-center text-2xl font-bold text-black">
<LogoIcon fill={"#1D2939"} />
</div>
</div>
<div className="sidebar-list w-full">
<ul className="flex flex-wrap">
{adminNavigationItems.map((item) => {
if (item.sub_categories) {
return (
<li
key={item.path}
style={{ display: "relative" }}
className={`super-nav relative mx-auto my-auto block w-10/12 list-none justify-between rounded-lg ${item.sub_categories.length > 7 ? "larger" : ""} ${item.sub_categories.length > 2 ? "large" : ""} ${item.sub_categories.length < 2 ? "small" : ""
}`}
onClick={(e) => e.currentTarget.classList.toggle("open")}
>
<NavLink
to={`/admin/${item.path}`}
className={`flex items-center rounded-lg group-hover:bg-[#1D2939] group-hover:text-white ${state.path == item.path ? "bg-[#1D2939] stroke-white text-white" : ""}`}
>
<span className="mr-3">{item.icon}</span>
<span>{item.title}</span>
<span className="flex flex-grow justify-end">
<ChevronDownIcon className="h-4 w-4" />
</span>
</NavLink>
<div className="nav-item-dropdown absolute w-full">
{item.sub_categories.map((sub, idx) => (
<div
key={idx}
className={`group mx-auto my-auto block w-10/12 list-none justify-between truncate rounded-lg`}
onClick={(e) => e.stopPropagation()}
>
<NavLink
to={`/admin/${sub.path}`}
className={`flex items-center rounded-lg group-hover:bg-[#1D2939] group-hover:text-white ${state.path == sub.path ? "bg-[#1D2939] stroke-white text-white" : ""}`}
>
<span className="mr-3">{sub.icon}</span>
<span>{sub.title}</span>
</NavLink>
</div>
))}
</div>
</li>
);
}
if (item.path == "notification") {
return (
<li
key={item.path}
className={`group relative mx-auto my-auto block w-10/12 list-none justify-between rounded-lg`}
>
<NavLink
to={`/admin/${item.path}`}
className={`flex items-center rounded-lg group-hover:bg-[#1D2939] group-hover:text-white ${state.path == item.path ? "bg-[#1D2939] stroke-white text-white" : ""}`}
>
<span className="mr-3">{item.icon}</span>
<strong
className={`${state.adminNotificationCount > 0 ? "inline" : "hidden"
} absolute right-1 flex h-8 w-8 items-center justify-center rounded-full border bg-red-400 px-2 text-xs text-white`}
>
{state.adminNotificationCount}
</strong>
<span>{item.title}</span>
</NavLink>
</li>
);
}
return (
<li
key={item.path}
className={`group mx-auto my-auto block w-10/12 list-none justify-between rounded-lg`}
>
<NavLink
to={`/admin/${item.path}`}
className={`flex items-center rounded-lg group-hover:bg-[#1D2939] group-hover:text-white ${state.path == item.path ? "bg-[#1D2939] stroke-white text-white" : ""}`}
>
<span className="mr-3">{item.icon}</span>
<span>{item.title}</span>
</NavLink>
</li>
);
})}
<li className="group group mx-auto w-10/12 !cursor-pointer list-none rounded-lg">
<a
className="flex items-center rounded-lg group-hover:bg-[#1D2939] group-hover:text-white"
onClick={() => {
globalDispatch({
type: "SHOWMODAL",
payload: {
showModal: true,
modalShowTitle: "Are you sure?",
modalShowMessage: "You are about to log out.",
modalBtnText: "Yes, Log Out",
},
});
}}
>
<span className="mr-3">{<ArrowLeftOnRectangleIcon className="h-6 w-6" />}</span>
<span>Logout</span>
</a>
</li>
<li className="group group mx-auto w-10/12 !cursor-pointer list-none rounded-lg">
<a
className="flex items-center rounded-lg group-hover:bg-[#1D2939] group-hover:text-white"
onClick={switchToCustomer}
>
<span className="mr-3">
<ArrowsRightLeftIcon className="h-6 w-6" />
</span>
<span>Switch to customer</span>
</a>
</li>
<li className="group group mx-auto w-10/12 !cursor-pointer list-none rounded-lg">
<a
className="flex items-center rounded-lg group-hover:bg-[#1D2939] group-hover:text-white"
onClick={switchToHost}
>
<span className="mr-3">
<ArrowsRightLeftIcon className="h-6 w-6" />
</span>
<span>Switch to Host</span>
</a>
</li>
</ul>
</div>
</div>
</div>
</>
);
};
export default AdminHeader;
@@ -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 (
<div>
{/* Add Modal UI here to allow card to be created */}
</div>
);
}
@@ -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 (
<Transition
appear
show={modalOpen}
as={Fragment}
>
<Dialog
as="div"
className="relative z-10"
onClose={closeModal}
>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-25" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel
as="form"
className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all"
onSubmit={onSubmit}
>
<div className="mb-[18px] flex items-center justify-between">
<Dialog.Title
as="h3"
className="mb-[8px] text-2xl font-semibold"
>
Delete Payment Method
</Dialog.Title>
<button
onClick={closeModal}
className="rounded-full border p-1 px-3 text-2xl font-normal duration-100 hover:bg-gray-200 active:bg-gray-300"
>
&#x2715;
</button>
</div>
<p>You are about to remove card ending with {card.last4}</p>
<div className="flex gap-4">
<button
type="button"
className="mt-4 flex-grow rounded border-2 border-[#98A2B3] py-2 tracking-wide outline-none focus:outline-none"
onClick={() => {
ctrl.abort();
closeModal();
}}
>
Cancel
</button>
<LoadingButton
loading={loading}
type="submit"
className={`mt-4 flex-grow rounded bg-[#D92D20] ${loading ? "py-1 px-4" : "py-2"} tracking-wide text-white outline-none focus:outline-none`}
>
Yes, remove
</LoadingButton>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
);
}
+10
View File
@@ -0,0 +1,10 @@
import React from 'react'
const Button = ({ text, setResetClicked }) => {
return (<button onClick={() => setResetClicked(false)} type="submit" className=" ml-2 rounded-md font-inter px-[66px] py-[10px] bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text text-transparent border border-[#33D4B7]">
{text}
</button>)
}
export default Button
+18
View File
@@ -0,0 +1,18 @@
import React from "react";
export default function CaretDownIcon() {
return (
<svg
className=" w-6 float-right inline-block"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z"
clipRule="evenodd"
></path>
</svg>
);
}
+18
View File
@@ -0,0 +1,18 @@
import React from "react";
export default function CaretUpIcon() {
return (
<svg
className="w-6 inline-block float-right"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clipRule="evenodd"
></path>
</svg>
);
}
+37
View File
@@ -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 (
<div className={"popup-container z-100 flex items-center justify-center normal-case"}>
<div
className={`${state.confirmation ? "pop-in" : "pop-out"} w-[510px] max-w-[80%] rounded-lg bg-white p-5 px-3 md:px-5`}
onClick={(e) => e.stopPropagation()}
>
<h2 className="mb-4 text-3xl font-semibold">
<GreenCheckIcon />
{state.confirmationHeading}
</h2>
<p className="mb-4 text-sm text-gray-500">{state.confirmationMsg}</p>
<button
type="button"
className="login-btn-gradient mt-4 w-full rounded py-2 tracking-wide text-white outline-none focus:outline-none"
onClick={() => {
if (state.confirmationCloseFn) {
state.confirmationCloseFn();
}
dispatch({ type: "CLOSE_CONFIRMATION" });
}}
>
{state.confirmationBtn}
</button>
</div>
</div>
);
}
+30
View File
@@ -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 (
<div className="flex items-center gap-[10px] p-2 font-semibold">
<button
type="button"
className={"rounded-md border-2 border-black px-3 text-2xl disabled:border-[#D0D5DD]"}
onClick={() => setValue(Number(field.value) - 1)}
disabled={Number(field.value) <= (minCount || 0)}
onBlur={field.onBlur}
>
-
</button>
<span>{field.value}</span>
<button
type="button"
className={"rounded-md border-2 border-black px-3 text-2xl disabled:border-[#D0D5DD]"}
onClick={() => setValue(Number(field.value) + 1)}
disabled={Number(field.value) >= maxCount}
onBlur={field.onBlur}
>
+
</button>
</div>
);
}
+63
View File
@@ -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 (
<Combobox
as={"div"}
className={`${containerClassName ?? ""}`}
value={field.value}
onChange={setValue}
>
<Combobox.Input
{...restProps}
{...field}
autoComplete="off"
/>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Combobox.Options
className={`tiny-scroll absolute left-0 right-0 top-full z-50 mt-2 max-h-60 w-full origin-top cursor-pointer divide-y divide-gray-100 overflow-y-auto rounded-xl bg-white ring-black ring-opacity-5 focus:outline-none ${
filteredItems.length > 0 ? "py-2 shadow-lg ring-1" : ""
}`}
>
{filteredItems.map((item, idx) => (
<Combobox.Option
className="flex w-full items-center truncate rounded-pill px-3 py-3 pr-5 text-sm ui-active:bg-gray-100 ui-active:text-black ui-not-active:text-gray-800"
key={idx}
value={item[valueField]}
>
{item[labelField]}
</Combobox.Option>
))}
</Combobox.Options>
</Transition>
</Combobox>
);
}
+75
View File
@@ -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 (
<Combobox
as={"div"}
className={`${className ?? ""} ${fieldState.error ? "border-red-500" : ""} normal-case`}
value={field.value}
onChange={setValue}
>
<Combobox.Input
{...restProps}
className="w-full truncate border-0 text-black focus:outline-none"
onChange={(e) => {
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"
/>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Combobox.Options
className={`tiny-scroll absolute left-0 right-0 top-full z-50 mt-2 max-h-60 w-full origin-top cursor-pointer divide-y divide-gray-100 overflow-y-auto rounded-xl bg-white ring-black ring-opacity-5 focus:outline-none ${
items.length > 0 ? "py-2 shadow-lg ring-1" : ""
}`}
>
{items.map((item, idx) => (
<Combobox.Option
className="flex w-full items-center truncate rounded-pill px-3 py-3 pr-5 text-sm ui-active:bg-gray-100 ui-active:text-black ui-not-active:text-gray-800"
key={idx}
value={item[valueField]}
>
{item[labelField]}
</Combobox.Option>
))}
</Combobox.Options>
</Transition>
</Combobox>
);
}
@@ -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 (
<Combobox
as={"div"}
className={`relative w-full normal-case z-100 ${containerClassName ?? ""}`}
value={(typeof field.value === "object") ? field.value : (field.value)?.replace(', undefined', '')}
>
{!hideIcons && <LocationIcon />}
<Combobox.Input
{...restProps}
autoComplete="off"
className={`w-full truncate text-black ${className ?? ""}`}
onBlur={field.onBlur}
value={(typeof field.value === "object") ? field.value : (field.value)?.replace(', undefined', '')}
onChange={(evt) => {
field.onChange(evt);
getPlacePredictions({ input: evt.target.value });
}}
/>
{!hideIcons && field.value && (
<button
type="button"
onClick={() => {
setValue("");
if (onClear) {
onClear();
}
}}
>
&#x2715;
</button>
)}
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
{isPlacePredictionsLoading ? (
<div className="absolute left-0 right-0 top-full z-50 mt-2 flex w-full origin-top justify-center rounded-xl border bg-white py-8">
<svg
style={{ margin: "auto", background: "none", display: "block", shapeRendering: "auto" }}
width="36px"
height="36px"
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid"
>
<path
fill="none"
stroke="#d0d5dd"
strokeWidth="10"
strokeDasharray="42.76482137044271 42.76482137044271"
d="M24.3 30C11.4 30 5 43.3 5 50s6.4 20 19.3 20c19.3 0 32.1-40 51.4-40 C88.6 30 95 43.3 95 50s-6.4 20-19.3 20C56.4 70 43.6 30 24.3 30z"
strokeLinecap="round"
style={{ transform: "scale(1)", transformOrigin: "50px 50px" }}
>
<animate
attributeName="stroke-dashoffset"
repeatCount="indefinite"
dur="1.6666666666666667s"
keyTimes="0;1"
values="0;256.58892822265625"
></animate>
</path>
</svg>
</div>
) : (
<Combobox.Options
className={`${placePredictions.length > 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) => (
<Combobox.Option
className="flex w-full items-center truncate rounded-pill px-3 py-3 pr-5 text-sm ui-active:bg-gray-100 ui-active:text-black ui-not-active:text-gray-800"
key={idx}
value={place.structured_formatting.main_text}
onClick={() => setValue(place?.structured_formatting.main_text + ', ' + place.structured_formatting?.secondary_text)}
>
<span>{`${place.structured_formatting.main_text} ${place.structured_formatting?.secondary_text ? "," : ""} ${place.structured_formatting?.secondary_text ? place.structured_formatting?.secondary_text : ""}`}</span>
</Combobox.Option>
))}
</Combobox.Options>
)}
</Transition>
</Combobox>
);
}
+59
View File
@@ -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 (
<div className={`relative rounded-md focus:outline-none active:outline-none ${containerClassName}`}>
<Listbox
as={"fragment"}
value={field.value}
onChange={field.onChange}
>
<Listbox.Button
className={`flex h-full w-full items-center justify-between ${field.value === "" ? "text-gray-500" : ""} ${restProps.className ?? ""} ${dropdownOpen ? restProps.openClassName ?? "" : ""}`}
>
<span className="block truncate">{selected ? selected[labelField] : defaultImage === undefined ? placeholder : defaultImage["name"]}</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronUpDownIcon
className="h-5 w-5 text-gray-400"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
afterEnter={field.onBlur}
beforeEnter={() => setDropdownOpen(true)}
afterLeave={() => setDropdownOpen(false)}
>
<Listbox.Options
className={`absolute z-50 mt-1 w-full max-h-60 md:max-w-lg overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm`}
>
{items.map((item, idx) => (
<Listbox.Option
key={idx}
className={`relative cursor-pointer select-none py-2 pr-4 pl-4 ui-active:bg-amber-100 ui-active:text-amber-900 ui-not-active:text-gray-900`}
value={item[valueField]}
>
{item[labelField]}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</Listbox>
</div>
);
}
@@ -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 (
<Combobox
as={"div"}
className={`relative w-full normal-case z-100 ${containerClassName ?? ""}`}
value={location}
>
{!hideIcons && <LocationIcon />}
<Combobox.Input
{...restProps}
autoComplete="off"
className={`w-full truncate text-black ${className ?? ""}`}
value={globalState.location}
onChange={(evt) => {
setLocation(evt.target.value)
getPlacePredictions({ input: evt.target.value });
}}
/>
{!hideIcons && globalState.location && (
<button
type="button"
onClick={() => {
setValue("");
setLocation("");
if (onClear) {
onClear();
}
}}
>
&#x2715;
</button>
)}
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
{isPlacePredictionsLoading ? (
<div className="absolute left-0 right-0 top-full z-50 mt-2 flex w-full origin-top justify-center rounded-xl border bg-white py-8">
<svg
style={{ margin: "auto", background: "none", display: "block", shapeRendering: "auto" }}
width="36px"
height="36px"
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid"
>
<path
fill="none"
stroke="#d0d5dd"
strokeWidth="10"
strokeDasharray="42.76482137044271 42.76482137044271"
d="M24.3 30C11.4 30 5 43.3 5 50s6.4 20 19.3 20c19.3 0 32.1-40 51.4-40 C88.6 30 95 43.3 95 50s-6.4 20-19.3 20C56.4 70 43.6 30 24.3 30z"
strokeLinecap="round"
style={{ transform: "scale(1)", transformOrigin: "50px 50px" }}
>
<animate
attributeName="stroke-dashoffset"
repeatCount="indefinite"
dur="1.6666666666666667s"
keyTimes="0;1"
values="0;256.58892822265625"
></animate>
</path>
</svg>
</div>
) : (
<Combobox.Options
className={`${placePredictions.length > 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) => (
<Combobox.Option
className="flex w-full items-center truncate rounded-pill px-3 py-3 pr-5 text-sm ui-active:bg-gray-100 ui-active:text-black ui-not-active:text-gray-800"
key={idx}
value={place.structured_formatting.main_text}
onClick={() =>
setValue(place?.structured_formatting.main_text + ', ' + place.structured_formatting?.secondary_text)
}
>
<span>{`${place.structured_formatting.main_text} ${place.structured_formatting?.secondary_text ? "," : ""} ${place.structured_formatting?.secondary_text ? place.structured_formatting?.secondary_text : ""}`}</span>
</Combobox.Option>
))}
</Combobox.Options>
)}
</Transition>
</Combobox>
);
}
@@ -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 (
<>
<Transition
appear
show={modalOpen && gettingStarted == 0}
as={Fragment}
>
<Dialog
as="div"
className="relative z-10"
onClose={() => setModalOpen(false)}
>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-25" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900"
>
First time login?
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">Would you like a tour of the site?</p>
</div>
<div className="mt-4 flex justify-end gap-4">
<button
type="button"
className="inline-flex justify-center rounded-md border px-4 py-2 text-sm font-medium focus:outline-none"
onClick={() => {
setModalOpen(false);
markAsNotFirstTimeUser();
}}
>
No thanks
</button>
<button
type="button"
className={`login-btn-gradient inline-flex justify-center rounded-md py-2 px-4 text-sm font-medium text-white`}
onClick={() => {
setModalOpen(false);
setIsOpen(true)
globalDispatch({ type: "START_TOUR" });
}}
>
Yes please
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
</>
);
}
+180
View File
@@ -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 (
<>
<header
className={`fixed top-0 left-0 z-50 flex w-screen flex-wrap items-center justify-between py-4 px-4 text-sm duration-500 md:flex-nowrap md:rounded-br-[32px] md:rounded-bl-[32px] md:px-12 header-${variant}`}
>
<nav className={`gap-6`}>
<Link
to="/"
className=""
>
<LogoIcon fill={variant == "transparent" || variant == "light" ? undefined : "#101828"} />
</Link>
</nav>
<StaticSearchBar className="hidden lg:block" />
<div className="flex gap-4 space-x-4">
<nav className={`z-50 inline `}>
{" "}
{pathname.startsWith("/account") && (
<button
className={`self-stretch rounded-md border px-6 py-[5px] pb-[7px] my-border-${variant} ${variant == "transparent" ? "" : "border-white"}`}
onClick={()=>navigate("/search?location=&booking_start_time=&max_capacity=&capacity=&size=")}
>
<span>Explore Spaces</span>
</button>
)}
</nav>
<nav className="z-50 flex items-center gap-6">
{authState.originalRole != "customer" ? (
<button
onClick={switchToHost}
className={`self-stretch rounded-md border px-6 py-[5px] pb-[7px] my-border-${variant} hidden whitespace-nowrap md:inline`}
>
<span>Join as host</span>
</button>
) : (
<button
onClick={becomeAHost}
className={`self-stretch rounded-md border px-6 py-[5px] pb-[7px] my-border-${variant} hidden whitespace-nowrap md:inline`}
>
<span>Become a host</span>
</button>
)}
<NavMenu variant={variant} />
</nav>
</div>
<StaticSearchBar className="flex w-full justify-center py-4 md:hidden" />
</header>
</>
);
};
export default CustomerHeader;
+82
View File
@@ -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 (
<div className={`w-full`}>
<Popover className="lg:relative">
<Popover.Button className={`flex w-full justify-between focus:outline-none ui-open:text-opacity-90`}>
<div className={`flex gap-2 ${labelClassName ?? ""}`}>
{!fieldState.isDirty ? (
<CalendarIcon />
) : (
<span
className={`self-end`}
type="button"
onClick={(e) => {
e.stopPropagation();
reset();
searchParams.delete(name);
}}
>
&#x2715;
</span>
)}
<span className={`${!fieldState.isDirty ? "text-gray-400" : ""}`}>{fieldState.isDirty || searchParams.get(name) ? formatDate(field.value) : placeholder}</span>
</div>
</Popover.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
afterEnter={field.onBlur}
>
<Popover.Panel className={`absolute left-1/2 z-10 mt-3 -translate-x-1/2 transform px-4 pb-12 sm:px-0`}>
{({ close }) => (
<div className="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5">
<Calendar
onChange={(val) => {
field.onChange(val);
close();
}}
value={field.value}
className={`calendar date-picker`}
nextLabel={<NextIcon />}
prevLabel={<PrevIcon />}
next2Label={
<div
className="h-full w-full cursor-default"
onClick={(e) => e.stopPropagation()}
></div>
}
prev2Label={
<div
className="h-full w-full cursor-default"
onClick={(e) => e.stopPropagation()}
></div>
}
maxDetail="month"
minDate={min}
/>
</div>
)}
</Popover.Panel>
</Transition>
</Popover>
</div>
);
}
View File
+74
View File
@@ -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 (
<Transition
appear
show={state.error}
as={Fragment}
>
<Dialog
as="div"
className="relative z-50"
onClose={() => dispatch({ type: "CLOSE_ERROR" })}
>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 z-10 bg-black bg-opacity-25" />
</Transition.Child>
<div className="fixed inset-0 z-50 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-lg transform overflow-hidden rounded-2xl bg-white p-6 text-center align-middle z-1000 shadow-xl transition-all">
<div className="flex justify-end">
<button
onClick={() => dispatch({ type: "CLOSE_ERROR" })}
className="text-gray-500 duration-100 hover:text-black"
>
<XMarkIcon className="h-6 w-6" />
</button>
</div>
<div className="mt-4 flex justify-center">
<ExclamationTriangleIcon className="h-6 w-6 text-red-600" />
</div>
<Dialog.Title
as="h3"
className="mt-8 text-2xl"
>
{state.errorHeading}
</Dialog.Title>
<p className="tiny-scroll mt-4 max-h-[300px] overflow-y-auto text-wrap break-normal text-sm text-gray-500">{state.errorMsg}</p>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
);
}
+83
View File
@@ -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 (
<div className="w-full p-1">
<div className="flex justify-between">
<div></div>
<div>
<button
className="pr-2 bg-gradient-to-r from-[#33D4B7] to-[#0D9895] bg-clip-text text-transparent font-bold border-r border-gray-200"
onClick={() => {
navigate(`/admin/edit-faq/${data.id}`, {
state: data,
});
}}
>
Edit
</button>
<button
className="font-semibold text-sm py-2.5 text-center inline-flex items-center mr-2 mb-2"
onClick={() => {
dispatch({
type: "SHOWMODAL",
payload: {
showModal: true,
modalShowMessage: "Are you sure you want to delete this question?",
modalShowTitle: "Confirm Changes",
type: "BaasDelete",
modalBtnText: "Yes, Delete",
itemId: data.id,
table1: "faq",
backTo: "/admin/faq",
},
});
}}
>
<span className="ml-2"> Delete</span>
</button>
</div>
</div>
<div className="py-2 px-4 bg-white bg-opacity-60 border-[1px] border-gray-200">
<div className="flex flex-wrap justify-between">
<div className="flex-1 p-2">
<div className={`${showAnswer ? " mb-4" : ""}`}>
<h4 className="text-lg font-semibold leading-normal">{data.question}</h4>
</div>
{showAnswer && (
<p
className="text-gray-600 font-medium sun-editor-editable"
dangerouslySetInnerHTML={{ __html: data.answer }}
></p>
)}
</div>
<div className="w-auto p-2">
{showAnswer ? (
<Icon
type="minus"
className="h-4 w-4 cursor-pointer"
onClick={() => setShowAnswer(!showAnswer)}
/>
) : (
<Icon
type="plus"
className="h-4 w-4 cursor-pointer"
onClick={() => setShowAnswer(!showAnswer)}
/>
)}
</div>
</div>
</div>
</div>
);
};
export default Faq;
+116
View File
@@ -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 (
<div className="mb-[34px]">
<Disclosure defaultOpen>
<div className="mb-[12px] flex justify-between">
<h4 className="flex w-full justify-between text-[16px] font-semibold lg:block">
<span className="lg:mr-2 lg:border-r lg:pr-2">{title}</span>
<button
className="text-sm font-normal lowercase lg:text-xs"
onClick={reset}
>
Clear
</button>
</h4>
<Disclosure.Button className="hidden duration-200 ui-open:rotate-180 lg:inline">
{" "}
<svg
width="14"
height="8"
viewBox="0 0 14 8"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M13 7L7 1L1 7"
stroke="#475467"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Disclosure.Button>
</div>
<Transition
as={Fragment}
enter="transition-all ease duration-500"
enterFrom="max-h-0"
enterTo="max-h-[900px]"
leave="transition-all ease duration-500"
leaveFrom="max-h-[900px]"
leaveTo="max-h-0"
>
<Disclosure.Panel className="overflow-hidden text-gray-500 duration-500">
{options.map((op, idx) => (
<div
className="checkbox-container flex gap-2 items-center mb-[12px]"
key={idx}
>
<input
type={`${name === "capacity" ? "radio" : "checkbox"}`}
id={op[valueField]}
value={op[valueField]}
className={`text-xl w-5 h-8 rounded ${name === "capacity" ? "accent-[#0D9895]" : ""}`}
name={name}
checked={ field.value.includes(op[valueField])}
onChange={() => {
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" ?
<label htmlFor={op[valueField]}>
{op[labelField]}{" "}
{title == "Reviews"
? Array(Number(op[valueField]))
.fill("")
.map((_, idx) => (
<span
className="ml-1"
key={idx}
>
<StarIcon />
</span>
))
: null}
</label>
:
<span >
{op[labelField]}{" "}
</span>
}
</div>
))}
</Disclosure.Panel>
</Transition>
</Disclosure>
</div>
);
}
+192
View File
@@ -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 (
<>
<PaginationHeader
currentPage={currentPage}
pageSize={pageSize}
totalNumber={dataTotal}
updatePageSize={updatePageSize}
/>
<div className="overflow-x-auto p-5 bg-white shadow rounded">
{data.map((data) => (
<div
key={data.id}
className="border rounded px-5 py-4 flex justify-between flex-col lg:flex-row mb-4"
>
<div>{ID_PREFIX.BOOKINGS + data.id}</div>
<img
src={data.image_url}
className="h-24 max-w-[135px]"
alt="property_image"
/>
<div className="min-w-[219px] max-w-[219px] mb-4">
<p className="font-semibold text-xl text-[#101828] mb-1">{data.property_name}</p>
<p className="text-xs font-medium mb-1">{data.space_category}</p>
<p className="bg-gray-200 text-xs p-2 w-fit rounded">{statusMapping.find((status) => status.key == data.status)?.value}</p>
</div>
<div className="min-w-[219px] max-w-[219px] mb-4">
<p className="text-xs mb-1 font-medium ">Host</p>
<p className="mb-1 text-sm">
{data.host_last_name}, {data.host_first_name}{" "}
</p>
<p className="text-xs mb-1 font-medium ">Customer</p>
<p className="mb-1 text-xs">
{data.customer_last_name}, {data.customer_first_name}{" "}
</p>
</div>
<div className="min-w-[72px] max-w-[72px] mb-4">
<p className="text-xs mb-1 font-medium ">Date</p>
<p className="mb-1 text-sm">{moment(data.booking_start_time).format("MM/DD/YY")} </p>
<p className="text-xs mb-1 font-medium ">Duration</p>
<p className="mb-1 text-xs">{secondsToHour(data.duration)} </p>
</div>
<div className="min-w-[72px] max-w-[72px] mb-4">
<p className="text-xs mb-1 font-medium ">Rate</p>
<p className="mb-1 text-sm">&#36;{data?.rate?.toFixed(2)} </p>
<p className="text-xs mb-1 font-medium ">Tax</p>
<p className="mb-1 text-xs">&#36;{data?.tax?.toFixed(2)}</p>
</div>
<div className="min-w-[72px] max-w-[72px] mb-4">
<p className="text-xs mb-1 font-medium ">Total</p>
<p className="mb-1 text-xs">&#36;{data?.total?.toFixed(2)} </p>
<p className="text-xs mb-1 font-medium ">Commission</p>
<p className="mb-1 text-xs">&#36;{data?.commission?.toFixed(2)}</p>
</div>
<Menu
as="div"
className="relative min-w-[60px] max-w-[60px] inline-block text-left"
>
<div className="">
<Menu.Button className="inline-flex justify-center rounded-md border border-gray-300 bg-white px-1 py-3 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-[#33D4B7] focus:ring-offset-2 focus:ring-offset-gray-100">
<Icon type="dots" />
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-10 mt-0 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1">
<Menu.Item>
{({ active }) => (
<button
onClick={() => navigate(`/admin/edit-booking/${data.id}`)}
className={`${active ? "bg-gray-100 text-gray-900" : "text-gray-700"} w-full text-left block px-4 py-2 text-sm`}
>
Edit
</button>
)}
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
</div>
))}
</div>
<PaginationBar
currentPage={currentPage}
pageCount={pageCount}
pageSize={pageSize}
totalNumber={dataTotal}
canPreviousPage={canPreviousPage}
canNextPage={canNextPage}
updatePageSize={updatePageSize}
previousPage={previousPage}
nextPage={nextPage}
/>
</>
);
};
export default History;
+140
View File
@@ -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 (
<div className={"popup-container flex items-center justify-center normal-case"}>
<div
className={`w-[510px] max-w-[80%] rounded-lg bg-white p-5 px-3 md:px-5`}
onClick={(e) => e.stopPropagation()}
>
<form
className=" w-full max-w-lg"
onSubmit={handleSubmit(onSubmit)}
>
<div className="mb-4 ">
<label
className="mb-2 block text-sm font-bold text-gray-700"
htmlFor="add_on_id"
>
Add-Ons
</label>
<input
type="text"
className="mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none"
{...register("name")}
placeholder="Addon Name"
/>
<p className="text-xs normal-case italic text-red-500">{errors.name?.message}</p>
</div>
<div className="mb-4 ">
<label
className="mb-2 block text-sm font-bold text-gray-700"
htmlFor="add_on_cost"
>
Add-On Cost
</label>
<input
type="number"
className="mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none"
{...register("cost")}
placeholder="Addon Cost"
/>
<p className="text-xs normal-case italic text-red-500">{errors.cost?.message}</p>
</div>
<div className="flex justify-between">
<button
onClick={() =>setAddOnModal(false)}
className="mb-1 flex-1 rounded border border-[#667085] !bg-gradient-to-r px-6 py-2 text-sm font-semibold text-[#667085] outline-none focus:outline-none"
>
Cancel
</button>
<LoadingButton
loading={loading}
type="submit"
className={`ml-5 mb-1 flex-1 rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none ${loading ? "py-1" : "py-2"}`}>
Save
</LoadingButton>
</div>
</form>
</div>
</div>
);
}
+120
View File
@@ -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 (
<div className={"popup-container flex items-center justify-center normal-case"}>
<div
className={`w-[510px] max-w-[80%] rounded-lg bg-white p-5 px-3 md:px-5`}
onClick={(e) => e.stopPropagation()}
>
<form
className=" w-full max-w-lg"
onSubmit={handleSubmit(onSubmit)}
>
<div className="mb-4 ">
<label
className="mb-2 block text-sm font-bold text-gray-700"
htmlFor="add_on_id"
>
Amenity
</label>
<input
type="text"
className="mb-3 w-full rounded border py-2 px-3 leading-tight text-gray-700 focus:outline-none"
{...register("name")}
placeholder="Amenity Name"
/>
<p className="text-xs normal-case italic text-red-500">{errors.name?.message}</p>
</div>
<div className="flex justify-between">
<button
onClick={() =>setAmenityModal(false)}
className="mb-1 flex-1 rounded border border-[#667085] !bg-gradient-to-r px-6 py-2 text-sm font-semibold text-[#667085] outline-none focus:outline-none"
>
Cancel
</button>
<LoadingButton
loading={loading}
type="submit"
className={`ml-5 mb-1 flex-1 rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-6 py-2 text-sm font-semibold text-white outline-none focus:outline-none ${loading ? "py-1" : "py-2"}`}>
Save
</LoadingButton>
</div>
</form>
</div>
</div>
);
}
+131
View File
@@ -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 (
<>
<Transition
appear
show={modalOpen && gettingStarted == 0}
as={Fragment}
>
<Dialog
as="div"
className="relative z-10"
onClose={() => setModalOpen(false)}
>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-25" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900"
>
First time login?
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">Would you like a tour of the site?</p>
</div>
<div className="mt-4 flex justify-end gap-4">
<button
type="button"
className="inline-flex justify-center rounded-md border px-4 py-2 text-sm font-medium focus:outline-none"
onClick={() => {
setModalOpen(false);
markAsNotFirstTimeUser();
}}
>
No thanks
</button>
<button
type="button"
className={`login-btn-gradient inline-flex justify-center rounded-md py-2 px-4 text-sm font-medium text-white`}
onClick={() => {
setTour()
}}
>
Yes please
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
</>
);
}
+149
View File
@@ -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 (
<header
className={`fixed top-0 left-0 z-50 flex w-screen flex-wrap items-center justify-between py-4 px-4 text-sm duration-500 md:flex-nowrap md:rounded-br-[32px] md:rounded-bl-[32px] md:px-12 header-${variant}`}
>
<nav className={`flex gap-6`}>
<Link
to="/"
className=""
>
<LogoIcon fill={variant == "transparent" || variant == "light" ? undefined : "#101828"} />
</Link>
</nav>
<StaticSearchBar className="hidden md:block" />
<div className="flex">
<nav className={`z-50 inline `}>
{" "}
{pathname.startsWith("/account") && (
<button
className={`self-stretch rounded-sm border px-6 py-[5px] pb-[7px] ${variant == "transparent" ? "" : "border-white"}`}
onClick={()=>navigate("/search?location=&booking_start_time=&max_capacity=&capacity=&size=")}
>
<span>Explore Spaces</span>
</button>
)}
</nav>
<nav className="hidden items-center gap-6 md:flex">
{["/spaces/add/4", "/spaces/add/5"].includes(pathname) || !pathname.startsWith("/spaces") ? (
<>
{" "}
<Link
to="/contact-us"
className={`self-stretch rounded-md px-6 py-[5px] pb-[7px] font-normal my-border-${variant}`}
>
<span>Support</span>
</Link>
<NavMenu variant={variant} />
</>
) : (
<button
className={`self-stretch rounded-sm border px-6 py-[5px] pb-[7px] ${variant == "transparent" ? "" : "border-white"}`}
onClick={saveAsDraft}
>
<span>Save as draft</span>
</button>
)}
</nav>
</div>
<nav className={`z-50 inline md:hidden`}>
{" "}
{pathname.startsWith("/spaces") && pathname != "/spaces/add/5" && pathname != "/spaces/add/4" ? (
<button
className={`self-stretch rounded-sm border px-6 py-[5px] pb-[7px] ${variant == "transparent" ? "" : "border-white"}`}
onClick={saveAsDraft}
>
<span>Save as draft</span>
</button>
) : (
<NavMenu variant={variant} />
)}
</nav>
<StaticSearchBar className="flex w-full justify-center py-4 md:hidden" />
</header>
);
};
export default HostHeader;
+24
View File
@@ -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 <ArrowNarrowRight
id={id}
className={`${className || ""}`}
onClick={onClick}
onKeyUp={onKeyUp}
/>
case "narrow-left": return <ArrowNarrowLeft
id={id}
className={`${className || ""}`}
onClick={onClick}
onKeyUp={onKeyUp}
/>
}
}
export default ArrowSvg
+16
View File
@@ -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 <BankNoteOne
id={id}
className={`${className || ""}`}
onClick={onClick}
onKeyUp={onKeyUp}
/>
}
}
export default BankNoteSvg
+16
View File
@@ -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 <BuildingOne
id={id}
className={`${className || ""}`}
onClick={onClick}
onKeyUp={onKeyUp}
/>
}
}
export default BuildingSvg
+16
View File
@@ -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 <Calender
id={id}
className={`${className || ""}`}
onClick={onClick}
onKeyUp={onKeyUp}
/>
}
}
export default CalenderSvg
+16
View File
@@ -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 <ChevronDown
id={id}
className={`${className || ""}`}
onClick={onClick}
onKeyUp={onKeyUp}
/>
}
}
export default CalenderSvg

Some files were not shown because too many files have changed in this diff Show More