Browse Source

Initial Q-Shop Commit in its own repo

feature/new-gateway-modal
Justin Ferrari 10 months ago
commit
98c5e7d37d
  1. 24
      .gitignore
  2. 10
      .prettierrc.json
  3. 13
      index.html
  4. 6895
      package-lock.json
  5. 54
      package.json
  6. 1
      public/vite.svg
  7. 48
      src/App.tsx
  8. BIN
      src/assets/img/ArrrLogoBlack.png
  9. BIN
      src/assets/img/ArrrLogoWhite.png
  10. BIN
      src/assets/img/Q-AppsLogo.webp
  11. BIN
      src/assets/img/QShopLogo.webp
  12. BIN
      src/assets/img/QShopLogoLight.webp
  13. BIN
      src/assets/img/arrr.png
  14. BIN
      src/assets/img/btc.png
  15. BIN
      src/assets/img/dgb.png
  16. BIN
      src/assets/img/doge.png
  17. BIN
      src/assets/img/ltc.png
  18. BIN
      src/assets/img/qort.png
  19. BIN
      src/assets/img/rvn.png
  20. 34
      src/assets/svgs/ARRRSVG.tsx
  21. 25
      src/assets/svgs/AccountCircleSVG.tsx
  22. 15
      src/assets/svgs/AddSVG.tsx
  23. 21
      src/assets/svgs/AlignCenterSVG.tsx
  24. 17
      src/assets/svgs/AlignLeftSVG.tsx
  25. 17
      src/assets/svgs/AlignRightSVG.tsx
  26. 21
      src/assets/svgs/BackArrowSVG.tsx
  27. 17
      src/assets/svgs/BoldSVG.tsx
  28. 21
      src/assets/svgs/BriefcaseSVG.tsx
  29. 23
      src/assets/svgs/CalendarSVG.tsx
  30. 27
      src/assets/svgs/CancelSVG.tsx
  31. 23
      src/assets/svgs/CartSVG.tsx
  32. 21
      src/assets/svgs/CategorySVG.tsx
  33. 17
      src/assets/svgs/CodeBlockSVG.tsx
  34. 19
      src/assets/svgs/CompareArrowsSVG.tsx
  35. 15
      src/assets/svgs/CurrencySVG.tsx
  36. 23
      src/assets/svgs/DarkModeSVG.tsx
  37. 21
      src/assets/svgs/DescriptionSVG.tsx
  38. 21
      src/assets/svgs/DialogsSVG.tsx
  39. 24
      src/assets/svgs/DoubleArrowDownSVG.tsx
  40. 21
      src/assets/svgs/DownloadSVG.tsx
  41. 22
      src/assets/svgs/ExpandMoreSVG.tsx
  42. 22
      src/assets/svgs/GarbageSVG.tsx
  43. 17
      src/assets/svgs/H2SVG.tsx
  44. 17
      src/assets/svgs/H3SVG.tsx
  45. 8
      src/assets/svgs/IconTypes.ts
  46. 17
      src/assets/svgs/ItalicSVG.tsx
  47. 23
      src/assets/svgs/LightModeSVG.tsx
  48. 17
      src/assets/svgs/LinkSVG.tsx
  49. 15
      src/assets/svgs/LocationSVG.tsx
  50. 21
      src/assets/svgs/LoyaltySVG.tsx
  51. 23
      src/assets/svgs/MinimizeSVG.tsx
  52. 23
      src/assets/svgs/MinusCircle.tsx
  53. 25
      src/assets/svgs/NewWindowSVG.tsx
  54. 21
      src/assets/svgs/OrdersSVG.tsx
  55. 21
      src/assets/svgs/OwnerSVG.tsx
  56. 23
      src/assets/svgs/PlusCircle.tsx
  57. 46
      src/assets/svgs/QortalSVG.tsx
  58. 15
      src/assets/svgs/ShippingSVG.tsx
  59. 21
      src/assets/svgs/StarSVG.tsx
  60. 23
      src/assets/svgs/StorefrontSVG.tsx
  61. 23
      src/assets/svgs/TimesSVG.tsx
  62. 17
      src/assets/svgs/UnderlineSVG.tsx
  63. 15
      src/assets/svgs/WarningSVG.tsx
  64. 5
      src/assets/svgs/interfaces.ts
  65. 28
      src/components/common/BlockedNamesModal/BlockedNamesModal-styles.ts
  66. 100
      src/components/common/BlockedNamesModal/BlockedNamesModal.tsx
  67. 18
      src/components/common/ConfirmationModal/ConfirmationModal-styles.tsx
  68. 50
      src/components/common/ConfirmationModal/ConfirmationModal.tsx
  69. 82
      src/components/common/ContextMenu/ContextMenuResource.tsx
  70. 16
      src/components/common/CustomIcon.tsx
  71. 55
      src/components/common/DraggableResizableGrid.tsx
  72. 40
      src/components/common/Error/Error-styles.tsx
  73. 34
      src/components/common/Error/ErrorElement.tsx
  74. 36
      src/components/common/ErrorBoundary.tsx
  75. 316
      src/components/common/GenericPublishModal.tsx
  76. 89
      src/components/common/ImageUploader.tsx
  77. 49
      src/components/common/LazyLoad.tsx
  78. 86
      src/components/common/Notification/Notification.tsx
  79. 132
      src/components/common/NumericTextFieldQshop.tsx
  80. 43
      src/components/common/PageLoader.tsx
  81. 25
      src/components/common/Portal.tsx
  82. 280
      src/components/common/PostPublishModal.tsx
  83. 111
      src/components/common/PublishAudio.tsx
  84. 119
      src/components/common/PublishGeneric.tsx
  85. 112
      src/components/common/PublishVideo.tsx
  86. 124
      src/components/common/ResponsiveImage.tsx
  87. 24
      src/components/common/TabImageList/TabImageList-styles.tsx
  88. 65
      src/components/common/TabImageList/TabImageList.tsx
  89. 51
      src/components/common/VideoContent.tsx
  90. 286
      src/components/common/VideoPublishModal.tsx
  91. 78
      src/components/editor/BlogEditor.css
  92. 574
      src/components/editor/BlogEditor.tsx
  93. 25
      src/components/editor/ReadOnlySlate.tsx
  94. 47
      src/components/editor/customTypes.ts
  95. 211
      src/components/layout/Navbar/Navbar-styles.tsx
  96. 288
      src/components/layout/Navbar/Navbar.tsx
  97. 70
      src/components/modals/ConsentModal.tsx
  98. 197
      src/components/modals/CreateStoreModal-styles.tsx
  99. 419
      src/components/modals/CreateStoreModal.tsx
  100. 383
      src/components/modals/EditStoreModal.tsx
  101. Some files were not shown because too many files have changed in this diff Show More

24
.gitignore vendored

@ -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

@ -0,0 +1,10 @@
{
"printWidth": 80,
"singleQuote": false,
"trailingComma": "es5",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"arrowParens": "avoid",
"tabWidth": 2,
"semi": true
}

13
index.html

@ -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

File diff suppressed because it is too large Load Diff

54
package.json

@ -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

@ -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

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
src/assets/img/ArrrLogoWhite.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
src/assets/img/Q-AppsLogo.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
src/assets/img/QShopLogo.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
src/assets/img/QShopLogoLight.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
src/assets/img/arrr.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
src/assets/img/btc.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
src/assets/img/dgb.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
src/assets/img/doge.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
src/assets/img/ltc.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
src/assets/img/qort.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
src/assets/img/rvn.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

34
src/assets/svgs/ARRRSVG.tsx

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -0,0 +1,5 @@
export interface SVGProps {
color: string
height: string
width: string
}

28
src/components/common/BlockedNamesModal/BlockedNamesModal-styles.ts

@ -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

@ -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>
);
};

18
src/components/common/ConfirmationModal/ConfirmationModal-styles.tsx

@ -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"
})
);

50
src/components/common/ConfirmationModal/ConfirmationModal.tsx

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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;

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

Loading…
Cancel
Save