First Commit
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
*.zip
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
10
.prettierrc.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"printWidth": 80,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"arrowParens": "avoid",
|
||||
"tabWidth": 2,
|
||||
"semi": true
|
||||
}
|
13
index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Q-Mail</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
9434
package-lock.json
generated
Normal file
65
package.json
Normal file
@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "q-mail",
|
||||
"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",
|
||||
"@tiptap/core": "^2.0.4",
|
||||
"@tiptap/extension-highlight": "^2.0.4",
|
||||
"@tiptap/extension-underline": "^2.0.4",
|
||||
"@tiptap/starter-kit": "^2.0.4",
|
||||
"@types/react-grid-layout": "^1.3.2",
|
||||
"axios": "^1.3.4",
|
||||
"compressorjs": "^1.2.1",
|
||||
"dompurify": "^3.0.3",
|
||||
"flexlayout-react": "^0.7.9",
|
||||
"localforage": "^1.10.0",
|
||||
"mime": "^4.0.1",
|
||||
"moment": "^2.29.4",
|
||||
"philliplm-react-modern-audio-player": "^1.4.6",
|
||||
"quill-image-resize-module-react": "^3.0.0",
|
||||
"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-quill": "^2.0.0",
|
||||
"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/dompurify": "^3.0.2",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-copy-to-clipboard": "^5.0.7",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"core-js": "^3.30.2",
|
||||
"prettier": "^2.8.6",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^5.0.10",
|
||||
"worker-loader": "^3.0.8"
|
||||
}
|
||||
}
|
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
37
src/App.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
// @ts-nocheck
|
||||
|
||||
import { Routes, Route } from 'react-router-dom'
|
||||
|
||||
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 GlobalWrapper from './wrappers/GlobalWrapper'
|
||||
import DownloadWrapper from './wrappers/DownloadWrapper'
|
||||
import Notification from './components/common/Notification/Notification'
|
||||
import { Mail } from './pages/Mail/Mail'
|
||||
|
||||
function App() {
|
||||
const themeColor = window._qdnTheme
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<ThemeProvider theme={darkTheme}>
|
||||
<Notification />
|
||||
<DownloadWrapper>
|
||||
<GlobalWrapper>
|
||||
<CssBaseline />
|
||||
|
||||
<Routes>
|
||||
<Route path="/" element={<Mail />} />
|
||||
<Route path="/to/:name" element={<Mail isFromTo />} />
|
||||
</Routes>
|
||||
</GlobalWrapper>
|
||||
</DownloadWrapper>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
BIN
src/assets/img/arrr.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/img/btc.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/assets/img/dgb.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
src/assets/img/doge.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
src/assets/img/ltc.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/img/q-mail-icon.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
src/assets/img/qBlogLogo.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
src/assets/img/qmaillogo.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
src/assets/img/qort.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/assets/img/rvn.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
1
src/assets/react.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
After Width: | Height: | Size: 4.0 KiB |
25
src/assets/svgs/AccountCircleSVG.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
interface AccountCircleSVGProps {
|
||||
color: string
|
||||
height: string
|
||||
width: string
|
||||
}
|
||||
|
||||
export const AccountCircleSVG: React.FC<AccountCircleSVGProps> = ({
|
||||
color,
|
||||
height,
|
||||
width
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path
|
||||
fill={color}
|
||||
d="M222 801q63-44 125-67.5T480 710q71 0 133.5 23.5T739 801q44-54 62.5-109T820 576q0-145-97.5-242.5T480 236q-145 0-242.5 97.5T140 576q0 61 19 116t63 109Zm257.814-195Q422 606 382.5 566.314q-39.5-39.686-39.5-97.5t39.686-97.314q39.686-39.5 97.5-39.5t97.314 39.686q39.5 39.686 39.5 97.5T577.314 566.5q-39.686 39.5-97.5 39.5Zm.654 370Q398 976 325 944.5q-73-31.5-127.5-86t-86-127.266Q80 658.468 80 575.734T111.5 420.5q31.5-72.5 86-127t127.266-86q72.766-31.5 155.5-31.5T635.5 207.5q72.5 31.5 127 86t86 127.032q31.5 72.532 31.5 155T848.5 731q-31.5 73-86 127.5t-127.032 86q-72.532 31.5-155 31.5ZM480 916q55 0 107.5-16T691 844q-51-36-104-55t-107-19q-54 0-107 19t-104 55q51 40 103.5 56T480 916Zm0-370q34 0 55.5-21.5T557 469q0-34-21.5-55.5T480 392q-34 0-55.5 21.5T403 469q0 34 21.5 55.5T480 546Zm0-77Zm0 374Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
4
src/assets/svgs/AddAlias.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.72075 17.0173L8.72075 0.424419" stroke="white" stroke-width="4.26652"/>
|
||||
<path d="M-4.89838e-05 8.29644L16.5928 8.29644" stroke="white" stroke-width="4.26652"/>
|
||||
</svg>
|
After Width: | Height: | Size: 275 B |
10
src/assets/svgs/AliasAvatar.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="51" height="51" viewBox="0 0 51 51" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<mask id="mask0_330_1156" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="51" height="51">
|
||||
<circle cx="25.5" cy="25.5" r="13" stroke="white" stroke-width="25"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_330_1156)">
|
||||
<path d="M31.8523 31.5381C30.594 31.5381 29.493 32.1672 28.7852 32.6391C28.392 32.875 28.392 33.4255 28.7852 33.7401C29.4144 34.2119 30.5153 34.841 31.8523 34.841C33.1105 34.841 34.2115 34.2119 34.8406 33.7401C35.2339 33.5041 35.2339 32.9536 34.8406 32.6391C34.2115 32.2459 33.1105 31.5381 31.8523 31.5381Z" fill="white"/>
|
||||
<path d="M19.4268 31.5381C18.1686 31.5381 17.0676 32.1672 16.4384 32.6391C16.0452 32.875 16.0452 33.4255 16.4384 33.7401C17.0676 34.2119 18.1686 34.841 19.4268 34.841C20.6851 34.841 21.7861 34.2119 22.4152 33.7401C22.8084 33.5041 22.8084 32.9536 22.4152 32.6391C21.7861 32.2459 20.6851 31.5381 19.4268 31.5381Z" fill="white"/>
|
||||
<path d="M-1.02 4.8786V19.506C-1.02 33.1897 5.58591 46.1656 16.5958 54.3443L25.6396 60.9502L34.6834 54.3443C45.6932 46.2442 52.2992 33.1897 52.2992 19.506V4.8786L25.6396 -1.01953L-1.02 4.8786ZM37.5931 37.6722C35.6271 37.9082 32.6387 38.1441 30.1222 38.2227C29.493 38.2227 28.7852 37.9868 28.3134 37.515L27.527 36.6499C27.0551 36.178 26.5046 35.9421 25.8755 35.9421C25.2464 35.9421 24.6172 36.178 24.224 36.6499L23.2803 37.5936C22.8085 38.0654 22.1793 38.3014 21.4716 38.3014C18.7977 38.1441 15.7307 37.9868 13.686 37.7509C12.6637 37.5936 11.8773 36.6499 12.0345 35.6276L13.0569 27.9207C16.4385 28.7071 20.9211 29.1003 25.5609 29.1003C30.2008 29.1003 34.6834 28.7071 38.1436 27.9207L39.166 35.6276C39.3232 36.5713 38.6155 37.515 37.5931 37.6722ZM28.3134 13.2146L31.4591 12.5069C33.0319 12.1137 34.6047 13.136 34.998 14.7088L36.8854 22.9662L38.5368 22.573L37.9863 20.2138C41.2107 21.0002 43.2553 22.0225 43.2553 23.2021C43.2553 25.4828 35.3912 27.3702 25.6396 27.3702C15.888 27.3702 8.02381 25.4828 8.02381 23.2021C8.02381 22.0225 10.0685 21.0002 13.2928 20.2138L12.7423 22.573L14.3938 22.9662L16.2812 14.7088C16.6744 13.136 18.2472 12.1137 19.8201 12.5069L22.9657 13.2146C24.6959 13.6078 26.5833 13.6078 28.3134 13.2146Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
21
src/assets/svgs/AlignCenterSVG.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { SVGProps } from './interfaces'
|
||||
|
||||
export const AlignCenterSVG: React.FC<SVGProps> = ({
|
||||
color,
|
||||
height,
|
||||
width
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
height={height}
|
||||
width={width}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 96 960 960"
|
||||
>
|
||||
<path
|
||||
fill={color}
|
||||
d="M150 936q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 876h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 936H150Zm164-165q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T314 711h333q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T647 771H314ZM150 606q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 546h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 606H150Zm164-165q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T314 381h333q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T647 441H314ZM150 276q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 216h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 276H150Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
17
src/assets/svgs/AlignLeftSVG.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { SVGProps } from './interfaces'
|
||||
|
||||
export const AlignLeftSVG: React.FC<SVGProps> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path
|
||||
fill={color}
|
||||
d="M150 771q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 711h412q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T562 771H150Zm0-330q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 381h412q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T562 441H150Zm0 165q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 546h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 606H150Zm0 330q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 876h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 936H150Zm0-660q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 216h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 276H150Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
17
src/assets/svgs/AlignRightSVG.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { SVGProps } from './interfaces'
|
||||
|
||||
export const AlignRightSVG: React.FC<SVGProps> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path
|
||||
fill={color}
|
||||
d="M150 936q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 876h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 936H150Zm249-165q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T399 711h411q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 771H399ZM150 606q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 546h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 606H150Zm249-165q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T399 381h411q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 441H399ZM150 276q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 216h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 276H150Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
3
src/assets/svgs/ArrowDown.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="11" height="7" viewBox="0 0 11 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.57143 0L0 1.55556L5.5 7L11 1.55556L9.42857 0L5.5 3.88889L1.57143 0Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 197 B |
3
src/assets/svgs/Attachment.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="11" height="19" viewBox="0 0 11 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.04183 -2.20378e-07C7.8285 -9.85692e-08 10.0835 2.255 10.0835 5.04167L10.0835 14.6667C10.0835 16.6925 8.44266 18.3333 6.41683 18.3333C4.391 18.3333 2.75016 16.6925 2.75016 14.6667L2.75016 6.875C2.75016 5.61 3.77683 4.58333 5.04183 4.58333C6.30683 4.58333 7.3335 5.61 7.3335 6.875L7.3335 13.75L5.50016 13.75L5.50016 6.7925C5.50016 6.28833 4.5835 6.28833 4.5835 6.7925L4.5835 14.6667C4.5835 15.675 5.4085 16.5 6.41683 16.5C7.42516 16.5 8.25016 15.675 8.25016 14.6667L8.25016 5.04167C8.25016 3.2725 6.811 1.83333 5.04183 1.83333C3.27266 1.83333 1.8335 3.2725 1.8335 5.04167L1.8335 13.75L0.000162477 13.75L0.000162858 5.04167C0.00016298 2.255 2.25516 -3.42187e-07 5.04183 -2.20378e-07Z" fill="#A6A0A0"/>
|
||||
</svg>
|
After Width: | Height: | Size: 814 B |
3
src/assets/svgs/AttachmentMail.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="19" height="10" viewBox="0 0 19 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 5C0 2.23636 2.337 0 5.225 0H15.2C17.2995 0 19 1.62727 19 3.63636C19 5.64545 17.2995 7.27273 15.2 7.27273H7.125C5.814 7.27273 4.75 6.25455 4.75 5C4.75 3.74545 5.814 2.72727 7.125 2.72727H14.25V4.54545H7.0395C6.517 4.54545 6.517 5.45455 7.0395 5.45455H15.2C16.245 5.45455 17.1 4.63636 17.1 3.63636C17.1 2.63636 16.245 1.81818 15.2 1.81818H5.225C3.3915 1.81818 1.9 3.24545 1.9 5C1.9 6.75455 3.3915 8.18182 5.225 8.18182H14.25V10H5.225C2.337 10 0 7.76364 0 5Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 587 B |
17
src/assets/svgs/BoldSVG.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { SVGProps } from './interfaces'
|
||||
|
||||
export const BoldSVG: React.FC<SVGProps> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path
|
||||
fill={color}
|
||||
d="M335 856q-25 0-42.5-17.5T275 796V356q0-25 17.5-42.5T335 296h168q66 0 114.5 42T666 444q0 38-21 70t-56 49v6q43 14 69.5 50t26.5 81q0 68-52.5 112T510 856H335Zm26-76h144q38 0 66-25t28-63q0-37-28-62t-66-25H361v175Zm0-247h136q35 0 60.5-23t25.5-58q0-35-25.5-58.5T497 370H361v163Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
3
src/assets/svgs/Check.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="22" height="17" viewBox="0 0 22 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.1745 3.10899L9.44157 14.8419L7.93313 16.3504L6.42468 14.8419L0.0249023 8.44214L3.04179 5.42525L7.93313 10.3166L18.1576 0.0921021L21.1745 3.10899Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 318 B |
23
src/assets/svgs/CircleSVG.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const CircleSVG: 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="m424-296 282-282-56-56-226 226-114-114-56 56 170 170Zm56 216q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
3
src/assets/svgs/Close.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 0L0 2L4 6L0 10L2 12L6 8L10 12L12 10L8 6L12 2L10 0L6 4L2 0Z" fill="white" fill-opacity="0.2"/>
|
||||
</svg>
|
After Width: | Height: | Size: 209 B |
35
src/assets/svgs/CloseSVG.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { styled } from '@mui/system';
|
||||
import { SVGProps } from './interfaces';
|
||||
|
||||
// Create a styled container with hover effects
|
||||
const HoverContainer = styled('div')({
|
||||
display: 'inline-block',
|
||||
transition: 'transform 0.3s ease, opacity 0.3s ease',
|
||||
opacity: 1, // Default opacity
|
||||
|
||||
'&:hover': {
|
||||
transform: 'scale(1.5)',
|
||||
opacity: 1, // Increased opacity on hover
|
||||
},
|
||||
});
|
||||
|
||||
export const CloseSVG:React.FC<SVGProps> = ({ color, opacity }) => {
|
||||
return (
|
||||
<HoverContainer>
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2 0L0 2L4 6L0 10L2 12L6 8L10 12L12 10L8 6L12 2L10 0L6 4L2 0Z"
|
||||
fill={color}
|
||||
fillOpacity={opacity}
|
||||
/>
|
||||
</svg>
|
||||
</HoverContainer>
|
||||
);
|
||||
};
|
17
src/assets/svgs/CodeBlockSVG.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { SVGProps } from './interfaces'
|
||||
|
||||
export const CodeBlockSVG: React.FC<SVGProps> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path
|
||||
fill={color}
|
||||
d="m330 576 70-70q9-9 9-22t-9-22q-9-9-21.833-9-12.834 0-22.167 9l-93 93q-5 5-7 10.133-2 5.134-2 11Q254 582 256 587q2 5 7 10l94 94q9.333 9 22.167 9Q392 700 401 691q9-9 9-22t-9-22l-71-71Zm300 0-71 71q-9 9-9 22t9 22q9 9 21.833 9 12.834 0 22.167-9l94-94q5-5 7-10.133 2-5.134 2-11Q706 570 704 565q-2-5-7-10l-94-94q-4-5-10-7t-12-2q-6 0-11.5 2t-10.167 6.8Q550 470.4 550 483.2q0 12.8 9 21.8l71 71ZM180 936q-24 0-42-18t-18-42V276q0-24 18-42t42-18h600q24 0 42 18t18 42v600q0 24-18 42t-42 18H180Zm0-60h600V276H180v600Zm0-600v600-600Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
9
src/assets/svgs/ComposeIcon.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<rect width="20" height="20" fill="url(#pattern0)"/>
|
||||
<defs>
|
||||
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||
<use xlink:href="#image0_127_477" transform="scale(0.015625)"/>
|
||||
</pattern>
|
||||
<image id="image0_127_477" width="64" height="64" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAAsTAAALEwEAmpwYAAACW0lEQVR4nO3bu2sUURSA8RsVMQRRfBArQ3yAb8RC0MZKsLBRtLHQQvEfEBEVS+2sjQHjo1TERhCLNHZWVsFGxShiFLTygSj+5OIKm2E3m12Tnbk789V7z+53zpl7z87uhFBRUZEiWIi9uIQx3MUozmMX+kIvgkU4iUkz8xxHeyoRWIlx7fEQy0LqYBivdMYzLA2Jy082EPuCERzDfpzCffxs8NoHSV4Omss/wmCTNTsw0WDNkdAj8jewoMXa5Q2SMJFMF2guP9ZKPtMJvzLrd4aE5W/OVr4uVtwT6jkXEpW/1a58LV7cGOsZCQnK34mTX4cxD2Ri3QuJyU9h4D/ixiOyntGQkHz9kbekw9jXTediSEy+4yRgDb6azu6QoHzbSYgbJh6bzstONtJ5AUMdzvZRqr9F7D5ca7D2eEi08rPuhFrl47CUZbzTk6Ro8k2TUJOPw1KW2GmrQsJt3/JyqMnHYSnLG6zvRfn6JAwUXX54jtq+GXFYyhLfb10vV34mClP5oUq+u7wtu/yGvN1DJa+qfDcpddu/x+a83UMlr6p8Nyl120+VXX5L3u6hkldVvptUbR/yRrXhyWO331qEyq/G6xyGnE2hCOBsDvL5Dzn/qP23rlt8KETbZ35X+1RK+UgcObsovy0UDZwurXwEt0srH8GLMssPllY+gsPzJP+x8PIRXJ0n+e0hBfC0zPL9+FFK+Qj2lVY+ggullZ+DL0Df8QSXsTakiL/ndDs3LuIzOGewB4tD6uBdE9nftUdN4kOJJ7Ax9CI4hM/4VmvnKziIFXl/toqKMG/8Ad4tdVYcSnGDAAAAAElFTkSuQmCC"/>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
3
src/assets/svgs/CreateThread.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 9.80209V9.0205C0.0460138 8.67679 0.080024 8.31425 0.144043 7.98466C0.469856 6.30568 1.25577 4.79934 2.38071 3.6977C4.13924 1.88262 6.22987 0.985679 8.52256 0.674927C9.9086 0.485649 11.3116 0.565177 12.6758 0.910345C14.5124 1.34351 16.1889 2.2075 17.6053 3.67886C18.7276 4.84183 19.5319 6.24257 19.858 7.98466C19.918 8.31189 19.952 8.64383 20 8.97577V9.80209C19.9827 9.8676 19.9693 9.93447 19.96 10.0022C19.8708 11.2186 19.5113 12.3861 18.9177 13.3875C17.961 15.0025 16.6297 16.2594 15.0825 17.0082C12.4657 18.3525 9.75693 18.5667 6.98209 17.8346C6.8589 17.8074 6.73157 17.8264 6.61799 17.8887C5.15955 18.7339 3.70511 19.5908 2.24867 20.4501C2.18866 20.4854 2.12464 20.5183 2.0146 20.5748L3.78714 16.3703C3.37301 16.0148 2.96889 15.7017 2.60078 15.3415C1.42243 14.1879 0.556167 12.7895 0.182055 11.0192C0.0980294 10.6213 0.060018 10.2094 0 9.80209ZM14.0042 10.5931C14.1362 10.5968 14.2676 10.5698 14.3907 10.5135C14.5138 10.4572 14.6262 10.3728 14.7214 10.2651C14.8167 10.1574 14.8928 10.0286 14.9455 9.8861C14.9982 9.7436 15.0264 9.59023 15.0285 9.43484V9.4113C15.0285 9.25517 15.0024 9.10058 14.9516 8.95634C14.9008 8.8121 14.8264 8.68104 14.7326 8.57064C14.6388 8.46025 14.5274 8.37268 14.4048 8.31293C14.2823 8.25319 14.1509 8.22243 14.0182 8.22243C13.8855 8.22243 13.7542 8.25319 13.6316 8.31293C13.509 8.37268 13.3976 8.46025 13.3038 8.57064C13.21 8.68104 13.1356 8.8121 13.0848 8.95634C13.034 9.10058 13.0079 9.25517 13.0079 9.4113C13.0074 9.56588 13.0327 9.71906 13.0825 9.86211C13.1323 10.0052 13.2055 10.1353 13.2981 10.245C13.3906 10.3547 13.5005 10.442 13.6217 10.5017C13.7429 10.5614 13.8728 10.5925 14.0042 10.5931ZM10.003 10.5931C10.203 10.5926 10.3983 10.5225 10.5644 10.3915C10.7306 10.2606 10.86 10.0746 10.9364 9.85719C11.0129 9.63976 11.0329 9.40056 10.9939 9.16977C10.9549 8.93898 10.8588 8.72694 10.7175 8.5604C10.5763 8.39385 10.3962 8.28026 10.2002 8.23396C10.0041 8.18765 9.80084 8.21071 9.61591 8.30022C9.43099 8.38973 9.27273 8.54168 9.1611 8.7369C9.04948 8.93212 8.98949 9.16187 8.9887 9.39717C8.98975 9.71356 9.09688 10.0167 9.28682 10.2406C9.47675 10.4646 9.73413 10.5912 10.003 10.5931ZM4.98349 9.3854C4.9836 9.61979 5.04316 9.8488 5.15456 10.0431C5.26595 10.2374 5.42411 10.3882 5.60876 10.476C5.79341 10.5639 5.99616 10.5849 6.19102 10.5364C6.38588 10.4878 6.56399 10.3719 6.70252 10.2035C6.84105 10.0351 6.93371 9.82183 6.96861 9.59108C7.00352 9.36032 6.97909 9.12255 6.89845 8.90823C6.8178 8.69392 6.68463 8.51281 6.51597 8.38811C6.34732 8.26342 6.15087 8.20081 5.95179 8.20831C5.69208 8.21809 5.44579 8.34641 5.26507 8.56611C5.08434 8.78581 4.98336 9.07963 4.98349 9.3854Z" fill="#29292B"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.7 KiB |
20
src/assets/svgs/CreateThreadIcon.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { styled } from '@mui/system';
|
||||
import { SVGProps } from './interfaces';
|
||||
|
||||
// Create a styled container with hover effects
|
||||
const SvgContainer = styled('svg')({
|
||||
'& path': {
|
||||
fill: 'rgba(41, 41, 43, 1)', // Default to red if no color prop
|
||||
}
|
||||
});
|
||||
|
||||
export const CreateThreadIcon:React.FC<SVGProps> = ({ color, opacity }) => {
|
||||
return (
|
||||
<SvgContainer width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 9.80209V9.0205C0.0460138 8.67679 0.080024 8.31425 0.144043 7.98466C0.469856 6.30568 1.25577 4.79934 2.38071 3.6977C4.13924 1.88262 6.22987 0.985679 8.52256 0.674927C9.9086 0.485649 11.3116 0.565177 12.6758 0.910345C14.5124 1.34351 16.1889 2.2075 17.6053 3.67886C18.7276 4.84183 19.5319 6.24257 19.858 7.98466C19.918 8.31189 19.952 8.64383 20 8.97577V9.80209C19.9827 9.8676 19.9693 9.93447 19.96 10.0022C19.8708 11.2186 19.5113 12.3861 18.9177 13.3875C17.961 15.0025 16.6297 16.2594 15.0825 17.0082C12.4657 18.3525 9.75693 18.5667 6.98209 17.8346C6.8589 17.8074 6.73157 17.8264 6.61799 17.8887C5.15955 18.7339 3.70511 19.5908 2.24867 20.4501C2.18866 20.4854 2.12464 20.5183 2.0146 20.5748L3.78714 16.3703C3.37301 16.0148 2.96889 15.7017 2.60078 15.3415C1.42243 14.1879 0.556167 12.7895 0.182055 11.0192C0.0980294 10.6213 0.060018 10.2094 0 9.80209ZM14.0042 10.5931C14.1362 10.5968 14.2676 10.5698 14.3907 10.5135C14.5138 10.4572 14.6262 10.3728 14.7214 10.2651C14.8167 10.1574 14.8928 10.0286 14.9455 9.8861C14.9982 9.7436 15.0264 9.59023 15.0285 9.43484V9.4113C15.0285 9.25517 15.0024 9.10058 14.9516 8.95634C14.9008 8.8121 14.8264 8.68104 14.7326 8.57064C14.6388 8.46025 14.5274 8.37268 14.4048 8.31293C14.2823 8.25319 14.1509 8.22243 14.0182 8.22243C13.8855 8.22243 13.7542 8.25319 13.6316 8.31293C13.509 8.37268 13.3976 8.46025 13.3038 8.57064C13.21 8.68104 13.1356 8.8121 13.0848 8.95634C13.034 9.10058 13.0079 9.25517 13.0079 9.4113C13.0074 9.56588 13.0327 9.71906 13.0825 9.86211C13.1323 10.0052 13.2055 10.1353 13.2981 10.245C13.3906 10.3547 13.5005 10.442 13.6217 10.5017C13.7429 10.5614 13.8728 10.5925 14.0042 10.5931ZM10.003 10.5931C10.203 10.5926 10.3983 10.5225 10.5644 10.3915C10.7306 10.2606 10.86 10.0746 10.9364 9.85719C11.0129 9.63976 11.0329 9.40056 10.9939 9.16977C10.9549 8.93898 10.8588 8.72694 10.7175 8.5604C10.5763 8.39385 10.3962 8.28026 10.2002 8.23396C10.0041 8.18765 9.80084 8.21071 9.61591 8.30022C9.43099 8.38973 9.27273 8.54168 9.1611 8.7369C9.04948 8.93212 8.98949 9.16187 8.9887 9.39717C8.98975 9.71356 9.09688 10.0167 9.28682 10.2406C9.47675 10.4646 9.73413 10.5912 10.003 10.5931ZM4.98349 9.3854C4.9836 9.61979 5.04316 9.8488 5.15456 10.0431C5.26595 10.2374 5.42411 10.3882 5.60876 10.476C5.79341 10.5639 5.99616 10.5849 6.19102 10.5364C6.38588 10.4878 6.56399 10.3719 6.70252 10.2035C6.84105 10.0351 6.93371 9.82183 6.96861 9.59108C7.00352 9.36032 6.97909 9.12255 6.89845 8.90823C6.8178 8.69392 6.68463 8.51281 6.51597 8.38811C6.34732 8.26342 6.15087 8.20081 5.95179 8.20831C5.69208 8.21809 5.44579 8.34641 5.26507 8.56611C5.08434 8.78581 4.98336 9.07963 4.98349 9.3854Z" fill="#29292B"/>
|
||||
</SvgContainer>
|
||||
|
||||
|
||||
);
|
||||
};
|
23
src/assets/svgs/EmptyCircleSVG.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { IconTypes } from "./IconTypes";
|
||||
|
||||
export const EmptyCircleSVG: React.FC<IconTypes> = ({
|
||||
color,
|
||||
height,
|
||||
width,
|
||||
className,
|
||||
onClickFunc,
|
||||
}) => {
|
||||
return (
|
||||
|
||||
<svg onClick={onClickFunc}
|
||||
className={className}
|
||||
fill={color}
|
||||
|
||||
height={height}
|
||||
|
||||
width={width} xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" ><path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
3
src/assets/svgs/Forward.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.66667 3.33333V0L13.3333 6.66667L6.66667 13.3333V10H0V3.33333H6.66667Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 201 B |
3
src/assets/svgs/Group.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="40" height="17" viewBox="0 0 40 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.2446 15.4664C29.2446 15.4664 28.7784 13.1043 27.8874 11.8448C26.9965 10.5853 25.1859 9.3098 25.1859 9.3098C25.1859 9.3098 26.1012 8.62402 26.5818 8.34489C26.7196 8.24619 26.8744 8.17296 27.0387 8.12875L27.3902 8.54312C27.7809 8.96455 28.2571 9.29986 28.7877 9.52718C29.1303 9.67031 29.4861 9.78013 29.8502 9.85509C31.4865 10.2142 32.8406 9.45647 33.6824 8.79323C33.9387 8.59129 34.0943 8.29425 34.3327 8.07688L34.8162 8.32729C35.2795 8.60158 35.723 8.90739 36.1434 9.24249C37.064 10.0033 37.8031 10.9557 38.3078 12.0317C38.8124 13.1077 39.0701 14.2805 39.0624 15.4664H29.2446ZM31.7937 8.08676C31.5014 8.1571 31.2049 8.20867 30.9059 8.24114C30.3225 8.28451 29.7373 8.18669 29.2009 7.95615C27.7165 7.35342 26.2374 5.54556 26.9421 3.33784C27.1115 2.77799 27.4088 2.26409 27.8112 1.83579C28.2135 1.4075 28.71 1.07627 29.2624 0.86767C29.5165 0.777226 29.7779 0.707923 30.0437 0.660484L30.4399 0.617564C32.4131 0.586687 33.6565 1.6078 34.2371 2.93984C34.6299 3.89702 34.633 4.96718 34.2459 5.92659C34.0229 6.44242 33.6875 6.90333 33.2637 7.27649C32.8398 7.64965 32.3379 7.92589 31.7937 8.08552V8.08676ZM28.1331 16.6737H11.4331C11.4256 15.4899 11.6826 14.3191 12.1857 13.2447C12.6888 12.1703 13.4257 11.2188 14.3437 10.4581C14.7582 10.126 15.1958 9.82303 15.6534 9.55157C15.7913 9.45285 15.9462 9.37963 16.1106 9.33543L16.4621 9.7498C16.8528 10.1712 17.329 10.5065 17.8596 10.7339C18.2022 10.877 18.558 10.9868 18.9221 11.0618C20.5587 11.4212 21.9127 10.6631 22.7543 9.99991C23.0109 9.79797 23.1665 9.50093 23.4046 9.28356L23.8881 9.53397C24.3514 9.80829 24.7948 10.1141 25.2152 10.4492C26.1357 11.2102 26.8746 12.1628 27.3791 13.2388C27.8835 14.3149 28.1409 15.4878 28.1331 16.6737ZM20.864 9.29405C20.5717 9.36443 20.2752 9.416 19.9762 9.44844C19.3928 9.4918 18.8076 9.39398 18.2712 9.16344C16.7868 8.56072 15.3077 6.75286 16.0124 4.54483C16.1818 3.98498 16.4792 3.47107 16.8815 3.04278C17.2838 2.61449 17.7803 2.28326 18.3327 2.07466C18.5869 1.98422 18.8482 1.91491 19.114 1.86747L19.5096 1.82424C21.4827 1.79337 22.7262 2.81448 23.3068 4.14652C23.6998 5.10367 23.7029 6.17389 23.3156 7.13327C23.0929 7.64941 22.7577 8.11066 22.334 8.48415C21.9102 8.85764 21.4083 9.13418 20.864 9.29405ZM10.4427 15.4664H0.624928C0.617117 14.2806 0.874661 13.1077 1.37913 12.0317C1.88361 10.9557 2.62254 10.0031 3.54305 9.24218C3.96345 8.90709 4.40692 8.60129 4.87024 8.32698L5.35368 8.07657C5.59212 8.29271 5.74774 8.59098 6.00399 8.79292C6.84555 9.45616 8.19962 10.2133 9.83618 9.85478C10.2003 9.77982 10.5561 9.67 10.8987 9.52687C11.4293 9.29955 11.9055 8.96424 12.2962 8.54281L12.6477 8.12844C12.8121 8.17262 12.967 8.24585 13.1049 8.34458C13.5856 8.62248 14.5009 9.30949 14.5009 9.30949C14.5009 9.30949 12.6902 10.585 11.7993 11.8448C10.9084 13.1046 10.4427 15.4664 10.4427 15.4664ZM8.78118 8.24114C8.48217 8.2087 8.18561 8.15713 7.89337 8.08676C7.34924 7.92719 6.84742 7.65104 6.42363 7.27799C5.99983 6.90494 5.6645 6.44415 5.44149 5.92844C5.05415 4.96906 5.05728 3.89884 5.45024 2.94169C6.03118 1.60872 7.2743 0.586687 9.24743 0.617564L9.64305 0.660792C9.90886 0.708232 10.1702 0.777535 10.4243 0.867978C10.9767 1.07658 11.4733 1.4078 11.8756 1.8361C12.2779 2.26439 12.5752 2.7783 12.7446 3.33815C13.449 5.54618 11.9702 7.35404 10.4856 7.95645C9.94928 8.18679 9.36428 8.28451 8.78118 8.24114Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.4 KiB |
17
src/assets/svgs/H2SVG.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { SVGProps } from './interfaces'
|
||||
|
||||
export const H2SVG: React.FC<SVGProps> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path
|
||||
fill={color}
|
||||
d="M149.825 776Q137 776 128.5 767.375T120 746V406q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T180 406v140h180V406q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T420 406v340q0 12.75-8.675 21.375-8.676 8.625-21.5 8.625-12.825 0-21.325-8.625T360 746V606H180v140q0 12.75-8.675 21.375-8.676 8.625-21.5 8.625ZM570 776q-12.75 0-21.375-8.625T540 746V606q0-24.75 17.625-42.375T600 546h180V436H570q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T570 376h210q24.75 0 42.375 17.625T840 436v110q0 24.75-17.625 42.375T780 606H600v110h210q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 776H570Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
17
src/assets/svgs/H3SVG.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { SVGProps } from './interfaces'
|
||||
|
||||
export const H3SVG: React.FC<SVGProps> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path
|
||||
fill={color}
|
||||
d="M149.825 776Q137 776 128.5 767.375T120 746V406q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T180 406v140h180V406q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T420 406v340q0 12.75-8.675 21.375-8.676 8.625-21.5 8.625-12.825 0-21.325-8.625T360 746V606H180v140q0 12.75-8.675 21.375-8.676 8.625-21.5 8.625ZM570 776q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T570 716h210V606H650q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T650 546h130V436H570q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T570 376h210q24.75 0 42.375 17.625T840 436v280q0 24.75-17.625 42.375T780 776H570Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
3
src/assets/svgs/Home.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="23" height="20" viewBox="0 0 23 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.5 0L0 10.4764H3.13636V20H9.40909V12.5927H13.5909V20H19.8636V10.4764H23L11.5 0Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 211 B |
7
src/assets/svgs/IconTypes.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface IconTypes {
|
||||
color?: string;
|
||||
height: string;
|
||||
width: string;
|
||||
className?: string;
|
||||
onClickFunc?: (e?: any) => void;
|
||||
}
|
17
src/assets/svgs/ItalicSVG.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { SVGProps } from './interfaces'
|
||||
|
||||
export const ItalicSVG: React.FC<SVGProps> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path
|
||||
fill={color}
|
||||
d="M264 857q-16.8 0-28.4-11.641-11.6-11.641-11.6-28.5t11.6-28.359Q247.2 777 264 777h94l139-409H378q-16.8 0-28.4-11.641-11.6-11.641-11.6-28.5t11.6-28.359Q361.2 288 378 288h300q16.8 0 28.4 11.641 11.6 11.641 11.6 28.5T706.4 356.5Q694.8 368 678 368h-94L445 777h119q16.8 0 28.4 11.641 11.6 11.641 11.6 28.5T592.4 845.5Q580.8 857 564 857H264Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
17
src/assets/svgs/LinkSVG.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { SVGProps } from './interfaces'
|
||||
|
||||
export const LinkSVG: React.FC<SVGProps> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path
|
||||
fill={color}
|
||||
d="M280 776q-85 0-142.5-57.5T80 576q0-85 57.5-142.5T280 376h140q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T420 436H280q-60 0-100 40t-40 100q0 60 40 100t100 40h140q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T420 776H280Zm75-170q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T355 546h250q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T605 606H355Zm185 170q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T540 716h140q60 0 100-40t40-100q0-60-40-100t-100-40H540q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T540 376h140q85 0 142.5 57.5T880 576q0 85-57.5 142.5T680 776H540Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
3
src/assets/svgs/Lock.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="12" height="14" viewBox="0 0 12 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 0C3.89764 0 2.18182 1.76157 2.18182 3.92V5.04H1.09091C0.488182 5.04 0 5.5412 0 6.16V12.88C0 13.4988 0.488182 14 1.09091 14H10.9091C11.5118 14 12 13.4988 12 12.88V6.16C12 5.5412 11.5118 5.04 10.9091 5.04H9.81818V3.92C9.81818 1.83209 8.20177 0.150397 6.19389 0.0404687C6.1322 0.0149579 6.06649 0.00124273 6 0ZM6 1.12C7.51291 1.12 8.72727 2.36675 8.72727 3.92V5.04H3.27273V3.92C3.27273 2.36675 4.48709 1.12 6 1.12Z" fill="#A6A0A0"/>
|
||||
</svg>
|
After Width: | Height: | Size: 545 B |
54
src/assets/svgs/Logo.svg
Normal file
@ -0,0 +1,54 @@
|
||||
<svg width="124" height="53" viewBox="0 0 124 53" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="22.6464" cy="22.6464" r="21.4464" fill="#434448" stroke="white" stroke-width="2.4"/>
|
||||
<g clip-path="url(#clip0_81_1037)">
|
||||
<g clip-path="url(#clip1_81_1037)">
|
||||
<g filter="url(#filter0_d_81_1037)">
|
||||
<path d="M16.611 21.8694L9.1033 26.1896L9.1033 17.2214L16.611 21.8694ZM27.8486 21.8694L35.3628 25.8025V16.8343L27.8486 21.8694ZM26.0009 23.2697L22.6138 25.2794C22.4975 25.3534 22.3637 25.3922 22.2298 25.3922C22.0959 25.3922 21.9621 25.3534 21.8458 25.2794L18.4586 23.2697L9.1033 28.9149V30.6415C9.1033 31.0286 8.97426 31.0286 9.90009 31.0286H34.5595C35.4918 31.0286 35.3628 31.1576 35.3628 30.1899V28.4478L26.0009 23.2697ZM22.2298 22.1894L34.5595 14.8239L35.3628 14.2587C35.1902 13.2617 35.6022 12.7102 34.5595 12.7102H9.90009C8.85735 12.7102 9.27591 13.2653 9.1033 14.2587L9.90009 14.8239L22.2298 22.1894Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g filter="url(#filter1_d_81_1037)">
|
||||
<path d="M50.3883 34V13.0527H53.8307L61.5211 30.5283L69.065 13.0527H72.3609V34H69.9439V16.8174L62.5025 34H60.3639L52.8053 16.8174V34H50.3883ZM77.4299 34H74.6174L83.509 13.0527H86.5119L95.4475 34H92.4445L89.7346 27.4082H82.8059L83.5529 25.2109H88.8264L84.9152 15.7188L77.4299 34ZM100.853 13.0527V34H98.1434V13.0527H100.853ZM108.398 13.0527V31.8027H119.355V34H105.688V13.0527H108.398Z" fill="white"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_d_81_1037)">
|
||||
<path d="M42.5779 44.5236L33.022 35.0611L34.1072 34.0206L34.6127 33.5128L35.1181 33.0049L44.6739 42.4674L42.5779 44.5236Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_81_1037" x="5.09717" y="12.7102" width="34.2715" height="26.3242" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_81_1037"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_81_1037" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_d_81_1037" x="46.3882" y="13.0527" width="76.9668" height="28.9473" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_81_1037"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_81_1037" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter2_d_81_1037" x="29.022" y="33.0049" width="19.6519" height="19.5188" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_81_1037"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_81_1037" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_81_1037">
|
||||
<rect width="35.2277" height="35.2277" fill="white" transform="translate(5.03271 5.03247)"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_81_1037">
|
||||
<rect width="35.2277" height="35.2277" fill="white" transform="translate(5.03271 5.03247)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 3.9 KiB |
3
src/assets/svgs/ModalClose.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.14468 0L0.394043 2.66667L5.89531 8L0.394043 13.3333L3.14468 16L8.64594 10.6667L14.1472 16L16.8978 13.3333L11.3966 8L16.8978 2.66667L14.1472 0L8.64594 5.33333L3.14468 0Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 300 B |
3
src/assets/svgs/More.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.87531 8.48462C5.49219 9.1523 4.52994 9.15487 4.14327 8.48923L1.54475 4.01604C1.15808 3.3504 1.63698 2.51579 2.40678 2.51374L7.57993 2.49995C8.34973 2.4979 8.83308 3.32995 8.44995 3.99764L5.87531 8.48462Z" fill="#D9D9D9"/>
|
||||
</svg>
|
After Width: | Height: | Size: 337 B |
3
src/assets/svgs/NewMessageAttachment.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="30" height="17" viewBox="0 0 30 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 8.5C0 3.80182 3.69 0 8.25 0H24C27.315 0 30 2.76636 30 6.18182C30 9.59727 27.315 12.3636 24 12.3636H11.25C9.18 12.3636 7.5 10.6327 7.5 8.5C7.5 6.36727 9.18 4.63636 11.25 4.63636H22.5V7.72727H11.115C10.29 7.72727 10.29 9.27273 11.115 9.27273H24C25.65 9.27273 27 7.88182 27 6.18182C27 4.48182 25.65 3.09091 24 3.09091H8.25C5.355 3.09091 3 5.51727 3 8.5C3 11.4827 5.355 13.9091 8.25 13.9091H22.5V17H8.25C3.69 17 0 13.1982 0 8.5Z" fill="#545454" fill-opacity="0.5"/>
|
||||
</svg>
|
After Width: | Height: | Size: 577 B |
25
src/assets/svgs/NewWindowSVG.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
interface NewWindowSVGProps {
|
||||
color: string
|
||||
height: string
|
||||
width: string
|
||||
}
|
||||
|
||||
export const NewWindowSVG: React.FC<NewWindowSVGProps> = ({
|
||||
color,
|
||||
height,
|
||||
width
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
width={width}
|
||||
viewBox="0 96 960 960"
|
||||
>
|
||||
<path
|
||||
d="M180 936q-24 0-42-18t-18-42V276q0-24 18-42t42-18h300v60H180v600h600V576h60v300q0 24-18 42t-42 18H180Zm480-420V396H540v-60h120V216h60v120h120v60H720v120h-60Z"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
3
src/assets/svgs/Reply.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="13" viewBox="0 0 16 13" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.33333 3.49984V0.166504L0.5 5.99984L6.33333 11.8332V8.4165C10.5 8.4165 13.4167 9.74984 15.5 12.6665C14.6667 8.49984 12.1667 4.33317 6.33333 3.49984Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 279 B |
3
src/assets/svgs/Return.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="21" height="14" viewBox="0 0 21 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.8483 4.02178H3.80552L6.14049 1.79897C6.34361 1.59876 6.456 1.33062 6.45346 1.05229C6.45092 0.773967 6.33365 0.507725 6.12691 0.310911C5.92016 0.114097 5.64049 0.00245871 5.34812 4.01281e-05C5.05575 -0.00237845 4.77408 0.104616 4.56377 0.29798L0.326479 4.33174C0.117435 4.53081 0 4.80076 0 5.08224C0 5.36371 0.117435 5.63367 0.326479 5.83273L4.56377 9.8665C4.77408 10.0599 5.05575 10.1669 5.34812 10.1644C5.64049 10.162 5.92016 10.0504 6.12691 9.85356C6.33365 9.65675 6.45092 9.39051 6.45346 9.11218C6.456 8.83386 6.34361 8.56571 6.14049 8.36551L3.80552 6.14482H15.8483C16.6232 6.14482 17.3663 6.43783 17.9142 6.9594C18.462 7.48098 18.7698 8.18838 18.7698 8.92599C18.7698 9.6636 18.462 10.371 17.9142 10.8926C17.3663 11.4141 16.6232 11.7072 15.8483 11.7072V13.8302C17.2146 13.8302 18.525 13.3135 19.4911 12.3938C20.4572 11.4741 21 10.2267 21 8.92599C21 7.62531 20.4572 6.37791 19.4911 5.45819C18.525 4.53847 17.2146 4.02178 15.8483 4.02178Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
9
src/assets/svgs/Send.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<rect width="20" height="20" fill="url(#pattern0)"/>
|
||||
<defs>
|
||||
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||
<use xlink:href="#image0_81_1290" transform="scale(0.0104167)"/>
|
||||
</pattern>
|
||||
<image id="image0_81_1290" width="96" height="96" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAACXBIWXMAAAsTAAALEwEAmpwYAAADW0lEQVR4nO3dO4hdRRgH8CPxGSO+ooVvUPCJAUEUHyAoaGMhKGhjIwrapBEFLVQQERREC8VOtFAs1UKxdFMo0ViImEjSiG8kMSKJkJufnOwE1+TeTXZz752ZM9+v2m4eZ/lz7pxvZrouhBBCCOXAepycux/NwkXYiVf7v3P3p0n4yqIRPsQdufvUFDzjcP1DeQSn5O7f4GGDyX7Fi7gwdz8HDTss7x+8H/E0I3jF0duc4inenqYFt1m5X1I8nT+1jrQKa/C71TkYTzflHkfV8JZj18fTgzgh93iqg3tMz894FufkHlc1sBZ/m669KZ5uzD2+KuADs7M5xdPxucdZLDxk9n5K8bQ+93iLg3Oxz3zsxdu4Nve4i4LPzN8C7ot4WnwAj8vnxxRPZ3etwqXy25Pi6ZquRfhGORZSPK3pWoHnlWc7nsRZ3dDheuX6C2/i6m6ocBx+ULb9+HSw8YTX1eP7FE9ndEOBO9Vnd4qnK7va9UvKqWSlRqMUT3f3cdrVCu+q3zZsxLquNrjfcPyZ4unyrhY4LS2aDcmoqnjCx4Zra4qnU7tS4VHDtyvVx17SlQbnpR89LRgdrI8tKp7wufZsSQVoa0t4AE9p187s5fu4KvcsFCBv+X56Ywi5yvfxUmo85Cjfxy1LGg7/F+X7IYQQQgizgVsPefUKh7+G3jzLB/DykgZDhh9isRTxny/nuhTRF8kuabxV+7ItxuFp7fotxUzW5egvtGdLER9k+p3wLX6S7EqBx7TzUf7irjT4xHB9l8pS8n/3HQenpx8ZQzIqsvJhHDxgOHYVW/szCd5Tv63FV7+Ng5NSQWuNRlXVf46Du9RbAX1FVzu8oR7bqt0DUPEmvVH1MTMJblCu3YPZBzYJXlDuTsgzu6HDt8qwf9B7gcfBZblnXQu74SfBExknfnsz50FMgk0ZJn6hqZg5wpFl/evdPOxp+kygcfDwHCZ+R/Mxs8wD+GiGE78Q58It/9+/LsXCNMXJiEcL905x4uPwvZXCO1OY+DgddzX68zrxxzHGzIZVNR4OPIDbVzHxcQTxtOC1FUx83BGQ8RKfOIZ+2nDdES5iiHtiZgnPRcxkhK8PiZm4jGde+iKlJddRXTC3hsOift0dJ8Z8hBBCCCGEEEIIIXRD8i8RfTaBoLdiiAAAAABJRU5ErkJggg=="/>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
3
src/assets/svgs/SendNewMessage.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.33271 10.2306C2.88006 10.001 2.89088 9.65814 3.3554 9.46527L16.3563 4.06742C16.8214 3.87427 17.0961 4.11004 16.9689 4.59692L14.1253 15.4847C13.9985 15.9703 13.5515 16.1438 13.1241 15.8705L10.0773 13.9219C9.8629 13.7848 9.56272 13.8345 9.40985 14.0292L8.41215 15.2997C8.10197 15.6946 7.71724 15.6311 7.5525 15.1567L6.67584 12.6326C6.51125 12.1587 6.01424 11.5902 5.55821 11.359L3.33271 10.2306Z" fill="#29292B"/>
|
||||
</svg>
|
After Width: | Height: | Size: 567 B |
19
src/assets/svgs/SendNewMessage.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { styled } from '@mui/system';
|
||||
import { SVGProps } from './interfaces';
|
||||
|
||||
// Create a styled container with hover effects
|
||||
const SvgContainer = styled('svg')({
|
||||
'& path': {
|
||||
fill: 'rgba(41, 41, 43, 1)', // Default to red if no color prop
|
||||
}
|
||||
});
|
||||
|
||||
export const SendNewMessage:React.FC<SVGProps> = ({ color, opacity }) => {
|
||||
return (
|
||||
<SvgContainer width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.33271 10.2306C2.88006 10.001 2.89088 9.65814 3.3554 9.46527L16.3563 4.06742C16.8214 3.87427 17.0961 4.11004 16.9689 4.59692L14.1253 15.4847C13.9985 15.9703 13.5515 16.1438 13.1241 15.8705L10.0773 13.9219C9.8629 13.7848 9.56272 13.8345 9.40985 14.0292L8.41215 15.2997C8.10197 15.6946 7.71724 15.6311 7.5525 15.1567L6.67584 12.6326C6.51125 12.1587 6.01424 11.5902 5.55821 11.359L3.33271 10.2306Z" />
|
||||
</SvgContainer>
|
||||
|
||||
);
|
||||
};
|
4
src/assets/svgs/Sort.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="15" height="16" viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.3347 0.271977C14.0797 0.0885134 13.79 0 13.5034 0C13.0191 0 12.5424 0.251056 12.2542 0.711326L12.0008 1.11366L10.6942 3.20097L9.44204 5.19976C9.15388 5.66003 9 6.19916 9 6.75116V14.3987C9 15.2822 9.67136 16 10.4996 16C10.9145 16 11.2902 15.8214 11.5602 15.5301C11.8318 15.2404 11.9992 14.8397 11.9992 14.3987V7.57353C11.9992 7.11809 12.1275 6.6723 12.3628 6.29411L14.7465 2.48964C14.917 2.21605 15 1.90706 15 1.60129C15 1.08469 14.7646 0.577751 14.3332 0.270368L14.3347 0.271977Z" fill="white"/>
|
||||
<path d="M4.30727 3.20032L3.00075 1.11344L2.74881 0.711183C2.46065 0.251006 1.98391 0 1.49962 0C1.21297 0 0.923309 0.0884956 0.668343 0.271923C0.235353 0.579244 0 1.08608 0 1.60257C0 1.90829 0.0829771 2.21722 0.254966 2.49075L2.63716 6.29445C2.87403 6.67257 3.00075 7.11826 3.00075 7.57361V14.399C3.00075 15.2824 3.67211 16 4.50038 16C5.32864 16 6 15.2824 6 14.399V6.75141C6 6.19952 5.84762 5.6605 5.55947 5.20032L4.30576 3.20193L4.30727 3.20032Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
17
src/assets/svgs/UnderlineSVG.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { SVGProps } from './interfaces'
|
||||
|
||||
export const UnderlineSVG: React.FC<SVGProps> = ({ color, height, width }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={height}
|
||||
viewBox="0 96 960 960"
|
||||
width={width}
|
||||
>
|
||||
<path
|
||||
fill={color}
|
||||
d="M230 916q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T230 856h500q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T730 916H230Zm250-140q-100 0-156.5-58.5T267 559V257q0-16.882 12.527-28.941Q292.055 216 309.027 216 326 216 338 228.059T350 257v302q0 63 34 101t96 38q62 0 96-38t34-101V257q0-16.882 12.527-28.941Q635.055 216 652.027 216 669 216 681 228.059T693 257v302q0 100-56.5 158.5T480 776Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
1
src/assets/svgs/accountCircle.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 96 960 960" width="48"><path 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>
|
After Width: | Height: | Size: 896 B |
6
src/assets/svgs/interfaces.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface SVGProps {
|
||||
color: string
|
||||
height: string
|
||||
width: string
|
||||
opacity?: number
|
||||
}
|
9
src/assets/svgs/mail.svg
Normal file
@ -0,0 +1,9 @@
|
||||
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<rect width="21" height="21" fill="url(#pattern0)"/>
|
||||
<defs>
|
||||
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||
<use xlink:href="#image0_81_1311" transform="scale(0.01)"/>
|
||||
</pattern>
|
||||
<image id="image0_81_1311" width="100" height="100" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEGklEQVR4nO2bW6hVVRSG90lLrSw9ViomEpbICUHLKMJLYJKIGpKiEF5A6bVHX330SfGxEok6RWWgKGimgWgkijdMtBQVb1goml295SeT1sHTcc65PWevvddYc//f02bD2nuu+a855pj/GKtSEUIIIYQQQgghhBBCCJEACFNIEGNIEGNIEGNIEIOC/FX0IMT/BRkLnOr0nSiO9o7UtxX4psCBNDu3gGVdzyMt7kvg36JH12RcAF6PHRRnAr8VPcomYRcwtKsAj3hEaQN+Lnq0ibMK6O1bEd/fp9J/3z8JbCx61AnyJzA/FJ0qsTimfSV3jgNjYvNcie709y6YDlzJf3xNxSZggGduXYa7JXZS/xTo57nweeDHxt5DEtwBVgAPeebUnQFPPoh1sh8Y4fmBx4GvGnMfSXAZeCsQdd71uSQxL+sSMCUQ797PQpwIcwB4zjN/vbMV0yNzMbavTAZ+rXJ9s/IJ8KhnzoYBP+Th9n4e+IPhwN587iEJrrvoEXiAJwIX87TfDwaWYF9gbW33kQTngNcCYrwH3KxHPcRtUlNr/dME2QEMDiRBX9a7QHU7O8S0eAYw4UGWZWIp7WqfBQK80JNjQi0Vwy+AxwIb127S53dgTiBazACuFlHCPQaM9gyoD/Ah6fIT8KLnvnsBy2spYeRRU78GvB14UhYCf5MWG5zx6rnXQcBWK00OMXvgZeAM5ed2ZO8cl1cZvFIHA8339DwNfEd5uQS8GYgCC/KMAvVoA3KFrbbuWgaG2Rfw9fpkGVYp+rJcBvJOd0w1o3wQqKg+W69MslKg7Wy59egfYEnEw/ulzJ2Lm4GBgazkW+xxBngl4nLfTKGV9ESgdNkrW0VuNVnAPTytnnH2B9al1tv7BzA3EAbmZcX/ooiF11HAkVSbrWPez+js5N9o3MF2duBBmdXoHrVKge7oM54JeAJY38BxHAJGWgqlRb6OcDayeS5rQEvrZwFz9ClgG036fohLLxfn7ZjWUJZ+CThNgRQtSOcD2MOBmsKRRjQ2WzFCrQji2AkMCaScX9elsfmeBfIRRrAkiOM88GpkX3GOa54r0DVp7MEQ1gTp6NxYGggr07rZ0upC0KLAb71hsY3JoiDVjL2RwGFqb2zu6WprWkGIvCrhWo8+pvuNzXntR00rSLXMqGvrUcwCcU7AUYxTBkGqnR0mZXZ4rLF5fsFeWXKCdLDGpameCR8RqOq5KuVKSkTZBAm+KhGo42+nZJRRkOCrEp3EGF/WTpeyChLcV7KN/gYlpcyCdNCeta8Oyz6XmhQESQoJYgwJYgwJYgwJYgwJYgwJYgwJYgwJYgwJYgwJYgwJYgwJYgwJYgwJYgwJYgwJYgwJYgwJYgwJYgwJYgwJYoxqDYBCCCGEEEIIIYQQQgghKiXgLoA6IICiG27oAAAAAElFTkSuQmCC"/>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
96
src/components/DynamicHeightItem.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import React, { useRef, useState, useEffect } from 'react'
|
||||
import ReactResizeDetector from 'react-resize-detector'
|
||||
import { Layouts, Layout } from 'react-grid-layout'
|
||||
|
||||
interface DynamicHeightItemProps {
|
||||
children: React.ReactNode
|
||||
layouts: Layouts
|
||||
setLayouts: (layouts: any) => void
|
||||
i: string
|
||||
breakpoint: keyof Layouts
|
||||
rows?: number
|
||||
count?: number
|
||||
type?: string
|
||||
padding?: number
|
||||
}
|
||||
|
||||
const DynamicHeightItem: React.FC<DynamicHeightItemProps> = ({
|
||||
children,
|
||||
layouts,
|
||||
setLayouts,
|
||||
i,
|
||||
breakpoint,
|
||||
rows = 1,
|
||||
count,
|
||||
type,
|
||||
padding
|
||||
}) => {
|
||||
const [height, setHeight] = useState<number>(rows * 150)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
setHeight(ref.current.clientHeight)
|
||||
}
|
||||
}, [ref.current])
|
||||
|
||||
const onResize = () => {
|
||||
if (ref.current) {
|
||||
setHeight(ref.current.clientHeight)
|
||||
}
|
||||
}
|
||||
|
||||
const getBreakpoint = (screenWidth: number) => {
|
||||
if (screenWidth >= 996) {
|
||||
return 'md'
|
||||
} else if (screenWidth >= 768) {
|
||||
return 'sm'
|
||||
} else {
|
||||
return 'xs'
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const widthWin = window.innerWidth
|
||||
let newBreakpoint = breakpoint
|
||||
// if (!newBreakpoint) {
|
||||
// newBreakpoint = getBreakpoint(widthWin)
|
||||
// }
|
||||
|
||||
setLayouts((prev: any) => {
|
||||
const newLayouts: any = { ...prev }
|
||||
newLayouts[newBreakpoint] = newLayouts[newBreakpoint]?.map(
|
||||
(item: Layout) => {
|
||||
if (item.i === i) {
|
||||
let constantNum = 25
|
||||
|
||||
return {
|
||||
...item,
|
||||
h: Math.ceil(height / (rows * constantNum)) // Adjust this value based on your rowHeight and the number of rows the element spans
|
||||
}
|
||||
}
|
||||
return item
|
||||
}
|
||||
)
|
||||
return newLayouts
|
||||
})
|
||||
}, [height, breakpoint, count, setLayouts])
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div ref={ref} style={{ width: '100%', height: 'auto' }}>
|
||||
<ReactResizeDetector handleHeight onResize={onResize}>
|
||||
<div
|
||||
style={{
|
||||
padding: `${padding ? padding : 0}px`
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</ReactResizeDetector>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DynamicHeightItem
|
39
src/components/DynamicHeightItemMinimal.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import React, { useRef, useState, useEffect } from 'react'
|
||||
import ReactResizeDetector from 'react-resize-detector'
|
||||
import { Layouts, Layout } from 'react-grid-layout'
|
||||
|
||||
interface DynamicHeightItemProps {
|
||||
children: React.ReactNode
|
||||
layouts: Layouts
|
||||
setLayouts: (layouts: any) => void
|
||||
i: string
|
||||
breakpoint: keyof Layouts
|
||||
rows?: number
|
||||
count?: number
|
||||
type?: string
|
||||
padding?: number
|
||||
}
|
||||
|
||||
export const DynamicHeightItemMinimal: React.FC<DynamicHeightItemProps> = ({
|
||||
children,
|
||||
layouts,
|
||||
setLayouts,
|
||||
i,
|
||||
breakpoint,
|
||||
rows = 1,
|
||||
count,
|
||||
type,
|
||||
padding
|
||||
}) => {
|
||||
return (
|
||||
<div style={{ width: '100%', height: 'auto' }}>
|
||||
<div
|
||||
style={{
|
||||
padding: `${padding ? padding : 0}px`
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
330
src/components/FileElement.tsx
Normal file
@ -0,0 +1,330 @@
|
||||
import * as React from "react";
|
||||
import { styled, useTheme } from "@mui/material/styles";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { CircularProgress } from "@mui/material";
|
||||
import { MyContext } from "../wrappers/DownloadWrapper";
|
||||
import { RootState } from "../state/store";
|
||||
import { setNotification } from "../state/features/notificationsSlice";
|
||||
import { base64ToUint8Array } from "../utils/toBase64";
|
||||
|
||||
|
||||
const Widget = styled("div")(({ theme }) => ({
|
||||
padding: 8,
|
||||
borderRadius: 10,
|
||||
maxWidth: 350,
|
||||
position: "relative",
|
||||
zIndex: 1,
|
||||
backdropFilter: "blur(40px)",
|
||||
background: "skyblue",
|
||||
transition: "0.2s all",
|
||||
"&:hover": {
|
||||
opacity: 0.75,
|
||||
},
|
||||
}));
|
||||
|
||||
const CoverImage = styled("div")({
|
||||
width: 40,
|
||||
height: 40,
|
||||
objectFit: "cover",
|
||||
overflow: "hidden",
|
||||
flexShrink: 0,
|
||||
borderRadius: 8,
|
||||
backgroundColor: "rgba(0,0,0,0.08)",
|
||||
"& > img": {
|
||||
width: "100%",
|
||||
},
|
||||
});
|
||||
|
||||
interface IAudioElement {
|
||||
title: string;
|
||||
description?: string;
|
||||
author?: string;
|
||||
fileInfo?: any;
|
||||
postId?: string;
|
||||
user?: string;
|
||||
children?: React.ReactNode;
|
||||
mimeTypeSaved?: string;
|
||||
disable?: boolean;
|
||||
mode?: string;
|
||||
otherUser?: string;
|
||||
customStyles?: any;
|
||||
loadStyles?: any
|
||||
}
|
||||
|
||||
interface CustomWindow extends Window {
|
||||
showSaveFilePicker: any; // Replace 'any' with the appropriate type if you know it
|
||||
}
|
||||
|
||||
const customWindow = window as unknown as CustomWindow;
|
||||
|
||||
export default function FileElement({
|
||||
title,
|
||||
description,
|
||||
author,
|
||||
fileInfo,
|
||||
children,
|
||||
mimeTypeSaved,
|
||||
disable,
|
||||
customStyles,
|
||||
loadStyles = {}
|
||||
}: IAudioElement) {
|
||||
const { downloadVideo } = React.useContext(MyContext);
|
||||
const [startedDownload, setStartedDownload] = React.useState<boolean>(false)
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const [downloadLoader, setDownloadLoader] = React.useState<any>(false);
|
||||
const downloads = useSelector((state: RootState) => state.global?.downloads);
|
||||
const hasCommencedDownload = React.useRef(false);
|
||||
const dispatch = useDispatch();
|
||||
const reDownload = React.useRef<boolean>(false)
|
||||
const status = React.useRef<null | string>(null)
|
||||
|
||||
const isFetchingProperties = React.useRef<boolean>(false)
|
||||
const download = React.useMemo(() => {
|
||||
if (!downloads || !fileInfo?.identifier) return {};
|
||||
const findDownload = downloads[fileInfo?.identifier];
|
||||
|
||||
if (!findDownload) return {};
|
||||
return findDownload;
|
||||
}, [downloads, fileInfo]);
|
||||
|
||||
const resourceStatus = React.useMemo(() => {
|
||||
return download?.status || {};
|
||||
}, [download]);
|
||||
|
||||
const handlePlay = async () => {
|
||||
if (disable) return;
|
||||
hasCommencedDownload.current = true;
|
||||
setStartedDownload(true)
|
||||
if (
|
||||
resourceStatus?.status === "READY"
|
||||
) {
|
||||
if (downloadLoader) return;
|
||||
|
||||
setDownloadLoader(true);
|
||||
let filename = download?.properties?.filename
|
||||
let mimeType = download?.properties?.type
|
||||
|
||||
try {
|
||||
const { name, service, identifier } = fileInfo;
|
||||
|
||||
const res = await qortalRequest({
|
||||
action: "GET_QDN_RESOURCE_PROPERTIES",
|
||||
name: name,
|
||||
service: service,
|
||||
identifier: identifier,
|
||||
});
|
||||
filename = res?.filename || filename;
|
||||
mimeType = res?.mimeType || mimeType || mimeTypeSaved;
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
try {
|
||||
const { name, service, identifier } = fileInfo;
|
||||
|
||||
let resData = await qortalRequest({
|
||||
action: 'FETCH_QDN_RESOURCE',
|
||||
name: name,
|
||||
service: service,
|
||||
identifier: identifier,
|
||||
encoding: 'base64'
|
||||
})
|
||||
|
||||
let requestEncryptBody: any = {
|
||||
action: 'DECRYPT_DATA',
|
||||
encryptedData: resData }
|
||||
const resDecrypt = await qortalRequest(requestEncryptBody)
|
||||
|
||||
if (!resDecrypt) throw new Error('Unable to decrypt file')
|
||||
const decryptToUnit8Array = base64ToUint8Array(resDecrypt)
|
||||
let blob = null
|
||||
if (mimeType) {
|
||||
blob = new Blob([decryptToUnit8Array], {
|
||||
type: mimeType
|
||||
})
|
||||
} else {
|
||||
blob = new Blob([decryptToUnit8Array])
|
||||
}
|
||||
|
||||
if (!blob) throw new Error('Unable to build file into blob')
|
||||
await qortalRequest({
|
||||
action: 'SAVE_FILE',
|
||||
blob,
|
||||
filename:
|
||||
download?.properties?.originalFilename ||
|
||||
filename,
|
||||
mimeType
|
||||
})
|
||||
|
||||
//old
|
||||
|
||||
// const url = `/arbitrary/${service}/${name}/${identifier}`;
|
||||
// fetch(url)
|
||||
// .then(response => response.blob())
|
||||
// .then(async blob => {
|
||||
|
||||
// await qortalRequest({
|
||||
// action: "SAVE_FILE",
|
||||
// blob,
|
||||
// filename: filename,
|
||||
// mimeType,
|
||||
// });
|
||||
// })
|
||||
// .catch(error => {
|
||||
// console.error("Error fetching the video:", error);
|
||||
// });
|
||||
} catch (error: any) {
|
||||
let notificationObj: any = null;
|
||||
if (typeof error === "string") {
|
||||
notificationObj = {
|
||||
msg: error || "Failed to send message",
|
||||
alertType: "error",
|
||||
};
|
||||
} else if (typeof error?.error === "string") {
|
||||
notificationObj = {
|
||||
msg: error?.error || "Failed to send message",
|
||||
alertType: "error",
|
||||
};
|
||||
} else {
|
||||
notificationObj = {
|
||||
msg: error?.message || "Failed to send message",
|
||||
alertType: "error",
|
||||
};
|
||||
}
|
||||
if (!notificationObj) return;
|
||||
dispatch(setNotification(notificationObj));
|
||||
} finally {
|
||||
setDownloadLoader(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { name, service, identifier } = fileInfo;
|
||||
|
||||
setIsLoading(true);
|
||||
downloadVideo({
|
||||
name,
|
||||
service,
|
||||
identifier,
|
||||
properties: {
|
||||
...fileInfo,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const refetch = React.useCallback(async () => {
|
||||
if (!fileInfo) return
|
||||
try {
|
||||
const { name, service, identifier } = fileInfo;
|
||||
isFetchingProperties.current = true
|
||||
await qortalRequest({
|
||||
action: 'GET_QDN_RESOURCE_PROPERTIES',
|
||||
name,
|
||||
service,
|
||||
identifier
|
||||
})
|
||||
|
||||
} catch (error) {
|
||||
|
||||
} finally {
|
||||
isFetchingProperties.current = false
|
||||
}
|
||||
|
||||
}, [fileInfo])
|
||||
|
||||
const refetchInInterval = ()=> {
|
||||
try {
|
||||
const interval = setInterval(()=> {
|
||||
if(status?.current === 'DOWNLOADED'){
|
||||
refetch()
|
||||
}
|
||||
if(status?.current === 'READY'){
|
||||
clearInterval(interval);
|
||||
}
|
||||
|
||||
}, 7500)
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if(resourceStatus?.status){
|
||||
status.current = resourceStatus?.status
|
||||
}
|
||||
if (
|
||||
resourceStatus?.status === "READY" &&
|
||||
download?.url &&
|
||||
download?.properties?.filename &&
|
||||
hasCommencedDownload.current
|
||||
) {
|
||||
setIsLoading(false);
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: "Download completed. Click to save file",
|
||||
alertType: "info",
|
||||
})
|
||||
);
|
||||
} else if (
|
||||
resourceStatus?.status === 'DOWNLOADED' &&
|
||||
reDownload?.current === false
|
||||
) {
|
||||
refetchInInterval()
|
||||
reDownload.current = true
|
||||
}
|
||||
}, [resourceStatus, download]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
onClick={handlePlay}
|
||||
sx={{
|
||||
width: "100%",
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
cursor: "pointer",
|
||||
...(customStyles || {}),
|
||||
}}
|
||||
>
|
||||
{children && (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
position: "relative",
|
||||
gap: "7px",
|
||||
}}
|
||||
>
|
||||
{children}{" "}
|
||||
{((resourceStatus.status && resourceStatus?.status !== "READY") ||
|
||||
isLoading) && startedDownload ? (
|
||||
<>
|
||||
<CircularProgress color="secondary" size={14} />
|
||||
<Typography style={{
|
||||
...loadStyles
|
||||
}} variant="body2">{`${Math.round(
|
||||
resourceStatus?.percentLoaded || 0
|
||||
).toFixed(0)}% loaded`}</Typography>
|
||||
</>
|
||||
) : resourceStatus?.status === "READY" ? (
|
||||
<>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "14px",
|
||||
}}
|
||||
style={{
|
||||
...loadStyles
|
||||
}}
|
||||
>
|
||||
Click to save
|
||||
</Typography>
|
||||
{downloadLoader && (
|
||||
<CircularProgress color="secondary" size={14} />
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
230
src/components/common/AudioPanel.tsx
Normal file
@ -0,0 +1,230 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { styled, Box } from '@mui/system'
|
||||
import {
|
||||
Drawer,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Typography,
|
||||
ButtonBase,
|
||||
Button,
|
||||
Tooltip
|
||||
} from '@mui/material'
|
||||
import VideoCallIcon from '@mui/icons-material/VideoCall'
|
||||
import VideoModal from './VideoPublishModal'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { RootState } from '../../state/store'
|
||||
import { AudioModal } from './AudioPublishModal'
|
||||
import AudioFileIcon from '@mui/icons-material/AudioFile'
|
||||
interface VideoPanelProps {
|
||||
onSelect: (video: Video) => void
|
||||
height?: string
|
||||
width?: string
|
||||
}
|
||||
|
||||
interface VideoApiResponse {
|
||||
videos: Video[]
|
||||
}
|
||||
|
||||
const Panel = styled('div')`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding-bottom: 10px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
`
|
||||
|
||||
const PublishButton = styled(Button)`
|
||||
/* position: absolute;
|
||||
bottom: 20px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto; */
|
||||
max-width: 80%;
|
||||
`
|
||||
|
||||
export const AudioPanel: React.FC<VideoPanelProps> = ({
|
||||
onSelect,
|
||||
height,
|
||||
width
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [videos, setVideos] = useState<Video[]>([])
|
||||
const [isOpenVideoModal, setIsOpenVideoModal] = useState<boolean>(false)
|
||||
const { user } = useSelector((state: RootState) => state.auth)
|
||||
|
||||
const fetchVideos = React.useCallback(async (): Promise<Video[]> => {
|
||||
if (!user?.name) return []
|
||||
|
||||
let res
|
||||
try {
|
||||
res = await qortalRequest({
|
||||
action: 'LIST_QDN_RESOURCES',
|
||||
service: 'AUDIO',
|
||||
name: user.name,
|
||||
includeMetadata: true,
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
reverse: true
|
||||
})
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
|
||||
// Replace this URL with the actual API endpoint
|
||||
|
||||
return res
|
||||
}, [user])
|
||||
useEffect(() => {
|
||||
fetchVideos().then((fetchedVideos) => setVideos(fetchedVideos))
|
||||
}, [])
|
||||
|
||||
const handleToggle = () => {
|
||||
setIsOpen(!isOpen)
|
||||
}
|
||||
|
||||
const handleClick = (video: Video) => {
|
||||
onSelect(video)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex'
|
||||
}}
|
||||
>
|
||||
<Tooltip title="Add an audio file" arrow>
|
||||
<AudioFileIcon
|
||||
onClick={handleToggle}
|
||||
sx={{
|
||||
height: height || '30px',
|
||||
width: width || 'auto',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
></AudioFileIcon>
|
||||
</Tooltip>
|
||||
<Drawer
|
||||
anchor="right"
|
||||
open={isOpen}
|
||||
onClose={handleToggle}
|
||||
ModalProps={{
|
||||
keepMounted: true // Better performance on mobile
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiPaper-root': {
|
||||
width: '400px'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Panel>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
flex: '0 0'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h5"
|
||||
component="div"
|
||||
sx={{ flexGrow: 1, mt: 2, mb: 1 }}
|
||||
>
|
||||
Select Audio
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
component="div"
|
||||
sx={{ flexGrow: 1, mb: 2 }}
|
||||
>
|
||||
List of audios in QDN under your name
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<List
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: '1',
|
||||
overflow: 'auto'
|
||||
}}
|
||||
>
|
||||
{videos.map((video) => (
|
||||
<ListItem key={video.identifier}>
|
||||
<ButtonBase
|
||||
onClick={() => handleClick(video)}
|
||||
sx={{ width: '100%' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={video?.metadata?.title || ''}
|
||||
secondary={video?.metadata?.description || ''}
|
||||
/>
|
||||
</ButtonBase>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
flex: '0 0 50px'
|
||||
}}
|
||||
>
|
||||
<PublishButton
|
||||
variant="contained"
|
||||
onClick={() => setIsOpenVideoModal(true)}
|
||||
>
|
||||
Publish new audio file
|
||||
</PublishButton>
|
||||
</Box>
|
||||
</Panel>
|
||||
</Drawer>
|
||||
<AudioModal
|
||||
onClose={() => {
|
||||
setIsOpenVideoModal(false)
|
||||
}}
|
||||
open={isOpenVideoModal}
|
||||
onPublish={(value) => {
|
||||
fetchVideos().then((fetchedVideos) => setVideos(fetchedVideos))
|
||||
setIsOpenVideoModal(false)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Add this to your 'types.ts' file
|
||||
export interface Video {
|
||||
name: string
|
||||
service: string
|
||||
identifier: string
|
||||
metadata: {
|
||||
title: string
|
||||
description: string
|
||||
tags: string[]
|
||||
category: string
|
||||
categoryName: string
|
||||
}
|
||||
size: number
|
||||
created: number
|
||||
updated: number
|
||||
}
|
192
src/components/common/AudioPlayer.tsx
Normal file
@ -0,0 +1,192 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { Box, IconButton, Slider } from '@mui/material'
|
||||
import { CircularProgress, Typography } from '@mui/material'
|
||||
import AudioPlyr from 'philliplm-react-modern-audio-player'
|
||||
import LinearProgress from '@mui/material/LinearProgress'
|
||||
|
||||
import {
|
||||
PlayArrow,
|
||||
Pause,
|
||||
VolumeUp,
|
||||
Fullscreen,
|
||||
PictureInPicture
|
||||
} from '@mui/icons-material'
|
||||
import { styled } from '@mui/system'
|
||||
import {
|
||||
removeAudio,
|
||||
setShowingAudioPlayer
|
||||
} from '../../state/features/globalSlice'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { RootState } from '../../state/store'
|
||||
|
||||
const VideoContainer = styled(Box)`
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 20px 0px;
|
||||
z-index: 501;
|
||||
`
|
||||
|
||||
const VideoElement = styled('video')`
|
||||
width: 100%;
|
||||
height: auto;
|
||||
background: rgb(33, 33, 33);
|
||||
`
|
||||
|
||||
const ControlsContainer = styled(Box)`
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 8px;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
`
|
||||
|
||||
interface VideoPlayerProps {
|
||||
src?: string
|
||||
poster?: string
|
||||
name?: string
|
||||
identifier?: string
|
||||
service?: string
|
||||
autoplay?: boolean
|
||||
title?: string
|
||||
description?: string
|
||||
playlist?: IPlaylist[]
|
||||
currAudio: number | null
|
||||
}
|
||||
|
||||
export interface IPlaylist {
|
||||
name: string
|
||||
identifier: string
|
||||
service: string
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
interface CustomWindow extends Window {
|
||||
_qdnTheme: any // Replace 'any' with the appropriate type if you know it
|
||||
}
|
||||
const customWindow = window as unknown as CustomWindow
|
||||
const themeColor = customWindow?._qdnTheme
|
||||
|
||||
export const AudioPlayer: React.FC<VideoPlayerProps> = ({ currAudio }) => {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false)
|
||||
const { downloads, showingAudioPlayer } = useSelector(
|
||||
(state: RootState) => state.global
|
||||
)
|
||||
const dispatch = useDispatch()
|
||||
const downloadsLength: number = useMemo(
|
||||
() =>
|
||||
Object.keys(downloads)
|
||||
.map((item) => {
|
||||
return downloads[item]
|
||||
})
|
||||
.filter(
|
||||
(download: any) =>
|
||||
download?.service === 'AUDIO' &&
|
||||
download?.status?.status === 'READY' &&
|
||||
!!download.url
|
||||
).length,
|
||||
[downloads]
|
||||
)
|
||||
|
||||
const audioPlayList = useMemo(() => {
|
||||
const filterAudios = Object.keys(downloads)
|
||||
.map((item) => {
|
||||
return downloads[item]
|
||||
})
|
||||
.filter(
|
||||
(download: any) =>
|
||||
download?.service === 'AUDIO' &&
|
||||
download?.url &&
|
||||
download?.status?.status === 'READY'
|
||||
)
|
||||
return filterAudios.map((audio: any, index: number) => {
|
||||
return {
|
||||
name: audio?.blogPost?.audioTitle,
|
||||
src: audio?.url,
|
||||
id: index + 1,
|
||||
identifier: audio?.identifier,
|
||||
description: audio?.blogPost?.audioDescription || ''
|
||||
}
|
||||
})
|
||||
}, [downloadsLength])
|
||||
|
||||
const currAudioMemo: number | null = useMemo(() => {
|
||||
const findIndex = audioPlayList.findIndex(
|
||||
(item) => item?.identifier === currAudio
|
||||
)
|
||||
if (findIndex !== -1) {
|
||||
return findIndex
|
||||
}
|
||||
return null
|
||||
}, [audioPlayList, currAudio])
|
||||
|
||||
if (isLoading)
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
isolation: 'isolate',
|
||||
width: '100%',
|
||||
position: 'fixed',
|
||||
colorScheme: 'light',
|
||||
bottom: '0px',
|
||||
padding: '10px',
|
||||
height: '50px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-start'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '10px'
|
||||
}}
|
||||
>
|
||||
Loading playlist...
|
||||
</Typography>
|
||||
<LinearProgress
|
||||
sx={{
|
||||
width: '100%'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
|
||||
if (audioPlayList.length === 0 || !showingAudioPlayer) return null
|
||||
return (
|
||||
<VideoContainer>
|
||||
<AudioPlyr
|
||||
rootContainerProps={{
|
||||
defaultColorScheme: themeColor === 'dark' ? 'dark' : 'light',
|
||||
colorScheme: themeColor === 'dark' ? 'dark' : 'light'
|
||||
}}
|
||||
currentIndex={currAudioMemo}
|
||||
playList={audioPlayList}
|
||||
activeUI={{
|
||||
all: true
|
||||
}}
|
||||
placement={{
|
||||
player: 'bottom',
|
||||
|
||||
playList: 'top',
|
||||
volumeSlider: 'top'
|
||||
}}
|
||||
closeCallback={() => {
|
||||
dispatch(setShowingAudioPlayer(false))
|
||||
}}
|
||||
// rootContainerProps={{
|
||||
// colorScheme: theme,
|
||||
// width
|
||||
// }}
|
||||
/>
|
||||
</VideoContainer>
|
||||
)
|
||||
}
|
358
src/components/common/AudioPublishModal.tsx
Normal file
@ -0,0 +1,358 @@
|
||||
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 { usePublishAudio } from './PublishAudio'
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
interface SelectOption {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
async function addAudioCoverImage(
|
||||
base64Audio: string,
|
||||
coverImageBase64: string
|
||||
): Promise<string> {
|
||||
// Decode the base64 audio data
|
||||
const audioData: Uint8Array = new Uint8Array(
|
||||
atob(base64Audio)
|
||||
.split('')
|
||||
.map((char) => char.charCodeAt(0))
|
||||
)
|
||||
|
||||
const decoder: TextDecoder = new TextDecoder('utf-8')
|
||||
const decodedAudioData: string = decoder.decode(audioData)
|
||||
|
||||
// Create a Blob object from the decoded audio data
|
||||
const blob: Blob = new Blob([decodedAudioData], { type: 'audio/mpeg' })
|
||||
|
||||
// Create a new file name for the audio with cover image
|
||||
const fileName: string = 'audio-with-cover.mp3'
|
||||
|
||||
// Create a new FormData object to hold the file and metadata
|
||||
const formData: FormData = new FormData()
|
||||
formData.append('file', blob, fileName)
|
||||
|
||||
// Create a new image object from the base64 data
|
||||
const image: HTMLImageElement = new Image()
|
||||
image.src = `data:image/png;base64,${coverImageBase64}`
|
||||
|
||||
// Wait for the image to load before getting its dimensions
|
||||
await new Promise((resolve) => {
|
||||
image.onload = () => resolve(null)
|
||||
})
|
||||
|
||||
// Get the image dimensions
|
||||
const width: number = image.width
|
||||
const height: number = image.height
|
||||
|
||||
// Create a new metadata object with the image dimensions
|
||||
const metadata: any = {
|
||||
title: 'Audio with Cover',
|
||||
artist: 'Artist Name',
|
||||
album: 'Album Name',
|
||||
trackNumber: 1,
|
||||
image: {
|
||||
mime: 'image/png',
|
||||
type: 3,
|
||||
description: 'Cover Image',
|
||||
data: coverImageBase64,
|
||||
width: width,
|
||||
height: height
|
||||
}
|
||||
}
|
||||
|
||||
// Set the metadata on the file
|
||||
formData.set('metadata', JSON.stringify(metadata))
|
||||
|
||||
// Create a new URL object for the file
|
||||
const url: string = URL.createObjectURL(blob)
|
||||
|
||||
// Create a download link for the file
|
||||
const link: HTMLAnchorElement = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = fileName
|
||||
link.click()
|
||||
|
||||
// Read the downloaded file and return its contents as a base64 string
|
||||
const fileReader: FileReader = new FileReader()
|
||||
fileReader.readAsDataURL(blob)
|
||||
return await new Promise<string>((resolve, reject) => {
|
||||
fileReader.onload = () => {
|
||||
const base64: string | undefined = fileReader.result?.toString()
|
||||
if (base64 !== undefined) {
|
||||
resolve(base64)
|
||||
} else {
|
||||
reject(new Error('Failed to read downloaded file.'))
|
||||
}
|
||||
}
|
||||
fileReader.onerror = () => reject(fileReader.error)
|
||||
})
|
||||
}
|
||||
|
||||
export const AudioModal: React.FC<VideoModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onPublish
|
||||
}) => {
|
||||
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 { publishAudio } = usePublishAudio()
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
accept: {
|
||||
'audio/*': []
|
||||
},
|
||||
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]
|
||||
|
||||
const res = await publishAudio({
|
||||
title,
|
||||
description,
|
||||
base64: base64String,
|
||||
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>
|
||||
<Typography variant="h6" component="h2" gutterBottom>
|
||||
Upload Audio
|
||||
</Typography>
|
||||
<Box
|
||||
{...getRootProps()}
|
||||
sx={{
|
||||
border: '1px dashed gray',
|
||||
padding: 2,
|
||||
textAlign: 'center',
|
||||
marginBottom: 2
|
||||
}}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<Typography>
|
||||
{file
|
||||
? file.name
|
||||
: 'Drag and drop an audio file here or click to select a file'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<TextField
|
||||
label="Audio Title"
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={title}
|
||||
onChange={handleTitleChange}
|
||||
inputProps={{ maxLength: 40 }}
|
||||
sx={{ marginBottom: 2 }}
|
||||
/>
|
||||
<TextField
|
||||
label="Audio 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,28 @@
|
||||
import { styled } from '@mui/system';
|
||||
import {
|
||||
Box,
|
||||
Modal,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
|
||||
export const StyledModal = styled(Modal)(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}))
|
||||
|
||||
export const ModalContent = styled(Box)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
padding: theme.spacing(4),
|
||||
borderRadius: theme.spacing(1),
|
||||
width: '40%',
|
||||
'&:focus': {
|
||||
outline: 'none'
|
||||
}
|
||||
}))
|
||||
|
||||
export const ModalText = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Raleway",
|
||||
fontSize: "25px",
|
||||
color: theme.palette.text.primary,
|
||||
}));
|
100
src/components/common/BlockedNamesModal/BlockedNamesModal.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Modal,
|
||||
Typography,
|
||||
SelectChangeEvent,
|
||||
ListItem,
|
||||
List,
|
||||
useTheme
|
||||
} from '@mui/material'
|
||||
import {
|
||||
StyledModal,
|
||||
ModalContent,
|
||||
ModalText
|
||||
} from './BlockedNamesModal-styles'
|
||||
|
||||
interface PostModalProps {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export const BlockedNamesModal: React.FC<PostModalProps> = ({
|
||||
open,
|
||||
onClose
|
||||
}) => {
|
||||
const [blockedNames, setBlockedNames] = useState<string[]>([])
|
||||
const theme = useTheme()
|
||||
const getBlockedNames = React.useCallback(async () => {
|
||||
try {
|
||||
const listName = `blockedNames`
|
||||
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',
|
||||
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: 'Arial'
|
||||
}}
|
||||
onClick={() => removeFromBlockList(name)}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
<Button variant="contained" color="primary" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalContent>
|
||||
</StyledModal>
|
||||
)
|
||||
}
|
118
src/components/common/ChipInputComponent/ChipInputComponent.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import React, { useState } from 'react';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { setNotification } from '../../../state/features/notificationsSlice';
|
||||
import { Input } from '@mui/material';
|
||||
|
||||
export interface NameChip {
|
||||
name: string;
|
||||
publicKey: string;
|
||||
address: string;
|
||||
}
|
||||
interface ChipInputComponent {
|
||||
chips: NameChip[];
|
||||
setChips: (val: NameChip[])=> void;
|
||||
}
|
||||
|
||||
export const ChipInputComponent = ({chips, setChips}: ChipInputComponent) => {
|
||||
const [inputValue, setInputValue] = useState<string>('');
|
||||
const dispatch = useDispatch()
|
||||
// Add chip on enter or onBlur
|
||||
const handleAddChip = async () => {
|
||||
try {
|
||||
if(!inputValue) return
|
||||
const recipientName = inputValue
|
||||
const resName = await qortalRequest({
|
||||
action: 'GET_NAME_DATA',
|
||||
name: recipientName
|
||||
})
|
||||
if (!resName?.owner) throw new Error("Name cannot be found")
|
||||
|
||||
const recipientAddress = resName.owner
|
||||
const resAddress = await qortalRequest({
|
||||
action: 'GET_ACCOUNT_DATA',
|
||||
address: recipientAddress
|
||||
})
|
||||
if (!resAddress?.publicKey) throw new Error("Cannot retrieve public key of name")
|
||||
const recipientPublicKey = resAddress.publicKey
|
||||
if (inputValue && !chips.find((item)=> item?.name === inputValue)) {
|
||||
setChips([...chips, {
|
||||
name: inputValue,
|
||||
publicKey: recipientPublicKey,
|
||||
address: recipientAddress
|
||||
}]);
|
||||
setInputValue('');
|
||||
}
|
||||
} catch (error:any) {
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: error?.message,
|
||||
alertType: 'error'
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// Remove chip
|
||||
const handleDeleteChip = (chipToDelete: string) => () => {
|
||||
setChips(chips.filter(chip => chip.name !== chipToDelete));
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{chips.map((chip, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={chip.name}
|
||||
onDelete={handleDeleteChip(chip.name)}
|
||||
sx={{
|
||||
color: 'rgba(84, 84, 84, 1)',
|
||||
'& .MuiChip-deleteIcon': {
|
||||
color: 'black' , // Style the delete icon,
|
||||
"&:hover": {
|
||||
color: 'rgba(84, 84, 84, 1)'
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{/* <TextField
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleAddChip()}
|
||||
placeholder="Type and press enter..."
|
||||
/> */}
|
||||
<Input
|
||||
id="standard-adornment-name"
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
setInputValue(e.target.value)
|
||||
}}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleAddChip()}
|
||||
disableUnderline
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
placeholder="Type and press enter..."
|
||||
sx={{
|
||||
width: '100%',
|
||||
color: 'var(--new-message-text)',
|
||||
'& .MuiInput-input::placeholder': {
|
||||
color: 'rgba(84, 84, 84, 0.70) !important',
|
||||
fontSize: '20px',
|
||||
fontStyle: 'normal',
|
||||
fontWeight: 400,
|
||||
lineHeight: '120%', // 24px
|
||||
letterSpacing: '0.15px',
|
||||
opacity: 1
|
||||
},
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
},
|
||||
// Add any additional styles for the input here
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
279
src/components/common/Comments/Comment.tsx
Normal file
@ -0,0 +1,279 @@
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Typography,
|
||||
useTheme
|
||||
} from '@mui/material'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { CommentEditor } from './CommentEditor'
|
||||
import { CardContentContainerComment } from '../../../pages/BlogList/PostPreview-styles'
|
||||
import { StyledCardHeaderComment } from '../../../pages/BlogList/PostPreview-styles'
|
||||
import { StyledCardColComment } from '../../../pages/BlogList/PostPreview-styles'
|
||||
import { AuthorTextComment } from '../../../pages/BlogList/PostPreview-styles'
|
||||
import { StyledCardContentComment } from '../../../pages/BlogList/PostPreview-styles'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { RootState } from '../../../state/store'
|
||||
import Portal from '../Portal'
|
||||
import { Tipping } from '../Tipping/Tipping'
|
||||
interface CommentProps {
|
||||
comment: any
|
||||
postId: string
|
||||
onSubmit: (obj?: any, isEdit?: boolean) => void
|
||||
}
|
||||
export const Comment = ({ comment, postId, onSubmit }: CommentProps) => {
|
||||
const [isReplying, setIsReplying] = useState<boolean>(false)
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false)
|
||||
const { user } = useSelector((state: RootState) => state.auth)
|
||||
const [currentEdit, setCurrentEdit] = useState<any>(null)
|
||||
const handleSubmit = useCallback((comment: any, isEdit?: boolean) => {
|
||||
onSubmit(comment, isEdit)
|
||||
setCurrentEdit(null)
|
||||
setIsReplying(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
{currentEdit && (
|
||||
<Portal>
|
||||
<Dialog
|
||||
open={!!currentEdit}
|
||||
onClose={() => setCurrentEdit(null)}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title"></DialogTitle>
|
||||
<DialogContent>
|
||||
<Box
|
||||
sx={{
|
||||
width: '300px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<CommentEditor
|
||||
onSubmit={(obj) => handleSubmit(obj, true)}
|
||||
postId={postId}
|
||||
isEdit
|
||||
commentId={currentEdit?.identifier}
|
||||
commentMessage={currentEdit?.message}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" onClick={() => setCurrentEdit(null)}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
<CommentCard
|
||||
name={comment?.name}
|
||||
message={comment?.message}
|
||||
replies={comment?.replies || []}
|
||||
setCurrentEdit={setCurrentEdit}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '5px',
|
||||
marginTop: '20px',
|
||||
justifyContent: 'flex-end'
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={() => setIsReplying(true)}
|
||||
>
|
||||
reply
|
||||
</Button>
|
||||
{user?.name === comment?.name && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={() => setCurrentEdit(comment)}
|
||||
>
|
||||
edit
|
||||
</Button>
|
||||
)}
|
||||
{isReplying && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
setIsReplying(false)
|
||||
setIsEditing(false)
|
||||
}}
|
||||
>
|
||||
close
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
</CommentCard>
|
||||
{/* <Typography variant="body1"> {comment?.message}</Typography> */}
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
{isReplying && (
|
||||
<CommentEditor
|
||||
onSubmit={handleSubmit}
|
||||
postId={postId}
|
||||
isReply
|
||||
commentId={comment.identifier}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const CommentCard = ({
|
||||
message,
|
||||
created,
|
||||
name,
|
||||
replies,
|
||||
children,
|
||||
setCurrentEdit
|
||||
}: any) => {
|
||||
const [avatarUrl, setAvatarUrl] = React.useState<string>('')
|
||||
const { user } = useSelector((state: RootState) => state.auth)
|
||||
|
||||
const theme = useTheme()
|
||||
|
||||
const getAvatar = React.useCallback(async (author: string) => {
|
||||
try {
|
||||
let url = await qortalRequest({
|
||||
action: 'GET_QDN_RESOURCE_URL',
|
||||
name: author,
|
||||
service: 'THUMBNAIL',
|
||||
identifier: 'qortal_avatar'
|
||||
})
|
||||
|
||||
setAvatarUrl(url)
|
||||
} catch (error) {}
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
getAvatar(name)
|
||||
}, [name])
|
||||
return (
|
||||
<CardContentContainerComment>
|
||||
<StyledCardHeaderComment
|
||||
sx={{
|
||||
'& .MuiCardHeader-content': {
|
||||
overflow: 'hidden'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Avatar src={avatarUrl} alt={`${name}'s avatar`} />
|
||||
</Box>
|
||||
<StyledCardColComment>
|
||||
<AuthorTextComment
|
||||
color={
|
||||
theme.palette.mode === 'light'
|
||||
? theme.palette.text.secondary
|
||||
: '#d6e8ff'
|
||||
}
|
||||
>
|
||||
{name}
|
||||
</AuthorTextComment>
|
||||
</StyledCardColComment>
|
||||
{name && (
|
||||
<Tipping
|
||||
name={name}
|
||||
onSubmit={() => {
|
||||
// setNameTip('')
|
||||
}}
|
||||
onClose={() => {
|
||||
// setNameTip('')
|
||||
}}
|
||||
onlyIcon={true}
|
||||
/>
|
||||
)}
|
||||
</StyledCardHeaderComment>
|
||||
<StyledCardContentComment>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={theme.palette.text.primary}
|
||||
sx={{
|
||||
fontSize: '16px'
|
||||
}}
|
||||
>
|
||||
{message}
|
||||
</Typography>
|
||||
</StyledCardContentComment>
|
||||
<Box
|
||||
sx={{
|
||||
paddingLeft: '15px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}
|
||||
>
|
||||
{replies?.map((reply: any) => {
|
||||
return (
|
||||
<Box
|
||||
key={reply?.identifier}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
border: '1px solid grey',
|
||||
borderRadius: '10px',
|
||||
marginTop: '8px'
|
||||
}}
|
||||
>
|
||||
<CommentCard
|
||||
name={reply?.name}
|
||||
message={reply?.message}
|
||||
setCurrentEdit={setCurrentEdit}
|
||||
>
|
||||
{user?.name === reply?.name && (
|
||||
<Button
|
||||
size="small"
|
||||
variant="contained"
|
||||
onClick={() => setCurrentEdit(reply)}
|
||||
sx={{
|
||||
width: '30px',
|
||||
alignSelf: 'flex-end',
|
||||
background: theme.palette.primary.light
|
||||
}}
|
||||
>
|
||||
edit
|
||||
</Button>
|
||||
)}
|
||||
</CommentCard>
|
||||
{/* <Typography variant="body2"> {reply?.message}</Typography> */}
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
{children}
|
||||
</CardContentContainerComment>
|
||||
)
|
||||
}
|
172
src/components/common/Comments/CommentEditor.tsx
Normal file
@ -0,0 +1,172 @@
|
||||
import { Box, Button, TextField } from '@mui/material'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { RootState } from '../../../state/store'
|
||||
import ShortUniqueId from 'short-unique-id'
|
||||
import { setNotification } from '../../../state/features/notificationsSlice'
|
||||
import { toBase64 } from '../../../utils/toBase64'
|
||||
const uid = new ShortUniqueId()
|
||||
interface CommentEditorProps {
|
||||
postId: string
|
||||
onSubmit: (obj: any) => void
|
||||
isReply?: boolean
|
||||
commentId?: string
|
||||
isEdit?: boolean
|
||||
commentMessage?: string
|
||||
}
|
||||
|
||||
function utf8ToBase64(inputString: string): string {
|
||||
// Encode the string as UTF-8
|
||||
const utf8String = encodeURIComponent(inputString).replace(
|
||||
/%([0-9A-F]{2})/g,
|
||||
(match, p1) => String.fromCharCode(Number('0x' + p1))
|
||||
)
|
||||
|
||||
// Convert the UTF-8 encoded string to base64
|
||||
const base64String = btoa(utf8String)
|
||||
return base64String
|
||||
}
|
||||
|
||||
export const CommentEditor = ({
|
||||
onSubmit,
|
||||
postId,
|
||||
isReply,
|
||||
commentId,
|
||||
isEdit,
|
||||
commentMessage
|
||||
}: CommentEditorProps) => {
|
||||
const [value, setValue] = useState<string>('')
|
||||
const dispatch = useDispatch()
|
||||
const { user } = useSelector((state: RootState) => state.auth)
|
||||
|
||||
useEffect(() => {
|
||||
if (isEdit && commentMessage) {
|
||||
setValue(commentMessage)
|
||||
}
|
||||
}, [isEdit, commentMessage])
|
||||
const publishComment = async (identifier: string) => {
|
||||
let address
|
||||
let name
|
||||
let errorMsg = ''
|
||||
|
||||
address = user?.address
|
||||
name = user?.name || ''
|
||||
|
||||
if (!address) {
|
||||
errorMsg = "Cannot post: your address isn't available"
|
||||
}
|
||||
if (!name) {
|
||||
errorMsg = 'Cannot post without a name'
|
||||
}
|
||||
|
||||
if (value.length > 200) {
|
||||
errorMsg = 'Comment needs to be under 200 characters'
|
||||
}
|
||||
|
||||
if (errorMsg) {
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: errorMsg,
|
||||
alertType: 'error'
|
||||
})
|
||||
)
|
||||
throw new Error(errorMsg)
|
||||
}
|
||||
|
||||
try {
|
||||
const base64 = utf8ToBase64(value)
|
||||
const resourceResponse = await qortalRequest({
|
||||
action: 'PUBLISH_QDN_RESOURCE',
|
||||
name: name,
|
||||
service: 'BLOG_COMMENT',
|
||||
data64: base64,
|
||||
identifier: identifier
|
||||
})
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: 'Comment successfully published',
|
||||
alertType: 'success'
|
||||
})
|
||||
)
|
||||
return resourceResponse
|
||||
} catch (error: any) {
|
||||
let notificationObj = null
|
||||
if (typeof error === 'string') {
|
||||
notificationObj = {
|
||||
msg: error || 'Failed to publish comment',
|
||||
alertType: 'error'
|
||||
}
|
||||
} else if (typeof error?.error === 'string') {
|
||||
notificationObj = {
|
||||
msg: error?.error || 'Failed to publish comment',
|
||||
alertType: 'error'
|
||||
}
|
||||
} else {
|
||||
notificationObj = {
|
||||
msg: error?.message || 'Failed to publish comment',
|
||||
alertType: 'error'
|
||||
}
|
||||
}
|
||||
if (!notificationObj) throw new Error('Failed to publish comment')
|
||||
|
||||
dispatch(setNotification(notificationObj))
|
||||
throw new Error('Failed to publish comment')
|
||||
}
|
||||
}
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const id = uid()
|
||||
|
||||
let identifier = `qcomment_v1_qblog_${postId.slice(-12)}_${id}`
|
||||
if (isReply && commentId) {
|
||||
identifier = `qcomment_v1_qblog_${postId.slice(
|
||||
-12
|
||||
)}_reply_${commentId.slice(-6)}_${id}`
|
||||
}
|
||||
if (isEdit && commentId) {
|
||||
identifier = commentId
|
||||
}
|
||||
await publishComment(identifier)
|
||||
onSubmit({
|
||||
created: Date.now(),
|
||||
identifier,
|
||||
message: value,
|
||||
service: 'BLOG_COMMENT',
|
||||
name: user?.name
|
||||
})
|
||||
setValue('')
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginTop: '15px',
|
||||
width: '90%'
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
id="standard-multiline-flexible"
|
||||
label="Your comment"
|
||||
multiline
|
||||
maxRows={4}
|
||||
variant="filled"
|
||||
value={value}
|
||||
inputProps={{
|
||||
maxLength: 200,
|
||||
style: {
|
||||
fontSize: '16px'
|
||||
}
|
||||
}}
|
||||
InputLabelProps={{ style: { fontSize: '18px' } }}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
/>
|
||||
|
||||
<Button variant="contained" onClick={handleSubmit}>
|
||||
{isReply ? 'Submit reply' : isEdit ? 'Edit' : 'Submit comment'}
|
||||
</Button>
|
||||
</Box>
|
||||
)
|
||||
}
|
307
src/components/common/Comments/CommentSection.tsx
Normal file
@ -0,0 +1,307 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { CommentEditor } from './CommentEditor'
|
||||
import { Comment } from './Comment'
|
||||
import { Box, Button, Drawer, Typography, useTheme } from '@mui/material'
|
||||
import { styled } from '@mui/system'
|
||||
import CloseIcon from '@mui/icons-material/Close'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { RootState } from '../../../state/store'
|
||||
import CommentIcon from '@mui/icons-material/Comment'
|
||||
interface CommentSectionProps {
|
||||
postId: string
|
||||
}
|
||||
|
||||
const Panel = styled('div')`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding-bottom: 10px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
`
|
||||
export const CommentSection = ({ postId }: CommentSectionProps) => {
|
||||
const [listComments, setListComments] = useState<any[]>([])
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false)
|
||||
const { user } = useSelector((state: RootState) => state.auth)
|
||||
const [newMessages, setNewMessages] = useState(0)
|
||||
const theme = useTheme()
|
||||
const onSubmit = (obj?: any, isEdit?: boolean) => {
|
||||
if (isEdit) {
|
||||
setListComments((prev: any[]) => {
|
||||
const findCommentIndex = prev.findIndex(
|
||||
(item) => item?.identifier === obj?.identifier
|
||||
)
|
||||
if (findCommentIndex === -1) return prev
|
||||
|
||||
const newArray = [...prev]
|
||||
newArray[findCommentIndex] = obj
|
||||
return newArray
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
setListComments((prev) => [
|
||||
...prev,
|
||||
{
|
||||
...obj
|
||||
}
|
||||
])
|
||||
}
|
||||
const getComments = useCallback(
|
||||
async (isNewMessages?: boolean, numberOfComments?: number) => {
|
||||
let offset: number = 0
|
||||
if (isNewMessages && numberOfComments) {
|
||||
offset = numberOfComments
|
||||
}
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=qcomment_v1_qblog_${postId.slice(
|
||||
-12
|
||||
)}&limit=20&includemetadata=true&offset=${offset}&reverse=false&excludeblocked=true`
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
const responseData = await response.json()
|
||||
let comments: any[] = []
|
||||
for (const comment of responseData) {
|
||||
if (comment.identifier && comment.name) {
|
||||
const url = `/arbitrary/BLOG_COMMENT/${comment.name}/${comment.identifier}`
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
const responseData2 = await response.text()
|
||||
if (responseData) {
|
||||
comments.push({
|
||||
message: responseData2,
|
||||
...comment
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isNewMessages) {
|
||||
setListComments((prev) => [...prev, ...comments])
|
||||
setNewMessages(0)
|
||||
} else {
|
||||
setListComments(comments)
|
||||
}
|
||||
|
||||
try {
|
||||
} catch (error) {}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
getComments()
|
||||
}, [getComments])
|
||||
|
||||
const structuredCommentList = useMemo(() => {
|
||||
return listComments.reduce((acc, curr, index, array) => {
|
||||
if (curr?.identifier?.includes('_reply_')) {
|
||||
return acc
|
||||
}
|
||||
acc.push({
|
||||
...curr,
|
||||
replies: array.filter((comment) =>
|
||||
comment.identifier.includes(`_reply_${curr.identifier.slice(-6)}`)
|
||||
)
|
||||
})
|
||||
return acc
|
||||
}, [])
|
||||
}, [listComments])
|
||||
|
||||
const interval = useRef<any>(null)
|
||||
|
||||
const checkNewComments = useCallback(async () => {
|
||||
try {
|
||||
const offset = listComments.length
|
||||
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=qcomment_v1_qblog_${postId.slice(
|
||||
-12
|
||||
)}&limit=20&includemetadata=true&offset=${offset}&reverse=false&excludeblocked=true`
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
const responseData = await response.json()
|
||||
setNewMessages(responseData.length)
|
||||
} catch (error) {}
|
||||
}, [listComments, postId])
|
||||
|
||||
const checkNewMessagesFunc = useCallback(() => {
|
||||
let isCalling = false
|
||||
interval.current = setInterval(async () => {
|
||||
if (isCalling) return
|
||||
isCalling = true
|
||||
const res = await checkNewComments()
|
||||
isCalling = false
|
||||
}, 15000)
|
||||
}, [checkNewComments])
|
||||
|
||||
useEffect(() => {
|
||||
checkNewMessagesFunc()
|
||||
|
||||
return () => {
|
||||
if (interval?.current) {
|
||||
clearInterval(interval.current)
|
||||
}
|
||||
}
|
||||
}, [checkNewMessagesFunc])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
<CommentIcon
|
||||
sx={{
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => setIsOpen((prev) => !prev)}
|
||||
>
|
||||
Comments
|
||||
</CommentIcon>
|
||||
{listComments?.length > 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: '12px',
|
||||
background: theme.palette.mode === 'dark' ? 'white' : 'black',
|
||||
color: theme.palette.mode === 'dark' ? 'black' : 'white',
|
||||
borderRadius: '50%',
|
||||
position: 'absolute',
|
||||
top: '-15px',
|
||||
right: '-15px',
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
{listComments.length < 10 ? listComments.length : '9+'}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Drawer
|
||||
variant="persistent"
|
||||
hideBackdrop={true}
|
||||
anchor="right"
|
||||
open={isOpen}
|
||||
onClose={() => {}}
|
||||
ModalProps={{
|
||||
keepMounted: true // Better performance on mobile
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiPaper-root': {
|
||||
width: '400px'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Panel>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
flex: '0 0',
|
||||
padding: '10px',
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex'
|
||||
}}
|
||||
>
|
||||
{newMessages > 0 && (
|
||||
<Button
|
||||
onClick={() => getComments(true, listComments.length)}
|
||||
variant="contained"
|
||||
size="small"
|
||||
>
|
||||
Load {newMessages} new{' '}
|
||||
{newMessages > 1 ? 'messages' : 'message'}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
<CloseIcon
|
||||
sx={{
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: '1',
|
||||
overflow: 'auto'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
margin: '25px 0px 50px 0px',
|
||||
maxWidth: '400px',
|
||||
width: '100%',
|
||||
gap: '10px',
|
||||
padding: '0px 5px'
|
||||
}}
|
||||
>
|
||||
{structuredCommentList.map((comment: any) => {
|
||||
return (
|
||||
<Comment
|
||||
key={comment?.identifier}
|
||||
comment={comment}
|
||||
onSubmit={onSubmit}
|
||||
postId={postId}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
flex: '0 0 100px'
|
||||
}}
|
||||
>
|
||||
<CommentEditor onSubmit={onSubmit} postId={postId} />
|
||||
</Box>
|
||||
</Panel>
|
||||
</Drawer>
|
||||
</>
|
||||
)
|
||||
}
|
56
src/components/common/ConfirmationModal.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Button
|
||||
} from '@mui/material'
|
||||
|
||||
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"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">{title}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
{message}
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" onClick={handleCancel} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleConfirm}
|
||||
color="primary"
|
||||
autoFocus
|
||||
>
|
||||
Proceed
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConfirmationModal
|
16
src/components/common/CustomIcon.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon'
|
||||
import { styled } from '@mui/system'
|
||||
|
||||
const CustomSvgIcon: React.FC<any> = styled(SvgIcon)(({ theme }) => ({
|
||||
cursor: 'pointer',
|
||||
color: '#5f6368',
|
||||
transition: 'all 0.2s',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.1)'
|
||||
}
|
||||
})) as unknown as React.FC<any>
|
||||
|
||||
export const CustomIcon: React.FC<any> = (props) => {
|
||||
return <CustomSvgIcon {...props} />
|
||||
}
|
289
src/components/common/DownloadTaskManager.tsx
Normal file
@ -0,0 +1,289 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Box,
|
||||
LinearProgress,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
Typography,
|
||||
useTheme
|
||||
} from '@mui/material'
|
||||
import { Movie, ArrowDropDown } from '@mui/icons-material'
|
||||
import { SxProps } from '@mui/system'
|
||||
import { Theme } from '@mui/material/styles'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { RootState } from '../../state/store'
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
||||
import { removePrefix } from '../../utils/blogIdformats'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import AudiotrackIcon from '@mui/icons-material/Audiotrack'
|
||||
import {
|
||||
setCurrAudio,
|
||||
setShowingAudioPlayer
|
||||
} from '../../state/features/globalSlice'
|
||||
import { MAIL_ATTACHMENT_SERVICE_TYPE } from '../../constants/mail'
|
||||
|
||||
type DownloadItem = {
|
||||
id: string
|
||||
name: string
|
||||
progress: number
|
||||
}
|
||||
|
||||
export const DownloadTaskManager: React.FC = () => {
|
||||
const { downloads } = useSelector((state: RootState) => state.global)
|
||||
const dispatch = useDispatch()
|
||||
const location = useLocation()
|
||||
const isMailRoute = location.pathname === '/mail'
|
||||
const theme = useTheme()
|
||||
const [visible, setVisible] = useState(false)
|
||||
const [hidden, setHidden] = useState(true)
|
||||
const navigate = useNavigate()
|
||||
const containerStyles: SxProps<Theme> = {
|
||||
position: 'fixed',
|
||||
top: '50px',
|
||||
right: 0,
|
||||
zIndex: 1000,
|
||||
maxHeight: '80%',
|
||||
overflowY: 'auto',
|
||||
backgroundColor: 'background.paper',
|
||||
boxShadow: 2,
|
||||
display: 'block'
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Simulate downloads for demo purposes
|
||||
|
||||
if (visible) {
|
||||
setTimeout(() => {
|
||||
setHidden(true)
|
||||
setVisible(false)
|
||||
}, 3000)
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
const toggleVisibility = () => {
|
||||
setVisible(true)
|
||||
setHidden(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(downloads).length === 0) return
|
||||
setVisible(true)
|
||||
setHidden(false)
|
||||
}, [downloads])
|
||||
|
||||
if (isMailRoute) return null
|
||||
if (
|
||||
!downloads ||
|
||||
Object.keys(downloads).filter(
|
||||
(item) => downloads[item].service !== MAIL_ATTACHMENT_SERVICE_TYPE
|
||||
).length === 0
|
||||
)
|
||||
return null
|
||||
|
||||
return (
|
||||
<Box sx={{ position: 'fixed', top: '50px', right: '5px', zIndex: 1000 }}>
|
||||
<Accordion
|
||||
sx={{
|
||||
width: '200px',
|
||||
backgroundColor: theme.palette.primary.main
|
||||
}}
|
||||
>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls="panel1a-content"
|
||||
id="panel1a-header"
|
||||
sx={{
|
||||
minHeight: 'unset',
|
||||
height: '36px',
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
'&.MuiAccordionSummary-content': {
|
||||
padding: 0,
|
||||
margin: 0
|
||||
},
|
||||
'&.Mui-expanded': {
|
||||
minHeight: 'unset',
|
||||
height: '36px'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: 'Arial',
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
Downloads
|
||||
</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails
|
||||
sx={{
|
||||
padding: '5px'
|
||||
}}
|
||||
>
|
||||
<List
|
||||
sx={{
|
||||
maxHeight: '50vh',
|
||||
overflow: 'auto'
|
||||
}}
|
||||
>
|
||||
{Object.keys(downloads)
|
||||
.filter(
|
||||
(item) =>
|
||||
downloads[item].service !== MAIL_ATTACHMENT_SERVICE_TYPE
|
||||
)
|
||||
.map((download: any) => {
|
||||
const downloadObj = downloads[download]
|
||||
const progress = downloads[download]?.status?.percentLoaded || 0
|
||||
const status = downloads[download]?.status?.status
|
||||
const service = downloads[download]?.service
|
||||
return (
|
||||
<ListItem
|
||||
key={downloadObj?.identifier}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
justifyContent: 'center',
|
||||
background: theme.palette.primary.main,
|
||||
color: theme.palette.text.primary,
|
||||
cursor: 'pointer',
|
||||
padding: '2px'
|
||||
}}
|
||||
onClick={() => {
|
||||
if (service === 'AUDIO' && downloadObj?.identifier) {
|
||||
dispatch(setCurrAudio(downloadObj?.identifier))
|
||||
dispatch(setShowingAudioPlayer(true))
|
||||
return
|
||||
}
|
||||
|
||||
const str = downloadObj?.blogPost?.postId
|
||||
if (!str) return
|
||||
const arr = str.split('-post-')
|
||||
const str1 = arr[0]
|
||||
const str2 = arr[1]
|
||||
const blogId = removePrefix(str1)
|
||||
navigate(
|
||||
`/${downloadObj?.blogPost.user}/${blogId}/${str2}`
|
||||
)
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
{service === 'AUDIO' && (
|
||||
<AudiotrackIcon
|
||||
sx={{ color: theme.palette.text.primary }}
|
||||
/>
|
||||
)}
|
||||
{service === 'VIDEO' && (
|
||||
<Movie sx={{ color: theme.palette.text.primary }} />
|
||||
)}
|
||||
</ListItemIcon>
|
||||
|
||||
<Box
|
||||
sx={{ width: '100px', marginLeft: 1, marginRight: 1 }}
|
||||
>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={progress}
|
||||
sx={{
|
||||
borderRadius: '5px',
|
||||
color: theme.palette.secondary.main
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: 'Arial',
|
||||
color: theme.palette.text.primary
|
||||
}}
|
||||
variant="caption"
|
||||
>
|
||||
{`${progress?.toFixed(0)}%`}{' '}
|
||||
{status && status === 'REFETCHING' && '- refetching'}
|
||||
{status && status === 'DOWNLOADED' && '- building'}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '10px',
|
||||
width: '100%',
|
||||
textAlign: 'end',
|
||||
fontFamily: 'Arial',
|
||||
color: theme.palette.text.primary
|
||||
}}
|
||||
>
|
||||
{downloadObj?.identifier}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
|
||||
{/* <IconButton onClick={() => {}} aria-label="toggle download manager">
|
||||
<ArrowDropDown />
|
||||
</IconButton> */}
|
||||
{/* <Box sx={containerStyles}>
|
||||
<List
|
||||
sx={{
|
||||
width: '200px'
|
||||
}}
|
||||
>
|
||||
{Object.keys(downloads).map((download: any) => {
|
||||
const downloadObj = downloads[download]
|
||||
const progress = downloads[download]?.status?.percentLoaded || 0
|
||||
return (
|
||||
<ListItem
|
||||
key={downloadObj?.identifier}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Movie />
|
||||
</ListItemIcon>
|
||||
|
||||
<Box sx={{ width: '100px', marginLeft: 1 }}>
|
||||
<LinearProgress variant="determinate" value={progress} />
|
||||
</Box>
|
||||
<Typography variant="caption">{`${progress}%`}</Typography>
|
||||
</Box>
|
||||
|
||||
<ListItemText
|
||||
primary={downloadObj?.identifier}
|
||||
sx={{
|
||||
fontSize: '14px',
|
||||
width: '100%',
|
||||
textAlign: 'end'
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
</Box> */}
|
||||
</Box>
|
||||
)
|
||||
}
|
55
src/components/common/DraggableResizableGrid.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
// DraggableResizableGrid.tsx
|
||||
import React from 'react'
|
||||
import { DndProvider } from 'react-dnd'
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend'
|
||||
import GridLayout, { Layout } from 'react-grid-layout'
|
||||
|
||||
import './DraggableResizableGrid.css' // Add your custom CSS for the grid layout
|
||||
|
||||
interface GridItem {
|
||||
id: string
|
||||
content: React.ReactNode
|
||||
}
|
||||
|
||||
interface DraggableResizableGridProps {
|
||||
items: GridItem[]
|
||||
cols?: number
|
||||
rowHeight?: number
|
||||
onLayoutChange?: (layout: Layout[]) => void
|
||||
}
|
||||
|
||||
const DraggableResizableGrid: React.FC<DraggableResizableGridProps> = ({
|
||||
items,
|
||||
cols = 12,
|
||||
rowHeight = 30,
|
||||
onLayoutChange
|
||||
}) => {
|
||||
const layout = items.map((item, index) => ({
|
||||
i: item.id,
|
||||
x: index % cols,
|
||||
y: Math.floor(index / cols),
|
||||
w: 4,
|
||||
h: 4
|
||||
}))
|
||||
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<GridLayout
|
||||
className="layout"
|
||||
layout={layout}
|
||||
cols={cols}
|
||||
rowHeight={rowHeight}
|
||||
width={1200}
|
||||
onLayoutChange={onLayoutChange}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className="grid-item">
|
||||
{item.content}
|
||||
</div>
|
||||
))}
|
||||
</GridLayout>
|
||||
</DndProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default DraggableResizableGrid
|
36
src/components/common/ErrorBoundary.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React, { ReactNode } from 'react'
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children: ReactNode
|
||||
fallback: ReactNode
|
||||
}
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean
|
||||
}
|
||||
|
||||
class ErrorBoundary extends React.Component<
|
||||
ErrorBoundaryProps,
|
||||
ErrorBoundaryState
|
||||
> {
|
||||
state: ErrorBoundaryState = {
|
||||
hasError: false
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(_: Error): ErrorBoundaryState {
|
||||
return { hasError: true }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
||||
// You can log the error and errorInfo here, for example, to an error reporting service.
|
||||
console.error('Error caught in ErrorBoundary:', error, errorInfo)
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
if (this.state.hasError) return this.props.fallback
|
||||
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary
|
232
src/components/common/FilePanel.tsx
Normal file
@ -0,0 +1,232 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { styled, Box } from '@mui/system'
|
||||
import {
|
||||
Drawer,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Typography,
|
||||
ButtonBase,
|
||||
Button,
|
||||
Tooltip
|
||||
} from '@mui/material'
|
||||
import VideoCallIcon from '@mui/icons-material/VideoCall'
|
||||
import VideoModal from './VideoPublishModal'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { RootState } from '../../state/store'
|
||||
import AttachFileIcon from '@mui/icons-material/AttachFile'
|
||||
import { AudioModal } from './AudioPublishModal'
|
||||
import AudioFileIcon from '@mui/icons-material/AudioFile'
|
||||
import { GenericModal } from './GenericPublishModal'
|
||||
interface VideoPanelProps {
|
||||
onSelect: (video: Video) => void
|
||||
height?: string
|
||||
width?: string
|
||||
}
|
||||
|
||||
interface VideoApiResponse {
|
||||
videos: Video[]
|
||||
}
|
||||
|
||||
const Panel = styled('div')`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding-bottom: 10px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
`
|
||||
|
||||
const PublishButton = styled(Button)`
|
||||
/* position: absolute;
|
||||
bottom: 20px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: auto; */
|
||||
max-width: 80%;
|
||||
`
|
||||
|
||||
export const FilePanel: React.FC<VideoPanelProps> = ({
|
||||
onSelect,
|
||||
height,
|
||||
width
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [videos, setVideos] = useState<Video[]>([])
|
||||
const [isOpenVideoModal, setIsOpenVideoModal] = useState<boolean>(false)
|
||||
const { user } = useSelector((state: RootState) => state.auth)
|
||||
|
||||
const fetchVideos = React.useCallback(async (): Promise<Video[]> => {
|
||||
if (!user?.name) return []
|
||||
|
||||
let res
|
||||
try {
|
||||
res = await qortalRequest({
|
||||
action: 'LIST_QDN_RESOURCES',
|
||||
service: 'FILE',
|
||||
name: user.name,
|
||||
includeMetadata: true,
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
reverse: true
|
||||
})
|
||||
} catch (error) {}
|
||||
|
||||
// Replace this URL with the actual API endpoint
|
||||
|
||||
return res
|
||||
}, [user])
|
||||
useEffect(() => {
|
||||
fetchVideos().then((fetchedVideos) => setVideos(fetchedVideos))
|
||||
}, [])
|
||||
|
||||
const handleToggle = () => {
|
||||
setIsOpen(!isOpen)
|
||||
}
|
||||
|
||||
const handleClick = (video: Video) => {
|
||||
onSelect(video)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex'
|
||||
}}
|
||||
>
|
||||
<Tooltip title="Add any type of file" arrow>
|
||||
<AttachFileIcon
|
||||
onClick={handleToggle}
|
||||
sx={{
|
||||
height: height || '30px',
|
||||
width: width || 'auto',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
></AttachFileIcon>
|
||||
</Tooltip>
|
||||
<Drawer
|
||||
anchor="right"
|
||||
open={isOpen}
|
||||
onClose={handleToggle}
|
||||
ModalProps={{
|
||||
keepMounted: true // Better performance on mobile
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiPaper-root': {
|
||||
width: '400px'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Panel>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
flex: '0 0'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h5"
|
||||
component="div"
|
||||
sx={{ flexGrow: 1, mt: 2, mb: 1 }}
|
||||
>
|
||||
Select File
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
component="div"
|
||||
sx={{ flexGrow: 1, mb: 2 }}
|
||||
>
|
||||
List of Files in QDN under your name (FILE service)
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<List
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: '1',
|
||||
overflow: 'auto'
|
||||
}}
|
||||
>
|
||||
{videos.map((video) => (
|
||||
<ListItem key={video.identifier}>
|
||||
<ButtonBase
|
||||
onClick={() => handleClick(video)}
|
||||
sx={{ width: '100%' }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={video?.metadata?.title || ''}
|
||||
secondary={video?.metadata?.description || ''}
|
||||
/>
|
||||
</ButtonBase>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
flex: '0 0 50px'
|
||||
}}
|
||||
>
|
||||
<PublishButton
|
||||
variant="contained"
|
||||
onClick={() => setIsOpenVideoModal(true)}
|
||||
>
|
||||
Publish new file
|
||||
</PublishButton>
|
||||
</Box>
|
||||
</Panel>
|
||||
</Drawer>
|
||||
<GenericModal
|
||||
service="FILE"
|
||||
identifierPrefix="qfile_qblog"
|
||||
onClose={() => {
|
||||
setIsOpenVideoModal(false)
|
||||
}}
|
||||
open={isOpenVideoModal}
|
||||
onPublish={(value) => {
|
||||
fetchVideos().then((fetchedVideos) => setVideos(fetchedVideos))
|
||||
setIsOpenVideoModal(false)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Add this to your 'types.ts' file
|
||||
export interface Video {
|
||||
name: string
|
||||
service: string
|
||||
identifier: string
|
||||
metadata: {
|
||||
title: string
|
||||
description: string
|
||||
tags: string[]
|
||||
category: string
|
||||
categoryName: string
|
||||
}
|
||||
size: number
|
||||
created: number
|
||||
updated: number
|
||||
}
|
308
src/components/common/GenericPublishModal.tsx
Normal file
@ -0,0 +1,308 @@
|
||||
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
|
||||
}
|
||||
|
||||
interface SelectOption {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
const maxSize = 500 * 1024 * 1024
|
||||
|
||||
export const GenericModal: React.FC<GenericModalProps> = ({
|
||||
open,
|
||||
onClose,
|
||||
onPublish,
|
||||
acceptedFileType,
|
||||
acceptedFileTypes,
|
||||
service,
|
||||
identifierPrefix
|
||||
}) => {
|
||||
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({
|
||||
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>
|
||||
<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,54 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Menu, MenuItem } from '@mui/material';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
|
||||
interface MousePosition {
|
||||
mouseX: number;
|
||||
mouseY: number;
|
||||
}
|
||||
|
||||
export const GlobalContextMenu: React.FC = () => {
|
||||
const [mousePosition, setMousePosition] = useState<MousePosition | null>(null);
|
||||
const [textToCopy, setTextToCopy] = useState<string>('');
|
||||
|
||||
const handleContextMenu = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
const selection = window.getSelection()?.toString();
|
||||
if (selection) {
|
||||
setTextToCopy(selection);
|
||||
setMousePosition({
|
||||
mouseX: event.clientX - 2,
|
||||
mouseY: event.clientY - 4,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setMousePosition(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('contextmenu', handleContextMenu as any);
|
||||
return () => {
|
||||
document.removeEventListener('contextmenu', handleContextMenu as any);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
open={mousePosition !== null}
|
||||
onClose={handleClose}
|
||||
anchorReference="anchorPosition"
|
||||
anchorPosition={
|
||||
mousePosition !== null
|
||||
? { top: mousePosition.mouseY, left: mousePosition.mouseX }
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<CopyToClipboard text={textToCopy} onCopy={handleClose}>
|
||||
<MenuItem>Copy</MenuItem>
|
||||
</CopyToClipboard>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
74
src/components/common/ImageUploader.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
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,
|
||||
success(result) {
|
||||
const file = new File([result], 'name', {
|
||||
type: image.type
|
||||
})
|
||||
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;
|
47
src/components/common/LazyLoad.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import { useInView } from 'react-intersection-observer'
|
||||
import CircularProgress from '@mui/material/CircularProgress'
|
||||
|
||||
interface Props {
|
||||
onLoadMore: () => Promise<void>
|
||||
}
|
||||
|
||||
const LazyLoad: React.FC<Props> = ({ onLoadMore }) => {
|
||||
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'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
visibility: isFetching ? 'visible' : 'hidden'
|
||||
}}
|
||||
>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LazyLoad
|
37
src/components/common/LoaderBar.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import * as React from 'react'
|
||||
import LinearProgress, {
|
||||
LinearProgressProps
|
||||
} from '@mui/material/LinearProgress'
|
||||
import Typography from '@mui/material/Typography'
|
||||
import Box from '@mui/material/Box'
|
||||
import { useTheme } from '@mui/material'
|
||||
|
||||
interface LoaderBarProps {
|
||||
message: string
|
||||
}
|
||||
export const LoaderBar = ({ message }: LoaderBarProps) => {
|
||||
const theme = useTheme()
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
position: 'fixed',
|
||||
bottom: '10px',
|
||||
right: '10px',
|
||||
zIndex: 1500,
|
||||
width: '250px',
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: '5px',
|
||||
padding: '5px'
|
||||
}}
|
||||
>
|
||||
<Box sx={{ width: '100%', mr: 1 }}>
|
||||
<LinearProgress color="secondary" />
|
||||
</Box>
|
||||
<Box sx={{ minWidth: 35 }}>
|
||||
<Typography variant="body2">{message}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
211
src/components/common/MultiplePublish/MultiplePublish.tsx
Normal file
@ -0,0 +1,211 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Modal,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import React, { useCallback, useEffect, useState, useRef } from "react";
|
||||
import { CircleSVG } from "../../../assets/svgs/CircleSVG";
|
||||
import { EmptyCircleSVG } from "../../../assets/svgs/EmptyCircleSVG";
|
||||
import { styled } from "@mui/system";
|
||||
|
||||
interface Publish {
|
||||
resources: any[];
|
||||
action: string;
|
||||
}
|
||||
|
||||
interface MultiplePublishProps {
|
||||
publishes: Publish;
|
||||
isOpen: boolean;
|
||||
onSubmit: ()=> void
|
||||
onError: (message?: string)=> void
|
||||
}
|
||||
export const MultiplePublish = ({ publishes, isOpen, onSubmit, onError}: MultiplePublishProps) => {
|
||||
const theme = useTheme();
|
||||
const listOfSuccessfulPublishesRef = useRef([])
|
||||
const [listOfSuccessfulPublishes, setListOfSuccessfulPublishes] = useState<
|
||||
any[]
|
||||
>([]);
|
||||
const [listOfUnsuccessfulPublishes, setListOfUnSuccessfulPublishes] = useState<
|
||||
any[]
|
||||
>([]);
|
||||
const [currentlyInPublish, setCurrentlyInPublish] = useState(null);
|
||||
const hasStarted = useRef(false);
|
||||
const publish = useCallback(async (pub: any) => {
|
||||
const lengthOfResources = pub?.resources?.length
|
||||
const lengthOfTimeout = lengthOfResources * 30000
|
||||
return await qortalRequestWithTimeout(pub, lengthOfTimeout);
|
||||
}, []);
|
||||
const [isPublishing, setIsPublishing] = useState(true)
|
||||
|
||||
const handlePublish = useCallback(
|
||||
async (pub: any) => {
|
||||
try {
|
||||
setCurrentlyInPublish(pub?.identifier);
|
||||
setIsPublishing(true)
|
||||
const res = await publish(pub);
|
||||
|
||||
onSubmit()
|
||||
setListOfUnSuccessfulPublishes([])
|
||||
|
||||
} catch (error: any) {
|
||||
const unsuccessfulPublishes = error?.error?.unsuccessfulPublishes || []
|
||||
if(error?.error === 'User declined request'){
|
||||
onError()
|
||||
return
|
||||
}
|
||||
|
||||
if(error?.error === 'The request timed out'){
|
||||
onError("The request timed out")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if(unsuccessfulPublishes?.length > 0){
|
||||
setListOfUnSuccessfulPublishes(unsuccessfulPublishes)
|
||||
|
||||
}
|
||||
} finally {
|
||||
|
||||
setIsPublishing(false)
|
||||
}
|
||||
},
|
||||
[publish]
|
||||
);
|
||||
|
||||
const retry = ()=> {
|
||||
let newlistOfMultiplePublishes: any[] = [];
|
||||
listOfUnsuccessfulPublishes?.forEach((item)=> {
|
||||
const findPub = publishes?.resources.find((res: any)=> res?.identifier === item.identifier)
|
||||
if(findPub){
|
||||
newlistOfMultiplePublishes.push(findPub)
|
||||
}
|
||||
})
|
||||
const multiplePublish = {
|
||||
...publishes,
|
||||
resources: newlistOfMultiplePublishes
|
||||
};
|
||||
handlePublish(multiplePublish)
|
||||
}
|
||||
|
||||
const startPublish = useCallback(
|
||||
async (pubs: any) => {
|
||||
await handlePublish(pubs);
|
||||
},
|
||||
[handlePublish, onSubmit, listOfSuccessfulPublishes, publishes]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (publishes && !hasStarted.current) {
|
||||
hasStarted.current = true;
|
||||
startPublish(publishes);
|
||||
}
|
||||
}, [startPublish, publishes, listOfSuccessfulPublishes]);
|
||||
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={isOpen}
|
||||
aria-labelledby="modal-title"
|
||||
aria-describedby="modal-description"
|
||||
>
|
||||
<ModalBody
|
||||
sx={{
|
||||
minHeight: "50vh",
|
||||
}}
|
||||
>
|
||||
{publishes?.resources?.map((publish: any) => {
|
||||
const unpublished = listOfUnsuccessfulPublishes.map(item => item?.identifier)
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Typography>{publish?.identifier}</Typography>
|
||||
{!isPublishing && hasStarted.current ? (
|
||||
<>
|
||||
{!unpublished.includes(publish.identifier) ? (
|
||||
<CircleSVG
|
||||
color={theme.palette.text.primary}
|
||||
height="24px"
|
||||
width="24px"
|
||||
/>
|
||||
) : (
|
||||
<EmptyCircleSVG
|
||||
color={theme.palette.text.primary}
|
||||
height="24px"
|
||||
width="24px"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
): <CircularProgress size={16} color="secondary"/>}
|
||||
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
{!isPublishing && listOfUnsuccessfulPublishes.length > 0 && (
|
||||
<>
|
||||
<Typography sx={{
|
||||
marginTop: '20px',
|
||||
fontSize: '16px'
|
||||
}}>Some files were not published. Please try again. It's important that all the files get published. Maybe wait a couple minutes if the error keeps occurring</Typography>
|
||||
<Button variant="contained" onClick={()=> {
|
||||
retry()
|
||||
}}>Try again</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
</ModalBody>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
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%",
|
||||
maxWidth: "900px",
|
||||
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",
|
||||
},
|
||||
}));
|
86
src/components/common/Notification/Notification.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { toast, ToastContainer, Zoom, Slide } from 'react-toastify'
|
||||
import { removeNotification } from '../../../state/features/notificationsSlice'
|
||||
import 'react-toastify/dist/ReactToastify.css'
|
||||
import { RootState } from '../../../state/store'
|
||||
|
||||
const Notification = () => {
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const { alertTypes } = useSelector((state: RootState) => state.notifications)
|
||||
|
||||
if (alertTypes.alertError) {
|
||||
toast.error(`❌ ${alertTypes?.alertError}`, {
|
||||
position: 'bottom-right',
|
||||
autoClose: 4000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
icon: false
|
||||
})
|
||||
dispatch(removeNotification())
|
||||
}
|
||||
if (alertTypes.alertSuccess) {
|
||||
toast.success(`✔️ ${alertTypes?.alertSuccess}`, {
|
||||
position: 'bottom-right',
|
||||
autoClose: 4000,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
icon: false
|
||||
})
|
||||
dispatch(removeNotification())
|
||||
}
|
||||
if (alertTypes.alertInfo) {
|
||||
toast.info(`${alertTypes?.alertInfo}`, {
|
||||
position: 'top-right',
|
||||
autoClose: 1300,
|
||||
hideProgressBar: false,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
progress: undefined,
|
||||
theme: 'light'
|
||||
})
|
||||
dispatch(removeNotification())
|
||||
}
|
||||
|
||||
if (alertTypes.alertInfo) {
|
||||
return (
|
||||
<ToastContainer
|
||||
position="top-right"
|
||||
autoClose={2000}
|
||||
hideProgressBar={false}
|
||||
newestOnTop={false}
|
||||
closeOnClick
|
||||
rtl={false}
|
||||
pauseOnFocusLoss
|
||||
draggable
|
||||
pauseOnHover
|
||||
theme="light"
|
||||
toastStyle={{ fontSize: '16px' }}
|
||||
transition={Slide}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ToastContainer
|
||||
transition={Zoom}
|
||||
position="bottom-right"
|
||||
autoClose={false}
|
||||
hideProgressBar={false}
|
||||
newestOnTop={false}
|
||||
closeOnClick
|
||||
rtl={false}
|
||||
draggable
|
||||
pauseOnHover
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Notification
|
43
src/components/common/PageLoader.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import React from 'react'
|
||||
import CircularProgress from '@mui/material/CircularProgress'
|
||||
import Box from '@mui/system/Box'
|
||||
import { useTheme } from '@mui/material'
|
||||
|
||||
interface PageLoaderProps {
|
||||
size?: number
|
||||
thickness?: number
|
||||
}
|
||||
|
||||
const PageLoader: React.FC<PageLoaderProps> = ({
|
||||
size = 40,
|
||||
thickness = 5
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100vh',
|
||||
width: '100%',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.25)',
|
||||
zIndex: 200000
|
||||
}}
|
||||
>
|
||||
<CircularProgress
|
||||
size={size}
|
||||
thickness={thickness}
|
||||
sx={{
|
||||
color: theme.palette.secondary.main
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default PageLoader
|
25
src/components/common/Portal.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
|
||||
interface PortalProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const Portal: React.FC<PortalProps> = ({ children }) => {
|
||||
const [mounted, setMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
|
||||
return () => setMounted(false)
|
||||
}, [])
|
||||
|
||||
return mounted
|
||||
? createPortal(
|
||||
children,
|
||||
document.querySelector('#modal-root') as HTMLElement
|
||||
)
|
||||
: null
|
||||
}
|
||||
|
||||
export default Portal
|
281
src/components/common/PostPublishModal.tsx
Normal file
@ -0,0 +1,281 @@
|
||||
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
|
106
src/components/common/PublishAudio.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
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
|
||||
}
|
||||
|
||||
export const usePublishAudio = () => {
|
||||
const { user } = useSelector((state: RootState) => state.auth)
|
||||
const dispatch = useDispatch()
|
||||
const publishAudio = async ({
|
||||
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()
|
||||
|
||||
const identifier = `qaudio_qblog_${id}`
|
||||
|
||||
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
|
||||
}
|
||||
}
|
113
src/components/common/PublishGeneric.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
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
|
||||
}
|
||||
|
||||
export const usePublishGeneric = () => {
|
||||
const { user } = useSelector((state: RootState) => state.auth)
|
||||
const dispatch = useDispatch()
|
||||
const publishGeneric = async ({
|
||||
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()
|
||||
|
||||
const identifier = `${identifierPrefix}_${id}`
|
||||
|
||||
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
|
||||
}
|
||||
}
|
106
src/components/common/PublishVideo.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
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
|
||||
}
|
||||
|
||||
export const usePublishVideo = () => {
|
||||
const { user } = useSelector((state: RootState) => state.auth)
|
||||
const dispatch = useDispatch()
|
||||
const publishVideo = async ({
|
||||
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()
|
||||
|
||||
const identifier = `qvideo_qblog_${id}`
|
||||
|
||||
const resourceResponse = await qortalRequest({
|
||||
action: 'PUBLISH_QDN_RESOURCE',
|
||||
name: name,
|
||||
service: 'VIDEO',
|
||||
data64: base64,
|
||||
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
|
||||
}
|
||||
}
|
13
src/components/common/Spacer.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
export const Spacer = ({ height }: any) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: height,
|
||||
display: 'flex',
|
||||
flexShrink: 0
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
45
src/components/common/TextEditor/DisplayHtml.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { useMemo } from "react";
|
||||
import DOMPurify from "dompurify";
|
||||
import "react-quill/dist/quill.snow.css";
|
||||
import "react-quill/dist/quill.core.css";
|
||||
import "react-quill/dist/quill.bubble.css";
|
||||
import { convertQortalLinks } from "./utils";
|
||||
import { Box, styled } from "@mui/material";
|
||||
|
||||
|
||||
const CrowdfundInlineContent = styled(Box)(({ theme }) => ({
|
||||
display: "flex",
|
||||
fontFamily: "Mulish",
|
||||
fontSize: "19px",
|
||||
fontWeight: 400,
|
||||
letterSpacing: 0,
|
||||
color: theme.palette.text.primary,
|
||||
width: '100%'
|
||||
}));
|
||||
|
||||
export const DisplayHtml = ({ html, textColor }: any) => {
|
||||
const cleanContent = useMemo(() => {
|
||||
if (!html) return null;
|
||||
|
||||
const sanitize: string = DOMPurify.sanitize(html, {
|
||||
USE_PROFILES: { html: true },
|
||||
});
|
||||
const anchorQortal = convertQortalLinks(sanitize);
|
||||
return anchorQortal;
|
||||
}, [html]);
|
||||
|
||||
if (!cleanContent) return null;
|
||||
return (
|
||||
<CrowdfundInlineContent>
|
||||
<div
|
||||
className="ql-editor-display"
|
||||
style={{
|
||||
color: textColor || 'white',
|
||||
fontWeight: 400,
|
||||
fontSize: '16px'
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: cleanContent }}
|
||||
/>
|
||||
</CrowdfundInlineContent>
|
||||
);
|
||||
};
|
39
src/components/common/TextEditor/TextEditor.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import React from "react";
|
||||
import ReactQuill, { Quill } from "react-quill";
|
||||
import "react-quill/dist/quill.snow.css";
|
||||
import ImageResize from "quill-image-resize-module-react";
|
||||
import './texteditor.css'
|
||||
Quill.register("modules/imageResize", ImageResize);
|
||||
|
||||
const modules = {
|
||||
imageResize: {
|
||||
parchment: Quill.import("parchment"),
|
||||
modules: ["Resize", "DisplaySize"],
|
||||
},
|
||||
toolbar: [
|
||||
["bold", "italic", "underline", "strike"], // styled text
|
||||
["blockquote", "code-block"], // blocks
|
||||
[{ header: 1 }, { header: 2 }], // custom button values
|
||||
[{ list: "ordered" }, { list: "bullet" }], // lists
|
||||
[{ script: "sub" }, { script: "super" }], // superscript/subscript
|
||||
[{ indent: "-1" }, { indent: "+1" }], // outdent/indent
|
||||
[{ direction: "rtl" }], // text direction
|
||||
[{ size: ["small", false, "large", "huge"] }], // custom dropdown
|
||||
[{ header: [1, 2, 3, 4, 5, 6, false] }], // custom button values
|
||||
[{ color: [] }, { background: [] }], // dropdown with defaults
|
||||
[{ font: [] }], // font family
|
||||
[{ align: [] }], // text align
|
||||
["clean"], // remove formatting
|
||||
// ["image"], // image
|
||||
],
|
||||
};
|
||||
export const TextEditor = ({ inlineContent, setInlineContent }: any) => {
|
||||
return (
|
||||
<ReactQuill
|
||||
theme="snow"
|
||||
value={inlineContent}
|
||||
onChange={setInlineContent}
|
||||
modules={modules}
|
||||
/>
|
||||
);
|
||||
};
|
71
src/components/common/TextEditor/texteditor.css
Normal file
@ -0,0 +1,71 @@
|
||||
.ql-editor {
|
||||
min-height: 200px;
|
||||
width: 100%;
|
||||
color: black;
|
||||
font-size: 16px;
|
||||
font-family: Roboto;
|
||||
max-height: 225px;
|
||||
overflow-y: scroll;
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
.ql-editor::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
cursor: default;
|
||||
}
|
||||
.ql-editor::-webkit-scrollbar-track:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.ql-editor::-webkit-scrollbar {
|
||||
width: 16px;
|
||||
height: 10px;
|
||||
background-color: rgba(229, 229, 229, 0.70);
|
||||
}
|
||||
|
||||
.ql-editor::-webkit-scrollbar-thumb {
|
||||
background-color: #B0B0B0;
|
||||
border-radius: 8px;
|
||||
background-clip: content-box;
|
||||
border: 4px solid transparent;
|
||||
}
|
||||
|
||||
|
||||
.ql-editor img {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ql-editor-display {
|
||||
min-height: 20px;
|
||||
width: 100%;
|
||||
color: black;
|
||||
font-size: 16px;
|
||||
font-family: Roboto;
|
||||
padding: 0px !important;
|
||||
}
|
||||
|
||||
.ql-editor-display img {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.ql-container {
|
||||
font-size: 16px
|
||||
}
|
||||
|
||||
.ql-toolbar .ql-stroke {
|
||||
fill: none !important;
|
||||
stroke: black !important;
|
||||
}
|
||||
|
||||
.ql-toolbar .ql-fill {
|
||||
fill: black !important;
|
||||
stroke: none !important;
|
||||
}
|
||||
|
||||
.ql-toolbar .ql-picker {
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.ql-toolbar .ql-picker-options {
|
||||
background-color: white !important;
|
||||
}
|
26
src/components/common/TextEditor/utils.ts
Normal file
@ -0,0 +1,26 @@
|
||||
export function convertQortalLinks(inputHtml: string) {
|
||||
// Regular expression to match 'qortal://...' URLs.
|
||||
// This will stop at the first whitespace, comma, or HTML tag
|
||||
var regex = /(qortal:\/\/[^\s,<]+)/g;
|
||||
|
||||
// Replace matches in inputHtml with formatted anchor tag
|
||||
var outputHtml = inputHtml.replace(regex, function (match) {
|
||||
return `<a href="${match}" class="qortal-link">${match}</a>`;
|
||||
});
|
||||
|
||||
return outputHtml;
|
||||
}
|
||||
|
||||
export function extractTextFromHTML(htmlString: any, length = 150) {
|
||||
// Create a temporary DOM element
|
||||
const tempDiv = document.createElement("div");
|
||||
// Replace br tags and block-level tags with a space before setting the HTML content
|
||||
const htmlWithSpaces = htmlString.replace(/<\/?(br|p|div|h[1-6]|ul|ol|li|blockquote)[^>]*>/gi, ' ');
|
||||
tempDiv.innerHTML = htmlWithSpaces;
|
||||
// Extract the text content
|
||||
let text = tempDiv.textContent || tempDiv.innerText || "";
|
||||
// Replace multiple spaces with a single space and trim
|
||||
text = text.replace(/\s+/g, ' ').trim();
|
||||
// Slice the text to the desired length
|
||||
return text.slice(0, length);
|
||||
}
|
289
src/components/common/Tipping/Tipping.tsx
Normal file
@ -0,0 +1,289 @@
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Input,
|
||||
InputAdornment,
|
||||
InputLabel,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useTheme
|
||||
} from '@mui/material'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { CardContentContainerComment } from '../../../pages/BlogList/PostPreview-styles'
|
||||
import { StyledCardHeaderComment } from '../../../pages/BlogList/PostPreview-styles'
|
||||
import { StyledCardColComment } from '../../../pages/BlogList/PostPreview-styles'
|
||||
import { AuthorTextComment } from '../../../pages/BlogList/PostPreview-styles'
|
||||
import { StyledCardContentComment } from '../../../pages/BlogList/PostPreview-styles'
|
||||
import MenuItem from '@mui/material/MenuItem'
|
||||
import Select, { SelectChangeEvent } from '@mui/material/Select'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { RootState } from '../../../state/store'
|
||||
import Portal from '../Portal'
|
||||
import MonetizationOnIcon from '@mui/icons-material/MonetizationOn'
|
||||
interface TippingProps {
|
||||
name: string
|
||||
onSubmit: () => void
|
||||
onClose: () => void
|
||||
onlyIcon?: boolean
|
||||
}
|
||||
import QORT from '../../../assets/img/qort.png'
|
||||
import ARRR from '../../../assets/img/arrr.png'
|
||||
import LTC from '../../../assets/img/ltc.png'
|
||||
import BTC from '../../../assets/img/btc.png'
|
||||
import DOGE from '../../../assets/img/doge.png'
|
||||
import DGB from '../../../assets/img/dgb.png'
|
||||
import RVN from '../../../assets/img/rvn.png'
|
||||
import { setNotification } from '../../../state/features/notificationsSlice'
|
||||
const coins = [
|
||||
{ value: 'QORT', label: 'QORT' },
|
||||
{ value: 'ARRR', label: 'ARRR' },
|
||||
{ value: 'LTC', label: 'LTC' },
|
||||
{ value: 'BTC', label: 'BTC' },
|
||||
{ value: 'DOGE', label: 'DOGE' },
|
||||
{ value: 'DGB', label: 'DGB' },
|
||||
{ value: 'RVN', label: 'RVN' }
|
||||
]
|
||||
export const Tipping = ({
|
||||
onSubmit,
|
||||
onClose,
|
||||
name,
|
||||
onlyIcon
|
||||
}: TippingProps) => {
|
||||
const { user } = useSelector((state: RootState) => state.auth)
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false)
|
||||
const [selectedCoin, setSelectedCoint] = useState<any>(coins[0])
|
||||
const [amount, setAmount] = useState<number>(0)
|
||||
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const resetValues = () => {
|
||||
setSelectedCoint(coins[0])
|
||||
setAmount(0)
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
const sendCoin = async () => {
|
||||
try {
|
||||
if (!name) return
|
||||
let res = await qortalRequest({
|
||||
action: 'GET_NAME_DATA',
|
||||
name: name
|
||||
})
|
||||
const address = res.owner
|
||||
if (!address || !amount || !selectedCoin?.value) return
|
||||
|
||||
if (isNaN(amount)) return
|
||||
await qortalRequest({
|
||||
action: 'SEND_COIN',
|
||||
coin: selectedCoin.value,
|
||||
destinationAddress: address,
|
||||
amount: amount
|
||||
})
|
||||
dispatch(
|
||||
setNotification({
|
||||
msg: 'Coin successfully sent',
|
||||
alertType: 'success'
|
||||
})
|
||||
)
|
||||
resetValues()
|
||||
onSubmit()
|
||||
} catch (error: any) {
|
||||
let notificationObj = null
|
||||
if (typeof error === 'string') {
|
||||
notificationObj = {
|
||||
msg: error || 'Failed to send coin',
|
||||
alertType: 'error'
|
||||
}
|
||||
} else if (typeof error?.error === 'string') {
|
||||
notificationObj = {
|
||||
msg: error?.error || 'Failed to send coin',
|
||||
alertType: 'error'
|
||||
}
|
||||
} else {
|
||||
notificationObj = {
|
||||
msg: error?.message || 'Failed to send coin',
|
||||
alertType: 'error'
|
||||
}
|
||||
}
|
||||
if (!notificationObj) return
|
||||
dispatch(setNotification(notificationObj))
|
||||
}
|
||||
}
|
||||
|
||||
const handleOptionChange = (event: SelectChangeEvent<string>) => {
|
||||
const optionId = event.target.value
|
||||
const selectedOption = coins.find(
|
||||
(option: any) => option.value === optionId
|
||||
)
|
||||
setSelectedCoint(selectedOption || null)
|
||||
}
|
||||
|
||||
const getLogo = (coin: string) => {
|
||||
switch (coin) {
|
||||
case 'QORT':
|
||||
return QORT
|
||||
case 'ARRR':
|
||||
return ARRR
|
||||
case 'LTC':
|
||||
return LTC
|
||||
case 'BTC':
|
||||
return BTC
|
||||
case 'DOGE':
|
||||
return DOGE
|
||||
case 'DGB':
|
||||
return DGB
|
||||
case 'RVN':
|
||||
return RVN
|
||||
default:
|
||||
''
|
||||
// code block
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1
|
||||
}}
|
||||
>
|
||||
<Tooltip title={`Support ${name}`} arrow>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={() => setIsOpen((prev) => !prev)}
|
||||
>
|
||||
<MonetizationOnIcon
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
color: 'gold'
|
||||
}}
|
||||
></MonetizationOnIcon>
|
||||
{!onlyIcon && (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
Support
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
{isOpen && (
|
||||
<Portal>
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title"></DialogTitle>
|
||||
<DialogContent>
|
||||
<Box
|
||||
sx={{
|
||||
width: '300px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<InputLabel htmlFor="standard-adornment-name">To</InputLabel>
|
||||
<Input id="standard-adornment-name" value={name} disabled />
|
||||
<InputLabel htmlFor="standard-adornment-coin">
|
||||
Coin
|
||||
</InputLabel>
|
||||
<Select
|
||||
id="standard-adornment-coin"
|
||||
sx={{ width: '100%' }}
|
||||
defaultValue=""
|
||||
displayEmpty
|
||||
value={selectedCoin?.value || ''}
|
||||
onChange={handleOptionChange}
|
||||
renderValue={(value) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
{value && (
|
||||
<img
|
||||
style={{
|
||||
height: '25px',
|
||||
width: '25px'
|
||||
}}
|
||||
src={getLogo(value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{value}
|
||||
</Box>
|
||||
)
|
||||
}}
|
||||
>
|
||||
{coins.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.value}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<InputLabel htmlFor="standard-adornment-amount">
|
||||
Amount
|
||||
</InputLabel>
|
||||
<Input
|
||||
id="standard-adornment-amount"
|
||||
type="number"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(+e.target.value)}
|
||||
startAdornment={
|
||||
<InputAdornment position="start">
|
||||
<img
|
||||
style={{
|
||||
height: '15px',
|
||||
width: '15px'
|
||||
}}
|
||||
src={getLogo(selectedCoin?.value || '')}
|
||||
/>
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
setIsOpen(false)
|
||||
resetValues()
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button variant="contained" onClick={sendCoin}>
|
||||
Send Coin
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
55
src/components/common/UserNavbar/UserNavbar-styles.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { styled } from '@mui/system'
|
||||
import {
|
||||
AppBar,
|
||||
Toolbar,
|
||||
Typography,
|
||||
Menu,
|
||||
MenuItem
|
||||
} from '@mui/material'
|
||||
|
||||
export const CustomAppBar = styled(AppBar)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.mode === "light" ? theme.palette.background.default : "#19191b",
|
||||
color: theme.palette.text.primary
|
||||
}))
|
||||
|
||||
export const CustomToolbar = styled(Toolbar)({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center'
|
||||
})
|
||||
|
||||
export const CustomTitle = styled(Typography)(({ theme }) => ({
|
||||
color: theme.palette.text.primary,
|
||||
fontFamily: 'Raleway, Arial',
|
||||
fontSize: '18px'
|
||||
}))
|
||||
|
||||
export const StyledAppBar = styled(AppBar)(({ theme }) => ({
|
||||
backgroundColor: theme.palette.primary.main
|
||||
}))
|
||||
|
||||
export const StyledToolbar = styled(Toolbar)(({ theme }) => ({
|
||||
justifyContent: 'space-between'
|
||||
}))
|
||||
|
||||
export const StyledMenu = styled(Menu)(({ theme }) => ({
|
||||
marginTop: theme.spacing(2),
|
||||
overflow: 'hidden',
|
||||
padding: 0,
|
||||
}))
|
||||
|
||||
export const StyledMenuItem = styled(MenuItem)(({ theme }) => ({
|
||||
width: '100%',
|
||||
whiteSpace: 'nowrap',
|
||||
maxWidth: '300px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
fontSize: "16px",
|
||||
fontFamily: "Arial",
|
||||
padding: "12px 10px",
|
||||
transition: "all 0.3s ease-in-out",
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
filter: "brightness(1.1)"
|
||||
}
|
||||
}))
|
135
src/components/common/UserNavbar/UserNavbar.tsx
Normal file
@ -0,0 +1,135 @@
|
||||
import React from 'react'
|
||||
import { styled } from '@mui/system'
|
||||
import {
|
||||
AppBar,
|
||||
Toolbar,
|
||||
Typography,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Box,
|
||||
Button
|
||||
} from '@mui/material'
|
||||
|
||||
import {
|
||||
CustomAppBar,
|
||||
CustomToolbar,
|
||||
CustomTitle,
|
||||
StyledAppBar,
|
||||
StyledToolbar,
|
||||
StyledMenu,
|
||||
StyledMenuItem
|
||||
} from './UserNavbar-styles'
|
||||
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Menu as MenuIcon } from '@mui/icons-material'
|
||||
import { removePrefix } from '../../../utils/blogIdformats'
|
||||
import { QblogLogoContainer } from '../../layout/Navbar/Navbar-styles'
|
||||
import QblogLogo from '../../../assets/img/qBlogLogo.png'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
menuItems: any[]
|
||||
name: string
|
||||
blogId: string
|
||||
}
|
||||
|
||||
export const UserNavbar: React.FC<Props> = ({
|
||||
title,
|
||||
menuItems,
|
||||
name,
|
||||
blogId
|
||||
}) => {
|
||||
const navigate = useNavigate()
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null)
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const goToPost = (item: any) => {
|
||||
if (!name) return
|
||||
const { postId } = item
|
||||
|
||||
const str = postId
|
||||
const arr = str.split('-post-')
|
||||
const str1 = arr[0]
|
||||
const str2 = arr[1]
|
||||
const blogId = removePrefix(str1)
|
||||
navigate(`/${name}/${blogId}/${str2}`)
|
||||
}
|
||||
|
||||
const handleAction = (action: () => void) => {
|
||||
handleClose()
|
||||
setTimeout(() => {
|
||||
action()
|
||||
}, 100)
|
||||
}
|
||||
|
||||
return (
|
||||
<CustomAppBar position="sticky">
|
||||
<CustomToolbar variant="dense">
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
edge="start"
|
||||
color="inherit"
|
||||
aria-label="menu"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<CustomTitle
|
||||
variant="h6"
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
marginLeft: '10px'
|
||||
}}
|
||||
onClick={() => {
|
||||
navigate(`/${name}/${blogId}`)
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</CustomTitle>
|
||||
</Box>
|
||||
<StyledMenu
|
||||
anchorEl={anchorEl}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleClose}
|
||||
PaperProps={{ style: { width: '250px' } }}
|
||||
>
|
||||
{menuItems.map((item, index) => (
|
||||
<StyledMenuItem
|
||||
key={index}
|
||||
onClick={() => handleAction(() => goToPost(item))}
|
||||
>
|
||||
{item.name}
|
||||
</StyledMenuItem>
|
||||
))}
|
||||
</StyledMenu>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<QblogLogoContainer
|
||||
src={QblogLogo}
|
||||
alt="Qblog Logo"
|
||||
onClick={() => {
|
||||
navigate(`/`)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</CustomToolbar>
|
||||
</CustomAppBar>
|
||||
)
|
||||
}
|