Initial q-blog commit in its own repo
							
								
								
									
										24
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,24 @@
 | 
			
		||||
# Logs
 | 
			
		||||
logs
 | 
			
		||||
*.log
 | 
			
		||||
npm-debug.log*
 | 
			
		||||
yarn-debug.log*
 | 
			
		||||
yarn-error.log*
 | 
			
		||||
pnpm-debug.log*
 | 
			
		||||
lerna-debug.log*
 | 
			
		||||
*.zip
 | 
			
		||||
node_modules
 | 
			
		||||
dist
 | 
			
		||||
dist-ssr
 | 
			
		||||
*.local
 | 
			
		||||
 | 
			
		||||
# Editor directories and files
 | 
			
		||||
.vscode/*
 | 
			
		||||
!.vscode/extensions.json
 | 
			
		||||
.idea
 | 
			
		||||
.DS_Store
 | 
			
		||||
*.suo
 | 
			
		||||
*.ntvs*
 | 
			
		||||
*.njsproj
 | 
			
		||||
*.sln
 | 
			
		||||
*.sw?
 | 
			
		||||
							
								
								
									
										10
									
								
								.prettierrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,10 @@
 | 
			
		||||
{
 | 
			
		||||
  "printWidth": 80,
 | 
			
		||||
  "singleQuote": false,
 | 
			
		||||
  "trailingComma": "es5",
 | 
			
		||||
  "bracketSpacing": true,
 | 
			
		||||
  "jsxBracketSameLine": false,
 | 
			
		||||
  "arrowParens": "avoid",
 | 
			
		||||
  "tabWidth": 2,
 | 
			
		||||
  "semi": true
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,14 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="UTF-8" />
 | 
			
		||||
    <link rel="icon" type="image/x-icon" href="/favicon.ico" />
 | 
			
		||||
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
    <title>Q-Blog</title>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <div id="root"></div>
 | 
			
		||||
    <script type="module" src="/src/main.tsx"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										6695
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										54
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,54 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "q-blog",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "version": "0.0.0",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "vite",
 | 
			
		||||
    "build": "tsc && vite build",
 | 
			
		||||
    "preview": "vite preview"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@emotion/react": "^11.10.6",
 | 
			
		||||
    "@emotion/styled": "^11.10.6",
 | 
			
		||||
    "@mui/icons-material": "^5.11.11",
 | 
			
		||||
    "@mui/material": "^5.11.13",
 | 
			
		||||
    "@reduxjs/toolkit": "^1.9.3",
 | 
			
		||||
    "@types/react-grid-layout": "^1.3.2",
 | 
			
		||||
    "axios": "^1.3.4",
 | 
			
		||||
    "compressorjs": "^1.2.1",
 | 
			
		||||
    "localforage": "^1.10.0",
 | 
			
		||||
    "moment": "^2.29.4",
 | 
			
		||||
    "philliplm-react-modern-audio-player": "^1.4.6",
 | 
			
		||||
    "react": "^18.2.0",
 | 
			
		||||
    "react-copy-to-clipboard": "^5.1.0",
 | 
			
		||||
    "react-dnd": "^16.0.1",
 | 
			
		||||
    "react-dnd-html5-backend": "^16.0.1",
 | 
			
		||||
    "react-dom": "^18.2.0",
 | 
			
		||||
    "react-dropzone": "^14.2.3",
 | 
			
		||||
    "react-grid-layout": "^1.3.4",
 | 
			
		||||
    "react-intersection-observer": "^9.4.3",
 | 
			
		||||
    "react-masonry-css": "^1.0.16",
 | 
			
		||||
    "react-redux": "^8.0.5",
 | 
			
		||||
    "react-resize-detector": "^8.0.4",
 | 
			
		||||
    "react-router-dom": "^6.9.0",
 | 
			
		||||
    "react-toastify": "^9.1.2",
 | 
			
		||||
    "react-virtuoso": "^4.3.3",
 | 
			
		||||
    "short-unique-id": "^4.4.4",
 | 
			
		||||
    "slate": "^0.91.4",
 | 
			
		||||
    "slate-history": "^0.86.0",
 | 
			
		||||
    "slate-react": "^0.91.11",
 | 
			
		||||
    "ts-key-enum": "^2.0.12"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@mui/types": "^7.2.3",
 | 
			
		||||
    "@types/react": "^18.0.28",
 | 
			
		||||
    "@types/react-copy-to-clipboard": "^5.0.4",
 | 
			
		||||
    "@types/react-dom": "^18.0.11",
 | 
			
		||||
    "@vitejs/plugin-react-swc": "^3.2.0",
 | 
			
		||||
    "prettier": "^2.8.6",
 | 
			
		||||
    "typescript": "^4.9.3",
 | 
			
		||||
    "vite": "^4.2.0",
 | 
			
		||||
    "worker-loader": "^3.0.8"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								public/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 15 KiB  | 
							
								
								
									
										67
									
								
								src/App.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,67 @@
 | 
			
		||||
// @ts-nocheck
 | 
			
		||||
 | 
			
		||||
import { Routes, Route } from 'react-router-dom'
 | 
			
		||||
import { BlogIndividualPost } from './pages/BlogIndividualPost/BlogIndividualPost'
 | 
			
		||||
import { BlogIndividualProfile } from './pages/BlogIndividualProfile/BlogIndividualProfile'
 | 
			
		||||
import { BlogList } from './pages/BlogList/BlogList'
 | 
			
		||||
import { CreatePost } from './pages/CreatePost/CreatePost'
 | 
			
		||||
import { CreatEditProfile } from './pages/CreateEditProfile/CreatEditProfile'
 | 
			
		||||
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 { useState } from 'react'
 | 
			
		||||
import { Mail } from './pages/Mail/Mail'
 | 
			
		||||
 | 
			
		||||
function App() {
 | 
			
		||||
  const themeColor = window._qdnTheme
 | 
			
		||||
 | 
			
		||||
  // const [colorTheme, setColorTheme] = useState('dark')
 | 
			
		||||
 | 
			
		||||
  // const toggleDarkMode = () => {
 | 
			
		||||
  //   setIsDarkMode("dark");
 | 
			
		||||
  // }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Provider store={store}>
 | 
			
		||||
      <ThemeProvider theme={themeColor === 'light' ? lightTheme : darkTheme}>
 | 
			
		||||
        <Notification />
 | 
			
		||||
        <DownloadWrapper>
 | 
			
		||||
          <GlobalWrapper>
 | 
			
		||||
            <CssBaseline />
 | 
			
		||||
 | 
			
		||||
            <Routes>
 | 
			
		||||
              <Route
 | 
			
		||||
                path="/:user/:blog/:postId"
 | 
			
		||||
                element={<BlogIndividualPost />}
 | 
			
		||||
              />
 | 
			
		||||
              <Route
 | 
			
		||||
                path="/:user/:blog/:postId/edit"
 | 
			
		||||
                element={<CreatePost mode="edit" />}
 | 
			
		||||
              />
 | 
			
		||||
              <Route path="/:user/:blog" element={<BlogIndividualProfile />} />
 | 
			
		||||
              <Route path="/post/new" element={<CreatePost />} />
 | 
			
		||||
              <Route path="/profile/new" element={<CreatEditProfile />} />
 | 
			
		||||
              <Route
 | 
			
		||||
                path="/favorites"
 | 
			
		||||
                element={<BlogList mode="favorites" />}
 | 
			
		||||
              />
 | 
			
		||||
              <Route
 | 
			
		||||
                path="/subscriptions"
 | 
			
		||||
                element={<BlogList mode="subscriptions" />}
 | 
			
		||||
              />
 | 
			
		||||
              <Route path="/mail" element={<Mail />} />
 | 
			
		||||
              <Route path="/" element={<BlogList />} />
 | 
			
		||||
            </Routes>
 | 
			
		||||
          </GlobalWrapper>
 | 
			
		||||
        </DownloadWrapper>
 | 
			
		||||
      </ThemeProvider>
 | 
			
		||||
    </Provider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default App
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								src/assets/img/arrr.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.4 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/img/btc.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.9 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/img/dgb.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 4.8 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/img/doge.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.8 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/img/ltc.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.4 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/img/qBlogLogo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 2.4 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/img/qort.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.9 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/img/rvn.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 2.3 KiB  | 
							
								
								
									
										1
									
								
								src/assets/react.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 4.0 KiB  | 
							
								
								
									
										25
									
								
								src/assets/svgs/AccountCircleSVG.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,25 @@
 | 
			
		||||
interface AccountCircleSVGProps {
 | 
			
		||||
  color: string
 | 
			
		||||
  height: string
 | 
			
		||||
  width: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const AccountCircleSVG: React.FC<AccountCircleSVGProps> = ({
 | 
			
		||||
  color,
 | 
			
		||||
  height,
 | 
			
		||||
  width
 | 
			
		||||
}) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
      height={height}
 | 
			
		||||
      viewBox="0 96 960 960"
 | 
			
		||||
      width={width}
 | 
			
		||||
    >
 | 
			
		||||
      <path
 | 
			
		||||
        fill={color}
 | 
			
		||||
        d="M222 801q63-44 125-67.5T480 710q71 0 133.5 23.5T739 801q44-54 62.5-109T820 576q0-145-97.5-242.5T480 236q-145 0-242.5 97.5T140 576q0 61 19 116t63 109Zm257.814-195Q422 606 382.5 566.314q-39.5-39.686-39.5-97.5t39.686-97.314q39.686-39.5 97.5-39.5t97.314 39.686q39.5 39.686 39.5 97.5T577.314 566.5q-39.686 39.5-97.5 39.5Zm.654 370Q398 976 325 944.5q-73-31.5-127.5-86t-86-127.266Q80 658.468 80 575.734T111.5 420.5q31.5-72.5 86-127t127.266-86q72.766-31.5 155.5-31.5T635.5 207.5q72.5 31.5 127 86t86 127.032q31.5 72.532 31.5 155T848.5 731q-31.5 73-86 127.5t-127.032 86q-72.532 31.5-155 31.5ZM480 916q55 0 107.5-16T691 844q-51-36-104-55t-107-19q-54 0-107 19t-104 55q51 40 103.5 56T480 916Zm0-370q34 0 55.5-21.5T557 469q0-34-21.5-55.5T480 392q-34 0-55.5 21.5T403 469q0 34 21.5 55.5T480 546Zm0-77Zm0 374Z"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								src/assets/svgs/AlignCenterSVG.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,21 @@
 | 
			
		||||
import { SVGProps } from './interfaces'
 | 
			
		||||
 | 
			
		||||
export const AlignCenterSVG: React.FC<SVGProps> = ({
 | 
			
		||||
  color,
 | 
			
		||||
  height,
 | 
			
		||||
  width
 | 
			
		||||
}) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg
 | 
			
		||||
      height={height}
 | 
			
		||||
      width={width}
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
      viewBox="0 96 960 960"
 | 
			
		||||
    >
 | 
			
		||||
      <path
 | 
			
		||||
        fill={color}
 | 
			
		||||
        d="M150 936q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 876h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 936H150Zm164-165q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T314 711h333q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T647 771H314ZM150 606q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 546h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 606H150Zm164-165q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T314 381h333q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T647 441H314ZM150 276q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 216h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 276H150Z"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								src/assets/svgs/AlignLeftSVG.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,17 @@
 | 
			
		||||
import { SVGProps } from './interfaces'
 | 
			
		||||
 | 
			
		||||
export const AlignLeftSVG: React.FC<SVGProps> = ({ color, height, width }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
      height={height}
 | 
			
		||||
      viewBox="0 96 960 960"
 | 
			
		||||
      width={width}
 | 
			
		||||
    >
 | 
			
		||||
      <path
 | 
			
		||||
        fill={color}
 | 
			
		||||
        d="M150 771q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 711h412q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T562 771H150Zm0-330q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 381h412q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T562 441H150Zm0 165q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 546h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 606H150Zm0 330q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 876h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 936H150Zm0-660q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 216h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 276H150Z"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								src/assets/svgs/AlignRightSVG.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,17 @@
 | 
			
		||||
import { SVGProps } from './interfaces'
 | 
			
		||||
 | 
			
		||||
export const AlignRightSVG: React.FC<SVGProps> = ({ color, height, width }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
      height={height}
 | 
			
		||||
      viewBox="0 96 960 960"
 | 
			
		||||
      width={width}
 | 
			
		||||
    >
 | 
			
		||||
      <path
 | 
			
		||||
        fill={color}
 | 
			
		||||
        d="M150 936q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 876h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 936H150Zm249-165q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T399 711h411q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 771H399ZM150 606q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 546h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 606H150Zm249-165q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T399 381h411q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 441H399ZM150 276q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T150 216h660q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 276H150Z"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								src/assets/svgs/BoldSVG.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,17 @@
 | 
			
		||||
import { SVGProps } from './interfaces'
 | 
			
		||||
 | 
			
		||||
export const BoldSVG: React.FC<SVGProps> = ({ color, height, width }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
      height={height}
 | 
			
		||||
      viewBox="0 96 960 960"
 | 
			
		||||
      width={width}
 | 
			
		||||
    >
 | 
			
		||||
      <path
 | 
			
		||||
        fill={color}
 | 
			
		||||
        d="M335 856q-25 0-42.5-17.5T275 796V356q0-25 17.5-42.5T335 296h168q66 0 114.5 42T666 444q0 38-21 70t-56 49v6q43 14 69.5 50t26.5 81q0 68-52.5 112T510 856H335Zm26-76h144q38 0 66-25t28-63q0-37-28-62t-66-25H361v175Zm0-247h136q35 0 60.5-23t25.5-58q0-35-25.5-58.5T497 370H361v163Z"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								src/assets/svgs/CodeBlockSVG.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,17 @@
 | 
			
		||||
import { SVGProps } from './interfaces'
 | 
			
		||||
 | 
			
		||||
export const CodeBlockSVG: React.FC<SVGProps> = ({ color, height, width }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
      height={height}
 | 
			
		||||
      viewBox="0 96 960 960"
 | 
			
		||||
      width={width}
 | 
			
		||||
    >
 | 
			
		||||
      <path
 | 
			
		||||
        fill={color}
 | 
			
		||||
        d="m330 576 70-70q9-9 9-22t-9-22q-9-9-21.833-9-12.834 0-22.167 9l-93 93q-5 5-7 10.133-2 5.134-2 11Q254 582 256 587q2 5 7 10l94 94q9.333 9 22.167 9Q392 700 401 691q9-9 9-22t-9-22l-71-71Zm300 0-71 71q-9 9-9 22t9 22q9 9 21.833 9 12.834 0 22.167-9l94-94q5-5 7-10.133 2-5.134 2-11Q706 570 704 565q-2-5-7-10l-94-94q-4-5-10-7t-12-2q-6 0-11.5 2t-10.167 6.8Q550 470.4 550 483.2q0 12.8 9 21.8l71 71ZM180 936q-24 0-42-18t-18-42V276q0-24 18-42t42-18h600q24 0 42 18t18 42v600q0 24-18 42t-42 18H180Zm0-60h600V276H180v600Zm0-600v600-600Z"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								src/assets/svgs/H2SVG.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,17 @@
 | 
			
		||||
import { SVGProps } from './interfaces'
 | 
			
		||||
 | 
			
		||||
export const H2SVG: React.FC<SVGProps> = ({ color, height, width }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
      height={height}
 | 
			
		||||
      viewBox="0 96 960 960"
 | 
			
		||||
      width={width}
 | 
			
		||||
    >
 | 
			
		||||
      <path
 | 
			
		||||
        fill={color}
 | 
			
		||||
        d="M149.825 776Q137 776 128.5 767.375T120 746V406q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T180 406v140h180V406q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T420 406v340q0 12.75-8.675 21.375-8.676 8.625-21.5 8.625-12.825 0-21.325-8.625T360 746V606H180v140q0 12.75-8.675 21.375-8.676 8.625-21.5 8.625ZM570 776q-12.75 0-21.375-8.625T540 746V606q0-24.75 17.625-42.375T600 546h180V436H570q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T570 376h210q24.75 0 42.375 17.625T840 436v110q0 24.75-17.625 42.375T780 606H600v110h210q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T810 776H570Z"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								src/assets/svgs/H3SVG.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,17 @@
 | 
			
		||||
import { SVGProps } from './interfaces'
 | 
			
		||||
 | 
			
		||||
export const H3SVG: React.FC<SVGProps> = ({ color, height, width }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
      height={height}
 | 
			
		||||
      viewBox="0 96 960 960"
 | 
			
		||||
      width={width}
 | 
			
		||||
    >
 | 
			
		||||
      <path
 | 
			
		||||
        fill={color}
 | 
			
		||||
        d="M149.825 776Q137 776 128.5 767.375T120 746V406q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T180 406v140h180V406q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T420 406v340q0 12.75-8.675 21.375-8.676 8.625-21.5 8.625-12.825 0-21.325-8.625T360 746V606H180v140q0 12.75-8.675 21.375-8.676 8.625-21.5 8.625ZM570 776q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T570 716h210V606H650q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T650 546h130V436H570q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T570 376h210q24.75 0 42.375 17.625T840 436v280q0 24.75-17.625 42.375T780 776H570Z"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								src/assets/svgs/ItalicSVG.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,17 @@
 | 
			
		||||
import { SVGProps } from './interfaces'
 | 
			
		||||
 | 
			
		||||
export const ItalicSVG: React.FC<SVGProps> = ({ color, height, width }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
      height={height}
 | 
			
		||||
      viewBox="0 96 960 960"
 | 
			
		||||
      width={width}
 | 
			
		||||
    >
 | 
			
		||||
      <path
 | 
			
		||||
        fill={color}
 | 
			
		||||
        d="M264 857q-16.8 0-28.4-11.641-11.6-11.641-11.6-28.5t11.6-28.359Q247.2 777 264 777h94l139-409H378q-16.8 0-28.4-11.641-11.6-11.641-11.6-28.5t11.6-28.359Q361.2 288 378 288h300q16.8 0 28.4 11.641 11.6 11.641 11.6 28.5T706.4 356.5Q694.8 368 678 368h-94L445 777h119q16.8 0 28.4 11.641 11.6 11.641 11.6 28.5T592.4 845.5Q580.8 857 564 857H264Z"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								src/assets/svgs/LinkSVG.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,17 @@
 | 
			
		||||
import { SVGProps } from './interfaces'
 | 
			
		||||
 | 
			
		||||
export const LinkSVG: React.FC<SVGProps> = ({ color, height, width }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
      height={height}
 | 
			
		||||
      viewBox="0 96 960 960"
 | 
			
		||||
      width={width}
 | 
			
		||||
    >
 | 
			
		||||
      <path
 | 
			
		||||
        fill={color}
 | 
			
		||||
        d="M280 776q-85 0-142.5-57.5T80 576q0-85 57.5-142.5T280 376h140q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T420 436H280q-60 0-100 40t-40 100q0 60 40 100t100 40h140q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T420 776H280Zm75-170q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T355 546h250q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T605 606H355Zm185 170q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T540 716h140q60 0 100-40t40-100q0-60-40-100t-100-40H540q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T540 376h140q85 0 142.5 57.5T880 576q0 85-57.5 142.5T680 776H540Z"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										25
									
								
								src/assets/svgs/NewWindowSVG.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,25 @@
 | 
			
		||||
interface NewWindowSVGProps {
 | 
			
		||||
  color: string
 | 
			
		||||
  height: string
 | 
			
		||||
  width: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const NewWindowSVG: React.FC<NewWindowSVGProps> = ({
 | 
			
		||||
  color,
 | 
			
		||||
  height,
 | 
			
		||||
  width
 | 
			
		||||
}) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
      height={height}
 | 
			
		||||
      width={width}
 | 
			
		||||
      viewBox="0 96 960 960"
 | 
			
		||||
    >
 | 
			
		||||
      <path
 | 
			
		||||
        d="M180 936q-24 0-42-18t-18-42V276q0-24 18-42t42-18h300v60H180v600h600V576h60v300q0 24-18 42t-42 18H180Zm480-420V396H540v-60h120V216h60v120h120v60H720v120h-60Z"
 | 
			
		||||
        fill={color}
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								src/assets/svgs/UnderlineSVG.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,17 @@
 | 
			
		||||
import { SVGProps } from './interfaces'
 | 
			
		||||
 | 
			
		||||
export const UnderlineSVG: React.FC<SVGProps> = ({ color, height, width }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <svg
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
      height={height}
 | 
			
		||||
      viewBox="0 96 960 960"
 | 
			
		||||
      width={width}
 | 
			
		||||
    >
 | 
			
		||||
      <path
 | 
			
		||||
        fill={color}
 | 
			
		||||
        d="M230 916q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T230 856h500q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T730 916H230Zm250-140q-100 0-156.5-58.5T267 559V257q0-16.882 12.527-28.941Q292.055 216 309.027 216 326 216 338 228.059T350 257v302q0 63 34 101t96 38q62 0 96-38t34-101V257q0-16.882 12.527-28.941Q635.055 216 652.027 216 669 216 681 228.059T693 257v302q0 100-56.5 158.5T480 776Z"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								src/assets/svgs/accountCircle.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
			
		||||
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 96 960 960" width="48"><path d="M222 801q63-44 125-67.5T480 710q71 0 133.5 23.5T739 801q44-54 62.5-109T820 576q0-145-97.5-242.5T480 236q-145 0-242.5 97.5T140 576q0 61 19 116t63 109Zm257.814-195Q422 606 382.5 566.314q-39.5-39.686-39.5-97.5t39.686-97.314q39.686-39.5 97.5-39.5t97.314 39.686q39.5 39.686 39.5 97.5T577.314 566.5q-39.686 39.5-97.5 39.5Zm.654 370Q398 976 325 944.5q-73-31.5-127.5-86t-86-127.266Q80 658.468 80 575.734T111.5 420.5q31.5-72.5 86-127t127.266-86q72.766-31.5 155.5-31.5T635.5 207.5q72.5 31.5 127 86t86 127.032q31.5 72.532 31.5 155T848.5 731q-31.5 73-86 127.5t-127.032 86q-72.532 31.5-155 31.5ZM480 916q55 0 107.5-16T691 844q-51-36-104-55t-107-19q-54 0-107 19t-104 55q51 40 103.5 56T480 916Zm0-370q34 0 55.5-21.5T557 469q0-34-21.5-55.5T480 392q-34 0-55.5 21.5T403 469q0 34 21.5 55.5T480 546Zm0-77Zm0 374Z"/></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 896 B  | 
							
								
								
									
										5
									
								
								src/assets/svgs/interfaces.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,5 @@
 | 
			
		||||
export interface SVGProps {
 | 
			
		||||
  color: string
 | 
			
		||||
  height: string
 | 
			
		||||
  width: string
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										230
									
								
								src/components/AudioElement.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,230 @@
 | 
			
		||||
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 AudiotrackIcon from '@mui/icons-material/Audiotrack'
 | 
			
		||||
import { MyContext } from '../wrappers/DownloadWrapper'
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux'
 | 
			
		||||
import { RootState } from '../state/store'
 | 
			
		||||
import { CircularProgress } from '@mui/material'
 | 
			
		||||
import {
 | 
			
		||||
  setCurrAudio,
 | 
			
		||||
  setShowingAudioPlayer
 | 
			
		||||
} from '../state/features/globalSlice'
 | 
			
		||||
 | 
			
		||||
const Widget = styled('div')(({ theme }) => ({
 | 
			
		||||
  padding: 16,
 | 
			
		||||
  borderRadius: 16,
 | 
			
		||||
  maxWidth: '100%',
 | 
			
		||||
  position: 'relative',
 | 
			
		||||
  zIndex: 1,
 | 
			
		||||
  //   backgroundColor:
 | 
			
		||||
  //     theme.palette.mode === 'dark' ? 'rgba(0,0,0,0.6)' : 'rgba(255,255,255,0.4)',
 | 
			
		||||
  backdropFilter: 'blur(40px)',
 | 
			
		||||
  background: 'skyblue',
 | 
			
		||||
  transition: '0.2s all',
 | 
			
		||||
  '&:hover': {
 | 
			
		||||
    opacity: 0.75
 | 
			
		||||
  }
 | 
			
		||||
}))
 | 
			
		||||
 | 
			
		||||
const CoverImage = styled('div')({
 | 
			
		||||
  width: 100,
 | 
			
		||||
  height: 100,
 | 
			
		||||
  objectFit: 'cover',
 | 
			
		||||
  overflow: 'hidden',
 | 
			
		||||
  flexShrink: 0,
 | 
			
		||||
  borderRadius: 8,
 | 
			
		||||
  backgroundColor: 'rgba(0,0,0,0.08)',
 | 
			
		||||
  '& > img': {
 | 
			
		||||
    width: '100%'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const TinyText = styled(Typography)({
 | 
			
		||||
  fontSize: '0.75rem',
 | 
			
		||||
  opacity: 0.38,
 | 
			
		||||
  fontWeight: 500,
 | 
			
		||||
  letterSpacing: 0.2
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
interface IAudioElement {
 | 
			
		||||
  onClick: () => void
 | 
			
		||||
  title: string
 | 
			
		||||
  description: string
 | 
			
		||||
  author: string
 | 
			
		||||
  audioInfo?: any
 | 
			
		||||
  postId?: string
 | 
			
		||||
  user?: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function AudioElement({
 | 
			
		||||
  onClick,
 | 
			
		||||
  title,
 | 
			
		||||
  description,
 | 
			
		||||
  author,
 | 
			
		||||
  audioInfo,
 | 
			
		||||
  postId,
 | 
			
		||||
  user
 | 
			
		||||
}: IAudioElement) {
 | 
			
		||||
  const { downloadVideo } = React.useContext(MyContext)
 | 
			
		||||
  const [isLoading, setIsLoading] = React.useState<boolean>(false)
 | 
			
		||||
  const { downloads } = useSelector((state: RootState) => state.global)
 | 
			
		||||
  const reDownload = React.useRef<boolean>(false)
 | 
			
		||||
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
  const download = React.useMemo(() => {
 | 
			
		||||
    if (!downloads || !audioInfo?.identifier) return {}
 | 
			
		||||
    const findDownload = downloads[audioInfo?.identifier]
 | 
			
		||||
 | 
			
		||||
    if (!findDownload) return {}
 | 
			
		||||
    return findDownload
 | 
			
		||||
  }, [downloads, audioInfo])
 | 
			
		||||
 | 
			
		||||
  const resourceStatus = React.useMemo(() => {
 | 
			
		||||
    return download?.status || {}
 | 
			
		||||
  }, [download])
 | 
			
		||||
  const handlePlay = () => {
 | 
			
		||||
    if (!postId) return
 | 
			
		||||
    const { name, service, identifier } = audioInfo
 | 
			
		||||
 | 
			
		||||
    if (download && resourceStatus?.status === 'READY') {
 | 
			
		||||
      dispatch(setShowingAudioPlayer(true))
 | 
			
		||||
      dispatch(setCurrAudio(identifier))
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    setIsLoading(true)
 | 
			
		||||
    downloadVideo({
 | 
			
		||||
      name,
 | 
			
		||||
      service,
 | 
			
		||||
      identifier,
 | 
			
		||||
      blogPost: {
 | 
			
		||||
        postId,
 | 
			
		||||
        user,
 | 
			
		||||
        audioTitle: title,
 | 
			
		||||
        audioDescription: description,
 | 
			
		||||
        audioAuthor: author
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    dispatch(setCurrAudio(identifier))
 | 
			
		||||
    dispatch(setShowingAudioPlayer(true))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    if (resourceStatus?.status === 'READY') {
 | 
			
		||||
      setIsLoading(false)
 | 
			
		||||
    }
 | 
			
		||||
  }, [resourceStatus])
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    if (
 | 
			
		||||
      resourceStatus?.status === 'DOWNLOADED' &&
 | 
			
		||||
      reDownload?.current === false
 | 
			
		||||
    ) {
 | 
			
		||||
      handlePlay()
 | 
			
		||||
      reDownload.current = true
 | 
			
		||||
    }
 | 
			
		||||
  }, [handlePlay, resourceStatus])
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
      onClick={handlePlay}
 | 
			
		||||
      sx={{
 | 
			
		||||
        width: '100%',
 | 
			
		||||
        overflow: 'hidden',
 | 
			
		||||
        position: 'relative',
 | 
			
		||||
        cursor: 'pointer'
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Widget>
 | 
			
		||||
        <Box sx={{ display: 'flex', alignItems: 'center' }}>
 | 
			
		||||
          <CoverImage>
 | 
			
		||||
            <AudiotrackIcon
 | 
			
		||||
              sx={{
 | 
			
		||||
                width: '90%',
 | 
			
		||||
                height: 'auto'
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          </CoverImage>
 | 
			
		||||
          <Box sx={{ ml: 1.5, minWidth: 0 }}>
 | 
			
		||||
            <Typography
 | 
			
		||||
              variant="caption"
 | 
			
		||||
              color="text.secondary"
 | 
			
		||||
              fontWeight={500}
 | 
			
		||||
            >
 | 
			
		||||
              {author}
 | 
			
		||||
            </Typography>
 | 
			
		||||
            <Typography noWrap>
 | 
			
		||||
              <b>{title}</b>
 | 
			
		||||
            </Typography>
 | 
			
		||||
            <Typography noWrap letterSpacing={-0.25}>
 | 
			
		||||
              {description}
 | 
			
		||||
            </Typography>
 | 
			
		||||
          </Box>
 | 
			
		||||
        </Box>
 | 
			
		||||
        {((resourceStatus.status && resourceStatus?.status !== 'READY') ||
 | 
			
		||||
          isLoading) && (
 | 
			
		||||
          <Box
 | 
			
		||||
            position="absolute"
 | 
			
		||||
            top={0}
 | 
			
		||||
            left={0}
 | 
			
		||||
            right={0}
 | 
			
		||||
            bottom={0}
 | 
			
		||||
            display="flex"
 | 
			
		||||
            justifyContent="center"
 | 
			
		||||
            alignItems="center"
 | 
			
		||||
            zIndex={4999}
 | 
			
		||||
            bgcolor="rgba(0, 0, 0, 0.6)"
 | 
			
		||||
            sx={{
 | 
			
		||||
              display: 'flex',
 | 
			
		||||
              flexDirection: 'column',
 | 
			
		||||
              gap: '10px',
 | 
			
		||||
              padding: '16px',
 | 
			
		||||
              borderRadius: '16px'
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <CircularProgress color="secondary" />
 | 
			
		||||
            {resourceStatus && (
 | 
			
		||||
              <Typography
 | 
			
		||||
                variant="subtitle2"
 | 
			
		||||
                component="div"
 | 
			
		||||
                sx={{
 | 
			
		||||
                  color: 'white',
 | 
			
		||||
                  fontSize: '14px'
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                {resourceStatus?.status === 'REFETCHING' ? (
 | 
			
		||||
                  <>
 | 
			
		||||
                    <>
 | 
			
		||||
                      {(
 | 
			
		||||
                        (resourceStatus?.localChunkCount /
 | 
			
		||||
                          resourceStatus?.totalChunkCount) *
 | 
			
		||||
                        100
 | 
			
		||||
                      )?.toFixed(0)}
 | 
			
		||||
                      %
 | 
			
		||||
                    </>
 | 
			
		||||
 | 
			
		||||
                    <> Refetching in 25 seconds</>
 | 
			
		||||
                  </>
 | 
			
		||||
                ) : resourceStatus?.status === 'DOWNLOADED' ? (
 | 
			
		||||
                  <>Download Completed: building audio...</>
 | 
			
		||||
                ) : resourceStatus?.status !== 'READY' ? (
 | 
			
		||||
                  <>
 | 
			
		||||
                    {(
 | 
			
		||||
                      (resourceStatus?.localChunkCount /
 | 
			
		||||
                        resourceStatus?.totalChunkCount) *
 | 
			
		||||
                      100
 | 
			
		||||
                    )?.toFixed(0)}
 | 
			
		||||
                    %
 | 
			
		||||
                  </>
 | 
			
		||||
                ) : (
 | 
			
		||||
                  <>Download Completed: fetching audio...</>
 | 
			
		||||
                )}
 | 
			
		||||
              </Typography>
 | 
			
		||||
            )}
 | 
			
		||||
          </Box>
 | 
			
		||||
        )}
 | 
			
		||||
      </Widget>
 | 
			
		||||
    </Box>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										96
									
								
								src/components/DynamicHeightItem.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,96 @@
 | 
			
		||||
import React, { useRef, useState, useEffect } from 'react'
 | 
			
		||||
import ReactResizeDetector from 'react-resize-detector'
 | 
			
		||||
import { Layouts, Layout } from 'react-grid-layout'
 | 
			
		||||
 | 
			
		||||
interface DynamicHeightItemProps {
 | 
			
		||||
  children: React.ReactNode
 | 
			
		||||
  layouts: Layouts
 | 
			
		||||
  setLayouts: (layouts: any) => void
 | 
			
		||||
  i: string
 | 
			
		||||
  breakpoint: keyof Layouts
 | 
			
		||||
  rows?: number
 | 
			
		||||
  count?: number
 | 
			
		||||
  type?: string
 | 
			
		||||
  padding?: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const DynamicHeightItem: React.FC<DynamicHeightItemProps> = ({
 | 
			
		||||
  children,
 | 
			
		||||
  layouts,
 | 
			
		||||
  setLayouts,
 | 
			
		||||
  i,
 | 
			
		||||
  breakpoint,
 | 
			
		||||
  rows = 1,
 | 
			
		||||
  count,
 | 
			
		||||
  type,
 | 
			
		||||
  padding
 | 
			
		||||
}) => {
 | 
			
		||||
  const [height, setHeight] = useState<number>(rows * 150)
 | 
			
		||||
  const ref = useRef<HTMLDivElement>(null)
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (ref.current) {
 | 
			
		||||
      setHeight(ref.current.clientHeight)
 | 
			
		||||
    }
 | 
			
		||||
  }, [ref.current])
 | 
			
		||||
 | 
			
		||||
  const onResize = () => {
 | 
			
		||||
    if (ref.current) {
 | 
			
		||||
      setHeight(ref.current.clientHeight)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getBreakpoint = (screenWidth: number) => {
 | 
			
		||||
    if (screenWidth >= 996) {
 | 
			
		||||
      return 'md'
 | 
			
		||||
    } else if (screenWidth >= 768) {
 | 
			
		||||
      return 'sm'
 | 
			
		||||
    } else {
 | 
			
		||||
      return 'xs'
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const widthWin = window.innerWidth
 | 
			
		||||
    let newBreakpoint = breakpoint
 | 
			
		||||
    // if (!newBreakpoint) {
 | 
			
		||||
    //   newBreakpoint = getBreakpoint(widthWin)
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    setLayouts((prev: any) => {
 | 
			
		||||
      const newLayouts: any = { ...prev }
 | 
			
		||||
      newLayouts[newBreakpoint] = newLayouts[newBreakpoint]?.map(
 | 
			
		||||
        (item: Layout) => {
 | 
			
		||||
          if (item.i === i) {
 | 
			
		||||
            let constantNum = 25
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
              ...item,
 | 
			
		||||
              h: Math.ceil(height / (rows * constantNum)) // Adjust this value based on your rowHeight and the number of rows the element spans
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          return item
 | 
			
		||||
        }
 | 
			
		||||
      )
 | 
			
		||||
      return newLayouts
 | 
			
		||||
    })
 | 
			
		||||
  }, [height, breakpoint, count, setLayouts])
 | 
			
		||||
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div ref={ref} style={{ width: '100%', height: 'auto' }}>
 | 
			
		||||
      <ReactResizeDetector handleHeight onResize={onResize}>
 | 
			
		||||
        <div
 | 
			
		||||
          style={{
 | 
			
		||||
            padding: `${padding ? padding : 0}px`
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {children}
 | 
			
		||||
        </div>
 | 
			
		||||
      </ReactResizeDetector>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default DynamicHeightItem
 | 
			
		||||
							
								
								
									
										39
									
								
								src/components/DynamicHeightItemMinimal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,39 @@
 | 
			
		||||
import React, { useRef, useState, useEffect } from 'react'
 | 
			
		||||
import ReactResizeDetector from 'react-resize-detector'
 | 
			
		||||
import { Layouts, Layout } from 'react-grid-layout'
 | 
			
		||||
 | 
			
		||||
interface DynamicHeightItemProps {
 | 
			
		||||
  children: React.ReactNode
 | 
			
		||||
  layouts: Layouts
 | 
			
		||||
  setLayouts: (layouts: any) => void
 | 
			
		||||
  i: string
 | 
			
		||||
  breakpoint: keyof Layouts
 | 
			
		||||
  rows?: number
 | 
			
		||||
  count?: number
 | 
			
		||||
  type?: string
 | 
			
		||||
  padding?: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const DynamicHeightItemMinimal: React.FC<DynamicHeightItemProps> = ({
 | 
			
		||||
  children,
 | 
			
		||||
  layouts,
 | 
			
		||||
  setLayouts,
 | 
			
		||||
  i,
 | 
			
		||||
  breakpoint,
 | 
			
		||||
  rows = 1,
 | 
			
		||||
  count,
 | 
			
		||||
  type,
 | 
			
		||||
  padding
 | 
			
		||||
}) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <div style={{ width: '100%', height: 'auto' }}>
 | 
			
		||||
      <div
 | 
			
		||||
        style={{
 | 
			
		||||
          padding: `${padding ? padding : 0}px`
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {children}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										445
									
								
								src/components/FileElement.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,445 @@
 | 
			
		||||
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 AudiotrackIcon from '@mui/icons-material/Audiotrack'
 | 
			
		||||
import { MyContext } from '../wrappers/DownloadWrapper'
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux'
 | 
			
		||||
import { RootState } from '../state/store'
 | 
			
		||||
import { CircularProgress } from '@mui/material'
 | 
			
		||||
import AttachFileIcon from '@mui/icons-material/AttachFile'
 | 
			
		||||
import {
 | 
			
		||||
  setCurrAudio,
 | 
			
		||||
  setShowingAudioPlayer
 | 
			
		||||
} from '../state/features/globalSlice'
 | 
			
		||||
import {
 | 
			
		||||
  base64ToUint8Array,
 | 
			
		||||
  objectToUint8ArrayFromResponse
 | 
			
		||||
} from '../utils/toBase64'
 | 
			
		||||
import { setNotification } from '../state/features/notificationsSlice'
 | 
			
		||||
 | 
			
		||||
const Widget = styled('div')(({ theme }) => ({
 | 
			
		||||
  padding: 8,
 | 
			
		||||
  borderRadius: 10,
 | 
			
		||||
  maxWidth: 350,
 | 
			
		||||
  position: 'relative',
 | 
			
		||||
  zIndex: 1,
 | 
			
		||||
  //   backgroundColor:
 | 
			
		||||
  //     theme.palette.mode === 'dark' ? 'rgba(0,0,0,0.6)' : 'rgba(255,255,255,0.4)',
 | 
			
		||||
  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%'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const TinyText = styled(Typography)({
 | 
			
		||||
  fontSize: '0.75rem',
 | 
			
		||||
  opacity: 0.38,
 | 
			
		||||
  fontWeight: 500,
 | 
			
		||||
  letterSpacing: 0.2
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
interface IAudioElement {
 | 
			
		||||
  title: string
 | 
			
		||||
  description?: string
 | 
			
		||||
  author?: string
 | 
			
		||||
  fileInfo?: any
 | 
			
		||||
  postId?: string
 | 
			
		||||
  user?: string
 | 
			
		||||
  children?: React.ReactNode
 | 
			
		||||
  mimeType?: string
 | 
			
		||||
  disable?: boolean
 | 
			
		||||
  mode?: string
 | 
			
		||||
  otherUser?: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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,
 | 
			
		||||
  postId = '',
 | 
			
		||||
  user,
 | 
			
		||||
  children,
 | 
			
		||||
  mimeType,
 | 
			
		||||
  disable,
 | 
			
		||||
  mode,
 | 
			
		||||
  otherUser
 | 
			
		||||
}: IAudioElement) {
 | 
			
		||||
  const { downloadVideo } = React.useContext(MyContext)
 | 
			
		||||
  const [isLoading, setIsLoading] = React.useState<boolean>(false)
 | 
			
		||||
  const [fileProperties, setFileProperties] = React.useState<any>(null)
 | 
			
		||||
  const [downloadLoader, setDownloadLoader] = React.useState<any>(false)
 | 
			
		||||
 | 
			
		||||
  const [pdfSrc, setPdfSrc] = React.useState('')
 | 
			
		||||
  const { downloads } = useSelector((state: RootState) => state.global)
 | 
			
		||||
  const { user: username } = useSelector((state: RootState) => state.auth)
 | 
			
		||||
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
  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 saveFileToDisk = async (blob: any, fileName: any) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const fileHandle = await customWindow.showSaveFilePicker({
 | 
			
		||||
        suggestedName: fileName,
 | 
			
		||||
        types: [
 | 
			
		||||
          {
 | 
			
		||||
            description: 'File'
 | 
			
		||||
          }
 | 
			
		||||
        ]
 | 
			
		||||
      })
 | 
			
		||||
      const writeFile = async (fileHandle: any, contents: any) => {
 | 
			
		||||
        const writable = await fileHandle.createWritable()
 | 
			
		||||
        await writable.write(contents)
 | 
			
		||||
        await writable.close()
 | 
			
		||||
      }
 | 
			
		||||
      writeFile(fileHandle, blob).then(() => console.log('FILE SAVED'))
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.log(error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const handlePlay = async () => {
 | 
			
		||||
    if (disable) return
 | 
			
		||||
    if (
 | 
			
		||||
      resourceStatus?.status === 'READY' &&
 | 
			
		||||
      download?.url &&
 | 
			
		||||
      download?.blogPost?.filename
 | 
			
		||||
    ) {
 | 
			
		||||
      if (downloadLoader) return
 | 
			
		||||
      dispatch(
 | 
			
		||||
        setNotification({
 | 
			
		||||
          msg: 'Saving file... please wait',
 | 
			
		||||
          alertType: 'info'
 | 
			
		||||
        })
 | 
			
		||||
      )
 | 
			
		||||
      try {
 | 
			
		||||
        const { name, service, identifier } = fileInfo
 | 
			
		||||
 | 
			
		||||
        setDownloadLoader(true)
 | 
			
		||||
        const url = `/arbitrary/${service}/${name}/${identifier}`
 | 
			
		||||
        fetch(url)
 | 
			
		||||
          .then((response) => response.blob())
 | 
			
		||||
          .then(async (blob) => {
 | 
			
		||||
            await qortalRequest({
 | 
			
		||||
              action: 'SAVE_FILE',
 | 
			
		||||
              blob,
 | 
			
		||||
              filename: download?.blogPost?.filename,
 | 
			
		||||
              mimeType: download?.blogPost?.mimeType || ''
 | 
			
		||||
            })
 | 
			
		||||
            // saveAs(blob, download?.blogPost?.filename)
 | 
			
		||||
          })
 | 
			
		||||
          .catch((error) => {
 | 
			
		||||
            console.error('Error fetching the video:', error)
 | 
			
		||||
            // clearInterval(intervalId)
 | 
			
		||||
          })
 | 
			
		||||
          .finally(() => {
 | 
			
		||||
            setDownloadLoader(false)
 | 
			
		||||
          })
 | 
			
		||||
      } catch (error: any) {
 | 
			
		||||
        let notificationObj = 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 {
 | 
			
		||||
        if (mode === 'mail') {
 | 
			
		||||
          setDownloadLoader(false)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (!postId) return
 | 
			
		||||
    const { name, service, identifier } = fileInfo
 | 
			
		||||
    let filename = fileProperties?.filename
 | 
			
		||||
    let mimeType = fileProperties?.mimeType
 | 
			
		||||
    if (!fileProperties) {
 | 
			
		||||
      try {
 | 
			
		||||
        dispatch(
 | 
			
		||||
          setNotification({
 | 
			
		||||
            msg: 'Downloading file... please wait',
 | 
			
		||||
            alertType: 'info'
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
        let res = await qortalRequest({
 | 
			
		||||
          action: 'GET_QDN_RESOURCE_PROPERTIES',
 | 
			
		||||
          name: name,
 | 
			
		||||
          service: service,
 | 
			
		||||
          identifier: identifier
 | 
			
		||||
        })
 | 
			
		||||
        setFileProperties(res)
 | 
			
		||||
        filename = res?.filename
 | 
			
		||||
        mimeType = res?.mimeType
 | 
			
		||||
      } catch (error: any) {
 | 
			
		||||
        dispatch(
 | 
			
		||||
          setNotification({
 | 
			
		||||
            msg: error?.message || 'Error with download. Please try again',
 | 
			
		||||
            alertType: 'error'
 | 
			
		||||
          })
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (!filename) return
 | 
			
		||||
 | 
			
		||||
    setIsLoading(true)
 | 
			
		||||
    downloadVideo({
 | 
			
		||||
      name,
 | 
			
		||||
      service,
 | 
			
		||||
      identifier,
 | 
			
		||||
      blogPost: {
 | 
			
		||||
        postId,
 | 
			
		||||
        user,
 | 
			
		||||
        audioTitle: title,
 | 
			
		||||
        audioDescription: description,
 | 
			
		||||
        audioAuthor: author,
 | 
			
		||||
        filename,
 | 
			
		||||
        mimeType
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    if (
 | 
			
		||||
      resourceStatus?.status === 'READY' &&
 | 
			
		||||
      download?.url &&
 | 
			
		||||
      download?.blogPost?.filename
 | 
			
		||||
    ) {
 | 
			
		||||
      setIsLoading(false)
 | 
			
		||||
    }
 | 
			
		||||
  }, [resourceStatus, download])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
      onClick={handlePlay}
 | 
			
		||||
      sx={{
 | 
			
		||||
        width: '100%',
 | 
			
		||||
        overflow: 'hidden',
 | 
			
		||||
        position: 'relative',
 | 
			
		||||
        cursor: 'pointer'
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {children && (
 | 
			
		||||
        <Box
 | 
			
		||||
          sx={{
 | 
			
		||||
            display: 'flex',
 | 
			
		||||
            alignItems: 'center',
 | 
			
		||||
            position: 'relative',
 | 
			
		||||
            gap: '7px'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {children}{' '}
 | 
			
		||||
          {(resourceStatus.status && resourceStatus?.status !== 'READY') ||
 | 
			
		||||
          isLoading ? (
 | 
			
		||||
            <CircularProgress color="secondary" size={14} />
 | 
			
		||||
          ) : resourceStatus?.status === 'READY' ? (
 | 
			
		||||
            <>
 | 
			
		||||
              <Typography
 | 
			
		||||
                sx={{
 | 
			
		||||
                  fontSize: '14px'
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                Ready to save: click here
 | 
			
		||||
              </Typography>
 | 
			
		||||
              {downloadLoader && (
 | 
			
		||||
                <CircularProgress color="secondary" size={14} />
 | 
			
		||||
              )}
 | 
			
		||||
            </>
 | 
			
		||||
          ) : null}
 | 
			
		||||
        </Box>
 | 
			
		||||
      )}
 | 
			
		||||
      {!children && (
 | 
			
		||||
        <Widget>
 | 
			
		||||
          <Box sx={{ display: 'flex', alignItems: 'center' }}>
 | 
			
		||||
            <CoverImage>
 | 
			
		||||
              <AttachFileIcon
 | 
			
		||||
                sx={{
 | 
			
		||||
                  width: '90%',
 | 
			
		||||
                  height: 'auto'
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
            </CoverImage>
 | 
			
		||||
            <Box sx={{ ml: 1.5, minWidth: 0 }}>
 | 
			
		||||
              <Typography
 | 
			
		||||
                variant="caption"
 | 
			
		||||
                color="text.secondary"
 | 
			
		||||
                fontWeight={500}
 | 
			
		||||
              >
 | 
			
		||||
                {author}
 | 
			
		||||
              </Typography>
 | 
			
		||||
              <Typography
 | 
			
		||||
                noWrap
 | 
			
		||||
                sx={{
 | 
			
		||||
                  fontSize: '16px'
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <b>{title}</b>
 | 
			
		||||
              </Typography>
 | 
			
		||||
              <Typography
 | 
			
		||||
                noWrap
 | 
			
		||||
                letterSpacing={-0.25}
 | 
			
		||||
                sx={{
 | 
			
		||||
                  fontSize: '14px'
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                {description}
 | 
			
		||||
              </Typography>
 | 
			
		||||
              {mimeType && (
 | 
			
		||||
                <Typography
 | 
			
		||||
                  noWrap
 | 
			
		||||
                  letterSpacing={-0.25}
 | 
			
		||||
                  sx={{
 | 
			
		||||
                    fontSize: '12px'
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  {mimeType}
 | 
			
		||||
                </Typography>
 | 
			
		||||
              )}
 | 
			
		||||
            </Box>
 | 
			
		||||
          </Box>
 | 
			
		||||
          {((resourceStatus.status && resourceStatus?.status !== 'READY') ||
 | 
			
		||||
            isLoading) && (
 | 
			
		||||
            <Box
 | 
			
		||||
              position="absolute"
 | 
			
		||||
              top={0}
 | 
			
		||||
              left={0}
 | 
			
		||||
              right={0}
 | 
			
		||||
              bottom={0}
 | 
			
		||||
              display="flex"
 | 
			
		||||
              justifyContent="center"
 | 
			
		||||
              alignItems="center"
 | 
			
		||||
              zIndex={4999}
 | 
			
		||||
              bgcolor="rgba(0, 0, 0, 0.6)"
 | 
			
		||||
              sx={{
 | 
			
		||||
                display: 'flex',
 | 
			
		||||
                flexDirection: 'column',
 | 
			
		||||
                gap: '10px',
 | 
			
		||||
                padding: '8px',
 | 
			
		||||
                borderRadius: '10px'
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <CircularProgress color="secondary" />
 | 
			
		||||
              {resourceStatus && (
 | 
			
		||||
                <Typography
 | 
			
		||||
                  variant="subtitle2"
 | 
			
		||||
                  component="div"
 | 
			
		||||
                  sx={{
 | 
			
		||||
                    color: 'white',
 | 
			
		||||
                    fontSize: '14px'
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  {resourceStatus?.status === 'REFETCHING' ? (
 | 
			
		||||
                    <>
 | 
			
		||||
                      <>
 | 
			
		||||
                        {(
 | 
			
		||||
                          (resourceStatus?.localChunkCount /
 | 
			
		||||
                            resourceStatus?.totalChunkCount) *
 | 
			
		||||
                          100
 | 
			
		||||
                        )?.toFixed(0)}
 | 
			
		||||
                        %
 | 
			
		||||
                      </>
 | 
			
		||||
 | 
			
		||||
                      <> Refetching in 2 minutes</>
 | 
			
		||||
                    </>
 | 
			
		||||
                  ) : resourceStatus?.status === 'DOWNLOADED' ? (
 | 
			
		||||
                    <>Download Completed: building file...</>
 | 
			
		||||
                  ) : resourceStatus?.status !== 'READY' ? (
 | 
			
		||||
                    <>
 | 
			
		||||
                      {(
 | 
			
		||||
                        (resourceStatus?.localChunkCount /
 | 
			
		||||
                          resourceStatus?.totalChunkCount) *
 | 
			
		||||
                        100
 | 
			
		||||
                      )?.toFixed(0)}
 | 
			
		||||
                      %
 | 
			
		||||
                    </>
 | 
			
		||||
                  ) : (
 | 
			
		||||
                    <>Download Completed: fetching file...</>
 | 
			
		||||
                  )}
 | 
			
		||||
                </Typography>
 | 
			
		||||
              )}
 | 
			
		||||
            </Box>
 | 
			
		||||
          )}
 | 
			
		||||
          {resourceStatus?.status === 'READY' &&
 | 
			
		||||
            download?.url &&
 | 
			
		||||
            download?.blogPost?.filename && (
 | 
			
		||||
              <Box
 | 
			
		||||
                position="absolute"
 | 
			
		||||
                top={0}
 | 
			
		||||
                left={0}
 | 
			
		||||
                right={0}
 | 
			
		||||
                bottom={0}
 | 
			
		||||
                display="flex"
 | 
			
		||||
                justifyContent="center"
 | 
			
		||||
                alignItems="center"
 | 
			
		||||
                zIndex={4999}
 | 
			
		||||
                bgcolor="rgba(0, 0, 0, 0.6)"
 | 
			
		||||
                sx={{
 | 
			
		||||
                  display: 'flex',
 | 
			
		||||
                  flexDirection: 'row',
 | 
			
		||||
                  gap: '10px',
 | 
			
		||||
                  padding: '8px',
 | 
			
		||||
                  borderRadius: '10px'
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Typography
 | 
			
		||||
                  variant="subtitle2"
 | 
			
		||||
                  component="div"
 | 
			
		||||
                  sx={{
 | 
			
		||||
                    color: 'white',
 | 
			
		||||
                    fontSize: '14px'
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  Ready to save: click here
 | 
			
		||||
                </Typography>
 | 
			
		||||
                {downloadLoader && (
 | 
			
		||||
                  <CircularProgress color="secondary" size={14} />
 | 
			
		||||
                )}
 | 
			
		||||
              </Box>
 | 
			
		||||
            )}
 | 
			
		||||
        </Widget>
 | 
			
		||||
      )}
 | 
			
		||||
    </Box>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										253
									
								
								src/components/common/AudioPanel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,253 @@
 | 
			
		||||
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 [editVideoIdentifier, setEditVideoIdentifier] = useState<
 | 
			
		||||
    string | null | undefined
 | 
			
		||||
  >()
 | 
			
		||||
 | 
			
		||||
  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
 | 
			
		||||
      // })
 | 
			
		||||
      const res2 = await fetch(
 | 
			
		||||
        `/arbitrary/resources?&service=AUDIO&name=${user.name}&includemetadata=true&limit=100&offset=0&reverse=true`
 | 
			
		||||
      )
 | 
			
		||||
      const resData = await res2.json()
 | 
			
		||||
      if (Array.isArray(resData)) {
 | 
			
		||||
        res = resData
 | 
			
		||||
      }
 | 
			
		||||
    } 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>
 | 
			
		||||
                <Button
 | 
			
		||||
                  size="small"
 | 
			
		||||
                  variant="contained"
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    setEditVideoIdentifier(video.identifier)
 | 
			
		||||
                    setIsOpenVideoModal(true)
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  Edit
 | 
			
		||||
                </Button>
 | 
			
		||||
              </ListItem>
 | 
			
		||||
            ))}
 | 
			
		||||
          </List>
 | 
			
		||||
          <Box
 | 
			
		||||
            sx={{
 | 
			
		||||
              width: '100%',
 | 
			
		||||
              display: 'flex',
 | 
			
		||||
              justifyContent: 'center',
 | 
			
		||||
              flex: '0 0 50px'
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <PublishButton
 | 
			
		||||
              variant="contained"
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                setEditVideoIdentifier(null)
 | 
			
		||||
                setIsOpenVideoModal(true)
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              Publish new audio file
 | 
			
		||||
            </PublishButton>
 | 
			
		||||
          </Box>
 | 
			
		||||
        </Panel>
 | 
			
		||||
      </Drawer>
 | 
			
		||||
      <AudioModal
 | 
			
		||||
        onClose={() => {
 | 
			
		||||
          setIsOpenVideoModal(false)
 | 
			
		||||
          setEditVideoIdentifier(null)
 | 
			
		||||
        }}
 | 
			
		||||
        open={isOpenVideoModal}
 | 
			
		||||
        onPublish={(value) => {
 | 
			
		||||
          fetchVideos().then((fetchedVideos) => setVideos(fetchedVideos))
 | 
			
		||||
          setIsOpenVideoModal(false)
 | 
			
		||||
        }}
 | 
			
		||||
        editVideoIdentifier={editVideoIdentifier}
 | 
			
		||||
      />
 | 
			
		||||
    </Box>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Add this to your 'types.ts' file
 | 
			
		||||
export interface Video {
 | 
			
		||||
  name: string
 | 
			
		||||
  service: string
 | 
			
		||||
  identifier: string
 | 
			
		||||
  metadata: {
 | 
			
		||||
    title: string
 | 
			
		||||
    description: string
 | 
			
		||||
    tags: string[]
 | 
			
		||||
    category: string
 | 
			
		||||
    categoryName: string
 | 
			
		||||
  }
 | 
			
		||||
  size: number
 | 
			
		||||
  created: number
 | 
			
		||||
  updated: number
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										192
									
								
								src/components/common/AudioPlayer.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,192 @@
 | 
			
		||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
 | 
			
		||||
import { Box, IconButton, Slider } from '@mui/material'
 | 
			
		||||
import { CircularProgress, Typography } from '@mui/material'
 | 
			
		||||
import AudioPlyr from 'philliplm-react-modern-audio-player'
 | 
			
		||||
import LinearProgress from '@mui/material/LinearProgress'
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  PlayArrow,
 | 
			
		||||
  Pause,
 | 
			
		||||
  VolumeUp,
 | 
			
		||||
  Fullscreen,
 | 
			
		||||
  PictureInPicture
 | 
			
		||||
} from '@mui/icons-material'
 | 
			
		||||
import { styled } from '@mui/system'
 | 
			
		||||
import {
 | 
			
		||||
  removeAudio,
 | 
			
		||||
  setShowingAudioPlayer
 | 
			
		||||
} from '../../state/features/globalSlice'
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux'
 | 
			
		||||
import { RootState } from '../../state/store'
 | 
			
		||||
 | 
			
		||||
const VideoContainer = styled(Box)`
 | 
			
		||||
  position: relative;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  margin: 20px 0px;
 | 
			
		||||
  z-index: 501;
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
const VideoElement = styled('video')`
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: auto;
 | 
			
		||||
  background: rgb(33, 33, 33);
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
const ControlsContainer = styled(Box)`
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  padding: 8px;
 | 
			
		||||
  background-color: rgba(0, 0, 0, 0.6);
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
interface VideoPlayerProps {
 | 
			
		||||
  src?: string
 | 
			
		||||
  poster?: string
 | 
			
		||||
  name?: string
 | 
			
		||||
  identifier?: string
 | 
			
		||||
  service?: string
 | 
			
		||||
  autoplay?: boolean
 | 
			
		||||
  title?: string
 | 
			
		||||
  description?: string
 | 
			
		||||
  playlist?: IPlaylist[]
 | 
			
		||||
  currAudio: number | null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IPlaylist {
 | 
			
		||||
  name: string
 | 
			
		||||
  identifier: string
 | 
			
		||||
  service: string
 | 
			
		||||
  title: string
 | 
			
		||||
  description: string
 | 
			
		||||
}
 | 
			
		||||
interface CustomWindow extends Window {
 | 
			
		||||
  _qdnTheme: any // Replace 'any' with the appropriate type if you know it
 | 
			
		||||
}
 | 
			
		||||
const customWindow = window as unknown as CustomWindow
 | 
			
		||||
const themeColor = customWindow?._qdnTheme
 | 
			
		||||
 | 
			
		||||
export const AudioPlayer: React.FC<VideoPlayerProps> = ({ currAudio }) => {
 | 
			
		||||
  const [isLoading, setIsLoading] = useState<boolean>(false)
 | 
			
		||||
  const { downloads, showingAudioPlayer } = useSelector(
 | 
			
		||||
    (state: RootState) => state.global
 | 
			
		||||
  )
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
  const downloadsLength: number = useMemo(
 | 
			
		||||
    () =>
 | 
			
		||||
      Object.keys(downloads)
 | 
			
		||||
        .map((item) => {
 | 
			
		||||
          return downloads[item]
 | 
			
		||||
        })
 | 
			
		||||
        .filter(
 | 
			
		||||
          (download: any) =>
 | 
			
		||||
            download?.service === 'AUDIO' &&
 | 
			
		||||
            download?.status?.status === 'READY' &&
 | 
			
		||||
            !!download.url
 | 
			
		||||
        ).length,
 | 
			
		||||
    [downloads]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const audioPlayList = useMemo(() => {
 | 
			
		||||
    const filterAudios = Object.keys(downloads)
 | 
			
		||||
      .map((item) => {
 | 
			
		||||
        return downloads[item]
 | 
			
		||||
      })
 | 
			
		||||
      .filter(
 | 
			
		||||
        (download: any) =>
 | 
			
		||||
          download?.service === 'AUDIO' &&
 | 
			
		||||
          download?.url &&
 | 
			
		||||
          download?.status?.status === 'READY'
 | 
			
		||||
      )
 | 
			
		||||
    return filterAudios.map((audio: any, index: number) => {
 | 
			
		||||
      return {
 | 
			
		||||
        name: audio?.blogPost?.audioTitle,
 | 
			
		||||
        src: audio?.url,
 | 
			
		||||
        id: index + 1,
 | 
			
		||||
        identifier: audio?.identifier,
 | 
			
		||||
        description: audio?.blogPost?.audioDescription || ''
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }, [downloadsLength])
 | 
			
		||||
 | 
			
		||||
  const currAudioMemo: number | null = useMemo(() => {
 | 
			
		||||
    const findIndex = audioPlayList.findIndex(
 | 
			
		||||
      (item) => item?.identifier === currAudio
 | 
			
		||||
    )
 | 
			
		||||
    if (findIndex !== -1) {
 | 
			
		||||
      return findIndex
 | 
			
		||||
    }
 | 
			
		||||
    return null
 | 
			
		||||
  }, [audioPlayList, currAudio])
 | 
			
		||||
 | 
			
		||||
  if (isLoading)
 | 
			
		||||
    return (
 | 
			
		||||
      <Box
 | 
			
		||||
        sx={{
 | 
			
		||||
          isolation: 'isolate',
 | 
			
		||||
          width: '100%',
 | 
			
		||||
          position: 'fixed',
 | 
			
		||||
          colorScheme: 'light',
 | 
			
		||||
          bottom: '0px',
 | 
			
		||||
          padding: '10px',
 | 
			
		||||
          height: '50px',
 | 
			
		||||
          display: 'flex',
 | 
			
		||||
          flexDirection: 'column',
 | 
			
		||||
          justifyContent: 'center',
 | 
			
		||||
          alignItems: 'flex-start'
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Typography
 | 
			
		||||
          sx={{
 | 
			
		||||
            fontSize: '10px'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          Loading playlist...
 | 
			
		||||
        </Typography>
 | 
			
		||||
        <LinearProgress
 | 
			
		||||
          sx={{
 | 
			
		||||
            width: '100%'
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      </Box>
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
  if (audioPlayList.length === 0 || !showingAudioPlayer) return null
 | 
			
		||||
  return (
 | 
			
		||||
    <VideoContainer>
 | 
			
		||||
      <AudioPlyr
 | 
			
		||||
        rootContainerProps={{
 | 
			
		||||
          defaultColorScheme: themeColor === 'dark' ? 'dark' : 'light',
 | 
			
		||||
          colorScheme: themeColor === 'dark' ? 'dark' : 'light'
 | 
			
		||||
        }}
 | 
			
		||||
        currentIndex={currAudioMemo}
 | 
			
		||||
        playList={audioPlayList}
 | 
			
		||||
        activeUI={{
 | 
			
		||||
          all: true
 | 
			
		||||
        }}
 | 
			
		||||
        placement={{
 | 
			
		||||
          player: 'bottom',
 | 
			
		||||
 | 
			
		||||
          playList: 'top',
 | 
			
		||||
          volumeSlider: 'top'
 | 
			
		||||
        }}
 | 
			
		||||
        closeCallback={() => {
 | 
			
		||||
          dispatch(setShowingAudioPlayer(false))
 | 
			
		||||
        }}
 | 
			
		||||
        // rootContainerProps={{
 | 
			
		||||
        //   colorScheme: theme,
 | 
			
		||||
        //   width
 | 
			
		||||
        // }}
 | 
			
		||||
      />
 | 
			
		||||
    </VideoContainer>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										366
									
								
								src/components/common/AudioPublishModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,366 @@
 | 
			
		||||
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
 | 
			
		||||
  editVideoIdentifier?: string | null | undefined
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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,
 | 
			
		||||
  editVideoIdentifier
 | 
			
		||||
}) => {
 | 
			
		||||
  const [file, setFile] = useState<File | null>(null)
 | 
			
		||||
  const [title, setTitle] = useState('')
 | 
			
		||||
  const [description, setDescription] = useState('')
 | 
			
		||||
  const [selectedOption, setSelectedOption] = useState<SelectOption | null>(
 | 
			
		||||
    null
 | 
			
		||||
  )
 | 
			
		||||
  const [inputValue, setInputValue] = useState<string>('')
 | 
			
		||||
  const [chips, setChips] = useState<string[]>([])
 | 
			
		||||
 | 
			
		||||
  const [options, setOptions] = useState<SelectOption[]>([])
 | 
			
		||||
  const [tags, setTags] = useState<string[]>([])
 | 
			
		||||
  const { 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({
 | 
			
		||||
        editVideoIdentifier,
 | 
			
		||||
        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>
 | 
			
		||||
        {editVideoIdentifier && (
 | 
			
		||||
          <Typography variant="h6">
 | 
			
		||||
            You are editing: {editVideoIdentifier}
 | 
			
		||||
          </Typography>
 | 
			
		||||
        )}
 | 
			
		||||
        <Typography variant="h6" component="h2" gutterBottom>
 | 
			
		||||
          Upload Audio
 | 
			
		||||
        </Typography>
 | 
			
		||||
        <Box
 | 
			
		||||
          {...getRootProps()}
 | 
			
		||||
          sx={{
 | 
			
		||||
            border: '1px dashed gray',
 | 
			
		||||
            padding: 2,
 | 
			
		||||
            textAlign: 'center',
 | 
			
		||||
            marginBottom: 2
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <input {...getInputProps()} />
 | 
			
		||||
          <Typography>
 | 
			
		||||
            {file
 | 
			
		||||
              ? file.name
 | 
			
		||||
              : 'Drag and drop an audio file here or click to select a file'}
 | 
			
		||||
          </Typography>
 | 
			
		||||
        </Box>
 | 
			
		||||
        <TextField
 | 
			
		||||
          label="Audio Title"
 | 
			
		||||
          variant="outlined"
 | 
			
		||||
          fullWidth
 | 
			
		||||
          value={title}
 | 
			
		||||
          onChange={handleTitleChange}
 | 
			
		||||
          inputProps={{ maxLength: 40 }}
 | 
			
		||||
          sx={{ marginBottom: 2 }}
 | 
			
		||||
        />
 | 
			
		||||
        <TextField
 | 
			
		||||
          label="Audio Description"
 | 
			
		||||
          variant="outlined"
 | 
			
		||||
          fullWidth
 | 
			
		||||
          multiline
 | 
			
		||||
          rows={4}
 | 
			
		||||
          value={description}
 | 
			
		||||
          onChange={handleDescriptionChange}
 | 
			
		||||
          inputProps={{ maxLength: 180 }}
 | 
			
		||||
          sx={{ marginBottom: 2 }}
 | 
			
		||||
        />
 | 
			
		||||
        {options.length > 0 && (
 | 
			
		||||
          <FormControl fullWidth sx={{ marginBottom: 2 }}>
 | 
			
		||||
            <InputLabel id="Category">Select a Category</InputLabel>
 | 
			
		||||
            <Select
 | 
			
		||||
              labelId="Category"
 | 
			
		||||
              input={<OutlinedInput label="Select a Category" />}
 | 
			
		||||
              value={selectedOption?.id || ''}
 | 
			
		||||
              onChange={handleOptionChange}
 | 
			
		||||
            >
 | 
			
		||||
              {options.map((option) => (
 | 
			
		||||
                <MenuItem key={option.id} value={option.id}>
 | 
			
		||||
                  {option.name}
 | 
			
		||||
                </MenuItem>
 | 
			
		||||
              ))}
 | 
			
		||||
            </Select>
 | 
			
		||||
          </FormControl>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        <FormControl fullWidth sx={{ marginBottom: 2 }}>
 | 
			
		||||
          <Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
 | 
			
		||||
            <TextField
 | 
			
		||||
              label="Add a tag"
 | 
			
		||||
              value={inputValue}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              onKeyDown={handleInputKeyDown}
 | 
			
		||||
              disabled={chips.length === 3}
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            <IconButton onClick={addChip} disabled={chips.length === 3}>
 | 
			
		||||
              <AddIcon />
 | 
			
		||||
            </IconButton>
 | 
			
		||||
          </Box>
 | 
			
		||||
          <ChipContainer>
 | 
			
		||||
            {chips.map((chip, index) => (
 | 
			
		||||
              <Chip
 | 
			
		||||
                key={index}
 | 
			
		||||
                label={chip}
 | 
			
		||||
                onDelete={() => handleChipDelete(index)}
 | 
			
		||||
                deleteIcon={<CloseIcon />}
 | 
			
		||||
              />
 | 
			
		||||
            ))}
 | 
			
		||||
          </ChipContainer>
 | 
			
		||||
        </FormControl>
 | 
			
		||||
        <Button variant="contained" color="primary" onClick={handleSubmit}>
 | 
			
		||||
          Submit
 | 
			
		||||
        </Button>
 | 
			
		||||
      </ModalContent>
 | 
			
		||||
    </StyledModal>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,28 @@
 | 
			
		||||
import { styled } from '@mui/system';
 | 
			
		||||
import {
 | 
			
		||||
  Box,
 | 
			
		||||
  Modal,
 | 
			
		||||
  Typography
 | 
			
		||||
} from '@mui/material';
 | 
			
		||||
 | 
			
		||||
export const StyledModal = styled(Modal)(({ theme }) => ({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  alignItems: 'center',
 | 
			
		||||
  justifyContent: 'center'
 | 
			
		||||
}))
 | 
			
		||||
 | 
			
		||||
export const ModalContent = styled(Box)(({ theme }) => ({
 | 
			
		||||
  backgroundColor: theme.palette.primary.main,
 | 
			
		||||
  padding: theme.spacing(4),
 | 
			
		||||
  borderRadius: theme.spacing(1),
 | 
			
		||||
  width: '40%',
 | 
			
		||||
  '&:focus': {
 | 
			
		||||
    outline: 'none'
 | 
			
		||||
  }
 | 
			
		||||
}))
 | 
			
		||||
 | 
			
		||||
export const ModalText = styled(Typography)(({ theme }) => ({
 | 
			
		||||
  fontFamily: "Raleway",
 | 
			
		||||
  fontSize: "25px",
 | 
			
		||||
  color: theme.palette.text.primary,
 | 
			
		||||
}));
 | 
			
		||||
							
								
								
									
										100
									
								
								src/components/common/BlockedNamesModal/BlockedNamesModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,100 @@
 | 
			
		||||
import React, { useState } from 'react'
 | 
			
		||||
import {
 | 
			
		||||
  Box,
 | 
			
		||||
  Button,
 | 
			
		||||
  Modal,
 | 
			
		||||
  Typography,
 | 
			
		||||
  SelectChangeEvent,
 | 
			
		||||
  ListItem,
 | 
			
		||||
  List,
 | 
			
		||||
  useTheme
 | 
			
		||||
} from '@mui/material'
 | 
			
		||||
import {
 | 
			
		||||
  StyledModal,
 | 
			
		||||
  ModalContent,
 | 
			
		||||
  ModalText
 | 
			
		||||
} from './BlockedNamesModal-styles'
 | 
			
		||||
 | 
			
		||||
interface PostModalProps {
 | 
			
		||||
  open: boolean
 | 
			
		||||
  onClose: () => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const BlockedNamesModal: React.FC<PostModalProps> = ({
 | 
			
		||||
  open,
 | 
			
		||||
  onClose
 | 
			
		||||
}) => {
 | 
			
		||||
  const [blockedNames, setBlockedNames] = useState<string[]>([])
 | 
			
		||||
  const theme = useTheme()
 | 
			
		||||
  const getBlockedNames = React.useCallback(async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const listName = `blockedNames_q-blog`
 | 
			
		||||
      const response = await qortalRequest({
 | 
			
		||||
        action: 'GET_LIST_ITEMS',
 | 
			
		||||
        list_name: listName
 | 
			
		||||
      })
 | 
			
		||||
      setBlockedNames(response)
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      onClose()
 | 
			
		||||
    }
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    getBlockedNames()
 | 
			
		||||
  }, [getBlockedNames])
 | 
			
		||||
 | 
			
		||||
  const removeFromBlockList = async (name: string) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await qortalRequest({
 | 
			
		||||
        action: 'DELETE_LIST_ITEM',
 | 
			
		||||
        list_name: 'blockedNames_q-blog',
 | 
			
		||||
        item: name
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      if (response === true) {
 | 
			
		||||
        setBlockedNames((prev) => prev.filter((n) => n !== name))
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <StyledModal open={open} onClose={onClose}>
 | 
			
		||||
      <ModalContent>
 | 
			
		||||
        <ModalText>Manage blocked names</ModalText>
 | 
			
		||||
        <List
 | 
			
		||||
          sx={{
 | 
			
		||||
            width: '100%',
 | 
			
		||||
            display: 'flex',
 | 
			
		||||
            flexDirection: 'column',
 | 
			
		||||
            flex: '1',
 | 
			
		||||
            overflow: 'auto'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {blockedNames.map((name, index) => (
 | 
			
		||||
            <ListItem
 | 
			
		||||
              key={name + index}
 | 
			
		||||
              sx={{
 | 
			
		||||
                display: 'flex'
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Typography>{name}</Typography>
 | 
			
		||||
              <Button
 | 
			
		||||
                sx={{
 | 
			
		||||
                  backgroundColor: theme.palette.primary.light,
 | 
			
		||||
                  color: theme.palette.text.primary,
 | 
			
		||||
                  fontFamily: 'Arial'
 | 
			
		||||
                }}
 | 
			
		||||
                onClick={() => removeFromBlockList(name)}
 | 
			
		||||
              >
 | 
			
		||||
                Remove
 | 
			
		||||
              </Button>
 | 
			
		||||
            </ListItem>
 | 
			
		||||
          ))}
 | 
			
		||||
        </List>
 | 
			
		||||
        <Button variant="contained" color="primary" onClick={onClose}>
 | 
			
		||||
          Close
 | 
			
		||||
        </Button>
 | 
			
		||||
      </ModalContent>
 | 
			
		||||
    </StyledModal>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										336
									
								
								src/components/common/Comments/Comment.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,336 @@
 | 
			
		||||
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'
 | 
			
		||||
import { formatDate } from '../../../utils/time'
 | 
			
		||||
interface CommentProps {
 | 
			
		||||
  comment: any
 | 
			
		||||
  postId: string
 | 
			
		||||
  postName: string
 | 
			
		||||
  onSubmit: (obj?: any, isEdit?: boolean) => void
 | 
			
		||||
}
 | 
			
		||||
export const Comment = ({
 | 
			
		||||
  comment,
 | 
			
		||||
  postId,
 | 
			
		||||
  postName,
 | 
			
		||||
  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 theme = useTheme()
 | 
			
		||||
 | 
			
		||||
  const handleSubmit = useCallback((comment: any, isEdit?: boolean) => {
 | 
			
		||||
    onSubmit(comment, isEdit)
 | 
			
		||||
    setCurrentEdit(null)
 | 
			
		||||
    setIsReplying(false)
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
      id={comment?.identifier}
 | 
			
		||||
      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}
 | 
			
		||||
                  postName={postName}
 | 
			
		||||
                  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: 'space-between'
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            {comment?.created && (
 | 
			
		||||
              <Typography
 | 
			
		||||
                variant="h6"
 | 
			
		||||
                sx={{
 | 
			
		||||
                  fontSize: '12px',
 | 
			
		||||
                  marginLeft: '5px'
 | 
			
		||||
                }}
 | 
			
		||||
                color={theme.palette.text.primary}
 | 
			
		||||
              >
 | 
			
		||||
                {formatDate(+comment?.created)}
 | 
			
		||||
              </Typography>
 | 
			
		||||
            )}
 | 
			
		||||
            <Box
 | 
			
		||||
              sx={{
 | 
			
		||||
                display: 'flex',
 | 
			
		||||
                alignItems: 'center',
 | 
			
		||||
                gap: '5px'
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <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>
 | 
			
		||||
          </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}
 | 
			
		||||
            postName={postName}
 | 
			
		||||
            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',
 | 
			
		||||
            wordBreak: 'break-word'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {message}
 | 
			
		||||
        </Typography>
 | 
			
		||||
      </StyledCardContentComment>
 | 
			
		||||
      <Box
 | 
			
		||||
        sx={{
 | 
			
		||||
          paddingLeft: '15px',
 | 
			
		||||
          display: 'flex',
 | 
			
		||||
          flexDirection: 'column'
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {replies?.map((reply: any) => {
 | 
			
		||||
          return (
 | 
			
		||||
            <Box
 | 
			
		||||
              key={reply?.identifier}
 | 
			
		||||
              id={reply?.identifier}
 | 
			
		||||
              sx={{
 | 
			
		||||
                display: 'flex',
 | 
			
		||||
                border: '1px solid grey',
 | 
			
		||||
                borderRadius: '10px',
 | 
			
		||||
                marginTop: '8px'
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <CommentCard
 | 
			
		||||
                name={reply?.name}
 | 
			
		||||
                message={reply?.message}
 | 
			
		||||
                setCurrentEdit={setCurrentEdit}
 | 
			
		||||
              >
 | 
			
		||||
                <Box
 | 
			
		||||
                  sx={{
 | 
			
		||||
                    display: 'flex',
 | 
			
		||||
                    alignItems: 'center',
 | 
			
		||||
                    gap: '5px',
 | 
			
		||||
                    justifyContent: 'space-between'
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  {reply?.created && (
 | 
			
		||||
                    <Typography
 | 
			
		||||
                      variant="h6"
 | 
			
		||||
                      sx={{
 | 
			
		||||
                        fontSize: '12px',
 | 
			
		||||
                        marginLeft: '5px'
 | 
			
		||||
                      }}
 | 
			
		||||
                      color={theme.palette.text.primary}
 | 
			
		||||
                    >
 | 
			
		||||
                      {formatDate(+reply?.created)}
 | 
			
		||||
                    </Typography>
 | 
			
		||||
                  )}
 | 
			
		||||
                  {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>
 | 
			
		||||
                  ) : (
 | 
			
		||||
                    <Box />
 | 
			
		||||
                  )}
 | 
			
		||||
                </Box>
 | 
			
		||||
              </CommentCard>
 | 
			
		||||
              {/* <Typography variant="body2"> {reply?.message}</Typography> */}
 | 
			
		||||
            </Box>
 | 
			
		||||
          )
 | 
			
		||||
        })}
 | 
			
		||||
      </Box>
 | 
			
		||||
      {children}
 | 
			
		||||
    </CardContentContainerComment>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										258
									
								
								src/components/common/Comments/CommentEditor.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,258 @@
 | 
			
		||||
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'
 | 
			
		||||
import localforage from 'localforage'
 | 
			
		||||
const uid = new ShortUniqueId()
 | 
			
		||||
 | 
			
		||||
const notification = localforage.createInstance({
 | 
			
		||||
  name: 'notification'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const MAX_ITEMS = 10
 | 
			
		||||
 | 
			
		||||
export interface Item {
 | 
			
		||||
  id: string
 | 
			
		||||
  lastSeen: number
 | 
			
		||||
  postId: string
 | 
			
		||||
  postName: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function addItem(item: Item): Promise<void> {
 | 
			
		||||
  // Get all items
 | 
			
		||||
  let notificationComments: Item[] =
 | 
			
		||||
    (await notification.getItem('comments')) || []
 | 
			
		||||
 | 
			
		||||
  // Find the item with the same id, if it exists
 | 
			
		||||
  let existingItemIndex = notificationComments.findIndex(
 | 
			
		||||
    (i) => i.id === item.id
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  if (existingItemIndex !== -1) {
 | 
			
		||||
    // If the item exists, update its date
 | 
			
		||||
    notificationComments[existingItemIndex].lastSeen = item.lastSeen
 | 
			
		||||
  } else {
 | 
			
		||||
    // If the item doesn't exist, add it
 | 
			
		||||
    notificationComments.push(item)
 | 
			
		||||
 | 
			
		||||
    // If adding the item has caused us to exceed the max number of items, remove the oldest one
 | 
			
		||||
    if (notificationComments.length > MAX_ITEMS) {
 | 
			
		||||
      notificationComments.sort((a, b) => b.lastSeen - a.lastSeen) // sort items by date, newest first
 | 
			
		||||
      notificationComments.pop() // remove the oldest item
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Store the items back into localForage
 | 
			
		||||
  await notification.setItem('comments', notificationComments)
 | 
			
		||||
}
 | 
			
		||||
export async function updateItemDate(item: any): Promise<void> {
 | 
			
		||||
  // Get all items
 | 
			
		||||
  let notificationComments: Item[] =
 | 
			
		||||
    (await notification.getItem('comments')) || []
 | 
			
		||||
 | 
			
		||||
  let notificationCreatorComment: any =
 | 
			
		||||
    (await notification.getItem('post-comments')) || {}
 | 
			
		||||
  const findPostId = notificationCreatorComment[item.postId]
 | 
			
		||||
  if (findPostId) {
 | 
			
		||||
    notificationCreatorComment[item.postId].lastSeen = item.lastSeen
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Find the item with the same id, if it exists
 | 
			
		||||
  notificationComments.forEach((nc, index) => {
 | 
			
		||||
    if (nc.postId === item.postId) {
 | 
			
		||||
      notificationComments[index].lastSeen = item.lastSeen
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // Store the items back into localForage
 | 
			
		||||
  await notification.setItem('comments', notificationComments)
 | 
			
		||||
  await notification.setItem('post-comments', notificationCreatorComment)
 | 
			
		||||
}
 | 
			
		||||
interface CommentEditorProps {
 | 
			
		||||
  postId: string
 | 
			
		||||
  postName: 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,
 | 
			
		||||
  postName,
 | 
			
		||||
  isReply,
 | 
			
		||||
  commentId,
 | 
			
		||||
  isEdit,
 | 
			
		||||
  commentMessage
 | 
			
		||||
}: CommentEditorProps) => {
 | 
			
		||||
  const [value, setValue] = useState<string>('')
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
  const { user } = useSelector((state: RootState) => state.auth)
 | 
			
		||||
  const notifications = useSelector(
 | 
			
		||||
    (state: RootState) => state.global.notifications
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (isEdit && commentMessage) {
 | 
			
		||||
      setValue(commentMessage)
 | 
			
		||||
    }
 | 
			
		||||
  }, [isEdit, commentMessage])
 | 
			
		||||
 | 
			
		||||
  const publishComment = async (
 | 
			
		||||
    identifier: string,
 | 
			
		||||
    idForNotification?: 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'
 | 
			
		||||
        })
 | 
			
		||||
      )
 | 
			
		||||
      if (idForNotification) {
 | 
			
		||||
        addItem({
 | 
			
		||||
          id: idForNotification,
 | 
			
		||||
          lastSeen: Date.now(),
 | 
			
		||||
          postId,
 | 
			
		||||
          postName: postName
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      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}`
 | 
			
		||||
      let idForNotification = identifier
 | 
			
		||||
      if (isReply && commentId) {
 | 
			
		||||
        identifier = `qcomment_v1_qblog_${postId.slice(
 | 
			
		||||
          -12
 | 
			
		||||
        )}_reply_${commentId.slice(-6)}_${id}`
 | 
			
		||||
        idForNotification = commentId
 | 
			
		||||
      }
 | 
			
		||||
      if (isEdit && commentId) {
 | 
			
		||||
        identifier = commentId
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await publishComment(identifier, idForNotification)
 | 
			
		||||
      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>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										386
									
								
								src/components/common/Comments/CommentSection.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,386 @@
 | 
			
		||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
 | 
			
		||||
import { CommentEditor, addItem, updateItemDate } 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'
 | 
			
		||||
import { useNavigate, useLocation } from 'react-router-dom'
 | 
			
		||||
 | 
			
		||||
interface CommentSectionProps {
 | 
			
		||||
  postId: string
 | 
			
		||||
  postName: 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, postName }: CommentSectionProps) => {
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
  const location = useLocation()
 | 
			
		||||
  const [listComments, setListComments] = useState<any[]>([])
 | 
			
		||||
  const [isOpen, setIsOpen] = useState<boolean>(false)
 | 
			
		||||
  const { user } = useSelector((state: RootState) => state.auth)
 | 
			
		||||
  const [newMessages, setNewMessages] = useState(0)
 | 
			
		||||
  const notifications = useSelector(
 | 
			
		||||
    (state: RootState) => state.global.notifications
 | 
			
		||||
  )
 | 
			
		||||
  const notificationCreatorComment = useSelector(
 | 
			
		||||
    (state: RootState) => state.global.notificationCreatorComment
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const fullNotifications = useMemo(() => {
 | 
			
		||||
    return [...notificationCreatorComment, ...notifications].sort(
 | 
			
		||||
      (a, b) => b.created - a.created
 | 
			
		||||
    )
 | 
			
		||||
  }, [notificationCreatorComment, notifications])
 | 
			
		||||
  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
 | 
			
		||||
      }
 | 
			
		||||
    ])
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const query = new URLSearchParams(location.search)
 | 
			
		||||
    let commentVar = query?.get('comment')
 | 
			
		||||
    if (commentVar) {
 | 
			
		||||
      if (commentVar && commentVar.endsWith('/')) {
 | 
			
		||||
        commentVar = commentVar.slice(0, -1)
 | 
			
		||||
      }
 | 
			
		||||
      setIsOpen(true)
 | 
			
		||||
      if (listComments.length > 0) {
 | 
			
		||||
        const el = document.getElementById(commentVar)
 | 
			
		||||
        if (el) {
 | 
			
		||||
          el.scrollIntoView()
 | 
			
		||||
          el.classList.add('glow')
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            el.classList.remove('glow')
 | 
			
		||||
          }, 2000)
 | 
			
		||||
        }
 | 
			
		||||
        navigate(location.pathname, { replace: true })
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, [navigate, location, listComments])
 | 
			
		||||
 | 
			
		||||
  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=false&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) {}
 | 
			
		||||
    },
 | 
			
		||||
    [postId]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const checkAndUpdateNotification = async () => {
 | 
			
		||||
    const filteredNotifications = fullNotifications.filter(
 | 
			
		||||
      (notification) =>
 | 
			
		||||
        postId.includes(notification?.partialPostId) ||
 | 
			
		||||
        notification?.postId === postId
 | 
			
		||||
    )
 | 
			
		||||
    filteredNotifications.forEach((notification) => {
 | 
			
		||||
      if (postId) {
 | 
			
		||||
        updateItemDate({
 | 
			
		||||
          id: notification?.identifier,
 | 
			
		||||
          lastSeen: Date.now(),
 | 
			
		||||
          postId
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (fullNotifications && isOpen) {
 | 
			
		||||
      checkAndUpdateNotification()
 | 
			
		||||
    }
 | 
			
		||||
  }, [fullNotifications, isOpen])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    getComments()
 | 
			
		||||
  }, [getComments, postId])
 | 
			
		||||
 | 
			
		||||
  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=false&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={() => {
 | 
			
		||||
                    // addItem({
 | 
			
		||||
                    //   id: notification.identifier,
 | 
			
		||||
                    //   lastSeen: Date.now(),
 | 
			
		||||
                    //   postId
 | 
			
		||||
                    // })
 | 
			
		||||
                    updateItemDate({
 | 
			
		||||
                      id: '',
 | 
			
		||||
                      lastSeen: Date.now(),
 | 
			
		||||
                      postId
 | 
			
		||||
                    })
 | 
			
		||||
                    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}
 | 
			
		||||
                    postName={postName}
 | 
			
		||||
                  />
 | 
			
		||||
                )
 | 
			
		||||
              })}
 | 
			
		||||
            </Box>
 | 
			
		||||
          </Box>
 | 
			
		||||
          <Box
 | 
			
		||||
            sx={{
 | 
			
		||||
              width: '100%',
 | 
			
		||||
              display: 'flex',
 | 
			
		||||
              justifyContent: 'center',
 | 
			
		||||
              flex: '0 0 100px'
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <CommentEditor
 | 
			
		||||
              onSubmit={onSubmit}
 | 
			
		||||
              postId={postId}
 | 
			
		||||
              postName={postName}
 | 
			
		||||
            />
 | 
			
		||||
          </Box>
 | 
			
		||||
        </Panel>
 | 
			
		||||
      </Drawer>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										82
									
								
								src/components/common/ContextMenu/ContextMenuResource.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,82 @@
 | 
			
		||||
import * as React from 'react'
 | 
			
		||||
import Menu from '@mui/material/Menu'
 | 
			
		||||
import MenuItem from '@mui/material/MenuItem'
 | 
			
		||||
import Typography from '@mui/material/Typography'
 | 
			
		||||
import { CopyToClipboard } from 'react-copy-to-clipboard'
 | 
			
		||||
import { useDispatch } from 'react-redux'
 | 
			
		||||
import { setNotification } from '../../../state/features/notificationsSlice'
 | 
			
		||||
import { Box } from '@mui/material'
 | 
			
		||||
 | 
			
		||||
export default function ContextMenuResource({
 | 
			
		||||
  children,
 | 
			
		||||
  name,
 | 
			
		||||
  service,
 | 
			
		||||
  identifier,
 | 
			
		||||
  link
 | 
			
		||||
}: any) {
 | 
			
		||||
  const [contextMenu, setContextMenu] = React.useState<{
 | 
			
		||||
    mouseX: number
 | 
			
		||||
    mouseY: number
 | 
			
		||||
  } | null>(null)
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
  const handleContextMenu = (event: React.MouseEvent) => {
 | 
			
		||||
    event.preventDefault()
 | 
			
		||||
    setContextMenu(
 | 
			
		||||
      contextMenu === null
 | 
			
		||||
        ? {
 | 
			
		||||
            mouseX: event.clientX + 2,
 | 
			
		||||
            mouseY: event.clientY - 6
 | 
			
		||||
          }
 | 
			
		||||
        : // repeated contextmenu when it is already open closes it with Chrome 84 on Ubuntu
 | 
			
		||||
          // Other native context menus might behave different.
 | 
			
		||||
          // With this behavior we prevent contextmenu from the backdrop to re-locale existing context menus.
 | 
			
		||||
          null
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleClose = () => {
 | 
			
		||||
    setContextMenu(null)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      onContextMenu={handleContextMenu}
 | 
			
		||||
      style={{ cursor: 'context-menu', width: '100%' }}
 | 
			
		||||
    >
 | 
			
		||||
      {children}
 | 
			
		||||
      <Menu
 | 
			
		||||
        open={contextMenu !== null}
 | 
			
		||||
        onClose={handleClose}
 | 
			
		||||
        anchorReference="anchorPosition"
 | 
			
		||||
        anchorPosition={
 | 
			
		||||
          contextMenu !== null
 | 
			
		||||
            ? { top: contextMenu.mouseY, left: contextMenu.mouseX }
 | 
			
		||||
            : undefined
 | 
			
		||||
        }
 | 
			
		||||
      >
 | 
			
		||||
        <MenuItem>
 | 
			
		||||
          <CopyToClipboard
 | 
			
		||||
            text={link}
 | 
			
		||||
            onCopy={() => {
 | 
			
		||||
              handleClose()
 | 
			
		||||
              dispatch(
 | 
			
		||||
                setNotification({
 | 
			
		||||
                  msg: 'Copied to clipboard!',
 | 
			
		||||
                  alertType: 'success'
 | 
			
		||||
                })
 | 
			
		||||
              )
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Box
 | 
			
		||||
              sx={{
 | 
			
		||||
                fontSize: '16px'
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              Copy Link
 | 
			
		||||
            </Box>
 | 
			
		||||
          </CopyToClipboard>
 | 
			
		||||
        </MenuItem>
 | 
			
		||||
      </Menu>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								src/components/common/CustomIcon.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,16 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon'
 | 
			
		||||
import { styled } from '@mui/system'
 | 
			
		||||
 | 
			
		||||
const CustomSvgIcon: React.FC<any> = styled(SvgIcon)(({ theme }) => ({
 | 
			
		||||
  cursor: 'pointer',
 | 
			
		||||
  color: '#5f6368',
 | 
			
		||||
  transition: 'all 0.2s',
 | 
			
		||||
  '&:hover': {
 | 
			
		||||
    transform: 'scale(1.1)'
 | 
			
		||||
  }
 | 
			
		||||
})) as unknown as React.FC<any>
 | 
			
		||||
 | 
			
		||||
export const CustomIcon: React.FC<any> = (props) => {
 | 
			
		||||
  return <CustomSvgIcon {...props} />
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										289
									
								
								src/components/common/DownloadTaskManager.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,289 @@
 | 
			
		||||
import React, { useState, useEffect } from 'react'
 | 
			
		||||
import {
 | 
			
		||||
  Accordion,
 | 
			
		||||
  AccordionDetails,
 | 
			
		||||
  AccordionSummary,
 | 
			
		||||
  Box,
 | 
			
		||||
  LinearProgress,
 | 
			
		||||
  List,
 | 
			
		||||
  ListItem,
 | 
			
		||||
  ListItemIcon,
 | 
			
		||||
  Typography,
 | 
			
		||||
  useTheme
 | 
			
		||||
} from '@mui/material'
 | 
			
		||||
import { Movie, ArrowDropDown } from '@mui/icons-material'
 | 
			
		||||
import { SxProps } from '@mui/system'
 | 
			
		||||
import { Theme } from '@mui/material/styles'
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux'
 | 
			
		||||
import { RootState } from '../../state/store'
 | 
			
		||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
 | 
			
		||||
import { removePrefix } from '../../utils/blogIdformats'
 | 
			
		||||
import { useLocation, useNavigate } from 'react-router-dom'
 | 
			
		||||
import AudiotrackIcon from '@mui/icons-material/Audiotrack'
 | 
			
		||||
import {
 | 
			
		||||
  setCurrAudio,
 | 
			
		||||
  setShowingAudioPlayer
 | 
			
		||||
} from '../../state/features/globalSlice'
 | 
			
		||||
import { MAIL_ATTACHMENT_SERVICE_TYPE } from '../../constants/mail'
 | 
			
		||||
 | 
			
		||||
type DownloadItem = {
 | 
			
		||||
  id: string
 | 
			
		||||
  name: string
 | 
			
		||||
  progress: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const DownloadTaskManager: React.FC = () => {
 | 
			
		||||
  const { downloads } = useSelector((state: RootState) => state.global)
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
  const location = useLocation()
 | 
			
		||||
  const isMailRoute = location.pathname === '/mail'
 | 
			
		||||
  const theme = useTheme()
 | 
			
		||||
  const [visible, setVisible] = useState(false)
 | 
			
		||||
  const [hidden, setHidden] = useState(true)
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
  const containerStyles: SxProps<Theme> = {
 | 
			
		||||
    position: 'fixed',
 | 
			
		||||
    top: '50px',
 | 
			
		||||
    right: 0,
 | 
			
		||||
    zIndex: 1000,
 | 
			
		||||
    maxHeight: '80%',
 | 
			
		||||
    overflowY: 'auto',
 | 
			
		||||
    backgroundColor: 'background.paper',
 | 
			
		||||
    boxShadow: 2,
 | 
			
		||||
    display: 'block'
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // Simulate downloads for demo purposes
 | 
			
		||||
 | 
			
		||||
    if (visible) {
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        setHidden(true)
 | 
			
		||||
        setVisible(false)
 | 
			
		||||
      }, 3000)
 | 
			
		||||
    }
 | 
			
		||||
  }, [visible])
 | 
			
		||||
 | 
			
		||||
  const toggleVisibility = () => {
 | 
			
		||||
    setVisible(true)
 | 
			
		||||
    setHidden(false)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (Object.keys(downloads).length === 0) return
 | 
			
		||||
    setVisible(true)
 | 
			
		||||
    setHidden(false)
 | 
			
		||||
  }, [downloads])
 | 
			
		||||
 | 
			
		||||
  if (isMailRoute) return null
 | 
			
		||||
  if (
 | 
			
		||||
    !downloads ||
 | 
			
		||||
    Object.keys(downloads).filter(
 | 
			
		||||
      (item) => downloads[item].service !== MAIL_ATTACHMENT_SERVICE_TYPE
 | 
			
		||||
    ).length === 0
 | 
			
		||||
  )
 | 
			
		||||
    return null
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box sx={{ position: 'fixed', top: '50px', right: '5px', zIndex: 1000 }}>
 | 
			
		||||
      <Accordion
 | 
			
		||||
        sx={{
 | 
			
		||||
          width: '200px',
 | 
			
		||||
          backgroundColor: theme.palette.primary.main
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <AccordionSummary
 | 
			
		||||
          expandIcon={<ExpandMoreIcon />}
 | 
			
		||||
          aria-controls="panel1a-content"
 | 
			
		||||
          id="panel1a-header"
 | 
			
		||||
          sx={{
 | 
			
		||||
            minHeight: 'unset',
 | 
			
		||||
            height: '36px',
 | 
			
		||||
            backgroundColor: theme.palette.primary.light,
 | 
			
		||||
            '&.MuiAccordionSummary-content': {
 | 
			
		||||
              padding: 0,
 | 
			
		||||
              margin: 0
 | 
			
		||||
            },
 | 
			
		||||
            '&.Mui-expanded': {
 | 
			
		||||
              minHeight: 'unset',
 | 
			
		||||
              height: '36px'
 | 
			
		||||
            }
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Typography
 | 
			
		||||
            sx={{
 | 
			
		||||
              fontFamily: 'Arial',
 | 
			
		||||
              color: theme.palette.text.primary,
 | 
			
		||||
              fontSize: '14px'
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            Downloads
 | 
			
		||||
          </Typography>
 | 
			
		||||
        </AccordionSummary>
 | 
			
		||||
        <AccordionDetails
 | 
			
		||||
          sx={{
 | 
			
		||||
            padding: '5px'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <List
 | 
			
		||||
            sx={{
 | 
			
		||||
              maxHeight: '50vh',
 | 
			
		||||
              overflow: 'auto'
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            {Object.keys(downloads)
 | 
			
		||||
              .filter(
 | 
			
		||||
                (item) =>
 | 
			
		||||
                  downloads[item].service !== MAIL_ATTACHMENT_SERVICE_TYPE
 | 
			
		||||
              )
 | 
			
		||||
              .map((download: any) => {
 | 
			
		||||
                const downloadObj = downloads[download]
 | 
			
		||||
                const progress = downloads[download]?.status?.percentLoaded || 0
 | 
			
		||||
                const status = downloads[download]?.status?.status
 | 
			
		||||
                const service = downloads[download]?.service
 | 
			
		||||
                return (
 | 
			
		||||
                  <ListItem
 | 
			
		||||
                    key={downloadObj?.identifier}
 | 
			
		||||
                    sx={{
 | 
			
		||||
                      display: 'flex',
 | 
			
		||||
                      flexDirection: 'column',
 | 
			
		||||
                      width: '100%',
 | 
			
		||||
                      justifyContent: 'center',
 | 
			
		||||
                      background: theme.palette.primary.main,
 | 
			
		||||
                      color: theme.palette.text.primary,
 | 
			
		||||
                      cursor: 'pointer',
 | 
			
		||||
                      padding: '2px'
 | 
			
		||||
                    }}
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      if (service === 'AUDIO' && downloadObj?.identifier) {
 | 
			
		||||
                        dispatch(setCurrAudio(downloadObj?.identifier))
 | 
			
		||||
                        dispatch(setShowingAudioPlayer(true))
 | 
			
		||||
                        return
 | 
			
		||||
                      }
 | 
			
		||||
 | 
			
		||||
                      const str = downloadObj?.blogPost?.postId
 | 
			
		||||
                      if (!str) return
 | 
			
		||||
                      const arr = str.split('-post-')
 | 
			
		||||
                      const str1 = arr[0]
 | 
			
		||||
                      const str2 = arr[1]
 | 
			
		||||
                      const blogId = removePrefix(str1)
 | 
			
		||||
                      navigate(
 | 
			
		||||
                        `/${downloadObj?.blogPost.user}/${blogId}/${str2}`
 | 
			
		||||
                      )
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <Box
 | 
			
		||||
                      sx={{
 | 
			
		||||
                        width: '100%',
 | 
			
		||||
                        display: 'flex',
 | 
			
		||||
                        alignItems: 'center'
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      <ListItemIcon>
 | 
			
		||||
                        {service === 'AUDIO' && (
 | 
			
		||||
                          <AudiotrackIcon
 | 
			
		||||
                            sx={{ color: theme.palette.text.primary }}
 | 
			
		||||
                          />
 | 
			
		||||
                        )}
 | 
			
		||||
                        {service === 'VIDEO' && (
 | 
			
		||||
                          <Movie sx={{ color: theme.palette.text.primary }} />
 | 
			
		||||
                        )}
 | 
			
		||||
                      </ListItemIcon>
 | 
			
		||||
 | 
			
		||||
                      <Box
 | 
			
		||||
                        sx={{ width: '100px', marginLeft: 1, marginRight: 1 }}
 | 
			
		||||
                      >
 | 
			
		||||
                        <LinearProgress
 | 
			
		||||
                          variant="determinate"
 | 
			
		||||
                          value={progress}
 | 
			
		||||
                          sx={{
 | 
			
		||||
                            borderRadius: '5px',
 | 
			
		||||
                            color: theme.palette.secondary.main
 | 
			
		||||
                          }}
 | 
			
		||||
                        />
 | 
			
		||||
                      </Box>
 | 
			
		||||
                      <Typography
 | 
			
		||||
                        sx={{
 | 
			
		||||
                          fontFamily: 'Arial',
 | 
			
		||||
                          color: theme.palette.text.primary
 | 
			
		||||
                        }}
 | 
			
		||||
                        variant="caption"
 | 
			
		||||
                      >
 | 
			
		||||
                        {`${progress?.toFixed(0)}%`}{' '}
 | 
			
		||||
                        {status && status === 'REFETCHING' && '- refetching'}
 | 
			
		||||
                        {status && status === 'DOWNLOADED' && '- building'}
 | 
			
		||||
                      </Typography>
 | 
			
		||||
                    </Box>
 | 
			
		||||
                    <Typography
 | 
			
		||||
                      sx={{
 | 
			
		||||
                        fontSize: '10px',
 | 
			
		||||
                        width: '100%',
 | 
			
		||||
                        textAlign: 'end',
 | 
			
		||||
                        fontFamily: 'Arial',
 | 
			
		||||
                        color: theme.palette.text.primary
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      {downloadObj?.identifier}
 | 
			
		||||
                    </Typography>
 | 
			
		||||
                  </ListItem>
 | 
			
		||||
                )
 | 
			
		||||
              })}
 | 
			
		||||
          </List>
 | 
			
		||||
        </AccordionDetails>
 | 
			
		||||
      </Accordion>
 | 
			
		||||
 | 
			
		||||
      {/* <IconButton onClick={() => {}} aria-label="toggle download manager">
 | 
			
		||||
        <ArrowDropDown />
 | 
			
		||||
      </IconButton> */}
 | 
			
		||||
      {/* <Box sx={containerStyles}>
 | 
			
		||||
        <List
 | 
			
		||||
          sx={{
 | 
			
		||||
            width: '200px'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {Object.keys(downloads).map((download: any) => {
 | 
			
		||||
            const downloadObj = downloads[download]
 | 
			
		||||
            const progress = downloads[download]?.status?.percentLoaded || 0
 | 
			
		||||
            return (
 | 
			
		||||
              <ListItem
 | 
			
		||||
                key={downloadObj?.identifier}
 | 
			
		||||
                sx={{
 | 
			
		||||
                  display: 'flex',
 | 
			
		||||
                  flexDirection: 'column',
 | 
			
		||||
                  width: '100%',
 | 
			
		||||
                  justifyContent: 'center'
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Box
 | 
			
		||||
                  sx={{
 | 
			
		||||
                    width: '100%',
 | 
			
		||||
                    display: 'flex',
 | 
			
		||||
                    alignItems: 'center'
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <ListItemIcon>
 | 
			
		||||
                    <Movie />
 | 
			
		||||
                  </ListItemIcon>
 | 
			
		||||
 | 
			
		||||
                  <Box sx={{ width: '100px', marginLeft: 1 }}>
 | 
			
		||||
                    <LinearProgress variant="determinate" value={progress} />
 | 
			
		||||
                  </Box>
 | 
			
		||||
                  <Typography variant="caption">{`${progress}%`}</Typography>
 | 
			
		||||
                </Box>
 | 
			
		||||
 | 
			
		||||
                <ListItemText
 | 
			
		||||
                  primary={downloadObj?.identifier}
 | 
			
		||||
                  sx={{
 | 
			
		||||
                    fontSize: '14px',
 | 
			
		||||
                    width: '100%',
 | 
			
		||||
                    textAlign: 'end'
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
              </ListItem>
 | 
			
		||||
            )
 | 
			
		||||
          })}
 | 
			
		||||
        </List>
 | 
			
		||||
      </Box> */}
 | 
			
		||||
    </Box>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										55
									
								
								src/components/common/DraggableResizableGrid.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,55 @@
 | 
			
		||||
// DraggableResizableGrid.tsx
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { DndProvider } from 'react-dnd'
 | 
			
		||||
import { HTML5Backend } from 'react-dnd-html5-backend'
 | 
			
		||||
import GridLayout, { Layout } from 'react-grid-layout'
 | 
			
		||||
 | 
			
		||||
import './DraggableResizableGrid.css' // Add your custom CSS for the grid layout
 | 
			
		||||
 | 
			
		||||
interface GridItem {
 | 
			
		||||
  id: string
 | 
			
		||||
  content: React.ReactNode
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface DraggableResizableGridProps {
 | 
			
		||||
  items: GridItem[]
 | 
			
		||||
  cols?: number
 | 
			
		||||
  rowHeight?: number
 | 
			
		||||
  onLayoutChange?: (layout: Layout[]) => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const DraggableResizableGrid: React.FC<DraggableResizableGridProps> = ({
 | 
			
		||||
  items,
 | 
			
		||||
  cols = 12,
 | 
			
		||||
  rowHeight = 30,
 | 
			
		||||
  onLayoutChange
 | 
			
		||||
}) => {
 | 
			
		||||
  const layout = items.map((item, index) => ({
 | 
			
		||||
    i: item.id,
 | 
			
		||||
    x: index % cols,
 | 
			
		||||
    y: Math.floor(index / cols),
 | 
			
		||||
    w: 4,
 | 
			
		||||
    h: 4
 | 
			
		||||
  }))
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <DndProvider backend={HTML5Backend}>
 | 
			
		||||
      <GridLayout
 | 
			
		||||
        className="layout"
 | 
			
		||||
        layout={layout}
 | 
			
		||||
        cols={cols}
 | 
			
		||||
        rowHeight={rowHeight}
 | 
			
		||||
        width={1200}
 | 
			
		||||
        onLayoutChange={onLayoutChange}
 | 
			
		||||
      >
 | 
			
		||||
        {items.map((item) => (
 | 
			
		||||
          <div key={item.id} className="grid-item">
 | 
			
		||||
            {item.content}
 | 
			
		||||
          </div>
 | 
			
		||||
        ))}
 | 
			
		||||
      </GridLayout>
 | 
			
		||||
    </DndProvider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default DraggableResizableGrid
 | 
			
		||||
							
								
								
									
										36
									
								
								src/components/common/ErrorBoundary.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,36 @@
 | 
			
		||||
import React, { ReactNode } from 'react'
 | 
			
		||||
 | 
			
		||||
interface ErrorBoundaryProps {
 | 
			
		||||
  children: ReactNode
 | 
			
		||||
  fallback: ReactNode
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface ErrorBoundaryState {
 | 
			
		||||
  hasError: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class ErrorBoundary extends React.Component<
 | 
			
		||||
  ErrorBoundaryProps,
 | 
			
		||||
  ErrorBoundaryState
 | 
			
		||||
> {
 | 
			
		||||
  state: ErrorBoundaryState = {
 | 
			
		||||
    hasError: false
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static getDerivedStateFromError(_: Error): ErrorBoundaryState {
 | 
			
		||||
    return { hasError: true }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
 | 
			
		||||
    // You can log the error and errorInfo here, for example, to an error reporting service.
 | 
			
		||||
    console.error('Error caught in ErrorBoundary:', error, errorInfo)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render(): React.ReactNode {
 | 
			
		||||
    if (this.state.hasError) return this.props.fallback
 | 
			
		||||
 | 
			
		||||
    return this.props.children
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ErrorBoundary
 | 
			
		||||
							
								
								
									
										257
									
								
								src/components/common/FilePanel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,257 @@
 | 
			
		||||
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 [editVideoIdentifier, setEditVideoIdentifier] = useState<
 | 
			
		||||
    string | null | undefined
 | 
			
		||||
  >()
 | 
			
		||||
  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
 | 
			
		||||
      // })
 | 
			
		||||
 | 
			
		||||
      const res2 = await fetch(
 | 
			
		||||
        `/arbitrary/resources?&service=FILE&name=${user.name}&includemetadata=true&limit=100&offset=0&reverse=true`
 | 
			
		||||
      )
 | 
			
		||||
      const resData = await res2.json()
 | 
			
		||||
      if (Array.isArray(resData)) {
 | 
			
		||||
        res = resData
 | 
			
		||||
      }
 | 
			
		||||
    } 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>
 | 
			
		||||
                <Button
 | 
			
		||||
                  size="small"
 | 
			
		||||
                  variant="contained"
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    setEditVideoIdentifier(video.identifier)
 | 
			
		||||
                    setIsOpenVideoModal(true)
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  Edit
 | 
			
		||||
                </Button>
 | 
			
		||||
              </ListItem>
 | 
			
		||||
            ))}
 | 
			
		||||
          </List>
 | 
			
		||||
          <Box
 | 
			
		||||
            sx={{
 | 
			
		||||
              width: '100%',
 | 
			
		||||
              display: 'flex',
 | 
			
		||||
              justifyContent: 'center',
 | 
			
		||||
              flex: '0 0 50px'
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <PublishButton
 | 
			
		||||
              variant="contained"
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                setEditVideoIdentifier(null)
 | 
			
		||||
                setIsOpenVideoModal(true)
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              Publish new file
 | 
			
		||||
            </PublishButton>
 | 
			
		||||
          </Box>
 | 
			
		||||
        </Panel>
 | 
			
		||||
      </Drawer>
 | 
			
		||||
      <GenericModal
 | 
			
		||||
        service="FILE"
 | 
			
		||||
        identifierPrefix="qfile_qblog"
 | 
			
		||||
        onClose={() => {
 | 
			
		||||
          setIsOpenVideoModal(false)
 | 
			
		||||
          setEditVideoIdentifier(null)
 | 
			
		||||
        }}
 | 
			
		||||
        open={isOpenVideoModal}
 | 
			
		||||
        onPublish={(value) => {
 | 
			
		||||
          fetchVideos().then((fetchedVideos) => setVideos(fetchedVideos))
 | 
			
		||||
          setIsOpenVideoModal(false)
 | 
			
		||||
        }}
 | 
			
		||||
        editVideoIdentifier={editVideoIdentifier}
 | 
			
		||||
      />
 | 
			
		||||
    </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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										317
									
								
								src/components/common/GenericPublishModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,317 @@
 | 
			
		||||
import React, { useState } from 'react'
 | 
			
		||||
import {
 | 
			
		||||
  Box,
 | 
			
		||||
  Button,
 | 
			
		||||
  Modal,
 | 
			
		||||
  TextField,
 | 
			
		||||
  Typography,
 | 
			
		||||
  Select,
 | 
			
		||||
  MenuItem,
 | 
			
		||||
  FormControl,
 | 
			
		||||
  InputLabel,
 | 
			
		||||
  SelectChangeEvent,
 | 
			
		||||
  OutlinedInput,
 | 
			
		||||
  Chip,
 | 
			
		||||
  IconButton
 | 
			
		||||
} from '@mui/material'
 | 
			
		||||
import { styled } from '@mui/system'
 | 
			
		||||
import { useDropzone } from 'react-dropzone'
 | 
			
		||||
import { toBase64 } from '../../utils/toBase64'
 | 
			
		||||
import AddIcon from '@mui/icons-material/Add'
 | 
			
		||||
import CloseIcon from '@mui/icons-material/Close'
 | 
			
		||||
import { usePublishGeneric } from './PublishGeneric'
 | 
			
		||||
import { useDispatch } from 'react-redux'
 | 
			
		||||
import { setNotification } from '../../state/features/notificationsSlice'
 | 
			
		||||
 | 
			
		||||
const StyledModal = styled(Modal)(({ theme }) => ({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  alignItems: 'center',
 | 
			
		||||
  justifyContent: 'center'
 | 
			
		||||
}))
 | 
			
		||||
 | 
			
		||||
const ChipContainer = styled(Box)({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  flexWrap: 'wrap',
 | 
			
		||||
  '& > *': {
 | 
			
		||||
    margin: '4px'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const ModalContent = styled(Box)(({ theme }) => ({
 | 
			
		||||
  backgroundColor: theme.palette.background.paper,
 | 
			
		||||
  padding: theme.spacing(4),
 | 
			
		||||
  borderRadius: theme.spacing(1),
 | 
			
		||||
  width: '40%',
 | 
			
		||||
  '&:focus': {
 | 
			
		||||
    outline: 'none'
 | 
			
		||||
  }
 | 
			
		||||
}))
 | 
			
		||||
 | 
			
		||||
interface GenericModalProps {
 | 
			
		||||
  open: boolean
 | 
			
		||||
  onClose: () => void
 | 
			
		||||
  onPublish: (value: any) => void
 | 
			
		||||
  acceptedFileType?: string
 | 
			
		||||
  acceptedFileTypes?: string[]
 | 
			
		||||
  service: string
 | 
			
		||||
  identifierPrefix: string
 | 
			
		||||
  editVideoIdentifier?: string | null | undefined
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface SelectOption {
 | 
			
		||||
  id: string
 | 
			
		||||
  name: string
 | 
			
		||||
}
 | 
			
		||||
const maxSize = 500 * 1024 * 1024
 | 
			
		||||
 | 
			
		||||
export const GenericModal: React.FC<GenericModalProps> = ({
 | 
			
		||||
  open,
 | 
			
		||||
  onClose,
 | 
			
		||||
  onPublish,
 | 
			
		||||
  acceptedFileType,
 | 
			
		||||
  acceptedFileTypes,
 | 
			
		||||
  service,
 | 
			
		||||
  identifierPrefix,
 | 
			
		||||
  editVideoIdentifier
 | 
			
		||||
}) => {
 | 
			
		||||
  const [file, setFile] = useState<File | null>(null)
 | 
			
		||||
  const [title, setTitle] = useState('')
 | 
			
		||||
  const [description, setDescription] = useState('')
 | 
			
		||||
  const [selectedOption, setSelectedOption] = useState<SelectOption | null>(
 | 
			
		||||
    null
 | 
			
		||||
  )
 | 
			
		||||
  const [inputValue, setInputValue] = useState<string>('')
 | 
			
		||||
  const [chips, setChips] = useState<string[]>([])
 | 
			
		||||
 | 
			
		||||
  const [options, setOptions] = useState<SelectOption[]>([])
 | 
			
		||||
  const [tags, setTags] = useState<string[]>([])
 | 
			
		||||
  const { publishGeneric } = usePublishGeneric()
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
 | 
			
		||||
  let acceptedFile = {}
 | 
			
		||||
  if (acceptedFileType) {
 | 
			
		||||
    acceptedFile = {
 | 
			
		||||
      [acceptedFileType]: []
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const { getRootProps, getInputProps } = useDropzone({
 | 
			
		||||
    ...acceptedFile,
 | 
			
		||||
    maxFiles: 1,
 | 
			
		||||
    maxSize,
 | 
			
		||||
    onDrop: (acceptedFiles) => {
 | 
			
		||||
      setFile(acceptedFiles[0])
 | 
			
		||||
    },
 | 
			
		||||
    onDropRejected: (rejectedFiles) => {
 | 
			
		||||
      dispatch(
 | 
			
		||||
        setNotification({
 | 
			
		||||
          msg: 'Your file is over the 500mb limit.',
 | 
			
		||||
          alertType: 'error'
 | 
			
		||||
        })
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const handleTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
 | 
			
		||||
    setTitle(event.target.value)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleDescriptionChange = (
 | 
			
		||||
    event: React.ChangeEvent<HTMLInputElement>
 | 
			
		||||
  ) => {
 | 
			
		||||
    setDescription(event.target.value)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleOptionChange = (event: SelectChangeEvent<string>) => {
 | 
			
		||||
    const optionId = event.target.value
 | 
			
		||||
    const selectedOption = options.find((option) => option.id === optionId)
 | 
			
		||||
    setSelectedOption(selectedOption || null)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleChipDelete = (index: number) => {
 | 
			
		||||
    const newChips = [...chips]
 | 
			
		||||
    newChips.splice(index, 1)
 | 
			
		||||
    setChips(newChips)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleSubmit = async () => {
 | 
			
		||||
    const missingFields = []
 | 
			
		||||
 | 
			
		||||
    if (!title) missingFields.push('title')
 | 
			
		||||
    if (!file) missingFields.push('file')
 | 
			
		||||
    if (missingFields.length > 0) {
 | 
			
		||||
      const missingFieldsString = missingFields.join(', ')
 | 
			
		||||
      const errMsg = `Missing: ${missingFieldsString}`
 | 
			
		||||
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (!file) return
 | 
			
		||||
 | 
			
		||||
    const formattedTags: { [key: string]: string } = {}
 | 
			
		||||
    chips.forEach((tag, i) => {
 | 
			
		||||
      formattedTags[`tag${i + 1}`] = tag
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const base64 = await toBase64(file)
 | 
			
		||||
      if (typeof base64 !== 'string') return
 | 
			
		||||
      const base64String = base64.split(',')[1]
 | 
			
		||||
      const fileExtension = file?.name?.split('.')?.pop()
 | 
			
		||||
      const fileTitle = title?.replace(/ /g, '_')?.slice(0, 20)
 | 
			
		||||
      const filename = `${fileTitle}.${fileExtension}`
 | 
			
		||||
      const res = await publishGeneric({
 | 
			
		||||
        editVideoIdentifier,
 | 
			
		||||
        service,
 | 
			
		||||
        identifierPrefix,
 | 
			
		||||
        title,
 | 
			
		||||
        description,
 | 
			
		||||
        // base64: base64String,
 | 
			
		||||
        file,
 | 
			
		||||
        filename: filename,
 | 
			
		||||
        category: selectedOption?.id || '',
 | 
			
		||||
        ...formattedTags
 | 
			
		||||
      })
 | 
			
		||||
      onPublish(res)
 | 
			
		||||
      setFile(null)
 | 
			
		||||
      setTitle('')
 | 
			
		||||
      setDescription('')
 | 
			
		||||
      onClose()
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleInputChange = (event: any) => {
 | 
			
		||||
    setInputValue(event.target.value)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleInputKeyDown = (event: any) => {
 | 
			
		||||
    if (event.key === 'Enter' && inputValue !== '') {
 | 
			
		||||
      if (chips.length < 5) {
 | 
			
		||||
        setChips([...chips, inputValue])
 | 
			
		||||
        setInputValue('')
 | 
			
		||||
      } else {
 | 
			
		||||
        event.preventDefault()
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const addChip = () => {
 | 
			
		||||
    if (chips.length < 5) {
 | 
			
		||||
      setChips([...chips, inputValue])
 | 
			
		||||
      setInputValue('')
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getListCategories = React.useCallback(async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const url = `/arbitrary/categories`
 | 
			
		||||
      const response = await fetch(url, {
 | 
			
		||||
        method: 'GET',
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'application/json'
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      const responseData = await response.json()
 | 
			
		||||
      setOptions(responseData)
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    getListCategories()
 | 
			
		||||
  }, [getListCategories])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <StyledModal open={open} onClose={onClose}>
 | 
			
		||||
      <ModalContent>
 | 
			
		||||
        {editVideoIdentifier && (
 | 
			
		||||
          <Typography variant="h6">
 | 
			
		||||
            You are editing: {editVideoIdentifier}
 | 
			
		||||
          </Typography>
 | 
			
		||||
        )}
 | 
			
		||||
        <Typography variant="h6" component="h2" gutterBottom>
 | 
			
		||||
          Upload {service}
 | 
			
		||||
        </Typography>
 | 
			
		||||
        <Box
 | 
			
		||||
          {...getRootProps()}
 | 
			
		||||
          sx={{
 | 
			
		||||
            border: '1px dashed gray',
 | 
			
		||||
            padding: 2,
 | 
			
		||||
            textAlign: 'center',
 | 
			
		||||
            marginBottom: 2
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <input {...getInputProps()} />
 | 
			
		||||
          <Typography>
 | 
			
		||||
            {file
 | 
			
		||||
              ? file.name
 | 
			
		||||
              : 'Drag and drop a file here or click to select a file'}
 | 
			
		||||
          </Typography>
 | 
			
		||||
        </Box>
 | 
			
		||||
        <TextField
 | 
			
		||||
          label="Title"
 | 
			
		||||
          variant="outlined"
 | 
			
		||||
          fullWidth
 | 
			
		||||
          value={title}
 | 
			
		||||
          onChange={handleTitleChange}
 | 
			
		||||
          inputProps={{ maxLength: 40 }}
 | 
			
		||||
          sx={{ marginBottom: 2 }}
 | 
			
		||||
        />
 | 
			
		||||
        <TextField
 | 
			
		||||
          label="Description"
 | 
			
		||||
          variant="outlined"
 | 
			
		||||
          fullWidth
 | 
			
		||||
          multiline
 | 
			
		||||
          rows={4}
 | 
			
		||||
          value={description}
 | 
			
		||||
          onChange={handleDescriptionChange}
 | 
			
		||||
          inputProps={{ maxLength: 180 }}
 | 
			
		||||
          sx={{ marginBottom: 2 }}
 | 
			
		||||
        />
 | 
			
		||||
        {options.length > 0 && (
 | 
			
		||||
          <FormControl fullWidth sx={{ marginBottom: 2 }}>
 | 
			
		||||
            <InputLabel id="Category">Select a Category</InputLabel>
 | 
			
		||||
            <Select
 | 
			
		||||
              labelId="Category"
 | 
			
		||||
              input={<OutlinedInput label="Select a Category" />}
 | 
			
		||||
              value={selectedOption?.id || ''}
 | 
			
		||||
              onChange={handleOptionChange}
 | 
			
		||||
            >
 | 
			
		||||
              {options.map((option) => (
 | 
			
		||||
                <MenuItem key={option.id} value={option.id}>
 | 
			
		||||
                  {option.name}
 | 
			
		||||
                </MenuItem>
 | 
			
		||||
              ))}
 | 
			
		||||
            </Select>
 | 
			
		||||
          </FormControl>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        <FormControl fullWidth sx={{ marginBottom: 2 }}>
 | 
			
		||||
          <Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
 | 
			
		||||
            <TextField
 | 
			
		||||
              label="Add a tag"
 | 
			
		||||
              value={inputValue}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              onKeyDown={handleInputKeyDown}
 | 
			
		||||
              disabled={chips.length === 3}
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            <IconButton onClick={addChip} disabled={chips.length === 3}>
 | 
			
		||||
              <AddIcon />
 | 
			
		||||
            </IconButton>
 | 
			
		||||
          </Box>
 | 
			
		||||
          <ChipContainer>
 | 
			
		||||
            {chips.map((chip, index) => (
 | 
			
		||||
              <Chip
 | 
			
		||||
                key={index}
 | 
			
		||||
                label={chip}
 | 
			
		||||
                onDelete={() => handleChipDelete(index)}
 | 
			
		||||
                deleteIcon={<CloseIcon />}
 | 
			
		||||
              />
 | 
			
		||||
            ))}
 | 
			
		||||
          </ChipContainer>
 | 
			
		||||
        </FormControl>
 | 
			
		||||
        <Button variant="contained" color="primary" onClick={handleSubmit}>
 | 
			
		||||
          Submit
 | 
			
		||||
        </Button>
 | 
			
		||||
      </ModalContent>
 | 
			
		||||
    </StyledModal>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										89
									
								
								src/components/common/ImageUploader.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,89 @@
 | 
			
		||||
import React, { useCallback } from 'react'
 | 
			
		||||
import { Box, Button, TextField, Typography, Modal } from '@mui/material'
 | 
			
		||||
import {
 | 
			
		||||
  useDropzone,
 | 
			
		||||
  DropzoneRootProps,
 | 
			
		||||
  DropzoneInputProps
 | 
			
		||||
} from 'react-dropzone'
 | 
			
		||||
import Compressor from 'compressorjs'
 | 
			
		||||
 | 
			
		||||
const toBase64 = (file: File): Promise<string | ArrayBuffer | null> =>
 | 
			
		||||
  new Promise((resolve, reject) => {
 | 
			
		||||
    const reader = new FileReader()
 | 
			
		||||
    reader.readAsDataURL(file)
 | 
			
		||||
    reader.onload = () => resolve(reader.result)
 | 
			
		||||
    reader.onerror = (error) => {
 | 
			
		||||
      reject(error)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
interface ImageUploaderProps {
 | 
			
		||||
  children: React.ReactNode
 | 
			
		||||
  onPick: (base64Img: string) => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ImageUploader: React.FC<ImageUploaderProps> = ({ children, onPick }) => {
 | 
			
		||||
  const onDrop = useCallback(
 | 
			
		||||
    async (acceptedFiles: File[]) => {
 | 
			
		||||
      if (acceptedFiles.length > 1) {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      let compressedFile: File | undefined
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        const image = acceptedFiles[0]
 | 
			
		||||
        await new Promise<void>((resolve) => {
 | 
			
		||||
          new Compressor(image, {
 | 
			
		||||
            quality: 0.6,
 | 
			
		||||
            maxWidth: 1200,
 | 
			
		||||
            mimeType: 'image/webp',
 | 
			
		||||
            success(result) {
 | 
			
		||||
              const file = new File([result], 'name', {
 | 
			
		||||
                type: 'image/webp'
 | 
			
		||||
              })
 | 
			
		||||
              compressedFile = file
 | 
			
		||||
              resolve()
 | 
			
		||||
            },
 | 
			
		||||
            error(err) {}
 | 
			
		||||
          })
 | 
			
		||||
        })
 | 
			
		||||
        if (!compressedFile) return
 | 
			
		||||
        const base64Img = await toBase64(compressedFile)
 | 
			
		||||
 | 
			
		||||
        onPick(base64Img as string)
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error(error)
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [onPick]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const {
 | 
			
		||||
    getRootProps,
 | 
			
		||||
    getInputProps,
 | 
			
		||||
    isDragActive
 | 
			
		||||
  }: {
 | 
			
		||||
    getRootProps: () => DropzoneRootProps
 | 
			
		||||
    getInputProps: () => DropzoneInputProps
 | 
			
		||||
    isDragActive: boolean
 | 
			
		||||
  } = useDropzone({
 | 
			
		||||
    onDrop,
 | 
			
		||||
    accept: {
 | 
			
		||||
      'image/*': []
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
      {...getRootProps()}
 | 
			
		||||
      sx={{
 | 
			
		||||
        display: 'flex'
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <input {...getInputProps()} />
 | 
			
		||||
      {children}
 | 
			
		||||
    </Box>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ImageUploader
 | 
			
		||||
							
								
								
									
										47
									
								
								src/components/common/LazyLoad.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,47 @@
 | 
			
		||||
import React, { useState, useEffect, useRef } from 'react'
 | 
			
		||||
import { useInView } from 'react-intersection-observer'
 | 
			
		||||
import CircularProgress from '@mui/material/CircularProgress'
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  onLoadMore: () => Promise<void>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const LazyLoad: React.FC<Props> = ({ onLoadMore }) => {
 | 
			
		||||
  const [isFetching, setIsFetching] = useState<boolean>(false)
 | 
			
		||||
 | 
			
		||||
  const firstLoad = useRef(false)
 | 
			
		||||
  const [ref, inView] = useInView({
 | 
			
		||||
    threshold: 0.7
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (inView) {
 | 
			
		||||
      setIsFetching(true)
 | 
			
		||||
      onLoadMore().finally(() => {
 | 
			
		||||
        setIsFetching(false)
 | 
			
		||||
        firstLoad.current = true
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }, [inView])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      ref={ref}
 | 
			
		||||
      style={{
 | 
			
		||||
        display: 'flex',
 | 
			
		||||
        justifyContent: 'center',
 | 
			
		||||
        minHeight: '25px'
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <div
 | 
			
		||||
        style={{
 | 
			
		||||
          visibility: isFetching ? 'visible' : 'hidden'
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <CircularProgress />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default LazyLoad
 | 
			
		||||
							
								
								
									
										86
									
								
								src/components/common/Notification/Notification.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,86 @@
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux'
 | 
			
		||||
import { toast, ToastContainer, Zoom, Slide } from 'react-toastify'
 | 
			
		||||
import { removeNotification } from '../../../state/features/notificationsSlice'
 | 
			
		||||
import 'react-toastify/dist/ReactToastify.css'
 | 
			
		||||
import { RootState } from '../../../state/store'
 | 
			
		||||
 | 
			
		||||
const Notification = () => {
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
 | 
			
		||||
  const { alertTypes } = useSelector((state: RootState) => state.notifications)
 | 
			
		||||
 | 
			
		||||
  if (alertTypes.alertError) {
 | 
			
		||||
    toast.error(`❌ ${alertTypes?.alertError}`, {
 | 
			
		||||
      position: 'bottom-right',
 | 
			
		||||
      autoClose: 4000,
 | 
			
		||||
      hideProgressBar: false,
 | 
			
		||||
      closeOnClick: true,
 | 
			
		||||
      pauseOnHover: true,
 | 
			
		||||
      draggable: true,
 | 
			
		||||
      progress: undefined,
 | 
			
		||||
      icon: false
 | 
			
		||||
    })
 | 
			
		||||
    dispatch(removeNotification())
 | 
			
		||||
  }
 | 
			
		||||
  if (alertTypes.alertSuccess) {
 | 
			
		||||
    toast.success(`✔️ ${alertTypes?.alertSuccess}`, {
 | 
			
		||||
      position: 'bottom-right',
 | 
			
		||||
      autoClose: 4000,
 | 
			
		||||
      hideProgressBar: false,
 | 
			
		||||
      closeOnClick: true,
 | 
			
		||||
      pauseOnHover: true,
 | 
			
		||||
      draggable: true,
 | 
			
		||||
      progress: undefined,
 | 
			
		||||
      icon: false
 | 
			
		||||
    })
 | 
			
		||||
    dispatch(removeNotification())
 | 
			
		||||
  }
 | 
			
		||||
  if (alertTypes.alertInfo) {
 | 
			
		||||
    toast.info(`${alertTypes?.alertInfo}`, {
 | 
			
		||||
      position: 'top-right',
 | 
			
		||||
      autoClose: 1300,
 | 
			
		||||
      hideProgressBar: false,
 | 
			
		||||
      closeOnClick: true,
 | 
			
		||||
      pauseOnHover: true,
 | 
			
		||||
      draggable: true,
 | 
			
		||||
      progress: undefined,
 | 
			
		||||
      theme: 'light'
 | 
			
		||||
    })
 | 
			
		||||
    dispatch(removeNotification())
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (alertTypes.alertInfo) {
 | 
			
		||||
    return (
 | 
			
		||||
      <ToastContainer
 | 
			
		||||
        position="top-right"
 | 
			
		||||
        autoClose={2000}
 | 
			
		||||
        hideProgressBar={false}
 | 
			
		||||
        newestOnTop={false}
 | 
			
		||||
        closeOnClick
 | 
			
		||||
        rtl={false}
 | 
			
		||||
        pauseOnFocusLoss
 | 
			
		||||
        draggable
 | 
			
		||||
        pauseOnHover
 | 
			
		||||
        theme="light"
 | 
			
		||||
        toastStyle={{ fontSize: '16px' }}
 | 
			
		||||
        transition={Slide}
 | 
			
		||||
      />
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ToastContainer
 | 
			
		||||
      transition={Zoom}
 | 
			
		||||
      position="bottom-right"
 | 
			
		||||
      autoClose={false}
 | 
			
		||||
      hideProgressBar={false}
 | 
			
		||||
      newestOnTop={false}
 | 
			
		||||
      closeOnClick
 | 
			
		||||
      rtl={false}
 | 
			
		||||
      draggable
 | 
			
		||||
      pauseOnHover
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Notification
 | 
			
		||||
							
								
								
									
										43
									
								
								src/components/common/PageLoader.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,43 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import CircularProgress from '@mui/material/CircularProgress';
 | 
			
		||||
import Box from '@mui/system/Box';
 | 
			
		||||
import { useTheme } from '@mui/material'
 | 
			
		||||
 | 
			
		||||
interface PageLoaderProps {
 | 
			
		||||
  size?: number
 | 
			
		||||
  thickness?: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const PageLoader: React.FC<PageLoaderProps> = ({
 | 
			
		||||
  size = 40,
 | 
			
		||||
  thickness = 5
 | 
			
		||||
}) => {
 | 
			
		||||
  const theme = useTheme()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
      sx={{
 | 
			
		||||
        display: 'flex',
 | 
			
		||||
        justifyContent: 'center',
 | 
			
		||||
        alignItems: 'center',
 | 
			
		||||
        height: '100vh',
 | 
			
		||||
        width: '100%',
 | 
			
		||||
        position: 'fixed',
 | 
			
		||||
        top: 0,
 | 
			
		||||
        left: 0,
 | 
			
		||||
        backgroundColor: 'rgba(255, 255, 255, 0.25)',
 | 
			
		||||
        zIndex: 1000
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <CircularProgress
 | 
			
		||||
        size={size}
 | 
			
		||||
        thickness={thickness}
 | 
			
		||||
        sx={{
 | 
			
		||||
          color: theme.palette.secondary.main
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    </Box>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default PageLoader;
 | 
			
		||||
							
								
								
									
										25
									
								
								src/components/common/Portal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,25 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react'
 | 
			
		||||
import { createPortal } from 'react-dom'
 | 
			
		||||
 | 
			
		||||
interface PortalProps {
 | 
			
		||||
  children: React.ReactNode
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Portal: React.FC<PortalProps> = ({ children }) => {
 | 
			
		||||
  const [mounted, setMounted] = useState(false)
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    setMounted(true)
 | 
			
		||||
 | 
			
		||||
    return () => setMounted(false)
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  return mounted
 | 
			
		||||
    ? createPortal(
 | 
			
		||||
        children,
 | 
			
		||||
        document.querySelector('#modal-root') as HTMLElement
 | 
			
		||||
      )
 | 
			
		||||
    : null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default Portal
 | 
			
		||||
							
								
								
									
										281
									
								
								src/components/common/PostPublishModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,281 @@
 | 
			
		||||
import React, { useState } from 'react'
 | 
			
		||||
import {
 | 
			
		||||
  Box,
 | 
			
		||||
  Button,
 | 
			
		||||
  Modal,
 | 
			
		||||
  TextField,
 | 
			
		||||
  Typography,
 | 
			
		||||
  Select,
 | 
			
		||||
  MenuItem,
 | 
			
		||||
  FormControl,
 | 
			
		||||
  InputLabel,
 | 
			
		||||
  SelectChangeEvent,
 | 
			
		||||
  OutlinedInput,
 | 
			
		||||
  Chip,
 | 
			
		||||
  IconButton
 | 
			
		||||
} from '@mui/material'
 | 
			
		||||
import { styled } from '@mui/system'
 | 
			
		||||
import { useDropzone } from 'react-dropzone'
 | 
			
		||||
import { usePublishVideo } from './PublishVideo'
 | 
			
		||||
import { toBase64 } from '../../utils/toBase64'
 | 
			
		||||
import AddIcon from '@mui/icons-material/Add'
 | 
			
		||||
import CloseIcon from '@mui/icons-material/Close'
 | 
			
		||||
const StyledModal = styled(Modal)(({ theme }) => ({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  alignItems: 'center',
 | 
			
		||||
  justifyContent: 'center'
 | 
			
		||||
}))
 | 
			
		||||
 | 
			
		||||
const ChipContainer = styled(Box)({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  flexWrap: 'wrap',
 | 
			
		||||
  '& > *': {
 | 
			
		||||
    margin: '4px'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const ModalContent = styled(Box)(({ theme }) => ({
 | 
			
		||||
  backgroundColor: theme.palette.background.paper,
 | 
			
		||||
  padding: theme.spacing(4),
 | 
			
		||||
  borderRadius: theme.spacing(1),
 | 
			
		||||
  width: '40%',
 | 
			
		||||
  '&:focus': {
 | 
			
		||||
    outline: 'none'
 | 
			
		||||
  }
 | 
			
		||||
}))
 | 
			
		||||
 | 
			
		||||
interface PostModalProps {
 | 
			
		||||
  open: boolean
 | 
			
		||||
  onClose: () => void
 | 
			
		||||
  onPublish: (value: any) => Promise<void>
 | 
			
		||||
  post: any
 | 
			
		||||
  mode?: string
 | 
			
		||||
  metadata?: any
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface SelectOption {
 | 
			
		||||
  id: string
 | 
			
		||||
  name: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const PostPublishModal: React.FC<PostModalProps> = ({
 | 
			
		||||
  open,
 | 
			
		||||
  onClose,
 | 
			
		||||
  onPublish,
 | 
			
		||||
  post,
 | 
			
		||||
  mode,
 | 
			
		||||
  metadata
 | 
			
		||||
}) => {
 | 
			
		||||
  const [file, setFile] = useState<File | null>(null)
 | 
			
		||||
  const [title, setTitle] = useState('')
 | 
			
		||||
  const [description, setDescription] = useState('')
 | 
			
		||||
  const [selectedOption, setSelectedOption] = useState<SelectOption | null>(
 | 
			
		||||
    null
 | 
			
		||||
  )
 | 
			
		||||
  const [inputValue, setInputValue] = useState<string>('')
 | 
			
		||||
  const [chips, setChips] = useState<string[]>([])
 | 
			
		||||
 | 
			
		||||
  const [options, setOptions] = useState<SelectOption[]>([])
 | 
			
		||||
  const [tags, setTags] = useState<string[]>([])
 | 
			
		||||
  const { publishVideo } = usePublishVideo()
 | 
			
		||||
  const { getRootProps, getInputProps } = useDropzone({
 | 
			
		||||
    accept: {
 | 
			
		||||
      'video/*': []
 | 
			
		||||
    },
 | 
			
		||||
    maxFiles: 1,
 | 
			
		||||
    onDrop: (acceptedFiles) => {
 | 
			
		||||
      setFile(acceptedFiles[0])
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    if (post.title) {
 | 
			
		||||
      setTitle(post.title)
 | 
			
		||||
    }
 | 
			
		||||
    // if (post.description) {
 | 
			
		||||
    //   setDescription(post.description)
 | 
			
		||||
    // }
 | 
			
		||||
  }, [post])
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    if (mode === 'edit' && metadata) {
 | 
			
		||||
      if (metadata.description) {
 | 
			
		||||
        setDescription(metadata.description)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const findCategory = options.find(
 | 
			
		||||
        (option) => option.id === metadata?.category
 | 
			
		||||
      )
 | 
			
		||||
      if (findCategory) {
 | 
			
		||||
        setSelectedOption(findCategory)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!metadata?.tags || !Array.isArray(metadata?.tags)) return
 | 
			
		||||
 | 
			
		||||
      setChips(metadata.tags.slice(0, -2))
 | 
			
		||||
    }
 | 
			
		||||
  }, [mode, metadata, options])
 | 
			
		||||
 | 
			
		||||
  const handleTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
 | 
			
		||||
    setTitle(event.target.value)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleDescriptionChange = (
 | 
			
		||||
    event: React.ChangeEvent<HTMLInputElement>
 | 
			
		||||
  ) => {
 | 
			
		||||
    setDescription(event.target.value)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleOptionChange = (event: SelectChangeEvent<string>) => {
 | 
			
		||||
    const optionId = event.target.value
 | 
			
		||||
    const selectedOption = options.find((option) => option.id === optionId)
 | 
			
		||||
    setSelectedOption(selectedOption || null)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleChipDelete = (index: number) => {
 | 
			
		||||
    const newChips = [...chips]
 | 
			
		||||
    newChips.splice(index, 1)
 | 
			
		||||
    setChips(newChips)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleSubmit = async () => {
 | 
			
		||||
    const formattedTags: { [key: string]: string } = {}
 | 
			
		||||
    chips.forEach((tag, i) => {
 | 
			
		||||
      formattedTags[`tag${i + 1}`] = tag
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      await onPublish({
 | 
			
		||||
        title,
 | 
			
		||||
        description,
 | 
			
		||||
        tags: chips,
 | 
			
		||||
        category: selectedOption?.id || ''
 | 
			
		||||
      })
 | 
			
		||||
      setFile(null)
 | 
			
		||||
      setTitle('')
 | 
			
		||||
      setDescription('')
 | 
			
		||||
      onClose()
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleInputChange = (event: any) => {
 | 
			
		||||
    setInputValue(event.target.value)
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  const handleInputKeyDown = (event: any) => {
 | 
			
		||||
    if (event.key === 'Enter' && inputValue !== '') {
 | 
			
		||||
      if (chips.length < 5) {
 | 
			
		||||
        setChips([...chips, inputValue])
 | 
			
		||||
        setInputValue('')
 | 
			
		||||
      } else {
 | 
			
		||||
        event.preventDefault()
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const addChip = () => {
 | 
			
		||||
    if (chips.length < 3) {
 | 
			
		||||
      setChips([...chips, inputValue])
 | 
			
		||||
      setInputValue('')
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getListCategories = React.useCallback(async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const url = `/arbitrary/categories`
 | 
			
		||||
      const response = await fetch(url, {
 | 
			
		||||
        method: 'GET',
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'application/json'
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      const responseData = await response.json()
 | 
			
		||||
      setOptions(responseData)
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    getListCategories()
 | 
			
		||||
  }, [getListCategories])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <StyledModal open={open} onClose={onClose}>
 | 
			
		||||
      <ModalContent>
 | 
			
		||||
        <Typography variant="h6" component="h2" gutterBottom>
 | 
			
		||||
          Upload Blog Post
 | 
			
		||||
        </Typography>
 | 
			
		||||
 | 
			
		||||
        <TextField
 | 
			
		||||
          label="Post Title"
 | 
			
		||||
          variant="outlined"
 | 
			
		||||
          fullWidth
 | 
			
		||||
          value={title}
 | 
			
		||||
          onChange={handleTitleChange}
 | 
			
		||||
          inputProps={{ maxLength: 40 }}
 | 
			
		||||
          sx={{ marginBottom: 2 }}
 | 
			
		||||
          disabled
 | 
			
		||||
        />
 | 
			
		||||
        <TextField
 | 
			
		||||
          label="Post Description"
 | 
			
		||||
          variant="outlined"
 | 
			
		||||
          fullWidth
 | 
			
		||||
          multiline
 | 
			
		||||
          rows={4}
 | 
			
		||||
          value={description}
 | 
			
		||||
          onChange={handleDescriptionChange}
 | 
			
		||||
          inputProps={{ maxLength: 180 }}
 | 
			
		||||
          sx={{ marginBottom: 2 }}
 | 
			
		||||
        />
 | 
			
		||||
        {options.length > 0 && (
 | 
			
		||||
          <FormControl fullWidth sx={{ marginBottom: 2 }}>
 | 
			
		||||
            <InputLabel id="Category">Select a Category</InputLabel>
 | 
			
		||||
            <Select
 | 
			
		||||
              labelId="Category"
 | 
			
		||||
              input={<OutlinedInput label="Select a Category" />}
 | 
			
		||||
              value={selectedOption?.id || ''}
 | 
			
		||||
              onChange={handleOptionChange}
 | 
			
		||||
            >
 | 
			
		||||
              {options.map((option) => (
 | 
			
		||||
                <MenuItem key={option.id} value={option.id}>
 | 
			
		||||
                  {option.name}
 | 
			
		||||
                </MenuItem>
 | 
			
		||||
              ))}
 | 
			
		||||
            </Select>
 | 
			
		||||
          </FormControl>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        <FormControl fullWidth sx={{ marginBottom: 2 }}>
 | 
			
		||||
          <Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
 | 
			
		||||
            <TextField
 | 
			
		||||
              label="Add a tag"
 | 
			
		||||
              value={inputValue}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              onKeyDown={handleInputKeyDown}
 | 
			
		||||
              disabled={chips.length === 3}
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            <IconButton onClick={addChip} disabled={chips.length === 3}>
 | 
			
		||||
              <AddIcon />
 | 
			
		||||
            </IconButton>
 | 
			
		||||
          </Box>
 | 
			
		||||
          <ChipContainer>
 | 
			
		||||
            {chips.map((chip, index) => (
 | 
			
		||||
              <Chip
 | 
			
		||||
                key={index}
 | 
			
		||||
                label={chip}
 | 
			
		||||
                onDelete={() => handleChipDelete(index)}
 | 
			
		||||
                deleteIcon={<CloseIcon />}
 | 
			
		||||
              />
 | 
			
		||||
            ))}
 | 
			
		||||
          </ChipContainer>
 | 
			
		||||
        </FormControl>
 | 
			
		||||
        <Button variant="contained" color="primary" onClick={handleSubmit}>
 | 
			
		||||
          Submit
 | 
			
		||||
        </Button>
 | 
			
		||||
      </ModalContent>
 | 
			
		||||
    </StyledModal>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default PostPublishModal
 | 
			
		||||
							
								
								
									
										111
									
								
								src/components/common/PublishAudio.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,111 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux'
 | 
			
		||||
import { setNotification } from '../../state/features/notificationsSlice'
 | 
			
		||||
import { RootState } from '../../state/store'
 | 
			
		||||
import ShortUniqueId from 'short-unique-id'
 | 
			
		||||
 | 
			
		||||
const uid = new ShortUniqueId()
 | 
			
		||||
 | 
			
		||||
interface IPublishVideo {
 | 
			
		||||
  title: string
 | 
			
		||||
  description: string
 | 
			
		||||
  base64: string
 | 
			
		||||
  category: string
 | 
			
		||||
    editVideoIdentifier?: string | null | undefined
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const usePublishAudio = () => {
 | 
			
		||||
  const { user } = useSelector((state: RootState) => state.auth)
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
  const publishAudio = async ({
 | 
			
		||||
    editVideoIdentifier,
 | 
			
		||||
    title,
 | 
			
		||||
    description,
 | 
			
		||||
    base64,
 | 
			
		||||
    category,
 | 
			
		||||
    ...rest
 | 
			
		||||
  }: IPublishVideo) => {
 | 
			
		||||
    let address
 | 
			
		||||
    let name
 | 
			
		||||
    let errorMsg = ''
 | 
			
		||||
 | 
			
		||||
    address = user?.address
 | 
			
		||||
    name = user?.name || ''
 | 
			
		||||
 | 
			
		||||
    const missingFields = []
 | 
			
		||||
    if (!address) {
 | 
			
		||||
      errorMsg = "Cannot post: your address isn't available"
 | 
			
		||||
    }
 | 
			
		||||
    if (!name) {
 | 
			
		||||
      errorMsg = 'Cannot post without a name'
 | 
			
		||||
    }
 | 
			
		||||
    if (!title) missingFields.push('title')
 | 
			
		||||
    if (missingFields.length > 0) {
 | 
			
		||||
      const missingFieldsString = missingFields.join(', ')
 | 
			
		||||
      const errMsg = `Missing: ${missingFieldsString}`
 | 
			
		||||
      errorMsg = errMsg
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (errorMsg) {
 | 
			
		||||
      dispatch(
 | 
			
		||||
        setNotification({
 | 
			
		||||
          msg: errorMsg,
 | 
			
		||||
          alertType: 'error'
 | 
			
		||||
        })
 | 
			
		||||
      )
 | 
			
		||||
      throw new Error(errorMsg)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const id = uid()
 | 
			
		||||
 | 
			
		||||
      let identifier = `qaudio_qblog_${id}`
 | 
			
		||||
      if(editVideoIdentifier){
 | 
			
		||||
        identifier = editVideoIdentifier
 | 
			
		||||
      }
 | 
			
		||||
      const resourceResponse = await qortalRequest({
 | 
			
		||||
        action: 'PUBLISH_QDN_RESOURCE',
 | 
			
		||||
        name: name,
 | 
			
		||||
        service: 'AUDIO',
 | 
			
		||||
        data64: base64,
 | 
			
		||||
        title: title,
 | 
			
		||||
        description: description,
 | 
			
		||||
        category: category,
 | 
			
		||||
        ...rest,
 | 
			
		||||
        identifier: identifier
 | 
			
		||||
      })
 | 
			
		||||
      dispatch(
 | 
			
		||||
        setNotification({
 | 
			
		||||
          msg: 'Audio successfully published',
 | 
			
		||||
          alertType: 'success'
 | 
			
		||||
        })
 | 
			
		||||
      )
 | 
			
		||||
      return resourceResponse
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
      let notificationObj = null
 | 
			
		||||
      if (typeof error === 'string') {
 | 
			
		||||
        notificationObj = {
 | 
			
		||||
          msg: error || 'Failed to publish audio',
 | 
			
		||||
          alertType: 'error'
 | 
			
		||||
        }
 | 
			
		||||
      } else if (typeof error?.error === 'string') {
 | 
			
		||||
        notificationObj = {
 | 
			
		||||
          msg: error?.error || 'Failed to publish audio',
 | 
			
		||||
          alertType: 'error'
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        notificationObj = {
 | 
			
		||||
          msg: error?.message || error?.message || 'Failed to publish audio',
 | 
			
		||||
          alertType: 'error'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (!notificationObj) return
 | 
			
		||||
      dispatch(setNotification(notificationObj))
 | 
			
		||||
     
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return {
 | 
			
		||||
    publishAudio
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										120
									
								
								src/components/common/PublishGeneric.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,120 @@
 | 
			
		||||
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
 | 
			
		||||
  file?: File
 | 
			
		||||
  category: string
 | 
			
		||||
  service: string
 | 
			
		||||
  identifierPrefix: string
 | 
			
		||||
  filename: string
 | 
			
		||||
  editVideoIdentifier?: string | null | undefined
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const usePublishGeneric = () => {
 | 
			
		||||
  const { user } = useSelector((state: RootState) => state.auth)
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
  const publishGeneric = async ({
 | 
			
		||||
    editVideoIdentifier,
 | 
			
		||||
    service,
 | 
			
		||||
    identifierPrefix,
 | 
			
		||||
    filename,
 | 
			
		||||
    title,
 | 
			
		||||
    description,
 | 
			
		||||
    base64,
 | 
			
		||||
    file,
 | 
			
		||||
    category,
 | 
			
		||||
    ...rest
 | 
			
		||||
  }: IPublishGeneric) => {
 | 
			
		||||
    let address
 | 
			
		||||
    let name
 | 
			
		||||
    let errorMsg = ''
 | 
			
		||||
 | 
			
		||||
    address = user?.address
 | 
			
		||||
    name = user?.name || ''
 | 
			
		||||
 | 
			
		||||
    const missingFields = []
 | 
			
		||||
    if (!address) {
 | 
			
		||||
      errorMsg = "Cannot post: your address isn't available"
 | 
			
		||||
    }
 | 
			
		||||
    if (!name) {
 | 
			
		||||
      errorMsg = 'Cannot post without a name'
 | 
			
		||||
    }
 | 
			
		||||
    if (!title) missingFields.push('title')
 | 
			
		||||
    if (missingFields.length > 0) {
 | 
			
		||||
      const missingFieldsString = missingFields.join(', ')
 | 
			
		||||
      const errMsg = `Missing: ${missingFieldsString}`
 | 
			
		||||
      errorMsg = errMsg
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (errorMsg) {
 | 
			
		||||
      dispatch(
 | 
			
		||||
        setNotification({
 | 
			
		||||
          msg: errorMsg,
 | 
			
		||||
          alertType: 'error'
 | 
			
		||||
        })
 | 
			
		||||
      )
 | 
			
		||||
      throw new Error(errorMsg)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const id = uid()
 | 
			
		||||
 | 
			
		||||
      let identifier = `${identifierPrefix}_${id}`
 | 
			
		||||
      if (editVideoIdentifier) {
 | 
			
		||||
        identifier = editVideoIdentifier
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const resourceResponse = await qortalRequest({
 | 
			
		||||
        action: 'PUBLISH_QDN_RESOURCE',
 | 
			
		||||
        name: name,
 | 
			
		||||
        service: service,
 | 
			
		||||
        file,
 | 
			
		||||
        title: title,
 | 
			
		||||
        description: description,
 | 
			
		||||
        category: category,
 | 
			
		||||
        filename,
 | 
			
		||||
        ...rest,
 | 
			
		||||
        identifier: identifier
 | 
			
		||||
      })
 | 
			
		||||
      dispatch(
 | 
			
		||||
        setNotification({
 | 
			
		||||
          msg: `${service} successfully published`,
 | 
			
		||||
          alertType: 'success'
 | 
			
		||||
        })
 | 
			
		||||
      )
 | 
			
		||||
      return resourceResponse
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
      let notificationObj = null
 | 
			
		||||
      if (typeof error === 'string') {
 | 
			
		||||
        notificationObj = {
 | 
			
		||||
          msg: error || `Failed to publish ${service}`,
 | 
			
		||||
          alertType: 'error'
 | 
			
		||||
        }
 | 
			
		||||
      } else if (typeof error?.error === 'string') {
 | 
			
		||||
        notificationObj = {
 | 
			
		||||
          msg: error?.error || `Failed to publish ${service}`,
 | 
			
		||||
          alertType: 'error'
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        notificationObj = {
 | 
			
		||||
          msg:
 | 
			
		||||
            error?.message || error?.message || `Failed to publish ${service}`,
 | 
			
		||||
          alertType: 'error'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (!notificationObj) return
 | 
			
		||||
      dispatch(setNotification(notificationObj))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return {
 | 
			
		||||
    publishGeneric
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										112
									
								
								src/components/common/PublishVideo.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,112 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux'
 | 
			
		||||
import { setNotification } from '../../state/features/notificationsSlice'
 | 
			
		||||
import { RootState } from '../../state/store'
 | 
			
		||||
import ShortUniqueId from 'short-unique-id'
 | 
			
		||||
 | 
			
		||||
const uid = new ShortUniqueId()
 | 
			
		||||
 | 
			
		||||
interface IPublishVideo {
 | 
			
		||||
  title: string
 | 
			
		||||
  description: string
 | 
			
		||||
  base64?: string
 | 
			
		||||
  category: string
 | 
			
		||||
  editVideoIdentifier?: string | null | undefined
 | 
			
		||||
  file?: File
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const usePublishVideo = () => {
 | 
			
		||||
  const { user } = useSelector((state: RootState) => state.auth)
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
  const publishVideo = async ({
 | 
			
		||||
    file,
 | 
			
		||||
    editVideoIdentifier,
 | 
			
		||||
    title,
 | 
			
		||||
    description,
 | 
			
		||||
    base64,
 | 
			
		||||
    category,
 | 
			
		||||
    ...rest
 | 
			
		||||
  }: IPublishVideo) => {
 | 
			
		||||
    let address
 | 
			
		||||
    let name
 | 
			
		||||
    let errorMsg = ''
 | 
			
		||||
 | 
			
		||||
    address = user?.address
 | 
			
		||||
    name = user?.name || ''
 | 
			
		||||
 | 
			
		||||
    const missingFields = []
 | 
			
		||||
    if (!address) {
 | 
			
		||||
      errorMsg = "Cannot post: your address isn't available"
 | 
			
		||||
    }
 | 
			
		||||
    if (!name) {
 | 
			
		||||
      errorMsg = 'Cannot post without a name'
 | 
			
		||||
    }
 | 
			
		||||
    if (!title) missingFields.push('title')
 | 
			
		||||
    if (missingFields.length > 0) {
 | 
			
		||||
      const missingFieldsString = missingFields.join(', ')
 | 
			
		||||
      const errMsg = `Missing: ${missingFieldsString}`
 | 
			
		||||
      errorMsg = errMsg
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (errorMsg) {
 | 
			
		||||
      dispatch(
 | 
			
		||||
        setNotification({
 | 
			
		||||
          msg: errorMsg,
 | 
			
		||||
          alertType: 'error'
 | 
			
		||||
        })
 | 
			
		||||
      )
 | 
			
		||||
      throw new Error(errorMsg)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const id = uid()
 | 
			
		||||
 | 
			
		||||
      let identifier = `qvideo_qblog_${id}`
 | 
			
		||||
      if (editVideoIdentifier) {
 | 
			
		||||
        identifier = editVideoIdentifier
 | 
			
		||||
      }
 | 
			
		||||
      const resourceResponse = await qortalRequest({
 | 
			
		||||
        action: 'PUBLISH_QDN_RESOURCE',
 | 
			
		||||
        name: name,
 | 
			
		||||
        service: 'VIDEO',
 | 
			
		||||
        // data64: base64,
 | 
			
		||||
        file: file,
 | 
			
		||||
        title: title,
 | 
			
		||||
        description: description,
 | 
			
		||||
        category: category,
 | 
			
		||||
        ...rest,
 | 
			
		||||
        identifier: identifier
 | 
			
		||||
      })
 | 
			
		||||
      dispatch(
 | 
			
		||||
        setNotification({
 | 
			
		||||
          msg: 'Video successfully published',
 | 
			
		||||
          alertType: 'success'
 | 
			
		||||
        })
 | 
			
		||||
      )
 | 
			
		||||
      return resourceResponse
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
      let notificationObj = null
 | 
			
		||||
      if (typeof error === 'string') {
 | 
			
		||||
        notificationObj = {
 | 
			
		||||
          msg: error || 'Failed to publish video',
 | 
			
		||||
          alertType: 'error'
 | 
			
		||||
        }
 | 
			
		||||
      } else if (typeof error?.error === 'string') {
 | 
			
		||||
        notificationObj = {
 | 
			
		||||
          msg: error?.error || 'Failed to publish video',
 | 
			
		||||
          alertType: 'error'
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        notificationObj = {
 | 
			
		||||
          msg: error?.message || 'Failed to publish video',
 | 
			
		||||
          alertType: 'error'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (!notificationObj) return
 | 
			
		||||
      dispatch(setNotification(notificationObj))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return {
 | 
			
		||||
    publishVideo
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										124
									
								
								src/components/common/ResponsiveImage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,124 @@
 | 
			
		||||
import React, { useState, useEffect, CSSProperties } from 'react'
 | 
			
		||||
import Skeleton from '@mui/material/Skeleton'
 | 
			
		||||
import { Box } from '@mui/material'
 | 
			
		||||
 | 
			
		||||
interface ResponsiveImageProps {
 | 
			
		||||
  src: string
 | 
			
		||||
  dimensions: string
 | 
			
		||||
  alt?: string
 | 
			
		||||
  className?: string
 | 
			
		||||
  style?: CSSProperties
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ResponsiveImage: React.FC<ResponsiveImageProps> = ({
 | 
			
		||||
  src,
 | 
			
		||||
  dimensions,
 | 
			
		||||
  alt,
 | 
			
		||||
  className,
 | 
			
		||||
  style
 | 
			
		||||
}) => {
 | 
			
		||||
  const [loading, setLoading] = useState(true)
 | 
			
		||||
  const matchResult = dimensions?.match(/v1\.(\d+(\.\d+)?)x(\d+)/)
 | 
			
		||||
 | 
			
		||||
  const width = matchResult ? parseFloat(matchResult[1]) : 1 // Default width value
 | 
			
		||||
  const height = matchResult ? parseInt(matchResult[3], 10) : 1 // Default height value
 | 
			
		||||
 | 
			
		||||
  const aspectRatio = (height / width) * 100
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (dimensions === 'v1.0x0') {
 | 
			
		||||
      setLoading(false)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
  }, [dimensions])
 | 
			
		||||
 | 
			
		||||
  if (dimensions === 'v1.0x0' || !dimensions) {
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const imageStyle: CSSProperties = {
 | 
			
		||||
    width: '100%',
 | 
			
		||||
    height: '100%',
 | 
			
		||||
    objectFit: 'cover'
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const wrapperStyle: CSSProperties = {
 | 
			
		||||
    position: 'relative',
 | 
			
		||||
    paddingBottom: `${aspectRatio}%`,
 | 
			
		||||
    overflow: 'hidden',
 | 
			
		||||
    ...style
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
      sx={{
 | 
			
		||||
        padding: '2px'
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {/* <img
 | 
			
		||||
        onLoad={() => setLoading(false)}
 | 
			
		||||
        src={src}
 | 
			
		||||
        style={{
 | 
			
		||||
          width: '100%',
 | 
			
		||||
          height: 'auto',
 | 
			
		||||
          borderRadius: '8px'
 | 
			
		||||
        }}
 | 
			
		||||
      /> */}
 | 
			
		||||
      {loading && (
 | 
			
		||||
        <Skeleton
 | 
			
		||||
          variant="rectangular"
 | 
			
		||||
          style={{
 | 
			
		||||
            width: '100%',
 | 
			
		||||
            height: 0,
 | 
			
		||||
            paddingBottom: `${(height / width) * 100}%`,
 | 
			
		||||
            objectFit: 'contain',
 | 
			
		||||
            visibility: loading ? 'visible' : 'hidden',
 | 
			
		||||
            borderRadius: '8px'
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      <img
 | 
			
		||||
        onLoad={() => setLoading(false)}
 | 
			
		||||
        src={src}
 | 
			
		||||
        style={{
 | 
			
		||||
          width: '100%',
 | 
			
		||||
          height: 'auto',
 | 
			
		||||
          borderRadius: '8px',
 | 
			
		||||
          visibility: loading ? 'hidden' : 'visible',
 | 
			
		||||
          position: loading ? 'absolute' : 'unset'
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    </Box>
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div style={wrapperStyle} className={className}>
 | 
			
		||||
      {loading ? (
 | 
			
		||||
        <Skeleton
 | 
			
		||||
          variant="rectangular"
 | 
			
		||||
          sx={{
 | 
			
		||||
            position: 'absolute',
 | 
			
		||||
            top: 0,
 | 
			
		||||
            left: 0,
 | 
			
		||||
            right: 0,
 | 
			
		||||
            bottom: 0
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <img
 | 
			
		||||
          src={src}
 | 
			
		||||
          alt={alt}
 | 
			
		||||
          style={{
 | 
			
		||||
            ...imageStyle,
 | 
			
		||||
            position: 'absolute',
 | 
			
		||||
            top: 0,
 | 
			
		||||
            left: 0
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ResponsiveImage
 | 
			
		||||
							
								
								
									
										289
									
								
								src/components/common/Tipping/Tipping.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,289 @@
 | 
			
		||||
import {
 | 
			
		||||
  Avatar,
 | 
			
		||||
  Box,
 | 
			
		||||
  Button,
 | 
			
		||||
  Dialog,
 | 
			
		||||
  DialogActions,
 | 
			
		||||
  DialogContent,
 | 
			
		||||
  DialogTitle,
 | 
			
		||||
  Input,
 | 
			
		||||
  InputAdornment,
 | 
			
		||||
  InputLabel,
 | 
			
		||||
  Tooltip,
 | 
			
		||||
  Typography,
 | 
			
		||||
  useTheme
 | 
			
		||||
} from '@mui/material'
 | 
			
		||||
import React, { useCallback, useState } from 'react'
 | 
			
		||||
import { CardContentContainerComment } from '../../../pages/BlogList/PostPreview-styles'
 | 
			
		||||
import { StyledCardHeaderComment } from '../../../pages/BlogList/PostPreview-styles'
 | 
			
		||||
import { StyledCardColComment } from '../../../pages/BlogList/PostPreview-styles'
 | 
			
		||||
import { AuthorTextComment } from '../../../pages/BlogList/PostPreview-styles'
 | 
			
		||||
import { StyledCardContentComment } from '../../../pages/BlogList/PostPreview-styles'
 | 
			
		||||
import MenuItem from '@mui/material/MenuItem'
 | 
			
		||||
import Select, { SelectChangeEvent } from '@mui/material/Select'
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux'
 | 
			
		||||
import { RootState } from '../../../state/store'
 | 
			
		||||
import Portal from '../Portal'
 | 
			
		||||
import MonetizationOnIcon from '@mui/icons-material/MonetizationOn'
 | 
			
		||||
interface TippingProps {
 | 
			
		||||
  name: string
 | 
			
		||||
  onSubmit: () => void
 | 
			
		||||
  onClose: () => void
 | 
			
		||||
  onlyIcon?: boolean
 | 
			
		||||
}
 | 
			
		||||
import QORT from '../../../assets/img/qort.png'
 | 
			
		||||
import ARRR from '../../../assets/img/arrr.png'
 | 
			
		||||
import LTC from '../../../assets/img/ltc.png'
 | 
			
		||||
import BTC from '../../../assets/img/btc.png'
 | 
			
		||||
import DOGE from '../../../assets/img/doge.png'
 | 
			
		||||
import DGB from '../../../assets/img/dgb.png'
 | 
			
		||||
import RVN from '../../../assets/img/rvn.png'
 | 
			
		||||
import { setNotification } from '../../../state/features/notificationsSlice'
 | 
			
		||||
const coins = [
 | 
			
		||||
  { value: 'QORT', label: 'QORT' },
 | 
			
		||||
  { value: 'ARRR', label: 'ARRR' },
 | 
			
		||||
  { value: 'LTC', label: 'LTC' },
 | 
			
		||||
  { value: 'BTC', label: 'BTC' },
 | 
			
		||||
  { value: 'DOGE', label: 'DOGE' },
 | 
			
		||||
  { value: 'DGB', label: 'DGB' },
 | 
			
		||||
  { value: 'RVN', label: 'RVN' }
 | 
			
		||||
]
 | 
			
		||||
export const Tipping = ({
 | 
			
		||||
  onSubmit,
 | 
			
		||||
  onClose,
 | 
			
		||||
  name,
 | 
			
		||||
  onlyIcon
 | 
			
		||||
}: TippingProps) => {
 | 
			
		||||
  const { user } = useSelector((state: RootState) => state.auth)
 | 
			
		||||
  const [isOpen, setIsOpen] = useState<boolean>(false)
 | 
			
		||||
  const [selectedCoin, setSelectedCoint] = useState<any>(coins[0])
 | 
			
		||||
  const [amount, setAmount] = useState<number>(0)
 | 
			
		||||
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
 | 
			
		||||
  const resetValues = () => {
 | 
			
		||||
    setSelectedCoint(coins[0])
 | 
			
		||||
    setAmount(0)
 | 
			
		||||
    setIsOpen(false)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const sendCoin = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      if (!name) return
 | 
			
		||||
      let res = await qortalRequest({
 | 
			
		||||
        action: 'GET_NAME_DATA',
 | 
			
		||||
        name: name
 | 
			
		||||
      })
 | 
			
		||||
      const address = res.owner
 | 
			
		||||
      if (!address || !amount || !selectedCoin?.value) return
 | 
			
		||||
 | 
			
		||||
      if (isNaN(amount)) return
 | 
			
		||||
      await qortalRequest({
 | 
			
		||||
        action: 'SEND_COIN',
 | 
			
		||||
        coin: selectedCoin.value,
 | 
			
		||||
        destinationAddress: address,
 | 
			
		||||
        amount: amount
 | 
			
		||||
      })
 | 
			
		||||
      dispatch(
 | 
			
		||||
        setNotification({
 | 
			
		||||
          msg: 'Coin successfully sent',
 | 
			
		||||
          alertType: 'success'
 | 
			
		||||
        })
 | 
			
		||||
      )
 | 
			
		||||
      resetValues()
 | 
			
		||||
      onSubmit()
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
      let notificationObj = null
 | 
			
		||||
      if (typeof error === 'string') {
 | 
			
		||||
        notificationObj = {
 | 
			
		||||
          msg: error || 'Failed to send coin',
 | 
			
		||||
          alertType: 'error'
 | 
			
		||||
        }
 | 
			
		||||
      } else if (typeof error?.error === 'string') {
 | 
			
		||||
        notificationObj = {
 | 
			
		||||
          msg: error?.error || 'Failed to send coin',
 | 
			
		||||
          alertType: 'error'
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        notificationObj = {
 | 
			
		||||
          msg: error?.message || 'Failed to send coin',
 | 
			
		||||
          alertType: 'error'
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (!notificationObj) return
 | 
			
		||||
      dispatch(setNotification(notificationObj))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleOptionChange = (event: SelectChangeEvent<string>) => {
 | 
			
		||||
    const optionId = event.target.value
 | 
			
		||||
    const selectedOption = coins.find(
 | 
			
		||||
      (option: any) => option.value === optionId
 | 
			
		||||
    )
 | 
			
		||||
    setSelectedCoint(selectedOption || null)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getLogo = (coin: string) => {
 | 
			
		||||
    switch (coin) {
 | 
			
		||||
      case 'QORT':
 | 
			
		||||
        return QORT
 | 
			
		||||
      case 'ARRR':
 | 
			
		||||
        return ARRR
 | 
			
		||||
      case 'LTC':
 | 
			
		||||
        return LTC
 | 
			
		||||
      case 'BTC':
 | 
			
		||||
        return BTC
 | 
			
		||||
      case 'DOGE':
 | 
			
		||||
        return DOGE
 | 
			
		||||
      case 'DGB':
 | 
			
		||||
        return DGB
 | 
			
		||||
      case 'RVN':
 | 
			
		||||
        return RVN
 | 
			
		||||
      default:
 | 
			
		||||
        ''
 | 
			
		||||
      // code block
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
      sx={{
 | 
			
		||||
        position: 'relative',
 | 
			
		||||
        display: 'flex',
 | 
			
		||||
        alignItems: 'center',
 | 
			
		||||
        gap: 1
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Tooltip title={`Support ${name}`} arrow>
 | 
			
		||||
        <Box
 | 
			
		||||
          sx={{
 | 
			
		||||
            position: 'relative',
 | 
			
		||||
            display: 'flex',
 | 
			
		||||
            alignItems: 'center',
 | 
			
		||||
            gap: 1,
 | 
			
		||||
            cursor: 'pointer'
 | 
			
		||||
          }}
 | 
			
		||||
          onClick={() => setIsOpen((prev) => !prev)}
 | 
			
		||||
        >
 | 
			
		||||
          <MonetizationOnIcon
 | 
			
		||||
            sx={{
 | 
			
		||||
              cursor: 'pointer',
 | 
			
		||||
              color: 'gold'
 | 
			
		||||
            }}
 | 
			
		||||
          ></MonetizationOnIcon>
 | 
			
		||||
          {!onlyIcon && (
 | 
			
		||||
            <Typography
 | 
			
		||||
              sx={{
 | 
			
		||||
                fontSize: '14px'
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              Support
 | 
			
		||||
            </Typography>
 | 
			
		||||
          )}
 | 
			
		||||
        </Box>
 | 
			
		||||
      </Tooltip>
 | 
			
		||||
      {isOpen && (
 | 
			
		||||
        <Portal>
 | 
			
		||||
          <Dialog
 | 
			
		||||
            open={isOpen}
 | 
			
		||||
            onClose={() => setIsOpen(false)}
 | 
			
		||||
            aria-labelledby="alert-dialog-title"
 | 
			
		||||
            aria-describedby="alert-dialog-description"
 | 
			
		||||
          >
 | 
			
		||||
            <DialogTitle id="alert-dialog-title"></DialogTitle>
 | 
			
		||||
            <DialogContent>
 | 
			
		||||
              <Box
 | 
			
		||||
                sx={{
 | 
			
		||||
                  width: '300px',
 | 
			
		||||
                  display: 'flex',
 | 
			
		||||
                  justifyContent: 'center'
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Box>
 | 
			
		||||
                  <InputLabel htmlFor="standard-adornment-name">To</InputLabel>
 | 
			
		||||
                  <Input id="standard-adornment-name" value={name} disabled />
 | 
			
		||||
                  <InputLabel htmlFor="standard-adornment-coin">
 | 
			
		||||
                    Coin
 | 
			
		||||
                  </InputLabel>
 | 
			
		||||
                  <Select
 | 
			
		||||
                    id="standard-adornment-coin"
 | 
			
		||||
                    sx={{ width: '100%' }}
 | 
			
		||||
                    defaultValue=""
 | 
			
		||||
                    displayEmpty
 | 
			
		||||
                    value={selectedCoin?.value || ''}
 | 
			
		||||
                    onChange={handleOptionChange}
 | 
			
		||||
                    renderValue={(value) => {
 | 
			
		||||
                      return (
 | 
			
		||||
                        <Box
 | 
			
		||||
                          sx={{
 | 
			
		||||
                            display: 'flex',
 | 
			
		||||
                            gap: 1,
 | 
			
		||||
                            justifyContent: 'center',
 | 
			
		||||
                            alignItems: 'center'
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          {value && (
 | 
			
		||||
                            <img
 | 
			
		||||
                              style={{
 | 
			
		||||
                                height: '25px',
 | 
			
		||||
                                width: '25px'
 | 
			
		||||
                              }}
 | 
			
		||||
                              src={getLogo(value)}
 | 
			
		||||
                            />
 | 
			
		||||
                          )}
 | 
			
		||||
 | 
			
		||||
                          {value}
 | 
			
		||||
                        </Box>
 | 
			
		||||
                      )
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    {coins.map((option) => (
 | 
			
		||||
                      <MenuItem key={option.value} value={option.value}>
 | 
			
		||||
                        {option.value}
 | 
			
		||||
                      </MenuItem>
 | 
			
		||||
                    ))}
 | 
			
		||||
                  </Select>
 | 
			
		||||
                  <InputLabel htmlFor="standard-adornment-amount">
 | 
			
		||||
                    Amount
 | 
			
		||||
                  </InputLabel>
 | 
			
		||||
                  <Input
 | 
			
		||||
                    id="standard-adornment-amount"
 | 
			
		||||
                    type="number"
 | 
			
		||||
                    value={amount}
 | 
			
		||||
                    onChange={(e) => setAmount(+e.target.value)}
 | 
			
		||||
                    startAdornment={
 | 
			
		||||
                      <InputAdornment position="start">
 | 
			
		||||
                        <img
 | 
			
		||||
                          style={{
 | 
			
		||||
                            height: '15px',
 | 
			
		||||
                            width: '15px'
 | 
			
		||||
                          }}
 | 
			
		||||
                          src={getLogo(selectedCoin?.value || '')}
 | 
			
		||||
                        />
 | 
			
		||||
                      </InputAdornment>
 | 
			
		||||
                    }
 | 
			
		||||
                  />
 | 
			
		||||
                </Box>
 | 
			
		||||
              </Box>
 | 
			
		||||
            </DialogContent>
 | 
			
		||||
            <DialogActions>
 | 
			
		||||
              <Button
 | 
			
		||||
                variant="contained"
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  setIsOpen(false)
 | 
			
		||||
                  resetValues()
 | 
			
		||||
                  onClose()
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                Close
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button variant="contained" onClick={sendCoin}>
 | 
			
		||||
                Send Coin
 | 
			
		||||
              </Button>
 | 
			
		||||
            </DialogActions>
 | 
			
		||||
          </Dialog>
 | 
			
		||||
        </Portal>
 | 
			
		||||
      )}
 | 
			
		||||
    </Box>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										55
									
								
								src/components/common/UserNavbar/UserNavbar-styles.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,55 @@
 | 
			
		||||
import { styled } from '@mui/system'
 | 
			
		||||
import {
 | 
			
		||||
  AppBar,
 | 
			
		||||
  Toolbar,
 | 
			
		||||
  Typography,
 | 
			
		||||
  Menu,
 | 
			
		||||
  MenuItem
 | 
			
		||||
} from '@mui/material'
 | 
			
		||||
 | 
			
		||||
export const CustomAppBar = styled(AppBar)(({ theme }) => ({
 | 
			
		||||
  backgroundColor: theme.palette.mode === "light" ? theme.palette.background.default : "#19191b",
 | 
			
		||||
  color: theme.palette.text.primary
 | 
			
		||||
}))
 | 
			
		||||
 | 
			
		||||
export const CustomToolbar = styled(Toolbar)({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  justifyContent: 'space-between',
 | 
			
		||||
  alignItems: 'center'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const CustomTitle = styled(Typography)(({ theme }) => ({
 | 
			
		||||
  color: theme.palette.text.primary,
 | 
			
		||||
  fontFamily: 'Raleway, Arial',
 | 
			
		||||
  fontSize: '18px'
 | 
			
		||||
}))
 | 
			
		||||
 | 
			
		||||
export const StyledAppBar = styled(AppBar)(({ theme }) => ({
 | 
			
		||||
  backgroundColor: theme.palette.primary.main
 | 
			
		||||
}))
 | 
			
		||||
 | 
			
		||||
export const StyledToolbar = styled(Toolbar)(({ theme }) => ({
 | 
			
		||||
  justifyContent: 'space-between'
 | 
			
		||||
}))
 | 
			
		||||
 | 
			
		||||
export const StyledMenu = styled(Menu)(({ theme }) => ({
 | 
			
		||||
  marginTop: theme.spacing(2),
 | 
			
		||||
  overflow: 'hidden',
 | 
			
		||||
  padding: 0,
 | 
			
		||||
}))
 | 
			
		||||
 | 
			
		||||
export const StyledMenuItem = styled(MenuItem)(({ theme }) => ({
 | 
			
		||||
  width: '100%',
 | 
			
		||||
  whiteSpace: 'nowrap',
 | 
			
		||||
  maxWidth: '300px',
 | 
			
		||||
  overflow: 'hidden',
 | 
			
		||||
  textOverflow: 'ellipsis',
 | 
			
		||||
  fontSize: "16px",
 | 
			
		||||
  fontFamily: "Arial",
 | 
			
		||||
  padding: "12px 10px",
 | 
			
		||||
  transition: "all 0.3s ease-in-out",
 | 
			
		||||
  "&:hover": {
 | 
			
		||||
    cursor: "pointer",
 | 
			
		||||
    filter: "brightness(1.1)"
 | 
			
		||||
  }
 | 
			
		||||
}))
 | 
			
		||||
							
								
								
									
										135
									
								
								src/components/common/UserNavbar/UserNavbar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,135 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { styled } from '@mui/system'
 | 
			
		||||
import {
 | 
			
		||||
  AppBar,
 | 
			
		||||
  Toolbar,
 | 
			
		||||
  Typography,
 | 
			
		||||
  IconButton,
 | 
			
		||||
  Menu,
 | 
			
		||||
  MenuItem,
 | 
			
		||||
  Box,
 | 
			
		||||
  Button
 | 
			
		||||
} from '@mui/material'
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  CustomAppBar,
 | 
			
		||||
  CustomToolbar,
 | 
			
		||||
  CustomTitle,
 | 
			
		||||
  StyledAppBar,
 | 
			
		||||
  StyledToolbar,
 | 
			
		||||
  StyledMenu,
 | 
			
		||||
  StyledMenuItem
 | 
			
		||||
} from './UserNavbar-styles'
 | 
			
		||||
 | 
			
		||||
import { useNavigate } from 'react-router-dom'
 | 
			
		||||
import { Menu as MenuIcon } from '@mui/icons-material'
 | 
			
		||||
import { removePrefix } from '../../../utils/blogIdformats'
 | 
			
		||||
import { QblogLogoContainer } from '../../layout/Navbar/Navbar-styles'
 | 
			
		||||
import QblogLogo from '../../../assets/img/qBlogLogo.png'
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  title: string
 | 
			
		||||
  menuItems: any[]
 | 
			
		||||
  name: string
 | 
			
		||||
  blogId: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const UserNavbar: React.FC<Props> = ({
 | 
			
		||||
  title,
 | 
			
		||||
  menuItems,
 | 
			
		||||
  name,
 | 
			
		||||
  blogId
 | 
			
		||||
}) => {
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
  const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null)
 | 
			
		||||
 | 
			
		||||
  const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
 | 
			
		||||
    setAnchorEl(event.currentTarget)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleClose = () => {
 | 
			
		||||
    setAnchorEl(null)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const goToPost = (item: any) => {
 | 
			
		||||
    if (!name) return
 | 
			
		||||
    const { postId } = item
 | 
			
		||||
 | 
			
		||||
    const str = postId
 | 
			
		||||
    const arr = str.split('-post-')
 | 
			
		||||
    const str1 = arr[0]
 | 
			
		||||
    const str2 = arr[1]
 | 
			
		||||
    const blogId = removePrefix(str1)
 | 
			
		||||
    navigate(`/${name}/${blogId}/${str2}`)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleAction = (action: () => void) => {
 | 
			
		||||
    handleClose()
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      action()
 | 
			
		||||
    }, 100)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <CustomAppBar position="sticky">
 | 
			
		||||
      <CustomToolbar variant="dense">
 | 
			
		||||
        <Box
 | 
			
		||||
          sx={{
 | 
			
		||||
            display: 'flex',
 | 
			
		||||
            alignItems: 'center'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <IconButton
 | 
			
		||||
            edge="start"
 | 
			
		||||
            color="inherit"
 | 
			
		||||
            aria-label="menu"
 | 
			
		||||
            onClick={handleClick}
 | 
			
		||||
          >
 | 
			
		||||
            <MenuIcon />
 | 
			
		||||
          </IconButton>
 | 
			
		||||
          <CustomTitle
 | 
			
		||||
            variant="h6"
 | 
			
		||||
            sx={{
 | 
			
		||||
              cursor: 'pointer',
 | 
			
		||||
              marginLeft: '10px'
 | 
			
		||||
            }}
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              navigate(`/${name}/${blogId}`)
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            {title}
 | 
			
		||||
          </CustomTitle>
 | 
			
		||||
        </Box>
 | 
			
		||||
        <StyledMenu
 | 
			
		||||
          anchorEl={anchorEl}
 | 
			
		||||
          open={Boolean(anchorEl)}
 | 
			
		||||
          onClose={handleClose}
 | 
			
		||||
          PaperProps={{ style: { width: '250px' } }}
 | 
			
		||||
        >
 | 
			
		||||
          {menuItems.map((item, index) => (
 | 
			
		||||
            <StyledMenuItem
 | 
			
		||||
              key={index}
 | 
			
		||||
              onClick={() => handleAction(() => goToPost(item))}
 | 
			
		||||
            >
 | 
			
		||||
              {item.name}
 | 
			
		||||
            </StyledMenuItem>
 | 
			
		||||
          ))}
 | 
			
		||||
        </StyledMenu>
 | 
			
		||||
        <Box
 | 
			
		||||
          sx={{
 | 
			
		||||
            display: 'flex',
 | 
			
		||||
            alignItems: 'center'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <QblogLogoContainer
 | 
			
		||||
            src={QblogLogo}
 | 
			
		||||
            alt="Qblog Logo"
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              navigate(`/`)
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </Box>
 | 
			
		||||
      </CustomToolbar>
 | 
			
		||||
    </CustomAppBar>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										51
									
								
								src/components/common/VideoContent.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,51 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Box, Typography } from '@mui/material'
 | 
			
		||||
import { styled } from '@mui/system'
 | 
			
		||||
import { Description, Movie } from '@mui/icons-material'
 | 
			
		||||
 | 
			
		||||
interface VideoProps {
 | 
			
		||||
  title: string
 | 
			
		||||
  description: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const StyledBox = styled(Box)`
 | 
			
		||||
  margin: 20px 0px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
const Title = styled(Typography)``
 | 
			
		||||
 | 
			
		||||
const DescriptionIcon = styled(Description)`
 | 
			
		||||
  color: #666;
 | 
			
		||||
  margin-right: 0.5rem;
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
const MovieIcon = styled(Movie)`
 | 
			
		||||
  color: #666;
 | 
			
		||||
  margin-right: 0.5rem;
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
export const VideoContent: React.FC<VideoProps> = ({ title, description }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <StyledBox>
 | 
			
		||||
      <Box
 | 
			
		||||
        sx={{
 | 
			
		||||
          display: 'flex',
 | 
			
		||||
          flexDirection: 'column',
 | 
			
		||||
          alignItems: 'flex-start'
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Box display="flex" alignItems="center">
 | 
			
		||||
          <MovieIcon />
 | 
			
		||||
          <Title variant="h4">{title}</Title>
 | 
			
		||||
        </Box>
 | 
			
		||||
 | 
			
		||||
        <Box display="flex" alignItems="center">
 | 
			
		||||
          <DescriptionIcon />
 | 
			
		||||
          <Typography variant="body1">{description}</Typography>
 | 
			
		||||
        </Box>
 | 
			
		||||
      </Box>
 | 
			
		||||
    </StyledBox>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										284
									
								
								src/components/common/VideoPanel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,284 @@
 | 
			
		||||
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 { useDispatch, useSelector } from 'react-redux'
 | 
			
		||||
import { CopyToClipboard } from 'react-copy-to-clipboard'
 | 
			
		||||
 | 
			
		||||
import { RootState } from '../../state/store'
 | 
			
		||||
import LinkIcon from '@mui/icons-material/Link'
 | 
			
		||||
import { setNotification } from '../../state/features/notificationsSlice'
 | 
			
		||||
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 VideoPanel: 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 [editVideoIdentifier, setEditVideoIdentifier] = useState<
 | 
			
		||||
    string | null | undefined
 | 
			
		||||
  >()
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
  const fetchVideos = React.useCallback(async (): Promise<Video[]> => {
 | 
			
		||||
    if (!user?.name) return []
 | 
			
		||||
    // Replace this URL with the actual API endpoint
 | 
			
		||||
    let res = []
 | 
			
		||||
    try {
 | 
			
		||||
      // res = await qortalRequest({
 | 
			
		||||
      //   action: 'LIST_QDN_RESOURCES',
 | 
			
		||||
      //   service: 'VIDEO',
 | 
			
		||||
      //   name: user.name,
 | 
			
		||||
      //   includeMetadata: true,
 | 
			
		||||
      //   limit: 100,
 | 
			
		||||
      //   offset: 0,
 | 
			
		||||
      //   reverse: true
 | 
			
		||||
      // })
 | 
			
		||||
 | 
			
		||||
      const res2 = await fetch(
 | 
			
		||||
        `/arbitrary/resources?&service=VIDEO&name=${user.name}&includemetadata=true&limit=100&offset=0&reverse=true`
 | 
			
		||||
      )
 | 
			
		||||
      const resData = await res2.json()
 | 
			
		||||
      if (Array.isArray(resData)) {
 | 
			
		||||
        res = resData
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      // const res2 = await fetch(
 | 
			
		||||
      //   '/arbitrary/resources?&service=VIDEO&name=Phil&includemetadata=true&limit=100&offset=0&reverse=true'
 | 
			
		||||
      // )
 | 
			
		||||
      // res = await res2.json()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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 a video" arrow>
 | 
			
		||||
        <VideoCallIcon
 | 
			
		||||
          onClick={handleToggle}
 | 
			
		||||
          sx={{
 | 
			
		||||
            height: height || '30px',
 | 
			
		||||
            width: width || 'auto',
 | 
			
		||||
            cursor: 'pointer'
 | 
			
		||||
          }}
 | 
			
		||||
        ></VideoCallIcon>
 | 
			
		||||
      </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 Video
 | 
			
		||||
            </Typography>
 | 
			
		||||
            <Typography
 | 
			
		||||
              variant="subtitle2"
 | 
			
		||||
              component="div"
 | 
			
		||||
              sx={{ flexGrow: 1, mb: 2 }}
 | 
			
		||||
            >
 | 
			
		||||
              List of videos 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>
 | 
			
		||||
                <Box
 | 
			
		||||
                  sx={{
 | 
			
		||||
                    display: 'flex',
 | 
			
		||||
                    gap: '5px'
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Button
 | 
			
		||||
                    size="small"
 | 
			
		||||
                    variant="contained"
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      setEditVideoIdentifier(video.identifier)
 | 
			
		||||
                      setIsOpenVideoModal(true)
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    Edit
 | 
			
		||||
                  </Button>
 | 
			
		||||
                  <CopyToClipboard
 | 
			
		||||
                    text={`qortal://${video.service}/${video.name}/${video.identifier}`}
 | 
			
		||||
                    onCopy={() => {
 | 
			
		||||
                      dispatch(
 | 
			
		||||
                        setNotification({
 | 
			
		||||
                          msg: 'Copied to clipboard!',
 | 
			
		||||
                          alertType: 'success'
 | 
			
		||||
                        })
 | 
			
		||||
                      )
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <LinkIcon
 | 
			
		||||
                      sx={{
 | 
			
		||||
                        fontSize: '14px',
 | 
			
		||||
                        cursor: 'pointer'
 | 
			
		||||
                      }}
 | 
			
		||||
                    />
 | 
			
		||||
                  </CopyToClipboard>
 | 
			
		||||
                </Box>
 | 
			
		||||
              </ListItem>
 | 
			
		||||
            ))}
 | 
			
		||||
          </List>
 | 
			
		||||
          <Box
 | 
			
		||||
            sx={{
 | 
			
		||||
              width: '100%',
 | 
			
		||||
              display: 'flex',
 | 
			
		||||
              justifyContent: 'center',
 | 
			
		||||
              flex: '0 0 50px'
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <PublishButton
 | 
			
		||||
              variant="contained"
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                setEditVideoIdentifier(null)
 | 
			
		||||
                setIsOpenVideoModal(true)
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              Publish new video
 | 
			
		||||
            </PublishButton>
 | 
			
		||||
          </Box>
 | 
			
		||||
        </Panel>
 | 
			
		||||
      </Drawer>
 | 
			
		||||
      <VideoModal
 | 
			
		||||
        onClose={() => {
 | 
			
		||||
          setIsOpenVideoModal(false)
 | 
			
		||||
          setEditVideoIdentifier(null)
 | 
			
		||||
        }}
 | 
			
		||||
        open={isOpenVideoModal}
 | 
			
		||||
        onPublish={(value) => {
 | 
			
		||||
          fetchVideos().then((fetchedVideos) => setVideos(fetchedVideos))
 | 
			
		||||
          setIsOpenVideoModal(false)
 | 
			
		||||
        }}
 | 
			
		||||
        editVideoIdentifier={editVideoIdentifier}
 | 
			
		||||
      />
 | 
			
		||||
    </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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										832
									
								
								src/components/common/VideoPlayer.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,832 @@
 | 
			
		||||
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'
 | 
			
		||||
import ReactDOM from 'react-dom'
 | 
			
		||||
import { Box, IconButton, Slider } from '@mui/material'
 | 
			
		||||
import { CircularProgress, Typography } from '@mui/material'
 | 
			
		||||
import { Key } from 'ts-key-enum'
 | 
			
		||||
import {
 | 
			
		||||
  PlayArrow,
 | 
			
		||||
  Pause,
 | 
			
		||||
  VolumeUp,
 | 
			
		||||
  Fullscreen,
 | 
			
		||||
  PictureInPicture, VolumeOff
 | 
			
		||||
} from '@mui/icons-material'
 | 
			
		||||
import { styled } from '@mui/system'
 | 
			
		||||
import { MyContext } from '../../wrappers/DownloadWrapper'
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux'
 | 
			
		||||
import { RootState } from '../../state/store'
 | 
			
		||||
import { Refresh } from '@mui/icons-material'
 | 
			
		||||
 | 
			
		||||
import { Menu, MenuItem } from '@mui/material'
 | 
			
		||||
import { MoreVert as MoreIcon } from '@mui/icons-material'
 | 
			
		||||
const VideoContainer = styled(Box)`
 | 
			
		||||
  position: relative;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  margin: 0px;
 | 
			
		||||
  padding: 0px;
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
  from?: string | null
 | 
			
		||||
  setCount?: () => void
 | 
			
		||||
  customStyle?: any
 | 
			
		||||
  user?: string
 | 
			
		||||
  postId?: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const VideoPlayer: React.FC<VideoPlayerProps> = ({
 | 
			
		||||
                                                          poster,
 | 
			
		||||
                                                          name,
 | 
			
		||||
                                                          identifier,
 | 
			
		||||
                                                          service,
 | 
			
		||||
                                                          autoplay = true,
 | 
			
		||||
                                                          from = null,
 | 
			
		||||
                                                          setCount,
 | 
			
		||||
                                                          customStyle = {},
 | 
			
		||||
                                                          user = '',
 | 
			
		||||
                                                          postId = ''
 | 
			
		||||
                                                        }) => {
 | 
			
		||||
  const videoRef = useRef<HTMLVideoElement | null>(null)
 | 
			
		||||
  const [playing, setPlaying] = useState(false)
 | 
			
		||||
  const [volume, setVolume] = useState(1)
 | 
			
		||||
  const [mutedVolume, setMutedVolume] = useState(1)
 | 
			
		||||
  const [isMuted, setIsMuted] = useState(false)
 | 
			
		||||
  const [progress, setProgress] = useState(0)
 | 
			
		||||
  const [isLoading, setIsLoading] = useState(false)
 | 
			
		||||
  const [canPlay, setCanPlay] = useState(false)
 | 
			
		||||
  const [startPlay, setStartPlay] = useState(false)
 | 
			
		||||
  const [isMobileView, setIsMobileView] = useState(false)
 | 
			
		||||
  const [playbackRate, setPlaybackRate] = useState(1)
 | 
			
		||||
  const [anchorEl, setAnchorEl] = useState(null)
 | 
			
		||||
  const [consoleLog, setConsoleLog] = useState('Console Log Here')
 | 
			
		||||
  const [debug, setDebug] = useState(false)
 | 
			
		||||
 | 
			
		||||
  const reDownload = useRef<boolean>(false)
 | 
			
		||||
  const { downloads } = useSelector((state: RootState) => state.global)
 | 
			
		||||
  const download = useMemo(() => {
 | 
			
		||||
    if (!downloads || !identifier) return {}
 | 
			
		||||
    const findDownload = downloads[identifier]
 | 
			
		||||
 | 
			
		||||
    if (!findDownload) return {}
 | 
			
		||||
    return findDownload
 | 
			
		||||
  }, [downloads, identifier])
 | 
			
		||||
 | 
			
		||||
  const src = useMemo(() => {
 | 
			
		||||
    return download?.url || ''
 | 
			
		||||
  }, [download?.url])
 | 
			
		||||
  const resourceStatus = useMemo(() => {
 | 
			
		||||
    return download?.status || {}
 | 
			
		||||
  }, [download])
 | 
			
		||||
 | 
			
		||||
  const minSpeed = 0.25;
 | 
			
		||||
  const maxSpeed = 4.0;
 | 
			
		||||
  const speedChange = 0.25;
 | 
			
		||||
 | 
			
		||||
  const updatePlaybackRate = (newSpeed: number) => {
 | 
			
		||||
    if(videoRef.current) {
 | 
			
		||||
      if(newSpeed > maxSpeed || newSpeed < minSpeed)
 | 
			
		||||
        newSpeed = minSpeed
 | 
			
		||||
      videoRef.current.playbackRate = newSpeed
 | 
			
		||||
      setPlaybackRate(newSpeed)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const increaseSpeed = (wrapOverflow = true) => {
 | 
			
		||||
    const changedSpeed = playbackRate + speedChange
 | 
			
		||||
    let newSpeed = wrapOverflow ? changedSpeed: Math.min(changedSpeed, maxSpeed)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    if (videoRef.current) {
 | 
			
		||||
      updatePlaybackRate(newSpeed);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const decreaseSpeed = () => {
 | 
			
		||||
    if (videoRef.current) {
 | 
			
		||||
      updatePlaybackRate(playbackRate - speedChange);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  const toggleRef = useRef<any>(null)
 | 
			
		||||
  const { downloadVideo } = useContext(MyContext)
 | 
			
		||||
  const togglePlay = async () => {
 | 
			
		||||
    if (!videoRef.current) return
 | 
			
		||||
    setStartPlay(true)
 | 
			
		||||
    if (!src) {
 | 
			
		||||
      const el = document.getElementById('videoWrapper')
 | 
			
		||||
      if (el) {
 | 
			
		||||
        el?.parentElement?.removeChild(el)
 | 
			
		||||
      }
 | 
			
		||||
      ReactDOM.flushSync(() => {
 | 
			
		||||
        setIsLoading(true)
 | 
			
		||||
      })
 | 
			
		||||
      getSrc()
 | 
			
		||||
    }
 | 
			
		||||
    if (playing) {
 | 
			
		||||
      videoRef.current.pause()
 | 
			
		||||
    } else {
 | 
			
		||||
      videoRef.current.play()
 | 
			
		||||
    }
 | 
			
		||||
    setPlaying(!playing)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onVolumeChange = (_: any, value: number | number[]) => {
 | 
			
		||||
    if (!videoRef.current) return
 | 
			
		||||
    videoRef.current.volume = value as number
 | 
			
		||||
    setVolume(value as number)
 | 
			
		||||
    setIsMuted(false)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onProgressChange = (_: any, value: number | number[]) => {
 | 
			
		||||
    if (!videoRef.current) return
 | 
			
		||||
    videoRef.current.currentTime = value as number
 | 
			
		||||
    setProgress(value as number)
 | 
			
		||||
    if (!playing) {
 | 
			
		||||
      videoRef.current.play()
 | 
			
		||||
      setPlaying(true)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleEnded = () => {
 | 
			
		||||
    setPlaying(false)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const updateProgress = () => {
 | 
			
		||||
    if (!videoRef.current) return
 | 
			
		||||
    setProgress(videoRef.current.currentTime)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const [isFullscreen, setIsFullscreen] = useState(false)
 | 
			
		||||
 | 
			
		||||
  const enterFullscreen = () => {
 | 
			
		||||
    if (!videoRef.current) return
 | 
			
		||||
    if (videoRef.current.requestFullscreen) {
 | 
			
		||||
      videoRef.current.requestFullscreen()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const exitFullscreen = () => {
 | 
			
		||||
    if (document.exitFullscreen) {
 | 
			
		||||
      document.exitFullscreen()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const toggleFullscreen = () => {
 | 
			
		||||
    isFullscreen ?   exitFullscreen(): enterFullscreen()
 | 
			
		||||
  }
 | 
			
		||||
  const togglePictureInPicture = async () => {
 | 
			
		||||
    if (!videoRef.current) return
 | 
			
		||||
    if (document.pictureInPictureElement === videoRef.current) {
 | 
			
		||||
      await document.exitPictureInPicture()
 | 
			
		||||
    } else {
 | 
			
		||||
      await videoRef.current.requestPictureInPicture()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const handleFullscreenChange = () => {
 | 
			
		||||
      setIsFullscreen(!!document.fullscreenElement)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    document.addEventListener('fullscreenchange', handleFullscreenChange)
 | 
			
		||||
    return () => {
 | 
			
		||||
      document.removeEventListener('fullscreenchange', handleFullscreenChange)
 | 
			
		||||
    }
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  const handleLoadedMetadata = () => {
 | 
			
		||||
    setIsLoading(false)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleCanPlay = () => {
 | 
			
		||||
    if (setCount) {
 | 
			
		||||
      setCount()
 | 
			
		||||
    }
 | 
			
		||||
    setIsLoading(false)
 | 
			
		||||
    setCanPlay(true)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getSrc = React.useCallback(async () => {
 | 
			
		||||
    if (!name || !identifier || !service || !postId || !user) return
 | 
			
		||||
    try {
 | 
			
		||||
      downloadVideo({
 | 
			
		||||
        name,
 | 
			
		||||
        service,
 | 
			
		||||
        identifier,
 | 
			
		||||
        blogPost: {
 | 
			
		||||
          postId,
 | 
			
		||||
          user
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
  }, [identifier, name, service])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const videoElement = videoRef.current
 | 
			
		||||
 | 
			
		||||
    const handleLeavePictureInPicture = async (event: any) => {
 | 
			
		||||
      const target = event?.target
 | 
			
		||||
      if (target) {
 | 
			
		||||
        target.pause()
 | 
			
		||||
        if (setPlaying) {
 | 
			
		||||
          setPlaying(false)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (videoElement) {
 | 
			
		||||
      videoElement.addEventListener(
 | 
			
		||||
          'leavepictureinpicture',
 | 
			
		||||
          handleLeavePictureInPicture
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      if (videoElement) {
 | 
			
		||||
        videoElement.removeEventListener(
 | 
			
		||||
            'leavepictureinpicture',
 | 
			
		||||
            handleLeavePictureInPicture
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const videoElement = videoRef.current
 | 
			
		||||
 | 
			
		||||
    const minimizeVideo = async () => {
 | 
			
		||||
      if (!videoElement) return
 | 
			
		||||
      const handleClose = () => {
 | 
			
		||||
        if (videoElement && videoElement.parentElement) {
 | 
			
		||||
          const el = document.getElementById('videoWrapper')
 | 
			
		||||
          if (el) {
 | 
			
		||||
            el?.parentElement?.removeChild(el)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      const createCloseButton = (): HTMLButtonElement => {
 | 
			
		||||
        const closeButton = document.createElement('button')
 | 
			
		||||
        closeButton.textContent = 'X'
 | 
			
		||||
        closeButton.style.position = 'absolute'
 | 
			
		||||
        closeButton.style.top = '0'
 | 
			
		||||
        closeButton.style.right = '0'
 | 
			
		||||
        closeButton.style.backgroundColor = 'rgba(255, 255, 255, 0.7)'
 | 
			
		||||
        closeButton.style.border = 'none'
 | 
			
		||||
        closeButton.style.fontWeight = 'bold'
 | 
			
		||||
        closeButton.style.fontSize = '1.2rem'
 | 
			
		||||
        closeButton.style.cursor = 'pointer'
 | 
			
		||||
        closeButton.style.padding = '2px 8px'
 | 
			
		||||
        closeButton.style.borderRadius = '0 0 0 4px'
 | 
			
		||||
 | 
			
		||||
        closeButton.addEventListener('click', handleClose)
 | 
			
		||||
 | 
			
		||||
        return closeButton
 | 
			
		||||
      }
 | 
			
		||||
      const buttonClose = createCloseButton()
 | 
			
		||||
      const videoWrapper = document.createElement('div')
 | 
			
		||||
      videoWrapper.id = 'videoWrapper'
 | 
			
		||||
      videoWrapper.style.position = 'fixed'
 | 
			
		||||
      videoWrapper.style.zIndex = '900000009'
 | 
			
		||||
      videoWrapper.style.bottom = '0px'
 | 
			
		||||
      videoWrapper.style.right = '0px'
 | 
			
		||||
 | 
			
		||||
      videoElement.parentElement?.insertBefore(videoWrapper, videoElement)
 | 
			
		||||
      videoWrapper.appendChild(videoElement)
 | 
			
		||||
 | 
			
		||||
      videoWrapper.appendChild(buttonClose)
 | 
			
		||||
      videoElement.controls = true
 | 
			
		||||
      videoElement.style.height = 'auto'
 | 
			
		||||
      videoElement.style.width = '300px'
 | 
			
		||||
 | 
			
		||||
      document.body.appendChild(videoWrapper)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      if (videoElement) {
 | 
			
		||||
        if (videoElement && !videoElement.paused && !videoElement.ended) {
 | 
			
		||||
          minimizeVideo()
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  function formatTime(seconds: number): string {
 | 
			
		||||
    seconds = Math.floor(seconds)
 | 
			
		||||
    let minutes: number | string = Math.floor(seconds / 60)
 | 
			
		||||
    let hours: number | string = Math.floor(minutes / 60)
 | 
			
		||||
 | 
			
		||||
    let remainingSeconds: number | string = seconds % 60
 | 
			
		||||
    let remainingMinutes: number | string = minutes % 60
 | 
			
		||||
 | 
			
		||||
    if (remainingSeconds < 10) {
 | 
			
		||||
      remainingSeconds = '0' + remainingSeconds
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (remainingMinutes < 10) {
 | 
			
		||||
      remainingMinutes = '0' + remainingMinutes
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if(hours === 0){
 | 
			
		||||
      hours = ''
 | 
			
		||||
    }
 | 
			
		||||
    else
 | 
			
		||||
    {
 | 
			
		||||
      hours = hours + ':'
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return hours + remainingMinutes + ':' + remainingSeconds
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const reloadVideo = () => {
 | 
			
		||||
    if (!videoRef.current) return
 | 
			
		||||
    const currentTime = videoRef.current.currentTime
 | 
			
		||||
    videoRef.current.src = src
 | 
			
		||||
    videoRef.current.load()
 | 
			
		||||
    videoRef.current.currentTime = currentTime
 | 
			
		||||
    if (playing) {
 | 
			
		||||
      videoRef.current.play()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (
 | 
			
		||||
        resourceStatus?.status === 'DOWNLOADED' &&
 | 
			
		||||
        reDownload?.current === false
 | 
			
		||||
    ) {
 | 
			
		||||
      getSrc()
 | 
			
		||||
      reDownload.current = true
 | 
			
		||||
    }
 | 
			
		||||
  }, [getSrc, resourceStatus])
 | 
			
		||||
 | 
			
		||||
  const handleMenuOpen = (event: any) => {
 | 
			
		||||
    setAnchorEl(event.currentTarget)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleMenuClose = () => {
 | 
			
		||||
    setAnchorEl(null)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const videoWidth = videoRef?.current?.offsetWidth
 | 
			
		||||
    if (videoWidth && videoWidth <= 600) {
 | 
			
		||||
      setIsMobileView(true)
 | 
			
		||||
    }
 | 
			
		||||
  }, [canPlay])
 | 
			
		||||
 | 
			
		||||
  const getDownloadProgress = (current: number, total: number) => {
 | 
			
		||||
    const progress = current /total * 100;
 | 
			
		||||
    return Number.isNaN(progress) ? '': progress.toFixed(0)+'%'
 | 
			
		||||
  }
 | 
			
		||||
  const mute = () => {
 | 
			
		||||
    setIsMuted(true)
 | 
			
		||||
    setMutedVolume(volume)
 | 
			
		||||
    setVolume(0)
 | 
			
		||||
    if(videoRef.current)  videoRef.current.volume = 0
 | 
			
		||||
  }
 | 
			
		||||
  const unMute = () => {
 | 
			
		||||
    setIsMuted(false)
 | 
			
		||||
    setVolume(mutedVolume)
 | 
			
		||||
    if(videoRef.current)  videoRef.current.volume = mutedVolume
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const toggleMute = () => {
 | 
			
		||||
    isMuted ? unMute() : mute();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
const changeVolume = (volumeChange: number) =>
 | 
			
		||||
{
 | 
			
		||||
  if(videoRef.current){
 | 
			
		||||
    const minVolume = 0;
 | 
			
		||||
    const maxVolume = 1;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    let newVolume = volumeChange + volume
 | 
			
		||||
 | 
			
		||||
    newVolume = Math.max(newVolume, minVolume)
 | 
			
		||||
    newVolume = Math.min(newVolume, maxVolume)
 | 
			
		||||
 | 
			
		||||
setIsMuted(false)
 | 
			
		||||
    setMutedVolume(newVolume)
 | 
			
		||||
    videoRef.current.volume = newVolume
 | 
			
		||||
    setVolume(newVolume);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
  const setProgressRelative = (secondsChange: number) => {
 | 
			
		||||
    if(videoRef.current){
 | 
			
		||||
      const currentTime = videoRef.current?.currentTime
 | 
			
		||||
      const minTime = 0
 | 
			
		||||
      const maxTime = videoRef.current?.duration || 100
 | 
			
		||||
 | 
			
		||||
      let newTime = currentTime + secondsChange;
 | 
			
		||||
      newTime = Math.max(newTime, minTime)
 | 
			
		||||
      newTime = Math.min(newTime, maxTime)
 | 
			
		||||
      videoRef.current.currentTime = newTime;
 | 
			
		||||
      setProgress(newTime);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const setProgressAbsolute = (videoPercent: number) => {
 | 
			
		||||
    if(videoRef.current){
 | 
			
		||||
      videoPercent = Math.min(videoPercent, 100)
 | 
			
		||||
      videoPercent = Math.max(videoPercent, 0)
 | 
			
		||||
       const finalTime = videoRef.current?.duration*videoPercent / 100
 | 
			
		||||
      videoRef.current.currentTime = finalTime
 | 
			
		||||
      setProgress(finalTime);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  const keyboardShortcutsDown = (e: React.KeyboardEvent<HTMLDivElement>) =>
 | 
			
		||||
  {
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
    //setConsoleLog(`Alt: ${e.altKey} Shift: ${e.shiftKey} Control: ${e.ctrlKey} Key: ${e.key}`)
 | 
			
		||||
 | 
			
		||||
    switch(e.key) {
 | 
			
		||||
      case Key.Add: increaseSpeed(false); break;
 | 
			
		||||
      case '+': increaseSpeed(false); break;
 | 
			
		||||
      case '>': increaseSpeed(false); break;
 | 
			
		||||
 | 
			
		||||
      case Key.Subtract: decreaseSpeed(); break;
 | 
			
		||||
      case '-': decreaseSpeed(); break;
 | 
			
		||||
      case '<': decreaseSpeed(); break;
 | 
			
		||||
 | 
			
		||||
      case Key.ArrowLeft:  {
 | 
			
		||||
        if(e.shiftKey) setProgressRelative(-300);
 | 
			
		||||
        else if(e.ctrlKey) setProgressRelative(-60);
 | 
			
		||||
        else if(e.altKey) setProgressRelative(-10);
 | 
			
		||||
        else setProgressRelative(-5);
 | 
			
		||||
      }  break;
 | 
			
		||||
 | 
			
		||||
      case Key.ArrowRight:  {
 | 
			
		||||
        if(e.shiftKey) setProgressRelative(300);
 | 
			
		||||
        else if(e.ctrlKey) setProgressRelative(60);
 | 
			
		||||
        else if(e.altKey) setProgressRelative(10);
 | 
			
		||||
        else setProgressRelative(5);
 | 
			
		||||
      }  break;
 | 
			
		||||
 | 
			
		||||
      case Key.ArrowDown: changeVolume(-0.05)  ;  break;
 | 
			
		||||
      case Key.ArrowUp: changeVolume(0.05) ;  break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const keyboardShortcutsUp = (e: React.KeyboardEvent<HTMLDivElement>) =>
 | 
			
		||||
  {
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
    //setConsoleLog(`Alt: ${e.altKey} Shift: ${e.shiftKey} Control: ${e.ctrlKey} Key: ${e.key}`)
 | 
			
		||||
 | 
			
		||||
    switch(e.key) {
 | 
			
		||||
      case ' ': togglePlay(); break;
 | 
			
		||||
      case 'm': toggleMute(); break;
 | 
			
		||||
 | 
			
		||||
      case 'f': enterFullscreen(); break;
 | 
			
		||||
      case Key.Escape: exitFullscreen(); break;
 | 
			
		||||
 | 
			
		||||
      case '0': setProgressAbsolute(0); break;
 | 
			
		||||
      case '1': setProgressAbsolute(10); break;
 | 
			
		||||
      case '2': setProgressAbsolute(20);  break;
 | 
			
		||||
      case '3': setProgressAbsolute(30);  break;
 | 
			
		||||
      case '4': setProgressAbsolute(40);  break;
 | 
			
		||||
      case '5': setProgressAbsolute(50);  break;
 | 
			
		||||
      case '6': setProgressAbsolute(60);  break;
 | 
			
		||||
      case '7': setProgressAbsolute(70);  break;
 | 
			
		||||
      case '8': setProgressAbsolute(80);  break;
 | 
			
		||||
      case '9': setProgressAbsolute(90);  break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
      <VideoContainer
 | 
			
		||||
          tabIndex={0}
 | 
			
		||||
          onKeyUp={keyboardShortcutsUp}
 | 
			
		||||
          onKeyDown={keyboardShortcutsDown}
 | 
			
		||||
          style={{
 | 
			
		||||
            padding: from === 'create' ? '8px' : 0
 | 
			
		||||
          }}
 | 
			
		||||
      >
 | 
			
		||||
        {/* <Box
 | 
			
		||||
        sx={{
 | 
			
		||||
          position: 'absolute',
 | 
			
		||||
          top: '-30px',
 | 
			
		||||
          right: '-15px'
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <CopyToClipboard
 | 
			
		||||
          text={`qortal://${service}/${name}/${identifier}`}
 | 
			
		||||
          onCopy={() => {
 | 
			
		||||
            dispatch(
 | 
			
		||||
              setNotification({
 | 
			
		||||
                msg: 'Copied to clipboard!',
 | 
			
		||||
                alertType: 'success'
 | 
			
		||||
              })
 | 
			
		||||
            )
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <LinkIcon
 | 
			
		||||
            sx={{
 | 
			
		||||
              fontSize: '14px',
 | 
			
		||||
              cursor: 'pointer'
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </CopyToClipboard>
 | 
			
		||||
      </Box> */}
 | 
			
		||||
        {isLoading && (
 | 
			
		||||
            <Box
 | 
			
		||||
                position="absolute"
 | 
			
		||||
                top={0}
 | 
			
		||||
                left={0}
 | 
			
		||||
                right={0}
 | 
			
		||||
                bottom={resourceStatus?.status === 'READY' ? '55px ' : 0}
 | 
			
		||||
                display="flex"
 | 
			
		||||
                justifyContent="center"
 | 
			
		||||
                alignItems="center"
 | 
			
		||||
                zIndex={4999}
 | 
			
		||||
                bgcolor="rgba(0, 0, 0, 0.6)"
 | 
			
		||||
                sx={{
 | 
			
		||||
                  display: 'flex',
 | 
			
		||||
                  flexDirection: 'column',
 | 
			
		||||
                  gap: '10px'
 | 
			
		||||
                }}
 | 
			
		||||
            >
 | 
			
		||||
              <CircularProgress color="secondary" />
 | 
			
		||||
              {resourceStatus && (
 | 
			
		||||
                  <Typography
 | 
			
		||||
                      variant="subtitle2"
 | 
			
		||||
                      component="div"
 | 
			
		||||
                      sx={{
 | 
			
		||||
                        color: 'white',
 | 
			
		||||
                        fontSize: '15px',
 | 
			
		||||
                        textAlign: 'center'
 | 
			
		||||
                      }}
 | 
			
		||||
                  >
 | 
			
		||||
                    {resourceStatus?.status === 'REFETCHING' ? (
 | 
			
		||||
                        <>
 | 
			
		||||
                          <>
 | 
			
		||||
                            {getDownloadProgress(resourceStatus?.localChunkCount,resourceStatus?.totalChunkCount)}
 | 
			
		||||
                          </>
 | 
			
		||||
 | 
			
		||||
                          <> Refetching in 25 seconds</>
 | 
			
		||||
                        </>
 | 
			
		||||
                    ) : resourceStatus?.status === 'DOWNLOADED' ? (
 | 
			
		||||
                        <>Download Completed: building video...</>
 | 
			
		||||
                    ) : resourceStatus?.status !== 'READY' ? (
 | 
			
		||||
                        <>
 | 
			
		||||
                          {getDownloadProgress(resourceStatus?.localChunkCount,resourceStatus?.totalChunkCount)}
 | 
			
		||||
 | 
			
		||||
                        </>
 | 
			
		||||
                    ) : (
 | 
			
		||||
                        <>Download Completed: fetching video...</>
 | 
			
		||||
                    )}
 | 
			
		||||
                  </Typography>
 | 
			
		||||
              )}
 | 
			
		||||
            </Box>
 | 
			
		||||
        )}
 | 
			
		||||
        {((!src && !isLoading) || !startPlay) && (
 | 
			
		||||
            <Box
 | 
			
		||||
                position="absolute"
 | 
			
		||||
                top={0}
 | 
			
		||||
                left={0}
 | 
			
		||||
                right={0}
 | 
			
		||||
                bottom={0}
 | 
			
		||||
                display="flex"
 | 
			
		||||
                justifyContent="center"
 | 
			
		||||
                alignItems="center"
 | 
			
		||||
                zIndex={500}
 | 
			
		||||
                bgcolor="rgba(0, 0, 0, 0.6)"
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  if (from === 'create') return
 | 
			
		||||
 | 
			
		||||
                  togglePlay()
 | 
			
		||||
                }}
 | 
			
		||||
                sx={{
 | 
			
		||||
                  cursor: 'pointer'
 | 
			
		||||
                }}
 | 
			
		||||
            >
 | 
			
		||||
              <PlayArrow
 | 
			
		||||
                  sx={{
 | 
			
		||||
                    width: '50px',
 | 
			
		||||
                    height: '50px',
 | 
			
		||||
                    color: 'white'
 | 
			
		||||
                  }}
 | 
			
		||||
              />
 | 
			
		||||
            </Box>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        <VideoElement
 | 
			
		||||
            ref={videoRef}
 | 
			
		||||
            src={!startPlay ? '' : resourceStatus?.status === 'READY' ? src : ''}
 | 
			
		||||
            poster={poster}
 | 
			
		||||
            onTimeUpdate={updateProgress}
 | 
			
		||||
            autoPlay={autoplay}
 | 
			
		||||
            onClick={togglePlay}
 | 
			
		||||
            onEnded={handleEnded}
 | 
			
		||||
            // onLoadedMetadata={handleLoadedMetadata}
 | 
			
		||||
            onCanPlay={handleCanPlay}
 | 
			
		||||
            preload="metadata"
 | 
			
		||||
            style={{
 | 
			
		||||
              ...customStyle
 | 
			
		||||
            }}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <ControlsContainer
 | 
			
		||||
            style={{
 | 
			
		||||
              bottom: from === 'create' ? '15px' : 0
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
          {isMobileView && canPlay ? (
 | 
			
		||||
              <>
 | 
			
		||||
                <IconButton
 | 
			
		||||
                    sx={{
 | 
			
		||||
                      color: 'rgba(255, 255, 255, 0.7)'
 | 
			
		||||
                    }}
 | 
			
		||||
                    onClick={togglePlay}
 | 
			
		||||
                >
 | 
			
		||||
                  {playing ? <Pause /> : <PlayArrow />}
 | 
			
		||||
                </IconButton>
 | 
			
		||||
                <IconButton
 | 
			
		||||
                    sx={{
 | 
			
		||||
                      color: 'rgba(255, 255, 255, 0.7)',
 | 
			
		||||
                      marginLeft: '15px'
 | 
			
		||||
                    }}
 | 
			
		||||
                    onClick={reloadVideo}
 | 
			
		||||
                >
 | 
			
		||||
                  <Refresh />
 | 
			
		||||
                </IconButton>
 | 
			
		||||
                <Slider
 | 
			
		||||
                    value={progress}
 | 
			
		||||
                    onChange={onProgressChange}
 | 
			
		||||
                    min={0}
 | 
			
		||||
                    max={videoRef.current?.duration || 100}
 | 
			
		||||
                    sx={{ flexGrow: 1, mx: 2 }}
 | 
			
		||||
                />
 | 
			
		||||
                <IconButton
 | 
			
		||||
                    edge="end"
 | 
			
		||||
                    color="inherit"
 | 
			
		||||
                    aria-label="menu"
 | 
			
		||||
                    onClick={handleMenuOpen}
 | 
			
		||||
                >
 | 
			
		||||
                  <MoreIcon />
 | 
			
		||||
                </IconButton>
 | 
			
		||||
                <Menu
 | 
			
		||||
                    id="simple-menu"
 | 
			
		||||
                    anchorEl={anchorEl}
 | 
			
		||||
                    keepMounted
 | 
			
		||||
                    open={Boolean(anchorEl)}
 | 
			
		||||
                    onClose={handleMenuClose}
 | 
			
		||||
                    PaperProps={{
 | 
			
		||||
                      style: {
 | 
			
		||||
                        width: '250px'
 | 
			
		||||
                      }
 | 
			
		||||
                    }}
 | 
			
		||||
                >
 | 
			
		||||
                  <MenuItem>
 | 
			
		||||
                    <VolumeUp />
 | 
			
		||||
                    <Slider
 | 
			
		||||
                        value={volume}
 | 
			
		||||
                        onChange={onVolumeChange}
 | 
			
		||||
                        min={0}
 | 
			
		||||
                        max={1}
 | 
			
		||||
                        step={0.01}/>
 | 
			
		||||
                  </MenuItem>
 | 
			
		||||
                  <MenuItem onClick={() => increaseSpeed()}>
 | 
			
		||||
                    <Typography
 | 
			
		||||
                        sx={{
 | 
			
		||||
                          color: 'rgba(255, 255, 255, 0.7)',
 | 
			
		||||
                          fontSize: '14px'
 | 
			
		||||
                        }}
 | 
			
		||||
                    >
 | 
			
		||||
                      Speed: {playbackRate}x
 | 
			
		||||
                    </Typography>
 | 
			
		||||
                  </MenuItem>
 | 
			
		||||
                  <MenuItem onClick={togglePictureInPicture}>
 | 
			
		||||
                    <PictureInPicture />
 | 
			
		||||
                  </MenuItem>
 | 
			
		||||
                  <MenuItem onClick={toggleFullscreen}>
 | 
			
		||||
                    <Fullscreen />
 | 
			
		||||
                  </MenuItem>
 | 
			
		||||
                </Menu>
 | 
			
		||||
              </>
 | 
			
		||||
          ) : canPlay ? (
 | 
			
		||||
              <>
 | 
			
		||||
                <IconButton
 | 
			
		||||
                    sx={{
 | 
			
		||||
                      color: 'rgba(255, 255, 255, 0.7)'
 | 
			
		||||
                    }}
 | 
			
		||||
                    onClick={togglePlay}
 | 
			
		||||
                >
 | 
			
		||||
                  {playing ? <Pause /> : <PlayArrow />}
 | 
			
		||||
                </IconButton>
 | 
			
		||||
                <IconButton
 | 
			
		||||
                    sx={{
 | 
			
		||||
                      color: 'rgba(255, 255, 255, 0.7)',
 | 
			
		||||
                      marginLeft: '15px'
 | 
			
		||||
                    }}
 | 
			
		||||
                    onClick={reloadVideo}
 | 
			
		||||
                >
 | 
			
		||||
                  <Refresh />
 | 
			
		||||
                </IconButton>
 | 
			
		||||
                <Slider
 | 
			
		||||
                    value={progress}
 | 
			
		||||
                    onChange={onProgressChange}
 | 
			
		||||
                    min={0}
 | 
			
		||||
                    max={videoRef.current?.duration || 100}
 | 
			
		||||
                    sx={{ flexGrow: 1, mx: 2 }}
 | 
			
		||||
                />
 | 
			
		||||
                <Typography
 | 
			
		||||
                    sx={{
 | 
			
		||||
                      fontSize: '14px',
 | 
			
		||||
                      marginRight: '5px',
 | 
			
		||||
                      color: 'rgba(255, 255, 255, 0.7)',
 | 
			
		||||
                      visibility:
 | 
			
		||||
                          !videoRef.current?.duration || !progress
 | 
			
		||||
                              ? 'hidden'
 | 
			
		||||
                              : 'visible'
 | 
			
		||||
                    }}
 | 
			
		||||
                >
 | 
			
		||||
                  {progress && videoRef.current?.duration && formatTime(progress)}/
 | 
			
		||||
                  {progress &&
 | 
			
		||||
                      videoRef.current?.duration &&
 | 
			
		||||
                      formatTime(videoRef.current?.duration)}
 | 
			
		||||
                </Typography>
 | 
			
		||||
                <IconButton
 | 
			
		||||
                    sx={{
 | 
			
		||||
                      color: 'rgba(255, 255, 255, 0.7)',
 | 
			
		||||
                      marginRight: '10px'
 | 
			
		||||
                    }}
 | 
			
		||||
                    onClick={toggleMute}
 | 
			
		||||
                >
 | 
			
		||||
                  {isMuted ? <VolumeOff/>:<VolumeUp/>}
 | 
			
		||||
                </IconButton>
 | 
			
		||||
                <Slider
 | 
			
		||||
                    value={volume}
 | 
			
		||||
                    onChange={onVolumeChange}
 | 
			
		||||
                    min={0}
 | 
			
		||||
                    max={1}
 | 
			
		||||
                    step={0.01}
 | 
			
		||||
                />
 | 
			
		||||
                <IconButton
 | 
			
		||||
                    sx={{
 | 
			
		||||
                      color: 'rgba(255, 255, 255, 0.7)',
 | 
			
		||||
                      fontSize: '14px',
 | 
			
		||||
                      marginLeft: '5px'
 | 
			
		||||
                    }}
 | 
			
		||||
                    onClick={(e) => increaseSpeed()}
 | 
			
		||||
                >
 | 
			
		||||
                  Speed: {playbackRate}x
 | 
			
		||||
                </IconButton>
 | 
			
		||||
 | 
			
		||||
                <IconButton
 | 
			
		||||
                    sx={{
 | 
			
		||||
                      color: 'rgba(255, 255, 255, 0.7)',
 | 
			
		||||
                      marginLeft: '15px'
 | 
			
		||||
                    }}
 | 
			
		||||
                    ref={toggleRef}
 | 
			
		||||
                    onClick={togglePictureInPicture}
 | 
			
		||||
                >
 | 
			
		||||
                  <PictureInPicture />
 | 
			
		||||
                </IconButton>
 | 
			
		||||
                <IconButton
 | 
			
		||||
                    sx={{
 | 
			
		||||
                      color: 'rgba(255, 255, 255, 0.7)'
 | 
			
		||||
                    }}
 | 
			
		||||
                    onClick={toggleFullscreen}
 | 
			
		||||
                >
 | 
			
		||||
                  <Fullscreen />
 | 
			
		||||
                </IconButton>
 | 
			
		||||
              </>
 | 
			
		||||
          ) : null}
 | 
			
		||||
        </ControlsContainer>
 | 
			
		||||
        {debug ? <span>{consoleLog}</span>: <></>}
 | 
			
		||||
      </VideoContainer>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										287
									
								
								src/components/common/VideoPublishModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,287 @@
 | 
			
		||||
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 VideoModalProps {
 | 
			
		||||
  open: boolean
 | 
			
		||||
  onClose: () => void
 | 
			
		||||
  onPublish: (value: any) => void
 | 
			
		||||
  editVideoIdentifier?: string | null | undefined
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface SelectOption {
 | 
			
		||||
  id: string
 | 
			
		||||
  name: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const VideoModal: React.FC<VideoModalProps> = ({
 | 
			
		||||
  open,
 | 
			
		||||
  onClose,
 | 
			
		||||
  onPublish,
 | 
			
		||||
  editVideoIdentifier
 | 
			
		||||
}) => {
 | 
			
		||||
  const [file, setFile] = useState<File | null>(null)
 | 
			
		||||
  const [title, setTitle] = useState('')
 | 
			
		||||
  const [description, setDescription] = useState('')
 | 
			
		||||
  const [selectedOption, setSelectedOption] = useState<SelectOption | null>(
 | 
			
		||||
    null
 | 
			
		||||
  )
 | 
			
		||||
  const [inputValue, setInputValue] = useState<string>('')
 | 
			
		||||
  const [chips, setChips] = useState<string[]>([])
 | 
			
		||||
 | 
			
		||||
  const [options, setOptions] = useState<SelectOption[]>([])
 | 
			
		||||
  const [tags, setTags] = useState<string[]>([])
 | 
			
		||||
  const { publishVideo } = usePublishVideo()
 | 
			
		||||
  const { getRootProps, getInputProps } = useDropzone({
 | 
			
		||||
    accept: {
 | 
			
		||||
      'video/*': []
 | 
			
		||||
    },
 | 
			
		||||
    maxFiles: 1,
 | 
			
		||||
    onDrop: (acceptedFiles) => {
 | 
			
		||||
      setFile(acceptedFiles[0])
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const handleTitleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
 | 
			
		||||
    setTitle(event.target.value)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleDescriptionChange = (
 | 
			
		||||
    event: React.ChangeEvent<HTMLInputElement>
 | 
			
		||||
  ) => {
 | 
			
		||||
    setDescription(event.target.value)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleOptionChange = (event: SelectChangeEvent<string>) => {
 | 
			
		||||
    const optionId = event.target.value
 | 
			
		||||
    const selectedOption = options.find((option) => option.id === optionId)
 | 
			
		||||
    setSelectedOption(selectedOption || null)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleChipDelete = (index: number) => {
 | 
			
		||||
    const newChips = [...chips]
 | 
			
		||||
    newChips.splice(index, 1)
 | 
			
		||||
    setChips(newChips)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleSubmit = async () => {
 | 
			
		||||
    const missingFields = []
 | 
			
		||||
 | 
			
		||||
    if (!title) missingFields.push('title')
 | 
			
		||||
    if (!file) missingFields.push('file')
 | 
			
		||||
    if (missingFields.length > 0) {
 | 
			
		||||
      const missingFieldsString = missingFields.join(', ')
 | 
			
		||||
      const errMsg = `Missing: ${missingFieldsString}`
 | 
			
		||||
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (!file) return
 | 
			
		||||
 | 
			
		||||
    const formattedTags: { [key: string]: string } = {}
 | 
			
		||||
    chips.forEach((tag, i) => {
 | 
			
		||||
      formattedTags[`tag${i + 1}`] = tag
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      // const base64 = await toBase64(file)
 | 
			
		||||
      // if (typeof base64 !== 'string') return
 | 
			
		||||
      // const base64String = base64.split(',')[1]
 | 
			
		||||
      // if (!file) return
 | 
			
		||||
 | 
			
		||||
      const res = await publishVideo({
 | 
			
		||||
        file: file,
 | 
			
		||||
        editVideoIdentifier,
 | 
			
		||||
        title,
 | 
			
		||||
        description,
 | 
			
		||||
        category: selectedOption?.id || '',
 | 
			
		||||
        ...formattedTags
 | 
			
		||||
      })
 | 
			
		||||
      onPublish(res)
 | 
			
		||||
      setFile(null)
 | 
			
		||||
      setTitle('')
 | 
			
		||||
      setDescription('')
 | 
			
		||||
      onClose()
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleInputChange = (event: any) => {
 | 
			
		||||
    setInputValue(event.target.value)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleInputKeyDown = (event: any) => {
 | 
			
		||||
    if (event.key === 'Enter' && inputValue !== '') {
 | 
			
		||||
      if (chips.length < 5) {
 | 
			
		||||
        setChips([...chips, inputValue])
 | 
			
		||||
        setInputValue('')
 | 
			
		||||
      } else {
 | 
			
		||||
        event.preventDefault()
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const addChip = () => {
 | 
			
		||||
    if (chips.length < 5) {
 | 
			
		||||
      setChips([...chips, inputValue])
 | 
			
		||||
      setInputValue('')
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getListCategories = React.useCallback(async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const url = `/arbitrary/categories`
 | 
			
		||||
      const response = await fetch(url, {
 | 
			
		||||
        method: 'GET',
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'application/json'
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      const responseData = await response.json()
 | 
			
		||||
      setOptions(responseData)
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    getListCategories()
 | 
			
		||||
  }, [getListCategories])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <StyledModal open={open} onClose={onClose}>
 | 
			
		||||
      <ModalContent>
 | 
			
		||||
        {editVideoIdentifier && (
 | 
			
		||||
          <Typography variant="h6">
 | 
			
		||||
            You are editing: {editVideoIdentifier}
 | 
			
		||||
          </Typography>
 | 
			
		||||
        )}
 | 
			
		||||
        <Typography variant="h6" component="h2" gutterBottom>
 | 
			
		||||
          Upload Video
 | 
			
		||||
        </Typography>
 | 
			
		||||
        <Box
 | 
			
		||||
          {...getRootProps()}
 | 
			
		||||
          sx={{
 | 
			
		||||
            border: '1px dashed gray',
 | 
			
		||||
            padding: 2,
 | 
			
		||||
            textAlign: 'center',
 | 
			
		||||
            marginBottom: 2
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <input {...getInputProps()} />
 | 
			
		||||
          <Typography>
 | 
			
		||||
            {file
 | 
			
		||||
              ? file.name
 | 
			
		||||
              : 'Drag and drop a video file here or click to select a file'}
 | 
			
		||||
          </Typography>
 | 
			
		||||
        </Box>
 | 
			
		||||
        <TextField
 | 
			
		||||
          label="Video Title"
 | 
			
		||||
          variant="outlined"
 | 
			
		||||
          fullWidth
 | 
			
		||||
          value={title}
 | 
			
		||||
          onChange={handleTitleChange}
 | 
			
		||||
          inputProps={{ maxLength: 40 }}
 | 
			
		||||
          sx={{ marginBottom: 2 }}
 | 
			
		||||
        />
 | 
			
		||||
        <TextField
 | 
			
		||||
          label="Video Description"
 | 
			
		||||
          variant="outlined"
 | 
			
		||||
          fullWidth
 | 
			
		||||
          multiline
 | 
			
		||||
          rows={4}
 | 
			
		||||
          value={description}
 | 
			
		||||
          onChange={handleDescriptionChange}
 | 
			
		||||
          inputProps={{ maxLength: 180 }}
 | 
			
		||||
          sx={{ marginBottom: 2 }}
 | 
			
		||||
        />
 | 
			
		||||
        {options.length > 0 && (
 | 
			
		||||
          <FormControl fullWidth sx={{ marginBottom: 2 }}>
 | 
			
		||||
            <InputLabel id="Category">Select a Category</InputLabel>
 | 
			
		||||
            <Select
 | 
			
		||||
              labelId="Category"
 | 
			
		||||
              input={<OutlinedInput label="Select a Category" />}
 | 
			
		||||
              value={selectedOption?.id || ''}
 | 
			
		||||
              onChange={handleOptionChange}
 | 
			
		||||
            >
 | 
			
		||||
              {options.map((option) => (
 | 
			
		||||
                <MenuItem key={option.id} value={option.id}>
 | 
			
		||||
                  {option.name}
 | 
			
		||||
                </MenuItem>
 | 
			
		||||
              ))}
 | 
			
		||||
            </Select>
 | 
			
		||||
          </FormControl>
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        <FormControl fullWidth sx={{ marginBottom: 2 }}>
 | 
			
		||||
          <Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
 | 
			
		||||
            <TextField
 | 
			
		||||
              label="Add a tag"
 | 
			
		||||
              value={inputValue}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              onKeyDown={handleInputKeyDown}
 | 
			
		||||
              disabled={chips.length === 3}
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            <IconButton onClick={addChip} disabled={chips.length === 3}>
 | 
			
		||||
              <AddIcon />
 | 
			
		||||
            </IconButton>
 | 
			
		||||
          </Box>
 | 
			
		||||
          <ChipContainer>
 | 
			
		||||
            {chips.map((chip, index) => (
 | 
			
		||||
              <Chip
 | 
			
		||||
                key={index}
 | 
			
		||||
                label={chip}
 | 
			
		||||
                onDelete={() => handleChipDelete(index)}
 | 
			
		||||
                deleteIcon={<CloseIcon />}
 | 
			
		||||
              />
 | 
			
		||||
            ))}
 | 
			
		||||
          </ChipContainer>
 | 
			
		||||
        </FormControl>
 | 
			
		||||
        <Button variant="contained" color="primary" onClick={handleSubmit}>
 | 
			
		||||
          Submit
 | 
			
		||||
        </Button>
 | 
			
		||||
      </ModalContent>
 | 
			
		||||
    </StyledModal>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default VideoModal
 | 
			
		||||
							
								
								
									
										78
									
								
								src/components/editor/BlogEditor.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,78 @@
 | 
			
		||||
/* src/components/BlogEditor.css */
 | 
			
		||||
.blog-editor {
 | 
			
		||||
    max-width: 800px;
 | 
			
		||||
    margin: 0 auto;
 | 
			
		||||
    padding: 1rem;
 | 
			
		||||
    line-height: 1.5;
 | 
			
		||||
    font-size: 18px;
 | 
			
		||||
    max-height: 50vh;
 | 
			
		||||
    overflow-y: auto;
 | 
			
		||||
    min-height: 200px;
 | 
			
		||||
    z-index: 500;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .toolbar {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    margin-bottom: 1rem;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  
 | 
			
		||||
  .toolbar-button:focus {
 | 
			
		||||
    outline: none;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .code-block {
 | 
			
		||||
    background-color: #2c2b31;
 | 
			
		||||
    color: rgb(238, 234, 234);
 | 
			
		||||
    border-radius: 3px;
 | 
			
		||||
    padding: 10px;
 | 
			
		||||
    margin: 10px 0;
 | 
			
		||||
    font-family: 'Courier New', Courier, monospace;
 | 
			
		||||
    white-space: pre-wrap;
 | 
			
		||||
    overflow-x: auto;
 | 
			
		||||
    max-width: 100%;
 | 
			
		||||
    font-size: 14px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .paragraph {
 | 
			
		||||
    font-size: 20px;
 | 
			
		||||
    margin: 0px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .paragraph-mail {
 | 
			
		||||
    font-size: 16px;
 | 
			
		||||
    margin: 0px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .toolbar-button {
 | 
			
		||||
    background-color: white;
 | 
			
		||||
    border: 1px solid gray;
 | 
			
		||||
    border-radius: 5px;
 | 
			
		||||
    margin-right: 5px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    outline: none;
 | 
			
		||||
    height: 32px;
 | 
			
		||||
    width: 32px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .toolbar-button.active {
 | 
			
		||||
    background-color: lightgray;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .h2 {
 | 
			
		||||
    font-size: 25px
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .h2 {
 | 
			
		||||
    font-size: 22px
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .align-center {
 | 
			
		||||
    text-align: center;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										574
									
								
								src/components/editor/BlogEditor.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,574 @@
 | 
			
		||||
// src/components/BlogEditor.tsx
 | 
			
		||||
// @ts-nocheck
 | 
			
		||||
 | 
			
		||||
import React, { useMemo, useState, useCallback } from 'react';
 | 
			
		||||
import { createEditor, Descendant, Editor, Transforms, Range } from 'slate'
 | 
			
		||||
import SvgIcon from '@material-ui/core/SvgIcon'
 | 
			
		||||
import {
 | 
			
		||||
  Slate,
 | 
			
		||||
  Editable,
 | 
			
		||||
  withReact,
 | 
			
		||||
  RenderElementProps,
 | 
			
		||||
  RenderLeafProps,
 | 
			
		||||
  useSlate
 | 
			
		||||
} from 'slate-react'
 | 
			
		||||
import { styled } from '@mui/system'
 | 
			
		||||
import { CustomElement, CustomText, FormatMark } from './customTypes'
 | 
			
		||||
import './BlogEditor.css'
 | 
			
		||||
import { Modal, Box, TextField, Button } from '@mui/material'
 | 
			
		||||
 | 
			
		||||
import { AlignCenterSVG } from '../../assets/svgs/AlignCenterSVG'
 | 
			
		||||
import { BoldSVG } from '../../assets/svgs/BoldSVG'
 | 
			
		||||
import { ItalicSVG } from '../../assets/svgs/ItalicSVG'
 | 
			
		||||
import { UnderlineSVG } from '../../assets/svgs/UnderlineSVG'
 | 
			
		||||
import { H2SVG } from '../../assets/svgs/H2SVG'
 | 
			
		||||
import { H3SVG } from '../../assets/svgs/H3SVG'
 | 
			
		||||
import { AlignLeftSVG } from '../../assets/svgs/AlignLeftSVG'
 | 
			
		||||
import { AlignRightSVG } from '../../assets/svgs/AlignRightSVG'
 | 
			
		||||
import { CodeBlockSVG } from '../../assets/svgs/CodeBlockSVG'
 | 
			
		||||
import { LinkSVG } from '../../assets/svgs/LinkSVG'
 | 
			
		||||
 | 
			
		||||
const initialValue: Descendant[] = [
 | 
			
		||||
  {
 | 
			
		||||
    type: 'paragraph',
 | 
			
		||||
    children: [{ text: 'Start writing your blog post...' }]
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
interface MyComponentProps {
 | 
			
		||||
  addPostSection?: (value: any) => void
 | 
			
		||||
  editPostSection?: (value: any, section: any) => void
 | 
			
		||||
  defaultValue?: any
 | 
			
		||||
  section?: any
 | 
			
		||||
  value: any
 | 
			
		||||
  setValue: (value: any) => void
 | 
			
		||||
  editorKey?: number
 | 
			
		||||
  mode?: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ModalBox = styled(Box)(({ theme }) => ({
 | 
			
		||||
  position: 'absolute',
 | 
			
		||||
  top: '50%',
 | 
			
		||||
  left: '50%',
 | 
			
		||||
  transform: 'translate(-50%, -50%)',
 | 
			
		||||
  backgroundColor: theme.palette.background.paper,
 | 
			
		||||
  boxShadow: theme.shadows[5],
 | 
			
		||||
  padding: theme.spacing(2, 4, 3),
 | 
			
		||||
  gap: '15px',
 | 
			
		||||
  borderRadius: '5px',
 | 
			
		||||
  alignItems: 'center',
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  flex: 0
 | 
			
		||||
}))
 | 
			
		||||
 | 
			
		||||
const BlogEditor: React.FC<MyComponentProps> = ({
 | 
			
		||||
  addPostSection,
 | 
			
		||||
  editPostSection,
 | 
			
		||||
  defaultValue,
 | 
			
		||||
  section,
 | 
			
		||||
  value,
 | 
			
		||||
  setValue,
 | 
			
		||||
  editorKey,
 | 
			
		||||
  mode
 | 
			
		||||
}) => {
 | 
			
		||||
  const editor = useMemo(() => withReact(createEditor()), [])
 | 
			
		||||
 | 
			
		||||
  // const [value, setValue] = useState(defaultValue || initialValue);
 | 
			
		||||
  const isTextAlignmentActive = (editor: Editor, alignment: string) => {
 | 
			
		||||
    const [match] = Editor.nodes(editor, {
 | 
			
		||||
      match: (n) => {
 | 
			
		||||
        return n?.textAlign === alignment?.replace(/^align-/, '')
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    return !!match
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const toggleTextAlignment = (editor: Editor, alignment: string) => {
 | 
			
		||||
    const isActive = isTextAlignmentActive(editor, alignment)
 | 
			
		||||
    Transforms.setNodes(
 | 
			
		||||
      editor,
 | 
			
		||||
      { style: { textAlign: isActive ? 'inherit' : alignment } },
 | 
			
		||||
      { match: (n) => Editor.isBlock(editor, n) }
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const toggleMark = (editor: Editor, format: FormatMark) => {
 | 
			
		||||
    if (
 | 
			
		||||
      format === 'align-left' ||
 | 
			
		||||
      format === 'align-center' ||
 | 
			
		||||
      format === 'align-right'
 | 
			
		||||
    ) {
 | 
			
		||||
      toggleTextAlignment(editor, format)
 | 
			
		||||
    } else {
 | 
			
		||||
      const isActive = Editor?.marks(editor)?.[format] === true
 | 
			
		||||
      if (isActive) {
 | 
			
		||||
        Editor?.removeMark(editor, format)
 | 
			
		||||
      } else {
 | 
			
		||||
        Editor?.addMark(editor, format, true)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const newValue = useMemo(() => [...(value || initialValue)], [value])
 | 
			
		||||
 | 
			
		||||
  const types = ['paragraph', 'heading-2', 'heading-3']
 | 
			
		||||
 | 
			
		||||
  const setTextAlignment = (editor, alignment) => {
 | 
			
		||||
    const isActive = isTextAlignmentActive(editor, alignment)
 | 
			
		||||
    const alignmentType = ''
 | 
			
		||||
    Transforms?.setNodes(
 | 
			
		||||
      editor,
 | 
			
		||||
      {
 | 
			
		||||
        textAlign: isActive ? null : alignment
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        match: (n) =>
 | 
			
		||||
          n.type === 'heading-2' ||
 | 
			
		||||
          n.type === 'heading-3' ||
 | 
			
		||||
          n.type === 'paragraph'
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const ToolbarButton: React.FC<{
 | 
			
		||||
    format: FormatMark | string
 | 
			
		||||
    label: string
 | 
			
		||||
    editor: Editor
 | 
			
		||||
    children: React.ReactNode
 | 
			
		||||
  }> = ({ format, label, editor, children }) => {
 | 
			
		||||
    useSlate()
 | 
			
		||||
 | 
			
		||||
    let onClick = () => {
 | 
			
		||||
      if (format === 'heading-2' || format === 'heading-3') {
 | 
			
		||||
        toggleBlock(editor, format)
 | 
			
		||||
      } else if (
 | 
			
		||||
        format === 'bold' ||
 | 
			
		||||
        format === 'italic' ||
 | 
			
		||||
        format === 'underline' ||
 | 
			
		||||
        format === ''
 | 
			
		||||
      ) {
 | 
			
		||||
        toggleMark(editor, format)
 | 
			
		||||
      } else if (
 | 
			
		||||
        format === 'align-left' ||
 | 
			
		||||
        format === 'align-center' ||
 | 
			
		||||
        format === 'align-right'
 | 
			
		||||
      ) {
 | 
			
		||||
        setTextAlignment(editor, format?.replace(/^align-/, ''))
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let isActive = false
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      if (
 | 
			
		||||
        format === 'align-left' ||
 | 
			
		||||
        format === 'align-center' ||
 | 
			
		||||
        format === 'align-right'
 | 
			
		||||
      ) {
 | 
			
		||||
        isActive = isTextAlignmentActive(editor, format)
 | 
			
		||||
      } else if (format === 'heading-2' || format === 'heading-3') {
 | 
			
		||||
        isActive = isBlockActive(editor, format)
 | 
			
		||||
      } else if (
 | 
			
		||||
        format === 'bold' ||
 | 
			
		||||
        format === 'italic' ||
 | 
			
		||||
        format === 'underline' ||
 | 
			
		||||
        format === ''
 | 
			
		||||
      ) {
 | 
			
		||||
        isActive = Editor?.marks(editor)?.[format] === true
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <button
 | 
			
		||||
        className={`toolbar-button ${isActive ? 'active' : ''}`}
 | 
			
		||||
        onMouseDown={(event) => {
 | 
			
		||||
          event.preventDefault()
 | 
			
		||||
          onClick()
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {children ? children : label}
 | 
			
		||||
      </button>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const ToolbarButtonCodeBlock: React.FC<{
 | 
			
		||||
    format: FormatMark | string
 | 
			
		||||
    label: string
 | 
			
		||||
    editor: Editor
 | 
			
		||||
    children: React.ReactNode
 | 
			
		||||
  }> = ({ format, label, editor, children }) => {
 | 
			
		||||
    const editor2 = useSlate()
 | 
			
		||||
 | 
			
		||||
    let onClick = () => {
 | 
			
		||||
      if (format === 'code-block') {
 | 
			
		||||
        toggleBlock(editor, 'code-block')
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    let isActive = false
 | 
			
		||||
    try {
 | 
			
		||||
      if (format === 'code-block') {
 | 
			
		||||
        isActive = isBlockActive(editor, format)
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <button
 | 
			
		||||
        className={`toolbar-button ${isActive ? 'active' : ''}`}
 | 
			
		||||
        onMouseDown={(event) => {
 | 
			
		||||
          event.preventDefault()
 | 
			
		||||
          onClick()
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {children ? children : label}
 | 
			
		||||
      </button>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const ToolbarButtonAlign: React.FC<{
 | 
			
		||||
    format: string
 | 
			
		||||
    label: string
 | 
			
		||||
    editor: Editor
 | 
			
		||||
  }> = ({ format, label, editor }) => {
 | 
			
		||||
    const isActive =
 | 
			
		||||
      Editor?.nodes(editor, {
 | 
			
		||||
        match: (n) => n?.align === format
 | 
			
		||||
      })?.length > 0
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <button
 | 
			
		||||
        className={`toolbar-button ${isActive ? 'active' : ''}`}
 | 
			
		||||
        onMouseDown={(event) => {
 | 
			
		||||
          event.preventDefault()
 | 
			
		||||
          Transforms?.setNodes(
 | 
			
		||||
            editor,
 | 
			
		||||
            { align: format },
 | 
			
		||||
            { match: (n) => Editor?.isBlock(editor, n) }
 | 
			
		||||
          )
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {label}
 | 
			
		||||
      </button>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const ToolbarButtonCodeLink: React.FC<{
 | 
			
		||||
    format: FormatMark | string
 | 
			
		||||
    label: string
 | 
			
		||||
    editor: Editor
 | 
			
		||||
    children: React.ReactNode
 | 
			
		||||
  }> = ({ format, label, editor, children }) => {
 | 
			
		||||
    useSlate()
 | 
			
		||||
 | 
			
		||||
    let isActive = false
 | 
			
		||||
    try {
 | 
			
		||||
      if (format === 'link') {
 | 
			
		||||
        isActive = !!Editor?.marks(editor)?.link
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <button
 | 
			
		||||
        className={`toolbar-button ${isActive ? 'active' : ''}`}
 | 
			
		||||
        onMouseDown={(event) => {
 | 
			
		||||
          event.preventDefault()
 | 
			
		||||
          const isActive2 = !!Editor?.marks(editor)?.link
 | 
			
		||||
          if (isActive2) {
 | 
			
		||||
            Editor?.removeMark(editor, 'link')
 | 
			
		||||
            return
 | 
			
		||||
          }
 | 
			
		||||
          // const url = window.prompt('Enter the URL of the link:')
 | 
			
		||||
          setOpen(true)
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {children ? children : label}
 | 
			
		||||
      </button>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Create a toggleBlock function and an isBlockActive function to handle block elements
 | 
			
		||||
  const toggleBlock = (editor: Editor, format: string) => {
 | 
			
		||||
    const isActive = isBlockActive(editor, format)
 | 
			
		||||
    Transforms?.unwrapNodes(editor, {
 | 
			
		||||
      match: (n) => Editor?.isBlock(editor, n),
 | 
			
		||||
      split: true
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    if (isActive) {
 | 
			
		||||
      Transforms?.setNodes(editor, { type: 'paragraph' })
 | 
			
		||||
    } else {
 | 
			
		||||
      Transforms?.setNodes(editor, { type: format })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const isBlockActive = (editor: Editor, format: string) => {
 | 
			
		||||
    const [match] = Editor?.nodes(editor, {
 | 
			
		||||
      match: (n) => n?.type === format
 | 
			
		||||
    })
 | 
			
		||||
    return !!match
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleKeyDown = (event: React.KeyboardEvent) => {
 | 
			
		||||
    if (event.key === 'Enter' && isBlockActive(editor, 'code-block')) {
 | 
			
		||||
      event.preventDefault()
 | 
			
		||||
      editor?.insertText('\n')
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (event.key === 'ArrowDown' && isBlockActive(editor, 'code-block')) {
 | 
			
		||||
      event.preventDefault()
 | 
			
		||||
      Transforms?.insertNodes(editor, {
 | 
			
		||||
        type: 'paragraph',
 | 
			
		||||
        children: [{ text: '' }]
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleChange = (newValue: Descendant[]) => {
 | 
			
		||||
    setValue(newValue)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const toggleLink = (editor: Editor, url: string) => {
 | 
			
		||||
    const { selection } = editor
 | 
			
		||||
 | 
			
		||||
    if (selection && !Range.isCollapsed(selection)) {
 | 
			
		||||
      const isLink = Editor?.marks(editor)?.link === true
 | 
			
		||||
      const isInsideLink = isLinkActive(editor)
 | 
			
		||||
 | 
			
		||||
      if (isLink) {
 | 
			
		||||
        Editor?.removeMark(editor, 'link')
 | 
			
		||||
      } else if (url) {
 | 
			
		||||
        Editor?.addMark(editor, 'link', url)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const [open, setOpen] = useState(false)
 | 
			
		||||
 | 
			
		||||
  const initialValue = 'qortal://'
 | 
			
		||||
  const [inputValue, setInputValue] = useState(initialValue)
 | 
			
		||||
 | 
			
		||||
  const handleChangeLink = (event) => {
 | 
			
		||||
    const newValue = event?.target?.value
 | 
			
		||||
    if (newValue?.startsWith(initialValue)) {
 | 
			
		||||
      setInputValue(newValue)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const isLinkActive = (editor: Editor) => {
 | 
			
		||||
    const [link] = Editor?.nodes(editor, {
 | 
			
		||||
      match: (n) => n?.type === 'link'
 | 
			
		||||
    })
 | 
			
		||||
    return !!link
 | 
			
		||||
  }
 | 
			
		||||
  const handleSaveClick = () => {
 | 
			
		||||
    const marks = Editor?.marks(editor)
 | 
			
		||||
    const isLink = marks?.link === true
 | 
			
		||||
 | 
			
		||||
    if (isLink) {
 | 
			
		||||
      Editor?.removeMark(editor, 'link')
 | 
			
		||||
      return // Return early to skip the rest of the function
 | 
			
		||||
    }
 | 
			
		||||
    toggleLink(editor, inputValue)
 | 
			
		||||
    setOpen(false)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const onClose = () => {
 | 
			
		||||
    setOpen(false)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handlePaste = (event: React.ClipboardEvent) => {
 | 
			
		||||
    event.preventDefault()
 | 
			
		||||
    const text = event?.clipboardData?.getData('text/plain')
 | 
			
		||||
    const isCodeBlock = isBlockActive(editor, 'code-block')
 | 
			
		||||
 | 
			
		||||
    if (isCodeBlock) {
 | 
			
		||||
      const lines = text?.split('\n')
 | 
			
		||||
      const fragment: Descendant[] = [
 | 
			
		||||
        {
 | 
			
		||||
          type: 'code-block',
 | 
			
		||||
          children: lines?.map((line) => ({
 | 
			
		||||
            type: 'code-line',
 | 
			
		||||
            children: [{ text: line }]
 | 
			
		||||
          }))
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
 | 
			
		||||
      Transforms?.insertFragment(editor, fragment)
 | 
			
		||||
    } else if (text) {
 | 
			
		||||
      const fragment = text?.split('\n').map((line) => ({
 | 
			
		||||
        type: 'paragraph',
 | 
			
		||||
        children: [{ text: line }]
 | 
			
		||||
      }))
 | 
			
		||||
 | 
			
		||||
      Transforms?.insertFragment(editor, fragment)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
      sx={{
 | 
			
		||||
        width: '100%',
 | 
			
		||||
        border: '1px solid',
 | 
			
		||||
        borderRadius: '5px',
 | 
			
		||||
        marginTop: '20px',
 | 
			
		||||
        padding: '10px'
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Slate
 | 
			
		||||
        editor={editor}
 | 
			
		||||
        value={newValue}
 | 
			
		||||
        onChange={(newValue) => handleChange(newValue)}
 | 
			
		||||
        key={editorKey || 1}
 | 
			
		||||
      >
 | 
			
		||||
        <div className="toolbar">
 | 
			
		||||
          <ToolbarButton format="bold" label="B" editor={editor}>
 | 
			
		||||
            <BoldSVG height="24px" width="auto" />
 | 
			
		||||
          </ToolbarButton>
 | 
			
		||||
          <ToolbarButton format="italic" label="I" editor={editor}>
 | 
			
		||||
            <ItalicSVG height="24px" width="auto" />
 | 
			
		||||
          </ToolbarButton>
 | 
			
		||||
          <ToolbarButton format="underline" label="U" editor={editor}>
 | 
			
		||||
            <UnderlineSVG height="24px" width="auto" />
 | 
			
		||||
          </ToolbarButton>
 | 
			
		||||
 | 
			
		||||
          <ToolbarButton format="heading-2" label="H2" editor={editor}>
 | 
			
		||||
            <H2SVG height="24px" width="auto" />
 | 
			
		||||
          </ToolbarButton>
 | 
			
		||||
          <ToolbarButton format="heading-3" label="H3" editor={editor}>
 | 
			
		||||
            <H3SVG height="24px" width="auto" />
 | 
			
		||||
          </ToolbarButton>
 | 
			
		||||
          <ToolbarButton format="align-left" label="L" editor={editor}>
 | 
			
		||||
            <AlignLeftSVG height="24px" width="auto" />
 | 
			
		||||
          </ToolbarButton>
 | 
			
		||||
          <ToolbarButton format="align-center" label="C" editor={editor}>
 | 
			
		||||
            <AlignCenterSVG height="24px" width="auto" />
 | 
			
		||||
          </ToolbarButton>
 | 
			
		||||
          <ToolbarButton format="align-right" label="R" editor={editor}>
 | 
			
		||||
            <AlignRightSVG height="24px" width="auto" />
 | 
			
		||||
          </ToolbarButton>
 | 
			
		||||
 | 
			
		||||
          <ToolbarButtonCodeBlock
 | 
			
		||||
            format="code-block"
 | 
			
		||||
            label="Code"
 | 
			
		||||
            editor={editor}
 | 
			
		||||
          >
 | 
			
		||||
            <CodeBlockSVG height="24px" width="auto" />
 | 
			
		||||
          </ToolbarButtonCodeBlock>
 | 
			
		||||
          <ToolbarButtonCodeLink format="link" label="Link" editor={editor}>
 | 
			
		||||
            <LinkSVG height="24px" width="auto" />
 | 
			
		||||
          </ToolbarButtonCodeLink>
 | 
			
		||||
        </div>
 | 
			
		||||
        <Editable
 | 
			
		||||
          className="blog-editor"
 | 
			
		||||
          renderElement={(props) => renderElement({ ...props, mode })}
 | 
			
		||||
          renderLeaf={renderLeaf}
 | 
			
		||||
          onKeyDown={handleKeyDown}
 | 
			
		||||
          onPaste={handlePaste}
 | 
			
		||||
          mode={mode}
 | 
			
		||||
        />
 | 
			
		||||
      </Slate>
 | 
			
		||||
      <Modal open={open} onClose={onClose}>
 | 
			
		||||
        <ModalBox>
 | 
			
		||||
          <TextField
 | 
			
		||||
            label="Link"
 | 
			
		||||
            value={inputValue}
 | 
			
		||||
            onChange={handleChangeLink}
 | 
			
		||||
          />
 | 
			
		||||
          <Button variant="contained" onClick={handleSaveClick}>
 | 
			
		||||
            Save
 | 
			
		||||
          </Button>
 | 
			
		||||
        </ModalBox>
 | 
			
		||||
      </Modal>
 | 
			
		||||
      {editPostSection && (
 | 
			
		||||
        <Button onClick={() => editPostSection(value, section)}>
 | 
			
		||||
          Edit Section
 | 
			
		||||
        </Button>
 | 
			
		||||
      )}
 | 
			
		||||
    </Box>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default BlogEditor
 | 
			
		||||
 | 
			
		||||
type ExtendedRenderElementProps = RenderElementProps & { mode?: string }
 | 
			
		||||
 | 
			
		||||
export const renderElement = ({
 | 
			
		||||
  attributes,
 | 
			
		||||
  children,
 | 
			
		||||
  element,
 | 
			
		||||
  mode
 | 
			
		||||
}: ExtendedRenderElementProps) => {
 | 
			
		||||
  switch (element.type) {
 | 
			
		||||
    case 'block-quote':
 | 
			
		||||
      return <blockquote {...attributes}>{children}</blockquote>
 | 
			
		||||
    case 'heading-2':
 | 
			
		||||
      return (
 | 
			
		||||
        <h2
 | 
			
		||||
          className="h2"
 | 
			
		||||
          {...attributes}
 | 
			
		||||
          style={{ textAlign: element.textAlign }}
 | 
			
		||||
        >
 | 
			
		||||
          {children}
 | 
			
		||||
        </h2>
 | 
			
		||||
      )
 | 
			
		||||
    case 'heading-3':
 | 
			
		||||
      return (
 | 
			
		||||
        <h3
 | 
			
		||||
          className="h3"
 | 
			
		||||
          {...attributes}
 | 
			
		||||
          style={{ textAlign: element.textAlign }}
 | 
			
		||||
        >
 | 
			
		||||
          {children}
 | 
			
		||||
        </h3>
 | 
			
		||||
      )
 | 
			
		||||
    case 'code-block':
 | 
			
		||||
      return (
 | 
			
		||||
        <pre {...attributes} className="code-block">
 | 
			
		||||
          <code>{children}</code>
 | 
			
		||||
        </pre>
 | 
			
		||||
      )
 | 
			
		||||
    case 'code-line':
 | 
			
		||||
      return <div {...attributes}>{children}</div>
 | 
			
		||||
    case 'link':
 | 
			
		||||
      return (
 | 
			
		||||
        <a href={element.url} {...attributes}>
 | 
			
		||||
          {children}
 | 
			
		||||
        </a>
 | 
			
		||||
      )
 | 
			
		||||
    default:
 | 
			
		||||
      return (
 | 
			
		||||
        <p
 | 
			
		||||
          className={`paragraph${mode ? `-${mode}` : ''}`}
 | 
			
		||||
          {...attributes}
 | 
			
		||||
          style={{ textAlign: element.textAlign }}
 | 
			
		||||
        >
 | 
			
		||||
          {children}
 | 
			
		||||
        </p>
 | 
			
		||||
      )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export const renderLeaf = ({ attributes, children, leaf }: RenderLeafProps) => {
 | 
			
		||||
  let el = children
 | 
			
		||||
 | 
			
		||||
  if (leaf.bold) {
 | 
			
		||||
    el = <strong>{el}</strong>
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (leaf.italic) {
 | 
			
		||||
    el = <em>{el}</em>
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (leaf.underline) {
 | 
			
		||||
    el = <u>{el}</u>
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (leaf.link) {
 | 
			
		||||
    el = (
 | 
			
		||||
      <a href={leaf.link} {...attributes}>
 | 
			
		||||
        {el}
 | 
			
		||||
      </a>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return <span {...attributes}>{el}</span>
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										25
									
								
								src/components/editor/ReadOnlySlate.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,25 @@
 | 
			
		||||
import React, { useMemo } from 'react';
 | 
			
		||||
import { createEditor, Descendant, Editor } from 'slate';
 | 
			
		||||
import { withReact, Slate, Editable, RenderElementProps, RenderLeafProps  } from 'slate-react';
 | 
			
		||||
import { renderElement, renderLeaf } from './BlogEditor';
 | 
			
		||||
 | 
			
		||||
interface ReadOnlySlateProps {
 | 
			
		||||
  content: any
 | 
			
		||||
  mode?: string
 | 
			
		||||
}
 | 
			
		||||
const ReadOnlySlate: React.FC<ReadOnlySlateProps> = ({ content, mode }) => {
 | 
			
		||||
  const editor = useMemo(() => withReact(createEditor()), [])
 | 
			
		||||
  const value = useMemo(() => content, [content])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Slate editor={editor} value={value} onChange={() => {}}>
 | 
			
		||||
      <Editable
 | 
			
		||||
        readOnly
 | 
			
		||||
        renderElement={(props) => renderElement({ ...props, mode })}
 | 
			
		||||
        renderLeaf={renderLeaf}
 | 
			
		||||
      />
 | 
			
		||||
    </Slate>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ReadOnlySlate;
 | 
			
		||||
							
								
								
									
										47
									
								
								src/components/editor/customTypes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,47 @@
 | 
			
		||||
// src/customTypes.ts
 | 
			
		||||
import { BaseEditor } from 'slate';
 | 
			
		||||
import { ReactEditor } from 'slate-react';
 | 
			
		||||
 | 
			
		||||
export type CustomText = {
 | 
			
		||||
  text: string
 | 
			
		||||
  bold?: boolean
 | 
			
		||||
  italic?: boolean
 | 
			
		||||
  underline?: boolean
 | 
			
		||||
  code?: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type HeadingElement = {
 | 
			
		||||
  type: 'heading'
 | 
			
		||||
  children: CustomText[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type BlockQuoteElement = {
 | 
			
		||||
  type: 'block-quote'
 | 
			
		||||
  children: CustomText[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type ParagraphElement = {
 | 
			
		||||
  type: 'paragraph'
 | 
			
		||||
  children: CustomText[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type CodeBlockElement = {
 | 
			
		||||
  type: 'code-block'
 | 
			
		||||
  children: CustomText[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type CustomElement =
 | 
			
		||||
  | HeadingElement
 | 
			
		||||
  | BlockQuoteElement
 | 
			
		||||
  | ParagraphElement
 | 
			
		||||
  | CodeBlockElement
 | 
			
		||||
 | 
			
		||||
export type FormatMark = 'bold' | 'italic' | 'underline' | 'code'
 | 
			
		||||
 | 
			
		||||
declare module 'slate' {
 | 
			
		||||
  interface CustomTypes {
 | 
			
		||||
    Editor: BaseEditor & ReactEditor;
 | 
			
		||||
    Element: CustomElement;
 | 
			
		||||
    Text: CustomText;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										112
									
								
								src/components/layout/Navbar/Navbar-styles.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,112 @@
 | 
			
		||||
import { AppBar, Button, Toolbar, Typography, Box } from '@mui/material'
 | 
			
		||||
import { styled } from '@mui/system'
 | 
			
		||||
 | 
			
		||||
export const QblogLogoContainer = styled('img')({
 | 
			
		||||
  width: 'auto',
 | 
			
		||||
  height: 'auto',
 | 
			
		||||
  userSelect: 'none',
 | 
			
		||||
  objectFit: 'contain',
 | 
			
		||||
  cursor: 'pointer'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const CustomAppBar = styled(AppBar)(({ theme }) => ({
 | 
			
		||||
  backgroundColor: theme.palette.mode === "light" ? theme.palette.background.default : "#19191b",
 | 
			
		||||
  [theme.breakpoints.only('xs')]: {
 | 
			
		||||
    gap: '15px',
 | 
			
		||||
  },
 | 
			
		||||
}))
 | 
			
		||||
 | 
			
		||||
export const CustomToolbar = styled(Toolbar)({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  justifyContent: 'space-between',
 | 
			
		||||
  alignItems: 'center'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const CustomTitle = styled(Typography)({
 | 
			
		||||
  fontWeight: 600,
 | 
			
		||||
  color: '#000000'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const StyledButton = styled(Button)(({ theme }) => ({
 | 
			
		||||
  fontWeight: 600,
 | 
			
		||||
  color: theme.palette.text.primary
 | 
			
		||||
}))
 | 
			
		||||
 | 
			
		||||
export const CreateBlogButton = styled(Button)(({ theme }) => ({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  flexDirection: 'row',
 | 
			
		||||
  alignItems: 'center',
 | 
			
		||||
  padding: '8px 15px',
 | 
			
		||||
  borderRadius: "40px",
 | 
			
		||||
  gap: '4px',
 | 
			
		||||
  backgroundColor: theme.palette.secondary.main,
 | 
			
		||||
  color: '#fff',
 | 
			
		||||
  fontFamily: "Arial",
 | 
			
		||||
  transition: "all 0.3s ease-in-out",
 | 
			
		||||
  boxShadow: "none",
 | 
			
		||||
  "&:hover": {
 | 
			
		||||
    cursor: "pointer",
 | 
			
		||||
    boxShadow: "rgba(0, 0, 0, 0.15) 1.95px 1.95px 2.6px;",
 | 
			
		||||
    backgroundColor: theme.palette.secondary.main,
 | 
			
		||||
    filter: "brightness(1.1)",
 | 
			
		||||
  }
 | 
			
		||||
}))
 | 
			
		||||
 | 
			
		||||
export const AuthenticateButton = styled(Button)({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  flexDirection: 'row',
 | 
			
		||||
  alignItems: 'center',
 | 
			
		||||
  padding: '8px 15px',
 | 
			
		||||
  borderRadius: "40px",
 | 
			
		||||
  gap: '4px',
 | 
			
		||||
  backgroundColor: "#4ACE91",
 | 
			
		||||
  color: '#fff',
 | 
			
		||||
  fontFamily: "Arial",
 | 
			
		||||
  transition: "all 0.3s ease-in-out",
 | 
			
		||||
  boxShadow: "none",
 | 
			
		||||
  "&:hover": {
 | 
			
		||||
    cursor: "pointer",
 | 
			
		||||
    boxShadow: "rgba(0, 0, 0, 0.15) 1.95px 1.95px 2.6px;",
 | 
			
		||||
    backgroundColor: "#4ACE91",
 | 
			
		||||
    filter: "brightness(1.1)",
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const AvatarContainer = styled(Box)({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  alignItems: 'center',
 | 
			
		||||
  "&:hover": {
 | 
			
		||||
    cursor: "pointer",
 | 
			
		||||
    "& #expand-icon": {
 | 
			
		||||
      transition: "all 0.3s ease-in-out",
 | 
			
		||||
      filter: "brightness(0.7)",
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const DropdownContainer = styled(Box)(({ theme }) => ({
 | 
			
		||||
  display: "flex",
 | 
			
		||||
  alignItems: "center",
 | 
			
		||||
  gap: "5px",
 | 
			
		||||
  backgroundColor: theme.palette.primary.main,
 | 
			
		||||
  padding: "10px 15px",
 | 
			
		||||
  transition: "all 0.4s ease-in-out",
 | 
			
		||||
  "&:hover": {
 | 
			
		||||
    cursor: "pointer",
 | 
			
		||||
    filter: "brightness(0.95)"
 | 
			
		||||
  }
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export const DropdownText = styled(Typography)(({ theme }) => ({
 | 
			
		||||
  fontFamily: "Arial",
 | 
			
		||||
  fontSize: "16px",
 | 
			
		||||
  color: theme.palette.text.primary,
 | 
			
		||||
  userSelect: "none"
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export const NavbarName = styled(Typography)(({ theme }) => ({
 | 
			
		||||
  fontFamily: "Arial",
 | 
			
		||||
  fontSize: "18px",
 | 
			
		||||
  color: theme.palette.text.primary,
 | 
			
		||||
  margin: "0 10px",
 | 
			
		||||
}));
 | 
			
		||||
							
								
								
									
										490
									
								
								src/components/layout/Navbar/Navbar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,490 @@
 | 
			
		||||
import React, { useMemo, useRef, useState } from 'react'
 | 
			
		||||
import {
 | 
			
		||||
  Typography,
 | 
			
		||||
  Box,
 | 
			
		||||
  Popover,
 | 
			
		||||
  useTheme,
 | 
			
		||||
  Button,
 | 
			
		||||
  Input,
 | 
			
		||||
  List,
 | 
			
		||||
  ListItem,
 | 
			
		||||
  ListItemText
 | 
			
		||||
} from '@mui/material'
 | 
			
		||||
import AccountCircle from '@mui/icons-material/AccountCircle'
 | 
			
		||||
import AddBoxIcon from '@mui/icons-material/AddBox'
 | 
			
		||||
import Badge from '@mui/material/Badge'
 | 
			
		||||
import NotificationsIcon from '@mui/icons-material/Notifications'
 | 
			
		||||
import ExitToAppIcon from '@mui/icons-material/ExitToApp'
 | 
			
		||||
import { useNavigate } from 'react-router-dom'
 | 
			
		||||
import { togglePublishBlogModal } from '../../../state/features/globalSlice'
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux'
 | 
			
		||||
import AutoStoriesIcon from '@mui/icons-material/AutoStories'
 | 
			
		||||
import { RootState } from '../../../state/store'
 | 
			
		||||
import { UserNavbar } from '../../common/UserNavbar/UserNavbar'
 | 
			
		||||
import { removePrefix } from '../../../utils/blogIdformats'
 | 
			
		||||
import { useLocation } from 'react-router-dom'
 | 
			
		||||
import BookmarkIcon from '@mui/icons-material/Bookmark'
 | 
			
		||||
import SubscriptionsIcon from '@mui/icons-material/Subscriptions'
 | 
			
		||||
import { BlockedNamesModal } from '../../common/BlockedNamesModal/BlockedNamesModal'
 | 
			
		||||
import SearchIcon from '@mui/icons-material/Search'
 | 
			
		||||
import EmailIcon from '@mui/icons-material/Email'
 | 
			
		||||
import localforage from 'localforage'
 | 
			
		||||
const notification = localforage.createInstance({
 | 
			
		||||
  name: 'notification'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
import BackspaceIcon from '@mui/icons-material/Backspace'
 | 
			
		||||
import {
 | 
			
		||||
  AvatarContainer,
 | 
			
		||||
  CreateBlogButton,
 | 
			
		||||
  CustomAppBar,
 | 
			
		||||
  CustomToolbar,
 | 
			
		||||
  DropdownContainer,
 | 
			
		||||
  DropdownText,
 | 
			
		||||
  QblogLogoContainer,
 | 
			
		||||
  StyledButton,
 | 
			
		||||
  AuthenticateButton,
 | 
			
		||||
  NavbarName
 | 
			
		||||
} from './Navbar-styles'
 | 
			
		||||
import { AccountCircleSVG } from '../../../assets/svgs/AccountCircleSVG'
 | 
			
		||||
import QblogLogo from '../../../assets/img/qBlogLogo.png'
 | 
			
		||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
 | 
			
		||||
import PersonOffIcon from '@mui/icons-material/PersonOff'
 | 
			
		||||
import { NewWindowSVG } from '../../../assets/svgs/NewWindowSVG'
 | 
			
		||||
import {
 | 
			
		||||
  addFilteredPosts,
 | 
			
		||||
  setFilterValue,
 | 
			
		||||
  setIsFiltering
 | 
			
		||||
} from '../../../state/features/blogSlice'
 | 
			
		||||
import { Item } from '../../common/Comments/CommentEditor'
 | 
			
		||||
import { formatDate } from '../../../utils/time'
 | 
			
		||||
interface Props {
 | 
			
		||||
  isAuthenticated: boolean
 | 
			
		||||
  hasBlog: boolean
 | 
			
		||||
  userName: string | null
 | 
			
		||||
  userAvatar: string
 | 
			
		||||
  blog: any
 | 
			
		||||
  authenticate: () => void
 | 
			
		||||
  hasAttemptedToFetchBlogInitial: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function useQuery() {
 | 
			
		||||
  return new URLSearchParams(useLocation().search)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const NavBar: React.FC<Props> = ({
 | 
			
		||||
  isAuthenticated,
 | 
			
		||||
  hasBlog,
 | 
			
		||||
  userName,
 | 
			
		||||
  userAvatar,
 | 
			
		||||
  blog,
 | 
			
		||||
  authenticate,
 | 
			
		||||
  hasAttemptedToFetchBlogInitial
 | 
			
		||||
}) => {
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
  const theme = useTheme()
 | 
			
		||||
  const query = useQuery()
 | 
			
		||||
  const { visitingBlog } = useSelector((state: RootState) => state.global)
 | 
			
		||||
  const notifications = useSelector(
 | 
			
		||||
    (state: RootState) => state.global.notifications
 | 
			
		||||
  )
 | 
			
		||||
  const notificationCreatorComment = useSelector(
 | 
			
		||||
    (state: RootState) => state.global.notificationCreatorComment
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const fullNotifications = useMemo(() => {
 | 
			
		||||
    return [...notificationCreatorComment, ...notifications].sort(
 | 
			
		||||
      (a, b) => b.created - a.created
 | 
			
		||||
    )
 | 
			
		||||
  }, [notificationCreatorComment, notifications])
 | 
			
		||||
  const location = useLocation()
 | 
			
		||||
  const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null)
 | 
			
		||||
  const [anchorElNotification, setAnchorElNotification] =
 | 
			
		||||
    React.useState<HTMLButtonElement | null>(null)
 | 
			
		||||
  const [isOpenModal, setIsOpenModal] = React.useState<boolean>(false)
 | 
			
		||||
  const [searchVal, setSearchVal] = useState<string>('')
 | 
			
		||||
  const searchValRef = useRef('')
 | 
			
		||||
  const inputRef = useRef<HTMLInputElement>(null)
 | 
			
		||||
  const stripBlogId = removePrefix(visitingBlog?.blogId || '')
 | 
			
		||||
  if (visitingBlog?.navbarConfig && location?.pathname?.includes(stripBlogId)) {
 | 
			
		||||
    return (
 | 
			
		||||
      <UserNavbar
 | 
			
		||||
        title={visitingBlog?.title || ''}
 | 
			
		||||
        menuItems={visitingBlog?.navbarConfig?.navItems || []}
 | 
			
		||||
        name={visitingBlog?.name || ''}
 | 
			
		||||
        blogId={visitingBlog?.blogId || ''}
 | 
			
		||||
      />
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
 | 
			
		||||
    const target = event.currentTarget as unknown as HTMLButtonElement | null
 | 
			
		||||
    setAnchorEl(target)
 | 
			
		||||
  }
 | 
			
		||||
  const openNotificationPopover = (event: any) => {
 | 
			
		||||
    const target = event.currentTarget as unknown as HTMLButtonElement | null
 | 
			
		||||
    setAnchorElNotification(target)
 | 
			
		||||
  }
 | 
			
		||||
  const closeNotificationPopover = () => {
 | 
			
		||||
    setAnchorElNotification(null)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleClose = () => {
 | 
			
		||||
    setAnchorEl(null)
 | 
			
		||||
  }
 | 
			
		||||
  const onClose = () => {
 | 
			
		||||
    setIsOpenModal(false)
 | 
			
		||||
  }
 | 
			
		||||
  const open = Boolean(anchorEl)
 | 
			
		||||
  const id = open ? 'simple-popover' : undefined
 | 
			
		||||
  const openPopover = Boolean(anchorElNotification)
 | 
			
		||||
  const idNotification = openPopover ? 'simple-popover-notification' : undefined
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <CustomAppBar position="sticky" elevation={2}>
 | 
			
		||||
      <CustomToolbar variant="dense">
 | 
			
		||||
        <QblogLogoContainer
 | 
			
		||||
          src={QblogLogo}
 | 
			
		||||
          alt="Qblog Logo"
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            navigate(`/`)
 | 
			
		||||
            dispatch(setIsFiltering(false))
 | 
			
		||||
            dispatch(setFilterValue(''))
 | 
			
		||||
            dispatch(addFilteredPosts([]))
 | 
			
		||||
            searchValRef.current = ''
 | 
			
		||||
            if (!inputRef.current) return
 | 
			
		||||
            inputRef.current.value = ''
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
        <Box
 | 
			
		||||
          sx={{
 | 
			
		||||
            display: 'flex',
 | 
			
		||||
            alignItems: 'center',
 | 
			
		||||
            gap: 1
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Input
 | 
			
		||||
            id="standard-adornment-name"
 | 
			
		||||
            inputRef={inputRef}
 | 
			
		||||
            onChange={(e) => {
 | 
			
		||||
              searchValRef.current = e.target.value
 | 
			
		||||
            }}
 | 
			
		||||
            onKeyDown={(event) => {
 | 
			
		||||
              if (event.key === 'Enter' || event.keyCode === 13) {
 | 
			
		||||
                if (!searchValRef.current) {
 | 
			
		||||
                  dispatch(setIsFiltering(false))
 | 
			
		||||
                  dispatch(setFilterValue(''))
 | 
			
		||||
                  dispatch(addFilteredPosts([]))
 | 
			
		||||
                  searchValRef.current = ''
 | 
			
		||||
                  if (!inputRef.current) return
 | 
			
		||||
                  inputRef.current.value = ''
 | 
			
		||||
                  return
 | 
			
		||||
                }
 | 
			
		||||
                navigate('/')
 | 
			
		||||
                dispatch(setIsFiltering(true))
 | 
			
		||||
                dispatch(addFilteredPosts([]))
 | 
			
		||||
                dispatch(setFilterValue(searchValRef.current))
 | 
			
		||||
              }
 | 
			
		||||
            }}
 | 
			
		||||
            placeholder="Filter by name"
 | 
			
		||||
            sx={{
 | 
			
		||||
              '&&:before': {
 | 
			
		||||
                borderBottom: 'none'
 | 
			
		||||
              },
 | 
			
		||||
              '&&:after': {
 | 
			
		||||
                borderBottom: 'none'
 | 
			
		||||
              },
 | 
			
		||||
              '&&:hover:before': {
 | 
			
		||||
                borderBottom: 'none'
 | 
			
		||||
              },
 | 
			
		||||
              '&&.Mui-focused:before': {
 | 
			
		||||
                borderBottom: 'none'
 | 
			
		||||
              },
 | 
			
		||||
              '&&.Mui-focused': {
 | 
			
		||||
                outline: 'none'
 | 
			
		||||
              },
 | 
			
		||||
              fontSize: '18px'
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
 | 
			
		||||
          <SearchIcon
 | 
			
		||||
            sx={{
 | 
			
		||||
              cursor: 'pointer'
 | 
			
		||||
            }}
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              if (!searchValRef.current) {
 | 
			
		||||
                dispatch(setIsFiltering(false))
 | 
			
		||||
                dispatch(setFilterValue(''))
 | 
			
		||||
                dispatch(addFilteredPosts([]))
 | 
			
		||||
                searchValRef.current = ''
 | 
			
		||||
                if (!inputRef.current) return
 | 
			
		||||
                inputRef.current.value = ''
 | 
			
		||||
                return
 | 
			
		||||
              }
 | 
			
		||||
              navigate('/')
 | 
			
		||||
              dispatch(setIsFiltering(true))
 | 
			
		||||
              dispatch(addFilteredPosts([]))
 | 
			
		||||
              dispatch(setFilterValue(searchValRef.current))
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
          <BackspaceIcon
 | 
			
		||||
            sx={{
 | 
			
		||||
              cursor: 'pointer'
 | 
			
		||||
            }}
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              dispatch(setIsFiltering(false))
 | 
			
		||||
              dispatch(setFilterValue(''))
 | 
			
		||||
              dispatch(addFilteredPosts([]))
 | 
			
		||||
              searchValRef.current = ''
 | 
			
		||||
              if (!inputRef.current) return
 | 
			
		||||
              inputRef.current.value = ''
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
        </Box>
 | 
			
		||||
        <Box
 | 
			
		||||
          sx={{
 | 
			
		||||
            display: 'flex',
 | 
			
		||||
            alignItems: 'center'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {/* Add isAuthenticated && before username and wrap StyledButton in this condition*/}
 | 
			
		||||
          {!isAuthenticated && (
 | 
			
		||||
            <AuthenticateButton onClick={authenticate}>
 | 
			
		||||
              <ExitToAppIcon />
 | 
			
		||||
              Authenticate
 | 
			
		||||
            </AuthenticateButton>
 | 
			
		||||
          )}
 | 
			
		||||
          <Badge
 | 
			
		||||
            badgeContent={fullNotifications.length}
 | 
			
		||||
            color="primary"
 | 
			
		||||
            sx={{
 | 
			
		||||
              margin: '0px 12px'
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Button
 | 
			
		||||
              onClick={(e) => {
 | 
			
		||||
                openNotificationPopover(e)
 | 
			
		||||
              }}
 | 
			
		||||
              sx={{
 | 
			
		||||
                margin: '0px',
 | 
			
		||||
                padding: '0px',
 | 
			
		||||
                height: 'auto',
 | 
			
		||||
                width: 'auto',
 | 
			
		||||
                minWidth: 'unset'
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <NotificationsIcon color="action" />
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Badge>
 | 
			
		||||
          <Popover
 | 
			
		||||
            id={idNotification}
 | 
			
		||||
            open={openPopover}
 | 
			
		||||
            anchorEl={anchorElNotification}
 | 
			
		||||
            onClose={closeNotificationPopover}
 | 
			
		||||
            anchorOrigin={{
 | 
			
		||||
              vertical: 'bottom',
 | 
			
		||||
              horizontal: 'left'
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Box>
 | 
			
		||||
              <List
 | 
			
		||||
                sx={{
 | 
			
		||||
                  maxHeight: '300px',
 | 
			
		||||
                  overflow: 'auto'
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                {fullNotifications.map((notification: any, index: number) => (
 | 
			
		||||
                  <ListItem
 | 
			
		||||
                    key={index}
 | 
			
		||||
                    divider
 | 
			
		||||
                    sx={{
 | 
			
		||||
                      cursor: 'pointer'
 | 
			
		||||
                    }}
 | 
			
		||||
                    onClick={async () => {
 | 
			
		||||
                      const str = notification.postId
 | 
			
		||||
                      const arr = str.split('-post-')
 | 
			
		||||
                      const str1 = arr[0]
 | 
			
		||||
                      const str2 = arr[1]
 | 
			
		||||
                      const blogId = removePrefix(str1)
 | 
			
		||||
                      navigate(
 | 
			
		||||
                        `/${notification.postName}/${blogId}/${str2}?comment=${notification.identifier}`
 | 
			
		||||
                      )
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <ListItemText
 | 
			
		||||
                      primary={
 | 
			
		||||
                        <React.Fragment>
 | 
			
		||||
                          <Typography
 | 
			
		||||
                            component="span"
 | 
			
		||||
                            variant="body1"
 | 
			
		||||
                            color="textPrimary"
 | 
			
		||||
                          >
 | 
			
		||||
                            From {notification.name}
 | 
			
		||||
                          </Typography>
 | 
			
		||||
                        </React.Fragment>
 | 
			
		||||
                      }
 | 
			
		||||
                      secondary={
 | 
			
		||||
                        <React.Fragment>
 | 
			
		||||
                          <Typography
 | 
			
		||||
                            component="span"
 | 
			
		||||
                            variant="body2"
 | 
			
		||||
                            color="textSecondary"
 | 
			
		||||
                          >
 | 
			
		||||
                            {formatDate(notification.created)}
 | 
			
		||||
                          </Typography>
 | 
			
		||||
                          <Typography
 | 
			
		||||
                            component="span"
 | 
			
		||||
                            variant="body2"
 | 
			
		||||
                            color="textSecondary"
 | 
			
		||||
                          >
 | 
			
		||||
                            {' -comment'}
 | 
			
		||||
                          </Typography>
 | 
			
		||||
                        </React.Fragment>
 | 
			
		||||
                      }
 | 
			
		||||
                    />
 | 
			
		||||
                  </ListItem>
 | 
			
		||||
                ))}
 | 
			
		||||
              </List>
 | 
			
		||||
            </Box>
 | 
			
		||||
          </Popover>
 | 
			
		||||
          {/* <button
 | 
			
		||||
            onClick={async () => {
 | 
			
		||||
              await qortalRequest({
 | 
			
		||||
                action: 'SET_TAB_NOTIFICATIONS',
 | 
			
		||||
                count: 2
 | 
			
		||||
              })
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            add notification
 | 
			
		||||
          </button> */}
 | 
			
		||||
          {isAuthenticated &&
 | 
			
		||||
            userName &&
 | 
			
		||||
            hasAttemptedToFetchBlogInitial &&
 | 
			
		||||
            !hasBlog && (
 | 
			
		||||
              <CreateBlogButton
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  dispatch(togglePublishBlogModal(true))
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <NewWindowSVG color="#fff" width="18" height="18" />
 | 
			
		||||
                Create Blog
 | 
			
		||||
              </CreateBlogButton>
 | 
			
		||||
            )}
 | 
			
		||||
          {isAuthenticated && userName && hasBlog && (
 | 
			
		||||
            <>
 | 
			
		||||
              <StyledButton
 | 
			
		||||
                color="primary"
 | 
			
		||||
                startIcon={<AddBoxIcon />}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  navigate(`/post/new`)
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                Create Post
 | 
			
		||||
              </StyledButton>
 | 
			
		||||
 | 
			
		||||
              <StyledButton
 | 
			
		||||
                color="primary"
 | 
			
		||||
                startIcon={<AutoStoriesIcon />}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  navigate(`/${userName}/${blog.blogId}`)
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                My Blog
 | 
			
		||||
              </StyledButton>
 | 
			
		||||
            </>
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          {isAuthenticated && userName && (
 | 
			
		||||
            <AvatarContainer onClick={handleClick}>
 | 
			
		||||
              <NavbarName>{userName}</NavbarName>
 | 
			
		||||
              {!userAvatar ? (
 | 
			
		||||
                <AccountCircleSVG
 | 
			
		||||
                  color={theme.palette.text.primary}
 | 
			
		||||
                  width="32"
 | 
			
		||||
                  height="32"
 | 
			
		||||
                />
 | 
			
		||||
              ) : (
 | 
			
		||||
                <img
 | 
			
		||||
                  src={userAvatar}
 | 
			
		||||
                  alt="User Avatar"
 | 
			
		||||
                  width="32"
 | 
			
		||||
                  height="32"
 | 
			
		||||
                  style={{
 | 
			
		||||
                    borderRadius: '50%'
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
              )}
 | 
			
		||||
              <ExpandMoreIcon id="expand-icon" sx={{ color: '#ACB6BF' }} />
 | 
			
		||||
            </AvatarContainer>
 | 
			
		||||
          )}
 | 
			
		||||
          <Popover
 | 
			
		||||
            id={id}
 | 
			
		||||
            open={open}
 | 
			
		||||
            anchorEl={anchorEl}
 | 
			
		||||
            onClose={handleClose}
 | 
			
		||||
            anchorOrigin={{
 | 
			
		||||
              vertical: 'bottom',
 | 
			
		||||
              horizontal: 'left'
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <DropdownContainer onClick={() => navigate('/favorites')}>
 | 
			
		||||
              <BookmarkIcon
 | 
			
		||||
                sx={{
 | 
			
		||||
                  color: '#50e3c2'
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
              <DropdownText>Favorites</DropdownText>
 | 
			
		||||
            </DropdownContainer>
 | 
			
		||||
            <DropdownContainer onClick={() => navigate('/subscriptions')}>
 | 
			
		||||
              <SubscriptionsIcon
 | 
			
		||||
                sx={{
 | 
			
		||||
                  color: '#5f50e3'
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
              <DropdownText>Subscriptions</DropdownText>
 | 
			
		||||
            </DropdownContainer>
 | 
			
		||||
            <DropdownContainer
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                setIsOpenModal(true)
 | 
			
		||||
                handleClose()
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <PersonOffIcon
 | 
			
		||||
                sx={{
 | 
			
		||||
                  color: '#e35050'
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
              <DropdownText>Blocked Names</DropdownText>
 | 
			
		||||
            </DropdownContainer>
 | 
			
		||||
            <DropdownContainer>
 | 
			
		||||
              <a
 | 
			
		||||
                href="qortal://APP/Q-Mail"
 | 
			
		||||
                className="qortal-link"
 | 
			
		||||
                style={{
 | 
			
		||||
                  width: '100%',
 | 
			
		||||
                  display: 'flex',
 | 
			
		||||
                  gap: '5px',
 | 
			
		||||
                  alignItems: 'center'
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <EmailIcon
 | 
			
		||||
                  sx={{
 | 
			
		||||
                    color: '#50e3c2'
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <DropdownText>Q-Mail</DropdownText>
 | 
			
		||||
              </a>
 | 
			
		||||
            </DropdownContainer>
 | 
			
		||||
          </Popover>
 | 
			
		||||
          {isOpenModal && (
 | 
			
		||||
            <BlockedNamesModal open={isOpenModal} onClose={onClose} />
 | 
			
		||||
          )}
 | 
			
		||||
        </Box>
 | 
			
		||||
      </CustomToolbar>
 | 
			
		||||
    </CustomAppBar>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default NavBar
 | 
			
		||||
							
								
								
									
										70
									
								
								src/components/modals/ConsentModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,70 @@
 | 
			
		||||
import * as React from 'react'
 | 
			
		||||
import Button from '@mui/material/Button'
 | 
			
		||||
import Dialog from '@mui/material/Dialog'
 | 
			
		||||
import DialogActions from '@mui/material/DialogActions'
 | 
			
		||||
import DialogContent from '@mui/material/DialogContent'
 | 
			
		||||
import DialogContentText from '@mui/material/DialogContentText'
 | 
			
		||||
import DialogTitle from '@mui/material/DialogTitle'
 | 
			
		||||
import localForage from 'localforage'
 | 
			
		||||
import { useTheme } from '@mui/material'
 | 
			
		||||
const generalLocal = localForage.createInstance({
 | 
			
		||||
  name: 'q-blog-general'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default function ConsentModal() {
 | 
			
		||||
  const theme = useTheme()
 | 
			
		||||
 | 
			
		||||
  const [open, setOpen] = React.useState(false)
 | 
			
		||||
 | 
			
		||||
  const handleClose = () => {
 | 
			
		||||
    setOpen(false)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getIsConsented = React.useCallback(async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const hasConsented = await generalLocal.getItem('general-consent')
 | 
			
		||||
      if (hasConsented) return
 | 
			
		||||
 | 
			
		||||
      setOpen(true)
 | 
			
		||||
      generalLocal.setItem('general-consent', true)
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    getIsConsented()
 | 
			
		||||
  }, [])
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <Dialog
 | 
			
		||||
        open={open}
 | 
			
		||||
        onClose={handleClose}
 | 
			
		||||
        aria-labelledby="alert-dialog-title"
 | 
			
		||||
        aria-describedby="alert-dialog-description"
 | 
			
		||||
      >
 | 
			
		||||
        <DialogTitle id="alert-dialog-title">Welcome</DialogTitle>
 | 
			
		||||
        <DialogContent>
 | 
			
		||||
          <DialogContentText id="alert-dialog-description">
 | 
			
		||||
            The Qortal community, along with its development team and the
 | 
			
		||||
            creators of this application, cannot be held accountable for any
 | 
			
		||||
            content published or displayed. Furthermore, they bear no
 | 
			
		||||
            responsibility for any data loss that may occur as a result of using
 | 
			
		||||
            this application.
 | 
			
		||||
          </DialogContentText>
 | 
			
		||||
        </DialogContent>
 | 
			
		||||
        <DialogActions>
 | 
			
		||||
          <Button
 | 
			
		||||
            sx={{
 | 
			
		||||
              backgroundColor: theme.palette.primary.light,
 | 
			
		||||
              color: theme.palette.text.primary,
 | 
			
		||||
              fontFamily: 'Arial'
 | 
			
		||||
            }}
 | 
			
		||||
            onClick={handleClose}
 | 
			
		||||
            autoFocus
 | 
			
		||||
          >
 | 
			
		||||
            Close
 | 
			
		||||
          </Button>
 | 
			
		||||
        </DialogActions>
 | 
			
		||||
      </Dialog>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										247
									
								
								src/components/modals/EditBlogModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,247 @@
 | 
			
		||||
import React, { useState } from 'react'
 | 
			
		||||
import {
 | 
			
		||||
  Box,
 | 
			
		||||
  Button,
 | 
			
		||||
  TextField,
 | 
			
		||||
  Typography,
 | 
			
		||||
  Modal,
 | 
			
		||||
  Select,
 | 
			
		||||
  MenuItem,
 | 
			
		||||
  FormControl,
 | 
			
		||||
  InputLabel,
 | 
			
		||||
  SelectChangeEvent,
 | 
			
		||||
  OutlinedInput,
 | 
			
		||||
  Chip,
 | 
			
		||||
  IconButton
 | 
			
		||||
} from '@mui/material'
 | 
			
		||||
import { useDispatch } from 'react-redux'
 | 
			
		||||
import { togglePublishBlogModal } from '../../state/features/globalSlice'
 | 
			
		||||
import AddIcon from '@mui/icons-material/Add'
 | 
			
		||||
import CloseIcon from '@mui/icons-material/Close'
 | 
			
		||||
import { styled } from '@mui/system'
 | 
			
		||||
interface SelectOption {
 | 
			
		||||
  id: string
 | 
			
		||||
  name: string
 | 
			
		||||
}
 | 
			
		||||
interface MyModalProps {
 | 
			
		||||
  open: boolean
 | 
			
		||||
  onClose: () => void
 | 
			
		||||
  onPublish: (
 | 
			
		||||
    title: string,
 | 
			
		||||
    description: string,
 | 
			
		||||
    category: string,
 | 
			
		||||
    tags: string[]
 | 
			
		||||
  ) => Promise<void>
 | 
			
		||||
  currentBlog: any
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ChipContainer = styled(Box)({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  flexWrap: 'wrap',
 | 
			
		||||
  '& > *': {
 | 
			
		||||
    margin: '4px'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const MyModal: React.FC<MyModalProps> = ({
 | 
			
		||||
  open,
 | 
			
		||||
  onClose,
 | 
			
		||||
  onPublish,
 | 
			
		||||
  currentBlog
 | 
			
		||||
}) => {
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
 | 
			
		||||
  const [title, setTitle] = useState<string>('')
 | 
			
		||||
  const [description, setDescription] = useState<string>('')
 | 
			
		||||
  const [errorMessage, setErrorMessage] = useState<string>('')
 | 
			
		||||
  const [selectedOption, setSelectedOption] = useState<SelectOption | null>(
 | 
			
		||||
    null
 | 
			
		||||
  )
 | 
			
		||||
  const [inputValue, setInputValue] = useState<string>('')
 | 
			
		||||
  const [chips, setChips] = useState<string[]>([])
 | 
			
		||||
 | 
			
		||||
  const [options, setOptions] = useState<SelectOption[]>([])
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    if (currentBlog) {
 | 
			
		||||
      setTitle(currentBlog?.title || '')
 | 
			
		||||
      setDescription(currentBlog?.description || '')
 | 
			
		||||
      const findCategory = options.find(
 | 
			
		||||
        (option) => option.id === currentBlog?.category
 | 
			
		||||
      )
 | 
			
		||||
      if (!findCategory) return
 | 
			
		||||
      setSelectedOption(findCategory)
 | 
			
		||||
      if (!currentBlog?.tags || !Array.isArray(currentBlog.tags)) return
 | 
			
		||||
      setChips(currentBlog.tags)
 | 
			
		||||
    }
 | 
			
		||||
  }, [currentBlog, options])
 | 
			
		||||
 | 
			
		||||
  const handlePublish = async (): Promise<void> => {
 | 
			
		||||
    try {
 | 
			
		||||
      await onPublish(title, description, selectedOption?.id || '', chips)
 | 
			
		||||
      handleClose()
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
      setErrorMessage(error.message)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleClose = (): void => {
 | 
			
		||||
    setErrorMessage('')
 | 
			
		||||
    dispatch(togglePublishBlogModal(false))
 | 
			
		||||
    onClose()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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 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 (
 | 
			
		||||
    <Modal
 | 
			
		||||
      open={open}
 | 
			
		||||
      onClose={onClose}
 | 
			
		||||
      aria-labelledby="modal-title"
 | 
			
		||||
      aria-describedby="modal-description"
 | 
			
		||||
    >
 | 
			
		||||
      <Box
 | 
			
		||||
        sx={{
 | 
			
		||||
          position: 'absolute',
 | 
			
		||||
          top: '50%',
 | 
			
		||||
          left: '50%',
 | 
			
		||||
          transform: 'translate(-50%, -50%)',
 | 
			
		||||
          width: 400,
 | 
			
		||||
          bgcolor: 'background.paper',
 | 
			
		||||
          boxShadow: 24,
 | 
			
		||||
          p: 4,
 | 
			
		||||
          display: 'flex',
 | 
			
		||||
          flexDirection: 'column',
 | 
			
		||||
          gap: 2
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Typography id="modal-title" variant="h6" component="h2">
 | 
			
		||||
          Edit Blog
 | 
			
		||||
        </Typography>
 | 
			
		||||
        <TextField
 | 
			
		||||
          id="modal-title-input"
 | 
			
		||||
          label="Title"
 | 
			
		||||
          value={title}
 | 
			
		||||
          onChange={(e) => setTitle(e.target.value)}
 | 
			
		||||
          fullWidth
 | 
			
		||||
        />
 | 
			
		||||
        <TextField
 | 
			
		||||
          id="modal-description-input"
 | 
			
		||||
          label="Description"
 | 
			
		||||
          value={description}
 | 
			
		||||
          onChange={(e) => setDescription(e.target.value)}
 | 
			
		||||
          multiline
 | 
			
		||||
          rows={4}
 | 
			
		||||
          fullWidth
 | 
			
		||||
        />
 | 
			
		||||
        {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>
 | 
			
		||||
        {errorMessage && (
 | 
			
		||||
          <Typography color="error" variant="body1">
 | 
			
		||||
            {errorMessage}
 | 
			
		||||
          </Typography>
 | 
			
		||||
        )}
 | 
			
		||||
        <Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
 | 
			
		||||
          <Button variant="outlined" color="error" onClick={handleClose}>
 | 
			
		||||
            Cancel
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button variant="contained" color="success" onClick={handlePublish}>
 | 
			
		||||
            Publish
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Box>
 | 
			
		||||
      </Box>
 | 
			
		||||
    </Modal>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default MyModal
 | 
			
		||||
							
								
								
									
										281
									
								
								src/components/modals/PublishBlogModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,281 @@
 | 
			
		||||
import React, { ChangeEvent, useState } from 'react'
 | 
			
		||||
import {
 | 
			
		||||
  Box,
 | 
			
		||||
  Button,
 | 
			
		||||
  TextField,
 | 
			
		||||
  Typography,
 | 
			
		||||
  Modal,
 | 
			
		||||
  Select,
 | 
			
		||||
  MenuItem,
 | 
			
		||||
  FormControl,
 | 
			
		||||
  InputLabel,
 | 
			
		||||
  SelectChangeEvent,
 | 
			
		||||
  OutlinedInput,
 | 
			
		||||
  Chip,
 | 
			
		||||
  IconButton
 | 
			
		||||
} from '@mui/material'
 | 
			
		||||
import { useDispatch } from 'react-redux'
 | 
			
		||||
import { togglePublishBlogModal } from '../../state/features/globalSlice'
 | 
			
		||||
import AddIcon from '@mui/icons-material/Add'
 | 
			
		||||
import CloseIcon from '@mui/icons-material/Close'
 | 
			
		||||
import { styled } from '@mui/system'
 | 
			
		||||
interface SelectOption {
 | 
			
		||||
  id: string
 | 
			
		||||
  name: string
 | 
			
		||||
}
 | 
			
		||||
interface MyModalProps {
 | 
			
		||||
  open: boolean
 | 
			
		||||
  onClose: () => void
 | 
			
		||||
  onPublish: (
 | 
			
		||||
    title: string,
 | 
			
		||||
    description: string,
 | 
			
		||||
    category: string,
 | 
			
		||||
    tags: string[],
 | 
			
		||||
    blogIdentifier: string
 | 
			
		||||
  ) => Promise<void>
 | 
			
		||||
  username: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ChipContainer = styled(Box)({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  flexWrap: 'wrap',
 | 
			
		||||
  '& > *': {
 | 
			
		||||
    margin: '4px'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const MyModal: React.FC<MyModalProps> = ({
 | 
			
		||||
  open,
 | 
			
		||||
  onClose,
 | 
			
		||||
  onPublish,
 | 
			
		||||
  username
 | 
			
		||||
}) => {
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
 | 
			
		||||
  const [title, setTitle] = useState<string>('')
 | 
			
		||||
  const [description, setDescription] = useState<string>('')
 | 
			
		||||
  const [errorMessage, setErrorMessage] = useState<string>('')
 | 
			
		||||
  const [selectedOption, setSelectedOption] = useState<SelectOption | null>(
 | 
			
		||||
    null
 | 
			
		||||
  )
 | 
			
		||||
  const [inputValue, setInputValue] = useState<string>('')
 | 
			
		||||
  const [chips, setChips] = useState<string[]>([])
 | 
			
		||||
  const [blogIdentifier, setBlogIdentifier] = useState(username || '')
 | 
			
		||||
  const [options, setOptions] = useState<SelectOption[]>([])
 | 
			
		||||
  const handlePublish = async (): Promise<void> => {
 | 
			
		||||
    try {
 | 
			
		||||
      await onPublish(
 | 
			
		||||
        title,
 | 
			
		||||
        description,
 | 
			
		||||
        selectedOption?.id || '',
 | 
			
		||||
        chips,
 | 
			
		||||
        blogIdentifier
 | 
			
		||||
      )
 | 
			
		||||
      handleClose()
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
      setErrorMessage(error.message)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleClose = (): void => {
 | 
			
		||||
    setTitle('')
 | 
			
		||||
    setDescription('')
 | 
			
		||||
    setErrorMessage('')
 | 
			
		||||
    dispatch(togglePublishBlogModal(false))
 | 
			
		||||
    onClose()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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 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])
 | 
			
		||||
 | 
			
		||||
  const handleInputChangeId = (event: ChangeEvent<HTMLInputElement>) => {
 | 
			
		||||
    // Replace any non-alphanumeric and non-space characters with an empty string
 | 
			
		||||
    // Replace multiple spaces with a single dash and remove any dashes that come one after another
 | 
			
		||||
    let newValue = event.target.value
 | 
			
		||||
      .replace(/[^a-zA-Z0-9\s-]/g, '')
 | 
			
		||||
      .replace(/\s+/g, '-')
 | 
			
		||||
      .replace(/-+/g, '-')
 | 
			
		||||
      .trim()
 | 
			
		||||
 | 
			
		||||
    if (newValue.toLowerCase().includes('post')) {
 | 
			
		||||
      // Replace the 'post' string with an empty string
 | 
			
		||||
      newValue = newValue.replace(/post/gi, '')
 | 
			
		||||
    }
 | 
			
		||||
    if (newValue.toLowerCase().includes('q-blog')) {
 | 
			
		||||
      // Replace the 'q-blog' string with an empty string
 | 
			
		||||
      newValue = newValue.replace(/q-blog/gi, '')
 | 
			
		||||
    }
 | 
			
		||||
    setBlogIdentifier(newValue)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Modal
 | 
			
		||||
      open={open}
 | 
			
		||||
      onClose={onClose}
 | 
			
		||||
      aria-labelledby="modal-title"
 | 
			
		||||
      aria-describedby="modal-description"
 | 
			
		||||
    >
 | 
			
		||||
      <Box
 | 
			
		||||
        sx={{
 | 
			
		||||
          position: 'absolute',
 | 
			
		||||
          top: '50%',
 | 
			
		||||
          left: '50%',
 | 
			
		||||
          transform: 'translate(-50%, -50%)',
 | 
			
		||||
          width: 400,
 | 
			
		||||
          bgcolor: 'background.paper',
 | 
			
		||||
          boxShadow: 24,
 | 
			
		||||
          p: 4,
 | 
			
		||||
          display: 'flex',
 | 
			
		||||
          flexDirection: 'column',
 | 
			
		||||
          gap: 2,
 | 
			
		||||
          overflowY: 'auto',
 | 
			
		||||
          maxHeight: '95vh'
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Typography id="modal-title" variant="h6" component="h2">
 | 
			
		||||
          Create blog
 | 
			
		||||
        </Typography>
 | 
			
		||||
        <TextField
 | 
			
		||||
          id="modal-title-input"
 | 
			
		||||
          label="Url Preview"
 | 
			
		||||
          value={`/${username}/${blogIdentifier}`}
 | 
			
		||||
          // onChange={(e) => setTitle(e.target.value)}
 | 
			
		||||
          fullWidth
 | 
			
		||||
          disabled={true}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <TextField
 | 
			
		||||
          id="modal-blogId-input"
 | 
			
		||||
          label="Blog Id"
 | 
			
		||||
          value={blogIdentifier}
 | 
			
		||||
          onChange={handleInputChangeId}
 | 
			
		||||
          fullWidth
 | 
			
		||||
          inputProps={{ maxLength: 20 }}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <TextField
 | 
			
		||||
          id="modal-title-input"
 | 
			
		||||
          label="Title"
 | 
			
		||||
          value={title}
 | 
			
		||||
          onChange={(e) => setTitle(e.target.value)}
 | 
			
		||||
          fullWidth
 | 
			
		||||
        />
 | 
			
		||||
        <TextField
 | 
			
		||||
          id="modal-description-input"
 | 
			
		||||
          label="Description"
 | 
			
		||||
          value={description}
 | 
			
		||||
          onChange={(e) => setDescription(e.target.value)}
 | 
			
		||||
          multiline
 | 
			
		||||
          rows={4}
 | 
			
		||||
          fullWidth
 | 
			
		||||
        />
 | 
			
		||||
        {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>
 | 
			
		||||
        {errorMessage && (
 | 
			
		||||
          <Typography color="error" variant="body1">
 | 
			
		||||
            {errorMessage}
 | 
			
		||||
          </Typography>
 | 
			
		||||
        )}
 | 
			
		||||
        <Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 1 }}>
 | 
			
		||||
          <Button variant="outlined" color="error" onClick={handleClose}>
 | 
			
		||||
            Cancel
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button variant="contained" color="success" onClick={handlePublish}>
 | 
			
		||||
            Publish
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Box>
 | 
			
		||||
      </Box>
 | 
			
		||||
    </Modal>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default MyModal
 | 
			
		||||
							
								
								
									
										47
									
								
								src/components/modals/ReusableModal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,47 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Box, Modal, useTheme } from '@mui/material'
 | 
			
		||||
 | 
			
		||||
interface MyModalProps {
 | 
			
		||||
  open: boolean
 | 
			
		||||
  onClose?: () => void
 | 
			
		||||
  onSubmit?: (obj: any) => Promise<void>
 | 
			
		||||
  children: any
 | 
			
		||||
  customStyles?: any
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const ReusableModal: React.FC<MyModalProps> = ({
 | 
			
		||||
  open,
 | 
			
		||||
  onClose,
 | 
			
		||||
  onSubmit,
 | 
			
		||||
  children,
 | 
			
		||||
  customStyles = {}
 | 
			
		||||
}) => {
 | 
			
		||||
  const theme = useTheme()
 | 
			
		||||
  return (
 | 
			
		||||
    <Modal
 | 
			
		||||
      open={open}
 | 
			
		||||
      onClose={onClose}
 | 
			
		||||
      aria-labelledby="modal-title"
 | 
			
		||||
      aria-describedby="modal-description"
 | 
			
		||||
    >
 | 
			
		||||
      <Box
 | 
			
		||||
        sx={{
 | 
			
		||||
          position: 'absolute',
 | 
			
		||||
          top: '50%',
 | 
			
		||||
          left: '50%',
 | 
			
		||||
          transform: 'translate(-50%, -50%)',
 | 
			
		||||
          width: '75%',
 | 
			
		||||
          bgcolor: theme.palette.primary.main,
 | 
			
		||||
          boxShadow: 24,
 | 
			
		||||
          p: 4,
 | 
			
		||||
          display: 'flex',
 | 
			
		||||
          flexDirection: 'column',
 | 
			
		||||
          gap: 2,
 | 
			
		||||
          ...customStyles
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {children}
 | 
			
		||||
      </Box>
 | 
			
		||||
    </Modal>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3
									
								
								src/constants/mail.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,3 @@
 | 
			
		||||
export const MAIL_SERVICE_TYPE: 'MAIL_PRIVATE' = 'MAIL_PRIVATE'
 | 
			
		||||
export const MAIL_ATTACHMENT_SERVICE_TYPE: 'ATTACHMENT_PRIVATE' =
 | 
			
		||||
  'ATTACHMENT_PRIVATE'
 | 
			
		||||
							
								
								
									
										61
									
								
								src/global.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,61 @@
 | 
			
		||||
// src/global.d.ts
 | 
			
		||||
interface QortalRequestOptions {
 | 
			
		||||
  action: string
 | 
			
		||||
  name?: string
 | 
			
		||||
  service?: string
 | 
			
		||||
  data64?: string
 | 
			
		||||
  title?: string
 | 
			
		||||
  description?: string
 | 
			
		||||
  category?: string
 | 
			
		||||
  tags?: string[]
 | 
			
		||||
  identifier?: string
 | 
			
		||||
  address?: string
 | 
			
		||||
  metaData?: string
 | 
			
		||||
  encoding?: string
 | 
			
		||||
  includeMetadata?: boolean
 | 
			
		||||
  limit?: numebr
 | 
			
		||||
  offset?: number
 | 
			
		||||
  reverse?: boolean
 | 
			
		||||
  resources?: any[]
 | 
			
		||||
  filename?: string
 | 
			
		||||
  list_name?: string
 | 
			
		||||
  item?: string
 | 
			
		||||
  items?: strings[]
 | 
			
		||||
  tag1?: string
 | 
			
		||||
  tag2?: string
 | 
			
		||||
  tag3?: string
 | 
			
		||||
  tag4?: string
 | 
			
		||||
  tag5?: string
 | 
			
		||||
  coin?: string
 | 
			
		||||
  destinationAddress?: string
 | 
			
		||||
  amount?: number
 | 
			
		||||
  blob?: Blob
 | 
			
		||||
  mimeType?: string
 | 
			
		||||
  file?: File
 | 
			
		||||
  count?: number
 | 
			
		||||
  query?: string
 | 
			
		||||
  exactMatchNames?: boolean
 | 
			
		||||
  excludeBlocked?: boolean
 | 
			
		||||
  mode?: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare function qortalRequest(options: QortalRequestOptions): Promise<any>
 | 
			
		||||
declare function qortalRequestWithTimeout(
 | 
			
		||||
  options: QortalRequestOptions,
 | 
			
		||||
  time: number
 | 
			
		||||
): Promise<any>
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface Window {
 | 
			
		||||
    _qdnBase: any // Replace 'any' with the appropriate type if you know it
 | 
			
		||||
    _qdnTheme: string
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface Window {
 | 
			
		||||
    showSaveFilePicker: (
 | 
			
		||||
      options?: SaveFilePickerOptions
 | 
			
		||||
    ) => Promise<FileSystemFileHandle>
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										469
									
								
								src/hooks/useFetchMail.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,469 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux'
 | 
			
		||||
import {
 | 
			
		||||
  addPosts,
 | 
			
		||||
  addToHashMap,
 | 
			
		||||
  BlogPost,
 | 
			
		||||
  populateFavorites,
 | 
			
		||||
  setCountNewPosts,
 | 
			
		||||
  upsertFilteredPosts,
 | 
			
		||||
  upsertPosts,
 | 
			
		||||
  upsertPostsBeginning,
 | 
			
		||||
  upsertSubscriptionPosts
 | 
			
		||||
} from '../state/features/blogSlice'
 | 
			
		||||
import {
 | 
			
		||||
  setCurrentBlog,
 | 
			
		||||
  setIsLoadingGlobal,
 | 
			
		||||
  setUserAvatarHash
 | 
			
		||||
} from '../state/features/globalSlice'
 | 
			
		||||
import { RootState } from '../state/store'
 | 
			
		||||
import { fetchAndEvaluatePosts } from '../utils/fetchPosts'
 | 
			
		||||
import { fetchAndEvaluateMail } from '../utils/fetchMail'
 | 
			
		||||
import {
 | 
			
		||||
  addToHashMapMail,
 | 
			
		||||
  upsertMessages,
 | 
			
		||||
  upsertMessagesBeginning
 | 
			
		||||
} from '../state/features/mailSlice'
 | 
			
		||||
import { MAIL_SERVICE_TYPE } from '../constants/mail'
 | 
			
		||||
 | 
			
		||||
export const useFetchMail = () => {
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
  const hashMapPosts = useSelector(
 | 
			
		||||
    (state: RootState) => state.blog.hashMapPosts
 | 
			
		||||
  )
 | 
			
		||||
  const hashMapMailMessages = useSelector(
 | 
			
		||||
    (state: RootState) => state.mail.hashMapMailMessages
 | 
			
		||||
  )
 | 
			
		||||
  const posts = useSelector((state: RootState) => state.blog.posts)
 | 
			
		||||
  const mailMessages = useSelector(
 | 
			
		||||
    (state: RootState) => state.mail.mailMessages
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const filteredPosts = useSelector(
 | 
			
		||||
    (state: RootState) => state.blog.filteredPosts
 | 
			
		||||
  )
 | 
			
		||||
  const favoritesLocal = useSelector(
 | 
			
		||||
    (state: RootState) => state.blog.favoritesLocal
 | 
			
		||||
  )
 | 
			
		||||
  const favorites = useSelector((state: RootState) => state.blog.favorites)
 | 
			
		||||
  const subscriptionPosts = useSelector(
 | 
			
		||||
    (state: RootState) => state.blog.subscriptionPosts
 | 
			
		||||
  )
 | 
			
		||||
  const subscriptions = useSelector(
 | 
			
		||||
    (state: RootState) => state.blog.subscriptions
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const checkAndUpdatePost = React.useCallback(
 | 
			
		||||
    (post: BlogPost) => {
 | 
			
		||||
      // Check if the post exists in hashMapPosts
 | 
			
		||||
      const existingPost = hashMapPosts[post.id]
 | 
			
		||||
      if (!existingPost) {
 | 
			
		||||
        // If the post doesn't exist, add it to hashMapPosts
 | 
			
		||||
        return true
 | 
			
		||||
      } else if (
 | 
			
		||||
        post?.updated &&
 | 
			
		||||
        existingPost?.updated &&
 | 
			
		||||
        (!existingPost?.updated || post?.updated) > existingPost?.updated
 | 
			
		||||
      ) {
 | 
			
		||||
        // If the post exists and its updated is more recent than the existing post's updated, update it in hashMapPosts
 | 
			
		||||
        return true
 | 
			
		||||
      } else {
 | 
			
		||||
        return false
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [hashMapPosts]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const getBlogPost = async (user: string, postId: string, content: any) => {
 | 
			
		||||
    const res = await fetchAndEvaluatePosts({
 | 
			
		||||
      user,
 | 
			
		||||
      postId,
 | 
			
		||||
      content
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    dispatch(addToHashMap(res))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getMailMessage = async (user: string, postId: string, content: any) => {
 | 
			
		||||
    const res = await fetchAndEvaluateMail({
 | 
			
		||||
      user,
 | 
			
		||||
      postId,
 | 
			
		||||
      content
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    dispatch(addToHashMapMail(res))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const checkNewMessages = React.useCallback(
 | 
			
		||||
    async (recipientName: string, recipientAddress: string) => {
 | 
			
		||||
      try {
 | 
			
		||||
        const query = `qortal_qmail_${recipientName.slice(
 | 
			
		||||
          0,
 | 
			
		||||
          20
 | 
			
		||||
        )}_${recipientAddress.slice(-6)}_mail_`
 | 
			
		||||
        const url = `/arbitrary/resources/search?mode=ALL&service=${MAIL_SERVICE_TYPE}&query=${query}&limit=20&includemetadata=true&reverse=true&excludeblocked=true`
 | 
			
		||||
        const response = await fetch(url, {
 | 
			
		||||
          method: 'GET',
 | 
			
		||||
          headers: {
 | 
			
		||||
            'Content-Type': 'application/json'
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        const responseData = await response.json()
 | 
			
		||||
        const latestPost = mailMessages[0]
 | 
			
		||||
        if (!latestPost) return
 | 
			
		||||
        const findPost = responseData?.findIndex(
 | 
			
		||||
          (item: any) => item?.identifier === latestPost?.id
 | 
			
		||||
        )
 | 
			
		||||
        if (findPost === -1) {
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
        const newArray = responseData.slice(0, findPost)
 | 
			
		||||
        const structureData = newArray.map((post: any): BlogPost => {
 | 
			
		||||
          return {
 | 
			
		||||
            title: post?.metadata?.title,
 | 
			
		||||
            category: post?.metadata?.category,
 | 
			
		||||
            categoryName: post?.metadata?.categoryName,
 | 
			
		||||
            tags: post?.metadata?.tags || [],
 | 
			
		||||
            description: post?.metadata?.description,
 | 
			
		||||
            createdAt: post?.created,
 | 
			
		||||
            updated: post?.updated,
 | 
			
		||||
            user: post.name,
 | 
			
		||||
            id: post.identifier
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        dispatch(upsertMessagesBeginning(structureData))
 | 
			
		||||
        return
 | 
			
		||||
      } catch (error) {}
 | 
			
		||||
    },
 | 
			
		||||
    [mailMessages]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const getNewPosts = React.useCallback(async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      dispatch(setIsLoadingGlobal(true))
 | 
			
		||||
      dispatch(setCountNewPosts(0))
 | 
			
		||||
      const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_POST&query=q-blog-&limit=20&includemetadata=true&reverse=true&excludeblocked=true`
 | 
			
		||||
      const response = await fetch(url, {
 | 
			
		||||
        method: 'GET',
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'application/json'
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      const responseData = await response.json()
 | 
			
		||||
      const latestPost = posts[0]
 | 
			
		||||
      if (!latestPost) return
 | 
			
		||||
      const findPost = responseData?.findIndex(
 | 
			
		||||
        (item: any) => item?.identifier === latestPost?.id
 | 
			
		||||
      )
 | 
			
		||||
      let fetchAll = responseData
 | 
			
		||||
      let willFetchAll = true
 | 
			
		||||
      if (findPost !== -1) {
 | 
			
		||||
        willFetchAll = false
 | 
			
		||||
        fetchAll = responseData.slice(0, findPost)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const structureData = fetchAll.map((post: any): BlogPost => {
 | 
			
		||||
        return {
 | 
			
		||||
          title: post?.metadata?.title,
 | 
			
		||||
          category: post?.metadata?.category,
 | 
			
		||||
          categoryName: post?.metadata?.categoryName,
 | 
			
		||||
          tags: post?.metadata?.tags || [],
 | 
			
		||||
          description: post?.metadata?.description,
 | 
			
		||||
          createdAt: post?.created,
 | 
			
		||||
          updated: post?.updated,
 | 
			
		||||
          user: post.name,
 | 
			
		||||
          postImage: '',
 | 
			
		||||
          id: post.identifier
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      if (!willFetchAll) {
 | 
			
		||||
        dispatch(upsertPostsBeginning(structureData))
 | 
			
		||||
      }
 | 
			
		||||
      if (willFetchAll) {
 | 
			
		||||
        dispatch(addPosts(structureData))
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      for (const content of structureData) {
 | 
			
		||||
        if (content.user && content.id) {
 | 
			
		||||
          const res = checkAndUpdatePost(content)
 | 
			
		||||
          if (res) {
 | 
			
		||||
            getBlogPost(content.user, content.id, content)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
    } finally {
 | 
			
		||||
      dispatch(setIsLoadingGlobal(false))
 | 
			
		||||
    }
 | 
			
		||||
  }, [posts, hashMapPosts])
 | 
			
		||||
 | 
			
		||||
  const getBlogPosts = React.useCallback(async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const offset = posts.length
 | 
			
		||||
 | 
			
		||||
      dispatch(setIsLoadingGlobal(true))
 | 
			
		||||
      const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_POST&query=q-blog-&limit=20&includemetadata=true&offset=${offset}&reverse=true&excludeblocked=true`
 | 
			
		||||
      const response = await fetch(url, {
 | 
			
		||||
        method: 'GET',
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'application/json'
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      const responseData = await response.json()
 | 
			
		||||
      const structureData = responseData.map((post: any): BlogPost => {
 | 
			
		||||
        return {
 | 
			
		||||
          title: post?.metadata?.title,
 | 
			
		||||
          category: post?.metadata?.category,
 | 
			
		||||
          categoryName: post?.metadata?.categoryName,
 | 
			
		||||
          tags: post?.metadata?.tags || [],
 | 
			
		||||
          description: post?.metadata?.description,
 | 
			
		||||
          createdAt: post?.created,
 | 
			
		||||
          updated: post?.updated,
 | 
			
		||||
          user: post.name,
 | 
			
		||||
          postImage: '',
 | 
			
		||||
          id: post.identifier
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      dispatch(upsertPosts(structureData))
 | 
			
		||||
 | 
			
		||||
      for (const content of structureData) {
 | 
			
		||||
        if (content.user && content.id) {
 | 
			
		||||
          const res = checkAndUpdatePost(content)
 | 
			
		||||
          if (res) {
 | 
			
		||||
            getBlogPost(content.user, content.id, content)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
    } finally {
 | 
			
		||||
      dispatch(setIsLoadingGlobal(false))
 | 
			
		||||
    }
 | 
			
		||||
  }, [posts, hashMapPosts])
 | 
			
		||||
 | 
			
		||||
  const getAvatar = async (user: string) => {
 | 
			
		||||
    try {
 | 
			
		||||
      let url = await qortalRequest({
 | 
			
		||||
        action: 'GET_QDN_RESOURCE_URL',
 | 
			
		||||
        name: user,
 | 
			
		||||
        service: 'THUMBNAIL',
 | 
			
		||||
        identifier: 'qortal_avatar'
 | 
			
		||||
      })
 | 
			
		||||
      dispatch(
 | 
			
		||||
        setUserAvatarHash({
 | 
			
		||||
          name: user,
 | 
			
		||||
          url
 | 
			
		||||
        })
 | 
			
		||||
      )
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
  }
 | 
			
		||||
  const getMailMessages = React.useCallback(
 | 
			
		||||
    async (recipientName: string, recipientAddress: string) => {
 | 
			
		||||
      try {
 | 
			
		||||
        const offset = mailMessages.length
 | 
			
		||||
 | 
			
		||||
        dispatch(setIsLoadingGlobal(true))
 | 
			
		||||
        const query = `qortal_qmail_${recipientName.slice(
 | 
			
		||||
          0,
 | 
			
		||||
          20
 | 
			
		||||
        )}_${recipientAddress.slice(-6)}_mail_`
 | 
			
		||||
        const url = `/arbitrary/resources/search?mode=ALL&service=${MAIL_SERVICE_TYPE}&query=${query}&limit=20&includemetadata=true&offset=${offset}&reverse=true&excludeblocked=true`
 | 
			
		||||
        const response = await fetch(url, {
 | 
			
		||||
          method: 'GET',
 | 
			
		||||
          headers: {
 | 
			
		||||
            'Content-Type': 'application/json'
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        const responseData = await response.json()
 | 
			
		||||
        const structureData = responseData.map((post: any): BlogPost => {
 | 
			
		||||
          return {
 | 
			
		||||
            title: post?.metadata?.title,
 | 
			
		||||
            category: post?.metadata?.category,
 | 
			
		||||
            categoryName: post?.metadata?.categoryName,
 | 
			
		||||
            tags: post?.metadata?.tags || [],
 | 
			
		||||
            description: post?.metadata?.description,
 | 
			
		||||
            createdAt: post?.created,
 | 
			
		||||
            updated: post?.updated,
 | 
			
		||||
            user: post.name,
 | 
			
		||||
            id: post.identifier
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        dispatch(upsertMessages(structureData))
 | 
			
		||||
 | 
			
		||||
        for (const content of structureData) {
 | 
			
		||||
          if (content.user && content.id) {
 | 
			
		||||
            getAvatar(content.user)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
      } finally {
 | 
			
		||||
        dispatch(setIsLoadingGlobal(false))
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [mailMessages, hashMapMailMessages]
 | 
			
		||||
  )
 | 
			
		||||
  const getBlogFilteredPosts = React.useCallback(
 | 
			
		||||
    async (filterValue: string) => {
 | 
			
		||||
      try {
 | 
			
		||||
        const offset = filteredPosts.length
 | 
			
		||||
 | 
			
		||||
        dispatch(setIsLoadingGlobal(true))
 | 
			
		||||
        const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_POST&query=q-blog-&limit=20&includemetadata=true&offset=${offset}&reverse=true&excludeblocked=true&name=${filterValue}`
 | 
			
		||||
        const response = await fetch(url, {
 | 
			
		||||
          method: 'GET',
 | 
			
		||||
          headers: {
 | 
			
		||||
            'Content-Type': 'application/json'
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        const responseData = await response.json()
 | 
			
		||||
        const structureData = responseData.map((post: any): BlogPost => {
 | 
			
		||||
          return {
 | 
			
		||||
            title: post?.metadata?.title,
 | 
			
		||||
            category: post?.metadata?.category,
 | 
			
		||||
            categoryName: post?.metadata?.categoryName,
 | 
			
		||||
            tags: post?.metadata?.tags || [],
 | 
			
		||||
            description: post?.metadata?.description,
 | 
			
		||||
            createdAt: post?.created,
 | 
			
		||||
            updated: post?.updated,
 | 
			
		||||
            user: post.name,
 | 
			
		||||
            postImage: '',
 | 
			
		||||
            id: post.identifier
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        dispatch(upsertFilteredPosts(structureData))
 | 
			
		||||
 | 
			
		||||
        for (const content of structureData) {
 | 
			
		||||
          if (content.user && content.id) {
 | 
			
		||||
            const res = checkAndUpdatePost(content)
 | 
			
		||||
            if (res) {
 | 
			
		||||
              getBlogPost(content.user, content.id, content)
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
      } finally {
 | 
			
		||||
        dispatch(setIsLoadingGlobal(false))
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [filteredPosts, hashMapPosts]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const getBlogPostsSubscriptions = React.useCallback(
 | 
			
		||||
    async (username: string) => {
 | 
			
		||||
      try {
 | 
			
		||||
        const offset = subscriptionPosts.length
 | 
			
		||||
        dispatch(setIsLoadingGlobal(true))
 | 
			
		||||
        const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_POST&query=q-blog-&limit=20&includemetadata=true&offset=${offset}&reverse=true&namefilter=q-blog-subscriptions-${username}&excludeblocked=true`
 | 
			
		||||
        const response = await fetch(url, {
 | 
			
		||||
          method: 'GET',
 | 
			
		||||
          headers: {
 | 
			
		||||
            'Content-Type': 'application/json'
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        const responseData = await response.json()
 | 
			
		||||
        const structureData = responseData.map((post: any): BlogPost => {
 | 
			
		||||
          return {
 | 
			
		||||
            title: post?.metadata?.title,
 | 
			
		||||
            category: post?.metadata?.category,
 | 
			
		||||
            categoryName: post?.metadata?.categoryName,
 | 
			
		||||
            tags: post?.metadata?.tags || [],
 | 
			
		||||
            description: post?.metadata?.description,
 | 
			
		||||
            createdAt: '',
 | 
			
		||||
            user: post.name,
 | 
			
		||||
            postImage: '',
 | 
			
		||||
            id: post.identifier
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        dispatch(upsertSubscriptionPosts(structureData))
 | 
			
		||||
 | 
			
		||||
        for (const content of structureData) {
 | 
			
		||||
          if (content.user && content.id) {
 | 
			
		||||
            const res = checkAndUpdatePost(content)
 | 
			
		||||
            if (res) {
 | 
			
		||||
              getBlogPost(content.user, content.id, content)
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
      } finally {
 | 
			
		||||
        dispatch(setIsLoadingGlobal(false))
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [subscriptionPosts, hashMapPosts, subscriptions]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const getBlogPostsFavorites = React.useCallback(async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const offset = favorites.length
 | 
			
		||||
      const favSlice = (favoritesLocal || []).slice(offset, 20)
 | 
			
		||||
      let favs = []
 | 
			
		||||
      for (const item of favSlice) {
 | 
			
		||||
        try {
 | 
			
		||||
          //   await qortalRequest({
 | 
			
		||||
          //     action: "SEARCH_QDN_RESOURCES",
 | 
			
		||||
          //     service: "THUMBNAIL",
 | 
			
		||||
          //     query: "search query goes here", // Optional - searches both "identifier" and "name" fields
 | 
			
		||||
          //     identifier: "search query goes here", // Optional - searches only the "identifier" field
 | 
			
		||||
          //     name: "search query goes here", // Optional - searches only the "name" field
 | 
			
		||||
          //     prefix: false, // Optional - if true, only the beginning of fields are matched in all of the above filters
 | 
			
		||||
          //     default: false, // Optional - if true, only resources without identifiers are returned
 | 
			
		||||
          //     includeStatus: false, // Optional - will take time to respond, so only request if necessary
 | 
			
		||||
          //     includeMetadata: false, // Optional - will take time to respond, so only request if necessary
 | 
			
		||||
          //     limit: 100,
 | 
			
		||||
          //     offset: 0,
 | 
			
		||||
          //     reverse: true
 | 
			
		||||
          // });
 | 
			
		||||
          //TODO - NAME SHOULD BE EXACT
 | 
			
		||||
          const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_POST&identifier=${item.id}&exactmatchnames=true&name=${item.user}&limit=20&includemetadata=true&reverse=true&excludeblocked=true`
 | 
			
		||||
          const response = await fetch(url, {
 | 
			
		||||
            method: 'GET',
 | 
			
		||||
            headers: {
 | 
			
		||||
              'Content-Type': 'application/json'
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
          const data = await response.json()
 | 
			
		||||
          //
 | 
			
		||||
          if (data.length > 0) {
 | 
			
		||||
            favs.push(data[0])
 | 
			
		||||
          }
 | 
			
		||||
        } catch (error) {}
 | 
			
		||||
      }
 | 
			
		||||
      const structureData = favs.map((post: any): BlogPost => {
 | 
			
		||||
        return {
 | 
			
		||||
          title: post?.metadata?.title,
 | 
			
		||||
          category: post?.metadata?.category,
 | 
			
		||||
          categoryName: post?.metadata?.categoryName,
 | 
			
		||||
          tags: post?.metadata?.tags || [],
 | 
			
		||||
          description: post?.metadata?.description,
 | 
			
		||||
          createdAt: '',
 | 
			
		||||
          user: post.name,
 | 
			
		||||
          postImage: '',
 | 
			
		||||
          id: post.identifier
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      dispatch(populateFavorites(structureData))
 | 
			
		||||
 | 
			
		||||
      for (const content of structureData) {
 | 
			
		||||
        if (content.user && content.id) {
 | 
			
		||||
          const res = checkAndUpdatePost(content)
 | 
			
		||||
          if (res) {
 | 
			
		||||
            getBlogPost(content.user, content.id, content)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
    } finally {
 | 
			
		||||
    }
 | 
			
		||||
  }, [hashMapPosts, favoritesLocal])
 | 
			
		||||
  return {
 | 
			
		||||
    getBlogPosts,
 | 
			
		||||
    getBlogPostsFavorites,
 | 
			
		||||
    getBlogPostsSubscriptions,
 | 
			
		||||
    checkAndUpdatePost,
 | 
			
		||||
    getBlogPost,
 | 
			
		||||
    hashMapPosts,
 | 
			
		||||
    checkNewMessages,
 | 
			
		||||
    getNewPosts,
 | 
			
		||||
    getBlogFilteredPosts,
 | 
			
		||||
    getMailMessages
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										362
									
								
								src/hooks/useFetchPosts.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,362 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux'
 | 
			
		||||
import {
 | 
			
		||||
  addPosts,
 | 
			
		||||
  addToHashMap,
 | 
			
		||||
  BlogPost,
 | 
			
		||||
  populateFavorites,
 | 
			
		||||
  setCountNewPosts,
 | 
			
		||||
  upsertFilteredPosts,
 | 
			
		||||
  upsertPosts,
 | 
			
		||||
  upsertPostsBeginning,
 | 
			
		||||
  upsertSubscriptionPosts
 | 
			
		||||
} from '../state/features/blogSlice'
 | 
			
		||||
import {
 | 
			
		||||
  setCurrentBlog,
 | 
			
		||||
  setIsLoadingGlobal
 | 
			
		||||
} from '../state/features/globalSlice'
 | 
			
		||||
import { RootState } from '../state/store'
 | 
			
		||||
import { fetchAndEvaluatePosts } from '../utils/fetchPosts'
 | 
			
		||||
 | 
			
		||||
export const useFetchPosts = () => {
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
  const hashMapPosts = useSelector(
 | 
			
		||||
    (state: RootState) => state.blog.hashMapPosts
 | 
			
		||||
  )
 | 
			
		||||
  const posts = useSelector((state: RootState) => state.blog.posts)
 | 
			
		||||
  const filteredPosts = useSelector(
 | 
			
		||||
    (state: RootState) => state.blog.filteredPosts
 | 
			
		||||
  )
 | 
			
		||||
  const favoritesLocal = useSelector(
 | 
			
		||||
    (state: RootState) => state.blog.favoritesLocal
 | 
			
		||||
  )
 | 
			
		||||
  const favorites = useSelector((state: RootState) => state.blog.favorites)
 | 
			
		||||
  const subscriptionPosts = useSelector(
 | 
			
		||||
    (state: RootState) => state.blog.subscriptionPosts
 | 
			
		||||
  )
 | 
			
		||||
  const subscriptions = useSelector(
 | 
			
		||||
    (state: RootState) => state.blog.subscriptions
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const checkAndUpdatePost = React.useCallback(
 | 
			
		||||
    (post: BlogPost) => {
 | 
			
		||||
      // Check if the post exists in hashMapPosts
 | 
			
		||||
      const existingPost = hashMapPosts[post.id]
 | 
			
		||||
      if (!existingPost) {
 | 
			
		||||
        // If the post doesn't exist, add it to hashMapPosts
 | 
			
		||||
        return true
 | 
			
		||||
      } else if (
 | 
			
		||||
        post?.updated &&
 | 
			
		||||
        existingPost?.updated &&
 | 
			
		||||
        (!existingPost?.updated || post?.updated) > existingPost?.updated
 | 
			
		||||
      ) {
 | 
			
		||||
        // If the post exists and its updated is more recent than the existing post's updated, update it in hashMapPosts
 | 
			
		||||
        return true
 | 
			
		||||
      } else {
 | 
			
		||||
        return false
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [hashMapPosts]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const getBlogPost = async (user: string, postId: string, content: any) => {
 | 
			
		||||
    const res = await fetchAndEvaluatePosts({
 | 
			
		||||
      user,
 | 
			
		||||
      postId,
 | 
			
		||||
      content
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    dispatch(addToHashMap(res))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const checkNewMessages = React.useCallback(async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_POST&query=q-blog-&limit=20&includemetadata=true&reverse=true&excludeblocked=true`
 | 
			
		||||
      const response = await fetch(url, {
 | 
			
		||||
        method: 'GET',
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'application/json'
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      const responseData = await response.json()
 | 
			
		||||
      const latestPost = posts[0]
 | 
			
		||||
      if (!latestPost) return
 | 
			
		||||
      const findPost = responseData?.findIndex(
 | 
			
		||||
        (item: any) => item?.identifier === latestPost?.id
 | 
			
		||||
      )
 | 
			
		||||
      if (findPost === -1) {
 | 
			
		||||
        dispatch(setCountNewPosts(responseData.length))
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      const newArray = responseData.slice(0, findPost)
 | 
			
		||||
      dispatch(setCountNewPosts(newArray.length))
 | 
			
		||||
      return
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
  }, [posts])
 | 
			
		||||
 | 
			
		||||
  const getNewPosts = React.useCallback(async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      dispatch(setIsLoadingGlobal(true))
 | 
			
		||||
      dispatch(setCountNewPosts(0))
 | 
			
		||||
      const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_POST&query=q-blog-&limit=20&includemetadata=true&reverse=true&excludeblocked=true`
 | 
			
		||||
      const response = await fetch(url, {
 | 
			
		||||
        method: 'GET',
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'application/json'
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      const responseData = await response.json()
 | 
			
		||||
      const latestPost = posts[0]
 | 
			
		||||
      if (!latestPost) return
 | 
			
		||||
      const findPost = responseData?.findIndex(
 | 
			
		||||
        (item: any) => item?.identifier === latestPost?.id
 | 
			
		||||
      )
 | 
			
		||||
      let fetchAll = responseData
 | 
			
		||||
      let willFetchAll = true
 | 
			
		||||
      if (findPost !== -1) {
 | 
			
		||||
        willFetchAll = false
 | 
			
		||||
        fetchAll = responseData.slice(0, findPost)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const structureData = fetchAll.map((post: any): BlogPost => {
 | 
			
		||||
        return {
 | 
			
		||||
          title: post?.metadata?.title,
 | 
			
		||||
          category: post?.metadata?.category,
 | 
			
		||||
          categoryName: post?.metadata?.categoryName,
 | 
			
		||||
          tags: post?.metadata?.tags || [],
 | 
			
		||||
          description: post?.metadata?.description,
 | 
			
		||||
          createdAt: post?.created,
 | 
			
		||||
          updated: post?.updated,
 | 
			
		||||
          user: post.name,
 | 
			
		||||
          postImage: '',
 | 
			
		||||
          id: post.identifier
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      if (!willFetchAll) {
 | 
			
		||||
        dispatch(upsertPostsBeginning(structureData))
 | 
			
		||||
      }
 | 
			
		||||
      if (willFetchAll) {
 | 
			
		||||
        dispatch(addPosts(structureData))
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      for (const content of structureData) {
 | 
			
		||||
        if (content.user && content.id) {
 | 
			
		||||
          const res = checkAndUpdatePost(content)
 | 
			
		||||
          if (res) {
 | 
			
		||||
            getBlogPost(content.user, content.id, content)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
    } finally {
 | 
			
		||||
      dispatch(setIsLoadingGlobal(false))
 | 
			
		||||
    }
 | 
			
		||||
  }, [posts, hashMapPosts])
 | 
			
		||||
 | 
			
		||||
  const getBlogPosts = React.useCallback(async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const offset = posts.length
 | 
			
		||||
 | 
			
		||||
      dispatch(setIsLoadingGlobal(true))
 | 
			
		||||
      const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_POST&query=q-blog-&limit=20&includemetadata=true&offset=${offset}&reverse=true&excludeblocked=true`
 | 
			
		||||
      const response = await fetch(url, {
 | 
			
		||||
        method: 'GET',
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'application/json'
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      const responseData = await response.json()
 | 
			
		||||
      const structureData = responseData.map((post: any): BlogPost => {
 | 
			
		||||
        return {
 | 
			
		||||
          title: post?.metadata?.title,
 | 
			
		||||
          category: post?.metadata?.category,
 | 
			
		||||
          categoryName: post?.metadata?.categoryName,
 | 
			
		||||
          tags: post?.metadata?.tags || [],
 | 
			
		||||
          description: post?.metadata?.description,
 | 
			
		||||
          createdAt: post?.created,
 | 
			
		||||
          updated: post?.updated,
 | 
			
		||||
          user: post.name,
 | 
			
		||||
          postImage: '',
 | 
			
		||||
          id: post.identifier
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      dispatch(upsertPosts(structureData))
 | 
			
		||||
 | 
			
		||||
      for (const content of structureData) {
 | 
			
		||||
        if (content.user && content.id) {
 | 
			
		||||
          const res = checkAndUpdatePost(content)
 | 
			
		||||
          if (res) {
 | 
			
		||||
            getBlogPost(content.user, content.id, content)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
    } finally {
 | 
			
		||||
      dispatch(setIsLoadingGlobal(false))
 | 
			
		||||
    }
 | 
			
		||||
  }, [posts, hashMapPosts])
 | 
			
		||||
  const getBlogFilteredPosts = React.useCallback(
 | 
			
		||||
    async (filterValue: string) => {
 | 
			
		||||
      try {
 | 
			
		||||
        const offset = filteredPosts.length
 | 
			
		||||
 | 
			
		||||
        dispatch(setIsLoadingGlobal(true))
 | 
			
		||||
        const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_POST&query=q-blog-&limit=20&includemetadata=true&offset=${offset}&reverse=true&excludeblocked=true&name=${filterValue}`
 | 
			
		||||
        const response = await fetch(url, {
 | 
			
		||||
          method: 'GET',
 | 
			
		||||
          headers: {
 | 
			
		||||
            'Content-Type': 'application/json'
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        const responseData = await response.json()
 | 
			
		||||
        const structureData = responseData.map((post: any): BlogPost => {
 | 
			
		||||
          return {
 | 
			
		||||
            title: post?.metadata?.title,
 | 
			
		||||
            category: post?.metadata?.category,
 | 
			
		||||
            categoryName: post?.metadata?.categoryName,
 | 
			
		||||
            tags: post?.metadata?.tags || [],
 | 
			
		||||
            description: post?.metadata?.description,
 | 
			
		||||
            createdAt: post?.created,
 | 
			
		||||
            updated: post?.updated,
 | 
			
		||||
            user: post.name,
 | 
			
		||||
            postImage: '',
 | 
			
		||||
            id: post.identifier
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        dispatch(upsertFilteredPosts(structureData))
 | 
			
		||||
 | 
			
		||||
        for (const content of structureData) {
 | 
			
		||||
          if (content.user && content.id) {
 | 
			
		||||
            const res = checkAndUpdatePost(content)
 | 
			
		||||
            if (res) {
 | 
			
		||||
              getBlogPost(content.user, content.id, content)
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
      } finally {
 | 
			
		||||
        dispatch(setIsLoadingGlobal(false))
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [filteredPosts, hashMapPosts]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const getBlogPostsSubscriptions = React.useCallback(
 | 
			
		||||
    async (username: string) => {
 | 
			
		||||
      try {
 | 
			
		||||
        const offset = subscriptionPosts.length
 | 
			
		||||
        dispatch(setIsLoadingGlobal(true))
 | 
			
		||||
        const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_POST&query=q-blog-&limit=20&includemetadata=true&offset=${offset}&reverse=true&namefilter=q-blog-subscriptions-${username}&excludeblocked=true`
 | 
			
		||||
        const response = await fetch(url, {
 | 
			
		||||
          method: 'GET',
 | 
			
		||||
          headers: {
 | 
			
		||||
            'Content-Type': 'application/json'
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        const responseData = await response.json()
 | 
			
		||||
        const structureData = responseData.map((post: any): BlogPost => {
 | 
			
		||||
          return {
 | 
			
		||||
            title: post?.metadata?.title,
 | 
			
		||||
            category: post?.metadata?.category,
 | 
			
		||||
            categoryName: post?.metadata?.categoryName,
 | 
			
		||||
            tags: post?.metadata?.tags || [],
 | 
			
		||||
            description: post?.metadata?.description,
 | 
			
		||||
            createdAt: '',
 | 
			
		||||
            user: post.name,
 | 
			
		||||
            postImage: '',
 | 
			
		||||
            id: post.identifier
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        dispatch(upsertSubscriptionPosts(structureData))
 | 
			
		||||
 | 
			
		||||
        for (const content of structureData) {
 | 
			
		||||
          if (content.user && content.id) {
 | 
			
		||||
            const res = checkAndUpdatePost(content)
 | 
			
		||||
            if (res) {
 | 
			
		||||
              getBlogPost(content.user, content.id, content)
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
      } finally {
 | 
			
		||||
        dispatch(setIsLoadingGlobal(false))
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [subscriptionPosts, hashMapPosts, subscriptions]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const getBlogPostsFavorites = React.useCallback(async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      const offset = favorites.length
 | 
			
		||||
      const favSlice = (favoritesLocal || []).slice(offset, 20)
 | 
			
		||||
      let favs = []
 | 
			
		||||
      for (const item of favSlice) {
 | 
			
		||||
        try {
 | 
			
		||||
          //   await qortalRequest({
 | 
			
		||||
          //     action: "SEARCH_QDN_RESOURCES",
 | 
			
		||||
          //     service: "THUMBNAIL",
 | 
			
		||||
          //     query: "search query goes here", // Optional - searches both "identifier" and "name" fields
 | 
			
		||||
          //     identifier: "search query goes here", // Optional - searches only the "identifier" field
 | 
			
		||||
          //     name: "search query goes here", // Optional - searches only the "name" field
 | 
			
		||||
          //     prefix: false, // Optional - if true, only the beginning of fields are matched in all of the above filters
 | 
			
		||||
          //     default: false, // Optional - if true, only resources without identifiers are returned
 | 
			
		||||
          //     includeStatus: false, // Optional - will take time to respond, so only request if necessary
 | 
			
		||||
          //     includeMetadata: false, // Optional - will take time to respond, so only request if necessary
 | 
			
		||||
          //     limit: 100,
 | 
			
		||||
          //     offset: 0,
 | 
			
		||||
          //     reverse: true
 | 
			
		||||
          // });
 | 
			
		||||
          //TODO - NAME SHOULD BE EXACT
 | 
			
		||||
          const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_POST&identifier=${item.id}&exactmatchnames=true&name=${item.user}&limit=20&includemetadata=true&reverse=true&excludeblocked=true`
 | 
			
		||||
          const response = await fetch(url, {
 | 
			
		||||
            method: 'GET',
 | 
			
		||||
            headers: {
 | 
			
		||||
              'Content-Type': 'application/json'
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
          const data = await response.json()
 | 
			
		||||
          //
 | 
			
		||||
          if (data.length > 0) {
 | 
			
		||||
            favs.push(data[0])
 | 
			
		||||
          }
 | 
			
		||||
        } catch (error) {}
 | 
			
		||||
      }
 | 
			
		||||
      const structureData = favs.map((post: any): BlogPost => {
 | 
			
		||||
        return {
 | 
			
		||||
          title: post?.metadata?.title,
 | 
			
		||||
          category: post?.metadata?.category,
 | 
			
		||||
          categoryName: post?.metadata?.categoryName,
 | 
			
		||||
          tags: post?.metadata?.tags || [],
 | 
			
		||||
          description: post?.metadata?.description,
 | 
			
		||||
          createdAt: '',
 | 
			
		||||
          user: post.name,
 | 
			
		||||
          postImage: '',
 | 
			
		||||
          id: post.identifier
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      dispatch(populateFavorites(structureData))
 | 
			
		||||
 | 
			
		||||
      for (const content of structureData) {
 | 
			
		||||
        if (content.user && content.id) {
 | 
			
		||||
          const res = checkAndUpdatePost(content)
 | 
			
		||||
          if (res) {
 | 
			
		||||
            getBlogPost(content.user, content.id, content)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
    } finally {
 | 
			
		||||
    }
 | 
			
		||||
  }, [hashMapPosts, favoritesLocal])
 | 
			
		||||
  return {
 | 
			
		||||
    getBlogPosts,
 | 
			
		||||
    getBlogPostsFavorites,
 | 
			
		||||
    getBlogPostsSubscriptions,
 | 
			
		||||
    checkAndUpdatePost,
 | 
			
		||||
    getBlogPost,
 | 
			
		||||
    hashMapPosts,
 | 
			
		||||
    checkNewMessages,
 | 
			
		||||
    getNewPosts,
 | 
			
		||||
    getBlogFilteredPosts
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										162
									
								
								src/index.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,162 @@
 | 
			
		||||
@font-face {
 | 
			
		||||
  font-family: 'CambonLight';
 | 
			
		||||
  src: url('./styles/fonts/Cambon-Light.ttf') format('truetype');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@font-face {
 | 
			
		||||
  font-family: 'Raleway';
 | 
			
		||||
  src: url('./styles/fonts/Raleway.ttf') format('truetype');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@font-face {
 | 
			
		||||
  font-family: 'Catamaran';
 | 
			
		||||
  src: url('./styles/fonts/Catamaran.ttf') format('truetype');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@font-face {
 | 
			
		||||
  font-family: 'Oxygen';
 | 
			
		||||
  src: url('./styles/fonts/Oxygen.ttf') format('truetype');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@font-face {
 | 
			
		||||
  font-family: 'Cairo';
 | 
			
		||||
  src: url('./styles/fonts/Cairo.ttf') format('truetype');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:root {
 | 
			
		||||
  padding: 0px;
 | 
			
		||||
  margin: 0px;
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.line-clamp {
 | 
			
		||||
  height: 100px;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  display: -webkit-box;
 | 
			
		||||
  -webkit-line-clamp: 5; /* number of lines to show */
 | 
			
		||||
  -webkit-box-orient: vertical;
 | 
			
		||||
  text-overflow: ellipsis;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.edit-btn:hover {
 | 
			
		||||
  opacity: 0.75;
 | 
			
		||||
  transition: 0.2s all;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.post-image {
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
  border-radius: 5px;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.grid-item {
 | 
			
		||||
  /* Other styles */
 | 
			
		||||
  /* overflow: auto; */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.grid-item-view {
 | 
			
		||||
  /* Other styles */
 | 
			
		||||
  /* overflow: auto; */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.test-grid {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: repeat(4, 1fr);
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  bottom: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  min-height: 25px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.test-grid-item {
 | 
			
		||||
  border: 1px solid powderblue;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body::-webkit-scrollbar-track {
 | 
			
		||||
  background-color: transparent;
 | 
			
		||||
}
 | 
			
		||||
body::-webkit-scrollbar-track:hover {
 | 
			
		||||
  background-color: transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body::-webkit-scrollbar {
 | 
			
		||||
  width: 16px;
 | 
			
		||||
  height: 10px;
 | 
			
		||||
  background-color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body::-webkit-scrollbar-thumb {
 | 
			
		||||
  background-color: #838eee;
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
  background-clip: content-box;
 | 
			
		||||
  border: 4px solid transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body::-webkit-scrollbar-thumb:hover {
 | 
			
		||||
  background-color: #6270f0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.MuiList-root::-webkit-scrollbar-track {
 | 
			
		||||
  background-color: transparent;
 | 
			
		||||
}
 | 
			
		||||
.MuiList-root::-webkit-scrollbar-track:hover {
 | 
			
		||||
  background-color: transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.MuiList-root::-webkit-scrollbar {
 | 
			
		||||
  width: 14px;
 | 
			
		||||
  height: 10px;
 | 
			
		||||
  background-color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.MuiList-root::-webkit-scrollbar-thumb {
 | 
			
		||||
  background-color: lightgray;
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
  background-clip: content-box;
 | 
			
		||||
  border: 4px solid transparent;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.MuiList-root::-webkit-scrollbar-thumb:hover {
 | 
			
		||||
  background-color: lightslategray;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.my-masonry-grid {
 | 
			
		||||
  display: -webkit-box; /* Not needed if autoprefixing */
 | 
			
		||||
  display: -ms-flexbox; /* Not needed if autoprefixing */
 | 
			
		||||
  display: flex;
 | 
			
		||||
  margin-left: -20px; /* gutter size offset */
 | 
			
		||||
  width: auto;
 | 
			
		||||
  padding: 15px 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.my-masonry-grid_column {
 | 
			
		||||
  padding-left: 20px; /* gutter size */
 | 
			
		||||
  background-clip: padding-box;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Style your items */
 | 
			
		||||
.my-masonry-grid_column > li {
 | 
			
		||||
  /* change div to reference your elements you put in <Masonry> */
 | 
			
		||||
  margin-bottom: 30px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.my-svg path {
 | 
			
		||||
  fill: red;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.qortal-link {
 | 
			
		||||
  text-decoration: none; /* Removes the underline */
 | 
			
		||||
  color: inherit; /* Inherits the color of the parent element */
 | 
			
		||||
}
 | 
			
		||||
.qortal-link:hover,
 | 
			
		||||
a:focus {
 | 
			
		||||
  text-decoration: underline; /* Adds underline on hover and focus for accessibility */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.glow {
 | 
			
		||||
  box-shadow: 0 0 10px #9ecaed, 0 0 20px #9ecaed, 0 0 30px #9ecaed,
 | 
			
		||||
    0 0 40px #9ecaed;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								src/index.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
			
		||||
declare module 'webworker:getBlogWorker' {
 | 
			
		||||
  const value: new () => Worker;
 | 
			
		||||
  export default value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare module 'webworker:decodeBase64' {
 | 
			
		||||
  const value: new () => Worker
 | 
			
		||||
  export default value
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								src/interfaces/interfaces.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,8 @@
 | 
			
		||||
export interface BlogContent {
 | 
			
		||||
  postContent: any[]
 | 
			
		||||
  title: string
 | 
			
		||||
  createdAt: number
 | 
			
		||||
  user?: any
 | 
			
		||||
  postId?: string
 | 
			
		||||
  layouts?: any
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								src/main.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,19 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import ReactDOM from 'react-dom/client'
 | 
			
		||||
import App from './App'
 | 
			
		||||
import './index.css'
 | 
			
		||||
import { HashRouter, BrowserRouter } from 'react-router-dom'
 | 
			
		||||
interface CustomWindow extends Window {
 | 
			
		||||
  _qdnBase: any // Replace 'any' with the appropriate type if you know it
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const customWindow = window as unknown as CustomWindow
 | 
			
		||||
 | 
			
		||||
// Now you can access the _qdnTheme property without TypeScript errors
 | 
			
		||||
const baseUrl = customWindow?._qdnBase || ''
 | 
			
		||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
 | 
			
		||||
  <BrowserRouter basename={baseUrl}>
 | 
			
		||||
    <App />
 | 
			
		||||
    <div id="modal-root" />
 | 
			
		||||
  </BrowserRouter>
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										951
									
								
								src/pages/BlogIndividualPost/BlogIndividualPost.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,951 @@
 | 
			
		||||
import React, { useMemo, useRef, useState } from 'react'
 | 
			
		||||
import { useParams } from 'react-router-dom'
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Box,
 | 
			
		||||
  Typography,
 | 
			
		||||
  CardHeader,
 | 
			
		||||
  Avatar,
 | 
			
		||||
  useTheme,
 | 
			
		||||
  Tooltip
 | 
			
		||||
} from '@mui/material'
 | 
			
		||||
import { useNavigate } from 'react-router-dom'
 | 
			
		||||
import { styled } from '@mui/system'
 | 
			
		||||
import AudiotrackIcon from '@mui/icons-material/Audiotrack'
 | 
			
		||||
import ReadOnlySlate from '../../components/editor/ReadOnlySlate'
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux'
 | 
			
		||||
import { RootState } from '../../state/store'
 | 
			
		||||
import { checkStructure } from '../../utils/checkStructure'
 | 
			
		||||
import { BlogContent } from '../../interfaces/interfaces'
 | 
			
		||||
import ShareIcon from '@mui/icons-material/Share'
 | 
			
		||||
import {
 | 
			
		||||
  setAudio,
 | 
			
		||||
  setCurrAudio,
 | 
			
		||||
  setIsLoadingGlobal,
 | 
			
		||||
  setVisitingBlog
 | 
			
		||||
} from '../../state/features/globalSlice'
 | 
			
		||||
import { VideoPlayer } from '../../components/common/VideoPlayer'
 | 
			
		||||
import { AudioPlayer, IPlaylist } from '../../components/common/AudioPlayer'
 | 
			
		||||
import { Responsive, WidthProvider } from 'react-grid-layout'
 | 
			
		||||
import '/node_modules/react-grid-layout/css/styles.css'
 | 
			
		||||
import '/node_modules/react-resizable/css/styles.css'
 | 
			
		||||
import DynamicHeightItem from '../../components/DynamicHeightItem'
 | 
			
		||||
import {
 | 
			
		||||
  addPrefix,
 | 
			
		||||
  buildIdentifierFromCreateTitleIdAndId,
 | 
			
		||||
  removePrefix
 | 
			
		||||
} from '../../utils/blogIdformats'
 | 
			
		||||
import { DynamicHeightItemMinimal } from '../../components/DynamicHeightItemMinimal'
 | 
			
		||||
import { ReusableModal } from '../../components/modals/ReusableModal'
 | 
			
		||||
import AudioElement from '../../components/AudioElement'
 | 
			
		||||
import ErrorBoundary from '../../components/common/ErrorBoundary'
 | 
			
		||||
import { CommentSection } from '../../components/common/Comments/CommentSection'
 | 
			
		||||
import { Tipping } from '../../components/common/Tipping/Tipping'
 | 
			
		||||
import FileElement from '../../components/FileElement'
 | 
			
		||||
import { CopyToClipboard } from 'react-copy-to-clipboard'
 | 
			
		||||
import { setNotification } from '../../state/features/notificationsSlice'
 | 
			
		||||
import ContextMenuResource from '../../components/common/ContextMenu/ContextMenuResource'
 | 
			
		||||
 | 
			
		||||
const ResponsiveGridLayout = WidthProvider(Responsive)
 | 
			
		||||
const initialMinHeight = 2 // Define an initial minimum height for grid items
 | 
			
		||||
 | 
			
		||||
const md = [
 | 
			
		||||
  { i: 'a', x: 0, y: 0, w: 4, h: initialMinHeight },
 | 
			
		||||
  { i: 'b', x: 6, y: 0, w: 4, h: initialMinHeight }
 | 
			
		||||
]
 | 
			
		||||
const sm = [
 | 
			
		||||
  { i: 'a', x: 0, y: 0, w: 6, h: initialMinHeight },
 | 
			
		||||
  { i: 'b', x: 6, y: 0, w: 6, h: initialMinHeight }
 | 
			
		||||
]
 | 
			
		||||
const xs = [
 | 
			
		||||
  { i: 'a', x: 0, y: 0, w: 6, h: initialMinHeight },
 | 
			
		||||
  { i: 'b', x: 6, y: 0, w: 6, h: initialMinHeight }
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
interface ILayoutGeneralSettings {
 | 
			
		||||
  padding: number
 | 
			
		||||
  blogPostType: string
 | 
			
		||||
}
 | 
			
		||||
export const BlogIndividualPost = () => {
 | 
			
		||||
  const { user, postId: postIdTemp, blog:blogTemp } = useParams()
 | 
			
		||||
 | 
			
		||||
  const blog = React.useMemo(()=> {
 | 
			
		||||
    if(postIdTemp && postIdTemp?.includes('-post-')){
 | 
			
		||||
      const str = postIdTemp
 | 
			
		||||
      const arr = str.split('-post-')
 | 
			
		||||
      const str1 = arr[0]
 | 
			
		||||
      const blogId = removePrefix(str1)
 | 
			
		||||
      return blogId
 | 
			
		||||
    } else {
 | 
			
		||||
      return blogTemp
 | 
			
		||||
    }
 | 
			
		||||
  }, [postIdTemp])
 | 
			
		||||
 | 
			
		||||
  const postId = React.useMemo(()=> {
 | 
			
		||||
    if(postIdTemp && postIdTemp?.includes('-post-')){
 | 
			
		||||
      const str = postIdTemp
 | 
			
		||||
      const arr = str.split('-post-')
 | 
			
		||||
      const str2 = arr[1]
 | 
			
		||||
      return str2
 | 
			
		||||
    } else {
 | 
			
		||||
      return postIdTemp
 | 
			
		||||
    }
 | 
			
		||||
  }, [postIdTemp])
 | 
			
		||||
  
 | 
			
		||||
  const blogFull = React.useMemo(() => {
 | 
			
		||||
    if (!blog) return ''
 | 
			
		||||
    return addPrefix(blog)
 | 
			
		||||
  }, [blog])
 | 
			
		||||
  const { user: userState } = useSelector((state: RootState) => state.auth)
 | 
			
		||||
  const { audios, audioPostId } = useSelector(
 | 
			
		||||
    (state: RootState) => state.global
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const [avatarUrl, setAvatarUrl] = React.useState<string>('')
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
  const theme = useTheme()
 | 
			
		||||
  // const [currAudio, setCurrAudio] = React.useState<number | null>(null)
 | 
			
		||||
  const [layouts, setLayouts] = React.useState<any>({ md, sm, xs })
 | 
			
		||||
  const [count, setCount] = React.useState<number>(1)
 | 
			
		||||
  const [layoutGeneralSettings, setLayoutGeneralSettings] =
 | 
			
		||||
    React.useState<ILayoutGeneralSettings | null>(null)
 | 
			
		||||
  const [currentBreakpoint, setCurrentBreakpoint] = React.useState<any>()
 | 
			
		||||
  const handleLayoutChange = (layout: any, layoutss: any) => {
 | 
			
		||||
    // const redoLayouts = setAutoHeight(layoutss)
 | 
			
		||||
    setLayouts(layoutss)
 | 
			
		||||
    // saveLayoutsToLocalStorage(layoutss)
 | 
			
		||||
  }
 | 
			
		||||
  const [blogContent, setBlogContent] = React.useState<BlogContent | null>(null)
 | 
			
		||||
  const [isOpenSwitchPlaylistModal, setisOpenSwitchPlaylistModal] =
 | 
			
		||||
    useState<boolean>(false)
 | 
			
		||||
  const tempSaveAudio = useRef<any>(null)
 | 
			
		||||
  const saveAudio = React.useRef<any>(null)
 | 
			
		||||
 | 
			
		||||
  const fullPostId = useMemo(() => {
 | 
			
		||||
    if (!blog || !postId) return ''
 | 
			
		||||
    dispatch(setIsLoadingGlobal(true))
 | 
			
		||||
    const formBlogId = addPrefix(blog)
 | 
			
		||||
    const formPostId = buildIdentifierFromCreateTitleIdAndId(formBlogId, postId)
 | 
			
		||||
    return formPostId
 | 
			
		||||
  }, [blog, postId])
 | 
			
		||||
  const getBlogPost = React.useCallback(async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      if (!blog || !postId) return
 | 
			
		||||
      dispatch(setIsLoadingGlobal(true))
 | 
			
		||||
      const formBlogId = addPrefix(blog)
 | 
			
		||||
      const formPostId = buildIdentifierFromCreateTitleIdAndId(
 | 
			
		||||
        formBlogId,
 | 
			
		||||
        postId
 | 
			
		||||
      )
 | 
			
		||||
      const url = `/arbitrary/BLOG_POST/${user}/${formPostId}`
 | 
			
		||||
      const response = await fetch(url, {
 | 
			
		||||
        method: 'GET',
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'application/json'
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      const responseData = await response.json()
 | 
			
		||||
 | 
			
		||||
      if (checkStructure(responseData)) {
 | 
			
		||||
        setBlogContent(responseData)
 | 
			
		||||
        if (responseData?.layouts) {
 | 
			
		||||
          setLayouts(responseData?.layouts)
 | 
			
		||||
        }
 | 
			
		||||
        if (responseData?.layoutGeneralSettings) {
 | 
			
		||||
          setLayoutGeneralSettings(responseData.layoutGeneralSettings)
 | 
			
		||||
        }
 | 
			
		||||
        const filteredAudios = (responseData?.postContent || []).filter(
 | 
			
		||||
          (content: any) => content?.type === 'audio'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        const transformAudios = filteredAudios?.map((fa: any) => {
 | 
			
		||||
          return {
 | 
			
		||||
            ...(fa?.content || {}),
 | 
			
		||||
            id: fa?.id
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        if (!audios && transformAudios.length > 0) {
 | 
			
		||||
          saveAudio.current = { audios: transformAudios, postId: formPostId }
 | 
			
		||||
          dispatch(setAudio({ audios: transformAudios, postId: formPostId }))
 | 
			
		||||
        } else if (
 | 
			
		||||
          formPostId === audioPostId &&
 | 
			
		||||
          audios?.length !== transformAudios.length
 | 
			
		||||
        ) {
 | 
			
		||||
          tempSaveAudio.current = {
 | 
			
		||||
            message:
 | 
			
		||||
              "This post's audio playlist has updated. Would you like to switch?"
 | 
			
		||||
          }
 | 
			
		||||
          setisOpenSwitchPlaylistModal(true)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
    } finally {
 | 
			
		||||
      dispatch(setIsLoadingGlobal(false))
 | 
			
		||||
    }
 | 
			
		||||
  }, [user, postId, blog])
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    getBlogPost()
 | 
			
		||||
  }, [postId])
 | 
			
		||||
 | 
			
		||||
  const switchPlayList = () => {
 | 
			
		||||
    const filteredAudios = (blogContent?.postContent || []).filter(
 | 
			
		||||
      (content) => content?.type === 'audio'
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    const formatAudios = filteredAudios.map((fa) => {
 | 
			
		||||
      return {
 | 
			
		||||
        ...(fa?.content || {}),
 | 
			
		||||
        id: fa?.id
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    if (!blog || !postId) return
 | 
			
		||||
    const formBlogId = addPrefix(blog)
 | 
			
		||||
    const formPostId = buildIdentifierFromCreateTitleIdAndId(formBlogId, postId)
 | 
			
		||||
    dispatch(setAudio({ audios: formatAudios, postId: formPostId }))
 | 
			
		||||
    if (tempSaveAudio?.current?.currentSelection) {
 | 
			
		||||
      const findIndex = (formatAudios || []).findIndex(
 | 
			
		||||
        (item) =>
 | 
			
		||||
          item?.identifier ===
 | 
			
		||||
          tempSaveAudio?.current?.currentSelection?.content?.identifier
 | 
			
		||||
      )
 | 
			
		||||
      if (findIndex >= 0) {
 | 
			
		||||
        dispatch(setCurrAudio(findIndex))
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    setisOpenSwitchPlaylistModal(false)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getAvatar = React.useCallback(async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      let url = await qortalRequest({
 | 
			
		||||
        action: 'GET_QDN_RESOURCE_URL',
 | 
			
		||||
        name: user,
 | 
			
		||||
        service: 'THUMBNAIL',
 | 
			
		||||
        identifier: 'qortal_avatar'
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      setAvatarUrl(url)
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
  }, [user])
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    getAvatar()
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  const onBreakpointChange = React.useCallback((newBreakpoint: any) => {
 | 
			
		||||
    setCurrentBreakpoint(newBreakpoint)
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  const onResizeStop = React.useCallback((layout: any, layoutItem: any) => {
 | 
			
		||||
    // Update the layout state with the new position and size of the component
 | 
			
		||||
    setCount((prev) => prev + 1)
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  // const audios = React.useMemo<IPlaylist[]>(() => {
 | 
			
		||||
  //   const filteredAudios = (blogContent?.postContent || []).filter(
 | 
			
		||||
  //     (content) => content.type === 'audio'
 | 
			
		||||
  //   )
 | 
			
		||||
 | 
			
		||||
  //   return filteredAudios.map((fa) => {
 | 
			
		||||
  //     return {
 | 
			
		||||
  //       ...fa.content,
 | 
			
		||||
  //       id: fa.id
 | 
			
		||||
  //     }
 | 
			
		||||
  //   })
 | 
			
		||||
  // }, [blogContent])
 | 
			
		||||
 | 
			
		||||
  const handleResize = () => {
 | 
			
		||||
    setCount((prev) => prev + 1)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    window.addEventListener('resize', handleResize)
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      window.removeEventListener('resize', handleResize)
 | 
			
		||||
    }
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  const handleCount = React.useCallback(() => {
 | 
			
		||||
    // Update the layout state with the new position and size of the component
 | 
			
		||||
    setCount((prev) => prev + 1)
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  const getBlog = React.useCallback(async () => {
 | 
			
		||||
    let name = user
 | 
			
		||||
    if (!name) return
 | 
			
		||||
    if (!blogFull) return
 | 
			
		||||
    try {
 | 
			
		||||
      const urlBlog = `/arbitrary/BLOG/${name}/${blogFull}`
 | 
			
		||||
      const response = await fetch(urlBlog, {
 | 
			
		||||
        method: 'GET',
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'application/json'
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      const responseData = await response.json()
 | 
			
		||||
      dispatch(setVisitingBlog({ ...responseData, name }))
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
  }, [user, blogFull])
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    getBlog()
 | 
			
		||||
  }, [user, blogFull])
 | 
			
		||||
 | 
			
		||||
  if (!blogContent) return null
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
      sx={{
 | 
			
		||||
        display: 'flex',
 | 
			
		||||
        alignItems: 'center',
 | 
			
		||||
        flexDirection: 'column'
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Box
 | 
			
		||||
        sx={{
 | 
			
		||||
          maxWidth: '1400px',
 | 
			
		||||
          // margin: '15px',
 | 
			
		||||
          width: '95%',
 | 
			
		||||
          paddingBottom: '50px'
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {user === userState?.name && (
 | 
			
		||||
          <Button
 | 
			
		||||
            sx={{ backgroundColor: theme.palette.secondary.main }}
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              navigate(`/${user}/${blog}/${postId}/edit`)
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            Edit Post
 | 
			
		||||
          </Button>
 | 
			
		||||
        )}
 | 
			
		||||
        <Box
 | 
			
		||||
          sx={{
 | 
			
		||||
            display: 'flex',
 | 
			
		||||
            alignItems: 'center',
 | 
			
		||||
            gap: 1
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <CardHeader
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              navigate(`/${user}/${blog}`)
 | 
			
		||||
            }}
 | 
			
		||||
            sx={{
 | 
			
		||||
              cursor: 'pointer',
 | 
			
		||||
              '& .MuiCardHeader-content': {
 | 
			
		||||
                overflow: 'hidden'
 | 
			
		||||
              },
 | 
			
		||||
              padding: '10px 0px'
 | 
			
		||||
            }}
 | 
			
		||||
            avatar={<Avatar src={avatarUrl} alt={`${user}'s avatar`} />}
 | 
			
		||||
            subheader={
 | 
			
		||||
              <Typography
 | 
			
		||||
                sx={{ fontFamily: 'Cairo', fontSize: '25px' }}
 | 
			
		||||
                color={theme.palette.text.primary}
 | 
			
		||||
              >{` ${user}`}</Typography>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          {user && (
 | 
			
		||||
            <Tipping
 | 
			
		||||
              name={user || ''}
 | 
			
		||||
              onSubmit={() => {
 | 
			
		||||
                // setNameTip('')
 | 
			
		||||
              }}
 | 
			
		||||
              onClose={() => {
 | 
			
		||||
                // setNameTip('')
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
        </Box>
 | 
			
		||||
        <Box
 | 
			
		||||
          sx={{
 | 
			
		||||
            display: 'flex',
 | 
			
		||||
            gap: 1,
 | 
			
		||||
            alignItems: 'center',
 | 
			
		||||
            justifyContent: 'center'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Typography
 | 
			
		||||
            variant="h1"
 | 
			
		||||
            color="textPrimary"
 | 
			
		||||
            sx={{
 | 
			
		||||
              textAlign: 'center'
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            {blogContent?.title}
 | 
			
		||||
          </Typography>
 | 
			
		||||
          <Tooltip title={`Copy post link`} arrow>
 | 
			
		||||
            <Box
 | 
			
		||||
              sx={{
 | 
			
		||||
                cursor: 'pointer'
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <CopyToClipboard
 | 
			
		||||
                text={`qortal://APP/Q-Blog/${user}/${blog}/${postId}`}
 | 
			
		||||
                onCopy={() => {
 | 
			
		||||
                  dispatch(
 | 
			
		||||
                    setNotification({
 | 
			
		||||
                      msg: 'Copied to clipboard!',
 | 
			
		||||
                      alertType: 'success'
 | 
			
		||||
                    })
 | 
			
		||||
                  )
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <ShareIcon />
 | 
			
		||||
              </CopyToClipboard>
 | 
			
		||||
            </Box>
 | 
			
		||||
          </Tooltip>
 | 
			
		||||
          <CommentSection postId={fullPostId} postName={user || ''} />
 | 
			
		||||
        </Box>
 | 
			
		||||
 | 
			
		||||
        {(layoutGeneralSettings?.blogPostType === 'builder' ||
 | 
			
		||||
          !layoutGeneralSettings?.blogPostType) && (
 | 
			
		||||
          <Content
 | 
			
		||||
            layouts={layouts}
 | 
			
		||||
            blogContent={blogContent}
 | 
			
		||||
            onResizeStop={onResizeStop}
 | 
			
		||||
            onBreakpointChange={onBreakpointChange}
 | 
			
		||||
            handleLayoutChange={handleLayoutChange}
 | 
			
		||||
          >
 | 
			
		||||
            {blogContent?.postContent?.map((section: any) => {
 | 
			
		||||
              if (section?.type === 'editor') {
 | 
			
		||||
                return (
 | 
			
		||||
                  <div key={section?.id} className="grid-item-view">
 | 
			
		||||
                    <ErrorBoundary
 | 
			
		||||
                      fallback={
 | 
			
		||||
                        <Typography>
 | 
			
		||||
                          Error loading content: Invalid Data
 | 
			
		||||
                        </Typography>
 | 
			
		||||
                      }
 | 
			
		||||
                    >
 | 
			
		||||
                      <DynamicHeightItem
 | 
			
		||||
                        layouts={layouts}
 | 
			
		||||
                        setLayouts={setLayouts}
 | 
			
		||||
                        i={section.id}
 | 
			
		||||
                        breakpoint={currentBreakpoint}
 | 
			
		||||
                        count={count}
 | 
			
		||||
                        padding={layoutGeneralSettings?.padding}
 | 
			
		||||
                      >
 | 
			
		||||
                        <ReadOnlySlate content={section.content} />
 | 
			
		||||
                      </DynamicHeightItem>
 | 
			
		||||
                    </ErrorBoundary>
 | 
			
		||||
                  </div>
 | 
			
		||||
                )
 | 
			
		||||
              }
 | 
			
		||||
              if (section?.type === 'image') {
 | 
			
		||||
                return (
 | 
			
		||||
                  <div key={section?.id} className="grid-item-view">
 | 
			
		||||
                    <ErrorBoundary
 | 
			
		||||
                      fallback={
 | 
			
		||||
                        <Typography>
 | 
			
		||||
                          Error loading content: Invalid Data
 | 
			
		||||
                        </Typography>
 | 
			
		||||
                      }
 | 
			
		||||
                    >
 | 
			
		||||
                      <DynamicHeightItem
 | 
			
		||||
                        layouts={layouts}
 | 
			
		||||
                        setLayouts={setLayouts}
 | 
			
		||||
                        i={section.id}
 | 
			
		||||
                        breakpoint={currentBreakpoint}
 | 
			
		||||
                        count={count}
 | 
			
		||||
                        padding={layoutGeneralSettings?.padding}
 | 
			
		||||
                      >
 | 
			
		||||
                        <img
 | 
			
		||||
                          src={section.content.image}
 | 
			
		||||
                          className="post-image"
 | 
			
		||||
                        />
 | 
			
		||||
                      </DynamicHeightItem>
 | 
			
		||||
                    </ErrorBoundary>
 | 
			
		||||
                  </div>
 | 
			
		||||
                )
 | 
			
		||||
              }
 | 
			
		||||
              if (section?.type === 'video') {
 | 
			
		||||
                return (
 | 
			
		||||
                  <div key={section?.id} className="grid-item-view">
 | 
			
		||||
                    <ErrorBoundary
 | 
			
		||||
                      fallback={
 | 
			
		||||
                        <Typography>
 | 
			
		||||
                          Error loading content: Invalid Data
 | 
			
		||||
                        </Typography>
 | 
			
		||||
                      }
 | 
			
		||||
                    >
 | 
			
		||||
                      <DynamicHeightItem
 | 
			
		||||
                        layouts={layouts}
 | 
			
		||||
                        setLayouts={setLayouts}
 | 
			
		||||
                        i={section.id}
 | 
			
		||||
                        breakpoint={currentBreakpoint}
 | 
			
		||||
                        count={count}
 | 
			
		||||
                        padding={layoutGeneralSettings?.padding}
 | 
			
		||||
                      >
 | 
			
		||||
                        <ContextMenuResource
 | 
			
		||||
                          name={section.content.name}
 | 
			
		||||
                          service={section.content.service}
 | 
			
		||||
                          identifier={section.content.identifier}
 | 
			
		||||
                          link={`qortal://${section?.content?.service}/${section?.content?.name}/${section?.content?.identifier}`}
 | 
			
		||||
                        >
 | 
			
		||||
                          <VideoPlayer
 | 
			
		||||
                            name={section.content.name}
 | 
			
		||||
                            service={section.content.service}
 | 
			
		||||
                            identifier={section.content.identifier}
 | 
			
		||||
                            setCount={handleCount}
 | 
			
		||||
                            user={user}
 | 
			
		||||
                            postId={fullPostId}
 | 
			
		||||
                          />
 | 
			
		||||
                        </ContextMenuResource>
 | 
			
		||||
                      </DynamicHeightItem>
 | 
			
		||||
                    </ErrorBoundary>
 | 
			
		||||
                  </div>
 | 
			
		||||
                )
 | 
			
		||||
              }
 | 
			
		||||
              if (section?.type === 'audio') {
 | 
			
		||||
                return (
 | 
			
		||||
                  <div key={section?.id} className="grid-item-view">
 | 
			
		||||
                    <ErrorBoundary
 | 
			
		||||
                      fallback={
 | 
			
		||||
                        <Typography>
 | 
			
		||||
                          Error loading content: Invalid Data
 | 
			
		||||
                        </Typography>
 | 
			
		||||
                      }
 | 
			
		||||
                    >
 | 
			
		||||
                      <DynamicHeightItem
 | 
			
		||||
                        layouts={layouts}
 | 
			
		||||
                        setLayouts={setLayouts}
 | 
			
		||||
                        i={section.id}
 | 
			
		||||
                        breakpoint={currentBreakpoint}
 | 
			
		||||
                        count={count}
 | 
			
		||||
                        padding={layoutGeneralSettings?.padding}
 | 
			
		||||
                      >
 | 
			
		||||
                        <ContextMenuResource
 | 
			
		||||
                          name={section.content.name}
 | 
			
		||||
                          service={section.content.service}
 | 
			
		||||
                          identifier={section.content.identifier}
 | 
			
		||||
                          link={`qortal://${section?.content?.service}/${section?.content?.name}/${section?.content?.identifier}`}
 | 
			
		||||
                        >
 | 
			
		||||
                          <AudioElement
 | 
			
		||||
                            key={section.id}
 | 
			
		||||
                            audioInfo={section.content}
 | 
			
		||||
                            postId={fullPostId}
 | 
			
		||||
                            user={user ? user : ''}
 | 
			
		||||
                            onClick={() => {
 | 
			
		||||
                              if (!blog || !postId) return
 | 
			
		||||
 | 
			
		||||
                              const formBlogId = addPrefix(blog)
 | 
			
		||||
                              const formPostId =
 | 
			
		||||
                                buildIdentifierFromCreateTitleIdAndId(
 | 
			
		||||
                                  formBlogId,
 | 
			
		||||
                                  postId
 | 
			
		||||
                                )
 | 
			
		||||
                              if (audioPostId && formPostId !== audioPostId) {
 | 
			
		||||
                                tempSaveAudio.current = {
 | 
			
		||||
                                  ...(tempSaveAudio.current || {}),
 | 
			
		||||
                                  currentSelection: section,
 | 
			
		||||
                                  message:
 | 
			
		||||
                                    'You are current on a playlist. Would you like to switch?'
 | 
			
		||||
                                }
 | 
			
		||||
                                setisOpenSwitchPlaylistModal(true)
 | 
			
		||||
                              } else {
 | 
			
		||||
                                if (!audios && saveAudio?.current) {
 | 
			
		||||
                                  const findIndex = (
 | 
			
		||||
                                    saveAudio?.current?.audios || []
 | 
			
		||||
                                  ).findIndex(
 | 
			
		||||
                                    (item: any) =>
 | 
			
		||||
                                      item.identifier ===
 | 
			
		||||
                                      section.content.identifier
 | 
			
		||||
                                  )
 | 
			
		||||
                                  dispatch(setAudio(saveAudio?.current))
 | 
			
		||||
                                  dispatch(setCurrAudio(findIndex))
 | 
			
		||||
                                  return
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                                const findIndex = (audios || []).findIndex(
 | 
			
		||||
                                  (item) =>
 | 
			
		||||
                                    item.identifier ===
 | 
			
		||||
                                    section.content.identifier
 | 
			
		||||
                                )
 | 
			
		||||
                                if (findIndex >= 0) {
 | 
			
		||||
                                  dispatch(setCurrAudio(findIndex))
 | 
			
		||||
                                }
 | 
			
		||||
                              }
 | 
			
		||||
                            }}
 | 
			
		||||
                            title={section.content?.title}
 | 
			
		||||
                            description={section.content?.description}
 | 
			
		||||
                            author=""
 | 
			
		||||
                          />
 | 
			
		||||
                        </ContextMenuResource>
 | 
			
		||||
                      </DynamicHeightItem>
 | 
			
		||||
                    </ErrorBoundary>
 | 
			
		||||
                  </div>
 | 
			
		||||
                )
 | 
			
		||||
              }
 | 
			
		||||
              if (section?.type === 'file') {
 | 
			
		||||
                return (
 | 
			
		||||
                  <div key={section?.id} className="grid-item">
 | 
			
		||||
                    <ErrorBoundary
 | 
			
		||||
                      fallback={
 | 
			
		||||
                        <Typography>
 | 
			
		||||
                          Error loading content: Invalid Data
 | 
			
		||||
                        </Typography>
 | 
			
		||||
                      }
 | 
			
		||||
                    >
 | 
			
		||||
                      <DynamicHeightItemMinimal
 | 
			
		||||
                        layouts={layouts}
 | 
			
		||||
                        setLayouts={setLayouts}
 | 
			
		||||
                        i={section.id}
 | 
			
		||||
                        breakpoint={currentBreakpoint}
 | 
			
		||||
                        count={count}
 | 
			
		||||
                        padding={0}
 | 
			
		||||
                      >
 | 
			
		||||
                        <ContextMenuResource
 | 
			
		||||
                          name={section.content.name}
 | 
			
		||||
                          service={section.content.service}
 | 
			
		||||
                          identifier={section.content.identifier}
 | 
			
		||||
                          link={`qortal://${section?.content?.service}/${section?.content?.name}/${section?.content?.identifier}`}
 | 
			
		||||
                        >
 | 
			
		||||
                          <FileElement
 | 
			
		||||
                            key={section.id}
 | 
			
		||||
                            fileInfo={section.content}
 | 
			
		||||
                            postId={fullPostId}
 | 
			
		||||
                            user={user ? user : ''}
 | 
			
		||||
                            title={section.content?.title}
 | 
			
		||||
                            description={section.content?.description}
 | 
			
		||||
                            mimeType={section.content?.mimeType}
 | 
			
		||||
                            author=""
 | 
			
		||||
                          />
 | 
			
		||||
                        </ContextMenuResource>
 | 
			
		||||
                      </DynamicHeightItemMinimal>
 | 
			
		||||
                    </ErrorBoundary>
 | 
			
		||||
                  </div>
 | 
			
		||||
                )
 | 
			
		||||
              }
 | 
			
		||||
            })}
 | 
			
		||||
          </Content>
 | 
			
		||||
        )}
 | 
			
		||||
        {layoutGeneralSettings?.blogPostType === 'minimal' && (
 | 
			
		||||
          <>
 | 
			
		||||
            {layouts?.rows?.map((row: any, rowIndex: number) => {
 | 
			
		||||
              return (
 | 
			
		||||
                <Box
 | 
			
		||||
                  sx={{
 | 
			
		||||
                    display: 'flex',
 | 
			
		||||
                    width: '100%',
 | 
			
		||||
                    flexDirection: 'row',
 | 
			
		||||
                    alignItems: 'center',
 | 
			
		||||
                    justifyContent: 'center',
 | 
			
		||||
                    marginTop: '25px',
 | 
			
		||||
                    gap: 2
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  {row?.ids?.map((elementId: string) => {
 | 
			
		||||
                    const section: any = blogContent?.postContent?.find(
 | 
			
		||||
                      (el) => el?.id === elementId
 | 
			
		||||
                    )
 | 
			
		||||
                    if (!section) return null
 | 
			
		||||
                    if (section?.type === 'editor') {
 | 
			
		||||
                      return (
 | 
			
		||||
                        <div
 | 
			
		||||
                          key={section?.id}
 | 
			
		||||
                          className="grid-item"
 | 
			
		||||
                          style={{
 | 
			
		||||
                            maxWidth: '800px',
 | 
			
		||||
                            display: 'flex',
 | 
			
		||||
                            flexDirection: 'column',
 | 
			
		||||
                            width: '100%'
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          <ErrorBoundary
 | 
			
		||||
                            fallback={
 | 
			
		||||
                              <Typography>
 | 
			
		||||
                                Error loading content: Invalid Data
 | 
			
		||||
                              </Typography>
 | 
			
		||||
                            }
 | 
			
		||||
                          >
 | 
			
		||||
                            <DynamicHeightItemMinimal
 | 
			
		||||
                              layouts={layouts}
 | 
			
		||||
                              setLayouts={setLayouts}
 | 
			
		||||
                              i={section.id}
 | 
			
		||||
                              breakpoint={currentBreakpoint}
 | 
			
		||||
                              count={count}
 | 
			
		||||
                              padding={0}
 | 
			
		||||
                            >
 | 
			
		||||
                              <ReadOnlySlate
 | 
			
		||||
                                key={section.id}
 | 
			
		||||
                                content={section.content}
 | 
			
		||||
                              />
 | 
			
		||||
                            </DynamicHeightItemMinimal>
 | 
			
		||||
                          </ErrorBoundary>
 | 
			
		||||
                        </div>
 | 
			
		||||
                      )
 | 
			
		||||
                    }
 | 
			
		||||
                    if (section?.type === 'image') {
 | 
			
		||||
                      return (
 | 
			
		||||
                        <div key={section.id} className="grid-item">
 | 
			
		||||
                          <ErrorBoundary
 | 
			
		||||
                            fallback={
 | 
			
		||||
                              <Typography>
 | 
			
		||||
                                Error loading content: Invalid Data
 | 
			
		||||
                              </Typography>
 | 
			
		||||
                            }
 | 
			
		||||
                          >
 | 
			
		||||
                            <DynamicHeightItemMinimal
 | 
			
		||||
                              layouts={layouts}
 | 
			
		||||
                              setLayouts={setLayouts}
 | 
			
		||||
                              i={section.id}
 | 
			
		||||
                              breakpoint={currentBreakpoint}
 | 
			
		||||
                              count={count}
 | 
			
		||||
                              type="image"
 | 
			
		||||
                              padding={0}
 | 
			
		||||
                            >
 | 
			
		||||
                              <Box
 | 
			
		||||
                                sx={{
 | 
			
		||||
                                  position: 'relative',
 | 
			
		||||
                                  width: '100%',
 | 
			
		||||
                                  height: '100%'
 | 
			
		||||
                                }}
 | 
			
		||||
                              >
 | 
			
		||||
                                <img
 | 
			
		||||
                                  src={section.content.image}
 | 
			
		||||
                                  className="post-image"
 | 
			
		||||
                                  style={{
 | 
			
		||||
                                    objectFit: 'contain',
 | 
			
		||||
                                    maxHeight: '50vh'
 | 
			
		||||
                                  }}
 | 
			
		||||
                                />
 | 
			
		||||
                              </Box>
 | 
			
		||||
                            </DynamicHeightItemMinimal>
 | 
			
		||||
                          </ErrorBoundary>
 | 
			
		||||
                        </div>
 | 
			
		||||
                      )
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (section?.type === 'video') {
 | 
			
		||||
                      return (
 | 
			
		||||
                        <div key={section?.id} className="grid-item">
 | 
			
		||||
                          <ErrorBoundary
 | 
			
		||||
                            fallback={
 | 
			
		||||
                              <Typography>
 | 
			
		||||
                                Error loading content: Invalid Data
 | 
			
		||||
                              </Typography>
 | 
			
		||||
                            }
 | 
			
		||||
                          >
 | 
			
		||||
                            <DynamicHeightItemMinimal
 | 
			
		||||
                              layouts={layouts}
 | 
			
		||||
                              setLayouts={setLayouts}
 | 
			
		||||
                              i={section.id}
 | 
			
		||||
                              breakpoint={currentBreakpoint}
 | 
			
		||||
                              count={count}
 | 
			
		||||
                              padding={0}
 | 
			
		||||
                            >
 | 
			
		||||
                              <ContextMenuResource
 | 
			
		||||
                                name={section.content.name}
 | 
			
		||||
                                service={section.content.service}
 | 
			
		||||
                                identifier={section.content.identifier}
 | 
			
		||||
                                link={`qortal://${section?.content?.service}/${section?.content?.name}/${section?.content?.identifier}`}
 | 
			
		||||
                              >
 | 
			
		||||
                                <Box
 | 
			
		||||
                                  sx={{
 | 
			
		||||
                                    position: 'relative',
 | 
			
		||||
                                    width: '100%',
 | 
			
		||||
                                    height: '100%'
 | 
			
		||||
                                  }}
 | 
			
		||||
                                >
 | 
			
		||||
                                  <VideoPlayer
 | 
			
		||||
                                    name={section.content.name}
 | 
			
		||||
                                    service={section.content.service}
 | 
			
		||||
                                    identifier={section.content.identifier}
 | 
			
		||||
                                    customStyle={{
 | 
			
		||||
                                      height: '50vh'
 | 
			
		||||
                                    }}
 | 
			
		||||
                                    user={user}
 | 
			
		||||
                                    postId={fullPostId}
 | 
			
		||||
                                  />
 | 
			
		||||
                                </Box>
 | 
			
		||||
                              </ContextMenuResource>
 | 
			
		||||
                            </DynamicHeightItemMinimal>
 | 
			
		||||
                          </ErrorBoundary>
 | 
			
		||||
                        </div>
 | 
			
		||||
                      )
 | 
			
		||||
                    }
 | 
			
		||||
                    if (section?.type === 'audio') {
 | 
			
		||||
                      return (
 | 
			
		||||
                        <div key={section?.id} className="grid-item">
 | 
			
		||||
                          <ErrorBoundary
 | 
			
		||||
                            fallback={
 | 
			
		||||
                              <Typography>
 | 
			
		||||
                                Error loading content: Invalid Data
 | 
			
		||||
                              </Typography>
 | 
			
		||||
                            }
 | 
			
		||||
                          >
 | 
			
		||||
                            <DynamicHeightItemMinimal
 | 
			
		||||
                              layouts={layouts}
 | 
			
		||||
                              setLayouts={setLayouts}
 | 
			
		||||
                              i={section.id}
 | 
			
		||||
                              breakpoint={currentBreakpoint}
 | 
			
		||||
                              count={count}
 | 
			
		||||
                              padding={0}
 | 
			
		||||
                            >
 | 
			
		||||
                              <ContextMenuResource
 | 
			
		||||
                                name={section.content.name}
 | 
			
		||||
                                service={section.content.service}
 | 
			
		||||
                                identifier={section.content.identifier}
 | 
			
		||||
                                link={`qortal://${section?.content?.service}/${section?.content?.name}/${section?.content?.identifier}`}
 | 
			
		||||
                              >
 | 
			
		||||
                                <AudioElement
 | 
			
		||||
                                  key={section.id}
 | 
			
		||||
                                  audioInfo={section.content}
 | 
			
		||||
                                  postId={fullPostId}
 | 
			
		||||
                                  user={user ? user : ''}
 | 
			
		||||
                                  onClick={() => {
 | 
			
		||||
                                    if (!blog || !postId) return
 | 
			
		||||
                                    const formBlogId = addPrefix(blog)
 | 
			
		||||
                                    const formPostId =
 | 
			
		||||
                                      buildIdentifierFromCreateTitleIdAndId(
 | 
			
		||||
                                        formBlogId,
 | 
			
		||||
                                        postId
 | 
			
		||||
                                      )
 | 
			
		||||
                                    if (formPostId !== audioPostId) {
 | 
			
		||||
                                      tempSaveAudio.current = {
 | 
			
		||||
                                        ...(tempSaveAudio.current || {}),
 | 
			
		||||
                                        currentSelection: section,
 | 
			
		||||
                                        message:
 | 
			
		||||
                                          'You are current on a playlist. Would you like to switch?'
 | 
			
		||||
                                      }
 | 
			
		||||
                                      setisOpenSwitchPlaylistModal(true)
 | 
			
		||||
                                    } else {
 | 
			
		||||
                                      const findIndex = (
 | 
			
		||||
                                        audios || []
 | 
			
		||||
                                      ).findIndex(
 | 
			
		||||
                                        (item) =>
 | 
			
		||||
                                          item.identifier ===
 | 
			
		||||
                                          section.content.identifier
 | 
			
		||||
                                      )
 | 
			
		||||
                                      if (findIndex >= 0) {
 | 
			
		||||
                                        dispatch(setCurrAudio(findIndex))
 | 
			
		||||
                                      }
 | 
			
		||||
                                    }
 | 
			
		||||
                                  }}
 | 
			
		||||
                                  title={section.content?.title}
 | 
			
		||||
                                  description={section.content?.description}
 | 
			
		||||
                                  author=""
 | 
			
		||||
                                />
 | 
			
		||||
                              </ContextMenuResource>
 | 
			
		||||
                            </DynamicHeightItemMinimal>
 | 
			
		||||
                          </ErrorBoundary>
 | 
			
		||||
                        </div>
 | 
			
		||||
                      )
 | 
			
		||||
                    }
 | 
			
		||||
                    if (section?.type === 'file') {
 | 
			
		||||
                      return (
 | 
			
		||||
                        <div key={section?.id} className="grid-item">
 | 
			
		||||
                          <ErrorBoundary
 | 
			
		||||
                            fallback={
 | 
			
		||||
                              <Typography>
 | 
			
		||||
                                Error loading content: Invalid Data
 | 
			
		||||
                              </Typography>
 | 
			
		||||
                            }
 | 
			
		||||
                          >
 | 
			
		||||
                            <DynamicHeightItemMinimal
 | 
			
		||||
                              layouts={layouts}
 | 
			
		||||
                              setLayouts={setLayouts}
 | 
			
		||||
                              i={section.id}
 | 
			
		||||
                              breakpoint={currentBreakpoint}
 | 
			
		||||
                              count={count}
 | 
			
		||||
                              padding={0}
 | 
			
		||||
                            >
 | 
			
		||||
                              <ContextMenuResource
 | 
			
		||||
                                name={section.content.name}
 | 
			
		||||
                                service={section.content.service}
 | 
			
		||||
                                identifier={section.content.identifier}
 | 
			
		||||
                                link={`qortal://${section?.content?.service}/${section?.content?.name}/${section?.content?.identifier}`}
 | 
			
		||||
                              >
 | 
			
		||||
                                <FileElement
 | 
			
		||||
                                  key={section.id}
 | 
			
		||||
                                  fileInfo={section.content}
 | 
			
		||||
                                  postId={fullPostId}
 | 
			
		||||
                                  user={user ? user : ''}
 | 
			
		||||
                                  title={section.content?.title}
 | 
			
		||||
                                  description={section.content?.description}
 | 
			
		||||
                                  mimeType={section.content?.mimeType}
 | 
			
		||||
                                  author=""
 | 
			
		||||
                                />
 | 
			
		||||
                              </ContextMenuResource>
 | 
			
		||||
                            </DynamicHeightItemMinimal>
 | 
			
		||||
                          </ErrorBoundary>
 | 
			
		||||
                        </div>
 | 
			
		||||
                      )
 | 
			
		||||
                    }
 | 
			
		||||
                  })}
 | 
			
		||||
                </Box>
 | 
			
		||||
              )
 | 
			
		||||
            })}
 | 
			
		||||
          </>
 | 
			
		||||
        )}
 | 
			
		||||
        <ReusableModal open={isOpenSwitchPlaylistModal}>
 | 
			
		||||
          <Box
 | 
			
		||||
            sx={{
 | 
			
		||||
              display: 'flex',
 | 
			
		||||
              alignItems: 'center',
 | 
			
		||||
              gap: 1
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Typography>
 | 
			
		||||
              {tempSaveAudio?.current?.message
 | 
			
		||||
                ? tempSaveAudio?.current?.message
 | 
			
		||||
                : 'You are current on a playlist. Would you like to switch?'}
 | 
			
		||||
            </Typography>
 | 
			
		||||
          </Box>
 | 
			
		||||
          <Button
 | 
			
		||||
            variant="contained"
 | 
			
		||||
            onClick={() => setisOpenSwitchPlaylistModal(false)}
 | 
			
		||||
          >
 | 
			
		||||
            Cancel
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Button variant="contained" onClick={switchPlayList}>
 | 
			
		||||
            Switch
 | 
			
		||||
          </Button>
 | 
			
		||||
        </ReusableModal>
 | 
			
		||||
      </Box>
 | 
			
		||||
    </Box>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const Content = ({
 | 
			
		||||
  children,
 | 
			
		||||
  layouts,
 | 
			
		||||
  blogContent,
 | 
			
		||||
  onResizeStop,
 | 
			
		||||
  onBreakpointChange,
 | 
			
		||||
  handleLayoutChange
 | 
			
		||||
}: any) => {
 | 
			
		||||
  if (layouts && blogContent?.layouts) {
 | 
			
		||||
    return (
 | 
			
		||||
      <ErrorBoundary
 | 
			
		||||
        fallback={
 | 
			
		||||
          <Typography>Error loading content: Invalid Layout</Typography>
 | 
			
		||||
        }
 | 
			
		||||
      >
 | 
			
		||||
        <ResponsiveGridLayout
 | 
			
		||||
          layouts={layouts}
 | 
			
		||||
          breakpoints={{ md: 996, sm: 768, xs: 480 }}
 | 
			
		||||
          cols={{ md: 4, sm: 3, xs: 1 }}
 | 
			
		||||
          measureBeforeMount={false}
 | 
			
		||||
          onLayoutChange={handleLayoutChange}
 | 
			
		||||
          autoSize={true}
 | 
			
		||||
          compactType={null}
 | 
			
		||||
          isBounded={true}
 | 
			
		||||
          resizeHandles={['se', 'sw', 'ne', 'nw']}
 | 
			
		||||
          rowHeight={25}
 | 
			
		||||
          onResizeStop={onResizeStop}
 | 
			
		||||
          onBreakpointChange={onBreakpointChange}
 | 
			
		||||
          isDraggable={false}
 | 
			
		||||
          isResizable={false}
 | 
			
		||||
          margin={[0, 0]}
 | 
			
		||||
        >
 | 
			
		||||
          {children}
 | 
			
		||||
        </ResponsiveGridLayout>
 | 
			
		||||
      </ErrorBoundary>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
  return children
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										301
									
								
								src/pages/BlogIndividualProfile/BlogIndividualProfile.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,301 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { useNavigate } from 'react-router-dom'
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux'
 | 
			
		||||
import { RootState } from '../../state/store'
 | 
			
		||||
import { useParams } from 'react-router-dom'
 | 
			
		||||
import { Typography, Box, Button, useTheme } from '@mui/material'
 | 
			
		||||
import EditIcon from '@mui/icons-material/Edit'
 | 
			
		||||
import BlogPostPreview from '../BlogList/PostPreview'
 | 
			
		||||
import {
 | 
			
		||||
  setIsLoadingGlobal,
 | 
			
		||||
  setVisitingBlog,
 | 
			
		||||
  toggleEditBlogModal
 | 
			
		||||
} from '../../state/features/globalSlice'
 | 
			
		||||
import {
 | 
			
		||||
  addSubscription,
 | 
			
		||||
  BlogPost,
 | 
			
		||||
  removeSubscription
 | 
			
		||||
} from '../../state/features/blogSlice'
 | 
			
		||||
import { useFetchPosts } from '../../hooks/useFetchPosts'
 | 
			
		||||
import LazyLoad from '../../components/common/LazyLoad'
 | 
			
		||||
import { addPrefix, removePrefix } from '../../utils/blogIdformats'
 | 
			
		||||
import Masonry from 'react-masonry-css'
 | 
			
		||||
import ContextMenuResource from '../../components/common/ContextMenu/ContextMenuResource'
 | 
			
		||||
 | 
			
		||||
const breakpointColumnsObj = {
 | 
			
		||||
  default: 5,
 | 
			
		||||
  1600: 4,
 | 
			
		||||
  1300: 3,
 | 
			
		||||
  940: 2,
 | 
			
		||||
  700: 1,
 | 
			
		||||
  500: 1
 | 
			
		||||
}
 | 
			
		||||
export const BlogIndividualProfile = () => {
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
  const theme = useTheme()
 | 
			
		||||
  const { user } = useSelector((state: RootState) => state.auth)
 | 
			
		||||
  const { currentBlog } = useSelector((state: RootState) => state.global)
 | 
			
		||||
  const subscriptions = useSelector(
 | 
			
		||||
    (state: RootState) => state.blog.subscriptions
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const { blog: blogShortVersion, user: username } = useParams()
 | 
			
		||||
  const blog = React.useMemo(() => {
 | 
			
		||||
    if (!blogShortVersion) return ''
 | 
			
		||||
    return addPrefix(blogShortVersion)
 | 
			
		||||
  }, [blogShortVersion])
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
  const [userBlog, setUserBlog] = React.useState<any>(null)
 | 
			
		||||
  const { checkAndUpdatePost, getBlogPost, hashMapPosts } = useFetchPosts()
 | 
			
		||||
 | 
			
		||||
  const [blogPosts, setBlogPosts] = React.useState<BlogPost[]>([])
 | 
			
		||||
 | 
			
		||||
  const getBlogPosts = React.useCallback(async () => {
 | 
			
		||||
    let name = username
 | 
			
		||||
 | 
			
		||||
    if (!name) return
 | 
			
		||||
    if (!blog) return
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      dispatch(setIsLoadingGlobal(true))
 | 
			
		||||
      const offset = blogPosts.length
 | 
			
		||||
      //TODO - NAME SHOULD BE EXACT
 | 
			
		||||
      const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_POST&query=${blog}-post-&limit=20&exactmatchnames=true&name=${name}&includemetadata=true&offset=${offset}&reverse=true`
 | 
			
		||||
      const response = await fetch(url, {
 | 
			
		||||
        method: 'GET',
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'application/json'
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      const responseData = await response.json()
 | 
			
		||||
 | 
			
		||||
      const structureData = responseData.map((post: any): BlogPost => {
 | 
			
		||||
        return {
 | 
			
		||||
          title: post?.metadata?.title,
 | 
			
		||||
          category: post?.metadata?.category,
 | 
			
		||||
          categoryName: post?.metadata?.categoryName,
 | 
			
		||||
          tags: post?.metadata?.tags || [],
 | 
			
		||||
          description: post?.metadata?.description,
 | 
			
		||||
          createdAt: '',
 | 
			
		||||
          user: post.name,
 | 
			
		||||
          postImage: '',
 | 
			
		||||
          id: post.identifier
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      setBlogPosts(structureData)
 | 
			
		||||
      const copiedBlogPosts: BlogPost[] = [...blogPosts]
 | 
			
		||||
      structureData.forEach((post: BlogPost) => {
 | 
			
		||||
        const index = blogPosts.findIndex((p) => p.id === post.id)
 | 
			
		||||
        if (index !== -1) {
 | 
			
		||||
          copiedBlogPosts[index] = post
 | 
			
		||||
        } else {
 | 
			
		||||
          copiedBlogPosts.push(post)
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      setBlogPosts(copiedBlogPosts)
 | 
			
		||||
 | 
			
		||||
      for (const content of structureData) {
 | 
			
		||||
        if (content.user && content.id) {
 | 
			
		||||
          const res = checkAndUpdatePost(content)
 | 
			
		||||
 | 
			
		||||
          if (res) {
 | 
			
		||||
            getBlogPost(content.user, content.id, content)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
    } finally {
 | 
			
		||||
      dispatch(setIsLoadingGlobal(false))
 | 
			
		||||
    }
 | 
			
		||||
  }, [username, blog, blogPosts])
 | 
			
		||||
  const getBlog = React.useCallback(async () => {
 | 
			
		||||
    let name = username
 | 
			
		||||
 | 
			
		||||
    if (!name) return
 | 
			
		||||
    if (!blog) return
 | 
			
		||||
    try {
 | 
			
		||||
      const urlBlog = `/arbitrary/BLOG/${name}/${blog}`
 | 
			
		||||
      const response = await fetch(urlBlog, {
 | 
			
		||||
        method: 'GET',
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'application/json'
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      const responseData = await response.json()
 | 
			
		||||
      dispatch(setVisitingBlog({ ...responseData, name }))
 | 
			
		||||
      setUserBlog(responseData)
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
  }, [username, blog])
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    getBlog()
 | 
			
		||||
  }, [username, blog])
 | 
			
		||||
  const getPosts = React.useCallback(async () => {
 | 
			
		||||
    await getBlogPosts()
 | 
			
		||||
  }, [getBlogPosts])
 | 
			
		||||
 | 
			
		||||
  const subscribe = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      if (!user?.name) return
 | 
			
		||||
      const body = {
 | 
			
		||||
        items: [username]
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const listName = `q-blog-subscriptions-${user.name}`
 | 
			
		||||
 | 
			
		||||
      const response = await qortalRequest({
 | 
			
		||||
        action: 'ADD_LIST_ITEMS',
 | 
			
		||||
        list_name: listName,
 | 
			
		||||
        items: [username]
 | 
			
		||||
      })
 | 
			
		||||
      if (response === true) {
 | 
			
		||||
        dispatch(addSubscription(username))
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
  }
 | 
			
		||||
  const unsubscribe = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      if (!user?.name) return
 | 
			
		||||
 | 
			
		||||
      const listName = `q-blog-subscriptions-${user.name}`
 | 
			
		||||
 | 
			
		||||
      const response = await qortalRequest({
 | 
			
		||||
        action: 'DELETE_LIST_ITEM',
 | 
			
		||||
        list_name: listName,
 | 
			
		||||
        item: username
 | 
			
		||||
      })
 | 
			
		||||
      if (response === true) {
 | 
			
		||||
        dispatch(removeSubscription(username))
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
  }
 | 
			
		||||
  if (!userBlog) return null
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Box
 | 
			
		||||
        sx={{
 | 
			
		||||
          display: 'flex',
 | 
			
		||||
          gap: 1,
 | 
			
		||||
          alignItems: 'center',
 | 
			
		||||
          justifyContent: 'center'
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Typography
 | 
			
		||||
          variant="h1"
 | 
			
		||||
          color="textPrimary"
 | 
			
		||||
          sx={{
 | 
			
		||||
            textAlign: 'center',
 | 
			
		||||
            marginTop: '20px'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {currentBlog?.blogId === blog ? currentBlog?.title : userBlog.title}
 | 
			
		||||
        </Typography>
 | 
			
		||||
        {currentBlog?.blogId === blog && (
 | 
			
		||||
          <EditIcon
 | 
			
		||||
            sx={{
 | 
			
		||||
              cursor: 'pointer'
 | 
			
		||||
            }}
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              dispatch(toggleEditBlogModal(true))
 | 
			
		||||
            }}
 | 
			
		||||
          ></EditIcon>
 | 
			
		||||
        )}
 | 
			
		||||
        {subscriptions.includes(username) && (
 | 
			
		||||
          <Button
 | 
			
		||||
            sx={{
 | 
			
		||||
              backgroundColor: theme.palette.primary.light,
 | 
			
		||||
              color: theme.palette.text.primary,
 | 
			
		||||
              fontFamily: 'Arial'
 | 
			
		||||
            }}
 | 
			
		||||
            onClick={unsubscribe}
 | 
			
		||||
          >
 | 
			
		||||
            Unsubscribe
 | 
			
		||||
          </Button>
 | 
			
		||||
        )}
 | 
			
		||||
        {!subscriptions.includes(username) && (
 | 
			
		||||
          <Button
 | 
			
		||||
            sx={{
 | 
			
		||||
              backgroundColor: theme.palette.primary.light,
 | 
			
		||||
              color: theme.palette.text.primary,
 | 
			
		||||
              fontFamily: 'Arial'
 | 
			
		||||
            }}
 | 
			
		||||
            onClick={subscribe}
 | 
			
		||||
          >
 | 
			
		||||
            Subscribe
 | 
			
		||||
          </Button>
 | 
			
		||||
        )}
 | 
			
		||||
      </Box>
 | 
			
		||||
 | 
			
		||||
      <Masonry
 | 
			
		||||
        breakpointCols={breakpointColumnsObj}
 | 
			
		||||
        className="my-masonry-grid"
 | 
			
		||||
        columnClassName="my-masonry-grid_column"
 | 
			
		||||
        style={{ backgroundColor: theme.palette.background.default }}
 | 
			
		||||
      >
 | 
			
		||||
        {blogPosts.map((post, index) => {
 | 
			
		||||
          const existingPost = hashMapPosts[post.id]
 | 
			
		||||
          let blogPost = post
 | 
			
		||||
          if (existingPost) {
 | 
			
		||||
            blogPost = existingPost
 | 
			
		||||
          }
 | 
			
		||||
          const str = blogPost.id
 | 
			
		||||
          const arr = str.split('-post-')
 | 
			
		||||
          const str1 = arr[0]
 | 
			
		||||
 | 
			
		||||
          const blogId = removePrefix(str1)
 | 
			
		||||
          const str2 = arr[1]
 | 
			
		||||
          return (
 | 
			
		||||
            <Box
 | 
			
		||||
              sx={{
 | 
			
		||||
                display: 'flex',
 | 
			
		||||
                gap: 1,
 | 
			
		||||
                alignItems: 'center',
 | 
			
		||||
                width: 'auto',
 | 
			
		||||
                position: 'relative',
 | 
			
		||||
                ' @media (max-width: 450px)': {
 | 
			
		||||
                  width: '100%'
 | 
			
		||||
                }
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <ContextMenuResource
 | 
			
		||||
                name={blogPost.user}
 | 
			
		||||
                service="BLOG_POST"
 | 
			
		||||
                identifier={blogPost.id}
 | 
			
		||||
                link={`qortal://APP/Q-Blog/${blogPost.user}/${blogId}/${str2}`}
 | 
			
		||||
              >
 | 
			
		||||
                <BlogPostPreview
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    navigate(`/${blogPost.user}/${blogId}/${str2}`)
 | 
			
		||||
                  }}
 | 
			
		||||
                  description={blogPost?.description}
 | 
			
		||||
                  title={blogPost?.title}
 | 
			
		||||
                  createdAt={blogPost?.createdAt}
 | 
			
		||||
                  author={blogPost.user}
 | 
			
		||||
                  postImage={blogPost?.postImage}
 | 
			
		||||
                  blogPost={blogPost}
 | 
			
		||||
                  tags={blogPost?.tags}
 | 
			
		||||
                />
 | 
			
		||||
              </ContextMenuResource>
 | 
			
		||||
              {blogPost.user === user?.name && (
 | 
			
		||||
                <EditIcon
 | 
			
		||||
                  className="edit-btn"
 | 
			
		||||
                  sx={{
 | 
			
		||||
                    position: 'absolute',
 | 
			
		||||
                    zIndex: 10,
 | 
			
		||||
                    bottom: '25px',
 | 
			
		||||
                    right: '25px',
 | 
			
		||||
                    cursor: 'pointer'
 | 
			
		||||
                  }}
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    navigate(`/${blogPost.user}/${blogId}/${str2}/edit`)
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
              )}
 | 
			
		||||
            </Box>
 | 
			
		||||
          )
 | 
			
		||||
        })}
 | 
			
		||||
      </Masonry>
 | 
			
		||||
      <LazyLoad onLoadMore={getPosts}></LazyLoad>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										225
									
								
								src/pages/BlogList/BlogList.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,225 @@
 | 
			
		||||
import React, { FC, useCallback, useEffect, useRef } from 'react'
 | 
			
		||||
import { useNavigate } from 'react-router-dom'
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux'
 | 
			
		||||
import { RootState } from '../../state/store'
 | 
			
		||||
import EditIcon from '@mui/icons-material/Edit'
 | 
			
		||||
import {
 | 
			
		||||
  Box,
 | 
			
		||||
  Button,
 | 
			
		||||
  List,
 | 
			
		||||
  ListItem,
 | 
			
		||||
  Typography,
 | 
			
		||||
  useTheme
 | 
			
		||||
} from '@mui/material'
 | 
			
		||||
import BlogPostPreview from './PostPreview'
 | 
			
		||||
import { useFetchPosts } from '../../hooks/useFetchPosts'
 | 
			
		||||
import LazyLoad from '../../components/common/LazyLoad'
 | 
			
		||||
import { removePrefix } from '../../utils/blogIdformats'
 | 
			
		||||
import Masonry from 'react-masonry-css'
 | 
			
		||||
import ContextMenuResource from '../../components/common/ContextMenu/ContextMenuResource'
 | 
			
		||||
 | 
			
		||||
const breakpointColumnsObj = {
 | 
			
		||||
  default: 5,
 | 
			
		||||
  1600: 4,
 | 
			
		||||
  1300: 3,
 | 
			
		||||
  940: 2,
 | 
			
		||||
  700: 1,
 | 
			
		||||
  500: 1
 | 
			
		||||
}
 | 
			
		||||
interface BlogListProps {
 | 
			
		||||
  mode?: string
 | 
			
		||||
}
 | 
			
		||||
export const BlogList = ({ mode }: BlogListProps) => {
 | 
			
		||||
  const theme = useTheme()
 | 
			
		||||
  const prevVal = useRef('')
 | 
			
		||||
  const { user } = useSelector((state: RootState) => state.auth)
 | 
			
		||||
  const hashMapPosts = useSelector(
 | 
			
		||||
    (state: RootState) => state.blog.hashMapPosts
 | 
			
		||||
  )
 | 
			
		||||
  const favoritesLocal = useSelector(
 | 
			
		||||
    (state: RootState) => state.blog.favoritesLocal
 | 
			
		||||
  )
 | 
			
		||||
  const subscriptionPosts = useSelector(
 | 
			
		||||
    (state: RootState) => state.blog.subscriptionPosts
 | 
			
		||||
  )
 | 
			
		||||
  const countNewPosts = useSelector(
 | 
			
		||||
    (state: RootState) => state.blog.countNewPosts
 | 
			
		||||
  )
 | 
			
		||||
  const isFiltering = useSelector((state: RootState) => state.blog.isFiltering)
 | 
			
		||||
  const filterValue = useSelector((state: RootState) => state.blog.filterValue)
 | 
			
		||||
  const filteredPosts = useSelector(
 | 
			
		||||
    (state: RootState) => state.blog.filteredPosts
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const { posts: globalPosts, favorites } = useSelector(
 | 
			
		||||
    (state: RootState) => state.blog
 | 
			
		||||
  )
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
  const {
 | 
			
		||||
    getBlogPosts,
 | 
			
		||||
    getBlogPostsFavorites,
 | 
			
		||||
    getBlogPostsSubscriptions,
 | 
			
		||||
    checkNewMessages,
 | 
			
		||||
    getNewPosts,
 | 
			
		||||
    getBlogFilteredPosts
 | 
			
		||||
  } = useFetchPosts()
 | 
			
		||||
  const getPosts = React.useCallback(async () => {
 | 
			
		||||
    if (isFiltering) {
 | 
			
		||||
      getBlogFilteredPosts(filterValue)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (mode === 'favorites') {
 | 
			
		||||
      getBlogPostsFavorites()
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (mode === 'subscriptions' && user?.name) {
 | 
			
		||||
      getBlogPostsSubscriptions(user.name)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    await getBlogPosts()
 | 
			
		||||
  }, [getBlogPosts, mode, favoritesLocal, user?.name, isFiltering, filterValue])
 | 
			
		||||
 | 
			
		||||
  let posts = globalPosts
 | 
			
		||||
 | 
			
		||||
  if (mode === 'favorites') {
 | 
			
		||||
    posts = favorites
 | 
			
		||||
  }
 | 
			
		||||
  if (mode === 'subscriptions') {
 | 
			
		||||
    posts = subscriptionPosts
 | 
			
		||||
  }
 | 
			
		||||
  if (isFiltering) {
 | 
			
		||||
    posts = filteredPosts
 | 
			
		||||
  }
 | 
			
		||||
  const interval = useRef<any>(null)
 | 
			
		||||
 | 
			
		||||
  const checkNewMessagesFunc = useCallback(() => {
 | 
			
		||||
    let isCalling = false
 | 
			
		||||
    interval.current = setInterval(async () => {
 | 
			
		||||
      if (isCalling) return
 | 
			
		||||
      isCalling = true
 | 
			
		||||
      const res = await checkNewMessages()
 | 
			
		||||
      isCalling = false
 | 
			
		||||
    }, 30000) // 1 second interval
 | 
			
		||||
  }, [checkNewMessages])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!mode) {
 | 
			
		||||
      checkNewMessagesFunc()
 | 
			
		||||
    }
 | 
			
		||||
    return () => {
 | 
			
		||||
      if (interval?.current) {
 | 
			
		||||
        clearInterval(interval.current)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, [mode, checkNewMessagesFunc])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (isFiltering && filterValue !== prevVal?.current) {
 | 
			
		||||
      prevVal.current = filterValue
 | 
			
		||||
      getPosts()
 | 
			
		||||
    }
 | 
			
		||||
  }, [filterValue, isFiltering, filteredPosts])
 | 
			
		||||
  // if (!favoritesLocal) return null
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {!mode && countNewPosts > 0 && !isFiltering && (
 | 
			
		||||
        <Box
 | 
			
		||||
          sx={{
 | 
			
		||||
            display: 'flex',
 | 
			
		||||
            alignItems: 'center',
 | 
			
		||||
            justifyContent: 'center'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Typography>
 | 
			
		||||
            {countNewPosts === 1
 | 
			
		||||
              ? `There is ${countNewPosts} new post`
 | 
			
		||||
              : `There are ${countNewPosts} new posts`}
 | 
			
		||||
          </Typography>
 | 
			
		||||
          <Button
 | 
			
		||||
            sx={{
 | 
			
		||||
              backgroundColor: theme.palette.primary.light,
 | 
			
		||||
              color: theme.palette.text.primary,
 | 
			
		||||
              fontFamily: 'Arial'
 | 
			
		||||
            }}
 | 
			
		||||
            onClick={getNewPosts}
 | 
			
		||||
          >
 | 
			
		||||
            Load new Posts
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Box>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      <Masonry
 | 
			
		||||
        breakpointCols={breakpointColumnsObj}
 | 
			
		||||
        className="my-masonry-grid"
 | 
			
		||||
        columnClassName="my-masonry-grid_column"
 | 
			
		||||
      >
 | 
			
		||||
        {posts.map((post, index) => {
 | 
			
		||||
          const existingPost = hashMapPosts[post.id]
 | 
			
		||||
          let blogPost = post
 | 
			
		||||
          if (existingPost) {
 | 
			
		||||
            blogPost = existingPost
 | 
			
		||||
          }
 | 
			
		||||
          const str = blogPost.id
 | 
			
		||||
          const arr = str.split('-post-')
 | 
			
		||||
          const str1 = arr[0]
 | 
			
		||||
          const str2 = arr[1]
 | 
			
		||||
          const blogId = removePrefix(str1)
 | 
			
		||||
          return (
 | 
			
		||||
            <Box
 | 
			
		||||
              sx={{
 | 
			
		||||
                display: 'flex',
 | 
			
		||||
                gap: 1,
 | 
			
		||||
                alignItems: 'center',
 | 
			
		||||
                width: 'auto',
 | 
			
		||||
                position: 'relative',
 | 
			
		||||
                ' @media (max-width: 450px)': {
 | 
			
		||||
                  width: '100%'
 | 
			
		||||
                }
 | 
			
		||||
              }}
 | 
			
		||||
              key={blogPost.id}
 | 
			
		||||
            >
 | 
			
		||||
              <ContextMenuResource
 | 
			
		||||
                name={blogPost.user}
 | 
			
		||||
                service="BLOG_POST"
 | 
			
		||||
                identifier={blogPost.id}
 | 
			
		||||
                link={`qortal://APP/Q-Blog/${blogPost.user}/${blogId}/${str2}`}
 | 
			
		||||
              >
 | 
			
		||||
                <BlogPostPreview
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    navigate(`/${blogPost.user}/${blogId}/${str2}`)
 | 
			
		||||
                  }}
 | 
			
		||||
                  description={blogPost?.description}
 | 
			
		||||
                  title={blogPost?.title}
 | 
			
		||||
                  createdAt={blogPost?.createdAt}
 | 
			
		||||
                  author={blogPost.user}
 | 
			
		||||
                  postImage={blogPost?.postImage}
 | 
			
		||||
                  blogPost={blogPost}
 | 
			
		||||
                  isValid={blogPost?.isValid}
 | 
			
		||||
                  tags={blogPost?.tags}
 | 
			
		||||
                />
 | 
			
		||||
              </ContextMenuResource>
 | 
			
		||||
              {blogPost.user === user?.name && (
 | 
			
		||||
                <EditIcon
 | 
			
		||||
                  className="edit-btn"
 | 
			
		||||
                  sx={{
 | 
			
		||||
                    position: 'absolute',
 | 
			
		||||
                    zIndex: 10,
 | 
			
		||||
                    bottom: '25px',
 | 
			
		||||
                    right: '25px',
 | 
			
		||||
                    cursor: 'pointer'
 | 
			
		||||
                  }}
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    navigate(`/${blogPost.user}/${blogId}/${str2}/edit`)
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
              )}
 | 
			
		||||
            </Box>
 | 
			
		||||
          )
 | 
			
		||||
        })}
 | 
			
		||||
      </Masonry>
 | 
			
		||||
      {/* </List> */}
 | 
			
		||||
      <LazyLoad onLoadMore={getPosts}></LazyLoad>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										134
									
								
								src/pages/BlogList/PostPreview-styles.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,134 @@
 | 
			
		||||
import { styled } from "@mui/system";
 | 
			
		||||
import { Card, Box, Typography } from "@mui/material";
 | 
			
		||||
 | 
			
		||||
export const StyledCard = styled(Card)(({ theme }) => ({ 
 | 
			
		||||
  backgroundColor: theme.palette.mode === "light" ? theme.palette.primary.main : theme.palette.primary.dark,
 | 
			
		||||
  maxWidth: "600px",
 | 
			
		||||
  width: "100%",
 | 
			
		||||
  margin: "10px 0px",
 | 
			
		||||
  cursor: "pointer",
 | 
			
		||||
  "@media (max-width: 450px)": {
 | 
			
		||||
    width: "100%;"
 | 
			
		||||
  }
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
export const CardContentContainer = styled(Box)(({ theme }) => ({ 
 | 
			
		||||
  backgroundColor: theme.palette.mode === "light" ? theme.palette.primary.dark : theme.palette.primary.light,
 | 
			
		||||
  margin: "5px 10px",
 | 
			
		||||
  borderRadius: "15px",
 | 
			
		||||
}));
 | 
			
		||||
export const CardContentContainerComment = styled(Box)(({ theme }) => ({
 | 
			
		||||
  backgroundColor:
 | 
			
		||||
    theme.palette.mode === 'light'
 | 
			
		||||
      ? theme.palette.primary.dark
 | 
			
		||||
      : theme.palette.primary.light,
 | 
			
		||||
  margin: '0px',
 | 
			
		||||
  borderRadius: '15px',
 | 
			
		||||
  width: '100%',
 | 
			
		||||
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  flexDirection: 'column'
 | 
			
		||||
}))
 | 
			
		||||
 | 
			
		||||
export const StyledCardHeader = styled(Box)({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  alignItems: 'center',
 | 
			
		||||
  justifyContent: 'flex-start',
 | 
			
		||||
  gap: '5px',
 | 
			
		||||
  padding: '7px'
 | 
			
		||||
})
 | 
			
		||||
export const StyledCardHeaderComment = styled(Box)({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  alignItems: 'center',
 | 
			
		||||
  justifyContent: 'flex-start',
 | 
			
		||||
  gap: '5px',
 | 
			
		||||
  padding: '7px'
 | 
			
		||||
})
 | 
			
		||||
export const StyledCardCol = styled(Box)({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  overflow: 'hidden',
 | 
			
		||||
  flexDirection: 'column',
 | 
			
		||||
  gap: '2px',
 | 
			
		||||
  alignItems: 'flex-start',
 | 
			
		||||
  width: '100%'
 | 
			
		||||
})
 | 
			
		||||
export const StyledCardColComment = styled(Box)({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  overflow: 'hidden',
 | 
			
		||||
  flexDirection: 'column',
 | 
			
		||||
  gap: '2px',
 | 
			
		||||
  alignItems: 'flex-start',
 | 
			
		||||
  width: '100%'
 | 
			
		||||
})
 | 
			
		||||
export const StyledCardContent = styled(Box)({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  flexDirection: 'column',
 | 
			
		||||
  alignItems: 'center',
 | 
			
		||||
  justifyContent: 'flex-start',
 | 
			
		||||
  padding: '5px 10px',
 | 
			
		||||
  gap: '10px'
 | 
			
		||||
})
 | 
			
		||||
export const StyledCardContentComment = styled(Box)({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  flexDirection: 'column',
 | 
			
		||||
  alignItems: 'flex-start',
 | 
			
		||||
  justifyContent: 'flex-start',
 | 
			
		||||
  padding: '5px 10px',
 | 
			
		||||
  gap: '10px'
 | 
			
		||||
})
 | 
			
		||||
export const TitleText = styled(Typography)({
 | 
			
		||||
  whiteSpace: 'nowrap',
 | 
			
		||||
  overflow: 'hidden',
 | 
			
		||||
  textOverflow: 'ellipsis',
 | 
			
		||||
  width: '100%',
 | 
			
		||||
  fontFamily: 'Cairo, sans-serif',
 | 
			
		||||
  fontSize: '22px',
 | 
			
		||||
  lineHeight: '1.2'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const AuthorText = styled(Typography)({
 | 
			
		||||
  fontFamily: 'Raleway, sans-serif',
 | 
			
		||||
  fontSize: '16px',
 | 
			
		||||
  lineHeight: '1.2'
 | 
			
		||||
})
 | 
			
		||||
export const AuthorTextComment = styled(Typography)({
 | 
			
		||||
  fontFamily: 'Raleway, sans-serif',
 | 
			
		||||
  fontSize: '16px',
 | 
			
		||||
  lineHeight: '1.2'
 | 
			
		||||
})
 | 
			
		||||
export const IconsBox = styled(Box)({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  gap: "3px",
 | 
			
		||||
  position: 'absolute',
 | 
			
		||||
  top: '12px',
 | 
			
		||||
  right: '5px',
 | 
			
		||||
  transition: 'all 0.3s ease-in-out',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const BookmarkIconContainer = styled(Box)({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  boxShadow: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;",
 | 
			
		||||
  backgroundColor: '#fbfbfb',
 | 
			
		||||
  color: "#50e3c2",
 | 
			
		||||
  padding: '5px',
 | 
			
		||||
  borderRadius: '3px',
 | 
			
		||||
  transition: 'all 0.3s ease-in-out',
 | 
			
		||||
  "&:hover": {
 | 
			
		||||
    cursor: 'pointer',
 | 
			
		||||
    transform: "scale(1.1)",
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const BlockIconContainer = styled(Box)({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  boxShadow: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;",
 | 
			
		||||
  backgroundColor: '#fbfbfb',
 | 
			
		||||
  color: "#c25252",
 | 
			
		||||
  padding: '5px',
 | 
			
		||||
  borderRadius: '3px',
 | 
			
		||||
  transition: 'all 0.3s ease-in-out',
 | 
			
		||||
  "&:hover": {
 | 
			
		||||
    cursor: 'pointer',
 | 
			
		||||
    transform: "scale(1.1)",
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										320
									
								
								src/pages/BlogList/PostPreview.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,320 @@
 | 
			
		||||
import React, { useMemo, useState } from 'react'
 | 
			
		||||
import {
 | 
			
		||||
  Avatar,
 | 
			
		||||
  Card,
 | 
			
		||||
  CardContent,
 | 
			
		||||
  CardHeader,
 | 
			
		||||
  CardMedia,
 | 
			
		||||
  Typography,
 | 
			
		||||
  Box,
 | 
			
		||||
  Button,
 | 
			
		||||
  Tooltip,
 | 
			
		||||
  useTheme
 | 
			
		||||
} from '@mui/material'
 | 
			
		||||
import Dialog from '@mui/material/Dialog'
 | 
			
		||||
import DialogActions from '@mui/material/DialogActions'
 | 
			
		||||
import DialogContent from '@mui/material/DialogContent'
 | 
			
		||||
import DialogContentText from '@mui/material/DialogContentText'
 | 
			
		||||
import DialogTitle from '@mui/material/DialogTitle'
 | 
			
		||||
import { styled } from '@mui/system'
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  CardContentContainer,
 | 
			
		||||
  StyledCard,
 | 
			
		||||
  StyledCardContent,
 | 
			
		||||
  TitleText,
 | 
			
		||||
  AuthorText,
 | 
			
		||||
  StyledCardHeader,
 | 
			
		||||
  StyledCardCol,
 | 
			
		||||
  IconsBox,
 | 
			
		||||
  BlockIconContainer,
 | 
			
		||||
  BookmarkIconContainer
 | 
			
		||||
} from './PostPreview-styles'
 | 
			
		||||
import moment from 'moment'
 | 
			
		||||
import {
 | 
			
		||||
  blockUser,
 | 
			
		||||
  BlogPost,
 | 
			
		||||
  removeFavorites,
 | 
			
		||||
  removeSubscription,
 | 
			
		||||
  upsertFavorites
 | 
			
		||||
} from '../../state/features/blogSlice'
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux'
 | 
			
		||||
import BookmarkBorderIcon from '@mui/icons-material/BookmarkBorder'
 | 
			
		||||
import BookmarkIcon from '@mui/icons-material/Bookmark'
 | 
			
		||||
import { AppDispatch, RootState } from '../../state/store'
 | 
			
		||||
import BlockIcon from '@mui/icons-material/Block'
 | 
			
		||||
import { CustomIcon } from '../../components/common/CustomIcon'
 | 
			
		||||
import ResponsiveImage from '../../components/common/ResponsiveImage'
 | 
			
		||||
import { formatDate } from '../../utils/time'
 | 
			
		||||
interface BlogPostPreviewProps {
 | 
			
		||||
  title: string
 | 
			
		||||
  createdAt: number | string
 | 
			
		||||
  author: string
 | 
			
		||||
  postImage?: string
 | 
			
		||||
  description: any
 | 
			
		||||
  blogPost: BlogPost
 | 
			
		||||
  onClick?: () => void
 | 
			
		||||
  isValid?: boolean
 | 
			
		||||
  tags?: string[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const BlogPostPreview: React.FC<BlogPostPreviewProps> = ({
 | 
			
		||||
  title,
 | 
			
		||||
  createdAt,
 | 
			
		||||
  author,
 | 
			
		||||
  postImage,
 | 
			
		||||
  description,
 | 
			
		||||
  onClick,
 | 
			
		||||
  blogPost,
 | 
			
		||||
  isValid,
 | 
			
		||||
  tags
 | 
			
		||||
}) => {
 | 
			
		||||
  const [avatarUrl, setAvatarUrl] = React.useState<string>('')
 | 
			
		||||
  const [showIcons, setShowIcons] = React.useState<boolean>(false)
 | 
			
		||||
 | 
			
		||||
  const dispatch = useDispatch<AppDispatch>()
 | 
			
		||||
  const theme = useTheme()
 | 
			
		||||
  const favoritesLocal = useSelector(
 | 
			
		||||
    (state: RootState) => state.blog.favoritesLocal
 | 
			
		||||
  )
 | 
			
		||||
  const [isOpenAlert, setIsOpenAlert] = useState<boolean>(false)
 | 
			
		||||
  const subscriptions = useSelector(
 | 
			
		||||
    (state: RootState) => state.blog.subscriptions
 | 
			
		||||
  )
 | 
			
		||||
  const username = useSelector((state: RootState) => state.auth?.user?.name)
 | 
			
		||||
 | 
			
		||||
  function extractTextFromSlate(nodes: any) {
 | 
			
		||||
    if (!Array.isArray(nodes)) return ''
 | 
			
		||||
    let text = ''
 | 
			
		||||
 | 
			
		||||
    for (const node of nodes) {
 | 
			
		||||
      if (node.text) {
 | 
			
		||||
        text += node.text
 | 
			
		||||
      } else if (node.children) {
 | 
			
		||||
        text += extractTextFromSlate(node.children)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return text
 | 
			
		||||
  }
 | 
			
		||||
  const getAvatar = React.useCallback(async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      let url = await qortalRequest({
 | 
			
		||||
        action: 'GET_QDN_RESOURCE_URL',
 | 
			
		||||
        name: author,
 | 
			
		||||
        service: 'THUMBNAIL',
 | 
			
		||||
        identifier: 'qortal_avatar'
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      setAvatarUrl(url)
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
  }, [author])
 | 
			
		||||
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    getAvatar()
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  const isFavorite = useMemo(() => {
 | 
			
		||||
    if (!favoritesLocal) return false
 | 
			
		||||
    return favoritesLocal.find((fav) => fav?.id === blogPost?.id)
 | 
			
		||||
  }, [favoritesLocal, blogPost?.id])
 | 
			
		||||
 | 
			
		||||
  const blockUserFunc = async (user: string) => {
 | 
			
		||||
    if (user === 'Q-Blog') return
 | 
			
		||||
    if (subscriptions.includes(user) && username) {
 | 
			
		||||
      try {
 | 
			
		||||
        const listName = `q-blog-subscriptions-${username}`
 | 
			
		||||
 | 
			
		||||
        const response = await qortalRequest({
 | 
			
		||||
          action: 'DELETE_LIST_ITEM',
 | 
			
		||||
          list_name: listName,
 | 
			
		||||
          item: user
 | 
			
		||||
        })
 | 
			
		||||
        if (response === true) {
 | 
			
		||||
          dispatch(removeSubscription(user))
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {}
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const response = await qortalRequest({
 | 
			
		||||
        action: 'ADD_LIST_ITEMS',
 | 
			
		||||
        list_name: 'blockedNames_q-blog',
 | 
			
		||||
        items: [user]
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      if (response === true) {
 | 
			
		||||
        dispatch(blockUser(user))
 | 
			
		||||
        dispatch(removeFavorites(blogPost.id))
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const continueToPost = () => {
 | 
			
		||||
    if (isValid === false) {
 | 
			
		||||
      setIsOpenAlert(true)
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (!onClick) return
 | 
			
		||||
    onClick()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleClose = () => {
 | 
			
		||||
    setIsOpenAlert(false)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const dimensions = useMemo(() => {
 | 
			
		||||
    if (Array.isArray(tags)) {
 | 
			
		||||
      const imgDimensions = tags[tags.length - 2]
 | 
			
		||||
      if (!imgDimensions?.includes('v1.')) return ''
 | 
			
		||||
      return imgDimensions
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return ''
 | 
			
		||||
  }, [tags])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <StyledCard
 | 
			
		||||
        onClick={continueToPost}
 | 
			
		||||
        onMouseEnter={() => setShowIcons(true)}
 | 
			
		||||
        onMouseLeave={() => setShowIcons(false)}
 | 
			
		||||
      >
 | 
			
		||||
        <ResponsiveImage src={postImage || ''} dimensions={dimensions} />
 | 
			
		||||
        {/* {postImage && (
 | 
			
		||||
          <Box sx={{ padding: '2px' }}>
 | 
			
		||||
            <img
 | 
			
		||||
              src={postImage}
 | 
			
		||||
              style={{
 | 
			
		||||
                width: '100%',
 | 
			
		||||
                height: 'auto',
 | 
			
		||||
                borderRadius: '8px'
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          </Box>
 | 
			
		||||
        )} */}
 | 
			
		||||
        <CardContentContainer>
 | 
			
		||||
          <StyledCardHeader
 | 
			
		||||
            sx={{
 | 
			
		||||
              '& .MuiCardHeader-content': {
 | 
			
		||||
                overflow: 'hidden'
 | 
			
		||||
              }
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Box>
 | 
			
		||||
              <Avatar src={avatarUrl} alt={`${author}'s avatar`} />
 | 
			
		||||
            </Box>
 | 
			
		||||
            <StyledCardCol>
 | 
			
		||||
              <TitleText
 | 
			
		||||
                color={theme.palette.text.primary}
 | 
			
		||||
                noWrap
 | 
			
		||||
                variant="body1"
 | 
			
		||||
              >
 | 
			
		||||
                {title}
 | 
			
		||||
              </TitleText>
 | 
			
		||||
              <AuthorText
 | 
			
		||||
                color={
 | 
			
		||||
                  theme.palette.mode === 'light'
 | 
			
		||||
                    ? theme.palette.text.secondary
 | 
			
		||||
                    : '#d6e8ff'
 | 
			
		||||
                }
 | 
			
		||||
              >
 | 
			
		||||
                {author}
 | 
			
		||||
              </AuthorText>
 | 
			
		||||
            </StyledCardCol>
 | 
			
		||||
          </StyledCardHeader>
 | 
			
		||||
          <StyledCardContent>
 | 
			
		||||
            <Typography
 | 
			
		||||
              variant="body2"
 | 
			
		||||
              color={theme.palette.text.primary}
 | 
			
		||||
              sx={{
 | 
			
		||||
                wordBreak: 'break-word'
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              {description}
 | 
			
		||||
            </Typography>
 | 
			
		||||
            <Box sx={{ textAlign: 'flex-start', width: '100%' }}>
 | 
			
		||||
              <Typography variant="h6" color={theme.palette.text.primary}>
 | 
			
		||||
                {formatDate(+createdAt)}
 | 
			
		||||
              </Typography>
 | 
			
		||||
            </Box>
 | 
			
		||||
          </StyledCardContent>
 | 
			
		||||
        </CardContentContainer>
 | 
			
		||||
      </StyledCard>
 | 
			
		||||
      <IconsBox
 | 
			
		||||
        sx={{ opacity: showIcons ? 1 : 0 }}
 | 
			
		||||
        onMouseEnter={() => setShowIcons(true)}
 | 
			
		||||
        onMouseLeave={() => setShowIcons(false)}
 | 
			
		||||
      >
 | 
			
		||||
        {username && isFavorite && (
 | 
			
		||||
          <Tooltip title="Remove from favorites" placement="top">
 | 
			
		||||
            <BookmarkIconContainer
 | 
			
		||||
              onMouseEnter={() => setShowIcons(true)}
 | 
			
		||||
              onMouseLeave={() => setShowIcons(false)}
 | 
			
		||||
            >
 | 
			
		||||
              <BookmarkIcon
 | 
			
		||||
                sx={{
 | 
			
		||||
                  color: 'red'
 | 
			
		||||
                }}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  dispatch(removeFavorites(blogPost.id))
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
            </BookmarkIconContainer>
 | 
			
		||||
          </Tooltip>
 | 
			
		||||
        )}
 | 
			
		||||
        {username && !isFavorite && (
 | 
			
		||||
          <Tooltip title="Save to favorites" placement="top">
 | 
			
		||||
            <BookmarkIconContainer
 | 
			
		||||
              onMouseEnter={() => setShowIcons(true)}
 | 
			
		||||
              onMouseLeave={() => setShowIcons(false)}
 | 
			
		||||
            >
 | 
			
		||||
              <BookmarkBorderIcon
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  dispatch(upsertFavorites([blogPost]))
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
            </BookmarkIconContainer>
 | 
			
		||||
          </Tooltip>
 | 
			
		||||
        )}
 | 
			
		||||
        <Tooltip title="Block user content" placement="top">
 | 
			
		||||
          <BlockIconContainer
 | 
			
		||||
            onMouseEnter={() => setShowIcons(true)}
 | 
			
		||||
            onMouseLeave={() => setShowIcons(false)}
 | 
			
		||||
          >
 | 
			
		||||
            <BlockIcon
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                blockUserFunc(blogPost.user)
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          </BlockIconContainer>
 | 
			
		||||
        </Tooltip>
 | 
			
		||||
      </IconsBox>
 | 
			
		||||
 | 
			
		||||
      <Dialog
 | 
			
		||||
        open={isOpenAlert}
 | 
			
		||||
        onClose={handleClose}
 | 
			
		||||
        aria-labelledby="alert-dialog-title"
 | 
			
		||||
        aria-describedby="alert-dialog-description"
 | 
			
		||||
      >
 | 
			
		||||
        <DialogTitle id="alert-dialog-title">
 | 
			
		||||
          Invalid Content Structure
 | 
			
		||||
        </DialogTitle>
 | 
			
		||||
        <DialogContent>
 | 
			
		||||
          <DialogContentText id="alert-dialog-description">
 | 
			
		||||
            This post seems to contain an invalid content structure. Click
 | 
			
		||||
            continue to proceed
 | 
			
		||||
          </DialogContentText>
 | 
			
		||||
        </DialogContent>
 | 
			
		||||
        <DialogActions>
 | 
			
		||||
          <Button onClick={handleClose}>Close</Button>
 | 
			
		||||
          <Button onClick={onClick} autoFocus>
 | 
			
		||||
            Continue
 | 
			
		||||
          </Button>
 | 
			
		||||
        </DialogActions>
 | 
			
		||||
      </Dialog>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default BlogPostPreview
 | 
			
		||||
							
								
								
									
										7
									
								
								src/pages/CreateEditProfile/CreatEditProfile.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
 | 
			
		||||
export const CreatEditProfile = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <div>CreatEditProfile</div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								src/pages/CreatePost/CreatePost-styles.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,14 @@
 | 
			
		||||
import { styled } from '@mui/system'
 | 
			
		||||
 | 
			
		||||
import { Button } from '@mui/material'
 | 
			
		||||
 | 
			
		||||
export const BuilderButton = styled(Button)(({ theme }) => ({
 | 
			
		||||
  backgroundColor: theme.palette.primary.light,
 | 
			
		||||
  color: theme.palette.text.primary,
 | 
			
		||||
  fontFamily: 'Arial',
 | 
			
		||||
  transition: "all 0.3s ease-in-out",
 | 
			
		||||
  "&:hover": {
 | 
			
		||||
    cursor: "pointer",
 | 
			
		||||
    filter: "brightness(0.9)"
 | 
			
		||||
  }
 | 
			
		||||
}));
 | 
			
		||||
							
								
								
									
										194
									
								
								src/pages/CreatePost/CreatePost.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,194 @@
 | 
			
		||||
import { Box, Button, Typography } from '@mui/material'
 | 
			
		||||
import React, { useMemo, useState } from 'react'
 | 
			
		||||
import { ReusableModal } from '../../components/modals/ReusableModal'
 | 
			
		||||
import { CreatePostBuilder } from './CreatePostBuilder'
 | 
			
		||||
import { CreatePostMinimal } from './CreatePostMinimal'
 | 
			
		||||
import HandymanRoundedIcon from '@mui/icons-material/HandymanRounded'
 | 
			
		||||
import HourglassFullRoundedIcon from '@mui/icons-material/HourglassFullRounded'
 | 
			
		||||
import { display } from '@mui/system'
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux'
 | 
			
		||||
import { setIsLoadingGlobal } from '../../state/features/globalSlice'
 | 
			
		||||
import { useParams } from 'react-router-dom'
 | 
			
		||||
import { checkStructure } from '../../utils/checkStructure'
 | 
			
		||||
import { RootState } from '../../state/store'
 | 
			
		||||
import {
 | 
			
		||||
  addPrefix,
 | 
			
		||||
  buildIdentifierFromCreateTitleIdAndId
 | 
			
		||||
} from '../../utils/blogIdformats'
 | 
			
		||||
import { Tipping } from '../../components/common/Tipping/Tipping'
 | 
			
		||||
type EditorType = 'minimal' | 'builder'
 | 
			
		||||
interface CreatePostProps {
 | 
			
		||||
  mode?: string
 | 
			
		||||
}
 | 
			
		||||
export const CreatePost = ({ mode }: CreatePostProps) => {
 | 
			
		||||
  const { user: username, postId, blog } = useParams()
 | 
			
		||||
  const fullPostId = useMemo(() => {
 | 
			
		||||
    if (!blog || !postId || mode !== 'edit') return ''
 | 
			
		||||
    const formBlogId = addPrefix(blog)
 | 
			
		||||
    const formPostId = buildIdentifierFromCreateTitleIdAndId(formBlogId, postId)
 | 
			
		||||
    return formPostId
 | 
			
		||||
  }, [blog, postId, mode])
 | 
			
		||||
  const { user } = useSelector((state: RootState) => state.auth)
 | 
			
		||||
 | 
			
		||||
  const [toggleEditorType, setToggleEditorType] = useState<EditorType | null>(
 | 
			
		||||
    null
 | 
			
		||||
  )
 | 
			
		||||
  const [blogContentForEdit, setBlogContentForEdit] = useState<any>(null)
 | 
			
		||||
  const [blogMetadataForEdit, setBlogMetadataForEdit] = useState<any>(null)
 | 
			
		||||
  const [editType, setEditType] = useState<EditorType | null>(null)
 | 
			
		||||
  const [isOpen, setIsOpen] = useState<boolean>(false)
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    if (!toggleEditorType && mode !== 'edit') {
 | 
			
		||||
      setIsOpen(true)
 | 
			
		||||
    }
 | 
			
		||||
  }, [setIsOpen, toggleEditorType])
 | 
			
		||||
 | 
			
		||||
  const switchType = () => {
 | 
			
		||||
    setIsOpen(true)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getBlogPost = React.useCallback(async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      dispatch(setIsLoadingGlobal(true))
 | 
			
		||||
      const url = `/arbitrary/BLOG_POST/${username}/${fullPostId}`
 | 
			
		||||
      const response = await fetch(url, {
 | 
			
		||||
        method: 'GET',
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'application/json'
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      const responseData = await response.json()
 | 
			
		||||
      if (checkStructure(responseData)) {
 | 
			
		||||
        // setNewPostContent(responseData.postContent)
 | 
			
		||||
        // setTitle(responseData?.title || '')
 | 
			
		||||
        // setBlogInfo(responseData)
 | 
			
		||||
        const blogType = responseData?.layoutGeneralSettings?.blogPostType
 | 
			
		||||
 | 
			
		||||
        if (blogType) {
 | 
			
		||||
          setEditType(blogType)
 | 
			
		||||
          setBlogContentForEdit(responseData)
 | 
			
		||||
        }
 | 
			
		||||
        //TODO - NAME SHOULD BE EXACT
 | 
			
		||||
        // const url2 = `/arbitrary/resources/search?mode=ALL&service=BLOG_POST&identifier=${fullPostId}&exactMatchNames=${username}&limit=1&includemetadata=true`
 | 
			
		||||
        const url2 = `/arbitrary/resources?service=BLOG_POST&identifier=${fullPostId}&name=${username}&limit=1&includemetadata=true`
 | 
			
		||||
 | 
			
		||||
        const responseBlogs = await fetch(url2, {
 | 
			
		||||
          method: 'GET',
 | 
			
		||||
          headers: {
 | 
			
		||||
            'Content-Type': 'application/json'
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        const dataMetadata = await responseBlogs.json()
 | 
			
		||||
        if (dataMetadata && dataMetadata.length > 0) {
 | 
			
		||||
          setBlogMetadataForEdit(dataMetadata[0])
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
    } finally {
 | 
			
		||||
      dispatch(setIsLoadingGlobal(false))
 | 
			
		||||
    }
 | 
			
		||||
  }, [username, fullPostId])
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    if (mode === 'edit') {
 | 
			
		||||
      getBlogPost()
 | 
			
		||||
    }
 | 
			
		||||
  }, [mode])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {/* {toggleEditorType === 'minimal' && (
 | 
			
		||||
        <Button onClick={() => switchType()}>Switch to Builder</Button>
 | 
			
		||||
      )}
 | 
			
		||||
      {toggleEditorType === 'builder' && (
 | 
			
		||||
        <Button onClick={() => switchType()}>Switch to Minimal</Button>
 | 
			
		||||
      )} */}
 | 
			
		||||
      {isOpen && (
 | 
			
		||||
        <ReusableModal
 | 
			
		||||
          open={isOpen}
 | 
			
		||||
          customStyles={{
 | 
			
		||||
            maxWidth: '500px'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {toggleEditorType && (
 | 
			
		||||
            <Typography>
 | 
			
		||||
              Switching editor type will delete your current progress
 | 
			
		||||
            </Typography>
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          <Box
 | 
			
		||||
            sx={{
 | 
			
		||||
              display: 'flex',
 | 
			
		||||
              justifyContent: 'center',
 | 
			
		||||
              alignItems: 'center',
 | 
			
		||||
              gap: 2
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Box
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                setToggleEditorType('minimal')
 | 
			
		||||
                setIsOpen(false)
 | 
			
		||||
              }}
 | 
			
		||||
              sx={{
 | 
			
		||||
                display: 'flex',
 | 
			
		||||
                flexDirection: 'column',
 | 
			
		||||
                justifyContent: 'center',
 | 
			
		||||
                alignItems: 'center',
 | 
			
		||||
                padding: '20px',
 | 
			
		||||
                borderRadius: '6px',
 | 
			
		||||
                border: '1px solid',
 | 
			
		||||
                cursor: 'pointer'
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Typography>Minimal Editor</Typography>
 | 
			
		||||
              <HourglassFullRoundedIcon />
 | 
			
		||||
            </Box>
 | 
			
		||||
            <Box
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                setToggleEditorType('builder')
 | 
			
		||||
                setIsOpen(false)
 | 
			
		||||
              }}
 | 
			
		||||
              sx={{
 | 
			
		||||
                display: 'flex',
 | 
			
		||||
                flexDirection: 'column',
 | 
			
		||||
                justifyContent: 'center',
 | 
			
		||||
                alignItems: 'center',
 | 
			
		||||
                padding: '20px',
 | 
			
		||||
                borderRadius: '6px',
 | 
			
		||||
                border: '1px solid',
 | 
			
		||||
                cursor: 'pointer'
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Typography>Builder Editor</Typography>
 | 
			
		||||
              <HandymanRoundedIcon />
 | 
			
		||||
            </Box>
 | 
			
		||||
          </Box>
 | 
			
		||||
          <Button onClick={() => setIsOpen(false)}>Close</Button>
 | 
			
		||||
        </ReusableModal>
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      {toggleEditorType === 'minimal' && (
 | 
			
		||||
        <CreatePostMinimal switchType={switchType} />
 | 
			
		||||
      )}
 | 
			
		||||
      {toggleEditorType === 'builder' && (
 | 
			
		||||
        <CreatePostBuilder switchType={switchType} />
 | 
			
		||||
      )}
 | 
			
		||||
      {mode === 'edit' && editType === 'minimal' && (
 | 
			
		||||
        <CreatePostMinimal
 | 
			
		||||
          blogContentForEdit={blogContentForEdit}
 | 
			
		||||
          postIdForEdit={fullPostId}
 | 
			
		||||
          blogMetadataForEdit={blogMetadataForEdit}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      {mode === 'edit' && editType === 'builder' && (
 | 
			
		||||
        <CreatePostBuilder
 | 
			
		||||
          blogContentForEdit={blogContentForEdit}
 | 
			
		||||
          postIdForEdit={fullPostId}
 | 
			
		||||
          blogMetadataForEdit={blogMetadataForEdit}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1409
									
								
								src/pages/CreatePost/CreatePostBuilder.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										1390
									
								
								src/pages/CreatePost/CreatePostMinimal.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										261
									
								
								src/pages/CreatePost/components/Navbar/NavbarBuilder.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,261 @@
 | 
			
		||||
import React, { useCallback, useEffect } from 'react'
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Box,
 | 
			
		||||
  Typography,
 | 
			
		||||
  Toolbar,
 | 
			
		||||
  AppBar,
 | 
			
		||||
  Select,
 | 
			
		||||
  InputLabel,
 | 
			
		||||
  FormControl,
 | 
			
		||||
  MenuItem,
 | 
			
		||||
  TextField,
 | 
			
		||||
  SelectChangeEvent,
 | 
			
		||||
  OutlinedInput,
 | 
			
		||||
  List,
 | 
			
		||||
  ListItem,
 | 
			
		||||
  useTheme
 | 
			
		||||
} from '@mui/material'
 | 
			
		||||
import { styled } from '@mui/system'
 | 
			
		||||
import { useSelector } from 'react-redux'
 | 
			
		||||
import { RootState } from '../../../../state/store'
 | 
			
		||||
import ShortUniqueId from 'short-unique-id'
 | 
			
		||||
import DeleteIcon from '@mui/icons-material/Delete'
 | 
			
		||||
import { CustomIcon } from '../../../../components/common/CustomIcon'
 | 
			
		||||
 | 
			
		||||
const uid = new ShortUniqueId()
 | 
			
		||||
interface INavbar {
 | 
			
		||||
  saveNav: (navMenu: any, navbarConfig: any) => void
 | 
			
		||||
  removeNav: () => void
 | 
			
		||||
  close: () => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Navbar = ({ saveNav, removeNav, close }: INavbar) => {
 | 
			
		||||
  const { user } = useSelector((state: RootState) => state.auth)
 | 
			
		||||
  const { currentBlog } = useSelector((state: RootState) => state.global)
 | 
			
		||||
  const theme = useTheme()
 | 
			
		||||
  const [navTitle, setNavTitle] = React.useState<string>('')
 | 
			
		||||
  const [blogPostOption, setBlogPostOption] = React.useState<any | null>(null)
 | 
			
		||||
  const [options, setOptions] = React.useState<any>([])
 | 
			
		||||
  const [navItems, setNavItems] = React.useState<any>([])
 | 
			
		||||
  const handleOptionChange = (event: SelectChangeEvent<string>) => {
 | 
			
		||||
    const optionId = event.target.value
 | 
			
		||||
    const selectedOption = options.find((option: any) => option.id === optionId)
 | 
			
		||||
    setBlogPostOption(selectedOption || null)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (currentBlog && currentBlog?.navbarConfig) {
 | 
			
		||||
      const { navItems } = currentBlog.navbarConfig
 | 
			
		||||
      if (!navItems || !Array.isArray(navItems)) return
 | 
			
		||||
 | 
			
		||||
      setNavItems(navItems)
 | 
			
		||||
    }
 | 
			
		||||
  }, [currentBlog])
 | 
			
		||||
 | 
			
		||||
  const getOptions = useCallback(async () => {
 | 
			
		||||
    if (!user || !currentBlog) return
 | 
			
		||||
    const name = user?.name
 | 
			
		||||
    const blog = currentBlog?.blogId
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      //TODO - NAME SHOULD BE EXACT
 | 
			
		||||
      const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_POST&query=${blog}-post-&exactmatchnames=true&name=${name}&includemetadata=true&reverse=true&limit=0`
 | 
			
		||||
      const response = await fetch(url, {
 | 
			
		||||
        method: 'GET',
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'application/json'
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      const responseData = await response.json()
 | 
			
		||||
      const formatOptions = responseData.map((option: any) => {
 | 
			
		||||
        return {
 | 
			
		||||
          id: option.identifier,
 | 
			
		||||
          name: option?.metadata.title
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      setOptions(formatOptions)
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
  }, [])
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    getOptions()
 | 
			
		||||
  }, [getOptions])
 | 
			
		||||
  const addToNav = () => {
 | 
			
		||||
    if (!navTitle || !blogPostOption) return
 | 
			
		||||
    setNavItems((prev: any) => [
 | 
			
		||||
      ...prev,
 | 
			
		||||
      {
 | 
			
		||||
        id: uid(),
 | 
			
		||||
        name: navTitle,
 | 
			
		||||
        postId: blogPostOption.id,
 | 
			
		||||
        postName: blogPostOption.name
 | 
			
		||||
      }
 | 
			
		||||
    ])
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleSaveNav = () => {
 | 
			
		||||
    if (!currentBlog) return
 | 
			
		||||
    saveNav(navItems, currentBlog?.navbarConfig || {})
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Box
 | 
			
		||||
        sx={{
 | 
			
		||||
          display: 'flex',
 | 
			
		||||
          alignItems: 'center',
 | 
			
		||||
          gap: 1
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Box
 | 
			
		||||
          sx={{
 | 
			
		||||
            display: 'flex',
 | 
			
		||||
            alignItems: 'center',
 | 
			
		||||
            gap: 1,
 | 
			
		||||
            flexWrap: 'wrap'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Box>
 | 
			
		||||
            <TextField
 | 
			
		||||
              label="Nav Item name"
 | 
			
		||||
              variant="outlined"
 | 
			
		||||
              fullWidth
 | 
			
		||||
              value={navTitle}
 | 
			
		||||
              onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
 | 
			
		||||
                setNavTitle(e.target.value)
 | 
			
		||||
              }
 | 
			
		||||
              inputProps={{ maxLength: 40 }}
 | 
			
		||||
              sx={{
 | 
			
		||||
                marginBottom: 2,
 | 
			
		||||
                backgroundColor: theme.palette.primary.light,
 | 
			
		||||
                color: theme.palette.text.primary,
 | 
			
		||||
                border: `1px solid ${theme.palette.text.primary}`
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          </Box>
 | 
			
		||||
          <Box>
 | 
			
		||||
            <FormControl
 | 
			
		||||
              fullWidth
 | 
			
		||||
              sx={{
 | 
			
		||||
                marginBottom: 2,
 | 
			
		||||
                width: '150px',
 | 
			
		||||
                backgroundColor: theme.palette.primary.light,
 | 
			
		||||
                color: theme.palette.text.primary,
 | 
			
		||||
                border: `1px solid ${theme.palette.text.primary}`
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <InputLabel sx={{ color: theme.palette.text.primary }} id="Post">
 | 
			
		||||
                Select a Post
 | 
			
		||||
              </InputLabel>
 | 
			
		||||
              <Select
 | 
			
		||||
                labelId="Post"
 | 
			
		||||
                input={<OutlinedInput label="Select a Post" />}
 | 
			
		||||
                value={blogPostOption?.id || ''}
 | 
			
		||||
                onChange={handleOptionChange}
 | 
			
		||||
                MenuProps={{
 | 
			
		||||
                  sx: {
 | 
			
		||||
                    maxHeight: '300px' // Adjust this value to set the max height,
 | 
			
		||||
                  }
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                {options.map((option: any) => (
 | 
			
		||||
                  <MenuItem
 | 
			
		||||
                    sx={{ color: theme.palette.text.primary }}
 | 
			
		||||
                    key={option.id}
 | 
			
		||||
                    value={option.id}
 | 
			
		||||
                  >
 | 
			
		||||
                    {option.name}
 | 
			
		||||
                  </MenuItem>
 | 
			
		||||
                ))}
 | 
			
		||||
              </Select>
 | 
			
		||||
            </FormControl>
 | 
			
		||||
          </Box>
 | 
			
		||||
        </Box>
 | 
			
		||||
        <Box>
 | 
			
		||||
          <Button
 | 
			
		||||
            sx={{
 | 
			
		||||
              backgroundColor: theme.palette.primary.light,
 | 
			
		||||
              color: theme.palette.text.primary,
 | 
			
		||||
              border: `1px solid ${theme.palette.text.primary}`
 | 
			
		||||
            }}
 | 
			
		||||
            onClick={addToNav}
 | 
			
		||||
          >
 | 
			
		||||
            Add
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Box>
 | 
			
		||||
      </Box>
 | 
			
		||||
 | 
			
		||||
      <Box>
 | 
			
		||||
        <List
 | 
			
		||||
          sx={{
 | 
			
		||||
            width: '100%',
 | 
			
		||||
            display: 'flex',
 | 
			
		||||
            flexDirection: 'column',
 | 
			
		||||
            flex: '1',
 | 
			
		||||
            overflow: 'auto'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          {navItems.map((navItem: any) => (
 | 
			
		||||
            <ListItem
 | 
			
		||||
              key={navItem.id}
 | 
			
		||||
              sx={{
 | 
			
		||||
                display: 'flex',
 | 
			
		||||
                alignItems: 'center',
 | 
			
		||||
                gap: '10px'
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Typography
 | 
			
		||||
                sx={{
 | 
			
		||||
                  fontWeight: 'bold'
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                {navItem.name}
 | 
			
		||||
              </Typography>{' '}
 | 
			
		||||
              <Typography>{navItem.postName}</Typography>{' '}
 | 
			
		||||
              <CustomIcon
 | 
			
		||||
                component={DeleteIcon}
 | 
			
		||||
                onClick={() =>
 | 
			
		||||
                  setNavItems((prev: any) =>
 | 
			
		||||
                    prev.filter((item: any) => item.id !== navItem.id)
 | 
			
		||||
                  )
 | 
			
		||||
                }
 | 
			
		||||
              />
 | 
			
		||||
            </ListItem>
 | 
			
		||||
          ))}
 | 
			
		||||
        </List>
 | 
			
		||||
      </Box>
 | 
			
		||||
      <Button
 | 
			
		||||
        sx={{
 | 
			
		||||
          backgroundColor: theme.palette.primary.dark,
 | 
			
		||||
          color: theme.palette.text.primary,
 | 
			
		||||
          fontFamily: 'Arial'
 | 
			
		||||
        }}
 | 
			
		||||
        onClick={handleSaveNav}
 | 
			
		||||
      >
 | 
			
		||||
        Save Navbar
 | 
			
		||||
      </Button>
 | 
			
		||||
      <Button
 | 
			
		||||
        sx={{
 | 
			
		||||
          backgroundColor: theme.palette.primary.dark,
 | 
			
		||||
          color: theme.palette.text.primary,
 | 
			
		||||
          fontFamily: 'Arial'
 | 
			
		||||
        }}
 | 
			
		||||
        onClick={removeNav}
 | 
			
		||||
      >
 | 
			
		||||
        Remove Navbar
 | 
			
		||||
      </Button>
 | 
			
		||||
      <Button
 | 
			
		||||
        sx={{
 | 
			
		||||
          backgroundColor: theme.palette.primary.dark,
 | 
			
		||||
          color: theme.palette.text.primary,
 | 
			
		||||
          fontFamily: 'Arial'
 | 
			
		||||
        }}
 | 
			
		||||
        onClick={close}
 | 
			
		||||
      >
 | 
			
		||||
        Close
 | 
			
		||||
      </Button>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										157
									
								
								src/pages/CreatePost/components/Toolbar/EditorToolbar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,157 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import TextFieldsIcon from '@mui/icons-material/TextFields'
 | 
			
		||||
import Slider from '@mui/material/Slider'
 | 
			
		||||
import { AudioPanel } from '../../../../components/common/AudioPanel'
 | 
			
		||||
import { Box, Toolbar, AppBar, useTheme } from '@mui/material'
 | 
			
		||||
import { styled } from '@mui/system'
 | 
			
		||||
import ImageUploader from '../../../../components/common/ImageUploader'
 | 
			
		||||
import AddPhotoAlternateIcon from '@mui/icons-material/AddPhotoAlternate'
 | 
			
		||||
import { VideoPanel } from '../../../../components/common/VideoPanel'
 | 
			
		||||
import MenuOpenIcon from '@mui/icons-material/MenuOpen'
 | 
			
		||||
import HandymanRoundedIcon from '@mui/icons-material/HandymanRounded'
 | 
			
		||||
import Tooltip from '@mui/material/Tooltip'
 | 
			
		||||
import { FilePanel } from '../../../../components/common/FilePanel'
 | 
			
		||||
 | 
			
		||||
const CustomToolbar = styled(Toolbar)({
 | 
			
		||||
  display: 'flex',
 | 
			
		||||
  justifyContent: 'space-between',
 | 
			
		||||
  alignItems: 'center'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const CustomAppBar = styled(AppBar)(({ theme }) => ({
 | 
			
		||||
  backgroundColor:
 | 
			
		||||
    theme.palette.mode === 'light'
 | 
			
		||||
      ? theme.palette.background.default
 | 
			
		||||
      : '#19191b'
 | 
			
		||||
}))
 | 
			
		||||
 | 
			
		||||
interface IEditorToolbar {
 | 
			
		||||
  setIsOpenAddTextModal: (val: boolean) => void
 | 
			
		||||
  addImage: (base64: string) => void
 | 
			
		||||
  onSelectVideo: (video: any) => void
 | 
			
		||||
  onSelectAudio: (audio: any) => void
 | 
			
		||||
  onSelectFile: (file: any) => void
 | 
			
		||||
  paddingValue: number
 | 
			
		||||
  onChangePadding: (padding: number) => void
 | 
			
		||||
  isMinimal?: boolean
 | 
			
		||||
  addNav?: () => void
 | 
			
		||||
  switchType?: () => void
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const EditorToolbar = ({
 | 
			
		||||
  setIsOpenAddTextModal,
 | 
			
		||||
  addImage,
 | 
			
		||||
  onSelectVideo,
 | 
			
		||||
  onSelectAudio,
 | 
			
		||||
  onSelectFile,
 | 
			
		||||
  paddingValue,
 | 
			
		||||
  onChangePadding,
 | 
			
		||||
  isMinimal = false,
 | 
			
		||||
  addNav,
 | 
			
		||||
  switchType
 | 
			
		||||
}: IEditorToolbar) => {
 | 
			
		||||
  const theme = useTheme()
 | 
			
		||||
  return (
 | 
			
		||||
    <CustomAppBar position="sticky">
 | 
			
		||||
      <CustomToolbar variant="dense">
 | 
			
		||||
        <Box
 | 
			
		||||
          sx={{
 | 
			
		||||
            display: 'flex',
 | 
			
		||||
            justifyContent: 'space-between',
 | 
			
		||||
            width: '100%',
 | 
			
		||||
            flexWrap: 'wrap',
 | 
			
		||||
            alignItems: 'center'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Box
 | 
			
		||||
            sx={{
 | 
			
		||||
              display: 'flex',
 | 
			
		||||
              gap: '10px'
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Tooltip title="Add Text" arrow>
 | 
			
		||||
              <TextFieldsIcon
 | 
			
		||||
                onClick={() => setIsOpenAddTextModal(true)}
 | 
			
		||||
                sx={{
 | 
			
		||||
                  cursor: 'pointer',
 | 
			
		||||
                  width: 'auto',
 | 
			
		||||
                  height: '30px'
 | 
			
		||||
                }}
 | 
			
		||||
              />
 | 
			
		||||
            </Tooltip>
 | 
			
		||||
 | 
			
		||||
            <ImageUploader onPick={addImage}>
 | 
			
		||||
              <Tooltip title="Add an image" arrow>
 | 
			
		||||
                <AddPhotoAlternateIcon
 | 
			
		||||
                  sx={{
 | 
			
		||||
                    cursor: 'pointer',
 | 
			
		||||
                    width: 'auto',
 | 
			
		||||
                    height: '30px'
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
              </Tooltip>
 | 
			
		||||
            </ImageUploader>
 | 
			
		||||
 | 
			
		||||
            <VideoPanel onSelect={onSelectVideo} />
 | 
			
		||||
 | 
			
		||||
            <AudioPanel onSelect={onSelectAudio} />
 | 
			
		||||
            <FilePanel onSelect={onSelectFile} />
 | 
			
		||||
          </Box>
 | 
			
		||||
          <Box
 | 
			
		||||
            sx={{
 | 
			
		||||
              display: 'flex',
 | 
			
		||||
              gap: '10px'
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            {!isMinimal && (
 | 
			
		||||
              <Tooltip title="Adjust padding between elements" arrow>
 | 
			
		||||
                <Box>
 | 
			
		||||
                  <Slider
 | 
			
		||||
                    size="small"
 | 
			
		||||
                    value={paddingValue}
 | 
			
		||||
                    onChange={(event: any) =>
 | 
			
		||||
                      onChangePadding(event.target.value)
 | 
			
		||||
                    }
 | 
			
		||||
                    defaultValue={5}
 | 
			
		||||
                    aria-label="Default"
 | 
			
		||||
                    valueLabelDisplay="auto"
 | 
			
		||||
                    min={0}
 | 
			
		||||
                    max={40}
 | 
			
		||||
                    sx={{
 | 
			
		||||
                      color: theme.palette.text.primary,
 | 
			
		||||
                      width: '100px'
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                </Box>
 | 
			
		||||
              </Tooltip>
 | 
			
		||||
            )}
 | 
			
		||||
            {!isMinimal && (
 | 
			
		||||
              <Tooltip title="Manage your custom navbar links" arrow>
 | 
			
		||||
                <MenuOpenIcon
 | 
			
		||||
                  onClick={addNav}
 | 
			
		||||
                  sx={{
 | 
			
		||||
                    cursor: 'pointer',
 | 
			
		||||
                    width: 'auto',
 | 
			
		||||
                    height: '30px'
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
              </Tooltip>
 | 
			
		||||
            )}
 | 
			
		||||
            {switchType && (
 | 
			
		||||
              <Tooltip title="Switch editor type" arrow>
 | 
			
		||||
                <HandymanRoundedIcon
 | 
			
		||||
                  onClick={switchType}
 | 
			
		||||
                  sx={{
 | 
			
		||||
                    cursor: 'pointer',
 | 
			
		||||
                    width: 'auto',
 | 
			
		||||
                    height: '30px'
 | 
			
		||||
                  }}
 | 
			
		||||
                />
 | 
			
		||||
              </Tooltip>
 | 
			
		||||
            )}
 | 
			
		||||
          </Box>
 | 
			
		||||
        </Box>
 | 
			
		||||
      </CustomToolbar>
 | 
			
		||||
    </CustomAppBar>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										562
									
								
								src/pages/EditPost/EditPost.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,562 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { useParams } from 'react-router-dom'
 | 
			
		||||
import BlogEditor from '../../components/editor/BlogEditor'
 | 
			
		||||
import ShortUniqueId from 'short-unique-id'
 | 
			
		||||
import { Button, TextField } from '@mui/material'
 | 
			
		||||
import ReadOnlySlate from '../../components/editor/ReadOnlySlate'
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux'
 | 
			
		||||
import { RootState } from '../../state/store'
 | 
			
		||||
import { Box } from '@mui/material'
 | 
			
		||||
import ImageUploader from '../../components/common/ImageUploader'
 | 
			
		||||
import AddPhotoAlternateIcon from '@mui/icons-material/AddPhotoAlternate'
 | 
			
		||||
import { checkStructure } from '../../utils/checkStructure'
 | 
			
		||||
import { BlogContent } from '../../interfaces/interfaces'
 | 
			
		||||
import PostAddIcon from '@mui/icons-material/PostAdd'
 | 
			
		||||
import RemoveCircleIcon from '@mui/icons-material/RemoveCircle'
 | 
			
		||||
import EditIcon from '@mui/icons-material/Edit'
 | 
			
		||||
import { createEditor, Descendant, Editor, Transforms } from 'slate'
 | 
			
		||||
import { styled } from '@mui/system'
 | 
			
		||||
import { setIsLoadingGlobal } from '../../state/features/globalSlice'
 | 
			
		||||
import { extractTextFromSlate } from '../../utils/extractTextFromSlate'
 | 
			
		||||
import { VideoContent } from '../../components/common/VideoContent'
 | 
			
		||||
import { VideoPanel } from '../../components/common/VideoPanel'
 | 
			
		||||
 | 
			
		||||
const initialValue: Descendant[] = [
 | 
			
		||||
  {
 | 
			
		||||
    type: 'paragraph',
 | 
			
		||||
    children: [
 | 
			
		||||
      { text: "Start writing your blog post... Don't forget to add a title :)" }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
const BlogTitleInput = styled(TextField)(({ theme }) => ({
 | 
			
		||||
  '& .MuiInputBase-input': {
 | 
			
		||||
    fontSize: '28px',
 | 
			
		||||
    height: '28px',
 | 
			
		||||
    '&::placeholder': {
 | 
			
		||||
      fontSize: '28px',
 | 
			
		||||
      color: theme.palette.text.secondary
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  '& .MuiInputLabel-root': {
 | 
			
		||||
    fontSize: '28px'
 | 
			
		||||
  }
 | 
			
		||||
}))
 | 
			
		||||
 | 
			
		||||
interface IaddVideo {
 | 
			
		||||
  name: string
 | 
			
		||||
  identifier: string
 | 
			
		||||
  service: string
 | 
			
		||||
  title: string
 | 
			
		||||
  description: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const uid = new ShortUniqueId()
 | 
			
		||||
export const EditPost = () => {
 | 
			
		||||
  const { user: username, postId } = useParams()
 | 
			
		||||
 | 
			
		||||
  const { user } = useSelector((state: RootState) => state.auth)
 | 
			
		||||
 | 
			
		||||
  const [newPostContent, setNewPostContent] = React.useState<any[]>([])
 | 
			
		||||
  const [blogInfo, setBlogInfo] = React.useState<BlogContent | null>(null)
 | 
			
		||||
  const [editingSection, setEditingSection] = React.useState<any>(null)
 | 
			
		||||
  const [value, setValue] = React.useState(initialValue)
 | 
			
		||||
  const [value2, setValue2] = React.useState(initialValue)
 | 
			
		||||
  const [title, setTitle] = React.useState('')
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
  const addPostSection = React.useCallback((content: any) => {
 | 
			
		||||
    const section = {
 | 
			
		||||
      type: 'editor',
 | 
			
		||||
      version: 1,
 | 
			
		||||
      content,
 | 
			
		||||
      id: uid()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setNewPostContent((prev) => [...prev, section])
 | 
			
		||||
  }, [])
 | 
			
		||||
  const editPostSection = React.useCallback(
 | 
			
		||||
    (content: any, section: any) => {
 | 
			
		||||
      const findSectionIndex = newPostContent.findIndex(
 | 
			
		||||
        (s) => s.id === section.id
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      if (findSectionIndex !== -1) {
 | 
			
		||||
        const copyNewPostContent = [...newPostContent]
 | 
			
		||||
        copyNewPostContent[findSectionIndex] = {
 | 
			
		||||
          ...section,
 | 
			
		||||
          content
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setNewPostContent(copyNewPostContent)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      setEditingSection(null)
 | 
			
		||||
    },
 | 
			
		||||
    [newPostContent]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  function objectToBase64(obj: any) {
 | 
			
		||||
    // Step 1: Convert the object to a JSON string
 | 
			
		||||
    const jsonString = JSON.stringify(obj)
 | 
			
		||||
 | 
			
		||||
    // Step 2: Create a Blob from the JSON string
 | 
			
		||||
    const blob = new Blob([jsonString], { type: 'application/json' })
 | 
			
		||||
 | 
			
		||||
    // Step 3: Create a FileReader to read the Blob as a base64-encoded string
 | 
			
		||||
    return new Promise<string>((resolve, reject) => {
 | 
			
		||||
      const reader = new FileReader()
 | 
			
		||||
      reader.onloadend = () => {
 | 
			
		||||
        if (typeof reader.result === 'string') {
 | 
			
		||||
          // Remove 'data:application/json;base64,' prefix
 | 
			
		||||
          const base64 = reader.result.replace(
 | 
			
		||||
            'data:application/json;base64,',
 | 
			
		||||
            ''
 | 
			
		||||
          )
 | 
			
		||||
          resolve(base64)
 | 
			
		||||
        } else {
 | 
			
		||||
          reject(
 | 
			
		||||
            new Error('Failed to read the Blob as a base64-encoded string')
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      reader.onerror = () => {
 | 
			
		||||
        reject(reader.error)
 | 
			
		||||
      }
 | 
			
		||||
      reader.readAsDataURL(blob)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const addImage = (base64: string) => {
 | 
			
		||||
    const section = {
 | 
			
		||||
      type: 'image',
 | 
			
		||||
      version: 1,
 | 
			
		||||
      content: {
 | 
			
		||||
        image: base64,
 | 
			
		||||
        caption: ''
 | 
			
		||||
      },
 | 
			
		||||
      id: uid()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setNewPostContent((prev) => [...prev, section])
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function getNameInfo(address: string) {
 | 
			
		||||
    const response = await fetch('/names/address/' + address)
 | 
			
		||||
    const nameData = await response.json()
 | 
			
		||||
 | 
			
		||||
    if (nameData?.length > 0) {
 | 
			
		||||
      return nameData[0].name
 | 
			
		||||
    } else {
 | 
			
		||||
      return ''
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function publishQDNResource() {
 | 
			
		||||
    let address
 | 
			
		||||
    let name
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      if (!user || !user.address) return
 | 
			
		||||
      address = user.address
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
    if (!address) return
 | 
			
		||||
    try {
 | 
			
		||||
      name = await getNameInfo(address)
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
    if (!name) return
 | 
			
		||||
    if (!blogInfo) return
 | 
			
		||||
    try {
 | 
			
		||||
      const postObject = {
 | 
			
		||||
        ...blogInfo,
 | 
			
		||||
        title,
 | 
			
		||||
        postContent: newPostContent
 | 
			
		||||
      }
 | 
			
		||||
      const blogPostToBase64 = await objectToBase64(postObject)
 | 
			
		||||
      let description = ''
 | 
			
		||||
      const findText = newPostContent.find((data) => data?.type === 'editor')
 | 
			
		||||
      if (findText && findText.content) {
 | 
			
		||||
        description = extractTextFromSlate(findText?.content)
 | 
			
		||||
        description = description.slice(0, 180)
 | 
			
		||||
      }
 | 
			
		||||
      const resourceResponse = await qortalRequest({
 | 
			
		||||
        action: 'PUBLISH_QDN_RESOURCE',
 | 
			
		||||
        name: name,
 | 
			
		||||
        service: 'BLOG_POST',
 | 
			
		||||
        data64: blogPostToBase64,
 | 
			
		||||
        title: title,
 | 
			
		||||
        description: description,
 | 
			
		||||
        category: 'TECHNOLOGY',
 | 
			
		||||
        tags: ['tag1', 'tag2', 'tag3', 'tag4', 'tag5'],
 | 
			
		||||
        metaData: 'description=destriptontest&category=catTest',
 | 
			
		||||
        identifier: postId
 | 
			
		||||
      })
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error(error)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const addSection = () => {
 | 
			
		||||
    addPostSection(value2)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const getBlogPost = React.useCallback(async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      dispatch(setIsLoadingGlobal(true))
 | 
			
		||||
      const url = `/arbitrary/BLOG_POST/${username}/${postId}`
 | 
			
		||||
      const response = await fetch(url, {
 | 
			
		||||
        method: 'GET',
 | 
			
		||||
        headers: {
 | 
			
		||||
          'Content-Type': 'application/json'
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      const responseData = await response.json()
 | 
			
		||||
      if (checkStructure(responseData)) {
 | 
			
		||||
        setNewPostContent(responseData.postContent)
 | 
			
		||||
        setTitle(responseData?.title || '')
 | 
			
		||||
        setBlogInfo(responseData)
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
    } finally {
 | 
			
		||||
      dispatch(setIsLoadingGlobal(false))
 | 
			
		||||
    }
 | 
			
		||||
  }, [user, postId])
 | 
			
		||||
  React.useEffect(() => {
 | 
			
		||||
    getBlogPost()
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  const editSection = (section: any) => {
 | 
			
		||||
    setEditingSection(section)
 | 
			
		||||
    setValue(section.content)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const removeSection = (section: any) => {
 | 
			
		||||
    const newContent = newPostContent.filter((s) => s.id !== section.id)
 | 
			
		||||
    setNewPostContent(newContent)
 | 
			
		||||
  }
 | 
			
		||||
  const editImage = (base64: string, section: any) => {
 | 
			
		||||
    const newSection = {
 | 
			
		||||
      ...section,
 | 
			
		||||
      content: {
 | 
			
		||||
        image: base64,
 | 
			
		||||
        caption: section.content.caption
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    const findSectionIndex = newPostContent.findIndex(
 | 
			
		||||
      (s) => s.id === section.id
 | 
			
		||||
    )
 | 
			
		||||
    if (findSectionIndex !== -1) {
 | 
			
		||||
      const copyNewPostContent = [...newPostContent]
 | 
			
		||||
      copyNewPostContent[findSectionIndex] = newSection
 | 
			
		||||
 | 
			
		||||
      setNewPostContent(copyNewPostContent)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const editVideo = (
 | 
			
		||||
    { name, identifier, service, description, title }: IaddVideo,
 | 
			
		||||
    section: any
 | 
			
		||||
  ) => {
 | 
			
		||||
    const newSection = {
 | 
			
		||||
      ...section,
 | 
			
		||||
      content: {
 | 
			
		||||
        name: name,
 | 
			
		||||
        identifier: identifier,
 | 
			
		||||
        service: service,
 | 
			
		||||
        description,
 | 
			
		||||
        title
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    const findSectionIndex = newPostContent.findIndex(
 | 
			
		||||
      (s) => s.id === section.id
 | 
			
		||||
    )
 | 
			
		||||
    if (findSectionIndex !== -1) {
 | 
			
		||||
      const copyNewPostContent = [...newPostContent]
 | 
			
		||||
      copyNewPostContent[findSectionIndex] = newSection
 | 
			
		||||
 | 
			
		||||
      setNewPostContent(copyNewPostContent)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
      sx={{
 | 
			
		||||
        display: 'flex',
 | 
			
		||||
        alignItems: 'center',
 | 
			
		||||
        flexDirection: 'column'
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Box
 | 
			
		||||
        sx={{
 | 
			
		||||
          maxWidth: '700px',
 | 
			
		||||
          margin: '15px',
 | 
			
		||||
          width: '100%'
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <BlogTitleInput
 | 
			
		||||
          id="modal-title-input"
 | 
			
		||||
          value={title}
 | 
			
		||||
          onChange={(e) => setTitle(e.target.value)}
 | 
			
		||||
          fullWidth
 | 
			
		||||
          placeholder="Title"
 | 
			
		||||
          variant="filled"
 | 
			
		||||
          multiline
 | 
			
		||||
          maxRows={2}
 | 
			
		||||
          InputLabelProps={{ shrink: false }}
 | 
			
		||||
        />
 | 
			
		||||
        {newPostContent.map((section: any) => {
 | 
			
		||||
          if (section.type === 'editor') {
 | 
			
		||||
            return (
 | 
			
		||||
              <Box key={section.id}>
 | 
			
		||||
                {editingSection && editingSection.id === section.id ? (
 | 
			
		||||
                  <BlogEditor
 | 
			
		||||
                    editPostSection={editPostSection}
 | 
			
		||||
                    defaultValue={section.content}
 | 
			
		||||
                    section={section}
 | 
			
		||||
                    value={value}
 | 
			
		||||
                    setValue={setValue}
 | 
			
		||||
                  />
 | 
			
		||||
                ) : (
 | 
			
		||||
                  <Box
 | 
			
		||||
                    sx={{
 | 
			
		||||
                      position: 'relative'
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <ReadOnlySlate key={section.id} content={section.content} />
 | 
			
		||||
                    <Box
 | 
			
		||||
                      sx={{
 | 
			
		||||
                        position: 'absolute',
 | 
			
		||||
                        right: '5px',
 | 
			
		||||
                        zIndex: 5,
 | 
			
		||||
                        top: '50%',
 | 
			
		||||
                        transform: 'translateY(-50%)',
 | 
			
		||||
                        display: 'flex',
 | 
			
		||||
                        // flexDirection: 'column',
 | 
			
		||||
                        gap: 2,
 | 
			
		||||
                        background: 'white',
 | 
			
		||||
                        padding: '5px',
 | 
			
		||||
                        borderRadius: '5px'
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      <RemoveCircleIcon
 | 
			
		||||
                        onClick={() => removeSection(section)}
 | 
			
		||||
                        sx={{
 | 
			
		||||
                          cursor: 'pointer'
 | 
			
		||||
                        }}
 | 
			
		||||
                      />
 | 
			
		||||
                      <EditIcon
 | 
			
		||||
                        onClick={() => editSection(section)}
 | 
			
		||||
                        sx={{
 | 
			
		||||
                          cursor: 'pointer'
 | 
			
		||||
                        }}
 | 
			
		||||
                      />
 | 
			
		||||
                    </Box>
 | 
			
		||||
                  </Box>
 | 
			
		||||
                )}
 | 
			
		||||
                {editingSection && editingSection.id === section.id ? (
 | 
			
		||||
                  <Box
 | 
			
		||||
                    sx={{
 | 
			
		||||
                      display: 'flex',
 | 
			
		||||
                      width: '100%',
 | 
			
		||||
                      justifyContent: 'flex-end'
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <Button onClick={() => setEditingSection(null)}>
 | 
			
		||||
                      Close
 | 
			
		||||
                    </Button>
 | 
			
		||||
                  </Box>
 | 
			
		||||
                ) : (
 | 
			
		||||
                  <></>
 | 
			
		||||
                )}
 | 
			
		||||
              </Box>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          if (section.type === 'image') {
 | 
			
		||||
            return (
 | 
			
		||||
              <Box key={section.id}>
 | 
			
		||||
                {editingSection && editingSection.id === section.id ? (
 | 
			
		||||
                  <ImageUploader
 | 
			
		||||
                    onPick={(base64) => editImage(base64, section)}
 | 
			
		||||
                  >
 | 
			
		||||
                    Add Image
 | 
			
		||||
                    <AddPhotoAlternateIcon />
 | 
			
		||||
                  </ImageUploader>
 | 
			
		||||
                ) : (
 | 
			
		||||
                  <Box
 | 
			
		||||
                    sx={{
 | 
			
		||||
                      position: 'relative'
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <img
 | 
			
		||||
                      src={section.content.image}
 | 
			
		||||
                      className="post-image"
 | 
			
		||||
                      style={{
 | 
			
		||||
                        marginTop: '20px'
 | 
			
		||||
                      }}
 | 
			
		||||
                    />
 | 
			
		||||
                    <Box
 | 
			
		||||
                      sx={{
 | 
			
		||||
                        position: 'absolute',
 | 
			
		||||
                        right: '5px',
 | 
			
		||||
                        zIndex: 5,
 | 
			
		||||
                        top: '50%',
 | 
			
		||||
                        transform: 'translateY(-50%)',
 | 
			
		||||
                        display: 'flex',
 | 
			
		||||
                        flexDirection: 'column',
 | 
			
		||||
                        gap: 2,
 | 
			
		||||
                        background: 'white',
 | 
			
		||||
                        padding: '5px',
 | 
			
		||||
                        borderRadius: '5px'
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      <RemoveCircleIcon
 | 
			
		||||
                        onClick={() => removeSection(section)}
 | 
			
		||||
                        sx={{
 | 
			
		||||
                          cursor: 'pointer'
 | 
			
		||||
                        }}
 | 
			
		||||
                      />
 | 
			
		||||
                      <ImageUploader
 | 
			
		||||
                        onPick={(base64) => editImage(base64, section)}
 | 
			
		||||
                      >
 | 
			
		||||
                        <EditIcon
 | 
			
		||||
                          sx={{
 | 
			
		||||
                            cursor: 'pointer'
 | 
			
		||||
                          }}
 | 
			
		||||
                        />
 | 
			
		||||
                      </ImageUploader>
 | 
			
		||||
                    </Box>
 | 
			
		||||
                  </Box>
 | 
			
		||||
                )}
 | 
			
		||||
                {editingSection && editingSection.id === section.id ? (
 | 
			
		||||
                  <Button onClick={() => setEditingSection(null)}>Close</Button>
 | 
			
		||||
                ) : (
 | 
			
		||||
                  <></>
 | 
			
		||||
                )}
 | 
			
		||||
              </Box>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
          if (section.type === 'video') {
 | 
			
		||||
            return (
 | 
			
		||||
              <Box key={section.id}>
 | 
			
		||||
                {editingSection && editingSection.id === section.id ? (
 | 
			
		||||
                  <VideoPanel
 | 
			
		||||
                    width="24px"
 | 
			
		||||
                    height="24px"
 | 
			
		||||
                    onSelect={(video) =>
 | 
			
		||||
                      editVideo(
 | 
			
		||||
                        {
 | 
			
		||||
                          name: video.name,
 | 
			
		||||
                          identifier: video.identifier,
 | 
			
		||||
                          service: video.service,
 | 
			
		||||
                          title: video?.metadata?.title,
 | 
			
		||||
                          description: video?.metadata?.description
 | 
			
		||||
                        },
 | 
			
		||||
                        section
 | 
			
		||||
                      )
 | 
			
		||||
                    }
 | 
			
		||||
                  />
 | 
			
		||||
                ) : (
 | 
			
		||||
                  <Box
 | 
			
		||||
                    sx={{
 | 
			
		||||
                      position: 'relative'
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <VideoContent
 | 
			
		||||
                      title={section.content?.title}
 | 
			
		||||
                      description={section.content?.description}
 | 
			
		||||
                    />
 | 
			
		||||
                    <Box
 | 
			
		||||
                      sx={{
 | 
			
		||||
                        position: 'absolute',
 | 
			
		||||
                        right: '5px',
 | 
			
		||||
                        zIndex: 5,
 | 
			
		||||
                        top: '50%',
 | 
			
		||||
                        transform: 'translateY(-50%)',
 | 
			
		||||
                        display: 'flex',
 | 
			
		||||
                        flexDirection: 'column',
 | 
			
		||||
                        gap: 2,
 | 
			
		||||
                        background: 'white',
 | 
			
		||||
                        padding: '5px',
 | 
			
		||||
                        borderRadius: '5px'
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      <RemoveCircleIcon
 | 
			
		||||
                        onClick={() => removeSection(section)}
 | 
			
		||||
                        sx={{
 | 
			
		||||
                          cursor: 'pointer'
 | 
			
		||||
                        }}
 | 
			
		||||
                      />
 | 
			
		||||
                      <VideoPanel
 | 
			
		||||
                        width="24px"
 | 
			
		||||
                        height="24px"
 | 
			
		||||
                        onSelect={(video) =>
 | 
			
		||||
                          editVideo(
 | 
			
		||||
                            {
 | 
			
		||||
                              name: video.name,
 | 
			
		||||
                              identifier: video.identifier,
 | 
			
		||||
                              service: video.service,
 | 
			
		||||
                              title: video?.metadata?.title,
 | 
			
		||||
                              description: video?.metadata?.description
 | 
			
		||||
                            },
 | 
			
		||||
                            section
 | 
			
		||||
                          )
 | 
			
		||||
                        }
 | 
			
		||||
                      />
 | 
			
		||||
                    </Box>
 | 
			
		||||
                  </Box>
 | 
			
		||||
                )}
 | 
			
		||||
                {editingSection && editingSection.id === section.id ? (
 | 
			
		||||
                  <Button onClick={() => setEditingSection(null)}>Close</Button>
 | 
			
		||||
                ) : (
 | 
			
		||||
                  <></>
 | 
			
		||||
                )}
 | 
			
		||||
              </Box>
 | 
			
		||||
            )
 | 
			
		||||
          }
 | 
			
		||||
        })}
 | 
			
		||||
 | 
			
		||||
        <BlogEditor
 | 
			
		||||
          addPostSection={addPostSection}
 | 
			
		||||
          value={value2}
 | 
			
		||||
          setValue={setValue2}
 | 
			
		||||
        />
 | 
			
		||||
        <Box
 | 
			
		||||
          sx={{
 | 
			
		||||
            display: 'flex'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <PostAddIcon
 | 
			
		||||
            onClick={addSection}
 | 
			
		||||
            sx={{
 | 
			
		||||
              cursor: 'pointer',
 | 
			
		||||
              width: '50px',
 | 
			
		||||
              height: '50px'
 | 
			
		||||
            }}
 | 
			
		||||
          />
 | 
			
		||||
          <ImageUploader onPick={addImage}>
 | 
			
		||||
            <AddPhotoAlternateIcon
 | 
			
		||||
              sx={{
 | 
			
		||||
                cursor: 'pointer',
 | 
			
		||||
                width: '50px',
 | 
			
		||||
                height: '50px'
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          </ImageUploader>
 | 
			
		||||
        </Box>
 | 
			
		||||
      </Box>
 | 
			
		||||
      <Box
 | 
			
		||||
        sx={{
 | 
			
		||||
          position: 'fixed',
 | 
			
		||||
          bottom: '30px',
 | 
			
		||||
          right: '30px',
 | 
			
		||||
          zIndex: 15,
 | 
			
		||||
          background: 'deepskyblue',
 | 
			
		||||
          padding: '10px',
 | 
			
		||||
          borderRadius: '5px'
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Button onClick={publishQDNResource}>PUBLISH UPDATE</Button>
 | 
			
		||||
      </Box>
 | 
			
		||||
    </Box>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								src/pages/Home/Home.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,7 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
 | 
			
		||||
export const Home = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <div>Home</div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										279
									
								
								src/pages/Mail/AliasMail.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,279 @@
 | 
			
		||||
import React, {
 | 
			
		||||
  FC,
 | 
			
		||||
  useCallback,
 | 
			
		||||
  useEffect,
 | 
			
		||||
  useMemo,
 | 
			
		||||
  useRef,
 | 
			
		||||
  useState
 | 
			
		||||
} from 'react'
 | 
			
		||||
import { useNavigate } from 'react-router-dom'
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux'
 | 
			
		||||
import { RootState } from '../../state/store'
 | 
			
		||||
import EditIcon from '@mui/icons-material/Edit'
 | 
			
		||||
import { Box, Button, Input, Typography, useTheme } from '@mui/material'
 | 
			
		||||
import { useFetchPosts } from '../../hooks/useFetchPosts'
 | 
			
		||||
import LazyLoad from '../../components/common/LazyLoad'
 | 
			
		||||
import { removePrefix } from '../../utils/blogIdformats'
 | 
			
		||||
import { NewMessage } from './NewMessage'
 | 
			
		||||
import Tabs from '@mui/material/Tabs'
 | 
			
		||||
import Tab from '@mui/material/Tab'
 | 
			
		||||
import { useFetchMail } from '../../hooks/useFetchMail'
 | 
			
		||||
import { ShowMessage } from './ShowMessage'
 | 
			
		||||
import { fetchAndEvaluateMail } from '../../utils/fetchMail'
 | 
			
		||||
import { addToHashMapMail } from '../../state/features/mailSlice'
 | 
			
		||||
import {
 | 
			
		||||
  setIsLoadingGlobal,
 | 
			
		||||
  setUserAvatarHash
 | 
			
		||||
} from '../../state/features/globalSlice'
 | 
			
		||||
import SimpleTable from './MailTable'
 | 
			
		||||
import { MAIL_SERVICE_TYPE } from '../../constants/mail'
 | 
			
		||||
import { BlogPost } from '../../state/features/blogSlice'
 | 
			
		||||
 | 
			
		||||
interface AliasMailProps {
 | 
			
		||||
  value: string
 | 
			
		||||
}
 | 
			
		||||
export const AliasMail = ({ value }: AliasMailProps) => {
 | 
			
		||||
  const theme = useTheme()
 | 
			
		||||
  const { user } = useSelector((state: RootState) => state.auth)
 | 
			
		||||
  const [isOpen, setIsOpen] = useState<boolean>(false)
 | 
			
		||||
  const [message, setMessage] = useState<any>(null)
 | 
			
		||||
  const [replyTo, setReplyTo] = useState<any>(null)
 | 
			
		||||
  const [valueTab, setValueTab] = React.useState(0)
 | 
			
		||||
  const [aliasValue, setAliasValue] = useState('')
 | 
			
		||||
  const [alias, setAlias] = useState<string[]>([])
 | 
			
		||||
  const hashMapPosts = useSelector(
 | 
			
		||||
    (state: RootState) => state.blog.hashMapPosts
 | 
			
		||||
  )
 | 
			
		||||
  const [mailMessages, setMailMessages] = useState<any[]>([])
 | 
			
		||||
  const hashMapMailMessages = useSelector(
 | 
			
		||||
    (state: RootState) => state.mail.hashMapMailMessages
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const fullMailMessages = useMemo(() => {
 | 
			
		||||
    return mailMessages.map((msg) => {
 | 
			
		||||
      let message = msg
 | 
			
		||||
      const existingMessage = hashMapMailMessages[msg.id]
 | 
			
		||||
      if (existingMessage) {
 | 
			
		||||
        message = existingMessage
 | 
			
		||||
      }
 | 
			
		||||
      return message
 | 
			
		||||
    })
 | 
			
		||||
  }, [mailMessages, hashMapMailMessages])
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
 | 
			
		||||
  const getAvatar = async (user: string) => {
 | 
			
		||||
    try {
 | 
			
		||||
      let url = await qortalRequest({
 | 
			
		||||
        action: 'GET_QDN_RESOURCE_URL',
 | 
			
		||||
        name: user,
 | 
			
		||||
        service: 'THUMBNAIL',
 | 
			
		||||
        identifier: 'qortal_avatar'
 | 
			
		||||
      })
 | 
			
		||||
      dispatch(
 | 
			
		||||
        setUserAvatarHash({
 | 
			
		||||
          name: user,
 | 
			
		||||
          url
 | 
			
		||||
        })
 | 
			
		||||
      )
 | 
			
		||||
    } catch (error) {}
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const checkNewMessages = React.useCallback(
 | 
			
		||||
    async (recipientName: string, recipientAddress: string) => {
 | 
			
		||||
      try {
 | 
			
		||||
        const query = `qortal_qmail_${value}_mail`
 | 
			
		||||
        const url = `/arbitrary/resources/search?mode=ALL&service=${MAIL_SERVICE_TYPE}&query=${query}&limit=20&includemetadata=true&reverse=true&excludeblocked=true`
 | 
			
		||||
        const response = await fetch(url, {
 | 
			
		||||
          method: 'GET',
 | 
			
		||||
          headers: {
 | 
			
		||||
            'Content-Type': 'application/json'
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        const responseData = await response.json()
 | 
			
		||||
        const latestPost = mailMessages[0]
 | 
			
		||||
        if (!latestPost) return
 | 
			
		||||
        const findPost = responseData?.findIndex(
 | 
			
		||||
          (item: any) => item?.identifier === latestPost?.id
 | 
			
		||||
        )
 | 
			
		||||
        if (findPost === -1) {
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
        const newArray = responseData.slice(0, findPost)
 | 
			
		||||
        const structureData = newArray.map((post: any): BlogPost => {
 | 
			
		||||
          return {
 | 
			
		||||
            title: post?.metadata?.title,
 | 
			
		||||
            category: post?.metadata?.category,
 | 
			
		||||
            categoryName: post?.metadata?.categoryName,
 | 
			
		||||
            tags: post?.metadata?.tags || [],
 | 
			
		||||
            description: post?.metadata?.description,
 | 
			
		||||
            createdAt: post?.created,
 | 
			
		||||
            updated: post?.updated,
 | 
			
		||||
            user: post.name,
 | 
			
		||||
            id: post.identifier
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        setMailMessages((prev) => {
 | 
			
		||||
          const updatedMessages = [...prev]
 | 
			
		||||
 | 
			
		||||
          structureData.forEach((newMessage: any) => {
 | 
			
		||||
            const existingIndex = updatedMessages.findIndex(
 | 
			
		||||
              (prevMessage) => prevMessage.id === newMessage.id
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            if (existingIndex !== -1) {
 | 
			
		||||
              // Replace existing message
 | 
			
		||||
              updatedMessages[existingIndex] = newMessage
 | 
			
		||||
            } else {
 | 
			
		||||
              // Add new message
 | 
			
		||||
              updatedMessages.unshift(newMessage)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
 | 
			
		||||
          return updatedMessages
 | 
			
		||||
        })
 | 
			
		||||
        return
 | 
			
		||||
      } catch (error) {}
 | 
			
		||||
    },
 | 
			
		||||
    [mailMessages]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const getMailMessages = React.useCallback(
 | 
			
		||||
    async (recipientName: string, recipientAddress: string) => {
 | 
			
		||||
      try {
 | 
			
		||||
        const offset = mailMessages.length
 | 
			
		||||
 | 
			
		||||
        dispatch(setIsLoadingGlobal(true))
 | 
			
		||||
        const query = `qortal_qmail_${value}_mail`
 | 
			
		||||
        const url = `/arbitrary/resources/search?mode=ALL&service=${MAIL_SERVICE_TYPE}&query=${query}&limit=20&includemetadata=true&offset=${offset}&reverse=true&excludeblocked=true`
 | 
			
		||||
        const response = await fetch(url, {
 | 
			
		||||
          method: 'GET',
 | 
			
		||||
          headers: {
 | 
			
		||||
            'Content-Type': 'application/json'
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        const responseData = await response.json()
 | 
			
		||||
        const structureData = responseData.map((post: any): BlogPost => {
 | 
			
		||||
          return {
 | 
			
		||||
            title: post?.metadata?.title,
 | 
			
		||||
            category: post?.metadata?.category,
 | 
			
		||||
            categoryName: post?.metadata?.categoryName,
 | 
			
		||||
            tags: post?.metadata?.tags || [],
 | 
			
		||||
            description: post?.metadata?.description,
 | 
			
		||||
            createdAt: post?.created,
 | 
			
		||||
            updated: post?.updated,
 | 
			
		||||
            user: post.name,
 | 
			
		||||
            id: post.identifier
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        setMailMessages((prev) => {
 | 
			
		||||
          const updatedMessages = [...prev]
 | 
			
		||||
 | 
			
		||||
          structureData.forEach((newMessage: any) => {
 | 
			
		||||
            const existingIndex = updatedMessages.findIndex(
 | 
			
		||||
              (prevMessage) => prevMessage.id === newMessage.id
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            if (existingIndex !== -1) {
 | 
			
		||||
              // Replace existing message
 | 
			
		||||
              updatedMessages[existingIndex] = newMessage
 | 
			
		||||
            } else {
 | 
			
		||||
              // Add new message
 | 
			
		||||
              updatedMessages.push(newMessage)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
 | 
			
		||||
          return updatedMessages
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        for (const content of structureData) {
 | 
			
		||||
          if (content.user && content.id) {
 | 
			
		||||
            getAvatar(content.user)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
      } finally {
 | 
			
		||||
        dispatch(setIsLoadingGlobal(false))
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [mailMessages, hashMapMailMessages]
 | 
			
		||||
  )
 | 
			
		||||
  const getMessages = React.useCallback(async () => {
 | 
			
		||||
    if (!user?.name || !user?.address) return
 | 
			
		||||
    await getMailMessages(user.name, user.address)
 | 
			
		||||
  }, [getMailMessages, user])
 | 
			
		||||
 | 
			
		||||
  const interval = useRef<any>(null)
 | 
			
		||||
 | 
			
		||||
  const checkNewMessagesFunc = useCallback(() => {
 | 
			
		||||
    if (!user?.name || !user?.address) return
 | 
			
		||||
    let isCalling = false
 | 
			
		||||
    interval.current = setInterval(async () => {
 | 
			
		||||
      if (isCalling || !user?.name || !user?.address) return
 | 
			
		||||
      isCalling = true
 | 
			
		||||
      const res = await checkNewMessages(user?.name, user.address)
 | 
			
		||||
      isCalling = false
 | 
			
		||||
    }, 30000)
 | 
			
		||||
  }, [checkNewMessages, user])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    checkNewMessagesFunc()
 | 
			
		||||
    return () => {
 | 
			
		||||
      if (interval?.current) {
 | 
			
		||||
        clearInterval(interval.current)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, [checkNewMessagesFunc])
 | 
			
		||||
 | 
			
		||||
  const openMessage = async (
 | 
			
		||||
    user: string,
 | 
			
		||||
    messageIdentifier: string,
 | 
			
		||||
    content: any
 | 
			
		||||
  ) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const existingMessage = hashMapMailMessages[messageIdentifier]
 | 
			
		||||
      if (existingMessage) {
 | 
			
		||||
        setMessage(existingMessage)
 | 
			
		||||
      }
 | 
			
		||||
      dispatch(setIsLoadingGlobal(true))
 | 
			
		||||
      const res = await fetchAndEvaluateMail({
 | 
			
		||||
        user,
 | 
			
		||||
        messageIdentifier,
 | 
			
		||||
        content,
 | 
			
		||||
        otherUser: user
 | 
			
		||||
      })
 | 
			
		||||
      setMessage(res)
 | 
			
		||||
      dispatch(addToHashMapMail(res))
 | 
			
		||||
      setIsOpen(true)
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
    } finally {
 | 
			
		||||
      dispatch(setIsLoadingGlobal(false))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const firstMount = useRef(false)
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (user?.name && !firstMount.current) {
 | 
			
		||||
      getMessages()
 | 
			
		||||
      firstMount.current = true
 | 
			
		||||
    }
 | 
			
		||||
  }, [user])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <NewMessage replyTo={replyTo} setReplyTo={setReplyTo} alias={value} />
 | 
			
		||||
      <ShowMessage
 | 
			
		||||
        isOpen={isOpen}
 | 
			
		||||
        setIsOpen={setIsOpen}
 | 
			
		||||
        message={message}
 | 
			
		||||
        setReplyTo={setReplyTo}
 | 
			
		||||
        alias={value}
 | 
			
		||||
      />
 | 
			
		||||
      <SimpleTable
 | 
			
		||||
        openMessage={openMessage}
 | 
			
		||||
        data={fullMailMessages}
 | 
			
		||||
      ></SimpleTable>
 | 
			
		||||
      <LazyLoad onLoadMore={getMessages}></LazyLoad>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										342
									
								
								src/pages/Mail/Mail.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,342 @@
 | 
			
		||||
import React, {
 | 
			
		||||
  FC,
 | 
			
		||||
  useCallback,
 | 
			
		||||
  useEffect,
 | 
			
		||||
  useMemo,
 | 
			
		||||
  useRef,
 | 
			
		||||
  useState
 | 
			
		||||
} from 'react'
 | 
			
		||||
import { useNavigate } from 'react-router-dom'
 | 
			
		||||
import { useDispatch, useSelector } from 'react-redux'
 | 
			
		||||
import { RootState } from '../../state/store'
 | 
			
		||||
import EditIcon from '@mui/icons-material/Edit'
 | 
			
		||||
import CloseIcon from '@mui/icons-material/Close'
 | 
			
		||||
import {
 | 
			
		||||
  Box,
 | 
			
		||||
  Button,
 | 
			
		||||
  Input,
 | 
			
		||||
  Typography,
 | 
			
		||||
  useTheme,
 | 
			
		||||
  IconButton
 | 
			
		||||
} from '@mui/material'
 | 
			
		||||
import { useFetchPosts } from '../../hooks/useFetchPosts'
 | 
			
		||||
import LazyLoad from '../../components/common/LazyLoad'
 | 
			
		||||
import { removePrefix } from '../../utils/blogIdformats'
 | 
			
		||||
import { NewMessage } from './NewMessage'
 | 
			
		||||
import Tabs from '@mui/material/Tabs'
 | 
			
		||||
import Tab from '@mui/material/Tab'
 | 
			
		||||
import { useFetchMail } from '../../hooks/useFetchMail'
 | 
			
		||||
import { ShowMessage } from './ShowMessage'
 | 
			
		||||
import { fetchAndEvaluateMail } from '../../utils/fetchMail'
 | 
			
		||||
import { addToHashMapMail } from '../../state/features/mailSlice'
 | 
			
		||||
import { setIsLoadingGlobal } from '../../state/features/globalSlice'
 | 
			
		||||
import SimpleTable from './MailTable'
 | 
			
		||||
import { AliasMail } from './AliasMail'
 | 
			
		||||
 | 
			
		||||
export const Mail = () => {
 | 
			
		||||
  const theme = useTheme()
 | 
			
		||||
  const { user } = useSelector((state: RootState) => state.auth)
 | 
			
		||||
  const [isOpen, setIsOpen] = useState<boolean>(false)
 | 
			
		||||
  const [message, setMessage] = useState<any>(null)
 | 
			
		||||
  const [replyTo, setReplyTo] = useState<any>(null)
 | 
			
		||||
  const [valueTab, setValueTab] = React.useState(0)
 | 
			
		||||
  const [aliasValue, setAliasValue] = useState('')
 | 
			
		||||
  const [alias, setAlias] = useState<string[]>([])
 | 
			
		||||
  const hashMapPosts = useSelector(
 | 
			
		||||
    (state: RootState) => state.blog.hashMapPosts
 | 
			
		||||
  )
 | 
			
		||||
  const hashMapMailMessages = useSelector(
 | 
			
		||||
    (state: RootState) => state.mail.hashMapMailMessages
 | 
			
		||||
  )
 | 
			
		||||
  const mailMessages = useSelector(
 | 
			
		||||
    (state: RootState) => state.mail.mailMessages
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const fullMailMessages = useMemo(() => {
 | 
			
		||||
    return mailMessages.map((msg) => {
 | 
			
		||||
      let message = msg
 | 
			
		||||
      const existingMessage = hashMapMailMessages[msg.id]
 | 
			
		||||
      if (existingMessage) {
 | 
			
		||||
        message = existingMessage
 | 
			
		||||
      }
 | 
			
		||||
      return message
 | 
			
		||||
    })
 | 
			
		||||
  }, [mailMessages, hashMapMailMessages])
 | 
			
		||||
  const dispatch = useDispatch()
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
 | 
			
		||||
  const { getMailMessages, checkNewMessages } = useFetchMail()
 | 
			
		||||
  const getMessages = React.useCallback(async () => {
 | 
			
		||||
    if (!user?.name || !user?.address) return
 | 
			
		||||
    await getMailMessages(user.name, user.address)
 | 
			
		||||
  }, [getMailMessages, user])
 | 
			
		||||
 | 
			
		||||
  const interval = useRef<any>(null)
 | 
			
		||||
 | 
			
		||||
  const checkNewMessagesFunc = useCallback(() => {
 | 
			
		||||
    if (!user?.name || !user?.address) return
 | 
			
		||||
    let isCalling = false
 | 
			
		||||
    interval.current = setInterval(async () => {
 | 
			
		||||
      if (isCalling || !user?.name || !user?.address) return
 | 
			
		||||
      isCalling = true
 | 
			
		||||
      const res = await checkNewMessages(user?.name, user.address)
 | 
			
		||||
      isCalling = false
 | 
			
		||||
    }, 30000)
 | 
			
		||||
  }, [checkNewMessages, user])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    checkNewMessagesFunc()
 | 
			
		||||
    return () => {
 | 
			
		||||
      if (interval?.current) {
 | 
			
		||||
        clearInterval(interval.current)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, [checkNewMessagesFunc])
 | 
			
		||||
 | 
			
		||||
  const openMessage = async (
 | 
			
		||||
    user: string,
 | 
			
		||||
    messageIdentifier: string,
 | 
			
		||||
    content: any
 | 
			
		||||
  ) => {
 | 
			
		||||
    try {
 | 
			
		||||
      const existingMessage = hashMapMailMessages[messageIdentifier]
 | 
			
		||||
      if (existingMessage) {
 | 
			
		||||
        setMessage(existingMessage)
 | 
			
		||||
      }
 | 
			
		||||
      dispatch(setIsLoadingGlobal(true))
 | 
			
		||||
      const res = await fetchAndEvaluateMail({
 | 
			
		||||
        user,
 | 
			
		||||
        messageIdentifier,
 | 
			
		||||
        content,
 | 
			
		||||
        otherUser: user
 | 
			
		||||
      })
 | 
			
		||||
      setMessage(res)
 | 
			
		||||
      dispatch(addToHashMapMail(res))
 | 
			
		||||
      setIsOpen(true)
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
    } finally {
 | 
			
		||||
      dispatch(setIsLoadingGlobal(false))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const firstMount = useRef(false)
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (user?.name && !firstMount.current) {
 | 
			
		||||
      getMessages()
 | 
			
		||||
      firstMount.current = true
 | 
			
		||||
    }
 | 
			
		||||
  }, [user])
 | 
			
		||||
 | 
			
		||||
  function a11yProps(index: number) {
 | 
			
		||||
    return {
 | 
			
		||||
      id: `mail-tabs-${index}`,
 | 
			
		||||
      'aria-controls': `mail-tabs-${index}`
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleChange = (event: React.SyntheticEvent, newValue: number) => {
 | 
			
		||||
    setValueTab(newValue)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function CustomTabLabel({ index, label }: any) {
 | 
			
		||||
    return (
 | 
			
		||||
      <div style={{ display: 'flex', alignItems: 'center' }}>
 | 
			
		||||
        <span>{label}</span>
 | 
			
		||||
        <IconButton
 | 
			
		||||
          edge="end"
 | 
			
		||||
          color="inherit"
 | 
			
		||||
          size="small"
 | 
			
		||||
          onClick={(event) => {
 | 
			
		||||
            setValueTab(0)
 | 
			
		||||
            const newList = [...alias]
 | 
			
		||||
 | 
			
		||||
            newList.splice(index, 1)
 | 
			
		||||
 | 
			
		||||
            setAlias(newList)
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <CloseIcon fontSize="inherit" />
 | 
			
		||||
        </IconButton>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Box
 | 
			
		||||
      sx={{
 | 
			
		||||
        display: 'flex',
 | 
			
		||||
        width: '100%',
 | 
			
		||||
        flexDirection: 'column',
 | 
			
		||||
        backgroundColor: 'background.paper'
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <Box
 | 
			
		||||
        sx={{
 | 
			
		||||
          borderBottom: 1,
 | 
			
		||||
          borderColor: 'divider',
 | 
			
		||||
          display: 'flex',
 | 
			
		||||
          width: '100%',
 | 
			
		||||
          alignItems: 'center',
 | 
			
		||||
          justifyContent: 'flex-start'
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Tabs
 | 
			
		||||
          value={valueTab}
 | 
			
		||||
          onChange={handleChange}
 | 
			
		||||
          aria-label="basic tabs example"
 | 
			
		||||
        >
 | 
			
		||||
          <Tab label={user?.name} {...a11yProps(0)} />
 | 
			
		||||
          {alias.map((alia, index) => {
 | 
			
		||||
            return (
 | 
			
		||||
              <Tab
 | 
			
		||||
                sx={{
 | 
			
		||||
                  '&.Mui-selected': {
 | 
			
		||||
                    color: theme.palette.text.primary,
 | 
			
		||||
                    fontWeight: theme.typography.fontWeightMedium
 | 
			
		||||
                  }
 | 
			
		||||
                }}
 | 
			
		||||
                key={alia}
 | 
			
		||||
                label={<CustomTabLabel index={index} label={alia} />}
 | 
			
		||||
                {...a11yProps(1 + index)}
 | 
			
		||||
              />
 | 
			
		||||
            )
 | 
			
		||||
          })}
 | 
			
		||||
        </Tabs>
 | 
			
		||||
        <Input
 | 
			
		||||
          id="standard-adornment-alias"
 | 
			
		||||
          onChange={(e) => {
 | 
			
		||||
            setAliasValue(e.target.value)
 | 
			
		||||
          }}
 | 
			
		||||
          value={aliasValue}
 | 
			
		||||
          placeholder="Type in alias"
 | 
			
		||||
          sx={{
 | 
			
		||||
            marginLeft: '20px',
 | 
			
		||||
            '&&:before': {
 | 
			
		||||
              borderBottom: 'none'
 | 
			
		||||
            },
 | 
			
		||||
            '&&:after': {
 | 
			
		||||
              borderBottom: 'none'
 | 
			
		||||
            },
 | 
			
		||||
            '&&:hover:before': {
 | 
			
		||||
              borderBottom: 'none'
 | 
			
		||||
            },
 | 
			
		||||
            '&&.Mui-focused:before': {
 | 
			
		||||
              borderBottom: 'none'
 | 
			
		||||
            },
 | 
			
		||||
            '&&.Mui-focused': {
 | 
			
		||||
              outline: 'none'
 | 
			
		||||
            },
 | 
			
		||||
            fontSize: '18px'
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
        <Button
 | 
			
		||||
          onClick={() => {
 | 
			
		||||
            setAlias((prev) => [...prev, aliasValue])
 | 
			
		||||
            setAliasValue('')
 | 
			
		||||
          }}
 | 
			
		||||
          variant="contained"
 | 
			
		||||
        >
 | 
			
		||||
          + alias
 | 
			
		||||
        </Button>
 | 
			
		||||
      </Box>
 | 
			
		||||
      <NewMessage replyTo={replyTo} setReplyTo={setReplyTo} />
 | 
			
		||||
      <ShowMessage
 | 
			
		||||
        isOpen={isOpen}
 | 
			
		||||
        setIsOpen={setIsOpen}
 | 
			
		||||
        message={message}
 | 
			
		||||
        setReplyTo={setReplyTo}
 | 
			
		||||
      />
 | 
			
		||||
      {/* {countNewPosts > 0 && (
 | 
			
		||||
        <Box
 | 
			
		||||
          sx={{
 | 
			
		||||
            display: 'flex',
 | 
			
		||||
            alignItems: 'center',
 | 
			
		||||
            justifyContent: 'center'
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Typography>
 | 
			
		||||
            {countNewPosts === 1
 | 
			
		||||
              ? `There is ${countNewPosts} new message`
 | 
			
		||||
              : `There are ${countNewPosts} new messages`}
 | 
			
		||||
          </Typography>
 | 
			
		||||
          <Button
 | 
			
		||||
            sx={{
 | 
			
		||||
              backgroundColor: theme.palette.primary.light,
 | 
			
		||||
              color: theme.palette.text.primary,
 | 
			
		||||
              fontFamily: 'Arial'
 | 
			
		||||
            }}
 | 
			
		||||
            onClick={getNewPosts}
 | 
			
		||||
          >
 | 
			
		||||
            Load new Posts
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Box>
 | 
			
		||||
      )} */}
 | 
			
		||||
      <TabPanel value={valueTab} index={0}>
 | 
			
		||||
        <SimpleTable
 | 
			
		||||
          openMessage={openMessage}
 | 
			
		||||
          data={fullMailMessages}
 | 
			
		||||
        ></SimpleTable>
 | 
			
		||||
        <LazyLoad onLoadMore={getMessages}></LazyLoad>
 | 
			
		||||
      </TabPanel>
 | 
			
		||||
      {alias.map((alia, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <TabPanel key={alia} value={valueTab} index={1 + index}>
 | 
			
		||||
            <AliasMail value={alia} />
 | 
			
		||||
          </TabPanel>
 | 
			
		||||
        )
 | 
			
		||||
      })}
 | 
			
		||||
 | 
			
		||||
      {/* <Box>
 | 
			
		||||
        {mailMessages.map((message, index) => {
 | 
			
		||||
          const existingMessage = hashMapMailMessages[message.id]
 | 
			
		||||
          let mailMessage = message
 | 
			
		||||
          if (existingMessage) {
 | 
			
		||||
            mailMessage = existingMessage
 | 
			
		||||
          }
 | 
			
		||||
          return (
 | 
			
		||||
            <Box
 | 
			
		||||
              sx={{
 | 
			
		||||
                display: 'flex',
 | 
			
		||||
                gap: 1,
 | 
			
		||||
                alignItems: 'center',
 | 
			
		||||
                width: 'auto',
 | 
			
		||||
                position: 'relative',
 | 
			
		||||
                ' @media (max-width: 450px)': {
 | 
			
		||||
                  width: '100%'
 | 
			
		||||
                }
 | 
			
		||||
              }}
 | 
			
		||||
              key={mailMessage.id}
 | 
			
		||||
            >
 | 
			
		||||
              hello
 | 
			
		||||
            </Box>
 | 
			
		||||
          )
 | 
			
		||||
        })}
 | 
			
		||||
      </Box> */}
 | 
			
		||||
    </Box>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface TabPanelProps {
 | 
			
		||||
  children?: React.ReactNode
 | 
			
		||||
  index: number
 | 
			
		||||
  value: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function TabPanel(props: TabPanelProps) {
 | 
			
		||||
  const { children, value, index, ...other } = props
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      role="tabpanel"
 | 
			
		||||
      hidden={value !== index}
 | 
			
		||||
      id={`mail-tabs-${index}`}
 | 
			
		||||
      aria-labelledby={`mail-tabs-${index}`}
 | 
			
		||||
      {...other}
 | 
			
		||||
      style={{
 | 
			
		||||
        width: '100%'
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {value === index && children}
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||