@ -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? |
@ -0,0 +1,10 @@ |
|||||||
|
{ |
||||||
|
"printWidth": 80, |
||||||
|
"singleQuote": false, |
||||||
|
"trailingComma": "es5", |
||||||
|
"bracketSpacing": true, |
||||||
|
"jsxBracketSameLine": false, |
||||||
|
"arrowParens": "avoid", |
||||||
|
"tabWidth": 2, |
||||||
|
"semi": true |
||||||
|
} |
@ -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> |
@ -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" |
||||||
|
} |
||||||
|
} |
@ -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; |
After Width: | Height: | Size: 5.6 KiB |
After Width: | Height: | Size: 9.0 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 7.0 KiB |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 2.3 KiB |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
) |
||||||
|
} |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
) |
||||||
|
} |
@ -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> |
||||||
|
) |
||||||
|
} |
@ -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> |
||||||
|
) |
||||||
|
} |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
) |
||||||
|
} |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
) |
||||||
|
} |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
) |
||||||
|
} |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
) |
||||||
|
} |
@ -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> |
||||||
|
) |
||||||
|
} |
@ -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; |
||||||
|
} |
@ -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> |
||||||
|
) |
||||||
|
} |
@ -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> |
||||||
|
) |
||||||
|
} |
@ -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> |
||||||
|
) |
||||||
|
} |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
) |
||||||
|
} |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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> |
||||||
|
) |
||||||
|
} |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -0,0 +1,5 @@ |
|||||||
|
export interface SVGProps { |
||||||
|
color: string |
||||||
|
height: string |
||||||
|
width: string |
||||||
|
} |
@ -0,0 +1,28 @@ |
|||||||
|
import { styled } from '@mui/system'; |
||||||
|
import { |
||||||
|
Box, |
||||||
|
Modal, |
||||||
|
Typography |
||||||
|
} from '@mui/material'; |
||||||
|
|
||||||
|
export const StyledModal = styled(Modal)(({ theme }) => ({ |
||||||
|
display: 'flex', |
||||||
|
alignItems: 'center', |
||||||
|
justifyContent: 'center' |
||||||
|
})) |
||||||
|
|
||||||
|
export const ModalContent = styled(Box)(({ theme }) => ({ |
||||||
|
backgroundColor: theme.palette.primary.main, |
||||||
|
padding: theme.spacing(4), |
||||||
|
borderRadius: theme.spacing(1), |
||||||
|
width: '40%', |
||||||
|
'&:focus': { |
||||||
|
outline: 'none' |
||||||
|
} |
||||||
|
})) |
||||||
|
|
||||||
|
export const ModalText = styled(Typography)(({ theme }) => ({ |
||||||
|
fontFamily: "Raleway", |
||||||
|
fontSize: "25px", |
||||||
|
color: theme.palette.text.primary, |
||||||
|
})); |
@ -0,0 +1,100 @@ |
|||||||
|
import React, { useState } from "react"; |
||||||
|
import { |
||||||
|
Box, |
||||||
|
Button, |
||||||
|
Modal, |
||||||
|
Typography, |
||||||
|
SelectChangeEvent, |
||||||
|
ListItem, |
||||||
|
List, |
||||||
|
useTheme |
||||||
|
} from "@mui/material"; |
||||||
|
import { |
||||||
|
StyledModal, |
||||||
|
ModalContent, |
||||||
|
ModalText |
||||||
|
} from "./BlockedNamesModal-styles"; |
||||||
|
|
||||||
|
interface PostModalProps { |
||||||
|
open: boolean; |
||||||
|
onClose: () => void; |
||||||
|
} |
||||||
|
|
||||||
|
export const BlockedNamesModal: React.FC<PostModalProps> = ({ |
||||||
|
open, |
||||||
|
onClose |
||||||
|
}) => { |
||||||
|
const [blockedNames, setBlockedNames] = useState<string[]>([]); |
||||||
|
const theme = useTheme(); |
||||||
|
const getBlockedNames = React.useCallback(async () => { |
||||||
|
try { |
||||||
|
const listName = `blockedNames_q-blog`; |
||||||
|
const response = await qortalRequest({ |
||||||
|
action: "GET_LIST_ITEMS", |
||||||
|
list_name: listName |
||||||
|
}); |
||||||
|
setBlockedNames(response); |
||||||
|
} catch (error) { |
||||||
|
onClose(); |
||||||
|
} |
||||||
|
}, []); |
||||||
|
|
||||||
|
React.useEffect(() => { |
||||||
|
getBlockedNames(); |
||||||
|
}, [getBlockedNames]); |
||||||
|
|
||||||
|
const removeFromBlockList = async (name: string) => { |
||||||
|
try { |
||||||
|
const response = await qortalRequest({ |
||||||
|
action: "DELETE_LIST_ITEM", |
||||||
|
list_name: "blockedNames_q-blog", |
||||||
|
item: name |
||||||
|
}); |
||||||
|
|
||||||
|
if (response === true) { |
||||||
|
setBlockedNames((prev) => prev.filter((n) => n !== name)); |
||||||
|
} |
||||||
|
} catch (error) {} |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<StyledModal open={open} onClose={onClose}> |
||||||
|
<ModalContent> |
||||||
|
<ModalText>Manage blocked names</ModalText> |
||||||
|
<List |
||||||
|
sx={{ |
||||||
|
width: "100%", |
||||||
|
display: "flex", |
||||||
|
flexDirection: "column", |
||||||
|
flex: "1", |
||||||
|
overflow: "auto" |
||||||
|
}} |
||||||
|
> |
||||||
|
{blockedNames.map((name, index) => ( |
||||||
|
<ListItem |
||||||
|
key={name + index} |
||||||
|
sx={{ |
||||||
|
display: "flex" |
||||||
|
}} |
||||||
|
> |
||||||
|
<Typography>{name}</Typography> |
||||||
|
<Button |
||||||
|
sx={{ |
||||||
|
backgroundColor: theme.palette.primary.light, |
||||||
|
color: theme.palette.text.primary, |
||||||
|
fontFamily: "Raleway" |
||||||
|
}} |
||||||
|
onClick={() => removeFromBlockList(name)} |
||||||
|
> |
||||||
|
Remove |
||||||
|
</Button> |
||||||
|
</ListItem> |
||||||
|
))} |
||||||
|
</List> |
||||||
|
<Button variant="contained" color="primary" onClick={onClose}> |
||||||
|
Close |
||||||
|
</Button> |
||||||
|
</ModalContent> |
||||||
|
</StyledModal> |
||||||
|
); |
||||||
|
}; |
@ -0,0 +1,18 @@ |
|||||||
|
import { styled } from "@mui/system"; |
||||||
|
import { DialogTitle, DialogContentText } from "@mui/material"; |
||||||
|
|
||||||
|
export const DialogTitleStyled = styled(DialogTitle)(({ theme }) => ({ |
||||||
|
fontFamily: "Merriweather Sans", |
||||||
|
fontSize: "18px", |
||||||
|
color: theme.palette.text.primary, |
||||||
|
userSelect: "none" |
||||||
|
})); |
||||||
|
|
||||||
|
export const DialogContentTextStyled = styled(DialogContentText)( |
||||||
|
({ theme }) => ({ |
||||||
|
fontFamily: "Karla", |
||||||
|
fontSize: "16px", |
||||||
|
color: theme.palette.text.primary, |
||||||
|
userSelect: "none" |
||||||
|
}) |
||||||
|
); |
@ -0,0 +1,50 @@ |
|||||||
|
import React from "react"; |
||||||
|
import { Dialog, DialogActions, DialogContent, Button } from "@mui/material"; |
||||||
|
import { |
||||||
|
DialogContentTextStyled, |
||||||
|
DialogTitleStyled |
||||||
|
} from "./ConfirmationModal-styles"; |
||||||
|
import { |
||||||
|
CancelButton, |
||||||
|
CreateButton |
||||||
|
} from "../../modals/CreateStoreModal-styles"; |
||||||
|
|
||||||
|
export interface ModalProps { |
||||||
|
open: boolean; |
||||||
|
title: string; |
||||||
|
message: string; |
||||||
|
handleConfirm: () => void; |
||||||
|
handleCancel: () => void; |
||||||
|
} |
||||||
|
|
||||||
|
const ConfirmationModal: React.FC<ModalProps> = ({ |
||||||
|
open, |
||||||
|
title, |
||||||
|
message, |
||||||
|
handleConfirm, |
||||||
|
handleCancel |
||||||
|
}) => { |
||||||
|
return ( |
||||||
|
<Dialog |
||||||
|
open={open} |
||||||
|
onClose={handleCancel} |
||||||
|
aria-labelledby="alert-dialog-title" |
||||||
|
aria-describedby="alert-dialog-description" |
||||||
|
> |
||||||
|
<DialogTitleStyled id="alert-dialog-title">{title}</DialogTitleStyled> |
||||||
|
<DialogContent> |
||||||
|
<DialogContentTextStyled id="alert-dialog-description"> |
||||||
|
{message} |
||||||
|
</DialogContentTextStyled> |
||||||
|
</DialogContent> |
||||||
|
<DialogActions> |
||||||
|
<CancelButton variant="outlined" onClick={handleCancel} color="error"> |
||||||
|
Cancel |
||||||
|
</CancelButton> |
||||||
|
<CreateButton onClick={handleConfirm}>Proceed</CreateButton> |
||||||
|
</DialogActions> |
||||||
|
</Dialog> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default ConfirmationModal; |
@ -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> |
||||||
|
); |
||||||
|
} |
@ -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} /> |
||||||
|
} |
@ -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 |
@ -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)' |
||||||
|
} |
||||||
|
})) |
@ -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> |
||||||
|
); |
||||||
|
}; |
@ -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 |
@ -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> |
||||||
|
) |
||||||
|
} |
@ -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 |
@ -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; |
@ -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} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
||||||
|
); |
@ -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; |
@ -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 |
@ -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 |
@ -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 |
||||||
|
} |
||||||
|
} |
@ -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 |
||||||
|
} |
||||||
|
} |
@ -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 |
||||||
|
} |
||||||
|
} |
@ -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; |
@ -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%" |
||||||
|
})); |
@ -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; |
@ -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> |
||||||
|
) |
||||||
|
} |
@ -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; |
@ -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; |
||||||
|
} |
||||||
|
|
@ -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> |
||||||
|
} |
@ -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; |
@ -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; |
||||||
|
} |
||||||
|
} |
@ -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" |
||||||
|
} |
||||||
|
})); |
@ -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; |
@ -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> |
||||||
|
); |
||||||
|
} |
@ -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", |
||||||
|
}, |
||||||
|
})); |
@ -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; |
@ -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; |