Browse Source

First Commit

master
Qortal Dev 4 weeks ago
commit
73b1b49384
  1. 24
      .gitignore
  2. 10
      .prettierrc.json
  3. 13
      index.html
  4. 9434
      package-lock.json
  5. 65
      package.json
  6. 1
      public/vite.svg
  7. 37
      src/App.tsx
  8. BIN
      src/assets/img/arrr.png
  9. BIN
      src/assets/img/btc.png
  10. BIN
      src/assets/img/dgb.png
  11. BIN
      src/assets/img/doge.png
  12. BIN
      src/assets/img/ltc.png
  13. BIN
      src/assets/img/q-mail-icon.png
  14. BIN
      src/assets/img/qBlogLogo.png
  15. BIN
      src/assets/img/qmaillogo.png
  16. BIN
      src/assets/img/qort.png
  17. BIN
      src/assets/img/rvn.png
  18. 1
      src/assets/react.svg
  19. 25
      src/assets/svgs/AccountCircleSVG.tsx
  20. 4
      src/assets/svgs/AddAlias.svg
  21. 10
      src/assets/svgs/AliasAvatar.svg
  22. 21
      src/assets/svgs/AlignCenterSVG.tsx
  23. 17
      src/assets/svgs/AlignLeftSVG.tsx
  24. 17
      src/assets/svgs/AlignRightSVG.tsx
  25. 3
      src/assets/svgs/ArrowDown.svg
  26. 3
      src/assets/svgs/Attachment.svg
  27. 3
      src/assets/svgs/AttachmentMail.svg
  28. 17
      src/assets/svgs/BoldSVG.tsx
  29. 3
      src/assets/svgs/Check.svg
  30. 23
      src/assets/svgs/CircleSVG.tsx
  31. 3
      src/assets/svgs/Close.svg
  32. 35
      src/assets/svgs/CloseSVG.tsx
  33. 17
      src/assets/svgs/CodeBlockSVG.tsx
  34. 9
      src/assets/svgs/ComposeIcon.svg
  35. 3
      src/assets/svgs/CreateThread.svg
  36. 20
      src/assets/svgs/CreateThreadIcon.tsx
  37. 23
      src/assets/svgs/EmptyCircleSVG.tsx
  38. 3
      src/assets/svgs/Forward.svg
  39. 3
      src/assets/svgs/Group.svg
  40. 17
      src/assets/svgs/H2SVG.tsx
  41. 17
      src/assets/svgs/H3SVG.tsx
  42. 3
      src/assets/svgs/Home.svg
  43. 7
      src/assets/svgs/IconTypes.ts
  44. 17
      src/assets/svgs/ItalicSVG.tsx
  45. 17
      src/assets/svgs/LinkSVG.tsx
  46. 3
      src/assets/svgs/Lock.svg
  47. 54
      src/assets/svgs/Logo.svg
  48. 3
      src/assets/svgs/ModalClose.svg
  49. 3
      src/assets/svgs/More.svg
  50. 3
      src/assets/svgs/NewMessageAttachment.svg
  51. 25
      src/assets/svgs/NewWindowSVG.tsx
  52. 3
      src/assets/svgs/Reply.svg
  53. 3
      src/assets/svgs/Return.svg
  54. 9
      src/assets/svgs/Send.svg
  55. 3
      src/assets/svgs/SendNewMessage.svg
  56. 19
      src/assets/svgs/SendNewMessage.tsx
  57. 4
      src/assets/svgs/Sort.svg
  58. 17
      src/assets/svgs/UnderlineSVG.tsx
  59. 1
      src/assets/svgs/accountCircle.svg
  60. 6
      src/assets/svgs/interfaces.ts
  61. 9
      src/assets/svgs/mail.svg
  62. 96
      src/components/DynamicHeightItem.tsx
  63. 39
      src/components/DynamicHeightItemMinimal.tsx
  64. 330
      src/components/FileElement.tsx
  65. 230
      src/components/common/AudioPanel.tsx
  66. 192
      src/components/common/AudioPlayer.tsx
  67. 358
      src/components/common/AudioPublishModal.tsx
  68. 28
      src/components/common/BlockedNamesModal/BlockedNamesModal-styles.ts
  69. 100
      src/components/common/BlockedNamesModal/BlockedNamesModal.tsx
  70. 118
      src/components/common/ChipInputComponent/ChipInputComponent.tsx
  71. 279
      src/components/common/Comments/Comment.tsx
  72. 172
      src/components/common/Comments/CommentEditor.tsx
  73. 307
      src/components/common/Comments/CommentSection.tsx
  74. 56
      src/components/common/ConfirmationModal.tsx
  75. 16
      src/components/common/CustomIcon.tsx
  76. 289
      src/components/common/DownloadTaskManager.tsx
  77. 55
      src/components/common/DraggableResizableGrid.tsx
  78. 36
      src/components/common/ErrorBoundary.tsx
  79. 232
      src/components/common/FilePanel.tsx
  80. 308
      src/components/common/GenericPublishModal.tsx
  81. 54
      src/components/common/GlobalContextMenu/GlobalContextMenu.tsx
  82. 74
      src/components/common/ImageUploader.tsx
  83. 47
      src/components/common/LazyLoad.tsx
  84. 37
      src/components/common/LoaderBar.tsx
  85. 211
      src/components/common/MultiplePublish/MultiplePublish.tsx
  86. 86
      src/components/common/Notification/Notification.tsx
  87. 43
      src/components/common/PageLoader.tsx
  88. 25
      src/components/common/Portal.tsx
  89. 281
      src/components/common/PostPublishModal.tsx
  90. 106
      src/components/common/PublishAudio.tsx
  91. 113
      src/components/common/PublishGeneric.tsx
  92. 106
      src/components/common/PublishVideo.tsx
  93. 13
      src/components/common/Spacer.tsx
  94. 45
      src/components/common/TextEditor/DisplayHtml.tsx
  95. 39
      src/components/common/TextEditor/TextEditor.tsx
  96. 71
      src/components/common/TextEditor/texteditor.css
  97. 26
      src/components/common/TextEditor/utils.ts
  98. 289
      src/components/common/Tipping/Tipping.tsx
  99. 55
      src/components/common/UserNavbar/UserNavbar-styles.ts
  100. 135
      src/components/common/UserNavbar/UserNavbar.tsx
  101. Some files were not shown because too many files have changed in this diff Show More

