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