Initial Q-Shop Commit in its own repo
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
*.zip
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
10
.prettierrc.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"arrowParens": "avoid",
|
||||
"tabWidth": 2,
|
||||
"semi": true
|
||||
}
|
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Q-Shop</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
6895
package-lock.json
generated
Normal file
54
package.json
Normal file
@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "q-blog",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@mui/icons-material": "^5.11.11",
|
||||
"@mui/material": "^5.11.13",
|
||||
"@reduxjs/toolkit": "^1.9.3",
|
||||
"@types/react-grid-layout": "^1.3.2",
|
||||
"axios": "^1.3.4",
|
||||
"compressorjs": "^1.2.1",
|
||||
"localforage": "^1.10.0",
|
||||
"moment": "^2.29.4",
|
||||
"philliplm-react-modern-audio-player": "^1.4.6",
|
||||
"react": "^18.2.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dnd": "^16.0.1",
|
||||
"react-dnd-html5-backend": "^16.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-grid-layout": "^1.3.4",
|
||||
"react-intersection-observer": "^9.4.3",
|
||||
"react-joyride": "^2.5.4",
|
||||
"react-masonry-css": "^1.0.16",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-resize-detector": "^8.0.4",
|
||||
"react-router-dom": "^6.9.0",
|
||||
"react-toastify": "^9.1.2",
|
||||
"react-virtuoso": "^4.3.3",
|
||||
"short-unique-id": "^4.4.4",
|
||||
"slate": "^0.91.4",
|
||||
"slate-history": "^0.86.0",
|
||||
"slate-react": "^0.91.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mui/types": "^7.2.3",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-copy-to-clipboard": "^5.0.4",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@vitejs/plugin-react-swc": "^3.2.0",
|
||||
"prettier": "^2.8.6",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^4.2.0",
|
||||
"worker-loader": "^3.0.8"
|
||||
}
|
||||
}
|
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
48
src/App.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
// @ts-nocheck
|
||||
import { useEffect, useState } from "react";
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import { ProductPage } from "./pages/Product/ProductPage";
|
||||
import { StoreList } from "./pages/StoreList/StoreList";
|
||||
import { ThemeProvider } from "@mui/material/styles";
|
||||
import { CssBaseline } from "@mui/material";
|
||||
import { lightTheme, darkTheme } from "./styles/theme";
|
||||
import { store } from "./state/store";
|
||||
import { Provider } from "react-redux";
|
||||
import { Store } from "./pages/Store/Store/Store";
|
||||
import { MyOrders } from "./pages/MyOrders/MyOrders";
|
||||
import { ErrorElement } from "./components/common/Error/ErrorElement";
|
||||
import GlobalWrapper from "./wrappers/GlobalWrapper";
|
||||
import Notification from "./components/common/Notification/Notification";
|
||||
import { ProductManager } from "./pages/ProductManager/ProductManager";
|
||||
|
||||
function App() {
|
||||
// const themeColor = window._qdnTheme
|
||||
|
||||
const [theme, setTheme] = useState("dark");
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<ThemeProvider theme={theme === "light" ? lightTheme : darkTheme}>
|
||||
<Notification />
|
||||
<GlobalWrapper setTheme={(val: string) => setTheme(val)}>
|
||||
<CssBaseline />
|
||||
<Routes>
|
||||
<Route
|
||||
path="/:user/:store/:product/:catalogue"
|
||||
element={<ProductPage />}
|
||||
/>
|
||||
<Route
|
||||
path="/product-manager/:store"
|
||||
element={<ProductManager />}
|
||||
/>
|
||||
<Route path="/my-orders" element={<MyOrders />} />
|
||||
<Route path="/:user/:store" element={<Store />} />
|
||||
<Route path="/" element={<StoreList />} />
|
||||
</Routes>
|
||||
</GlobalWrapper>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
BIN
src/assets/img/ArrrLogoBlack.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
src/assets/img/ArrrLogoWhite.png
Normal file
After Width: | Height: | Size: 9.0 KiB |
BIN
src/assets/img/Q-AppsLogo.webp
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
src/assets/img/QShopLogo.webp
Normal file
After Width: | Height: | Size: 7.0 KiB |
BIN
src/assets/img/QShopLogoLight.webp
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
src/assets/img/arrr.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/img/btc.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/assets/img/dgb.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
src/assets/img/doge.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src/assets/img/ltc.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/img/qort.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/assets/img/rvn.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
34
src/assets/svgs/ARRRSVG.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const ARRRSVG: React.FC<IconTypes> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 2000 2000"
|
||||
style={{ width, height }}
|
||||
xmlSpace="preserve"
|
||||
>
|
||||
<linearGradient
|
||||
id="SVGID_1_"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1="0"
|
||||
y1="-2"
|
||||
x2="2000"
|
||||
y2="-2"
|
||||
gradientTransform="matrix(1 0 0 1 0 1002)"
|
||||
>
|
||||
<stop offset="0"></stop>
|
||||
<stop offset="1"></stop>
|
||||
</linearGradient>
|
||||
<path
|
||||
fill={color}
|
||||
d="M1000,0C447.6,0,0,447.6,0,1000s447.6,1000,1000,1000s1000-447.6,1000-1000S1552.4,0,1000,0z M548.6,741.5 c0-123.6,100.2-223.1,224.6-223.1h512.4c58.6,0,114.9,23,156.7,64.1l-262.2,131.9h-361c-40.7,0-73.1,29.4-73.1,64.8v160.5 l-196.7,102.5V741.5L548.6,741.5z M1507.9,1075.4c0,123.6-100.2,223.1-223.1,223.1H745.3v190.7c0,32.4-24.1,58.8-52.8,65.6 l-67.1,1.5h-76.9v-331.6l257-134.1h431.8c40.7,0,74.6-29.4,74.6-65.6V828.9l195.9-98.7L1507.9,1075.4L1507.9,1075.4z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
};
|
25
src/assets/svgs/AccountCircleSVG.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
interface AccountCircleSVGProps {
|
||||
color: string
|
||||
height: string
|
||||
width: string
|
||||
}
|
||||
|
||||
export const AccountCircleSVG: React.FC<AccountCircleSVGProps> = ({
|
||||
color,
|
||||
height,
|
||||
width
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path
|
||||
fill={color}
|
||||
d="M222 801q63-44 125-67.5T480 710q71 0 133.5 23.5T739 801q44-54 62.5-109T820 576q0-145-97.5-242.5T480 236q-145 0-242.5 97.5T140 576q0 61 19 116t63 109Zm257.814-195Q422 606 382.5 566.314q-39.5-39.686-39.5-97.5t39.686-97.314q39.686-39.5 97.5-39.5t97.314 39.686q39.5 39.686 39.5 97.5T577.314 566.5q-39.686 39.5-97.5 39.5Zm.654 370Q398 976 325 944.5q-73-31.5-127.5-86t-86-127.266Q80 658.468 80 575.734T111.5 420.5q31.5-72.5 86-127t127.266-86q72.766-31.5 155.5-31.5T635.5 207.5q72.5 31.5 127 86t86 127.032q31.5 72.532 31.5 155T848.5 731q-31.5 73-86 127.5t-127.032 86q-72.532 31.5-155 31.5ZM480 916q55 0 107.5-16T691 844q-51-36-104-55t-107-19q-54 0-107 19t-104 55q51 40 103.5 56T480 916Zm0-370q34 0 55.5-21.5T557 469q0-34-21.5-55.5T480 392q-34 0-55.5 21.5T403 469q0 34 21.5 55.5T480 546Zm0-77Zm0 374Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
15
src/assets/svgs/AddSVG.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const AddSVG: React.FC<IconTypes> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 -960 960 960"
|
||||
width={width}
|
||||
fill={color}
|
||||
>
|
||||
<path d="M450-280h60v-170h170v-60H510v-170h-60v170H280v60h170v170ZM180-120q-24 0-42-18t-18-42v-600q0-24 18-42t42-18h600q24 0 42 18t18 42v600q0 24-18 42t-42 18H180Zm0-60h600v-600H180v600Zm0-600v600-600Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
21
src/assets/svgs/AlignCenterSVG.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { SVGProps } from './interfaces'
|
||||
|
||||
export const AlignCenterSVG: React.FC<SVGProps> = ({
|
||||
color,
|
||||
height,
|
||||
width
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
height={height}
|
||||
width={width}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 96 960 960"
|
||||
>
|
||||
<path
|
||||
fill={color}
|
||||
d="M150 936q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 876h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 936H150Zm164-165q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T314 711h333q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T647 771H314ZM150 606q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 546h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 606H150Zm164-165q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T314 381h333q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T647 441H314ZM150 276q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 216h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 276H150Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
17
src/assets/svgs/AlignLeftSVG.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { SVGProps } from './interfaces'
|
||||
|
||||
export const AlignLeftSVG: React.FC<SVGProps> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path
|
||||
fill={color}
|
||||
d="M150 771q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 711h412q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T562 771H150Zm0-330q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 381h412q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T562 441H150Zm0 165q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 546h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 606H150Zm0 330q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 876h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 936H150Zm0-660q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 216h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 276H150Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
17
src/assets/svgs/AlignRightSVG.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { SVGProps } from './interfaces'
|
||||
|
||||
export const AlignRightSVG: React.FC<SVGProps> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path
|
||||
fill={color}
|
||||
d="M150 936q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 876h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 936H150Zm249-165q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T399 711h411q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 771H399ZM150 606q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 546h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 606H150Zm249-165q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T399 381h411q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 441H399ZM150 276q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 216h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 276H150Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
21
src/assets/svgs/BackArrowSVG.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const BackArrowSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill={color}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path d="M480 896 160 576l320-320 42 42-248 248h526v60H274l248 248-42 42Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
17
src/assets/svgs/BoldSVG.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { SVGProps } from './interfaces'
|
||||
|
||||
export const BoldSVG: React.FC<SVGProps> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path
|
||||
fill={color}
|
||||
d="M335 856q-25 0-42.5-17.5T275 796V356q0-25 17.5-42.5T335 296h168q66 0 114.5 42T666 444q0 38-21 70t-56 49v6q43 14 69.5 50t26.5 81q0 68-52.5 112T510 856H335Zm26-76h144q38 0 66-25t28-63q0-37-28-62t-66-25H361v175Zm0-247h136q35 0 60.5-23t25.5-58q0-35-25.5-58.5T497 370H361v163Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
21
src/assets/svgs/BriefcaseSVG.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const BriefcaseSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
fill={color}
|
||||
height={height}
|
||||
width={width}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
>
|
||||
<path d="M140-180v-21.75V-180v-480 480Zm0 60q-24 0-42-18t-18-42v-480q0-24 18-42t42-18h180v-100q0-23 18-41.5t42-18.5h200q24 0 42 18.5t18 41.5v100h180q24 0 42 18t18 42v225q-14-11-28.5-20T820-472v-188H140v480h334q4 16 10 31t14 29H140Zm240-600h200v-100H380v100ZM720-47q-79 0-136-57t-57-136q0-79 57-136t136-57q79 0 136 57t57 136q0 79-57 136T720-47Zm0-79 113-113-21-21-77 77v-171h-30v171l-77-77-21 21 113 113Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
23
src/assets/svgs/CalendarSVG.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
interface AccountCircleSVGProps {
|
||||
color: string;
|
||||
height: string;
|
||||
width: string;
|
||||
}
|
||||
|
||||
export const CalendarSVG: React.FC<AccountCircleSVGProps> = ({
|
||||
color,
|
||||
height,
|
||||
width
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
fill={color}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 -960 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path d="M180-80q-24 0-42-18t-18-42v-620q0-24 18-42t42-18h65v-60h65v60h340v-60h65v60h65q24 0 42 18t18 42v620q0 24-18 42t-42 18H180Zm0-60h600v-430H180v430Zm0-490h600v-130H180v130Zm0 0v-130 130Zm300 230q-17 0-28.5-11.5T440-440q0-17 11.5-28.5T480-480q17 0 28.5 11.5T520-440q0 17-11.5 28.5T480-400Zm-160 0q-17 0-28.5-11.5T280-440q0-17 11.5-28.5T320-480q17 0 28.5 11.5T360-440q0 17-11.5 28.5T320-400Zm320 0q-17 0-28.5-11.5T600-440q0-17 11.5-28.5T640-480q17 0 28.5 11.5T680-440q0 17-11.5 28.5T640-400ZM480-240q-17 0-28.5-11.5T440-280q0-17 11.5-28.5T480-320q17 0 28.5 11.5T520-280q0 17-11.5 28.5T480-240Zm-160 0q-17 0-28.5-11.5T280-280q0-17 11.5-28.5T320-320q17 0 28.5 11.5T360-280q0 17-11.5 28.5T320-240Zm320 0q-17 0-28.5-11.5T600-280q0-17 11.5-28.5T640-320q17 0 28.5 11.5T680-280q0 17-11.5 28.5T640-240Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
27
src/assets/svgs/CancelSVG.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
interface CancelSVGProps extends IconTypes {
|
||||
onMouseDownFunc?: (e: any) => void;
|
||||
}
|
||||
|
||||
export const CancelSVG: React.FC<CancelSVGProps> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
onMouseDownFunc
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
onMouseDown={onMouseDownFunc}
|
||||
height={height}
|
||||
width={width}
|
||||
fill={color}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
>
|
||||
<path d="m330-288 150-150 150 150 42-42-150-150 150-150-42-42-150 150-150-150-42 42 150 150-150 150 42 42ZM480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
23
src/assets/svgs/CartSVG.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const CartSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
onClickFunc
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill={color}
|
||||
height={height}
|
||||
width={width}
|
||||
onClick={onClickFunc}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
>
|
||||
<path d="M286.788-81Q257-81 236-102.212q-21-21.213-21-51Q215-183 236.212-204q21.213-21 51-21Q317-225 338-203.788q21 21.213 21 51Q359-123 337.788-102q-21.213 21-51 21Zm400 0Q657-81 636-102.212q-21-21.213-21-51Q615-183 636.212-204q21.213-21 51-21Q717-225 738-203.788q21 21.213 21 51Q759-123 737.788-102q-21.213 21-51 21ZM235-741l110 228h288l125-228H235Zm-30-60h589.074q22.964 0 34.945 21Q841-759 829-738L694-495q-11 19-28.559 30.5Q647.881-453 627-453H324l-56 104h491v60H277q-42 0-60.5-28t.5-63l64-118-152-322H51v-60h117l37 79Zm140 288h288-288Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
21
src/assets/svgs/CategorySVG.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const CategorySVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
fill={color}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 -960 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path d="m261-526 220-354 220 354H261ZM706-80q-74 0-124-50t-50-124q0-74 50-124t124-50q74 0 124 50t50 124q0 74-50 124T706-80Zm-586-25v-304h304v304H120Zm586.085-35Q754-140 787-173.085q33-33.084 33-81Q820-302 786.916-335q-33.085-33-81.001-33Q658-368 625-334.915q-33 33.084-33 81Q592-206 625.084-173q33.085 33 81.001 33ZM180-165h184v-184H180v184Zm189-421h224L481-767 369-586Zm112 0ZM364-349Zm342 95Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
17
src/assets/svgs/CodeBlockSVG.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { SVGProps } from './interfaces'
|
||||
|
||||
export const CodeBlockSVG: React.FC<SVGProps> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path
|
||||
fill={color}
|
||||
d="m330 576 70-70q9-9 9-22t-9-22q-9-9-21.833-9-12.834 0-22.167 9l-93 93q-5 5-7 10.133-2 5.134-2 11Q254 582 256 587q2 5 7 10l94 94q9.333 9 22.167 9Q392 700 401 691q9-9 9-22t-9-22l-71-71Zm300 0-71 71q-9 9-9 22t9 22q9 9 21.833 9 12.834 0 22.167-9l94-94q5-5 7-10.133 2-5.134 2-11Q706 570 704 565q-2-5-7-10l-94-94q-4-5-10-7t-12-2q-6 0-11.5 2t-10.167 6.8Q550 470.4 550 483.2q0 12.8 9 21.8l71 71ZM180 936q-24 0-42-18t-18-42V276q0-24 18-42t42-18h600q24 0 42 18t18 42v600q0 24-18 42t-42 18H180Zm0-60h600V276H180v600Zm0-600v600-600Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
19
src/assets/svgs/CompareArrowsSVG.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const CompareArrowsSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
fill={color}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 -960 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path d="m320-160-56-57 103-103H80v-80h287L264-503l56-57 200 200-200 200Zm320-240L440-600l200-200 56 57-103 103h287v80H593l103 103-56 57Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
15
src/assets/svgs/CurrencySVG.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const CurrencySVG: React.FC<IconTypes> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
fill={color}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 -960 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path d="M480-40q-112 0-206-51T120-227v107H40v-240h240v80h-99q48 72 126.5 116T480-120q75 0 140.5-28.5t114-77q48.5-48.5 77-114T840-480h80q0 91-34.5 171T791-169q-60 60-140 94.5T480-40Zm-36-160v-52q-47-11-76.5-40.5T324-370l66-26q12 41 37.5 61.5T486-314q33 0 56.5-15.5T566-378q0-29-24.5-47T454-466q-59-21-86.5-50T340-592q0-41 28.5-74.5T446-710v-50h70v50q36 3 65.5 29t40.5 61l-64 26q-8-23-26-38.5T482-648q-35 0-53.5 15T410-592q0 26 23 41t83 35q72 26 96 61t24 77q0 29-10 51t-26.5 37.5Q583-274 561-264.5T514-250v50h-70ZM40-480q0-91 34.5-171T169-791q60-60 140-94.5T480-920q112 0 206 51t154 136v-107h80v240H680v-80h99q-48-72-126.5-116T480-840q-75 0-140.5 28.5t-114 77q-48.5 48.5-77 114T120-480H40Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
23
src/assets/svgs/DarkModeSVG.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { IconTypes } from './IconTypes'
|
||||
|
||||
export const DarkModeSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
onClickFunc
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
onClick={onClickFunc}
|
||||
fill={color}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path d="M480 936q-150 0-255-105T120 576q0-150 105-255t255-105q8 0 17 .5t23 1.5q-36 32-56 79t-20 99q0 90 63 153t153 63q52 0 99-18.5t79-51.5q1 12 1.5 19.5t.5 14.5q0 150-105 255T480 936Zm0-60q109 0 190-67.5T771 650q-25 11-53.667 16.5Q688.667 672 660 672q-114.689 0-195.345-80.655Q384 510.689 384 396q0-24 5-51.5t18-62.5q-98 27-162.5 109.5T180 576q0 125 87.5 212.5T480 876Zm-4-297Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
21
src/assets/svgs/DescriptionSVG.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const DescriptionSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
fill={color}
|
||||
height={height}
|
||||
width={width}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
>
|
||||
<path d="M319-250h322v-60H319v60Zm0-170h322v-60H319v60ZM220-80q-24 0-42-18t-18-42v-680q0-24 18-42t42-18h361l219 219v521q0 24-18 42t-42 18H220Zm331-554v-186H220v680h520v-494H551ZM220-820v186-186 680-680Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
21
src/assets/svgs/DialogsSVG.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const DialogsSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
height={height}
|
||||
width={width}
|
||||
fill={color}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
>
|
||||
<path d="M299.003-299.003h361.994v-361.994H299.003v361.994ZM197.694-140.001q-23.529 0-40.611-17.082-17.082-17.082-17.082-40.611v-564.612q0-23.529 17.082-40.611 17.082-17.082 40.611-17.082h564.612q23.529 0 40.611 17.082 17.082 17.082 17.082 40.611v564.612q0 23.529-17.082 40.611-17.082 17.082-40.611 17.082H197.694Zm0-45.384h564.612q4.616 0 8.463-3.846 3.846-3.847 3.846-8.463v-564.612q0-4.616-3.846-8.463-3.847-3.846-8.463-3.846H197.694q-4.616 0-8.463 3.846-3.846 3.847-3.846 8.463v564.612q0 4.616 3.846 8.463 3.847 3.846 8.463 3.846Zm-12.309-589.23V-185.385-774.615Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
24
src/assets/svgs/DoubleArrowDownSVG.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const DoubleArrowDownSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
id,
|
||||
onClickFunc
|
||||
}) => {
|
||||
return (
|
||||
<div className={className} id={id} onClick={onClickFunc}>
|
||||
<svg
|
||||
fill={color}
|
||||
height={height}
|
||||
width={width}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
>
|
||||
<path d="M480-200 240-440l42-42 198 198 198-198 42 42-240 240Zm0-253L240-693l42-42 198 198 198-198 42 42-240 240Z" />
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
21
src/assets/svgs/DownloadSVG.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const DownloadSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 -960 960 960"
|
||||
width={width}
|
||||
fill={color}
|
||||
>
|
||||
<path d="M480-320 280-520l56-58 104 104v-326h80v326l104-104 56 58-200 200ZM240-160q-33 0-56.5-23.5T160-240v-120h80v120h480v-120h80v120q0 33-23.5 56.5T720-160H240Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
22
src/assets/svgs/ExpandMoreSVG.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
export const ExpandMoreSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
onClickFunc
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
onClick={onClickFunc}
|
||||
height={height}
|
||||
width={width}
|
||||
fill={color}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
>
|
||||
<path d="M480-345 240-585l43-43 197 198 197-197 43 43-240 239Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
22
src/assets/svgs/GarbageSVG.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
export const GarbageSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
onClickFunc
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
onClick={onClickFunc}
|
||||
height={height}
|
||||
width={width}
|
||||
fill={color}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
>
|
||||
<path d="M261-120q-24.75 0-42.375-17.625T201-180v-570h-41v-60h188v-30h264v30h188v60h-41v570q0 24-18 42t-42 18H261Zm438-630H261v570h438v-570ZM367-266h60v-399h-60v399Zm166 0h60v-399h-60v399ZM261-750v570-570Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
17
src/assets/svgs/H2SVG.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { SVGProps } from './interfaces'
|
||||
|
||||
export const H2SVG: React.FC<SVGProps> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path
|
||||
fill={color}
|
||||
d="M149.825 776Q137 776 128.5 767.375T120 746V406q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T180 406v140h180V406q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T420 406v340q0 12.75-8.675 21.375-8.676 8.625-21.5 8.625-12.825 0-21.325-8.625T360 746V606H180v140q0 12.75-8.675 21.375-8.676 8.625-21.5 8.625ZM570 776q-12.75 0-21.375-8.625T540 746V606q0-24.75 17.625-42.375T600 546h180V436H570q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T570 376h210q24.75 0 42.375 17.625T840 436v110q0 24.75-17.625 42.375T780 606H600v110h210q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 776H570Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
17
src/assets/svgs/H3SVG.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { SVGProps } from './interfaces'
|
||||
|
||||
export const H3SVG: React.FC<SVGProps> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path
|
||||
fill={color}
|
||||
d="M149.825 776Q137 776 128.5 767.375T120 746V406q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T180 406v140h180V406q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T420 406v340q0 12.75-8.675 21.375-8.676 8.625-21.5 8.625-12.825 0-21.325-8.625T360 746V606H180v140q0 12.75-8.675 21.375-8.676 8.625-21.5 8.625ZM570 776q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T570 716h210V606H650q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T650 546h130V436H570q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T570 376h210q24.75 0 42.375 17.625T840 436v280q0 24.75-17.625 42.375T780 776H570Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
8
src/assets/svgs/IconTypes.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface IconTypes {
|
||||
color: string;
|
||||
height: string;
|
||||
width: string;
|
||||
className?: string;
|
||||
onClickFunc?: ((e: React.MouseEvent<any>) => void) | ((params?: any) => void);
|
||||
id?: string;
|
||||
}
|
17
src/assets/svgs/ItalicSVG.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { SVGProps } from './interfaces'
|
||||
|
||||
export const ItalicSVG: React.FC<SVGProps> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path
|
||||
fill={color}
|
||||
d="M264 857q-16.8 0-28.4-11.641-11.6-11.641-11.6-28.5t11.6-28.359Q247.2 777 264 777h94l139-409H378q-16.8 0-28.4-11.641-11.6-11.641-11.6-28.5t11.6-28.359Q361.2 288 378 288h300q16.8 0 28.4 11.641 11.6 11.641 11.6 28.5T706.4 356.5Q694.8 368 678 368h-94L445 777h119q16.8 0 28.4 11.641 11.6 11.641 11.6 28.5T592.4 845.5Q580.8 857 564 857H264Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
23
src/assets/svgs/LightModeSVG.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { IconTypes } from './IconTypes'
|
||||
|
||||
export const LightModeSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
onClickFunc
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
onClick={onClickFunc}
|
||||
fill={color}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path d="M479.765 716Q538 716 579 675.235q41-40.764 41-99Q620 518 579.235 477q-40.764-41-99-41Q422 436 381 476.765q-41 40.764-41 99Q340 634 380.765 675q40.764 41 99 41Zm.235 60q-83 0-141.5-58.5T280 576q0-83 58.5-141.5T480 376q83 0 141.5 58.5T680 576q0 83-58.5 141.5T480 776ZM70 606q-12.75 0-21.375-8.675Q40 588.649 40 575.825 40 563 48.625 554.5T70 546h100q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T170 606H70Zm720 0q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T790 546h100q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T890 606H790ZM479.825 296Q467 296 458.5 287.375T450 266V166q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T510 166v100q0 12.75-8.675 21.375-8.676 8.625-21.5 8.625Zm0 720q-12.825 0-21.325-8.62-8.5-8.63-8.5-21.38V886q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T510 886v100q0 12.75-8.675 21.38-8.676 8.62-21.5 8.62ZM240 378l-57-56q-9-9-8.629-21.603.37-12.604 8.526-21.5 8.896-8.897 21.5-8.897Q217 270 226 279l56 57q8 9 8 21t-8 20.5q-8 8.5-20.5 8.5t-21.5-8Zm494 495-56-57q-8-9-8-21.375T678.5 774q8.5-9 20.5-9t21 9l57 56q9 9 8.629 21.603-.37 12.604-8.526 21.5-8.896 8.897-21.5 8.897Q743 882 734 873Zm-56-495q-9-9-9-21t9-21l56-57q9-9 21.603-8.629 12.604.37 21.5 8.526 8.897 8.896 8.897 21.5Q786 313 777 322l-57 56q-8 8-20.364 8-12.363 0-21.636-8ZM182.897 873.103q-8.897-8.896-8.897-21.5Q174 839 183 830l57-56q8.8-9 20.9-9 12.1 0 20.709 9Q291 783 291 795t-9 21l-56 57q-9 9-21.603 8.629-12.604-.37-21.5-8.526ZM480 576Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
17
src/assets/svgs/LinkSVG.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { SVGProps } from './interfaces'
|
||||
|
||||
export const LinkSVG: React.FC<SVGProps> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path
|
||||
fill={color}
|
||||
d="M280 776q-85 0-142.5-57.5T80 576q0-85 57.5-142.5T280 376h140q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T420 436H280q-60 0-100 40t-40 100q0 60 40 100t100 40h140q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T420 776H280Zm75-170q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T355 546h250q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T605 606H355Zm185 170q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T540 716h140q60 0 100-40t40-100q0-60-40-100t-100-40H540q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T540 376h140q85 0 142.5 57.5T880 576q0 85-57.5 142.5T680 776H540Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
15
src/assets/svgs/LocationSVG.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const LocationSVG: React.FC<IconTypes> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
fill={color}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 -960 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path d="M480.089-490Q509-490 529.5-510.589q20.5-20.588 20.5-49.5Q550-589 529.411-609.5q-20.588-20.5-49.5-20.5Q451-630 430.5-609.411q-20.5 20.588-20.5 49.5Q410-531 430.589-510.5q20.588 20.5 49.5 20.5ZM480-159q133-121 196.5-219.5T740-552q0-117.79-75.292-192.895Q589.417-820 480-820t-184.708 75.105Q220-669.79 220-552q0 75 65 173.5T480-159Zm0 79Q319-217 239.5-334.5T160-552q0-150 96.5-239T480-880q127 0 223.5 89T800-552q0 100-79.5 217.5T480-80Zm0-480Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
21
src/assets/svgs/LoyaltySVG.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const LoyaltySVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
fill={color}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 -960 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path d="m524-262 140-140q11-11 16-24.5t5-28.5q0-32-22.5-54.5T608-532q-20 0-40 13t-44 42q-24-29-44-42t-40-13q-32 0-54.5 22.5T363-455q0 15 5 28.5t16 24.5l140 140Zm35 165q-18 18-43.5 18T472-97L97-472q-10-10-13.5-21T80-516v-304q0-26 17-43t43-17h304q12 0 24 3.5t22 13.5l373 373q19 19 19 44.5T863-401L559-97Zm-41-41 304-304-378-378H140v304l378 378ZM245-664q21 0 36.5-15.5T297-716q0-21-15.5-36.5T245-768q-21 0-36.5 15.5T193-716q0 21 15.5 36.5T245-664ZM140-820Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
23
src/assets/svgs/MinimizeSVG.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const MinimizeSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
onClickFunc
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
fill={color}
|
||||
className={className}
|
||||
onClick={onClickFunc}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 -960 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path d="M240-130v-60h481v60H240Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
23
src/assets/svgs/MinusCircle.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const MinusCircleSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
onClickFunc
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
onClick={onClickFunc}
|
||||
height={height}
|
||||
width={width}
|
||||
fill={color}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
>
|
||||
<path d="M280-453h400v-60H280v60ZM480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-156t86-127Q252-817 325-848.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 82-31.5 155T763-197.5q-54 54.5-127 86T480-80Zm0-60q142 0 241-99.5T820-480q0-142-99-241t-241-99q-141 0-240.5 99T140-480q0 141 99.5 240.5T480-140Zm0-340Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
25
src/assets/svgs/NewWindowSVG.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
interface NewWindowSVGProps {
|
||||
color: string
|
||||
height: string
|
||||
width: string
|
||||
}
|
||||
|
||||
export const NewWindowSVG: React.FC<NewWindowSVGProps> = ({
|
||||
color,
|
||||
height,
|
||||
width
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
width={width}
|
||||
viewBox="0 96 960 960"
|
||||
>
|
||||
<path
|
||||
d="M180 936q-24 0-42-18t-18-42V276q0-24 18-42t42-18h300v60H180v600h600V576h60v300q0 24-18 42t-42 18H180Zm480-420V396H540v-60h120V216h60v120h120v60H720v120h-60Z"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
21
src/assets/svgs/OrdersSVG.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const OrdersSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
fill={color}
|
||||
height={height}
|
||||
width={width}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
>
|
||||
<path d="M180-80q-24 0-42-18t-18-42v-530q0-24 18-42t42-18h110q0-79 53-134.5T475-920q79 0 137 55.575T670-730h110q24 0 42 18t18 42v530q0 24-18 42t-42 18H180Zm0-60h600v-530H180v530Zm300-290q79 0 137-58t58-137h-60q0 55-40 95t-95 40q-55 0-95-40t-40-95h-60q0 79 58 137t137 58ZM350-730h260q0-55-37.5-92.5T480-860q-55 0-92.5 37.5T350-730ZM180-140v-530 530Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
21
src/assets/svgs/OwnerSVG.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const OwnerSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
className={className}
|
||||
fill={color}
|
||||
width={width}
|
||||
viewBox="0 -960 960 960"
|
||||
>
|
||||
<path d="M684-381q-39.48 0-66.74-27.26Q590-435.52 590-475q0-39.48 27.26-66.74Q644.52-569 684-569q39.48 0 66.74 27.26Q778-514.48 778-475q0 39.48-27.26 66.74Q723.48-381 684-381ZM488-160v-51q0-26 11-44.5t31-28.5q37-19 75-28t79-9q41 0 79 8.5t75 28.5q20 9 31 28t11 45v51H488Zm-88-321q-66 0-108-42t-42-108q0-66 42-108t108-42q66 0 108 42t42 108q0 66-42 108t-108 42Zm0-150ZM80-160v-94q0-34 17-62.5t50.667-43.5Q215-390 276.5-405t123.245-15Q432-420 457-417t54 9l-25.5 25.5L460-357q-13-2-28-2.5t-32-.5q-56.627 0-110.814 11.5Q235-337 172-306q-14 7-23 22t-9 30v34h288v60H80Zm348-60Zm-28-321q39 0 64.5-25.5T490-631q0-39-25.5-64.5T400-721q-39 0-64.5 25.5T310-631q0 39 25.5 64.5T400-541Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
23
src/assets/svgs/PlusCircle.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const PlusCircleSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
onClickFunc
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
onClick={onClickFunc}
|
||||
height={height}
|
||||
width={width}
|
||||
fill={color}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
>
|
||||
<path d="M453-280h60v-166h167v-60H513v-174h-60v174H280v60h173v166Zm27.266 200q-82.734 0-155.5-31.5t-127.266-86q-54.5-54.5-86-127.341Q80-397.681 80-480.5q0-82.819 31.5-155.659Q143-709 197.5-763t127.341-85.5Q397.681-880 480.5-880q82.819 0 155.659 31.5Q709-817 763-763t85.5 127Q880-563 880-480.266q0 82.734-31.5 155.5T763-197.684q-54 54.316-127 86Q563-80 480.266-80Zm.234-60Q622-140 721-239.5t99-241Q820-622 721.188-721 622.375-820 480-820q-141 0-240.5 98.812Q140-622.375 140-480q0 141 99.5 240.5t241 99.5Zm-.5-340Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
46
src/assets/svgs/QortalSVG.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const QortalSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
fill={color}
|
||||
version="1.0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 695.000000 754.000000"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
>
|
||||
<g
|
||||
transform="translate(0.000000,754.000000) scale(0.100000,-0.100000)"
|
||||
stroke="none"
|
||||
>
|
||||
<path
|
||||
d="M3035 7289 c-374 -216 -536 -309 -1090 -629 -409 -236 -1129 -652
|
||||
-1280 -739 -82 -48 -228 -132 -322 -186 l-173 -100 0 -1882 0 -1883 38 -24
|
||||
c20 -13 228 -134 462 -269 389 -223 1779 -1026 2335 -1347 127 -73 268 -155
|
||||
314 -182 56 -32 95 -48 118 -48 33 0 207 97 991 552 l102 60 0 779 c0 428 -2
|
||||
779 -4 779 -3 0 -247 -140 -543 -311 -296 -170 -544 -308 -553 -306 -8 2 -188
|
||||
104 -400 226 -212 123 -636 368 -942 544 l-558 322 0 1105 c0 1042 1 1106 18
|
||||
1116 9 6 107 63 217 126 110 64 421 243 690 398 270 156 601 347 736 425 l247
|
||||
142 363 -210 c200 -115 551 -317 779 -449 228 -132 495 -286 594 -341 l178
|
||||
-102 -6 -1889 -6 -1888 23 14 c12 8 318 185 680 393 l657 379 0 1887 0 1886
|
||||
-77 46 c-43 25 -458 264 -923 532 -465 268 -1047 605 -1295 748 -646 373 -965
|
||||
557 -968 557 -1 0 -182 -104 -402 -231z"
|
||||
/>
|
||||
<path
|
||||
d="M3010 4769 c-228 -133 -471 -274 -540 -313 l-125 -72 0 -633 0 -632
|
||||
295 -171 c162 -94 407 -235 544 -315 137 -79 255 -142 261 -139 6 2 200 113
|
||||
431 247 230 133 471 272 534 308 l115 66 2 635 3 635 -536 309 c-294 169 -543
|
||||
310 -552 312 -9 2 -204 -105 -432 -237z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
15
src/assets/svgs/ShippingSVG.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const ShippingSVG: React.FC<IconTypes> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill={color}
|
||||
height={height}
|
||||
viewBox="0 -960 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path d="M224.118-161Q175-161 140.5-195.417 106-229.833 106-279H40v-461q0-24 18-42t42-18h579v167h105l136 181v173h-71q0 49.167-34.382 83.583Q780.235-161 731.118-161 682-161 647.5-195.417 613-229.833 613-279H342q0 49-34.382 83.5-34.383 34.5-83.5 34.5ZM224-221q24 0 41-17t17-41q0-24-17-41t-41-17q-24 0-41 17t-17 41q0 24 17 41t41 17ZM100-339h22q17-27 43.041-43 26.041-16 58-16t58.459 16.5Q308-365 325-339h294v-401H100v401Zm631 118q24 0 41-17t17-41q0-24-17-41t-41-17q-24 0-41 17t-17 41q0 24 17 41t41 17Zm-52-204h186L754-573h-75v148ZM360-529Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
21
src/assets/svgs/StarSVG.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const StarSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
fill={color}
|
||||
height={height}
|
||||
width={width}
|
||||
className={className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
>
|
||||
<path d="m233-80 65-281L80-550l288-25 112-265 112 265 288 25-218 189 65 281-247-149L233-80Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
23
src/assets/svgs/StorefrontSVG.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const StorefrontSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
onClickFunc
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
className={className}
|
||||
onClick={onClickFunc}
|
||||
fill={color}
|
||||
width={width}
|
||||
height={height}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
>
|
||||
<path d="M840-519v339q0 24-18 42t-42 18H179q-24 0-42-18t-18-42v-339q-28-24-37-59t2-70l43-135q8-27 28-42t46-15h553q28 0 49 15.5t29 41.5l44 135q11 35 1.5 70T840-519Zm-270-31q29 0 49-19t16-46l-25-165H510v165q0 26 17 45.5t43 19.5Zm-187 0q28 0 47.5-19t19.5-46v-165H350l-25 165q-4 26 14 45.5t44 19.5Zm-182 0q24 0 41.5-16.5T263-607l26-173H189l-46 146q-10 31 8 57.5t50 26.5Zm557 0q32 0 50.5-26t8.5-58l-46-146H671l26 173q3 24 20.5 40.5T758-550ZM179-180h601v-311q1 1-6.5 1H758q-25 0-47.5-10.5T666-533q-16 20-40 31.5T573-490q-30 0-51.5-8.5T480-527q-15 18-38 27.5t-52 9.5q-31 0-55-11t-41-32q-24 21-47 32t-46 11h-13.5q-6.5 0-8.5-1v311Zm601 0H179h601Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
23
src/assets/svgs/TimesSVG.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const TimesSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
onClickFunc
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
onClick={onClickFunc}
|
||||
className={className}
|
||||
fill={color}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 -960 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path d="m249-207-42-42 231-231-231-231 42-42 231 231 231-231 42 42-231 231 231 231-42 42-231-231-231 231Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
17
src/assets/svgs/UnderlineSVG.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { SVGProps } from './interfaces'
|
||||
|
||||
export const UnderlineSVG: React.FC<SVGProps> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path
|
||||
fill={color}
|
||||
d="M230 916q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T230 856h500q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T730 916H230Zm250-140q-100 0-156.5-58.5T267 559V257q0-16.882 12.527-28.941Q292.055 216 309.027 216 326 216 338 228.059T350 257v302q0 63 34 101t96 38q62 0 96-38t34-101V257q0-16.882 12.527-28.941Q635.055 216 652.027 216 669 216 681 228.059T693 257v302q0 100-56.5 158.5T480 776Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
15
src/assets/svgs/WarningSVG.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const WarningSVG: React.FC<IconTypes> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
fill={color}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path d="m40 936 440-760 440 760H40Zm104-60h672L480 296 144 876Zm340.175-57q12.825 0 21.325-8.675 8.5-8.676 8.5-21.5 0-12.825-8.675-21.325-8.676-8.5-21.5-8.5-12.825 0-21.325 8.675-8.5 8.676-8.5 21.5 0 12.825 8.675 21.325 8.676 8.5 21.5 8.5ZM454 708h60V484h-60v224Zm26-122Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
5
src/assets/svgs/interfaces.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface SVGProps {
|
||||
color: string
|
||||
height: string
|
||||
width: string
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import { styled } from '@mui/system';
|
||||
import {
|
||||
Box,
|
||||
Modal,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
|
||||
export const StyledModal = styled(Modal)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}))
|
||||
|
||||
export const ModalContent = styled(Box)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
padding: theme.spacing(4),
|
||||
borderRadius: theme.spacing(1),
|
||||
width: '40%',
|
||||
'&:focus': {
|
||||
outline: 'none'
|
||||
}
|
||||
}))
|
||||
|
||||
export const ModalText = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Raleway",
|
||||
fontSize: "25px",
|
||||
color: theme.palette.text.primary,
|
||||
}));
|
100
src/components/common/BlockedNamesModal/BlockedNamesModal.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Modal,
|
||||
Typography,
|
||||
SelectChangeEvent,
|
||||
ListItem,
|
||||
List,
|
||||
useTheme
|
||||
} from "@mui/material";
|
||||
import {
|
||||
StyledModal,
|
||||
ModalContent,
|
||||
ModalText
|
||||
} from "./BlockedNamesModal-styles";
|
||||
|
||||
interface PostModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const BlockedNamesModal: React.FC<PostModalProps> = ({
|
||||
open,
|
||||
onClose
|
||||
}) => {
|
||||
const [blockedNames, setBlockedNames] = useState<string[]>([]);
|
||||
const theme = useTheme();
|
||||
const getBlockedNames = React.useCallback(async () => {
|
||||
try {
|
||||
const listName = `blockedNames_q-blog`;
|
||||
const response = await qortalRequest({
|
||||
action: "GET_LIST_ITEMS",
|
||||
list_name: listName
|
||||
});
|
||||
setBlockedNames(response);
|
||||
} catch (error) {
|
||||
onClose();
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
getBlockedNames();
|
||||
}, [getBlockedNames]);
|
||||
|
||||
const removeFromBlockList = async (name: string) => {
|
||||
try {
|
||||
const response = await qortalRequest({
|
||||
action: "DELETE_LIST_ITEM",
|
||||
list_name: "blockedNames_q-blog",
|
||||
item: name
|
||||
});
|
||||
|
||||
if (response === true) {
|
||||
setBlockedNames((prev) => prev.filter((n) => n !== name));
|
||||
}
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledModal open={open} onClose={onClose}>
|
||||
<ModalContent>
|
||||
<ModalText>Manage blocked names</ModalText>
|
||||
<List
|
||||
sx={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flex: "1",
|
||||
overflow: "auto"
|
||||
}}
|
||||
>
|
||||
{blockedNames.map((name, index) => (
|
||||
<ListItem
|
||||
key={name + index}
|
||||
sx={{
|
||||
display: "flex"
|
||||
}}
|
||||
>
|
||||
<Typography>{name}</Typography>
|
||||
<Button
|
||||
sx={{
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
color: theme.palette.text.primary,
|
||||
fontFamily: "Raleway"
|
||||
}}
|
||||
onClick={() => removeFromBlockList(name)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
<Button variant="contained" color="primary" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalContent>
|
||||
</StyledModal>
|
||||
);
|
||||
};
|
@ -0,0 +1,18 @@
|
||||
import { styled } from "@mui/system";
|
||||
import { DialogTitle, DialogContentText } from "@mui/material";
|
||||
|
||||
export const DialogTitleStyled = styled(DialogTitle)(({ theme }) => ({
|
||||
fontFamily: "Merriweather Sans",
|
||||
fontSize: "18px",
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: "none"
|
||||
}));
|
||||
|
||||
export const DialogContentTextStyled = styled(DialogContentText)(
|
||||
({ theme }) => ({
|
||||
fontFamily: "Karla",
|
||||
fontSize: "16px",
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: "none"
|
||||
})
|
||||
);
|
@ -0,0 +1,50 @@
|
||||
import React from "react";
|
||||
import { Dialog, DialogActions, DialogContent, Button } from "@mui/material";
|
||||
import {
|
||||
DialogContentTextStyled,
|
||||
DialogTitleStyled
|
||||
} from "./ConfirmationModal-styles";
|
||||
import {
|
||||
CancelButton,
|
||||
CreateButton
|
||||
} from "../../modals/CreateStoreModal-styles";
|
||||
|
||||
export interface ModalProps {
|
||||
open: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
handleConfirm: () => void;
|
||||
handleCancel: () => void;
|
||||
}
|
||||
|
||||
const ConfirmationModal: React.FC<ModalProps> = ({
|
||||
open,
|
||||
title,
|
||||
message,
|
||||
handleConfirm,
|
||||
handleCancel
|
||||
}) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleCancel}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitleStyled id="alert-dialog-title">{title}</DialogTitleStyled>
|
||||
<DialogContent>
|
||||
<DialogContentTextStyled id="alert-dialog-description">
|
||||
{message}
|
||||
</DialogContentTextStyled>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<CancelButton variant="outlined" onClick={handleCancel} color="error">
|
||||
Cancel
|
||||
</CancelButton>
|
||||
<CreateButton onClick={handleConfirm}>Proceed</CreateButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfirmationModal;
|
82
src/components/common/ContextMenu/ContextMenuResource.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import * as React from "react";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { setNotification } from "../../../state/features/notificationsSlice";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
export default function ContextMenuResource({
|
||||
children,
|
||||
name,
|
||||
service,
|
||||
identifier,
|
||||
link
|
||||
}: any) {
|
||||
const [contextMenu, setContextMenu] = React.useState<{
|
||||
mouseX: number;
|
||||
mouseY: number;
|
||||
} | null>(null);
|
||||
const dispatch = useDispatch();
|
||||
const handleContextMenu = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
setContextMenu(
|
||||
contextMenu === null
|
||||
? {
|
||||
mouseX: event.clientX + 2,
|
||||
mouseY: event.clientY - 6
|
||||
}
|
||||
: // repeated contextmenu when it is already open closes it with Chrome 84 on Ubuntu
|
||||
// Other native context menus might behave different.
|
||||
// With this behavior we prevent contextmenu from the backdrop to re-locale existing context menus.
|
||||
null
|
||||
);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setContextMenu(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onContextMenu={handleContextMenu}
|
||||
style={{ cursor: "context-menu", width: "100%", height: "100%" }}
|
||||
>
|
||||
{children}
|
||||
<Menu
|
||||
open={contextMenu !== null}
|
||||
onClose={handleClose}
|
||||
anchorReference="anchorPosition"
|
||||
anchorPosition={
|
||||
contextMenu !== null
|
||||
? { top: contextMenu.mouseY, left: contextMenu.mouseX }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<MenuItem>
|
||||
<CopyToClipboard
|
||||
text={link}
|
||||
onCopy={() => {
|
||||
handleClose();
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: "Copied to clipboard!",
|
||||
alertType: "success"
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: "16px"
|
||||
}}
|
||||
>
|
||||
Copy Link
|
||||
</Box>
|
||||
</CopyToClipboard>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
16
src/components/common/CustomIcon.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon'
|
||||
import { styled } from '@mui/system'
|
||||
|
||||
const CustomSvgIcon: React.FC<any> = styled(SvgIcon)(({ theme }) => ({
|
||||
cursor: 'pointer',
|
||||
color: '#5f6368',
|
||||
transition: 'all 0.2s',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.1)'
|
||||
}
|
||||
})) as unknown as React.FC<any>
|
||||
|
||||
export const CustomIcon: React.FC<any> = (props) => {
|
||||
return <CustomSvgIcon {...props} />
|
||||
}
|
55
src/components/common/DraggableResizableGrid.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
// DraggableResizableGrid.tsx
|
||||
import React from 'react'
|
||||
import { DndProvider } from 'react-dnd'
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend'
|
||||
import GridLayout, { Layout } from 'react-grid-layout'
|
||||
|
||||
import './DraggableResizableGrid.css' // Add your custom CSS for the grid layout
|
||||
|
||||
interface GridItem {
|
||||
id: string
|
||||
content: React.ReactNode
|
||||
}
|
||||
|
||||
interface DraggableResizableGridProps {
|
||||
items: GridItem[]
|
||||
cols?: number
|
||||
rowHeight?: number
|
||||
onLayoutChange?: (layout: Layout[]) => void
|
||||
}
|
||||
|
||||
const DraggableResizableGrid: React.FC<DraggableResizableGridProps> = ({
|
||||
items,
|
||||
cols = 12,
|
||||
rowHeight = 30,
|
||||
onLayoutChange
|
||||
}) => {
|
||||
const layout = items.map((item, index) => ({
|
||||
i: item.id,
|
||||
x: index % cols,
|
||||
y: Math.floor(index / cols),
|
||||
w: 4,
|
||||
h: 4
|
||||
}))
|
||||
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<GridLayout
|
||||
className="layout"
|
||||
layout={layout}
|
||||
cols={cols}
|
||||
rowHeight={rowHeight}
|
||||
width={1200}
|
||||
onLayoutChange={onLayoutChange}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className="grid-item">
|
||||
{item.content}
|
||||
</div>
|
||||
))}
|
||||
</GridLayout>
|
||||
</DndProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default DraggableResizableGrid
|
40
src/components/common/Error/Error-styles.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { styled } from '@mui/system'
|
||||
import { Box, Button, Grid, Typography } from '@mui/material'
|
||||
|
||||
export const Container = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
gap: '15px',
|
||||
padding: '25px 10px'
|
||||
}))
|
||||
|
||||
export const HeaderRow = styled(Box)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px'
|
||||
}))
|
||||
|
||||
export const HeaderText = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: 'Oxygen',
|
||||
color: theme.palette.text.primary,
|
||||
fontWeight: '400'
|
||||
}))
|
||||
|
||||
export const BackButton = styled(Button)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.secondary.light,
|
||||
color: '#fff',
|
||||
padding: '8px 16px',
|
||||
borderRadius: '7px',
|
||||
fontFamily: 'Oxygen',
|
||||
fontSize: '18px',
|
||||
fontWeight: 500,
|
||||
textTransform: 'none',
|
||||
transition: 'all 0.3s ease-in-out',
|
||||
'&:hover': {
|
||||
cursor: 'pointer',
|
||||
backgroundColor: theme.palette.secondary.light,
|
||||
filter: 'brightness(0.9)'
|
||||
}
|
||||
}))
|
34
src/components/common/Error/ErrorElement.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { Container, HeaderText, BackButton, HeaderRow } from "./Error-styles";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { WarningSVG } from "../../../assets/svgs/WarningSVG";
|
||||
|
||||
interface ErrorElementProps {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const ErrorElement: React.FC<ErrorElementProps> = ({ message }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<HeaderRow>
|
||||
<WarningSVG
|
||||
color={theme.palette.text.primary}
|
||||
height={"35"}
|
||||
width={"35"}
|
||||
/>
|
||||
<HeaderText variant="h1">{message}</HeaderText>
|
||||
</HeaderRow>
|
||||
<HeaderText variant="h2">
|
||||
Please return home or try refreshing the page!
|
||||
</HeaderText>
|
||||
<BackButton
|
||||
onClick={() => {
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
Back Home
|
||||
</BackButton>
|
||||
</Container>
|
||||
);
|
||||
};
|
36
src/components/common/ErrorBoundary.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React, { ReactNode } from 'react'
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children: ReactNode
|
||||
fallback: ReactNode
|
||||
}
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean
|
||||
}
|
||||
|
||||
class ErrorBoundary extends React.Component<
|
||||
ErrorBoundaryProps,
|
||||
ErrorBoundaryState
|
||||
> {
|
||||
state: ErrorBoundaryState = {
|
||||
hasError: false
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(_: Error): ErrorBoundaryState {
|
||||
return { hasError: true }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
||||
// You can log the error and errorInfo here, for example, to an error reporting service.
|
||||
console.error('Error caught in ErrorBoundary:', error, errorInfo)
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
if (this.state.hasError) return this.props.fallback
|
||||
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary
|
316
src/components/common/GenericPublishModal.tsx
Normal file
@ -0,0 +1,316 @@
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Modal,
|
||||
TextField,
|
||||
Typography,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
SelectChangeEvent,
|
||||
OutlinedInput,
|
||||
Chip,
|
||||
IconButton
|
||||
} from '@mui/material'
|
||||
import { styled } from '@mui/system'
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
import { toBase64 } from '../../utils/toBase64'
|
||||
import AddIcon from '@mui/icons-material/Add'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
import { usePublishGeneric } from './PublishGeneric'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { setNotification } from '../../state/features/notificationsSlice'
|
||||
|
||||
const StyledModal = styled(Modal)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}))
|
||||
|
||||
const ChipContainer = styled(Box)({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
'& > *': {
|
||||
margin: '4px'
|
||||
}
|
||||
})
|
||||
|
||||
const ModalContent = styled(Box)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
padding: theme.spacing(4),
|
||||
borderRadius: theme.spacing(1),
|
||||
width: '40%',
|
||||
'&:focus': {
|
||||
outline: 'none'
|
||||
}
|
||||
}))
|
||||
|
||||
interface GenericModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onPublish: (value: any) => void
|
||||
acceptedFileType?: string
|
||||
acceptedFileTypes?: string[]
|
||||
service: string
|
||||
identifierPrefix: string
|
||||
editVideoIdentifier?: string | null | undefined
|
||||
}
|
||||
|
||||
interface SelectOption {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
const maxSize = 500 * 1024 * 1024
|
||||
|
||||
export const GenericModal: React.FC<GenericModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onPublish,
|
||||
acceptedFileType,
|
||||
acceptedFileTypes,
|
||||
service,
|
||||
identifierPrefix,
|
||||
editVideoIdentifier
|
||||
}) => {
|
||||
const [file, setFile] = useState<File | null>(null)
|
||||
const [title, setTitle] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [selectedOption, setSelectedOption] = useState<SelectOption | null>(
|
||||
null
|
||||
)
|
||||
const [inputValue, setInputValue] = useState<string>('')
|
||||
const [chips, setChips] = useState<string[]>([])
|
||||
|
||||
const [options, setOptions] = useState<SelectOption[]>([])
|
||||
const [tags, setTags] = useState<string[]>([])
|
||||
const { publishGeneric } = usePublishGeneric()
|
||||
const dispatch = useDispatch()
|
||||
|
||||
let acceptedFile = {}
|
||||
if (acceptedFileType) {
|
||||
acceptedFile = {
|
||||
[acceptedFileType]: []
|
||||
}
|
||||
}
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
...acceptedFile,
|
||||
maxFiles: 1,
|
||||
maxSize,
|
||||
onDrop: (acceptedFiles) => {
|
||||
setFile(acceptedFiles[0])
|
||||
},
|
||||
onDropRejected: (rejectedFiles) => {
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: 'Your file is over the 500mb limit.',
|
||||
alertType: 'error'
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const handleTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(event.target.value)
|
||||
}
|
||||
|
||||
const handleDescriptionChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
setDescription(event.target.value)
|
||||
}
|
||||
|
||||
const handleOptionChange = (event: SelectChangeEvent<string>) => {
|
||||
const optionId = event.target.value
|
||||
const selectedOption = options.find((option) => option.id === optionId)
|
||||
setSelectedOption(selectedOption || null)
|
||||
}
|
||||
|
||||
const handleChipDelete = (index: number) => {
|
||||
const newChips = [...chips]
|
||||
newChips.splice(index, 1)
|
||||
setChips(newChips)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const missingFields = []
|
||||
|
||||
if (!title) missingFields.push('title')
|
||||
if (!file) missingFields.push('file')
|
||||
if (missingFields.length > 0) {
|
||||
const missingFieldsString = missingFields.join(', ')
|
||||
const errMsg = `Missing: ${missingFieldsString}`
|
||||
|
||||
return
|
||||
}
|
||||
if (!file) return
|
||||
|
||||
const formattedTags: { [key: string]: string } = {}
|
||||
chips.forEach((tag, i) => {
|
||||
formattedTags[`tag${i + 1}`] = tag
|
||||
})
|
||||
|
||||
try {
|
||||
const base64 = await toBase64(file)
|
||||
if (typeof base64 !== 'string') return
|
||||
const base64String = base64.split(',')[1]
|
||||
const fileExtension = file?.name?.split('.')?.pop()
|
||||
const fileTitle = title?.replace(/ /g, '_')?.slice(0, 20)
|
||||
const filename = `${fileTitle}.${fileExtension}`
|
||||
const res = await publishGeneric({
|
||||
editVideoIdentifier,
|
||||
service,
|
||||
identifierPrefix,
|
||||
title,
|
||||
description,
|
||||
base64: base64String,
|
||||
filename: filename,
|
||||
category: selectedOption?.id || '',
|
||||
...formattedTags
|
||||
})
|
||||
onPublish(res)
|
||||
setFile(null)
|
||||
setTitle('')
|
||||
setDescription('')
|
||||
onClose()
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
const handleInputChange = (event: any) => {
|
||||
setInputValue(event.target.value)
|
||||
}
|
||||
|
||||
const handleInputKeyDown = (event: any) => {
|
||||
if (event.key === 'Enter' && inputValue !== '') {
|
||||
if (chips.length < 5) {
|
||||
setChips([...chips, inputValue])
|
||||
setInputValue('')
|
||||
} else {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const addChip = () => {
|
||||
if (chips.length < 5) {
|
||||
setChips([...chips, inputValue])
|
||||
setInputValue('')
|
||||
}
|
||||
}
|
||||
|
||||
const getListCategories = React.useCallback(async () => {
|
||||
try {
|
||||
const url = `/arbitrary/categories`
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
const responseData = await response.json()
|
||||
setOptions(responseData)
|
||||
} catch (error) {}
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
getListCategories()
|
||||
}, [getListCategories])
|
||||
|
||||
return (
|
||||
<StyledModal open={open} onClose={onClose}>
|
||||
<ModalContent>
|
||||
{editVideoIdentifier && (
|
||||
<Typography variant="h6">
|
||||
You are editing: {editVideoIdentifier}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
Upload {service}
|
||||
</Typography>
|
||||
<Box
|
||||
{...getRootProps()}
|
||||
sx={{
|
||||
border: '1px dashed gray',
|
||||
padding: 2,
|
||||
textAlign: 'center',
|
||||
marginBottom: 2
|
||||
}}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<Typography>
|
||||
{file
|
||||
? file.name
|
||||
: 'Drag and drop a file here or click to select a file'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<TextField
|
||||
label="Title"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={title}
|
||||
onChange={handleTitleChange}
|
||||
inputProps={{ maxLength: 40 }}
|
||||
sx={{ marginBottom: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Description"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
multiline
|
||||
rows={4}
|
||||
value={description}
|
||||
onChange={handleDescriptionChange}
|
||||
inputProps={{ maxLength: 180 }}
|
||||
sx={{ marginBottom: 2 }}
|
||||
/>
|
||||
{options.length > 0 && (
|
||||
<FormControl fullWidth sx={{ marginBottom: 2 }}>
|
||||
<InputLabel id="Category">Select a Category</InputLabel>
|
||||
<Select
|
||||
labelId="Category"
|
||||
input={<OutlinedInput label="Select a Category" />}
|
||||
value={selectedOption?.id || ''}
|
||||
onChange={handleOptionChange}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
<FormControl fullWidth sx={{ marginBottom: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
|
||||
<TextField
|
||||
label="Add a tag"
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
disabled={chips.length === 3}
|
||||
/>
|
||||
|
||||
<IconButton onClick={addChip} disabled={chips.length === 3}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<ChipContainer>
|
||||
{chips.map((chip, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={chip}
|
||||
onDelete={() => handleChipDelete(index)}
|
||||
deleteIcon={<CloseIcon />}
|
||||
/>
|
||||
))}
|
||||
</ChipContainer>
|
||||
</FormControl>
|
||||
<Button variant="contained" color="primary" onClick={handleSubmit}>
|
||||
Submit
|
||||
</Button>
|
||||
</ModalContent>
|
||||
</StyledModal>
|
||||
)
|
||||
}
|
89
src/components/common/ImageUploader.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { Box, Button, TextField, Typography, Modal } from '@mui/material'
|
||||
import {
|
||||
useDropzone,
|
||||
DropzoneRootProps,
|
||||
DropzoneInputProps
|
||||
} from 'react-dropzone'
|
||||
import Compressor from 'compressorjs'
|
||||
|
||||
const toBase64 = (file: File): Promise<string | ArrayBuffer | null> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = () => resolve(reader.result)
|
||||
reader.onerror = (error) => {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
|
||||
interface ImageUploaderProps {
|
||||
children: React.ReactNode
|
||||
onPick: (base64Img: string) => void
|
||||
}
|
||||
|
||||
const ImageUploader: React.FC<ImageUploaderProps> = ({ children, onPick }) => {
|
||||
const onDrop = useCallback(
|
||||
async (acceptedFiles: File[]) => {
|
||||
if (acceptedFiles.length > 1) {
|
||||
return
|
||||
}
|
||||
let compressedFile: File | undefined
|
||||
|
||||
try {
|
||||
const image = acceptedFiles[0]
|
||||
await new Promise<void>((resolve) => {
|
||||
new Compressor(image, {
|
||||
quality: 0.6,
|
||||
maxWidth: 1200,
|
||||
mimeType: 'image/webp',
|
||||
success(result) {
|
||||
const file = new File([result], 'name', {
|
||||
type: 'image/webp'
|
||||
})
|
||||
compressedFile = file
|
||||
resolve()
|
||||
},
|
||||
error(err) {}
|
||||
})
|
||||
})
|
||||
if (!compressedFile) return
|
||||
const base64Img = await toBase64(compressedFile)
|
||||
|
||||
onPick(base64Img as string)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
},
|
||||
[onPick]
|
||||
)
|
||||
|
||||
const {
|
||||
getRootProps,
|
||||
getInputProps,
|
||||
isDragActive
|
||||
}: {
|
||||
getRootProps: () => DropzoneRootProps
|
||||
getInputProps: () => DropzoneInputProps
|
||||
isDragActive: boolean
|
||||
} = useDropzone({
|
||||
onDrop,
|
||||
accept: {
|
||||
'image/*': []
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Box
|
||||
{...getRootProps()}
|
||||
sx={{
|
||||
display: 'flex'
|
||||
}}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default ImageUploader
|
49
src/components/common/LazyLoad.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
|
||||
interface Props {
|
||||
onLoadMore: () => Promise<void>;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const LazyLoad: React.FC<Props> = ({ onLoadMore, isLoading }) => {
|
||||
const [isFetching, setIsFetching] = useState<boolean>(false);
|
||||
|
||||
const firstLoad = useRef(false);
|
||||
const [ref, inView] = useInView({
|
||||
threshold: 0.7
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (inView) {
|
||||
setIsFetching(true);
|
||||
onLoadMore().finally(() => {
|
||||
setIsFetching(false);
|
||||
firstLoad.current = true;
|
||||
});
|
||||
}
|
||||
}, [inView]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
minHeight: "25px",
|
||||
width: "100%"
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
visibility: isFetching || isLoading ? "visible" : "hidden"
|
||||
}}
|
||||
>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LazyLoad;
|
86
src/components/common/Notification/Notification.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { toast, ToastContainer, Zoom, Slide } from 'react-toastify'
|
||||
import { removeNotification } from '../../../state/features/notificationsSlice'
|
||||
import 'react-toastify/dist/ReactToastify.css'
|
||||
import { RootState } from '../../../state/store'
|
||||
|
||||
const Notification = () => {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const { alertTypes } = useSelector((state: RootState) => state.notifications)
|
||||
|
||||
if (alertTypes.alertError) {
|
||||
toast.error(`❌ ${alertTypes?.alertError}`, {
|
||||
position: 'bottom-right',
|
||||
autoClose: 4000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
icon: false
|
||||
})
|
||||
dispatch(removeNotification())
|
||||
}
|
||||
if (alertTypes.alertSuccess) {
|
||||
toast.success(`✔️ ${alertTypes?.alertSuccess}`, {
|
||||
position: 'bottom-right',
|
||||
autoClose: 4000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
icon: false
|
||||
})
|
||||
dispatch(removeNotification())
|
||||
}
|
||||
if (alertTypes.alertInfo) {
|
||||
toast.info(`${alertTypes?.alertInfo}`, {
|
||||
position: 'top-right',
|
||||
autoClose: 1300,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: 'light'
|
||||
})
|
||||
dispatch(removeNotification())
|
||||
}
|
||||
|
||||
if (alertTypes.alertInfo) {
|
||||
return (
|
||||
<ToastContainer
|
||||
position="top-right"
|
||||
autoClose={2000}
|
||||
hideProgressBar={false}
|
||||
newestOnTop={false}
|
||||
closeOnClick
|
||||
rtl={false}
|
||||
pauseOnFocusLoss
|
||||
draggable
|
||||
pauseOnHover
|
||||
theme="light"
|
||||
toastStyle={{ fontSize: '16px' }}
|
||||
transition={Slide}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ToastContainer
|
||||
transition={Zoom}
|
||||
position="bottom-right"
|
||||
autoClose={false}
|
||||
hideProgressBar={false}
|
||||
newestOnTop={false}
|
||||
closeOnClick
|
||||
rtl={false}
|
||||
draggable
|
||||
pauseOnHover
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Notification
|
132
src/components/common/NumericTextFieldQshop.tsx
Normal file
@ -0,0 +1,132 @@
|
||||
import { IconButton, InputAdornment, TextField } from "@mui/material";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import RemoveIcon from "@mui/icons-material/Remove";
|
||||
import React, { useImperativeHandle, useState } from "react";
|
||||
|
||||
export enum Variant {
|
||||
filled = "filled",
|
||||
standard = "standard",
|
||||
outlined = "outlined"
|
||||
}
|
||||
interface TextFieldProps {
|
||||
name: string;
|
||||
label: string;
|
||||
required: boolean;
|
||||
minValue: number;
|
||||
maxValue: number;
|
||||
variant?: Variant;
|
||||
addIconButtons?: boolean;
|
||||
allowDecimals?: boolean;
|
||||
onChangeFunc?: (e: string) => void;
|
||||
initialValue?: string;
|
||||
style?: object;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export type NumericTextFieldRef = {
|
||||
getTextFieldValue: () => string;
|
||||
};
|
||||
|
||||
export const NumericTextFieldQshop = React.forwardRef<
|
||||
NumericTextFieldRef,
|
||||
TextFieldProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
name,
|
||||
label,
|
||||
variant,
|
||||
required,
|
||||
style,
|
||||
minValue,
|
||||
maxValue,
|
||||
addIconButtons = true,
|
||||
allowDecimals = true,
|
||||
onChangeFunc,
|
||||
initialValue,
|
||||
className
|
||||
}: TextFieldProps,
|
||||
ref
|
||||
) => {
|
||||
const [textFieldValue, setTextFieldValue] = useState<string>(
|
||||
initialValue || ""
|
||||
);
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
getTextFieldValue: () => {
|
||||
return textFieldValue;
|
||||
}
|
||||
}),
|
||||
[textFieldValue]
|
||||
);
|
||||
|
||||
const setMinMaxValue = (value: string): string => {
|
||||
const lastIndexIsDecimal = value.charAt(value.length - 1) === ".";
|
||||
if (lastIndexIsDecimal) return value;
|
||||
|
||||
const valueNum = Number(value);
|
||||
|
||||
// Bounds checking on valueNum
|
||||
let minMaxNum = valueNum;
|
||||
minMaxNum = Math.min(minMaxNum, maxValue);
|
||||
minMaxNum = Math.max(minMaxNum, minValue);
|
||||
|
||||
return minMaxNum === valueNum ? value : minMaxNum.toString();
|
||||
};
|
||||
|
||||
const filterValue = (value: string, emptyReturn = "") => {
|
||||
if (allowDecimals === false) value = value.replace(".", "");
|
||||
if (value === "-1") return emptyReturn;
|
||||
|
||||
const isPositiveNum = /^[0-9]*\.?[0-9]*$/;
|
||||
|
||||
if (isPositiveNum.test(value)) {
|
||||
return setMinMaxValue(value);
|
||||
}
|
||||
return textFieldValue;
|
||||
};
|
||||
|
||||
const changeValueWithButton = (changeAmount: number) => {
|
||||
const valueNum = Number(textFieldValue);
|
||||
const newValue = setMinMaxValue((valueNum + changeAmount).toString());
|
||||
setTextFieldValue(newValue);
|
||||
};
|
||||
|
||||
const listeners = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = filterValue(e.target.value || "-1");
|
||||
setTextFieldValue(newValue);
|
||||
if (onChangeFunc) onChangeFunc(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<TextField
|
||||
{...style}
|
||||
name={name}
|
||||
label={label}
|
||||
required={required}
|
||||
variant={variant}
|
||||
InputProps={
|
||||
addIconButtons
|
||||
? {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={(e) => changeValueWithButton(1)}>
|
||||
<AddIcon />{" "}
|
||||
</IconButton>
|
||||
<IconButton onClick={(e) => changeValueWithButton(-1)}>
|
||||
<RemoveIcon />{" "}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}
|
||||
: {}
|
||||
}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => listeners(e)}
|
||||
autoComplete="off"
|
||||
value={textFieldValue}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
43
src/components/common/PageLoader.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import React from "react";
|
||||
import CircularProgress from "@mui/material/CircularProgress";
|
||||
import Box from "@mui/system/Box";
|
||||
import { useTheme } from "@mui/material";
|
||||
|
||||
interface PageLoaderProps {
|
||||
size?: number;
|
||||
thickness?: number;
|
||||
}
|
||||
|
||||
const PageLoader: React.FC<PageLoaderProps> = ({
|
||||
size = 40,
|
||||
thickness = 5
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100vh",
|
||||
width: "100%",
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
zIndex: 10000
|
||||
}}
|
||||
>
|
||||
<CircularProgress
|
||||
size={size}
|
||||
thickness={thickness}
|
||||
sx={{
|
||||
color: theme.palette.secondary.main
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageLoader;
|
25
src/components/common/Portal.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
interface PortalProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const Portal: React.FC<PortalProps> = ({ children }) => {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
|
||||
return () => setMounted(false)
|
||||
}, [])
|
||||
|
||||
return mounted
|
||||
? createPortal(
|
||||
children,
|
||||
document.querySelector('#modal-root') as HTMLElement
|
||||
)
|
||||
: null
|
||||
}
|
||||
|
||||
export default Portal
|
280
src/components/common/PostPublishModal.tsx
Normal file
@ -0,0 +1,280 @@
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Modal,
|
||||
TextField,
|
||||
Typography,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
SelectChangeEvent,
|
||||
OutlinedInput,
|
||||
Chip,
|
||||
IconButton
|
||||
} from '@mui/material'
|
||||
import { styled } from '@mui/system'
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
import { usePublishVideo } from './PublishVideo'
|
||||
import { toBase64 } from '../../utils/toBase64'
|
||||
import AddIcon from '@mui/icons-material/Add'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
const StyledModal = styled(Modal)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}))
|
||||
|
||||
const ChipContainer = styled(Box)({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
'& > *': {
|
||||
margin: '4px'
|
||||
}
|
||||
})
|
||||
|
||||
const ModalContent = styled(Box)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
padding: theme.spacing(4),
|
||||
borderRadius: theme.spacing(1),
|
||||
width: '40%',
|
||||
'&:focus': {
|
||||
outline: 'none'
|
||||
}
|
||||
}))
|
||||
|
||||
interface PostModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onPublish: (value: any) => Promise<void>
|
||||
post: any
|
||||
mode?: string
|
||||
metadata?: any
|
||||
}
|
||||
|
||||
interface SelectOption {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
const PostPublishModal: React.FC<PostModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onPublish,
|
||||
post,
|
||||
mode,
|
||||
metadata
|
||||
}) => {
|
||||
const [file, setFile] = useState<File | null>(null)
|
||||
const [title, setTitle] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [selectedOption, setSelectedOption] = useState<SelectOption | null>(
|
||||
null
|
||||
)
|
||||
const [inputValue, setInputValue] = useState<string>('')
|
||||
const [chips, setChips] = useState<string[]>([])
|
||||
|
||||
const [options, setOptions] = useState<SelectOption[]>([])
|
||||
const [tags, setTags] = useState<string[]>([])
|
||||
const { publishVideo } = usePublishVideo()
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
accept: {
|
||||
'video/*': []
|
||||
},
|
||||
maxFiles: 1,
|
||||
onDrop: (acceptedFiles) => {
|
||||
setFile(acceptedFiles[0])
|
||||
}
|
||||
})
|
||||
|
||||
React.useEffect(() => {
|
||||
if (post.title) {
|
||||
setTitle(post.title)
|
||||
}
|
||||
// if (post.description) {
|
||||
// setDescription(post.description)
|
||||
// }
|
||||
}, [post])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (mode === 'edit' && metadata) {
|
||||
if (metadata.description) {
|
||||
setDescription(metadata.description)
|
||||
}
|
||||
|
||||
const findCategory = options.find(
|
||||
(option) => option.id === metadata?.category
|
||||
)
|
||||
if (findCategory) {
|
||||
setSelectedOption(findCategory)
|
||||
}
|
||||
|
||||
if (!metadata?.tags || !Array.isArray(metadata?.tags)) return
|
||||
|
||||
setChips(metadata.tags.slice(0, -2))
|
||||
}
|
||||
}, [mode, metadata, options])
|
||||
|
||||
const handleTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(event.target.value)
|
||||
}
|
||||
|
||||
const handleDescriptionChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
setDescription(event.target.value)
|
||||
}
|
||||
|
||||
const handleOptionChange = (event: SelectChangeEvent<string>) => {
|
||||
const optionId = event.target.value
|
||||
const selectedOption = options.find((option) => option.id === optionId)
|
||||
setSelectedOption(selectedOption || null)
|
||||
}
|
||||
|
||||
const handleChipDelete = (index: number) => {
|
||||
const newChips = [...chips]
|
||||
newChips.splice(index, 1)
|
||||
setChips(newChips)
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const formattedTags: { [key: string]: string } = {}
|
||||
chips.forEach((tag, i) => {
|
||||
formattedTags[`tag${i + 1}`] = tag
|
||||
})
|
||||
|
||||
try {
|
||||
await onPublish({
|
||||
title,
|
||||
description,
|
||||
tags: chips,
|
||||
category: selectedOption?.id || ''
|
||||
})
|
||||
setFile(null)
|
||||
setTitle('')
|
||||
setDescription('')
|
||||
onClose()
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
const handleInputChange = (event: any) => {
|
||||
setInputValue(event.target.value)
|
||||
}
|
||||
|
||||
const handleInputKeyDown = (event: any) => {
|
||||
if (event.key === 'Enter' && inputValue !== '') {
|
||||
if (chips.length < 5) {
|
||||
setChips([...chips, inputValue])
|
||||
setInputValue('')
|
||||
} else {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const addChip = () => {
|
||||
if (chips.length < 3) {
|
||||
setChips([...chips, inputValue])
|
||||
setInputValue('')
|
||||
}
|
||||
}
|
||||
|
||||
const getListCategories = React.useCallback(async () => {
|
||||
try {
|
||||
const url = `/arbitrary/categories`
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
const responseData = await response.json()
|
||||
setOptions(responseData)
|
||||
} catch (error) {}
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
getListCategories()
|
||||
}, [getListCategories])
|
||||
|
||||
return (
|
||||
<StyledModal open={open} onClose={onClose}>
|
||||
<ModalContent>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
Upload Blog Post
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
label="Post Title"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={title}
|
||||
onChange={handleTitleChange}
|
||||
inputProps={{ maxLength: 40 }}
|
||||
sx={{ marginBottom: 2 }}
|
||||
disabled
|
||||
/>
|
||||
<TextField
|
||||
label="Post Description"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
multiline
|
||||
rows={4}
|
||||
value={description}
|
||||
onChange={handleDescriptionChange}
|
||||
inputProps={{ maxLength: 180 }}
|
||||
sx={{ marginBottom: 2 }}
|
||||
/>
|
||||
{options.length > 0 && (
|
||||
<FormControl fullWidth sx={{ marginBottom: 2 }}>
|
||||
<InputLabel id="Category">Select a Category</InputLabel>
|
||||
<Select
|
||||
labelId="Category"
|
||||
input={<OutlinedInput label="Select a Category" />}
|
||||
value={selectedOption?.id || ''}
|
||||
onChange={handleOptionChange}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
<FormControl fullWidth sx={{ marginBottom: 2 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
|
||||
<TextField
|
||||
label="Add a tag"
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
disabled={chips.length === 3}
|
||||
/>
|
||||
|
||||
<IconButton onClick={addChip} disabled={chips.length === 3}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<ChipContainer>
|
||||
{chips.map((chip, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={chip}
|
||||
onDelete={() => handleChipDelete(index)}
|
||||
deleteIcon={<CloseIcon />}
|
||||
/>
|
||||
))}
|
||||
</ChipContainer>
|
||||
</FormControl>
|
||||
<Button variant="contained" color="primary" onClick={handleSubmit}>
|
||||
Submit
|
||||
</Button>
|
||||
</ModalContent>
|
||||
</StyledModal>
|
||||
)
|
||||
}
|
||||
|
||||
export default PostPublishModal
|
111
src/components/common/PublishAudio.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import React from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { setNotification } from '../../state/features/notificationsSlice'
|
||||
import { RootState } from '../../state/store'
|
||||
import ShortUniqueId from 'short-unique-id'
|
||||
|
||||
const uid = new ShortUniqueId()
|
||||
|
||||
interface IPublishVideo {
|
||||
title: string
|
||||
description: string
|
||||
base64: string
|
||||
category: string
|
||||
editVideoIdentifier?: string | null | undefined
|
||||
|
||||
}
|
||||
|
||||
export const usePublishAudio = () => {
|
||||
const { user } = useSelector((state: RootState) => state.auth)
|
||||
const dispatch = useDispatch()
|
||||
const publishAudio = async ({
|
||||
editVideoIdentifier,
|
||||
title,
|
||||
description,
|
||||
base64,
|
||||
category,
|
||||
...rest
|
||||
}: IPublishVideo) => {
|
||||
let address
|
||||
let name
|
||||
let errorMsg = ''
|
||||
|
||||
address = user?.address
|
||||
name = user?.name || ''
|
||||
|
||||
const missingFields = []
|
||||
if (!address) {
|
||||
errorMsg = "Cannot post: your address isn't available"
|
||||
}
|
||||
if (!name) {
|
||||
errorMsg = 'Cannot post without a name'
|
||||
}
|
||||
if (!title) missingFields.push('title')
|
||||
if (missingFields.length > 0) {
|
||||
const missingFieldsString = missingFields.join(', ')
|
||||
const errMsg = `Missing: ${missingFieldsString}`
|
||||
errorMsg = errMsg
|
||||
}
|
||||
|
||||
if (errorMsg) {
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: errorMsg,
|
||||
alertType: 'error'
|
||||
})
|
||||
)
|
||||
throw new Error(errorMsg)
|
||||
}
|
||||
|
||||
try {
|
||||
const id = uid()
|
||||
|
||||
let identifier = `qaudio_qblog_${id}`
|
||||
if(editVideoIdentifier){
|
||||
identifier = editVideoIdentifier
|
||||
}
|
||||
const resourceResponse = await qortalRequest({
|
||||
action: 'PUBLISH_QDN_RESOURCE',
|
||||
name: name,
|
||||
service: 'AUDIO',
|
||||
data64: base64,
|
||||
title: title,
|
||||
description: description,
|
||||
category: category,
|
||||
...rest,
|
||||
identifier: identifier
|
||||
})
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: 'Audio successfully published',
|
||||
alertType: 'success'
|
||||
})
|
||||
)
|
||||
return resourceResponse
|
||||
} catch (error: any) {
|
||||
let notificationObj = null
|
||||
if (typeof error === 'string') {
|
||||
notificationObj = {
|
||||
msg: error || 'Failed to publish audio',
|
||||
alertType: 'error'
|
||||
}
|
||||
} else if (typeof error?.error === 'string') {
|
||||
notificationObj = {
|
||||
msg: error?.error || 'Failed to publish audio',
|
||||
alertType: 'error'
|
||||
}
|
||||
} else {
|
||||
notificationObj = {
|
||||
msg: error?.message || error?.message || 'Failed to publish audio',
|
||||
alertType: 'error'
|
||||
}
|
||||
}
|
||||
if (!notificationObj) return
|
||||
dispatch(setNotification(notificationObj))
|
||||
|
||||
}
|
||||
}
|
||||
return {
|
||||
publishAudio
|
||||
}
|
||||
}
|
119
src/components/common/PublishGeneric.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import React from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { setNotification } from '../../state/features/notificationsSlice'
|
||||
import { RootState } from '../../state/store'
|
||||
import ShortUniqueId from 'short-unique-id'
|
||||
|
||||
const uid = new ShortUniqueId()
|
||||
|
||||
interface IPublishGeneric {
|
||||
title: string
|
||||
description: string
|
||||
base64: string
|
||||
category: string
|
||||
service: string
|
||||
identifierPrefix: string
|
||||
filename: string
|
||||
editVideoIdentifier?: string | null | undefined
|
||||
|
||||
}
|
||||
|
||||
export const usePublishGeneric = () => {
|
||||
const { user } = useSelector((state: RootState) => state.auth)
|
||||
const dispatch = useDispatch()
|
||||
const publishGeneric = async ({
|
||||
editVideoIdentifier,
|
||||
service,
|
||||
identifierPrefix,
|
||||
filename,
|
||||
title,
|
||||
description,
|
||||
base64,
|
||||
category,
|
||||
...rest
|
||||
}: IPublishGeneric) => {
|
||||
let address
|
||||
let name
|
||||
let errorMsg = ''
|
||||
|
||||
address = user?.address
|
||||
name = user?.name || ''
|
||||
|
||||
const missingFields = []
|
||||
if (!address) {
|
||||
errorMsg = "Cannot post: your address isn't available"
|
||||
}
|
||||
if (!name) {
|
||||
errorMsg = 'Cannot post without a name'
|
||||
}
|
||||
if (!title) missingFields.push('title')
|
||||
if (missingFields.length > 0) {
|
||||
const missingFieldsString = missingFields.join(', ')
|
||||
const errMsg = `Missing: ${missingFieldsString}`
|
||||
errorMsg = errMsg
|
||||
}
|
||||
|
||||
if (errorMsg) {
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: errorMsg,
|
||||
alertType: 'error'
|
||||
})
|
||||
)
|
||||
throw new Error(errorMsg)
|
||||
}
|
||||
|
||||
try {
|
||||
const id = uid()
|
||||
|
||||
let identifier = `${identifierPrefix}_${id}`
|
||||
if(editVideoIdentifier){
|
||||
identifier = editVideoIdentifier
|
||||
}
|
||||
|
||||
const resourceResponse = await qortalRequest({
|
||||
action: 'PUBLISH_QDN_RESOURCE',
|
||||
name: name,
|
||||
service: service,
|
||||
data64: base64,
|
||||
title: title,
|
||||
description: description,
|
||||
category: category,
|
||||
filename,
|
||||
...rest,
|
||||
identifier: identifier
|
||||
})
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: `${service} successfully published`,
|
||||
alertType: 'success'
|
||||
})
|
||||
)
|
||||
return resourceResponse
|
||||
} catch (error: any) {
|
||||
let notificationObj = null
|
||||
if (typeof error === 'string') {
|
||||
notificationObj = {
|
||||
msg: error || `Failed to publish ${service}`,
|
||||
alertType: 'error'
|
||||
}
|
||||
} else if (typeof error?.error === 'string') {
|
||||
notificationObj = {
|
||||
msg: error?.error || `Failed to publish ${service}`,
|
||||
alertType: 'error'
|
||||
}
|
||||
} else {
|
||||
notificationObj = {
|
||||
msg:
|
||||
error?.message || error?.message || `Failed to publish ${service}`,
|
||||
alertType: 'error'
|
||||
}
|
||||
}
|
||||
if (!notificationObj) return
|
||||
dispatch(setNotification(notificationObj))
|
||||
}
|
||||
}
|
||||
return {
|
||||
publishGeneric
|
||||
}
|
||||
}
|
112
src/components/common/PublishVideo.tsx
Normal file
@ -0,0 +1,112 @@
|
||||
import React from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { setNotification } from '../../state/features/notificationsSlice'
|
||||
import { RootState } from '../../state/store'
|
||||
import ShortUniqueId from 'short-unique-id'
|
||||
|
||||
const uid = new ShortUniqueId()
|
||||
|
||||
interface IPublishVideo {
|
||||
title: string
|
||||
description: string
|
||||
base64?: string
|
||||
category: string
|
||||
editVideoIdentifier?: string | null | undefined
|
||||
file?: File
|
||||
}
|
||||
|
||||
export const usePublishVideo = () => {
|
||||
const { user } = useSelector((state: RootState) => state.auth)
|
||||
const dispatch = useDispatch()
|
||||
const publishVideo = async ({
|
||||
file,
|
||||
editVideoIdentifier,
|
||||
title,
|
||||
description,
|
||||
base64,
|
||||
category,
|
||||
...rest
|
||||
}: IPublishVideo) => {
|
||||
let address
|
||||
let name
|
||||
let errorMsg = ''
|
||||
|
||||
address = user?.address
|
||||
name = user?.name || ''
|
||||
|
||||
const missingFields = []
|
||||
if (!address) {
|
||||
errorMsg = "Cannot post: your address isn't available"
|
||||
}
|
||||
if (!name) {
|
||||
errorMsg = 'Cannot post without a name'
|
||||
}
|
||||
if (!title) missingFields.push('title')
|
||||
if (missingFields.length > 0) {
|
||||
const missingFieldsString = missingFields.join(', ')
|
||||
const errMsg = `Missing: ${missingFieldsString}`
|
||||
errorMsg = errMsg
|
||||
}
|
||||
|
||||
if (errorMsg) {
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: errorMsg,
|
||||
alertType: 'error'
|
||||
})
|
||||
)
|
||||
throw new Error(errorMsg)
|
||||
}
|
||||
|
||||
try {
|
||||
const id = uid()
|
||||
|
||||
let identifier = `qvideo_qblog_${id}`
|
||||
if (editVideoIdentifier) {
|
||||
identifier = editVideoIdentifier
|
||||
}
|
||||
const resourceResponse = await qortalRequest({
|
||||
action: 'PUBLISH_QDN_RESOURCE',
|
||||
name: name,
|
||||
service: 'VIDEO',
|
||||
// data64: base64,
|
||||
file: file,
|
||||
title: title,
|
||||
description: description,
|
||||
category: category,
|
||||
...rest,
|
||||
identifier: identifier
|
||||
})
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: 'Video successfully published',
|
||||
alertType: 'success'
|
||||
})
|
||||
)
|
||||
return resourceResponse
|
||||
} catch (error: any) {
|
||||
let notificationObj = null
|
||||
if (typeof error === 'string') {
|
||||
notificationObj = {
|
||||
msg: error || 'Failed to publish video',
|
||||
alertType: 'error'
|
||||
}
|
||||
} else if (typeof error?.error === 'string') {
|
||||
notificationObj = {
|
||||
msg: error?.error || 'Failed to publish video',
|
||||
alertType: 'error'
|
||||
}
|
||||
} else {
|
||||
notificationObj = {
|
||||
msg: error?.message || 'Failed to publish video',
|
||||
alertType: 'error'
|
||||
}
|
||||
}
|
||||
if (!notificationObj) return
|
||||
dispatch(setNotification(notificationObj))
|
||||
}
|
||||
}
|
||||
return {
|
||||
publishVideo
|
||||
}
|
||||
}
|
124
src/components/common/ResponsiveImage.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import React, { useState, useEffect, CSSProperties } from "react";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
interface ResponsiveImageProps {
|
||||
src: string;
|
||||
dimensions: string;
|
||||
alt?: string;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
const ResponsiveImage: React.FC<ResponsiveImageProps> = ({
|
||||
src,
|
||||
dimensions,
|
||||
alt,
|
||||
className,
|
||||
style
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const matchResult = dimensions?.match(/v1\.(\d+(\.\d+)?)x(\d+)/);
|
||||
|
||||
const width = matchResult ? parseFloat(matchResult[1]) : 1; // Default width value
|
||||
const height = matchResult ? parseInt(matchResult[3], 10) : 1; // Default height value
|
||||
|
||||
const aspectRatio = (height / width) * 100;
|
||||
|
||||
useEffect(() => {
|
||||
if (dimensions === "v1.0x0") {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}, [dimensions]);
|
||||
|
||||
if (dimensions === "v1.0x0" || !dimensions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const imageStyle: CSSProperties = {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover"
|
||||
};
|
||||
|
||||
const wrapperStyle: CSSProperties = {
|
||||
position: "relative",
|
||||
paddingBottom: `${aspectRatio}%`,
|
||||
overflow: "hidden",
|
||||
...style
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
padding: "2px"
|
||||
}}
|
||||
>
|
||||
{/* <img
|
||||
onLoad={() => setLoading(false)}
|
||||
src={src}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: 'auto',
|
||||
borderRadius: '8px'
|
||||
}}
|
||||
/> */}
|
||||
{loading && (
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 0,
|
||||
paddingBottom: `${(height / width) * 100}%`,
|
||||
objectFit: "contain",
|
||||
visibility: loading ? "visible" : "hidden",
|
||||
borderRadius: "8px"
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<img
|
||||
onLoad={() => setLoading(false)}
|
||||
src={src}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "auto",
|
||||
borderRadius: "8px",
|
||||
visibility: loading ? "hidden" : "visible",
|
||||
position: loading ? "absolute" : "unset"
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={wrapperStyle} className={className}>
|
||||
{loading ? (
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
style={{
|
||||
...imageStyle,
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResponsiveImage;
|
24
src/components/common/TabImageList/TabImageList-styles.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { styled } from "@mui/system";
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
export const TabImageListStyle = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
flexDirection: "column",
|
||||
gap: "10px",
|
||||
justifyContent: "center",
|
||||
width: "100%"
|
||||
}));
|
||||
|
||||
export const TabImageContainer = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
width: "100%",
|
||||
gap: "15px"
|
||||
}));
|
||||
|
||||
export const TabImageStyle = styled("img")(({ theme }) => ({
|
||||
width: "30%",
|
||||
height: "100%"
|
||||
}));
|
65
src/components/common/TabImageList/TabImageList.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
TabImageContainer,
|
||||
TabImageListStyle,
|
||||
TabImageStyle
|
||||
} from "./TabImageList-styles";
|
||||
import { Box } from "@mui/material";
|
||||
import CSS from "csstype";
|
||||
|
||||
export interface TabImageListProps {
|
||||
divStyle?: CSS.Properties;
|
||||
imgStyle?: CSS.Properties;
|
||||
images: string[] | undefined;
|
||||
}
|
||||
const TabImageList = ({
|
||||
divStyle = {},
|
||||
imgStyle = {},
|
||||
images
|
||||
}: TabImageListProps) => {
|
||||
if (images) {
|
||||
const [mainImage, setMainImage] = useState<string>(images[0]);
|
||||
const [imageFocusedIndex, setImageFocusedIndex] = useState<number>(0);
|
||||
|
||||
const imageTabOutlineStyle = {
|
||||
outline: "4px solid #03A9F4"
|
||||
};
|
||||
|
||||
const switchMainImage = (index: number) => {
|
||||
setMainImage(images[index]);
|
||||
setImageFocusedIndex(index);
|
||||
};
|
||||
const imageRow =
|
||||
images.length > 1 ? (
|
||||
images.map((image, index) => (
|
||||
<TabImageStyle
|
||||
style={imageFocusedIndex === index ? imageTabOutlineStyle : {}}
|
||||
src={image}
|
||||
alt={`Image #${index}`}
|
||||
onClick={() => switchMainImage(index)}
|
||||
key={image + index.toString()}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div />
|
||||
);
|
||||
|
||||
const defaultStyle = { width: "100%" };
|
||||
return (
|
||||
<TabImageListStyle>
|
||||
<Box style={{ ...defaultStyle, ...divStyle }}>
|
||||
<img
|
||||
style={{ width: "100%", aspectRatio: "1", ...imgStyle }}
|
||||
src={mainImage}
|
||||
alt="No product image found"
|
||||
/>
|
||||
</Box>
|
||||
<TabImageContainer>{imageRow}</TabImageContainer>
|
||||
</TabImageListStyle>
|
||||
);
|
||||
} else {
|
||||
return <div />;
|
||||
}
|
||||
};
|
||||
|
||||
export default TabImageList;
|
51
src/components/common/VideoContent.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import React from 'react'
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import { styled } from '@mui/system'
|
||||
import { Description, Movie } from '@mui/icons-material'
|
||||
|
||||
interface VideoProps {
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
const StyledBox = styled(Box)`
|
||||
margin: 20px 0px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const Title = styled(Typography)``
|
||||
|
||||
const DescriptionIcon = styled(Description)`
|
||||
color: #666;
|
||||
margin-right: 0.5rem;
|
||||
`
|
||||
|
||||
const MovieIcon = styled(Movie)`
|
||||
color: #666;
|
||||
margin-right: 0.5rem;
|
||||
`
|
||||
|
||||
export const VideoContent: React.FC<VideoProps> = ({ title, description }) => {
|
||||
return (
|
||||
<StyledBox>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start'
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center">
|
||||
<MovieIcon />
|
||||
<Title variant="h4">{title}</Title>
|
||||
</Box>
|
||||
|
||||
<Box display="flex" alignItems="center">
|
||||
<DescriptionIcon />
|
||||
<Typography variant="body1">{description}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</StyledBox>
|
||||
)
|
||||
}
|
286
src/components/common/VideoPublishModal.tsx
Normal file
@ -0,0 +1,286 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Modal,
|
||||
TextField,
|
||||
Typography,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
SelectChangeEvent,
|
||||
OutlinedInput,
|
||||
Chip,
|
||||
IconButton
|
||||
} from "@mui/material";
|
||||
import { styled } from "@mui/system";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { usePublishVideo } from "./PublishVideo";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
const StyledModal = styled(Modal)(({ theme }) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
}));
|
||||
|
||||
const ChipContainer = styled(Box)({
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
"& > *": {
|
||||
margin: "4px"
|
||||
}
|
||||
});
|
||||
|
||||
const ModalContent = styled(Box)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
padding: theme.spacing(4),
|
||||
borderRadius: theme.spacing(1),
|
||||
width: "40%",
|
||||
"&:focus": {
|
||||
outline: "none"
|
||||
}
|
||||
}));
|
||||
|
||||
interface VideoModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onPublish: (value: any) => void;
|
||||
editVideoIdentifier?: string | null | undefined;
|
||||
}
|
||||
|
||||
interface SelectOption {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const VideoModal: React.FC<VideoModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onPublish,
|
||||
editVideoIdentifier
|
||||
}) => {
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [title, setTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [selectedOption, setSelectedOption] = useState<SelectOption | null>(
|
||||
null
|
||||
);
|
||||
const [inputValue, setInputValue] = useState<string>("");
|
||||
const [chips, setChips] = useState<string[]>([]);
|
||||
|
||||
const [options, setOptions] = useState<SelectOption[]>([]);
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
const { publishVideo } = usePublishVideo();
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
accept: {
|
||||
"video/*": []
|
||||
},
|
||||
maxFiles: 1,
|
||||
onDrop: (acceptedFiles) => {
|
||||
setFile(acceptedFiles[0]);
|
||||
}
|
||||
});
|
||||
|
||||
const handleTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(event.target.value);
|
||||
};
|
||||
|
||||
const handleDescriptionChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
setDescription(event.target.value);
|
||||
};
|
||||
|
||||
const handleOptionChange = (event: SelectChangeEvent<string>) => {
|
||||
const optionId = event.target.value;
|
||||
const selectedOption = options.find((option) => option.id === optionId);
|
||||
setSelectedOption(selectedOption || null);
|
||||
};
|
||||
|
||||
const handleChipDelete = (index: number) => {
|
||||
const newChips = [...chips];
|
||||
newChips.splice(index, 1);
|
||||
setChips(newChips);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const missingFields = [];
|
||||
|
||||
if (!title) missingFields.push("title");
|
||||
if (!file) missingFields.push("file");
|
||||
if (missingFields.length > 0) {
|
||||
const missingFieldsString = missingFields.join(", ");
|
||||
const errMsg = `Missing: ${missingFieldsString}`;
|
||||
|
||||
return;
|
||||
}
|
||||
if (!file) return;
|
||||
|
||||
const formattedTags: { [key: string]: string } = {};
|
||||
chips.forEach((tag, i) => {
|
||||
formattedTags[`tag${i + 1}`] = tag;
|
||||
});
|
||||
|
||||
try {
|
||||
// const base64 = await toBase64(file)
|
||||
// if (typeof base64 !== 'string') return
|
||||
// const base64String = base64.split(',')[1]
|
||||
// if (!file) return
|
||||
|
||||
const res = await publishVideo({
|
||||
file: file,
|
||||
editVideoIdentifier,
|
||||
title,
|
||||
description,
|
||||
category: selectedOption?.id || "",
|
||||
...formattedTags
|
||||
});
|
||||
onPublish(res);
|
||||
setFile(null);
|
||||
setTitle("");
|
||||
setDescription("");
|
||||
onClose();
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
const handleInputChange = (event: any) => {
|
||||
setInputValue(event.target.value);
|
||||
};
|
||||
|
||||
const handleInputKeyDown = (event: any) => {
|
||||
if (event.key === "Enter" && inputValue !== "") {
|
||||
if (chips.length < 5) {
|
||||
setChips([...chips, inputValue]);
|
||||
setInputValue("");
|
||||
} else {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const addChip = () => {
|
||||
if (chips.length < 5) {
|
||||
setChips([...chips, inputValue]);
|
||||
setInputValue("");
|
||||
}
|
||||
};
|
||||
|
||||
const getListCategories = React.useCallback(async () => {
|
||||
try {
|
||||
const url = `/arbitrary/categories`;
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
const responseData = await response.json();
|
||||
setOptions(responseData);
|
||||
} catch (error) {}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
getListCategories();
|
||||
}, [getListCategories]);
|
||||
|
||||
return (
|
||||
<StyledModal open={open} onClose={onClose}>
|
||||
<ModalContent>
|
||||
{editVideoIdentifier && (
|
||||
<Typography variant="h6">
|
||||
You are editing: {editVideoIdentifier}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
Upload Video
|
||||
</Typography>
|
||||
<Box
|
||||
{...getRootProps()}
|
||||
sx={{
|
||||
border: "1px dashed gray",
|
||||
padding: 2,
|
||||
textAlign: "center",
|
||||
marginBottom: 2
|
||||
}}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<Typography>
|
||||
{file
|
||||
? file.name
|
||||
: "Drag and drop a video file here or click to select a file"}
|
||||
</Typography>
|
||||
</Box>
|
||||
<TextField
|
||||
label="Video Title"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={title}
|
||||
onChange={handleTitleChange}
|
||||
inputProps={{ maxLength: 40 }}
|
||||
sx={{ marginBottom: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Video Description"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
multiline
|
||||
rows={4}
|
||||
value={description}
|
||||
onChange={handleDescriptionChange}
|
||||
inputProps={{ maxLength: 180 }}
|
||||
sx={{ marginBottom: 2 }}
|
||||
/>
|
||||
{options.length > 0 && (
|
||||
<FormControl fullWidth sx={{ marginBottom: 2 }}>
|
||||
<InputLabel id="Category">Select a Category</InputLabel>
|
||||
<Select
|
||||
labelId="Category"
|
||||
input={<OutlinedInput label="Select a Category" />}
|
||||
value={selectedOption?.id || ""}
|
||||
onChange={handleOptionChange}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<MenuItem key={option.id} value={option.id}>
|
||||
{option.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
<FormControl fullWidth sx={{ marginBottom: 2 }}>
|
||||
<Box sx={{ display: "flex", alignItems: "flex-end" }}>
|
||||
<TextField
|
||||
label="Add a tag"
|
||||
value={inputValue}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
disabled={chips.length === 3}
|
||||
/>
|
||||
|
||||
<IconButton onClick={addChip} disabled={chips.length === 3}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<ChipContainer>
|
||||
{chips.map((chip, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={chip}
|
||||
onDelete={() => handleChipDelete(index)}
|
||||
deleteIcon={<CloseIcon />}
|
||||
/>
|
||||
))}
|
||||
</ChipContainer>
|
||||
</FormControl>
|
||||
<Button variant="contained" color="primary" onClick={handleSubmit}>
|
||||
Submit
|
||||
</Button>
|
||||
</ModalContent>
|
||||
</StyledModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default VideoModal;
|
78
src/components/editor/BlogEditor.css
Normal file
@ -0,0 +1,78 @@
|
||||
/* src/components/BlogEditor.css */
|
||||
.blog-editor {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
line-height: 1.5;
|
||||
font-size: 18px;
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
min-height: 200px;
|
||||
z-index: 500;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.toolbar-button:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background-color: #2c2b31;
|
||||
color: rgb(238, 234, 234);
|
||||
border-radius: 3px;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
white-space: pre-wrap;
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.paragraph {
|
||||
font-size: 20px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.paragraph-mail {
|
||||
font-size: 16px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.toolbar-button {
|
||||
background-color: white;
|
||||
border: 1px solid gray;
|
||||
border-radius: 5px;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toolbar-button.active {
|
||||
background-color: lightgray;
|
||||
}
|
||||
|
||||
.h2 {
|
||||
font-size: 25px
|
||||
}
|
||||
|
||||
.h2 {
|
||||
font-size: 22px
|
||||
}
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
574
src/components/editor/BlogEditor.tsx
Normal file
@ -0,0 +1,574 @@
|
||||
// src/components/BlogEditor.tsx
|
||||
// @ts-nocheck
|
||||
|
||||
import React, { useMemo, useState, useCallback } from 'react';
|
||||
import { createEditor, Descendant, Editor, Transforms, Range } from 'slate'
|
||||
import SvgIcon from '@material-ui/core/SvgIcon'
|
||||
import {
|
||||
Slate,
|
||||
Editable,
|
||||
withReact,
|
||||
RenderElementProps,
|
||||
RenderLeafProps,
|
||||
useSlate
|
||||
} from 'slate-react'
|
||||
import { styled } from '@mui/system'
|
||||
import { CustomElement, CustomText, FormatMark } from './customTypes'
|
||||
import './BlogEditor.css'
|
||||
import { Modal, Box, TextField, Button } from '@mui/material'
|
||||
|
||||
import { AlignCenterSVG } from '../../assets/svgs/AlignCenterSVG'
|
||||
import { BoldSVG } from '../../assets/svgs/BoldSVG'
|
||||
import { ItalicSVG } from '../../assets/svgs/ItalicSVG'
|
||||
import { UnderlineSVG } from '../../assets/svgs/UnderlineSVG'
|
||||
import { H2SVG } from '../../assets/svgs/H2SVG'
|
||||
import { H3SVG } from '../../assets/svgs/H3SVG'
|
||||
import { AlignLeftSVG } from '../../assets/svgs/AlignLeftSVG'
|
||||
import { AlignRightSVG } from '../../assets/svgs/AlignRightSVG'
|
||||
import { CodeBlockSVG } from '../../assets/svgs/CodeBlockSVG'
|
||||
import { LinkSVG } from '../../assets/svgs/LinkSVG'
|
||||
|
||||
const initialValue: Descendant[] = [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{ text: 'Start writing your blog post...' }]
|
||||
}
|
||||
]
|
||||
|
||||
interface MyComponentProps {
|
||||
addPostSection?: (value: any) => void
|
||||
editPostSection?: (value: any, section: any) => void
|
||||
defaultValue?: any
|
||||
section?: any
|
||||
value: any
|
||||
setValue: (value: any) => void
|
||||
editorKey?: number
|
||||
mode?: string
|
||||
}
|
||||
|
||||
const ModalBox = styled(Box)(({ theme }) => ({
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
boxShadow: theme.shadows[5],
|
||||
padding: theme.spacing(2, 4, 3),
|
||||
gap: '15px',
|
||||
borderRadius: '5px',
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flex: 0
|
||||
}))
|
||||
|
||||
const BlogEditor: React.FC<MyComponentProps> = ({
|
||||
addPostSection,
|
||||
editPostSection,
|
||||
defaultValue,
|
||||
section,
|
||||
value,
|
||||
setValue,
|
||||
editorKey,
|
||||
mode
|
||||
}) => {
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
|
||||
// const [value, setValue] = useState(defaultValue || initialValue);
|
||||
const isTextAlignmentActive = (editor: Editor, alignment: string) => {
|
||||
const [match] = Editor.nodes(editor, {
|
||||
match: (n) => {
|
||||
return n?.textAlign === alignment?.replace(/^align-/, '')
|
||||
}
|
||||
})
|
||||
return !!match
|
||||
}
|
||||
|
||||
const toggleTextAlignment = (editor: Editor, alignment: string) => {
|
||||
const isActive = isTextAlignmentActive(editor, alignment)
|
||||
Transforms.setNodes(
|
||||
editor,
|
||||
{ style: { textAlign: isActive ? 'inherit' : alignment } },
|
||||
{ match: (n) => Editor.isBlock(editor, n) }
|
||||
)
|
||||
}
|
||||
|
||||
const toggleMark = (editor: Editor, format: FormatMark) => {
|
||||
if (
|
||||
format === 'align-left' ||
|
||||
format === 'align-center' ||
|
||||
format === 'align-right'
|
||||
) {
|
||||
toggleTextAlignment(editor, format)
|
||||
} else {
|
||||
const isActive = Editor?.marks(editor)?.[format] === true
|
||||
if (isActive) {
|
||||
Editor?.removeMark(editor, format)
|
||||
} else {
|
||||
Editor?.addMark(editor, format, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newValue = useMemo(() => [...(value || initialValue)], [value])
|
||||
|
||||
const types = ['paragraph', 'heading-2', 'heading-3']
|
||||
|
||||
const setTextAlignment = (editor, alignment) => {
|
||||
const isActive = isTextAlignmentActive(editor, alignment)
|
||||
const alignmentType = ''
|
||||
Transforms?.setNodes(
|
||||
editor,
|
||||
{
|
||||
textAlign: isActive ? null : alignment
|
||||
},
|
||||
{
|
||||
match: (n) =>
|
||||
n.type === 'heading-2' ||
|
||||
n.type === 'heading-3' ||
|
||||
n.type === 'paragraph'
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const ToolbarButton: React.FC<{
|
||||
format: FormatMark | string
|
||||
label: string
|
||||
editor: Editor
|
||||
children: React.ReactNode
|
||||
}> = ({ format, label, editor, children }) => {
|
||||
useSlate()
|
||||
|
||||
let onClick = () => {
|
||||
if (format === 'heading-2' || format === 'heading-3') {
|
||||
toggleBlock(editor, format)
|
||||
} else if (
|
||||
format === 'bold' ||
|
||||
format === 'italic' ||
|
||||
format === 'underline' ||
|
||||
format === ''
|
||||
) {
|
||||
toggleMark(editor, format)
|
||||
} else if (
|
||||
format === 'align-left' ||
|
||||
format === 'align-center' ||
|
||||
format === 'align-right'
|
||||
) {
|
||||
setTextAlignment(editor, format?.replace(/^align-/, ''))
|
||||
}
|
||||
}
|
||||
|
||||
let isActive = false
|
||||
|
||||
try {
|
||||
if (
|
||||
format === 'align-left' ||
|
||||
format === 'align-center' ||
|
||||
format === 'align-right'
|
||||
) {
|
||||
isActive = isTextAlignmentActive(editor, format)
|
||||
} else if (format === 'heading-2' || format === 'heading-3') {
|
||||
isActive = isBlockActive(editor, format)
|
||||
} else if (
|
||||
format === 'bold' ||
|
||||
format === 'italic' ||
|
||||
format === 'underline' ||
|
||||
format === ''
|
||||
) {
|
||||
isActive = Editor?.marks(editor)?.[format] === true
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`toolbar-button ${isActive ? 'active' : ''}`}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault()
|
||||
onClick()
|
||||
}}
|
||||
>
|
||||
{children ? children : label}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const ToolbarButtonCodeBlock: React.FC<{
|
||||
format: FormatMark | string
|
||||
label: string
|
||||
editor: Editor
|
||||
children: React.ReactNode
|
||||
}> = ({ format, label, editor, children }) => {
|
||||
const editor2 = useSlate()
|
||||
|
||||
let onClick = () => {
|
||||
if (format === 'code-block') {
|
||||
toggleBlock(editor, 'code-block')
|
||||
}
|
||||
}
|
||||
let isActive = false
|
||||
try {
|
||||
if (format === 'code-block') {
|
||||
isActive = isBlockActive(editor, format)
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`toolbar-button ${isActive ? 'active' : ''}`}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault()
|
||||
onClick()
|
||||
}}
|
||||
>
|
||||
{children ? children : label}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const ToolbarButtonAlign: React.FC<{
|
||||
format: string
|
||||
label: string
|
||||
editor: Editor
|
||||
}> = ({ format, label, editor }) => {
|
||||
const isActive =
|
||||
Editor?.nodes(editor, {
|
||||
match: (n) => n?.align === format
|
||||
})?.length > 0
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`toolbar-button ${isActive ? 'active' : ''}`}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault()
|
||||
Transforms?.setNodes(
|
||||
editor,
|
||||
{ align: format },
|
||||
{ match: (n) => Editor?.isBlock(editor, n) }
|
||||
)
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const ToolbarButtonCodeLink: React.FC<{
|
||||
format: FormatMark | string
|
||||
label: string
|
||||
editor: Editor
|
||||
children: React.ReactNode
|
||||
}> = ({ format, label, editor, children }) => {
|
||||
useSlate()
|
||||
|
||||
let isActive = false
|
||||
try {
|
||||
if (format === 'link') {
|
||||
isActive = !!Editor?.marks(editor)?.link
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`toolbar-button ${isActive ? 'active' : ''}`}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault()
|
||||
const isActive2 = !!Editor?.marks(editor)?.link
|
||||
if (isActive2) {
|
||||
Editor?.removeMark(editor, 'link')
|
||||
return
|
||||
}
|
||||
// const url = window.prompt('Enter the URL of the link:')
|
||||
setOpen(true)
|
||||
}}
|
||||
>
|
||||
{children ? children : label}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// Create a toggleBlock function and an isBlockActive function to handle block elements
|
||||
const toggleBlock = (editor: Editor, format: string) => {
|
||||
const isActive = isBlockActive(editor, format)
|
||||
Transforms?.unwrapNodes(editor, {
|
||||
match: (n) => Editor?.isBlock(editor, n),
|
||||
split: true
|
||||
})
|
||||
|
||||
if (isActive) {
|
||||
Transforms?.setNodes(editor, { type: 'paragraph' })
|
||||
} else {
|
||||
Transforms?.setNodes(editor, { type: format })
|
||||
}
|
||||
}
|
||||
|
||||
const isBlockActive = (editor: Editor, format: string) => {
|
||||
const [match] = Editor?.nodes(editor, {
|
||||
match: (n) => n?.type === format
|
||||
})
|
||||
return !!match
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (event.key === 'Enter' && isBlockActive(editor, 'code-block')) {
|
||||
event.preventDefault()
|
||||
editor?.insertText('\n')
|
||||
}
|
||||
|
||||
if (event.key === 'ArrowDown' && isBlockActive(editor, 'code-block')) {
|
||||
event.preventDefault()
|
||||
Transforms?.insertNodes(editor, {
|
||||
type: 'paragraph',
|
||||
children: [{ text: '' }]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = (newValue: Descendant[]) => {
|
||||
setValue(newValue)
|
||||
}
|
||||
|
||||
const toggleLink = (editor: Editor, url: string) => {
|
||||
const { selection } = editor
|
||||
|
||||
if (selection && !Range.isCollapsed(selection)) {
|
||||
const isLink = Editor?.marks(editor)?.link === true
|
||||
const isInsideLink = isLinkActive(editor)
|
||||
|
||||
if (isLink) {
|
||||
Editor?.removeMark(editor, 'link')
|
||||
} else if (url) {
|
||||
Editor?.addMark(editor, 'link', url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const initialValue = 'qortal://'
|
||||
const [inputValue, setInputValue] = useState(initialValue)
|
||||
|
||||
const handleChangeLink = (event) => {
|
||||
const newValue = event?.target?.value
|
||||
if (newValue?.startsWith(initialValue)) {
|
||||
setInputValue(newValue)
|
||||
}
|
||||
}
|
||||
const isLinkActive = (editor: Editor) => {
|
||||
const [link] = Editor?.nodes(editor, {
|
||||
match: (n) => n?.type === 'link'
|
||||
})
|
||||
return !!link
|
||||
}
|
||||
const handleSaveClick = () => {
|
||||
const marks = Editor?.marks(editor)
|
||||
const isLink = marks?.link === true
|
||||
|
||||
if (isLink) {
|
||||
Editor?.removeMark(editor, 'link')
|
||||
return // Return early to skip the rest of the function
|
||||
}
|
||||
toggleLink(editor, inputValue)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const handlePaste = (event: React.ClipboardEvent) => {
|
||||
event.preventDefault()
|
||||
const text = event?.clipboardData?.getData('text/plain')
|
||||
const isCodeBlock = isBlockActive(editor, 'code-block')
|
||||
|
||||
if (isCodeBlock) {
|
||||
const lines = text?.split('\n')
|
||||
const fragment: Descendant[] = [
|
||||
{
|
||||
type: 'code-block',
|
||||
children: lines?.map((line) => ({
|
||||
type: 'code-line',
|
||||
children: [{ text: line }]
|
||||
}))
|
||||
}
|
||||
]
|
||||
|
||||
Transforms?.insertFragment(editor, fragment)
|
||||
} else if (text) {
|
||||
const fragment = text?.split('\n').map((line) => ({
|
||||
type: 'paragraph',
|
||||
children: [{ text: line }]
|
||||
}))
|
||||
|
||||
Transforms?.insertFragment(editor, fragment)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
border: '1px solid',
|
||||
borderRadius: '5px',
|
||||
marginTop: '20px',
|
||||
padding: '10px'
|
||||
}}
|
||||
>
|
||||
<Slate
|
||||
editor={editor}
|
||||
value={newValue}
|
||||
onChange={(newValue) => handleChange(newValue)}
|
||||
key={editorKey || 1}
|
||||
>
|
||||
<div className="toolbar">
|
||||
<ToolbarButton format="bold" label="B" editor={editor}>
|
||||
<BoldSVG height="24px" width="auto" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton format="italic" label="I" editor={editor}>
|
||||
<ItalicSVG height="24px" width="auto" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton format="underline" label="U" editor={editor}>
|
||||
<UnderlineSVG height="24px" width="auto" />
|
||||
</ToolbarButton>
|
||||
|
||||
<ToolbarButton format="heading-2" label="H2" editor={editor}>
|
||||
<H2SVG height="24px" width="auto" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton format="heading-3" label="H3" editor={editor}>
|
||||
<H3SVG height="24px" width="auto" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton format="align-left" label="L" editor={editor}>
|
||||
<AlignLeftSVG height="24px" width="auto" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton format="align-center" label="C" editor={editor}>
|
||||
<AlignCenterSVG height="24px" width="auto" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton format="align-right" label="R" editor={editor}>
|
||||
<AlignRightSVG height="24px" width="auto" />
|
||||
</ToolbarButton>
|
||||
|
||||
<ToolbarButtonCodeBlock
|
||||
format="code-block"
|
||||
label="Code"
|
||||
editor={editor}
|
||||
>
|
||||
<CodeBlockSVG height="24px" width="auto" />
|
||||
</ToolbarButtonCodeBlock>
|
||||
<ToolbarButtonCodeLink format="link" label="Link" editor={editor}>
|
||||
<LinkSVG height="24px" width="auto" />
|
||||
</ToolbarButtonCodeLink>
|
||||
</div>
|
||||
<Editable
|
||||
className="blog-editor"
|
||||
renderElement={(props) => renderElement({ ...props, mode })}
|
||||
renderLeaf={renderLeaf}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={handlePaste}
|
||||
mode={mode}
|
||||
/>
|
||||
</Slate>
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<ModalBox>
|
||||
<TextField
|
||||
label="Link"
|
||||
value={inputValue}
|
||||
onChange={handleChangeLink}
|
||||
/>
|
||||
<Button variant="contained" onClick={handleSaveClick}>
|
||||
Save
|
||||
</Button>
|
||||
</ModalBox>
|
||||
</Modal>
|
||||
{editPostSection && (
|
||||
<Button onClick={() => editPostSection(value, section)}>
|
||||
Edit Section
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogEditor
|
||||
|
||||
type ExtendedRenderElementProps = RenderElementProps & { mode?: string }
|
||||
|
||||
export const renderElement = ({
|
||||
attributes,
|
||||
children,
|
||||
element,
|
||||
mode
|
||||
}: ExtendedRenderElementProps) => {
|
||||
switch (element.type) {
|
||||
case 'block-quote':
|
||||
return <blockquote {...attributes}>{children}</blockquote>
|
||||
case 'heading-2':
|
||||
return (
|
||||
<h2
|
||||
className="h2"
|
||||
{...attributes}
|
||||
style={{ textAlign: element.textAlign }}
|
||||
>
|
||||
{children}
|
||||
</h2>
|
||||
)
|
||||
case 'heading-3':
|
||||
return (
|
||||
<h3
|
||||
className="h3"
|
||||
{...attributes}
|
||||
style={{ textAlign: element.textAlign }}
|
||||
>
|
||||
{children}
|
||||
</h3>
|
||||
)
|
||||
case 'code-block':
|
||||
return (
|
||||
<pre {...attributes} className="code-block">
|
||||
<code>{children}</code>
|
||||
</pre>
|
||||
)
|
||||
case 'code-line':
|
||||
return <div {...attributes}>{children}</div>
|
||||
case 'link':
|
||||
return (
|
||||
<a href={element.url} {...attributes}>
|
||||
{children}
|
||||
</a>
|
||||
)
|
||||
default:
|
||||
return (
|
||||
<p
|
||||
className={`paragraph${mode ? `-${mode}` : ''}`}
|
||||
{...attributes}
|
||||
style={{ textAlign: element.textAlign }}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const renderLeaf = ({ attributes, children, leaf }: RenderLeafProps) => {
|
||||
let el = children
|
||||
|
||||
if (leaf.bold) {
|
||||
el = <strong>{el}</strong>
|
||||
}
|
||||
|
||||
if (leaf.italic) {
|
||||
el = <em>{el}</em>
|
||||
}
|
||||
|
||||
if (leaf.underline) {
|
||||
el = <u>{el}</u>
|
||||
}
|
||||
|
||||
if (leaf.link) {
|
||||
el = (
|
||||
<a href={leaf.link} {...attributes}>
|
||||
{el}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
return <span {...attributes}>{el}</span>
|
||||
}
|
25
src/components/editor/ReadOnlySlate.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { createEditor, Descendant, Editor } from 'slate';
|
||||
import { withReact, Slate, Editable, RenderElementProps, RenderLeafProps } from 'slate-react';
|
||||
import { renderElement, renderLeaf } from './BlogEditor';
|
||||
|
||||
interface ReadOnlySlateProps {
|
||||
content: any
|
||||
mode?: string
|
||||
}
|
||||
const ReadOnlySlate: React.FC<ReadOnlySlateProps> = ({ content, mode }) => {
|
||||
const editor = useMemo(() => withReact(createEditor()), [])
|
||||
const value = useMemo(() => content, [content])
|
||||
|
||||
return (
|
||||
<Slate editor={editor} value={value} onChange={() => {}}>
|
||||
<Editable
|
||||
readOnly
|
||||
renderElement={(props) => renderElement({ ...props, mode })}
|
||||
renderLeaf={renderLeaf}
|
||||
/>
|
||||
</Slate>
|
||||
)
|
||||
}
|
||||
|
||||
export default ReadOnlySlate;
|
47
src/components/editor/customTypes.ts
Normal file
@ -0,0 +1,47 @@
|
||||
// src/customTypes.ts
|
||||
import { BaseEditor } from 'slate';
|
||||
import { ReactEditor } from 'slate-react';
|
||||
|
||||
export type CustomText = {
|
||||
text: string
|
||||
bold?: boolean
|
||||
italic?: boolean
|
||||
underline?: boolean
|
||||
code?: boolean
|
||||
}
|
||||
|
||||
export type HeadingElement = {
|
||||
type: 'heading'
|
||||
children: CustomText[]
|
||||
}
|
||||
|
||||
export type BlockQuoteElement = {
|
||||
type: 'block-quote'
|
||||
children: CustomText[]
|
||||
}
|
||||
|
||||
export type ParagraphElement = {
|
||||
type: 'paragraph'
|
||||
children: CustomText[]
|
||||
}
|
||||
|
||||
export type CodeBlockElement = {
|
||||
type: 'code-block'
|
||||
children: CustomText[]
|
||||
}
|
||||
|
||||
export type CustomElement =
|
||||
| HeadingElement
|
||||
| BlockQuoteElement
|
||||
| ParagraphElement
|
||||
| CodeBlockElement
|
||||
|
||||
export type FormatMark = 'bold' | 'italic' | 'underline' | 'code'
|
||||
|
||||
declare module 'slate' {
|
||||
interface CustomTypes {
|
||||
Editor: BaseEditor & ReactEditor;
|
||||
Element: CustomElement;
|
||||
Text: CustomText;
|
||||
}
|
||||
}
|
211
src/components/layout/Navbar/Navbar-styles.tsx
Normal file
@ -0,0 +1,211 @@
|
||||
import { AppBar, Button, Typography, Box, Popover } from "@mui/material";
|
||||
import { styled } from "@mui/system";
|
||||
import { LightModeSVG } from "../../../assets/svgs/LightModeSVG";
|
||||
import { DarkModeSVG } from "../../../assets/svgs/DarkModeSVG";
|
||||
import { StorefrontSVG } from "../../../assets/svgs/StorefrontSVG";
|
||||
import { CartSVG } from "../../../assets/svgs/CartSVG";
|
||||
|
||||
export const CustomAppBar = styled(AppBar)(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
padding: "5px 16px",
|
||||
backgroundImage: "none",
|
||||
borderBottom: `1px solid ${theme.palette.primary.light}`,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
[theme.breakpoints.only("xs")]: {
|
||||
gap: "15px"
|
||||
}
|
||||
}));
|
||||
|
||||
export const QShopLogoContainer = styled("img")({
|
||||
width: "12%",
|
||||
minWidth: "50px",
|
||||
height: "auto",
|
||||
padding: "2px 0",
|
||||
userSelect: "none",
|
||||
objectFit: "contain",
|
||||
cursor: "pointer"
|
||||
});
|
||||
|
||||
export const CustomTitle = styled(Typography)({
|
||||
fontWeight: 600,
|
||||
color: "#000000"
|
||||
});
|
||||
|
||||
export const StoreManagerIcon = styled(StorefrontSVG)(({ theme }) => ({
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
filter:
|
||||
theme.palette.mode === "dark"
|
||||
? "drop-shadow(0px 4px 6px rgba(255, 255, 255, 0.6))"
|
||||
: "drop-shadow(0px 4px 6px rgba(99, 88, 88, 0.1))"
|
||||
}
|
||||
}));
|
||||
|
||||
export const CartIcon = styled(CartSVG)(({ theme }) => ({
|
||||
cursor: "pointer",
|
||||
"&:hover": {
|
||||
filter:
|
||||
theme.palette.mode === "dark"
|
||||
? "drop-shadow(0px 4px 6px rgba(255, 255, 255, 0.6))"
|
||||
: "drop-shadow(0px 4px 6px rgba(99, 88, 88, 0.1))"
|
||||
}
|
||||
}));
|
||||
|
||||
export const CreateBlogButton = styled(Button)(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
padding: "8px 15px",
|
||||
borderRadius: "40px",
|
||||
gap: "4px",
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
color: "#fff",
|
||||
fontFamily: "Raleway",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
boxShadow: "none",
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
boxShadow: "rgba(0, 0, 0, 0.15) 1.95px 1.95px 2.6px;",
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
filter: "brightness(1.1)"
|
||||
}
|
||||
}));
|
||||
|
||||
export const AuthenticateButton = styled(Button)(({ theme }) => ({
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
padding: "8px 15px",
|
||||
borderRadius: "40px",
|
||||
gap: "4px",
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
color: "#fff",
|
||||
fontFamily: "Raleway",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
boxShadow: "none",
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
boxShadow: "rgba(0, 0, 0, 0.15) 1.95px 1.95px 2.6px;",
|
||||
backgroundColor: theme.palette.secondary.dark,
|
||||
filter: "brightness(1.1)"
|
||||
}
|
||||
}));
|
||||
|
||||
export const AvatarContainer = styled(Box)({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
"& #expand-icon": {
|
||||
transition: "all 0.3s ease-in-out",
|
||||
filter: "brightness(0.7)"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const DropdownContainer = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "5px",
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
padding: "10px 15px",
|
||||
transition: "all 0.4s ease-in-out",
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
filter:
|
||||
theme.palette.mode === "light" ? "brightness(0.95)" : "brightness(1.7)"
|
||||
}
|
||||
}));
|
||||
|
||||
export const DropdownText = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Raleway",
|
||||
fontSize: "16px",
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: "none"
|
||||
}));
|
||||
|
||||
export const NavbarName = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Raleway",
|
||||
fontSize: "18px",
|
||||
color: theme.palette.text.primary,
|
||||
margin: "0 10px"
|
||||
}));
|
||||
|
||||
export const ThemeSelectRow = styled(Box)({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "5px",
|
||||
flexBasis: 0
|
||||
});
|
||||
|
||||
export const LightModeIcon = styled(LightModeSVG)(({ theme }) => ({
|
||||
transition: "all 0.1s ease-in-out",
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
filter:
|
||||
theme.palette.mode === "dark"
|
||||
? "drop-shadow(0px 4px 6px rgba(255, 255, 255, 0.6))"
|
||||
: "drop-shadow(0px 4px 6px rgba(99, 88, 88, 0.1))"
|
||||
}
|
||||
}));
|
||||
|
||||
export const DarkModeIcon = styled(DarkModeSVG)(({ theme }) => ({
|
||||
transition: "all 0.1s ease-in-out",
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
filter:
|
||||
theme.palette.mode === "dark"
|
||||
? "drop-shadow(0px 4px 6px rgba(255, 255, 255, 0.6))"
|
||||
: "drop-shadow(0px 4px 6px rgba(99, 88, 88, 0.1))"
|
||||
}
|
||||
}));
|
||||
|
||||
export const StoresButton = styled(Button)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
textTransform: "none",
|
||||
fontFamily: "Raleway",
|
||||
gap: "5px",
|
||||
fontSize: "17px",
|
||||
borderRadius: "5px",
|
||||
border: "none",
|
||||
color: theme.palette.text.primary,
|
||||
padding: "2px 15px",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
boxShadow:
|
||||
"rgba(50, 50, 93, 0.25) 0px 2px 5px -1px, rgba(0, 0, 0, 0.3) 0px 1px 3px -1px;",
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
backgroundColor: theme.palette.secondary.dark,
|
||||
boxShadow:
|
||||
"rgba(50, 50, 93, 0.35) 0px 3px 5px -1px, rgba(0, 0, 0, 0.4) 0px 2px 3px -1px;"
|
||||
}
|
||||
}));
|
||||
|
||||
export const CustomPopover = styled(Popover)(({ theme }) => ({
|
||||
maxHeight: "400px",
|
||||
overflowY: "auto",
|
||||
"&::-webkit-scrollbar-track": {
|
||||
backgroundColor: "transparent"
|
||||
},
|
||||
"&::-webkit-scrollbar-track:hover": {
|
||||
backgroundColor: "transparent"
|
||||
},
|
||||
"&::-webkit-scrollbar": {
|
||||
width: "8px",
|
||||
height: "10px",
|
||||
backgroundColor: "transparent"
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: theme.palette.mode === "light" ? "#d3d9e1" : "#414763",
|
||||
borderRadius: "8px",
|
||||
backgroundClip: "content-box",
|
||||
border: "4px solid transparent"
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb:hover": {
|
||||
backgroundColor: theme.palette.mode === "light" ? "#b7bcc4" : "#40455f"
|
||||
}
|
||||
}));
|
288
src/components/layout/Navbar/Navbar.tsx
Normal file
@ -0,0 +1,288 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
import { RootState } from "../../../state/store";
|
||||
import { useSelector } from "react-redux";
|
||||
import { Box, Popover, useTheme } from "@mui/material";
|
||||
import ExitToAppIcon from "@mui/icons-material/ExitToApp";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
resetProducts,
|
||||
toggleCreateStoreModal
|
||||
} from "../../../state/features/globalSlice";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { BlockedNamesModal } from "../../common/BlockedNamesModal/BlockedNamesModal";
|
||||
import EmailIcon from "@mui/icons-material/Email";
|
||||
import {
|
||||
AvatarContainer,
|
||||
CustomAppBar,
|
||||
DropdownContainer,
|
||||
DropdownText,
|
||||
AuthenticateButton,
|
||||
NavbarName,
|
||||
LightModeIcon,
|
||||
DarkModeIcon,
|
||||
ThemeSelectRow,
|
||||
QShopLogoContainer,
|
||||
StoreManagerIcon,
|
||||
StoresButton
|
||||
} from "./Navbar-styles";
|
||||
import { AccountCircleSVG } from "../../../assets/svgs/AccountCircleSVG";
|
||||
import QShopLogo from "../../../assets/img/QShopLogo.webp";
|
||||
import QShopLogoLight from "../../../assets/img/QShopLogoLight.webp";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import PersonOffIcon from "@mui/icons-material/PersonOff";
|
||||
|
||||
import { Store } from "../../../state/features/storeSlice";
|
||||
import { OrdersSVG } from "../../../assets/svgs/OrdersSVG";
|
||||
import { resetOrders } from "../../../state/features/orderSlice";
|
||||
interface Props {
|
||||
isAuthenticated: boolean;
|
||||
userName: string | null;
|
||||
userAvatar: string;
|
||||
authenticate: () => void;
|
||||
hasAttemptedToFetchShopInitial: boolean;
|
||||
setTheme: (val: string) => void;
|
||||
}
|
||||
|
||||
const NavBar: React.FC<Props> = ({
|
||||
isAuthenticated,
|
||||
userName,
|
||||
userAvatar,
|
||||
authenticate,
|
||||
hasAttemptedToFetchShopInitial,
|
||||
setTheme
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
|
||||
// Get All My Stores from Redux To Display In Store Manager Dropdown
|
||||
|
||||
const myStores = useSelector((state: RootState) => state.store.myStores);
|
||||
const hashMapStores = useSelector(
|
||||
(state: RootState) => state.store.hashMapStores
|
||||
);
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
const [isOpenBlockedNamesModal, setIsOpenBlockedNamesModal] =
|
||||
useState<boolean>(false);
|
||||
const [openStoreManagerDropdown, setOpenStoreManagerDropdown] =
|
||||
useState<boolean>(false);
|
||||
const [openUserDropdown, setOpenUserDropdown] = useState<boolean>(false);
|
||||
|
||||
const searchValRef = useRef("");
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleClick = (event?: React.MouseEvent<HTMLDivElement>) => {
|
||||
const target = event?.currentTarget as unknown as HTMLButtonElement | null;
|
||||
setAnchorEl(target);
|
||||
};
|
||||
|
||||
const handleCloseUserDropdown = () => {
|
||||
setAnchorEl(null);
|
||||
setOpenUserDropdown(false);
|
||||
};
|
||||
|
||||
const handleCloseStoreDropdown = () => {
|
||||
setAnchorEl(null);
|
||||
setOpenStoreManagerDropdown(false);
|
||||
};
|
||||
|
||||
const onCloseBlockedNames = () => {
|
||||
setIsOpenBlockedNamesModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<CustomAppBar position="sticky" elevation={2}>
|
||||
<ThemeSelectRow>
|
||||
{theme.palette.mode === "dark" ? (
|
||||
<LightModeIcon
|
||||
onClickFunc={() => setTheme("light")}
|
||||
color="white"
|
||||
height="22"
|
||||
width="22"
|
||||
/>
|
||||
) : (
|
||||
<DarkModeIcon
|
||||
onClickFunc={() => setTheme("dark")}
|
||||
color="black"
|
||||
height="22"
|
||||
width="22"
|
||||
/>
|
||||
)}
|
||||
<QShopLogoContainer
|
||||
src={theme.palette.mode === "dark" ? QShopLogoLight : QShopLogo}
|
||||
alt="QShop Logo"
|
||||
onClick={() => {
|
||||
navigate(`/`);
|
||||
searchValRef.current = "";
|
||||
if (!inputRef.current) return;
|
||||
inputRef.current.value = "";
|
||||
}}
|
||||
/>
|
||||
</ThemeSelectRow>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px"
|
||||
}}
|
||||
>
|
||||
{!isAuthenticated && (
|
||||
<AuthenticateButton onClick={authenticate}>
|
||||
<ExitToAppIcon />
|
||||
Authenticate
|
||||
</AuthenticateButton>
|
||||
)}
|
||||
{isAuthenticated && userName && hasAttemptedToFetchShopInitial && (
|
||||
<StoresButton
|
||||
onClick={(e: any) => {
|
||||
if (myStores.length > 0) {
|
||||
handleClick(e);
|
||||
setOpenStoreManagerDropdown(true);
|
||||
} else {
|
||||
dispatch(toggleCreateStoreModal(true));
|
||||
}
|
||||
}}
|
||||
>
|
||||
My Stores
|
||||
<StoreManagerIcon
|
||||
color={theme.palette.text.primary}
|
||||
height={"32"}
|
||||
width={"32"}
|
||||
/>
|
||||
</StoresButton>
|
||||
)}
|
||||
{isAuthenticated && userName && (
|
||||
<>
|
||||
<AvatarContainer
|
||||
onClick={(e: any) => {
|
||||
handleClick(e);
|
||||
setOpenUserDropdown(true);
|
||||
}}
|
||||
>
|
||||
<NavbarName>{userName}</NavbarName>
|
||||
{!userAvatar ? (
|
||||
<AccountCircleSVG
|
||||
color={theme.palette.text.primary}
|
||||
width="32"
|
||||
height="32"
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={userAvatar}
|
||||
alt="User Avatar"
|
||||
width="32"
|
||||
height="32"
|
||||
style={{
|
||||
borderRadius: "50%"
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ExpandMoreIcon id="expand-icon" sx={{ color: "#ACB6BF" }} />
|
||||
</AvatarContainer>
|
||||
</>
|
||||
)}
|
||||
<Popover
|
||||
id={"store-manager-popover"}
|
||||
open={openStoreManagerDropdown}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleCloseStoreDropdown}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left"
|
||||
}}
|
||||
>
|
||||
<DropdownContainer>
|
||||
<DropdownText
|
||||
onClick={() => {
|
||||
dispatch(toggleCreateStoreModal(true));
|
||||
handleCloseStoreDropdown();
|
||||
}}
|
||||
>
|
||||
Create Store
|
||||
</DropdownText>
|
||||
</DropdownContainer>
|
||||
{myStores.length > 0 &&
|
||||
myStores.map((store: Store) => (
|
||||
<DropdownContainer key={store.id}>
|
||||
<DropdownText
|
||||
onClick={() => {
|
||||
dispatch(resetOrders());
|
||||
dispatch(resetProducts());
|
||||
navigate(`/${userName}/${store.id}`);
|
||||
handleCloseStoreDropdown();
|
||||
}}
|
||||
>
|
||||
{hashMapStores[store.id]?.title}
|
||||
</DropdownText>
|
||||
</DropdownContainer>
|
||||
))}
|
||||
</Popover>
|
||||
<Popover
|
||||
id={"user-popover"}
|
||||
open={openUserDropdown}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleCloseUserDropdown}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left"
|
||||
}}
|
||||
>
|
||||
<DropdownContainer
|
||||
onClick={() => {
|
||||
handleCloseUserDropdown();
|
||||
handleCloseStoreDropdown();
|
||||
navigate("/my-orders");
|
||||
}}
|
||||
>
|
||||
<OrdersSVG color={"#f9ff34"} height={"22"} width={"22"} />
|
||||
<DropdownText>My Orders</DropdownText>
|
||||
</DropdownContainer>
|
||||
<DropdownContainer
|
||||
onClick={() => {
|
||||
setIsOpenBlockedNamesModal(true);
|
||||
handleCloseUserDropdown();
|
||||
handleCloseStoreDropdown();
|
||||
}}
|
||||
>
|
||||
<PersonOffIcon
|
||||
sx={{
|
||||
color: "#e35050"
|
||||
}}
|
||||
/>
|
||||
<DropdownText>Blocked Names</DropdownText>
|
||||
</DropdownContainer>
|
||||
<DropdownContainer>
|
||||
<a
|
||||
href="qortal://APP/Q-Mail"
|
||||
className="qortal-link"
|
||||
style={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
gap: "5px",
|
||||
alignItems: "center",
|
||||
textDecoration: "none"
|
||||
}}
|
||||
>
|
||||
<EmailIcon
|
||||
sx={{
|
||||
color: "#50e3c2"
|
||||
}}
|
||||
/>
|
||||
|
||||
<DropdownText>Q-Mail</DropdownText>
|
||||
</a>
|
||||
</DropdownContainer>
|
||||
</Popover>
|
||||
{isOpenBlockedNamesModal && (
|
||||
<BlockedNamesModal
|
||||
open={isOpenBlockedNamesModal}
|
||||
onClose={onCloseBlockedNames}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</CustomAppBar>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavBar;
|
70
src/components/modals/ConsentModal.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import * as React from "react";
|
||||
import Button from "@mui/material/Button";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogContentText from "@mui/material/DialogContentText";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import localForage from "localforage";
|
||||
import { useTheme } from "@mui/material";
|
||||
const generalLocal = localForage.createInstance({
|
||||
name: "q-blog-general"
|
||||
});
|
||||
|
||||
export default function ConsentModal() {
|
||||
const theme = useTheme();
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const getIsConsented = React.useCallback(async () => {
|
||||
try {
|
||||
const hasConsented = await generalLocal.getItem("general-consent");
|
||||
if (hasConsented) return;
|
||||
|
||||
setOpen(true);
|
||||
generalLocal.setItem("general-consent", true);
|
||||
} catch (error) {}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
getIsConsented();
|
||||
}, []);
|
||||
return (
|
||||
<div>
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">Welcome</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
The Qortal community, along with its development team and the
|
||||
creators of this application, cannot be held accountable for any
|
||||
content published or displayed. Furthermore, they bear no
|
||||
responsibility for any data loss that may occur as a result of using
|
||||
this application.
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
sx={{
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
color: theme.palette.text.primary,
|
||||
fontFamily: "Raleway"
|
||||
}}
|
||||
onClick={handleClose}
|
||||
autoFocus
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
197
src/components/modals/CreateStoreModal-styles.tsx
Normal file
@ -0,0 +1,197 @@
|
||||
import { styled } from "@mui/system";
|
||||
import { Box, Button, TextField, Theme, Typography } from "@mui/material";
|
||||
import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternate";
|
||||
import { TimesSVG } from "../../assets/svgs/TimesSVG";
|
||||
import { NumericTextFieldQshop } from "../common/NumericTextFieldQshop";
|
||||
import { DownloadSVG } from "../../assets/svgs/DownloadSVG";
|
||||
|
||||
export const ModalBody = styled(Box)(({ theme }) => ({
|
||||
position: "absolute",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
borderRadius: "4px",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
width: "75%",
|
||||
padding: "15px 35px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "17px",
|
||||
overflowY: "auto",
|
||||
maxHeight: "95vh",
|
||||
boxShadow:
|
||||
theme.palette.mode === "dark"
|
||||
? "0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)"
|
||||
: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px",
|
||||
"&::-webkit-scrollbar-track": {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
"&::-webkit-scrollbar-track:hover": {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
"&::-webkit-scrollbar": {
|
||||
width: "16px",
|
||||
height: "10px",
|
||||
backgroundColor: theme.palette.mode === "light" ? "#f6f8fa" : "#292d3e",
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb": {
|
||||
backgroundColor: theme.palette.mode === "light" ? "#d3d9e1" : "#575757",
|
||||
borderRadius: "8px",
|
||||
backgroundClip: "content-box",
|
||||
border: "4px solid transparent",
|
||||
},
|
||||
"&::-webkit-scrollbar-thumb:hover": {
|
||||
backgroundColor: theme.palette.mode === "light" ? "#b7bcc4" : "#474646",
|
||||
},
|
||||
}));
|
||||
|
||||
export const ModalTitle = styled(Typography)(({ theme }) => ({
|
||||
fontWeight: 400,
|
||||
fontFamily: "Raleway",
|
||||
fontSize: "25px",
|
||||
userSelect: "none",
|
||||
}));
|
||||
|
||||
export const StoreLogoPreview = styled("img")(({ theme }) => ({
|
||||
width: "100px",
|
||||
height: "100px",
|
||||
objectFit: "contain",
|
||||
userSelect: "none",
|
||||
borderRadius: "3px",
|
||||
marginBottom: "10px",
|
||||
}));
|
||||
|
||||
export const AddLogoButton = styled(Button)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
color: "#fff",
|
||||
fontFamily: "Raleway",
|
||||
fontSize: "17px",
|
||||
padding: "5px 10px",
|
||||
borderRadius: "5px",
|
||||
gap: "5px",
|
||||
border: "none",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
boxShadow:
|
||||
theme.palette.mode === "dark"
|
||||
? "0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)"
|
||||
: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px",
|
||||
marginBottom: "5px",
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
boxShadow:
|
||||
theme.palette.mode === "dark"
|
||||
? "0px 8px 10px 1px hsla(0,0%,0%,0.14), 0px 3px 14px 2px hsla(0,0%,0%,0.12), 0px 5px 5px -3px hsla(0,0%,0%,0.2)"
|
||||
: "rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;",
|
||||
backgroundColor: theme.palette.secondary.dark,
|
||||
},
|
||||
}));
|
||||
|
||||
export const LogoPreviewRow = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
}));
|
||||
|
||||
export const AddLogoIcon = styled(AddPhotoAlternateIcon)(({ theme }) => ({
|
||||
color: "#fff",
|
||||
height: "25px",
|
||||
width: "auto",
|
||||
}));
|
||||
|
||||
export const TimesIcon = styled(TimesSVG)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: "50%",
|
||||
padding: "5px",
|
||||
transition: "all 0.2s ease-in-out",
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
scale: "1.1",
|
||||
},
|
||||
}));
|
||||
|
||||
const customInputStyle = (theme: Theme) => {
|
||||
return {
|
||||
fontFamily: "Karla",
|
||||
fontSize: "18px",
|
||||
fontWeight: 300,
|
||||
color: theme.palette.text.primary,
|
||||
backgroundColor: theme.palette.background.default,
|
||||
borderColor: theme.palette.background.paper,
|
||||
"& label": {
|
||||
color: theme.palette.mode === "light" ? "#808183" : "#edeef0",
|
||||
fontFamily: "Karla",
|
||||
fontSize: "18px",
|
||||
letterSpacing: "0px",
|
||||
},
|
||||
"& label.Mui-focused": {
|
||||
color: theme.palette.mode === "light" ? "#A0AAB4" : "#d7d8da",
|
||||
},
|
||||
"& .MuiInput-underline:after": {
|
||||
borderBottomColor: theme.palette.mode === "light" ? "#B2BAC2" : "#c9cccf",
|
||||
},
|
||||
"& .MuiOutlinedInput-root": {
|
||||
"& fieldset": {
|
||||
borderColor: "#E0E3E7",
|
||||
},
|
||||
"&:hover fieldset": {
|
||||
borderColor: "#B2BAC2",
|
||||
},
|
||||
"&.Mui-focused fieldset": {
|
||||
borderColor: "#6F7E8C",
|
||||
},
|
||||
},
|
||||
"& .MuiInputBase-root": {
|
||||
fontFamily: "Karla",
|
||||
fontSize: "18px",
|
||||
letterSpacing: "0px",
|
||||
},
|
||||
"& .MuiFilledInput-root:after": {
|
||||
borderBottomColor: theme.palette.secondary.main,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const CustomInputField = styled(TextField)(({ theme }) =>
|
||||
customInputStyle(theme as Theme)
|
||||
);
|
||||
|
||||
export const CustomNumberField = styled(NumericTextFieldQshop)(({ theme }) =>
|
||||
customInputStyle(theme as Theme)
|
||||
);
|
||||
|
||||
export const ButtonRow = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
gap: "10px",
|
||||
justifyContent: "flex-end",
|
||||
}));
|
||||
|
||||
export const CancelButton = styled(Button)(({ theme }) => ({
|
||||
fontFamily: "Raleway",
|
||||
fontSize: "15px",
|
||||
}));
|
||||
|
||||
export const CreateButton = styled(Button)(({ theme }) => ({
|
||||
fontFamily: "Raleway",
|
||||
fontSize: "15px",
|
||||
backgroundColor: "#32d43a",
|
||||
color: "black",
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
backgroundColor: "#2bb131",
|
||||
},
|
||||
}));
|
||||
|
||||
export const WalletRow = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
}));
|
||||
|
||||
export const DownloadArrrWalletIcon = styled(DownloadSVG)(({ theme }) => ({
|
||||
padding: "5px 7px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
},
|
||||
}));
|
419
src/components/modals/CreateStoreModal.tsx
Normal file
@ -0,0 +1,419 @@
|
||||
import { FC, ChangeEvent, useState, useEffect } from "react";
|
||||
import {
|
||||
Typography,
|
||||
Modal,
|
||||
FormControl,
|
||||
useTheme,
|
||||
IconButton,
|
||||
Zoom,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { setIsLoadingGlobal, toggleCreateStoreModal } from "../../state/features/globalSlice";
|
||||
import ImageUploader from "../common/ImageUploader";
|
||||
import {
|
||||
ModalTitle,
|
||||
StoreLogoPreview,
|
||||
AddLogoButton,
|
||||
AddLogoIcon,
|
||||
TimesIcon,
|
||||
LogoPreviewRow,
|
||||
CustomInputField,
|
||||
ModalBody,
|
||||
ButtonRow,
|
||||
CancelButton,
|
||||
CreateButton,
|
||||
WalletRow,
|
||||
DownloadArrrWalletIcon,
|
||||
} from "./CreateStoreModal-styles";
|
||||
import {
|
||||
FilterSelect,
|
||||
FilterSelectMenuItems,
|
||||
FiltersCheckbox,
|
||||
FiltersChip,
|
||||
FiltersOption,
|
||||
} from "../../pages/Store/Store/Store-styles";
|
||||
import { supportedCoinsArray } from "../../constants/supported-coins";
|
||||
import { QortalSVG } from "../../assets/svgs/QortalSVG";
|
||||
import { ARRRSVG } from "../../assets/svgs/ARRRSVG";
|
||||
import { setNotification } from "../../state/features/notificationsSlice";
|
||||
export interface ForeignCoins {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface onPublishParam {
|
||||
title: string;
|
||||
description: string;
|
||||
shipsTo: string;
|
||||
location: string;
|
||||
storeIdentifier: string;
|
||||
logo: string;
|
||||
foreignCoins: ForeignCoins;
|
||||
supportedCoins: string[];
|
||||
}
|
||||
|
||||
interface CreateStoreModalProps {
|
||||
open: boolean;
|
||||
closeCreateStoreModal: boolean;
|
||||
setCloseCreateStoreModal: (val: boolean) => void;
|
||||
onPublish: (param: onPublishParam) => Promise<void>;
|
||||
username: string;
|
||||
}
|
||||
|
||||
|
||||
const CreateStoreModal: React.FC<CreateStoreModalProps> = ({
|
||||
open,
|
||||
closeCreateStoreModal,
|
||||
setCloseCreateStoreModal,
|
||||
onPublish,
|
||||
username,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
|
||||
const [title, setTitle] = useState<string>("");
|
||||
const [description, setDescription] = useState<string>("");
|
||||
const [location, setLocation] = useState<string>("");
|
||||
const [shipsTo, setShipsTo] = useState<string>("");
|
||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||
const [storeIdentifier, setStoreIdentifier] = useState("");
|
||||
const [logo, setLogo] = useState<string | null>(null);
|
||||
const [supportedCoinsSelected, setSupportedCoinsSelected] = useState<
|
||||
string[]
|
||||
>(["QORT"]);
|
||||
const [qortWalletAddress, setQortWalletAddress] = useState<string>("");
|
||||
const [arrrWalletAddress, setArrrWalletAddress] = useState<string>("");
|
||||
|
||||
const handlePublish = async (): Promise<void> => {
|
||||
try {
|
||||
setErrorMessage("");
|
||||
if (!logo) {
|
||||
setErrorMessage("A logo is required");
|
||||
return;
|
||||
}
|
||||
const foreignCoins: ForeignCoins = {
|
||||
ARRR: arrrWalletAddress
|
||||
}
|
||||
supportedCoinsSelected.filter((coin)=> coin !== 'QORT').forEach((item: string)=> {
|
||||
if(!foreignCoins[item]) throw new Error(`Please add a ${item} address`)
|
||||
})
|
||||
await onPublish({
|
||||
title,
|
||||
description,
|
||||
shipsTo,
|
||||
location,
|
||||
storeIdentifier,
|
||||
logo,
|
||||
foreignCoins: {
|
||||
ARRR: arrrWalletAddress
|
||||
},
|
||||
supportedCoins: supportedCoinsSelected
|
||||
});
|
||||
} catch (error: any) {
|
||||
setErrorMessage(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = (): void => {
|
||||
setTitle("");
|
||||
setDescription("");
|
||||
setErrorMessage("");
|
||||
setArrrWalletAddress("")
|
||||
setSupportedCoinsSelected(["QORT"])
|
||||
dispatch(toggleCreateStoreModal(false));
|
||||
};
|
||||
|
||||
const handleInputChangeId = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
// Replace any non-alphanumeric and non-space characters with an empty string
|
||||
// Replace multiple spaces with a single dash and remove any dashes that come one after another
|
||||
let newValue = event.target.value
|
||||
.replace(/[^a-zA-Z0-9\s-]/g, "")
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.trim();
|
||||
|
||||
if (newValue.toLowerCase().includes("post")) {
|
||||
// Replace the 'post' string with an empty string
|
||||
newValue = newValue.replace(/post/gi, "");
|
||||
}
|
||||
if (newValue.toLowerCase().includes("q-shop")) {
|
||||
// Replace the 'q-shop' string with an empty string
|
||||
newValue = newValue.replace(/q-shop/gi, "");
|
||||
}
|
||||
setStoreIdentifier(newValue);
|
||||
};
|
||||
|
||||
// Close modal when closeCreateStoreModal is true and reset closeCreateStoreModal to false. This is done once the data container is created inside the GlobalWrapper createStore function.
|
||||
useEffect(() => {
|
||||
if (closeCreateStoreModal) {
|
||||
handleClose();
|
||||
setCloseCreateStoreModal(false);
|
||||
}
|
||||
}, [closeCreateStoreModal]);
|
||||
|
||||
const handleChipSelect = (value: string[]) => {
|
||||
setSupportedCoinsSelected(value);
|
||||
};
|
||||
|
||||
const handleChipRemove = (chip: string) => {
|
||||
if (chip === "QORT") return;
|
||||
setSupportedCoinsSelected(prevChips => prevChips.filter(c => c !== chip));
|
||||
};
|
||||
|
||||
const importAddress = async (coin: string)=> {
|
||||
try {
|
||||
dispatch(setIsLoadingGlobal(true));
|
||||
|
||||
const res = await qortalRequest({
|
||||
action: 'GET_USER_WALLET',
|
||||
coin
|
||||
})
|
||||
|
||||
if(res?.address){
|
||||
setArrrWalletAddress(res.address)
|
||||
}
|
||||
} catch (error) {
|
||||
dispatch(
|
||||
setNotification({
|
||||
alertType: "error",
|
||||
msg: "Unable to import ARRR address. Please insert it manually",
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
dispatch(setIsLoadingGlobal(false));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
aria-labelledby="modal-title"
|
||||
aria-describedby="modal-description"
|
||||
>
|
||||
<ModalBody>
|
||||
<ModalTitle id="modal-title">Create Shop</ModalTitle>
|
||||
{!logo ? (
|
||||
<ImageUploader onPick={(img: string) => setLogo(img)}>
|
||||
<AddLogoButton>
|
||||
Add Shop Logo
|
||||
<AddLogoIcon
|
||||
sx={{
|
||||
height: "25px",
|
||||
width: "auto",
|
||||
}}
|
||||
></AddLogoIcon>
|
||||
</AddLogoButton>
|
||||
</ImageUploader>
|
||||
) : (
|
||||
<LogoPreviewRow>
|
||||
<StoreLogoPreview src={logo} alt="logo" />
|
||||
<TimesIcon
|
||||
color={theme.palette.text.primary}
|
||||
onClickFunc={() => setLogo(null)}
|
||||
height={"32"}
|
||||
width={"32"}
|
||||
></TimesIcon>
|
||||
</LogoPreviewRow>
|
||||
)}
|
||||
|
||||
<CustomInputField
|
||||
id="modal-title-input"
|
||||
label="Url Preview"
|
||||
value={`/${username}/${storeIdentifier}`}
|
||||
// onChange={(e) => setTitle(e.target.value)}
|
||||
fullWidth
|
||||
disabled={true}
|
||||
variant="filled"
|
||||
/>
|
||||
|
||||
<CustomInputField
|
||||
id="modal-shopId-input"
|
||||
label="Shop Id"
|
||||
value={storeIdentifier}
|
||||
onChange={handleInputChangeId}
|
||||
fullWidth
|
||||
inputProps={{ maxLength: 25 }}
|
||||
required
|
||||
variant="filled"
|
||||
/>
|
||||
|
||||
<CustomInputField
|
||||
id="modal-title-input"
|
||||
label="Title"
|
||||
value={title}
|
||||
onChange={(e: any) => setTitle(e.target.value)}
|
||||
fullWidth
|
||||
required
|
||||
variant="filled"
|
||||
inputProps={{ maxLength: 50 }}
|
||||
/>
|
||||
|
||||
<CustomInputField
|
||||
id="modal-description-input"
|
||||
label="Description"
|
||||
value={description}
|
||||
onChange={(e: any) => setDescription(e.target.value)}
|
||||
multiline
|
||||
rows={4}
|
||||
fullWidth
|
||||
required
|
||||
variant="filled"
|
||||
/>
|
||||
|
||||
<CustomInputField
|
||||
id="modal-location-input"
|
||||
label="Location"
|
||||
value={location}
|
||||
onChange={(e: any) => setLocation(e.target.value)}
|
||||
fullWidth
|
||||
required
|
||||
variant="filled"
|
||||
/>
|
||||
|
||||
<CustomInputField
|
||||
id="modal-shipsTo-input"
|
||||
label="Ships To"
|
||||
value={shipsTo}
|
||||
onChange={(e: any) => setShipsTo(e.target.value)}
|
||||
fullWidth
|
||||
required
|
||||
variant="filled"
|
||||
/>
|
||||
|
||||
{/* QORT Wallet Input Field */}
|
||||
{/* <WalletRow>
|
||||
<CustomInputField
|
||||
id="modal-qort-wallet-input"
|
||||
label="QORT Wallet Address"
|
||||
value={qortWalletAddress}
|
||||
onChange={(e: any) => {
|
||||
setQortWalletAddress(e.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
required
|
||||
variant="filled"
|
||||
/>
|
||||
<Tooltip
|
||||
TransitionComponent={Zoom}
|
||||
placement="top"
|
||||
arrow={true}
|
||||
title="Import your QORT Wallet Address from your current account"
|
||||
>
|
||||
<IconButton disableFocusRipple={true} disableRipple={true}>
|
||||
<DownloadArrrWalletIcon
|
||||
color={theme.palette.text.primary}
|
||||
height="40"
|
||||
width="40"
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</WalletRow> */}
|
||||
|
||||
{/* ARRR Wallet Input Field */}
|
||||
<WalletRow>
|
||||
<CustomInputField
|
||||
id="modal-arrr-wallet-input"
|
||||
label="ARRR Wallet Address"
|
||||
value={arrrWalletAddress}
|
||||
onChange={(e: any) => {
|
||||
setArrrWalletAddress(e.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
required
|
||||
variant="filled"
|
||||
/>
|
||||
<Tooltip
|
||||
TransitionComponent={Zoom}
|
||||
placement="top"
|
||||
arrow={true}
|
||||
title="Import your ARRR Wallet Address from your current account"
|
||||
>
|
||||
<IconButton disableFocusRipple={true} disableRipple={true} onClick={()=> importAddress('ARRR')}>
|
||||
<DownloadArrrWalletIcon
|
||||
color={theme.palette.text.primary}
|
||||
height="40"
|
||||
width="40"
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</WalletRow>
|
||||
{/* Coin selection available for your shop */}
|
||||
<FilterSelect
|
||||
disableClearable
|
||||
multiple
|
||||
id="coin-select"
|
||||
value={supportedCoinsSelected}
|
||||
options={supportedCoinsArray}
|
||||
disableCloseOnSelect
|
||||
onChange={(e: any, value) => {
|
||||
if (e.target.textContent === "QORT") return;
|
||||
handleChipSelect(value as string[]);
|
||||
}}
|
||||
renderTags={(values: any) =>
|
||||
values.map((value: string) => {
|
||||
return (
|
||||
<FiltersChip
|
||||
key={value}
|
||||
label={value}
|
||||
onDelete={
|
||||
value !== "QORT" ? () => handleChipRemove(value) : undefined
|
||||
}
|
||||
clickable={value === "QORT" ? false : true}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
renderOption={(props, option: any) => {
|
||||
const isDisabled = option === "QORT";
|
||||
return (
|
||||
<FiltersOption {...props}>
|
||||
<FiltersCheckbox
|
||||
disabled={isDisabled}
|
||||
checked={supportedCoinsSelected.some(coin => coin === option)}
|
||||
/>
|
||||
{option === "QORT" ? (
|
||||
<QortalSVG
|
||||
height="22"
|
||||
width="22"
|
||||
color={theme.palette.text.primary}
|
||||
/>
|
||||
) : option === "ARRR" ? (
|
||||
<ARRRSVG
|
||||
height="22"
|
||||
width="22"
|
||||
color={theme.palette.text.primary}
|
||||
/>
|
||||
) : null}
|
||||
<span style={{ marginLeft: "5px" }}>{option}</span>
|
||||
</FiltersOption>
|
||||
);
|
||||
}}
|
||||
renderInput={params => (
|
||||
<FilterSelectMenuItems
|
||||
{...params}
|
||||
label="Supported Coins"
|
||||
placeholder="Choose the coins that will be supported by your shop"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormControl fullWidth sx={{ marginBottom: 2 }}></FormControl>
|
||||
{errorMessage && (
|
||||
<Typography color="error" variant="body1">
|
||||
{errorMessage}
|
||||
</Typography>
|
||||
)}
|
||||
<ButtonRow>
|
||||
<CancelButton variant="outlined" color="error" onClick={handleClose}>
|
||||
Cancel
|
||||
</CancelButton>
|
||||
<CreateButton variant="contained" onClick={handlePublish}>
|
||||
Create Shop
|
||||
</CreateButton>
|
||||
</ButtonRow>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateStoreModal;
|
383
src/components/modals/EditStoreModal.tsx
Normal file
@ -0,0 +1,383 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
Typography,
|
||||
Modal,
|
||||
FormControl,
|
||||
useTheme,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Zoom,
|
||||
} from "@mui/material";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { toggleCreateStoreModal } from "../../state/features/globalSlice";
|
||||
import { RootState } from "../../state/store";
|
||||
import {
|
||||
AddLogoButton,
|
||||
AddLogoIcon,
|
||||
WalletRow,
|
||||
ButtonRow,
|
||||
CancelButton,
|
||||
CreateButton,
|
||||
CustomInputField,
|
||||
DownloadArrrWalletIcon,
|
||||
LogoPreviewRow,
|
||||
ModalBody,
|
||||
ModalTitle,
|
||||
StoreLogoPreview,
|
||||
TimesIcon,
|
||||
} from "./CreateStoreModal-styles";
|
||||
import ImageUploader from "../common/ImageUploader";
|
||||
import {
|
||||
FilterSelect,
|
||||
FilterSelectMenuItems,
|
||||
FiltersCheckbox,
|
||||
FiltersChip,
|
||||
FiltersOption,
|
||||
} from "../../pages/Store/Store/Store-styles";
|
||||
import { supportedCoinsArray } from "../../constants/supported-coins";
|
||||
import { QortalSVG } from "../../assets/svgs/QortalSVG";
|
||||
import { ARRRSVG } from "../../assets/svgs/ARRRSVG";
|
||||
|
||||
interface ForeignCoins {
|
||||
[key: string]: string;
|
||||
}
|
||||
export interface onPublishParamEdit {
|
||||
title: string;
|
||||
description: string;
|
||||
shipsTo: string;
|
||||
location: string;
|
||||
logo: string;
|
||||
foreignCoins: ForeignCoins;
|
||||
supportedCoins: string[];
|
||||
}
|
||||
interface MyModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onPublish: (param: onPublishParamEdit) => Promise<void>;
|
||||
username: string;
|
||||
}
|
||||
|
||||
const MyModal: React.FC<MyModalProps> = ({ open, onClose, onPublish }) => {
|
||||
const dispatch = useDispatch();
|
||||
const currentStore = useSelector(
|
||||
(state: RootState) => state.global.currentStore
|
||||
);
|
||||
|
||||
const storeId = useSelector((state: RootState) => state.store.storeId);
|
||||
|
||||
const [title, setTitle] = useState<string>("");
|
||||
const [description, setDescription] = useState<string>("");
|
||||
const [location, setLocation] = useState<string>("");
|
||||
const [shipsTo, setShipsTo] = useState<string>("");
|
||||
const [errorMessage, setErrorMessage] = useState<string>("");
|
||||
const [logo, setLogo] = useState<string | null>(null);
|
||||
const [supportedCoinsSelected, setSupportedCoinsSelected] = useState<
|
||||
string[]
|
||||
>(["QORT"]);
|
||||
const [qortWalletAddress, setQortWalletAddress] = useState<string>("");
|
||||
const [arrrWalletAddress, setArrrWalletAddress] = useState<string>("");
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const handlePublish = async (): Promise<void> => {
|
||||
try {
|
||||
setErrorMessage("");
|
||||
if (!logo) {
|
||||
setErrorMessage("A logo is required");
|
||||
return;
|
||||
}
|
||||
|
||||
const foreignCoins: ForeignCoins = {
|
||||
ARRR: arrrWalletAddress
|
||||
}
|
||||
supportedCoinsSelected.filter((coin)=> coin !== 'QORT').forEach((item: string)=> {
|
||||
if(!foreignCoins[item]) throw new Error(`Please add a ${item} address`)
|
||||
})
|
||||
await onPublish({
|
||||
title,
|
||||
description,
|
||||
shipsTo,
|
||||
location,
|
||||
logo,
|
||||
foreignCoins: {
|
||||
ARRR: arrrWalletAddress
|
||||
},
|
||||
supportedCoins: supportedCoinsSelected
|
||||
});
|
||||
handleClose();
|
||||
} catch (error: any) {
|
||||
setErrorMessage(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open && currentStore && storeId === currentStore.id) {
|
||||
setTitle(currentStore?.title || "");
|
||||
setDescription(currentStore?.description || "");
|
||||
setLogo(currentStore?.logo || null);
|
||||
setLocation(currentStore?.location || "");
|
||||
setShipsTo(currentStore?.shipsTo || "");
|
||||
setSupportedCoinsSelected(currentStore?.supportedCoins || ['QORT'])
|
||||
setArrrWalletAddress(currentStore?.foreignCoins?.ARRR || "")
|
||||
}
|
||||
}, [currentStore, storeId, open]);
|
||||
|
||||
const handleClose = (): void => {
|
||||
setTitle("");
|
||||
setDescription("");
|
||||
setErrorMessage("");
|
||||
setDescription("");
|
||||
setLogo(null);
|
||||
setLocation("");
|
||||
setShipsTo("");
|
||||
setArrrWalletAddress("")
|
||||
setSupportedCoinsSelected(["QORT"])
|
||||
dispatch(toggleCreateStoreModal(false));
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleChipSelect = (value: string[]) => {
|
||||
setSupportedCoinsSelected(value);
|
||||
};
|
||||
|
||||
const handleChipRemove = (chip: string) => {
|
||||
if (chip === "QORT") return;
|
||||
setSupportedCoinsSelected(prevChips => prevChips.filter(c => c !== chip));
|
||||
};
|
||||
|
||||
const importAddress = async (coin: string)=> {
|
||||
try {
|
||||
const res = await qortalRequest({
|
||||
action: 'GET_USER_WALLET',
|
||||
coin
|
||||
})
|
||||
if(res?.address){
|
||||
setArrrWalletAddress(res.address)
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
aria-labelledby="modal-title"
|
||||
aria-describedby="modal-description"
|
||||
>
|
||||
<ModalBody>
|
||||
<ModalTitle id="modal-title" variant="h6">
|
||||
Edit Shop
|
||||
</ModalTitle>
|
||||
{!logo ? (
|
||||
<ImageUploader onPick={(img: string) => setLogo(img)}>
|
||||
<AddLogoButton>
|
||||
Add Shop Logo
|
||||
<AddLogoIcon
|
||||
sx={{
|
||||
height: "25px",
|
||||
width: "auto",
|
||||
}}
|
||||
></AddLogoIcon>
|
||||
</AddLogoButton>
|
||||
</ImageUploader>
|
||||
) : (
|
||||
<LogoPreviewRow>
|
||||
<StoreLogoPreview src={logo} alt="logo" />
|
||||
<TimesIcon
|
||||
color={theme.palette.text.primary}
|
||||
onClickFunc={() => setLogo(null)}
|
||||
height={"32"}
|
||||
width={"32"}
|
||||
></TimesIcon>
|
||||
</LogoPreviewRow>
|
||||
)}
|
||||
<CustomInputField
|
||||
id="modal-title-input"
|
||||
label="Title"
|
||||
value={title}
|
||||
onChange={e => setTitle(e.target.value)}
|
||||
inputProps={{ maxLength: 50 }}
|
||||
fullWidth
|
||||
required
|
||||
variant="filled"
|
||||
/>
|
||||
<CustomInputField
|
||||
id="modal-description-input"
|
||||
label="Description"
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
multiline
|
||||
rows={4}
|
||||
fullWidth
|
||||
required
|
||||
variant="filled"
|
||||
/>
|
||||
<CustomInputField
|
||||
id="modal-location-input"
|
||||
label="Location"
|
||||
value={location}
|
||||
onChange={e => setLocation(e.target.value)}
|
||||
fullWidth
|
||||
required
|
||||
variant="filled"
|
||||
/>
|
||||
<CustomInputField
|
||||
id="modal-shipsTo-input"
|
||||
label="Ships To"
|
||||
value={shipsTo}
|
||||
onChange={e => setShipsTo(e.target.value)}
|
||||
fullWidth
|
||||
required
|
||||
variant="filled"
|
||||
/>
|
||||
|
||||
{/* QORT Wallet Input Field */}
|
||||
{/* <WalletRow>
|
||||
<CustomInputField
|
||||
id="modal-qort-wallet-input"
|
||||
label="QORT Wallet Address"
|
||||
value={qortWalletAddress}
|
||||
onChange={(e: any) => {
|
||||
setQortWalletAddress(e.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
required
|
||||
variant="filled"
|
||||
/>
|
||||
<Tooltip
|
||||
TransitionComponent={Zoom}
|
||||
placement="top"
|
||||
arrow={true} const importAddress = async (coin: string)=> {
|
||||
try {
|
||||
const res = await qortalRequest({
|
||||
action: 'GET_USER_WALLET',
|
||||
coin
|
||||
})
|
||||
if(res?.address){
|
||||
setArrrWalletAddress(res.address)
|
||||
}
|
||||
console.log({res})
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
}
|
||||
title="Import your QORT Wallet Address from your current account"
|
||||
>
|
||||
<IconButton disableFocusRipple={true} disableRipple={true}>
|
||||
<DownloadArrrWalletIcon
|
||||
color={theme.palette.text.primary}
|
||||
height="40"
|
||||
width="40"
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</WalletRow> */}
|
||||
|
||||
{/* ARRR Wallet Input Field */}
|
||||
<WalletRow>
|
||||
<CustomInputField
|
||||
id="modal-arrr-wallet-input"
|
||||
label="ARRR Wallet Address"
|
||||
value={arrrWalletAddress}
|
||||
onChange={(e: any) => {
|
||||
setArrrWalletAddress(e.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
required
|
||||
variant="filled"
|
||||
/>
|
||||
<Tooltip
|
||||
TransitionComponent={Zoom}
|
||||
placement="top"
|
||||
arrow={true}
|
||||
title="Import your ARRR Wallet Address from your current account"
|
||||
>
|
||||
<IconButton disableFocusRipple={true} disableRipple={true} onClick={()=> importAddress('ARRR')}>
|
||||
<DownloadArrrWalletIcon
|
||||
color={theme.palette.text.primary}
|
||||
height="40"
|
||||
width="40"
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</WalletRow>
|
||||
<FilterSelect
|
||||
disableClearable
|
||||
multiple
|
||||
id="coin-select"
|
||||
value={supportedCoinsSelected}
|
||||
options={supportedCoinsArray}
|
||||
disableCloseOnSelect
|
||||
onChange={(e: any, value) => {
|
||||
if (e.target.textContent === "QORT") return;
|
||||
handleChipSelect(value as string[]);
|
||||
}}
|
||||
renderTags={(values: any) =>
|
||||
values.map((value: string) => {
|
||||
return (
|
||||
<FiltersChip
|
||||
key={value}
|
||||
label={value}
|
||||
onDelete={
|
||||
value !== "QORT" ? () => handleChipRemove(value) : undefined
|
||||
}
|
||||
clickable={value === "QORT" ? false : true}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
renderOption={(props, option: any) => {
|
||||
const isDisabled = option === "QORT";
|
||||
return (
|
||||
<FiltersOption {...props}>
|
||||
<FiltersCheckbox
|
||||
disabled={isDisabled}
|
||||
checked={supportedCoinsSelected.some(coin => coin === option)}
|
||||
/>
|
||||
{option === "QORT" ? (
|
||||
<QortalSVG
|
||||
height="22"
|
||||
width="22"
|
||||
color={theme.palette.text.primary}
|
||||
/>
|
||||
) : option === "ARRR" ? (
|
||||
<ARRRSVG
|
||||
height="22"
|
||||
width="22"
|
||||
color={theme.palette.text.primary}
|
||||
/>
|
||||
) : null}
|
||||
<span style={{ marginLeft: "5px" }}>{option}</span>
|
||||
</FiltersOption>
|
||||
);
|
||||
}}
|
||||
renderInput={params => (
|
||||
<FilterSelectMenuItems
|
||||
{...params}
|
||||
label="Supported Coins"
|
||||
placeholder="Choose the coins that will be supported by your shop"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<FormControl fullWidth sx={{ marginBottom: 2 }}></FormControl>
|
||||
{errorMessage && (
|
||||
<Typography color="error" variant="body1">
|
||||
{errorMessage}
|
||||
</Typography>
|
||||
)}
|
||||
<ButtonRow sx={{ display: "flex", justifyContent: "flex-end", gap: 1 }}>
|
||||
<CancelButton variant="outlined" color="error" onClick={handleClose}>
|
||||
Cancel
|
||||
</CancelButton>
|
||||
<CreateButton variant="contained" onClick={handlePublish}>
|
||||
Edit Shop
|
||||
</CreateButton>
|
||||
</ButtonRow>
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyModal;
|