24
.gitignore vendored

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
*.zip
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

10
.prettierrc.json

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

13
index.html

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Q-Mail</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

9434
package-lock.json generated

File diff suppressed because it is too large Load Diff

65
package.json

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
src/assets/img/btc.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
src/assets/img/dgb.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
src/assets/img/doge.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
src/assets/img/ltc.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
src/assets/img/q-mail-icon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
src/assets/img/qBlogLogo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
src/assets/img/qmaillogo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
src/assets/img/qort.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
src/assets/img/rvn.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

1
src/assets/react.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="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

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

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

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

@ -0,0 +1,21 @@
import { SVGProps } from './interfaces'
export const AlignCenterSVG: React.FC<SVGProps> = ({
color,
height,
width
}) => {
return (
<svg
height={height}
width={width}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 96 960 960"
>
<path
fill={color}
d="M150 936q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 876h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 936H150Zm164-165q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T314 711h333q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T647 771H314ZM150 606q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 546h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 606H150Zm164-165q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T314 381h333q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T647 441H314ZM150 276q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 216h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 276H150Z"
/>
</svg>
)
}

17
src/assets/svgs/AlignLeftSVG.tsx

@ -0,0 +1,17 @@
import { SVGProps } from './interfaces'
export const AlignLeftSVG: React.FC<SVGProps> = ({ color, height, width }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height={height}
viewBox="0 96 960 960"
width={width}
>
<path
fill={color}
d="M150 771q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 711h412q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T562 771H150Zm0-330q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 381h412q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T562 441H150Zm0 165q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 546h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 606H150Zm0 330q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 876h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 936H150Zm0-660q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 216h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 276H150Z"
/>
</svg>
)
}

17
src/assets/svgs/AlignRightSVG.tsx

@ -0,0 +1,17 @@
import { SVGProps } from './interfaces'
export const AlignRightSVG: React.FC<SVGProps> = ({ color, height, width }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height={height}
viewBox="0 96 960 960"
width={width}
>
<path
fill={color}
d="M150 936q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 876h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 936H150Zm249-165q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T399 711h411q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 771H399ZM150 606q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 546h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 606H150Zm249-165q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T399 381h411q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 441H399ZM150 276q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 216h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 276H150Z"
/>
</svg>
)
}

3
src/assets/svgs/ArrowDown.svg

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -0,0 +1,17 @@
import { SVGProps } from './interfaces'
export const H2SVG: React.FC<SVGProps> = ({ color, height, width }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height={height}
viewBox="0 96 960 960"
width={width}
>
<path
fill={color}
d="M149.825 776Q137 776 128.5 767.375T120 746V406q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T180 406v140h180V406q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T420 406v340q0 12.75-8.675 21.375-8.676 8.625-21.5 8.625-12.825 0-21.325-8.625T360 746V606H180v140q0 12.75-8.675 21.375-8.676 8.625-21.5 8.625ZM570 776q-12.75 0-21.375-8.625T540 746V606q0-24.75 17.625-42.375T600 546h180V436H570q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T570 376h210q24.75 0 42.375 17.625T840 436v110q0 24.75-17.625 42.375T780 606H600v110h210q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 776H570Z"
/>
</svg>
)
}

17
src/assets/svgs/H3SVG.tsx

@ -0,0 +1,17 @@
import { SVGProps } from './interfaces'
export const H3SVG: React.FC<SVGProps> = ({ color, height, width }) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height={height}
viewBox="0 96 960 960"
width={width}
>
<path
fill={color}
d="M149.825 776Q137 776 128.5 767.375T120 746V406q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T180 406v140h180V406q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T420 406v340q0 12.75-8.675 21.375-8.676 8.625-21.5 8.625-12.825 0-21.325-8.625T360 746V606H180v140q0 12.75-8.675 21.375-8.676 8.625-21.5 8.625ZM570 776q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T570 716h210V606H650q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T650 546h130V436H570q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T570 376h210q24.75 0 42.375 17.625T840 436v280q0 24.75-17.625 42.375T780 776H570Z"
/>
</svg>
)
}

3
src/assets/svgs/Home.svg

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -0,0 +1,6 @@
export interface SVGProps {
color: string
height: string
width: string
opacity?: number
}

9
src/assets/svgs/mail.svg

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

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

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

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

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

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

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

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

@ -0,0 +1,28 @@
import { styled } from '@mui/system';
import {
Box,
Modal,
Typography
} from '@mui/material';
export const StyledModal = styled(Modal)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}))
export const ModalContent = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.primary.main,
padding: theme.spacing(4),
borderRadius: theme.spacing(1),
width: '40%',
'&:focus': {
outline: 'none'
}
}))
export const ModalText = styled(Typography)(({ theme }) => ({
fontFamily: "Raleway",
fontSize: "25px",
color: theme.palette.text.primary,
}));

100
src/components/common/BlockedNamesModal/BlockedNamesModal.tsx

@ -0,0 +1,100 @@
import React, { useState } from 'react'
import {
Box,
Button,
Modal,
Typography,
SelectChangeEvent,
ListItem,
List,
useTheme
} from '@mui/material'
import {
StyledModal,
ModalContent,
ModalText
} from './BlockedNamesModal-styles'
interface PostModalProps {
open: boolean
onClose: () => void
}
export const BlockedNamesModal: React.FC<PostModalProps> = ({
open,
onClose
}) => {
const [blockedNames, setBlockedNames] = useState<string[]>([])
const theme = useTheme()
const getBlockedNames = React.useCallback(async () => {
try {
const listName = `blockedNames`
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

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

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

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

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

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

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

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

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

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

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

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

54
src/components/common/GlobalContextMenu/GlobalContextMenu.tsx

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save