16
.eslintrc.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': 'warn',
|
||||||
|
'@typescript-eslint/no-explicit-any': "off"
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
25
.gitignore
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
*.zip
|
10
.prettierrc
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 80,
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"jsxBracketSameLine": false,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": true
|
||||||
|
}
|
13
index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Q-Share</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
7530
package-lock.json
generated
Normal file
48
package.json
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"name": "qsupport",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"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",
|
||||||
|
"compressorjs": "^1.2.1",
|
||||||
|
"dompurify": "^3.0.6",
|
||||||
|
"localforage": "^1.10.0",
|
||||||
|
"moment": "^2.29.4",
|
||||||
|
"prettier": "^3.2.4",
|
||||||
|
"quill-image-resize-module-react": "^3.0.0",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-dropzone": "^14.2.3",
|
||||||
|
"react-intersection-observer": "^9.4.3",
|
||||||
|
"react-quill": "^2.0.0",
|
||||||
|
"react-redux": "^8.0.5",
|
||||||
|
"react-rnd": "^10.4.1",
|
||||||
|
"react-router-dom": "^6.9.0",
|
||||||
|
"react-toastify": "^9.1.2",
|
||||||
|
"short-unique-id": "^4.4.4",
|
||||||
|
"ts-key-enum": "^2.0.12"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.0.28",
|
||||||
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.57.1",
|
||||||
|
"@typescript-eslint/parser": "^5.57.1",
|
||||||
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
|
"eslint": "^8.38.0",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.3.4",
|
||||||
|
"typescript": "^5.0.2",
|
||||||
|
"vite": "6.0.0-alpha.1"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
43
src/App.css
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
#root {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 6em;
|
||||||
|
padding: 1.5em;
|
||||||
|
will-change: filter;
|
||||||
|
transition: filter 300ms;
|
||||||
|
}
|
||||||
|
.logo:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
|
}
|
||||||
|
.logo.react:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
a:nth-of-type(2) .logo {
|
||||||
|
animation: logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
39
src/App.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { Route, Routes } from "react-router-dom";
|
||||||
|
import { ThemeProvider } from "@mui/material/styles";
|
||||||
|
import { CssBaseline } from "@mui/material";
|
||||||
|
import { darkTheme, lightTheme } from "./styles/theme";
|
||||||
|
import { store } from "./state/store";
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
import GlobalWrapper from "./wrappers/GlobalWrapper";
|
||||||
|
import Notification from "./components/common/Notification/Notification";
|
||||||
|
import { Home } from "./pages/Home/Home";
|
||||||
|
import { IssueContent } from "./pages/IssueContent/IssueContent.tsx";
|
||||||
|
import DownloadWrapper from "./wrappers/DownloadWrapper";
|
||||||
|
import { IndividualProfile } from "./pages/IndividualProfile/IndividualProfile";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
// const themeColor = window._qdnTheme
|
||||||
|
|
||||||
|
const [theme, setTheme] = useState("dark");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
|
<ThemeProvider theme={theme === "light" ? lightTheme : darkTheme}>
|
||||||
|
<Notification />
|
||||||
|
<DownloadWrapper>
|
||||||
|
<GlobalWrapper setTheme={(val: string) => setTheme(val)}>
|
||||||
|
<CssBaseline />
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Home />} />
|
||||||
|
<Route path="/share/:name/:id" element={<IssueContent />} />
|
||||||
|
<Route path="/channel/:name" element={<IndividualProfile />} />
|
||||||
|
</Routes>
|
||||||
|
</GlobalWrapper>
|
||||||
|
</DownloadWrapper>
|
||||||
|
</ThemeProvider>
|
||||||
|
</Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
BIN
src/assets/icons/ClosedIcon.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
src/assets/icons/CompleteIcon.png
Normal file
After Width: | Height: | Size: 132 KiB |
BIN
src/assets/icons/InProgressIcon.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
src/assets/icons/OpenIcon.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/icons/audio.webp
Normal file
After Width: | Height: | Size: 77 KiB |
BIN
src/assets/icons/book.webp
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
src/assets/icons/document.webp
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
src/assets/icons/gaming.webp
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
src/assets/icons/image.webp
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
src/assets/icons/media.webp
Normal file
After Width: | Height: | Size: 35 KiB |
BIN
src/assets/icons/software.webp
Normal file
After Width: | Height: | Size: 43 KiB |
BIN
src/assets/icons/unknown.webp
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
src/assets/icons/video.webp
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
src/assets/img/Q-SupportIcon.webp
Normal file
After Width: | Height: | Size: 39 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>
|
||||||
|
)
|
||||||
|
}
|
23
src/assets/svgs/CircleSVG.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { IconTypes } from "./IconTypes";
|
||||||
|
|
||||||
|
export const CircleSVG: React.FC<IconTypes> = ({
|
||||||
|
color,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
className,
|
||||||
|
onClickFunc,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
onClick={onClickFunc}
|
||||||
|
className={className}
|
||||||
|
fill={color}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
height={height}
|
||||||
|
viewBox="0 -960 960 960"
|
||||||
|
width={width}
|
||||||
|
>
|
||||||
|
<path d="m424-296 282-282-56-56-226 226-114-114-56 56 170 170Zm56 216q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
23
src/assets/svgs/DarkModeSVG.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { IconTypes } from './IconTypes'
|
||||||
|
|
||||||
|
export const DarkModeSVG: React.FC<IconTypes> = ({
|
||||||
|
color,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
className,
|
||||||
|
onClickFunc
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={className}
|
||||||
|
onClick={onClickFunc}
|
||||||
|
fill={color}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
height={height}
|
||||||
|
viewBox="0 96 960 960"
|
||||||
|
width={width}
|
||||||
|
>
|
||||||
|
<path d="M480 936q-150 0-255-105T120 576q0-150 105-255t255-105q8 0 17 .5t23 1.5q-36 32-56 79t-20 99q0 90 63 153t153 63q52 0 99-18.5t79-51.5q1 12 1.5 19.5t.5 14.5q0 150-105 255T480 936Zm0-60q109 0 190-67.5T771 650q-25 11-53.667 16.5Q688.667 672 660 672q-114.689 0-195.345-80.655Q384 510.689 384 396q0-24 5-51.5t18-62.5q-98 27-162.5 109.5T180 576q0 125 87.5 212.5T480 876Zm-4-297Z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
13
src/assets/svgs/DownloadedLight.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { IconTypes } from './IconTypes'
|
||||||
|
|
||||||
|
export const DownloadedLight: React.FC<IconTypes> = ({
|
||||||
|
color,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
className,
|
||||||
|
onClickFunc
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<svg className={className} xmlns="http://www.w3.org/2000/svg" height={height} viewBox="0 0 24 24" width={width} fill="#FFFFFF"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M5 18h14v2H5v-2zm4.6-2.7L5 10.7l2-1.9 2.6 2.6L17 4l2 2-9.4 9.3z"/></svg>
|
||||||
|
)
|
||||||
|
}
|
13
src/assets/svgs/DownloadingLight.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { IconTypes } from './IconTypes'
|
||||||
|
|
||||||
|
export const DownloadingLight: React.FC<IconTypes> = ({
|
||||||
|
color,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
className,
|
||||||
|
onClickFunc
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<svg className={className} xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height={height} viewBox="0 0 24 24" width={width} fill="#FFFFFF"><g><rect fill="none" /></g><g><g><path d="M18.32,4.26C16.84,3.05,15.01,2.25,13,2.05v2.02c1.46,0.18,2.79,0.76,3.9,1.62L18.32,4.26z M19.93,11h2.02 c-0.2-2.01-1-3.84-2.21-5.32L18.31,7.1C19.17,8.21,19.75,9.54,19.93,11z M18.31,16.9l1.43,1.43c1.21-1.48,2.01-3.32,2.21-5.32 h-2.02C19.75,14.46,19.17,15.79,18.31,16.9z M13,19.93v2.02c2.01-0.2,3.84-1,5.32-2.21l-1.43-1.43 C15.79,19.17,14.46,19.75,13,19.93z M13,12V7h-2v5H7l5,5l5-5H13z M11,19.93v2.02c-5.05-0.5-9-4.76-9-9.95s3.95-9.45,9-9.95v2.02 C7.05,4.56,4,7.92,4,12S7.05,19.44,11,19.93z"/></g></g></svg>
|
||||||
|
)
|
||||||
|
}
|
23
src/assets/svgs/EmptyCircleSVG.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { IconTypes } from "./IconTypes";
|
||||||
|
|
||||||
|
export const EmptyCircleSVG: React.FC<IconTypes> = ({
|
||||||
|
color,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
className,
|
||||||
|
onClickFunc,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
|
||||||
|
<svg onClick={onClickFunc}
|
||||||
|
className={className}
|
||||||
|
fill={color}
|
||||||
|
|
||||||
|
height={height}
|
||||||
|
|
||||||
|
width={width} xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" ><path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/></svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
22
src/assets/svgs/ExpandMoreSVG.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { IconTypes } from "./IconTypes";
|
||||||
|
export const ExpandMoreSVG: React.FC<IconTypes> = ({
|
||||||
|
color,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
className,
|
||||||
|
onClickFunc
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
onClick={onClickFunc}
|
||||||
|
height={height}
|
||||||
|
width={width}
|
||||||
|
fill={color}
|
||||||
|
className={className}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 -960 960 960"
|
||||||
|
>
|
||||||
|
<path d="M480-345 240-585l43-43 197 198 197-197 43 43-240 239Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
7
src/assets/svgs/IconTypes.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export interface IconTypes {
|
||||||
|
color?: string;
|
||||||
|
height: string;
|
||||||
|
width: string;
|
||||||
|
className?: string;
|
||||||
|
onClickFunc?: (e?: any) => void;
|
||||||
|
}
|
23
src/assets/svgs/LightModeSVG.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { IconTypes } from './IconTypes'
|
||||||
|
|
||||||
|
export const LightModeSVG: React.FC<IconTypes> = ({
|
||||||
|
color,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
className,
|
||||||
|
onClickFunc
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={className}
|
||||||
|
onClick={onClickFunc}
|
||||||
|
fill={color}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
height={height}
|
||||||
|
viewBox="0 96 960 960"
|
||||||
|
width={width}
|
||||||
|
>
|
||||||
|
<path d="M479.765 716Q538 716 579 675.235q41-40.764 41-99Q620 518 579.235 477q-40.764-41-99-41Q422 436 381 476.765q-41 40.764-41 99Q340 634 380.765 675q40.764 41 99 41Zm.235 60q-83 0-141.5-58.5T280 576q0-83 58.5-141.5T480 376q83 0 141.5 58.5T680 576q0 83-58.5 141.5T480 776ZM70 606q-12.75 0-21.375-8.675Q40 588.649 40 575.825 40 563 48.625 554.5T70 546h100q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T170 606H70Zm720 0q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T790 546h100q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T890 606H790ZM479.825 296Q467 296 458.5 287.375T450 266V166q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T510 166v100q0 12.75-8.675 21.375-8.676 8.625-21.5 8.625Zm0 720q-12.825 0-21.325-8.62-8.5-8.63-8.5-21.38V886q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T510 886v100q0 12.75-8.675 21.38-8.676 8.62-21.5 8.62ZM240 378l-57-56q-9-9-8.629-21.603.37-12.604 8.526-21.5 8.896-8.897 21.5-8.897Q217 270 226 279l56 57q8 9 8 21t-8 20.5q-8 8.5-20.5 8.5t-21.5-8Zm494 495-56-57q-8-9-8-21.375T678.5 774q8.5-9 20.5-9t21 9l57 56q9 9 8.629 21.603-.37 12.604-8.526 21.5-8.896 8.897-21.5 8.897Q743 882 734 873Zm-56-495q-9-9-9-21t9-21l56-57q9-9 21.603-8.629 12.604.37 21.5 8.526 8.897 8.896 8.897 21.5Q786 313 777 322l-57 56q-8 8-20.364 8-12.363 0-21.636-8ZM182.897 873.103q-8.897-8.896-8.897-21.5Q174 839 183 830l57-56q8.8-9 20.9-9 12.1 0 20.709 9Q291 783 291 795t-9 21l-56 57q-9 9-21.603 8.629-12.604-.37-21.5-8.526ZM480 576Z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
18
src/assets/svgs/PlaylistSVG.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { IconTypes } from "./IconTypes";
|
||||||
|
|
||||||
|
export const PlaylistSVG: React.FC<IconTypes> = ({
|
||||||
|
color,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
className,
|
||||||
|
onClickFunc
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<svg onClick={onClickFunc}
|
||||||
|
className={className}
|
||||||
|
fill={color}
|
||||||
|
height={height}
|
||||||
|
width={width} xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" ><path d="M120-320v-80h320v80H120Zm0-160v-80h480v80H120Zm0-160v-80h480v80H120Zm520 520v-320l240 160-240 160Z"/></svg>
|
||||||
|
|
||||||
|
);
|
||||||
|
};
|
23
src/assets/svgs/TimesSVG.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { IconTypes } from "./IconTypes";
|
||||||
|
|
||||||
|
export const TimesSVG: React.FC<IconTypes> = ({
|
||||||
|
color,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
className,
|
||||||
|
onClickFunc
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
onClick={onClickFunc}
|
||||||
|
className={className}
|
||||||
|
fill={color}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
height={height}
|
||||||
|
viewBox="0 -960 960 960"
|
||||||
|
width={width}
|
||||||
|
>
|
||||||
|
<path d="m249-207-42-42 231-231-231-231 42-42 231 231 231-231 42 42-231 231 231 231-42 42-231-231-231 231Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
511
src/components/EditIssue/EditIssue.tsx
Normal file
@ -0,0 +1,511 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
CrowdfundActionButton,
|
||||||
|
CrowdfundActionButtonRow,
|
||||||
|
CustomInputField,
|
||||||
|
ModalBody,
|
||||||
|
NewCrowdfundTitle,
|
||||||
|
} from "./Upload-styles";
|
||||||
|
import { Box, Modal, Typography, useTheme } from "@mui/material";
|
||||||
|
import RemoveIcon from "@mui/icons-material/Remove";
|
||||||
|
|
||||||
|
import ShortUniqueId from "short-unique-id";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { useDropzone } from "react-dropzone";
|
||||||
|
|
||||||
|
import { setNotification } from "../../state/features/notificationsSlice";
|
||||||
|
import { objectToBase64 } from "../../utils/toBase64";
|
||||||
|
import { RootState } from "../../state/store";
|
||||||
|
import {
|
||||||
|
setEditFile,
|
||||||
|
updateFile,
|
||||||
|
updateInHashMap,
|
||||||
|
} from "../../state/features/fileSlice.ts";
|
||||||
|
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
|
||||||
|
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll";
|
||||||
|
import { TextEditor } from "../common/TextEditor/TextEditor";
|
||||||
|
import { extractTextFromHTML } from "../common/TextEditor/utils";
|
||||||
|
import { allCategoryData } from "../../constants/Categories/1stCategories.ts";
|
||||||
|
import { titleFormatter } from "../../constants/Misc.ts";
|
||||||
|
import {
|
||||||
|
CategoryList,
|
||||||
|
CategoryListRef,
|
||||||
|
getCategoriesFromObject,
|
||||||
|
} from "../common/CategoryList/CategoryList.tsx";
|
||||||
|
import {
|
||||||
|
ImagePublisher,
|
||||||
|
ImagePublisherRef,
|
||||||
|
} from "../common/ImagePublisher/ImagePublisher.tsx";
|
||||||
|
|
||||||
|
const uid = new ShortUniqueId();
|
||||||
|
const shortuid = new ShortUniqueId({ length: 5 });
|
||||||
|
|
||||||
|
interface NewCrowdfundProps {
|
||||||
|
editId?: string;
|
||||||
|
editContent?: null | {
|
||||||
|
title: string;
|
||||||
|
user: string;
|
||||||
|
coverImage: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VideoFile {
|
||||||
|
file: File;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
coverImage?: string;
|
||||||
|
identifier?: string;
|
||||||
|
filename?: string;
|
||||||
|
}
|
||||||
|
export const EditIssue = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const username = useSelector((state: RootState) => state.auth?.user?.name);
|
||||||
|
const userAddress = useSelector(
|
||||||
|
(state: RootState) => state.auth?.user?.address
|
||||||
|
);
|
||||||
|
const editFileProperties = useSelector(
|
||||||
|
(state: RootState) => state.file.editFileProperties
|
||||||
|
);
|
||||||
|
const [publishes, setPublishes] = useState<any>(null);
|
||||||
|
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false);
|
||||||
|
const [videoPropertiesToSetToRedux, setVideoPropertiesToSetToRedux] =
|
||||||
|
useState(null);
|
||||||
|
|
||||||
|
const [title, setTitle] = useState<string>("");
|
||||||
|
const [description, setDescription] = useState<string>("");
|
||||||
|
const [coverImage, setCoverImage] = useState<string>("");
|
||||||
|
const [file, setFile] = useState(null);
|
||||||
|
const [files, setFiles] = useState<VideoFile[]>([]);
|
||||||
|
const [editCategories, setEditCategories] = useState<string[]>([]);
|
||||||
|
const categoryListRef = useRef<CategoryListRef>(null);
|
||||||
|
const imagePublisherRef = useRef<ImagePublisherRef>(null);
|
||||||
|
|
||||||
|
const { getRootProps, getInputProps } = useDropzone({
|
||||||
|
maxFiles: 10,
|
||||||
|
maxSize: 419430400, // 400 MB in bytes
|
||||||
|
onDrop: (acceptedFiles, rejectedFiles) => {
|
||||||
|
const formatArray = acceptedFiles.map(item => {
|
||||||
|
return {
|
||||||
|
file: item,
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
coverImage: "",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setFiles(prev => [...prev, ...formatArray]);
|
||||||
|
|
||||||
|
let errorString = null;
|
||||||
|
rejectedFiles.forEach(({ file, errors }) => {
|
||||||
|
errors.forEach(error => {
|
||||||
|
if (error.code === "file-too-large") {
|
||||||
|
errorString = "File must be under 400mb";
|
||||||
|
}
|
||||||
|
console.log(`Error with file ${file.name}: ${error.message}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (errorString) {
|
||||||
|
const notificationObj = {
|
||||||
|
msg: errorString,
|
||||||
|
alertType: "error",
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(setNotification(notificationObj));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editFileProperties) {
|
||||||
|
setTitle(editFileProperties?.title || "");
|
||||||
|
setFiles(editFileProperties?.files || []);
|
||||||
|
if (editFileProperties?.htmlDescription) {
|
||||||
|
setDescription(editFileProperties?.htmlDescription);
|
||||||
|
} else if (editFileProperties?.fullDescription) {
|
||||||
|
const paragraph = `<p>${editFileProperties?.fullDescription}</p>`;
|
||||||
|
setDescription(paragraph);
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoriesFromEditFile =
|
||||||
|
getCategoriesFromObject(editFileProperties);
|
||||||
|
setEditCategories(categoriesFromEditFile);
|
||||||
|
}
|
||||||
|
}, [editFileProperties]);
|
||||||
|
const onClose = () => {
|
||||||
|
dispatch(setEditFile(null));
|
||||||
|
setVideoPropertiesToSetToRedux(null);
|
||||||
|
setFile(null);
|
||||||
|
setTitle("");
|
||||||
|
setDescription("");
|
||||||
|
setCoverImage("");
|
||||||
|
};
|
||||||
|
|
||||||
|
async function publishQDNResource() {
|
||||||
|
try {
|
||||||
|
const categoryList = categoryListRef.current?.getSelectedCategories();
|
||||||
|
if (!title) throw new Error("Please enter a title");
|
||||||
|
if (!description) throw new Error("Please enter a description");
|
||||||
|
if (!categoryList[0]) throw new Error("Please select a category");
|
||||||
|
if (!editFileProperties) return;
|
||||||
|
if (!userAddress) throw new Error("Unable to locate user address");
|
||||||
|
|
||||||
|
let errorMsg = "";
|
||||||
|
let name = "";
|
||||||
|
if (username) {
|
||||||
|
name = username;
|
||||||
|
}
|
||||||
|
if (!name) {
|
||||||
|
errorMsg =
|
||||||
|
"Cannot publish without access to your name. Please authenticate.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editFileProperties?.user !== username) {
|
||||||
|
errorMsg = "Cannot publish another user's resource";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorMsg) {
|
||||||
|
dispatch(
|
||||||
|
setNotification({
|
||||||
|
msg: errorMsg,
|
||||||
|
alertType: "error",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let fileReferences = [];
|
||||||
|
|
||||||
|
let listOfPublishes = [];
|
||||||
|
const fullDescription = extractTextFromHTML(description);
|
||||||
|
|
||||||
|
const sanitizeTitle = title
|
||||||
|
.replace(/[^a-zA-Z0-9\s-]/g, "")
|
||||||
|
.replace(/\s+/g, "-")
|
||||||
|
.replace(/-+/g, "-")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
for (const publish of files) {
|
||||||
|
if (publish?.identifier) {
|
||||||
|
fileReferences.push(publish);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const file = publish.file;
|
||||||
|
const id = uid();
|
||||||
|
|
||||||
|
const identifier = `${QSUPPORT_FILE_BASE}${sanitizeTitle.slice(0, 30)}_${id}`;
|
||||||
|
|
||||||
|
let fileExtension = "";
|
||||||
|
const fileExtensionSplit = file?.name?.split(".");
|
||||||
|
if (fileExtensionSplit?.length > 1) {
|
||||||
|
fileExtension = fileExtensionSplit?.pop() || "";
|
||||||
|
}
|
||||||
|
let firstPartName = fileExtensionSplit[0];
|
||||||
|
|
||||||
|
let filename = firstPartName.slice(0, 15);
|
||||||
|
|
||||||
|
// Step 1: Replace all white spaces with underscores
|
||||||
|
|
||||||
|
// Replace all forms of whitespace (including non-standard ones) with underscores
|
||||||
|
let stringWithUnderscores = filename.replace(/[\s\uFEFF\xA0]+/g, "_");
|
||||||
|
|
||||||
|
// Remove all non-alphanumeric characters (except underscores)
|
||||||
|
let alphanumericString = stringWithUnderscores.replace(
|
||||||
|
/[^a-zA-Z0-9_]/g,
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fileExtension) {
|
||||||
|
filename = `${alphanumericString.trim()}.${fileExtension}`;
|
||||||
|
} else {
|
||||||
|
filename = alphanumericString;
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadescription =
|
||||||
|
`**${categoryListRef.current?.getCategoriesFetchString()}**` +
|
||||||
|
fullDescription.slice(0, 150);
|
||||||
|
|
||||||
|
const requestBodyVideo: any = {
|
||||||
|
action: "PUBLISH_QDN_RESOURCE",
|
||||||
|
name: name,
|
||||||
|
service: "FILE",
|
||||||
|
file,
|
||||||
|
title: title.slice(0, 50),
|
||||||
|
description: metadescription,
|
||||||
|
identifier,
|
||||||
|
filename,
|
||||||
|
tag1: QSUPPORT_FILE_BASE,
|
||||||
|
};
|
||||||
|
listOfPublishes.push(requestBodyVideo);
|
||||||
|
fileReferences.push({
|
||||||
|
filename: file.name,
|
||||||
|
identifier,
|
||||||
|
name,
|
||||||
|
service: "FILE",
|
||||||
|
mimetype: file.type,
|
||||||
|
size: file.size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileObject: any = {
|
||||||
|
title,
|
||||||
|
version: editFileProperties.version,
|
||||||
|
fullDescription,
|
||||||
|
htmlDescription: description,
|
||||||
|
commentsId: editFileProperties.commentsId,
|
||||||
|
...categoryListRef.current?.categoriesToObject(),
|
||||||
|
files: fileReferences,
|
||||||
|
images: imagePublisherRef?.current?.getImageArray(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let metadescription =
|
||||||
|
`**${categoryListRef.current?.getCategoriesFetchString()}**` +
|
||||||
|
fullDescription.slice(0, 150);
|
||||||
|
|
||||||
|
const fileObjectToBase64 = await objectToBase64(fileObject);
|
||||||
|
// Description is obtained from raw data
|
||||||
|
|
||||||
|
const requestBodyJson: any = {
|
||||||
|
action: "PUBLISH_QDN_RESOURCE",
|
||||||
|
name: name,
|
||||||
|
service: "DOCUMENT",
|
||||||
|
data64: fileObjectToBase64,
|
||||||
|
title: title.slice(0, 50),
|
||||||
|
description: metadescription,
|
||||||
|
identifier: editFileProperties.id,
|
||||||
|
tag1: QSUPPORT_FILE_BASE,
|
||||||
|
filename: `video_metadata.json`,
|
||||||
|
};
|
||||||
|
listOfPublishes.push(requestBodyJson);
|
||||||
|
|
||||||
|
const multiplePublish = {
|
||||||
|
action: "PUBLISH_MULTIPLE_QDN_RESOURCES",
|
||||||
|
resources: [...listOfPublishes],
|
||||||
|
};
|
||||||
|
setPublishes(multiplePublish);
|
||||||
|
setIsOpenMultiplePublish(true);
|
||||||
|
setVideoPropertiesToSetToRedux({
|
||||||
|
...editFileProperties,
|
||||||
|
...fileObject,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
let notificationObj: any = null;
|
||||||
|
if (typeof error === "string") {
|
||||||
|
notificationObj = {
|
||||||
|
msg: error || "Failed to publish update",
|
||||||
|
alertType: "error",
|
||||||
|
};
|
||||||
|
} else if (typeof error?.error === "string") {
|
||||||
|
notificationObj = {
|
||||||
|
msg: error?.error || "Failed to publish update",
|
||||||
|
alertType: "error",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
notificationObj = {
|
||||||
|
msg: error?.message || "Failed to publish update",
|
||||||
|
alertType: "error",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!notificationObj) return;
|
||||||
|
dispatch(setNotification(notificationObj));
|
||||||
|
|
||||||
|
throw new Error("Failed to publish update");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOnchange = (index: number, type: string, value: string) => {
|
||||||
|
// setFiles((prev) => {
|
||||||
|
// let formattedValue = value
|
||||||
|
// console.log({type})
|
||||||
|
// if(type === 'title'){
|
||||||
|
// formattedValue = value.replace(/[^a-zA-Z0-9\s]/g, "")
|
||||||
|
// }
|
||||||
|
// const copyFiles = [...prev];
|
||||||
|
// copyFiles[index] = {
|
||||||
|
// ...copyFiles[index],
|
||||||
|
// [type]: formattedValue,
|
||||||
|
// };
|
||||||
|
// return copyFiles;
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
open={!!editFileProperties}
|
||||||
|
aria-labelledby="modal-title"
|
||||||
|
aria-describedby="modal-description"
|
||||||
|
>
|
||||||
|
<ModalBody>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<NewCrowdfundTitle>Update Issue</NewCrowdfundTitle>
|
||||||
|
</Box>
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
{...getRootProps()}
|
||||||
|
sx={{
|
||||||
|
border: "1px dashed gray",
|
||||||
|
padding: 2,
|
||||||
|
textAlign: "center",
|
||||||
|
marginBottom: 2,
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
<Typography>Click to add more files</Typography>
|
||||||
|
</Box>
|
||||||
|
{files.map((file, index) => {
|
||||||
|
const isExistingFile = !!file?.identifier;
|
||||||
|
return (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography>
|
||||||
|
{isExistingFile ? file.filename : file?.file?.name}
|
||||||
|
</Typography>
|
||||||
|
<RemoveIcon
|
||||||
|
onClick={() => {
|
||||||
|
setFiles(prev => {
|
||||||
|
const copyPrev = [...prev];
|
||||||
|
copyPrev.splice(index, 1);
|
||||||
|
return copyPrev;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "20px",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "20px",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CategoryList
|
||||||
|
categoryData={allCategoryData}
|
||||||
|
initialCategories={editCategories}
|
||||||
|
columns={3}
|
||||||
|
ref={categoryListRef}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<ImagePublisher
|
||||||
|
ref={imagePublisherRef}
|
||||||
|
initialImages={editFileProperties?.images}
|
||||||
|
/>
|
||||||
|
<CustomInputField
|
||||||
|
name="title"
|
||||||
|
label="Title of Issue"
|
||||||
|
variant="filled"
|
||||||
|
value={title}
|
||||||
|
onChange={e => {
|
||||||
|
const value = e.target.value;
|
||||||
|
const formattedValue = value.replace(titleFormatter, "");
|
||||||
|
setTitle(formattedValue);
|
||||||
|
}}
|
||||||
|
inputProps={{ maxLength: 180 }}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "18px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Description of Issue
|
||||||
|
</Typography>
|
||||||
|
<TextEditor
|
||||||
|
inlineContent={description}
|
||||||
|
setInlineContent={value => {
|
||||||
|
setDescription(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
|
||||||
|
<CrowdfundActionButtonRow>
|
||||||
|
<CrowdfundActionButton
|
||||||
|
onClick={() => {
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</CrowdfundActionButton>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "20px",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CrowdfundActionButton
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => {
|
||||||
|
publishQDNResource();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Publish
|
||||||
|
</CrowdfundActionButton>
|
||||||
|
</Box>
|
||||||
|
</CrowdfundActionButtonRow>
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
|
{isOpenMultiplePublish && (
|
||||||
|
<MultiplePublish
|
||||||
|
isOpen={isOpenMultiplePublish}
|
||||||
|
onError={messageNotification => {
|
||||||
|
setIsOpenMultiplePublish(false);
|
||||||
|
setPublishes(null);
|
||||||
|
if (messageNotification) {
|
||||||
|
dispatch(
|
||||||
|
setNotification({
|
||||||
|
msg: messageNotification,
|
||||||
|
alertType: "error",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onSubmit={() => {
|
||||||
|
setIsOpenMultiplePublish(false);
|
||||||
|
const clonedCopy = structuredClone(videoPropertiesToSetToRedux);
|
||||||
|
dispatch(updateFile(clonedCopy));
|
||||||
|
dispatch(updateInHashMap(clonedCopy));
|
||||||
|
dispatch(
|
||||||
|
setNotification({
|
||||||
|
msg: "File updated",
|
||||||
|
alertType: "success",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
publishes={publishes}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
586
src/components/EditIssue/Upload-styles.tsx
Normal file
@ -0,0 +1,586 @@
|
|||||||
|
import { styled } from "@mui/system";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Grid,
|
||||||
|
Rating,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
Select
|
||||||
|
} from "@mui/material";
|
||||||
|
import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternate";
|
||||||
|
import { TimesSVG } from "../../assets/svgs/TimesSVG";
|
||||||
|
|
||||||
|
export const DoubleLine = styled(Typography)`
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
overflow: hidden;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const MainContainer = styled(Grid)({
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
justifyContent: "center",
|
||||||
|
margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MainCol = styled(Grid)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
width: "100%",
|
||||||
|
padding: "20px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CreateContainer = styled(Box)(({ theme }) => ({
|
||||||
|
position: "fixed",
|
||||||
|
bottom: "20px",
|
||||||
|
right: "20px",
|
||||||
|
cursor: "pointer",
|
||||||
|
background: theme.palette.background.default,
|
||||||
|
width: "50px",
|
||||||
|
height: "50px",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
borderRadius: "50%",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const ModalBody = styled(Box)(({ theme }) => ({
|
||||||
|
position: "absolute",
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
borderRadius: "4px",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
width: "75%",
|
||||||
|
maxWidth: "900px",
|
||||||
|
padding: "15px 35px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "17px",
|
||||||
|
overflowY: "auto",
|
||||||
|
maxHeight: "95vh",
|
||||||
|
boxShadow:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)"
|
||||||
|
: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px",
|
||||||
|
"&::-webkit-scrollbar-track": {
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar-track:hover": {
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar": {
|
||||||
|
width: "16px",
|
||||||
|
height: "10px",
|
||||||
|
backgroundColor: theme.palette.mode === "light" ? "#f6f8fa" : "#292d3e",
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar-thumb": {
|
||||||
|
backgroundColor: theme.palette.mode === "light" ? "#d3d9e1" : "#575757",
|
||||||
|
borderRadius: "8px",
|
||||||
|
backgroundClip: "content-box",
|
||||||
|
border: "4px solid transparent",
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar-thumb:hover": {
|
||||||
|
backgroundColor: theme.palette.mode === "light" ? "#b7bcc4" : "#474646",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const NewCrowdfundTitle = styled(Typography)(({ theme }) => ({
|
||||||
|
fontWeight: 400,
|
||||||
|
fontFamily: "Raleway",
|
||||||
|
fontSize: "25px",
|
||||||
|
userSelect: "none",
|
||||||
|
}));
|
||||||
|
export const NewCrowdFundFont = styled(Typography)(({ theme }) => ({
|
||||||
|
fontWeight: 400,
|
||||||
|
fontFamily: "Raleway",
|
||||||
|
fontSize: "18px",
|
||||||
|
userSelect: "none",
|
||||||
|
}));
|
||||||
|
export const NewCrowdfundTimeDescription = styled(Typography)(({ theme }) => ({
|
||||||
|
fontWeight: 400,
|
||||||
|
fontFamily: "Raleway",
|
||||||
|
fontSize: "18px",
|
||||||
|
userSelect: "none",
|
||||||
|
fontStyle: "italic",
|
||||||
|
textDecoration: "underline",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CustomInputField = styled(TextField)(({ theme }) => ({
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "19px",
|
||||||
|
letterSpacing: "0px",
|
||||||
|
fontWeight: 400,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
borderColor: theme.palette.background.paper,
|
||||||
|
"& label": {
|
||||||
|
color: theme.palette.mode === "light" ? "#808183" : "#edeef0",
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "19px",
|
||||||
|
letterSpacing: "0px",
|
||||||
|
fontWeight: 400,
|
||||||
|
},
|
||||||
|
"& label.Mui-focused": {
|
||||||
|
color: theme.palette.mode === "light" ? "#A0AAB4" : "#d7d8da",
|
||||||
|
},
|
||||||
|
"& .MuiInput-underline:after": {
|
||||||
|
borderBottomColor: theme.palette.mode === "light" ? "#B2BAC2" : "#c9cccf",
|
||||||
|
},
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
"& fieldset": {
|
||||||
|
borderColor: "#E0E3E7",
|
||||||
|
},
|
||||||
|
"&:hover fieldset": {
|
||||||
|
borderColor: "#B2BAC2",
|
||||||
|
},
|
||||||
|
"&.Mui-focused fieldset": {
|
||||||
|
borderColor: "#6F7E8C",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"& .MuiInputBase-root": {
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "19px",
|
||||||
|
letterSpacing: "0px",
|
||||||
|
fontWeight: 400,
|
||||||
|
},
|
||||||
|
"& [class$='-MuiFilledInput-root']": {
|
||||||
|
padding: "30px 12px 8px",
|
||||||
|
},
|
||||||
|
"& .MuiFilledInput-root:after": {
|
||||||
|
borderBottomColor: theme.palette.secondary.main,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const CrowdfundTitle = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Copse",
|
||||||
|
letterSpacing: "1px",
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: "20px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundSubTitleRow = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "100%",
|
||||||
|
flexDirection: "row",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CrowdfundSubTitle = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Copse",
|
||||||
|
letterSpacing: "1px",
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: "17px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
borderBottom: `1px solid ${theme.palette.text.primary}`,
|
||||||
|
paddingBottom: "1.5px",
|
||||||
|
width: "fit-content",
|
||||||
|
textDecoration: "none",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundDescription = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Raleway",
|
||||||
|
fontSize: "16px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const Spacer = ({ height }: any) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: height,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StyledCardHeaderComment = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
gap: "5px",
|
||||||
|
padding: "7px 0px",
|
||||||
|
});
|
||||||
|
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 AuthorTextComment = styled(Typography)({
|
||||||
|
fontFamily: "Raleway, sans-serif",
|
||||||
|
fontSize: "16px",
|
||||||
|
lineHeight: "1.2",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AddLogoIcon = styled(AddPhotoAlternateIcon)(({ theme }) => ({
|
||||||
|
color: "#fff",
|
||||||
|
height: "25px",
|
||||||
|
width: "auto",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CoverImagePreview = styled("img")(({ theme }) => ({
|
||||||
|
width: "100px",
|
||||||
|
height: "100px",
|
||||||
|
objectFit: "contain",
|
||||||
|
userSelect: "none",
|
||||||
|
borderRadius: "3px",
|
||||||
|
marginBottom: "10px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const LogoPreviewRow = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const TimesIcon = styled(TimesSVG)(({ theme }) => ({
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
borderRadius: "50%",
|
||||||
|
padding: "5px",
|
||||||
|
transition: "all 0.2s ease-in-out",
|
||||||
|
"&:hover": {
|
||||||
|
cursor: "pointer",
|
||||||
|
scale: "1.1",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundCardTitle = styled(DoubleLine)(({ theme }) => ({
|
||||||
|
fontFamily: "Montserrat",
|
||||||
|
fontSize: "24px",
|
||||||
|
letterSpacing: "-0.3px",
|
||||||
|
userSelect: "none",
|
||||||
|
marginBottom: "auto",
|
||||||
|
textAlign: "center",
|
||||||
|
"@media (max-width: 650px)": {
|
||||||
|
fontSize: "18px",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundUploadDate = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Montserrat",
|
||||||
|
fontSize: "12px",
|
||||||
|
letterSpacing: "0.2px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CATContainer = styled(Box)(({ theme }) => ({
|
||||||
|
position: "relative",
|
||||||
|
display: "flex",
|
||||||
|
padding: "15px",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "20px",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "100%",
|
||||||
|
alignItems: "center",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const AddCrowdFundButton = styled(Button)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
textTransform: "none",
|
||||||
|
padding: "10px 25px",
|
||||||
|
fontSize: "15px",
|
||||||
|
gap: "8px",
|
||||||
|
color: "#ffffff",
|
||||||
|
backgroundColor:
|
||||||
|
theme.palette.mode === "dark" ? theme.palette.primary.main : "#2a9a86",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "5px",
|
||||||
|
transition: "all 0.3s ease-in-out",
|
||||||
|
"&:hover": {
|
||||||
|
cursor: "pointer",
|
||||||
|
backgroundColor:
|
||||||
|
theme.palette.mode === "dark" ? theme.palette.primary.dark : "#217e6d",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const EditCrowdFundButton = styled(Button)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
textTransform: "none",
|
||||||
|
padding: "5px 12px",
|
||||||
|
gap: "8px",
|
||||||
|
color: "#ffffff",
|
||||||
|
backgroundColor:
|
||||||
|
theme.palette.mode === "dark" ? theme.palette.primary.main : "#2a9a86",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "5px",
|
||||||
|
transition: "all 0.3s ease-in-out",
|
||||||
|
"&:hover": {
|
||||||
|
cursor: "pointer",
|
||||||
|
backgroundColor:
|
||||||
|
theme.palette.mode === "dark" ? theme.palette.primary.dark : "#217e6d",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundListWrapper = styled(Box)(({ theme }) => ({
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: "0px",
|
||||||
|
background: theme.palette.background.default,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundTitleRow = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "100%",
|
||||||
|
gap: "10px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundPageTitle = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Copse",
|
||||||
|
fontSize: "35px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: "1px",
|
||||||
|
userSelect: "none",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundStatusRow = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "21px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: 0,
|
||||||
|
border: `1px solid ${theme.palette.text.primary}`,
|
||||||
|
borderRadius: "8px",
|
||||||
|
padding: "15px 25px",
|
||||||
|
userSelect: "none",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundDescriptionRow = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "100%",
|
||||||
|
fontFamily: "Montserrat",
|
||||||
|
fontSize: "18px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AboutMyCrowdfund = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Copse",
|
||||||
|
fontSize: "23px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: "1px",
|
||||||
|
userSelect: "none",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundInlineContentRow = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "100%",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CrowdfundInlineContent = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "19px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: 0,
|
||||||
|
userSelect: "none",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundAccordion = styled(Accordion)(({ theme }) => ({
|
||||||
|
backgroundColor: theme.palette.primary.light,
|
||||||
|
"& .Mui-expanded": {
|
||||||
|
minHeight: "auto !important",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundAccordionSummary = styled(AccordionSummary)({
|
||||||
|
height: "50px",
|
||||||
|
"& .Mui-expanded": {
|
||||||
|
margin: "0px !important",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CrowdfundAccordionFont = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "20px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: "0px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundAccordionDetails = styled(AccordionDetails)({
|
||||||
|
padding: "0px 16px 16px 16px",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AddCoverImageButton = styled(Button)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
fontFamily: "Montserrat",
|
||||||
|
fontSize: "16px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: "0.2px",
|
||||||
|
color: "white",
|
||||||
|
gap: "5px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CoverImage = styled("img")({
|
||||||
|
width: "100%",
|
||||||
|
height: "250px",
|
||||||
|
objectFit: "cover",
|
||||||
|
objectPosition: "center",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CrowdfundActionButtonRow = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
width: "100%",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CrowdfundActionButton = styled(Button)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
fontFamily: "Montserrat",
|
||||||
|
fontSize: "16px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: "0.2px",
|
||||||
|
color: "white",
|
||||||
|
gap: "5px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const BackToHomeButton = styled(Button)(({ theme }) => ({
|
||||||
|
position: "absolute",
|
||||||
|
top: "20px",
|
||||||
|
left: "20px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
fontFamily: "Montserrat",
|
||||||
|
fontSize: "13px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: "0.2px",
|
||||||
|
color: "white",
|
||||||
|
gap: "5px",
|
||||||
|
padding: "5px 10px",
|
||||||
|
backgroundColor: theme.palette.secondary.main,
|
||||||
|
transition: "all 0.3s ease-in-out",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: theme.palette.secondary.dark,
|
||||||
|
cursor: "pointer",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundLoaderRow = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: "10px",
|
||||||
|
padding: "10px",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const RatingContainer = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "1px 5px",
|
||||||
|
borderRadius: "5px",
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
transition: "all 0.3s ease-in-out",
|
||||||
|
"&:hover": {
|
||||||
|
cursor: "pointer",
|
||||||
|
backgroundColor: "#e4ddddac",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const StyledRating = styled(Rating)({
|
||||||
|
fontSize: "28px",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const NoReviewsFont = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: 0,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledButton = styled(Button)(({ theme }) => ({
|
||||||
|
fontWeight: 600,
|
||||||
|
color: theme.palette.text.primary
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const CustomSelect = styled(Select)(({ theme }) => ({
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "19px",
|
||||||
|
letterSpacing: "0px",
|
||||||
|
fontWeight: 400,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
'& .MuiSelect-select': {
|
||||||
|
padding: '12px',
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "19px",
|
||||||
|
letterSpacing: "0px",
|
||||||
|
fontWeight: 400,
|
||||||
|
borderRadius: theme.shape.borderRadius, // Match border radius
|
||||||
|
},
|
||||||
|
'&:before': {
|
||||||
|
// Underline style
|
||||||
|
borderBottomColor: theme.palette.mode === "light" ? "#B2BAC2" : "#c9cccf",
|
||||||
|
},
|
||||||
|
'&:after': {
|
||||||
|
// Underline style when focused
|
||||||
|
borderBottomColor: theme.palette.secondary.main,
|
||||||
|
},
|
||||||
|
'& .MuiOutlinedInput-root': {
|
||||||
|
'& fieldset': {
|
||||||
|
borderColor: "#E0E3E7",
|
||||||
|
},
|
||||||
|
'&:hover fieldset': {
|
||||||
|
borderColor: "#B2BAC2",
|
||||||
|
},
|
||||||
|
'&.Mui-focused fieldset': {
|
||||||
|
borderColor: "#6F7E8C",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'& .MuiInputBase-root': {
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "19px",
|
||||||
|
letterSpacing: "0px",
|
||||||
|
fontWeight: 400,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
},
|
||||||
|
}));
|
620
src/components/EditPlaylist/EditPlaylist.tsx
Normal file
@ -0,0 +1,620 @@
|
|||||||
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
AddCoverImageButton,
|
||||||
|
AddLogoIcon,
|
||||||
|
CoverImagePreview,
|
||||||
|
CrowdfundActionButton,
|
||||||
|
CrowdfundActionButtonRow,
|
||||||
|
CustomInputField,
|
||||||
|
LogoPreviewRow,
|
||||||
|
ModalBody,
|
||||||
|
NewCrowdfundTitle,
|
||||||
|
TimesIcon,
|
||||||
|
} from "./Upload-styles.tsx";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Modal,
|
||||||
|
OutlinedInput,
|
||||||
|
Select,
|
||||||
|
SelectChangeEvent,
|
||||||
|
Typography,
|
||||||
|
useTheme,
|
||||||
|
} from "@mui/material";
|
||||||
|
import ShortUniqueId from "short-unique-id";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
|
import { setNotification } from "../../state/features/notificationsSlice";
|
||||||
|
import { objectToBase64 } from "../../utils/toBase64";
|
||||||
|
import { RootState } from "../../state/store";
|
||||||
|
import {
|
||||||
|
setEditPlaylist,
|
||||||
|
updateFile,
|
||||||
|
updateInHashMap,
|
||||||
|
} from "../../state/features/fileSlice.ts";
|
||||||
|
import ImageUploader from "../common/ImagePublisher/ImageUploader.tsx";
|
||||||
|
import {
|
||||||
|
QSUPPORT_FILE_BASE,
|
||||||
|
QSUPPORT_PLAYLIST_BASE,
|
||||||
|
} from "../../constants/Identifiers.ts";
|
||||||
|
import { PlaylistListEdit } from "../PlaylistListEdit/PlaylistListEdit";
|
||||||
|
import { TextEditor } from "../common/TextEditor/TextEditor";
|
||||||
|
import { extractTextFromHTML } from "../common/TextEditor/utils";
|
||||||
|
import {
|
||||||
|
firstCategories,
|
||||||
|
secondCategories,
|
||||||
|
} from "../../constants/Categories/1stCategories.ts";
|
||||||
|
|
||||||
|
const uid = new ShortUniqueId();
|
||||||
|
const shortuid = new ShortUniqueId({ length: 5 });
|
||||||
|
|
||||||
|
interface NewCrowdfundProps {
|
||||||
|
editId?: string;
|
||||||
|
editContent?: null | {
|
||||||
|
title: string;
|
||||||
|
user: string;
|
||||||
|
coverImage: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VideoFile {
|
||||||
|
file: File;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
coverImage?: string;
|
||||||
|
}
|
||||||
|
export const EditPlaylist = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const username = useSelector((state: RootState) => state.auth?.user?.name);
|
||||||
|
const userAddress = useSelector(
|
||||||
|
(state: RootState) => state.auth?.user?.address
|
||||||
|
);
|
||||||
|
const editVideoProperties = useSelector(
|
||||||
|
(state: RootState) => state.file.editPlaylistProperties
|
||||||
|
);
|
||||||
|
const [playlistData, setPlaylistData] = useState<any>(null);
|
||||||
|
const [title, setTitle] = useState<string>("");
|
||||||
|
const [description, setDescription] = useState<string>("");
|
||||||
|
const [coverImage, setCoverImage] = useState<string[]>([]);
|
||||||
|
const [videos, setVideos] = useState([]);
|
||||||
|
const [selectedCategoryVideos, setSelectedCategoryVideos] =
|
||||||
|
useState<any>(null);
|
||||||
|
const [selectedSubCategoryVideos, setSelectedSubCategoryVideos] =
|
||||||
|
useState<any>(null);
|
||||||
|
|
||||||
|
const isNew = useMemo(() => {
|
||||||
|
return editVideoProperties?.mode === "new";
|
||||||
|
}, [editVideoProperties]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isNew) {
|
||||||
|
setPlaylistData({
|
||||||
|
videos: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isNew]);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (editVideoProperties) {
|
||||||
|
// const descriptionString = editVideoProperties?.description || "";
|
||||||
|
// // Splitting the string at the asterisks
|
||||||
|
// const parts = descriptionString.split("**");
|
||||||
|
|
||||||
|
// // The part within the asterisks
|
||||||
|
// const extractedString = parts[1];
|
||||||
|
|
||||||
|
// // The part after the last asterisks
|
||||||
|
// const description = parts[2] || ""; // Using '|| '' to handle cases where there is no text after the last **
|
||||||
|
// setTitle(editVideoProperties?.title || "");
|
||||||
|
// setDescription(editVideoProperties?.fullDescription || "");
|
||||||
|
// setCoverImage(editVideoProperties?.videoImage || "");
|
||||||
|
|
||||||
|
// // Split the extracted string into key-value pairs
|
||||||
|
// const keyValuePairs = extractedString.split(";");
|
||||||
|
|
||||||
|
// // Initialize variables to hold the category and subcategory values
|
||||||
|
// let category, subcategory;
|
||||||
|
|
||||||
|
// // Loop through each key-value pair
|
||||||
|
// keyValuePairs.forEach((pair) => {
|
||||||
|
// const [key, value] = pair.split(":");
|
||||||
|
|
||||||
|
// // Check the key and assign the value to the appropriate variable
|
||||||
|
// if (key === "category") {
|
||||||
|
// category = value;
|
||||||
|
// } else if (key === "subcategory") {
|
||||||
|
// subcategory = value;
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
// if(category){
|
||||||
|
// const selectedOption = categories.find((option) => option.id === +category);
|
||||||
|
// setSelectedCategoryVideos(selectedOption || null);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if(subcategory){
|
||||||
|
// const selectedOption = categories.find((option) => option.id === +subcategory);
|
||||||
|
// setSelectedCategoryVideos(selectedOption || null);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// }
|
||||||
|
// }, [editVideoProperties]);
|
||||||
|
|
||||||
|
const checkforPlaylist = React.useCallback(async videoList => {
|
||||||
|
try {
|
||||||
|
const combinedData: any = {};
|
||||||
|
const videos = [];
|
||||||
|
if (videoList) {
|
||||||
|
for (const vid of videoList) {
|
||||||
|
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${vid.identifier}&limit=1&includemetadata=true&reverse=true&name=${vid.name}&exactmatchnames=true&offset=0`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const responseDataSearchVid = await response.json();
|
||||||
|
|
||||||
|
if (responseDataSearchVid?.length > 0) {
|
||||||
|
let resourceData2 = responseDataSearchVid[0];
|
||||||
|
videos.push(resourceData2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
combinedData.videos = videos;
|
||||||
|
setPlaylistData(combinedData);
|
||||||
|
} catch (error) {}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editVideoProperties) {
|
||||||
|
setTitle(editVideoProperties?.title || "");
|
||||||
|
|
||||||
|
if (editVideoProperties?.htmlDescription) {
|
||||||
|
setDescription(editVideoProperties?.htmlDescription);
|
||||||
|
} else if (editVideoProperties?.description) {
|
||||||
|
const paragraph = `<p>${editVideoProperties?.description}</p>`;
|
||||||
|
setDescription(paragraph);
|
||||||
|
}
|
||||||
|
setCoverImage(editVideoProperties?.image || "");
|
||||||
|
setVideos(editVideoProperties?.videos || []);
|
||||||
|
|
||||||
|
if (editVideoProperties?.category) {
|
||||||
|
const selectedOption = firstCategories.find(
|
||||||
|
option => option.id === +editVideoProperties.category
|
||||||
|
);
|
||||||
|
setSelectedCategoryVideos(selectedOption || null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
editVideoProperties?.category &&
|
||||||
|
editVideoProperties?.subcategory &&
|
||||||
|
secondCategories[+editVideoProperties?.category]
|
||||||
|
) {
|
||||||
|
const selectedOption = secondCategories[
|
||||||
|
+editVideoProperties?.category
|
||||||
|
]?.find(option => option.id === +editVideoProperties.subcategory);
|
||||||
|
setSelectedSubCategoryVideos(selectedOption || null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editVideoProperties?.videos) {
|
||||||
|
checkforPlaylist(editVideoProperties?.videos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [editVideoProperties]);
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
setTitle("");
|
||||||
|
setDescription("");
|
||||||
|
setVideos([]);
|
||||||
|
setPlaylistData(null);
|
||||||
|
setSelectedCategoryVideos(null);
|
||||||
|
setSelectedSubCategoryVideos(null);
|
||||||
|
setCoverImage([]);
|
||||||
|
dispatch(setEditPlaylist(null));
|
||||||
|
};
|
||||||
|
|
||||||
|
async function publishQDNResource() {
|
||||||
|
try {
|
||||||
|
if (!title) throw new Error("Please enter a title");
|
||||||
|
if (!description) throw new Error("Please enter a description");
|
||||||
|
if (!coverImage) throw new Error("Please select cover image");
|
||||||
|
if (!selectedCategoryVideos) throw new Error("Please select a category");
|
||||||
|
|
||||||
|
if (!editVideoProperties) return;
|
||||||
|
if (!userAddress) throw new Error("Unable to locate user address");
|
||||||
|
let errorMsg = "";
|
||||||
|
let name = "";
|
||||||
|
if (username) {
|
||||||
|
name = username;
|
||||||
|
}
|
||||||
|
if (!name) {
|
||||||
|
errorMsg =
|
||||||
|
"Cannot publish without access to your name. Please authenticate.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isNew && editVideoProperties?.user !== username) {
|
||||||
|
errorMsg = "Cannot publish another user's resource";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorMsg) {
|
||||||
|
dispatch(
|
||||||
|
setNotification({
|
||||||
|
msg: errorMsg,
|
||||||
|
alertType: "error",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const category = selectedCategoryVideos.id;
|
||||||
|
const subcategory = selectedSubCategoryVideos?.id || "";
|
||||||
|
|
||||||
|
const videoStructured = playlistData.videos.map(item => {
|
||||||
|
const descriptionVid = item?.metadata?.description;
|
||||||
|
if (!descriptionVid) throw new Error("cannot find video code");
|
||||||
|
|
||||||
|
// Split the string by ';'
|
||||||
|
let parts = descriptionVid.split(";");
|
||||||
|
|
||||||
|
// Initialize a variable to hold the code value
|
||||||
|
let codeValue = "";
|
||||||
|
|
||||||
|
// Loop through the parts to find the one that starts with 'code:'
|
||||||
|
for (let part of parts) {
|
||||||
|
if (part.startsWith("code:")) {
|
||||||
|
codeValue = part.split(":")[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!codeValue) throw new Error("cannot find video code");
|
||||||
|
|
||||||
|
return {
|
||||||
|
identifier: item.identifier,
|
||||||
|
name: item.name,
|
||||||
|
service: item.service,
|
||||||
|
code: codeValue,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const id = uid();
|
||||||
|
|
||||||
|
let commentsId = editVideoProperties?.id;
|
||||||
|
|
||||||
|
if (isNew) {
|
||||||
|
commentsId = `${QSUPPORT_PLAYLIST_BASE}_cm_${id}`;
|
||||||
|
}
|
||||||
|
const stringDescription = extractTextFromHTML(description);
|
||||||
|
|
||||||
|
const playlistObject: any = {
|
||||||
|
title,
|
||||||
|
version: 1,
|
||||||
|
description: stringDescription,
|
||||||
|
htmlDescription: description,
|
||||||
|
image: coverImage,
|
||||||
|
videos: videoStructured,
|
||||||
|
commentsId: commentsId,
|
||||||
|
category,
|
||||||
|
subcategory,
|
||||||
|
};
|
||||||
|
|
||||||
|
const codes = videoStructured.map(item => `c:${item.code};`).join("");
|
||||||
|
let metadescription =
|
||||||
|
`**category:${category};subcategory:${subcategory};${codes}**` +
|
||||||
|
stringDescription.slice(0, 120);
|
||||||
|
|
||||||
|
const crowdfundObjectToBase64 = await objectToBase64(playlistObject);
|
||||||
|
// Description is obtained from raw data
|
||||||
|
|
||||||
|
let identifier = editVideoProperties?.id;
|
||||||
|
const sanitizeTitle = title
|
||||||
|
.replace(/[^a-zA-Z0-9\s-]/g, "")
|
||||||
|
.replace(/\s+/g, "-")
|
||||||
|
.replace(/-+/g, "-")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
if (isNew) {
|
||||||
|
identifier = `${QSUPPORT_PLAYLIST_BASE}${sanitizeTitle.slice(0, 30)}_${id}`;
|
||||||
|
}
|
||||||
|
const requestBodyJson: any = {
|
||||||
|
action: "PUBLISH_QDN_RESOURCE",
|
||||||
|
name: username,
|
||||||
|
service: "PLAYLIST",
|
||||||
|
data64: crowdfundObjectToBase64,
|
||||||
|
title: title.slice(0, 50),
|
||||||
|
description: metadescription,
|
||||||
|
identifier: identifier,
|
||||||
|
tag1: QSUPPORT_FILE_BASE,
|
||||||
|
};
|
||||||
|
|
||||||
|
await qortalRequest(requestBodyJson);
|
||||||
|
if (isNew) {
|
||||||
|
const objectToStore = {
|
||||||
|
title: title.slice(0, 50),
|
||||||
|
description: metadescription,
|
||||||
|
id: identifier,
|
||||||
|
service: "PLAYLIST",
|
||||||
|
name: username,
|
||||||
|
...playlistObject,
|
||||||
|
};
|
||||||
|
dispatch(updateFile(objectToStore));
|
||||||
|
dispatch(updateInHashMap(objectToStore));
|
||||||
|
} else {
|
||||||
|
dispatch(
|
||||||
|
updateFile({
|
||||||
|
...editVideoProperties,
|
||||||
|
...playlistObject,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
dispatch(
|
||||||
|
updateInHashMap({
|
||||||
|
...editVideoProperties,
|
||||||
|
...playlistObject,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
} catch (error: any) {
|
||||||
|
let notificationObj: any = null;
|
||||||
|
if (typeof error === "string") {
|
||||||
|
notificationObj = {
|
||||||
|
msg: error || "Failed to publish update",
|
||||||
|
alertType: "error",
|
||||||
|
};
|
||||||
|
} else if (typeof error?.error === "string") {
|
||||||
|
notificationObj = {
|
||||||
|
msg: error?.error || "Failed to publish update",
|
||||||
|
alertType: "error",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
notificationObj = {
|
||||||
|
msg: error?.message || "Failed to publish update",
|
||||||
|
alertType: "error",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!notificationObj) return;
|
||||||
|
dispatch(setNotification(notificationObj));
|
||||||
|
|
||||||
|
throw new Error("Failed to publish update");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOnchange = (index: number, type: string, value: string) => {
|
||||||
|
// setFiles((prev) => {
|
||||||
|
// let formattedValue = value
|
||||||
|
// console.log({type})
|
||||||
|
// if(type === 'title'){
|
||||||
|
// formattedValue = value.replace(/[^a-zA-Z0-9\s]/g, "")
|
||||||
|
// }
|
||||||
|
// const copyFiles = [...prev];
|
||||||
|
// copyFiles[index] = {
|
||||||
|
// ...copyFiles[index],
|
||||||
|
// [type]: formattedValue,
|
||||||
|
// };
|
||||||
|
// return copyFiles;
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOptionCategoryChangeVideos = (
|
||||||
|
event: SelectChangeEvent<string>
|
||||||
|
) => {
|
||||||
|
const optionId = event.target.value;
|
||||||
|
const selectedOption = firstCategories.find(
|
||||||
|
option => option.id === +optionId
|
||||||
|
);
|
||||||
|
setSelectedCategoryVideos(selectedOption || null);
|
||||||
|
};
|
||||||
|
const handleOptionSubCategoryChangeVideos = (
|
||||||
|
event: SelectChangeEvent<string>,
|
||||||
|
subcategories: any[]
|
||||||
|
) => {
|
||||||
|
const optionId = event.target.value;
|
||||||
|
const selectedOption = subcategories.find(
|
||||||
|
option => option.id === +optionId
|
||||||
|
);
|
||||||
|
setSelectedSubCategoryVideos(selectedOption || null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeVideo = index => {
|
||||||
|
const copyData = structuredClone(playlistData);
|
||||||
|
copyData.videos.splice(index, 1);
|
||||||
|
setPlaylistData(copyData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addVideo = data => {
|
||||||
|
if (playlistData?.videos?.length > 9) {
|
||||||
|
dispatch(
|
||||||
|
setNotification({
|
||||||
|
msg: "Max 10 videos per playlist",
|
||||||
|
alertType: "error",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const copyData = structuredClone(playlistData);
|
||||||
|
copyData.videos = [...copyData.videos, { ...data }];
|
||||||
|
setPlaylistData(copyData);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
open={!!editVideoProperties}
|
||||||
|
aria-labelledby="modal-title"
|
||||||
|
aria-describedby="modal-description"
|
||||||
|
>
|
||||||
|
<ModalBody>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isNew ? (
|
||||||
|
<NewCrowdfundTitle>Create new playlist</NewCrowdfundTitle>
|
||||||
|
) : (
|
||||||
|
<NewCrowdfundTitle>Update Playlist properties</NewCrowdfundTitle>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "20px",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl fullWidth sx={{ marginBottom: 2 }}>
|
||||||
|
<InputLabel id="Category">Select a Category</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="Category"
|
||||||
|
input={<OutlinedInput label="Select a Category" />}
|
||||||
|
value={selectedCategoryVideos?.id || ""}
|
||||||
|
onChange={handleOptionCategoryChangeVideos}
|
||||||
|
>
|
||||||
|
{firstCategories.map(option => (
|
||||||
|
<MenuItem key={option.id} value={option.id}>
|
||||||
|
{option.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
{selectedCategoryVideos &&
|
||||||
|
secondCategories[selectedCategoryVideos?.id] && (
|
||||||
|
<FormControl fullWidth sx={{ marginBottom: 2 }}>
|
||||||
|
<InputLabel id="Category">Select a Sub-Category</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="Sub-Category"
|
||||||
|
input={<OutlinedInput label="Select a Sub-Category" />}
|
||||||
|
value={selectedSubCategoryVideos?.id || ""}
|
||||||
|
onChange={e =>
|
||||||
|
handleOptionSubCategoryChangeVideos(
|
||||||
|
e,
|
||||||
|
secondCategories[selectedCategoryVideos?.id]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{secondCategories[selectedCategoryVideos.id].map(
|
||||||
|
option => (
|
||||||
|
<MenuItem key={option.id} value={option.id}>
|
||||||
|
{option.name}
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<React.Fragment>
|
||||||
|
{!coverImage ? (
|
||||||
|
<ImageUploader onPick={(img: string[]) => setCoverImage(img)}>
|
||||||
|
<AddCoverImageButton variant="contained">
|
||||||
|
Add Cover Image
|
||||||
|
<AddLogoIcon
|
||||||
|
sx={{
|
||||||
|
height: "25px",
|
||||||
|
width: "auto",
|
||||||
|
}}
|
||||||
|
></AddLogoIcon>
|
||||||
|
</AddCoverImageButton>
|
||||||
|
</ImageUploader>
|
||||||
|
) : (
|
||||||
|
<LogoPreviewRow>
|
||||||
|
{coverImage.map(
|
||||||
|
image =>
|
||||||
|
image && (
|
||||||
|
<CoverImagePreview src={image} alt="logo" key={image} />
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<TimesIcon
|
||||||
|
color={theme.palette.text.primary}
|
||||||
|
onClickFunc={() => setCoverImage([])}
|
||||||
|
height={"32"}
|
||||||
|
width={"32"}
|
||||||
|
></TimesIcon>
|
||||||
|
</LogoPreviewRow>
|
||||||
|
)}
|
||||||
|
<CustomInputField
|
||||||
|
name="title"
|
||||||
|
label="Title of playlist"
|
||||||
|
variant="filled"
|
||||||
|
value={title}
|
||||||
|
onChange={e => {
|
||||||
|
const value = e.target.value;
|
||||||
|
const formattedValue = value.replace(
|
||||||
|
/[^a-zA-Z0-9\s-_!?]/g,
|
||||||
|
""
|
||||||
|
);
|
||||||
|
setTitle(formattedValue);
|
||||||
|
}}
|
||||||
|
inputProps={{ maxLength: 180 }}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{/* <CustomInputField
|
||||||
|
name="description"
|
||||||
|
label="Describe your playlist in a few words"
|
||||||
|
variant="filled"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
inputProps={{ maxLength: 10000 }}
|
||||||
|
multiline
|
||||||
|
maxRows={3}
|
||||||
|
required
|
||||||
|
/> */}
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "18px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Description of playlist
|
||||||
|
</Typography>
|
||||||
|
<TextEditor
|
||||||
|
inlineContent={description}
|
||||||
|
setInlineContent={value => {
|
||||||
|
setDescription(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
|
||||||
|
<PlaylistListEdit
|
||||||
|
playlistData={playlistData}
|
||||||
|
removeVideo={removeVideo}
|
||||||
|
addVideo={addVideo}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
|
||||||
|
<CrowdfundActionButtonRow>
|
||||||
|
<CrowdfundActionButton
|
||||||
|
onClick={() => {
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</CrowdfundActionButton>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "20px",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CrowdfundActionButton
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => {
|
||||||
|
publishQDNResource();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Publish
|
||||||
|
</CrowdfundActionButton>
|
||||||
|
</Box>
|
||||||
|
</CrowdfundActionButtonRow>
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
586
src/components/EditPlaylist/Upload-styles.tsx
Normal file
@ -0,0 +1,586 @@
|
|||||||
|
import { styled } from "@mui/system";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Grid,
|
||||||
|
Rating,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
Select
|
||||||
|
} from "@mui/material";
|
||||||
|
import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternate";
|
||||||
|
import { TimesSVG } from "../../assets/svgs/TimesSVG";
|
||||||
|
|
||||||
|
export const DoubleLine = styled(Typography)`
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
overflow: hidden;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const MainContainer = styled(Grid)({
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
justifyContent: "center",
|
||||||
|
margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MainCol = styled(Grid)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
width: "100%",
|
||||||
|
padding: "20px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CreateContainer = styled(Box)(({ theme }) => ({
|
||||||
|
position: "fixed",
|
||||||
|
bottom: "20px",
|
||||||
|
right: "20px",
|
||||||
|
cursor: "pointer",
|
||||||
|
background: theme.palette.background.default,
|
||||||
|
width: "50px",
|
||||||
|
height: "50px",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
borderRadius: "50%",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const ModalBody = styled(Box)(({ theme }) => ({
|
||||||
|
position: "absolute",
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
borderRadius: "4px",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
width: "75%",
|
||||||
|
maxWidth: "900px",
|
||||||
|
padding: "15px 35px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "17px",
|
||||||
|
overflowY: "auto",
|
||||||
|
maxHeight: "95vh",
|
||||||
|
boxShadow:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)"
|
||||||
|
: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px",
|
||||||
|
"&::-webkit-scrollbar-track": {
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar-track:hover": {
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar": {
|
||||||
|
width: "16px",
|
||||||
|
height: "10px",
|
||||||
|
backgroundColor: theme.palette.mode === "light" ? "#f6f8fa" : "#292d3e",
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar-thumb": {
|
||||||
|
backgroundColor: theme.palette.mode === "light" ? "#d3d9e1" : "#575757",
|
||||||
|
borderRadius: "8px",
|
||||||
|
backgroundClip: "content-box",
|
||||||
|
border: "4px solid transparent",
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar-thumb:hover": {
|
||||||
|
backgroundColor: theme.palette.mode === "light" ? "#b7bcc4" : "#474646",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const NewCrowdfundTitle = styled(Typography)(({ theme }) => ({
|
||||||
|
fontWeight: 400,
|
||||||
|
fontFamily: "Raleway",
|
||||||
|
fontSize: "25px",
|
||||||
|
userSelect: "none",
|
||||||
|
}));
|
||||||
|
export const NewCrowdFundFont = styled(Typography)(({ theme }) => ({
|
||||||
|
fontWeight: 400,
|
||||||
|
fontFamily: "Raleway",
|
||||||
|
fontSize: "18px",
|
||||||
|
userSelect: "none",
|
||||||
|
}));
|
||||||
|
export const NewCrowdfundTimeDescription = styled(Typography)(({ theme }) => ({
|
||||||
|
fontWeight: 400,
|
||||||
|
fontFamily: "Raleway",
|
||||||
|
fontSize: "18px",
|
||||||
|
userSelect: "none",
|
||||||
|
fontStyle: "italic",
|
||||||
|
textDecoration: "underline",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CustomInputField = styled(TextField)(({ theme }) => ({
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "19px",
|
||||||
|
letterSpacing: "0px",
|
||||||
|
fontWeight: 400,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
borderColor: theme.palette.background.paper,
|
||||||
|
"& label": {
|
||||||
|
color: theme.palette.mode === "light" ? "#808183" : "#edeef0",
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "19px",
|
||||||
|
letterSpacing: "0px",
|
||||||
|
fontWeight: 400,
|
||||||
|
},
|
||||||
|
"& label.Mui-focused": {
|
||||||
|
color: theme.palette.mode === "light" ? "#A0AAB4" : "#d7d8da",
|
||||||
|
},
|
||||||
|
"& .MuiInput-underline:after": {
|
||||||
|
borderBottomColor: theme.palette.mode === "light" ? "#B2BAC2" : "#c9cccf",
|
||||||
|
},
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
"& fieldset": {
|
||||||
|
borderColor: "#E0E3E7",
|
||||||
|
},
|
||||||
|
"&:hover fieldset": {
|
||||||
|
borderColor: "#B2BAC2",
|
||||||
|
},
|
||||||
|
"&.Mui-focused fieldset": {
|
||||||
|
borderColor: "#6F7E8C",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"& .MuiInputBase-root": {
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "19px",
|
||||||
|
letterSpacing: "0px",
|
||||||
|
fontWeight: 400,
|
||||||
|
},
|
||||||
|
"& [class$='-MuiFilledInput-root']": {
|
||||||
|
padding: "30px 12px 8px",
|
||||||
|
},
|
||||||
|
"& .MuiFilledInput-root:after": {
|
||||||
|
borderBottomColor: theme.palette.secondary.main,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const CrowdfundTitle = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Copse",
|
||||||
|
letterSpacing: "1px",
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: "20px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundSubTitleRow = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "100%",
|
||||||
|
flexDirection: "row",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CrowdfundSubTitle = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Copse",
|
||||||
|
letterSpacing: "1px",
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: "17px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
borderBottom: `1px solid ${theme.palette.text.primary}`,
|
||||||
|
paddingBottom: "1.5px",
|
||||||
|
width: "fit-content",
|
||||||
|
textDecoration: "none",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundDescription = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Raleway",
|
||||||
|
fontSize: "16px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const Spacer = ({ height }: any) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: height,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StyledCardHeaderComment = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
gap: "5px",
|
||||||
|
padding: "7px 0px",
|
||||||
|
});
|
||||||
|
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 AuthorTextComment = styled(Typography)({
|
||||||
|
fontFamily: "Raleway, sans-serif",
|
||||||
|
fontSize: "16px",
|
||||||
|
lineHeight: "1.2",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AddLogoIcon = styled(AddPhotoAlternateIcon)(({ theme }) => ({
|
||||||
|
color: "#fff",
|
||||||
|
height: "25px",
|
||||||
|
width: "auto",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CoverImagePreview = styled("img")(({ theme }) => ({
|
||||||
|
width: "100px",
|
||||||
|
height: "100px",
|
||||||
|
objectFit: "contain",
|
||||||
|
userSelect: "none",
|
||||||
|
borderRadius: "3px",
|
||||||
|
marginBottom: "10px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const LogoPreviewRow = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const TimesIcon = styled(TimesSVG)(({ theme }) => ({
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
borderRadius: "50%",
|
||||||
|
padding: "5px",
|
||||||
|
transition: "all 0.2s ease-in-out",
|
||||||
|
"&:hover": {
|
||||||
|
cursor: "pointer",
|
||||||
|
scale: "1.1",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundCardTitle = styled(DoubleLine)(({ theme }) => ({
|
||||||
|
fontFamily: "Montserrat",
|
||||||
|
fontSize: "24px",
|
||||||
|
letterSpacing: "-0.3px",
|
||||||
|
userSelect: "none",
|
||||||
|
marginBottom: "auto",
|
||||||
|
textAlign: "center",
|
||||||
|
"@media (max-width: 650px)": {
|
||||||
|
fontSize: "18px",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundUploadDate = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Montserrat",
|
||||||
|
fontSize: "12px",
|
||||||
|
letterSpacing: "0.2px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CATContainer = styled(Box)(({ theme }) => ({
|
||||||
|
position: "relative",
|
||||||
|
display: "flex",
|
||||||
|
padding: "15px",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "20px",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "100%",
|
||||||
|
alignItems: "center",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const AddCrowdFundButton = styled(Button)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
textTransform: "none",
|
||||||
|
padding: "10px 25px",
|
||||||
|
fontSize: "15px",
|
||||||
|
gap: "8px",
|
||||||
|
color: "#ffffff",
|
||||||
|
backgroundColor:
|
||||||
|
theme.palette.mode === "dark" ? theme.palette.primary.main : "#2a9a86",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "5px",
|
||||||
|
transition: "all 0.3s ease-in-out",
|
||||||
|
"&:hover": {
|
||||||
|
cursor: "pointer",
|
||||||
|
backgroundColor:
|
||||||
|
theme.palette.mode === "dark" ? theme.palette.primary.dark : "#217e6d",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const EditCrowdFundButton = styled(Button)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
textTransform: "none",
|
||||||
|
padding: "5px 12px",
|
||||||
|
gap: "8px",
|
||||||
|
color: "#ffffff",
|
||||||
|
backgroundColor:
|
||||||
|
theme.palette.mode === "dark" ? theme.palette.primary.main : "#2a9a86",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "5px",
|
||||||
|
transition: "all 0.3s ease-in-out",
|
||||||
|
"&:hover": {
|
||||||
|
cursor: "pointer",
|
||||||
|
backgroundColor:
|
||||||
|
theme.palette.mode === "dark" ? theme.palette.primary.dark : "#217e6d",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundListWrapper = styled(Box)(({ theme }) => ({
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: "0px",
|
||||||
|
background: theme.palette.background.default,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundTitleRow = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "100%",
|
||||||
|
gap: "10px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundPageTitle = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Copse",
|
||||||
|
fontSize: "35px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: "1px",
|
||||||
|
userSelect: "none",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundStatusRow = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "21px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: 0,
|
||||||
|
border: `1px solid ${theme.palette.text.primary}`,
|
||||||
|
borderRadius: "8px",
|
||||||
|
padding: "15px 25px",
|
||||||
|
userSelect: "none",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundDescriptionRow = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "100%",
|
||||||
|
fontFamily: "Montserrat",
|
||||||
|
fontSize: "18px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AboutMyCrowdfund = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Copse",
|
||||||
|
fontSize: "23px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: "1px",
|
||||||
|
userSelect: "none",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundInlineContentRow = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "100%",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CrowdfundInlineContent = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "19px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: 0,
|
||||||
|
userSelect: "none",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundAccordion = styled(Accordion)(({ theme }) => ({
|
||||||
|
backgroundColor: theme.palette.primary.light,
|
||||||
|
"& .Mui-expanded": {
|
||||||
|
minHeight: "auto !important",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundAccordionSummary = styled(AccordionSummary)({
|
||||||
|
height: "50px",
|
||||||
|
"& .Mui-expanded": {
|
||||||
|
margin: "0px !important",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CrowdfundAccordionFont = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "20px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: "0px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundAccordionDetails = styled(AccordionDetails)({
|
||||||
|
padding: "0px 16px 16px 16px",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AddCoverImageButton = styled(Button)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
fontFamily: "Montserrat",
|
||||||
|
fontSize: "16px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: "0.2px",
|
||||||
|
color: "white",
|
||||||
|
gap: "5px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CoverImage = styled("img")({
|
||||||
|
width: "100%",
|
||||||
|
height: "250px",
|
||||||
|
objectFit: "cover",
|
||||||
|
objectPosition: "center",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CrowdfundActionButtonRow = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
width: "100%",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CrowdfundActionButton = styled(Button)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
fontFamily: "Montserrat",
|
||||||
|
fontSize: "16px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: "0.2px",
|
||||||
|
color: "white",
|
||||||
|
gap: "5px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const BackToHomeButton = styled(Button)(({ theme }) => ({
|
||||||
|
position: "absolute",
|
||||||
|
top: "20px",
|
||||||
|
left: "20px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
fontFamily: "Montserrat",
|
||||||
|
fontSize: "13px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: "0.2px",
|
||||||
|
color: "white",
|
||||||
|
gap: "5px",
|
||||||
|
padding: "5px 10px",
|
||||||
|
backgroundColor: theme.palette.secondary.main,
|
||||||
|
transition: "all 0.3s ease-in-out",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: theme.palette.secondary.dark,
|
||||||
|
cursor: "pointer",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundLoaderRow = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: "10px",
|
||||||
|
padding: "10px",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const RatingContainer = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "1px 5px",
|
||||||
|
borderRadius: "5px",
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
transition: "all 0.3s ease-in-out",
|
||||||
|
"&:hover": {
|
||||||
|
cursor: "pointer",
|
||||||
|
backgroundColor: "#e4ddddac",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const StyledRating = styled(Rating)({
|
||||||
|
fontSize: "28px",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const NoReviewsFont = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: 0,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledButton = styled(Button)(({ theme }) => ({
|
||||||
|
fontWeight: 600,
|
||||||
|
color: theme.palette.text.primary
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const CustomSelect = styled(Select)(({ theme }) => ({
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "19px",
|
||||||
|
letterSpacing: "0px",
|
||||||
|
fontWeight: 400,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
'& .MuiSelect-select': {
|
||||||
|
padding: '12px',
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "19px",
|
||||||
|
letterSpacing: "0px",
|
||||||
|
fontWeight: 400,
|
||||||
|
borderRadius: theme.shape.borderRadius, // Match border radius
|
||||||
|
},
|
||||||
|
'&:before': {
|
||||||
|
// Underline style
|
||||||
|
borderBottomColor: theme.palette.mode === "light" ? "#B2BAC2" : "#c9cccf",
|
||||||
|
},
|
||||||
|
'&:after': {
|
||||||
|
// Underline style when focused
|
||||||
|
borderBottomColor: theme.palette.secondary.main,
|
||||||
|
},
|
||||||
|
'& .MuiOutlinedInput-root': {
|
||||||
|
'& fieldset': {
|
||||||
|
borderColor: "#E0E3E7",
|
||||||
|
},
|
||||||
|
'&:hover fieldset': {
|
||||||
|
borderColor: "#B2BAC2",
|
||||||
|
},
|
||||||
|
'&.Mui-focused fieldset': {
|
||||||
|
borderColor: "#6F7E8C",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'& .MuiInputBase-root': {
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "19px",
|
||||||
|
letterSpacing: "0px",
|
||||||
|
fontWeight: 400,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
},
|
||||||
|
}));
|
210
src/components/PlaylistListEdit/PlaylistListEdit.tsx
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { CardContentContainerComment } from "../common/Comments/Comments-styles";
|
||||||
|
import {
|
||||||
|
CrowdfundSubTitle,
|
||||||
|
CrowdfundSubTitleRow,
|
||||||
|
} from "../PublishIssue/Upload-styles.tsx";
|
||||||
|
import { Box, Button, Input, Typography, useTheme } from "@mui/material";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
|
||||||
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { RootState } from "../../state/store";
|
||||||
|
|
||||||
|
export const PlaylistListEdit = ({ playlistData, removeVideo, addVideo }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const username = useSelector((state: RootState) => state.auth?.user?.name);
|
||||||
|
|
||||||
|
const [searchResults, setSearchResults] = useState([]);
|
||||||
|
const [filterSearch, setFilterSearch] = useState("");
|
||||||
|
const search = async () => {
|
||||||
|
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&mode=ALL&identifier=${QSUPPORT_FILE_BASE}&title=${filterSearch}&limit=20&includemetadata=true&reverse=true&name=${username}&exactmatchnames=true&offset=0`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const responseDataSearchVid = await response.json();
|
||||||
|
setSearchResults(responseDataSearchVid);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "10px",
|
||||||
|
width: "100%",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
|
||||||
|
maxWidth: "300px",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CrowdfundSubTitleRow>
|
||||||
|
<CrowdfundSubTitle>Playlist</CrowdfundSubTitle>
|
||||||
|
</CrowdfundSubTitleRow>
|
||||||
|
<CardContentContainerComment
|
||||||
|
sx={{
|
||||||
|
marginTop: "25px",
|
||||||
|
height: "450px",
|
||||||
|
overflow: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{playlistData?.videos?.map((vid, index) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={vid?.identifier}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "10px",
|
||||||
|
width: "100%",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "10px",
|
||||||
|
borderRadius: "5px",
|
||||||
|
userSelect: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{index + 1}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "18px",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{vid?.metadata?.title}
|
||||||
|
</Typography>
|
||||||
|
<DeleteOutlineIcon
|
||||||
|
onClick={() => {
|
||||||
|
removeVideo(index);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CardContentContainerComment>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
|
||||||
|
maxWidth: "300px",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CrowdfundSubTitleRow>
|
||||||
|
<CrowdfundSubTitle>Add videos to playlist</CrowdfundSubTitle>
|
||||||
|
</CrowdfundSubTitleRow>
|
||||||
|
<CardContentContainerComment
|
||||||
|
sx={{
|
||||||
|
marginTop: "25px",
|
||||||
|
height: "450px",
|
||||||
|
overflow: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
id="standard-adornment-name"
|
||||||
|
onChange={e => {
|
||||||
|
setFilterSearch(e.target.value);
|
||||||
|
}}
|
||||||
|
value={filterSearch}
|
||||||
|
placeholder="Search by title"
|
||||||
|
sx={{
|
||||||
|
borderBottom: "1px solid white",
|
||||||
|
"&&:before": {
|
||||||
|
borderBottom: "none",
|
||||||
|
},
|
||||||
|
"&&:after": {
|
||||||
|
borderBottom: "none",
|
||||||
|
},
|
||||||
|
"&&:hover:before": {
|
||||||
|
borderBottom: "none",
|
||||||
|
},
|
||||||
|
"&&.Mui-focused:before": {
|
||||||
|
borderBottom: "none",
|
||||||
|
},
|
||||||
|
"&&.Mui-focused": {
|
||||||
|
outline: "none",
|
||||||
|
},
|
||||||
|
fontSize: "18px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
search();
|
||||||
|
}}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{searchResults?.map((vid, index) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={vid?.identifier}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "10px",
|
||||||
|
width: "100%",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "10px",
|
||||||
|
borderRadius: "5px",
|
||||||
|
userSelect: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{index + 1}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "18px",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{vid?.metadata?.title}
|
||||||
|
</Typography>
|
||||||
|
<AddIcon
|
||||||
|
onClick={() => {
|
||||||
|
addVideo(vid);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CardContentContainerComment>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
79
src/components/Playlists/Playlists.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { CardContentContainerComment } from "../common/Comments/Comments-styles";
|
||||||
|
import {
|
||||||
|
CrowdfundSubTitle,
|
||||||
|
CrowdfundSubTitleRow,
|
||||||
|
} from "../PublishIssue/Upload-styles.tsx";
|
||||||
|
import { Box, Typography, useTheme } from "@mui/material";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export const Playlists = ({ playlistData, currentVideoIdentifier }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
|
||||||
|
maxWidth: "400px",
|
||||||
|
width: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CrowdfundSubTitleRow>
|
||||||
|
<CrowdfundSubTitle>Playlist</CrowdfundSubTitle>
|
||||||
|
</CrowdfundSubTitleRow>
|
||||||
|
<CardContentContainerComment
|
||||||
|
sx={{
|
||||||
|
marginTop: "25px",
|
||||||
|
height: "450px",
|
||||||
|
overflow: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{playlistData?.videos?.map((vid, index) => {
|
||||||
|
const isCurrentVidPlayling =
|
||||||
|
vid?.identifier === currentVideoIdentifier;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
key={vid?.identifier}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "10px",
|
||||||
|
width: "100%",
|
||||||
|
background: isCurrentVidPlayling && theme.palette.primary.main,
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "10px",
|
||||||
|
borderRadius: "5px",
|
||||||
|
cursor: isCurrentVidPlayling ? "default" : "pointer",
|
||||||
|
userSelect: "none",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (isCurrentVidPlayling) return;
|
||||||
|
|
||||||
|
navigate(`/video/${vid.name}/${vid.identifier}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "14px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{index + 1}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "18px",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{vid?.metadata?.title}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CardContentContainerComment>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
493
src/components/PublishIssue/PublishIssue.tsx
Normal file
@ -0,0 +1,493 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
ActionButton,
|
||||||
|
ActionButtonRow,
|
||||||
|
CustomInputField,
|
||||||
|
ModalBody,
|
||||||
|
NewCrowdfundTitle,
|
||||||
|
StyledButton,
|
||||||
|
} from "./Upload-styles";
|
||||||
|
import { Box, Modal, Typography, useTheme } from "@mui/material";
|
||||||
|
import RemoveIcon from "@mui/icons-material/Remove";
|
||||||
|
import ShortUniqueId from "short-unique-id";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import AddBoxIcon from "@mui/icons-material/AddBox";
|
||||||
|
import { useDropzone } from "react-dropzone";
|
||||||
|
|
||||||
|
import { setNotification } from "../../state/features/notificationsSlice";
|
||||||
|
import { objectToBase64 } from "../../utils/toBase64";
|
||||||
|
import { RootState } from "../../state/store";
|
||||||
|
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
|
||||||
|
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll";
|
||||||
|
import { TextEditor } from "../common/TextEditor/TextEditor";
|
||||||
|
import { extractTextFromHTML } from "../common/TextEditor/utils";
|
||||||
|
import { allCategoryData } from "../../constants/Categories/1stCategories.ts";
|
||||||
|
import { titleFormatter } from "../../constants/Misc.ts";
|
||||||
|
import {
|
||||||
|
appendCategoryToList,
|
||||||
|
CategoryList,
|
||||||
|
CategoryListRef,
|
||||||
|
} from "../common/CategoryList/CategoryList.tsx";
|
||||||
|
import { SupportState } from "../../constants/Categories/2ndCategories.ts";
|
||||||
|
import {
|
||||||
|
ImagePublisher,
|
||||||
|
ImagePublisherRef,
|
||||||
|
} from "../common/ImagePublisher/ImagePublisher.tsx";
|
||||||
|
|
||||||
|
const uid = new ShortUniqueId();
|
||||||
|
const shortuid = new ShortUniqueId({ length: 5 });
|
||||||
|
|
||||||
|
interface NewCrowdfundProps {
|
||||||
|
editId?: string;
|
||||||
|
editContent?: null | {
|
||||||
|
title: string;
|
||||||
|
user: string;
|
||||||
|
coverImage: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VideoFile {
|
||||||
|
file: File;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
coverImage?: string;
|
||||||
|
}
|
||||||
|
export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false);
|
||||||
|
const username = useSelector((state: RootState) => state.auth?.user?.name);
|
||||||
|
const userAddress = useSelector(
|
||||||
|
(state: RootState) => state.auth?.user?.address
|
||||||
|
);
|
||||||
|
const [files, setFiles] = useState<VideoFile[]>([]);
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
|
const [title, setTitle] = useState<string>("");
|
||||||
|
const [description, setDescription] = useState<string>("");
|
||||||
|
const [step, setStep] = useState<string>("videos");
|
||||||
|
const [playlistCoverImage, setPlaylistCoverImage] = useState<null | string>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [selectExistingPlaylist, setSelectExistingPlaylist] =
|
||||||
|
useState<any>(null);
|
||||||
|
const [playlistTitle, setPlaylistTitle] = useState<string>("");
|
||||||
|
const [playlistDescription, setPlaylistDescription] = useState<string>("");
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState<any>(null);
|
||||||
|
const [selectedSubCategory, setSelectedSubCategory] = useState<any>(null);
|
||||||
|
|
||||||
|
const [playlistSetting, setPlaylistSetting] = useState<null | string>(null);
|
||||||
|
const [publishes, setPublishes] = useState<any>(null);
|
||||||
|
const categoryListRef = useRef<CategoryListRef>(null);
|
||||||
|
const imagePublisherRef = useRef<ImagePublisherRef>(null);
|
||||||
|
const { getRootProps, getInputProps } = useDropzone({
|
||||||
|
maxFiles: 10,
|
||||||
|
maxSize: 419430400, // 400 MB in bytes
|
||||||
|
onDrop: (acceptedFiles, rejectedFiles) => {
|
||||||
|
const formatArray = acceptedFiles.map(item => {
|
||||||
|
return {
|
||||||
|
file: item,
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
coverImage: "",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
setFiles(prev => [...prev, ...formatArray]);
|
||||||
|
|
||||||
|
let errorString = null;
|
||||||
|
rejectedFiles.forEach(({ file, errors }) => {
|
||||||
|
errors.forEach(error => {
|
||||||
|
if (error.code === "file-too-large") {
|
||||||
|
errorString = "File must be under 400mb";
|
||||||
|
}
|
||||||
|
console.log(`Error with file ${file.name}: ${error.message}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (errorString) {
|
||||||
|
const notificationObj = {
|
||||||
|
msg: errorString,
|
||||||
|
alertType: "error",
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(setNotification(notificationObj));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (editContent) {
|
||||||
|
}
|
||||||
|
}, [editContent]);
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function publishQDNResource() {
|
||||||
|
try {
|
||||||
|
if (!categoryListRef.current) throw new Error("No CategoryListRef found");
|
||||||
|
if (!userAddress) throw new Error("Unable to locate user address");
|
||||||
|
|
||||||
|
if (!title) throw new Error("Please enter a title");
|
||||||
|
if (!description) throw new Error("Please enter a description");
|
||||||
|
if (!categoryListRef.current?.getSelectedCategories()[0])
|
||||||
|
throw new Error("Please select a category");
|
||||||
|
let errorMsg = "";
|
||||||
|
let name = "";
|
||||||
|
if (username) {
|
||||||
|
name = username;
|
||||||
|
}
|
||||||
|
if (!name) {
|
||||||
|
errorMsg =
|
||||||
|
"Cannot publish without access to your name. Please authenticate.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editId && editContent?.user !== name) {
|
||||||
|
errorMsg = "Cannot publish another user's resource";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorMsg) {
|
||||||
|
dispatch(
|
||||||
|
setNotification({
|
||||||
|
msg: errorMsg,
|
||||||
|
alertType: "error",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileReferences = [];
|
||||||
|
|
||||||
|
let listOfPublishes = [];
|
||||||
|
|
||||||
|
const fullDescription = extractTextFromHTML(description);
|
||||||
|
|
||||||
|
const sanitizeTitle = title
|
||||||
|
.replace(/[^a-zA-Z0-9\s-]/g, "")
|
||||||
|
.replace(/\s+/g, "-")
|
||||||
|
.replace(/-+/g, "-")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
|
||||||
|
for (const publish of files) {
|
||||||
|
const file = publish.file;
|
||||||
|
const id = uid();
|
||||||
|
|
||||||
|
const identifier = `${QSUPPORT_FILE_BASE}${sanitizeTitle.slice(0, 30)}_${id}`;
|
||||||
|
|
||||||
|
let fileExtension = "";
|
||||||
|
const fileExtensionSplit = file?.name?.split(".");
|
||||||
|
if (fileExtensionSplit?.length > 1) {
|
||||||
|
fileExtension = fileExtensionSplit?.pop() || "";
|
||||||
|
}
|
||||||
|
let firstPartName = fileExtensionSplit[0];
|
||||||
|
|
||||||
|
let filename = firstPartName.slice(0, 15);
|
||||||
|
|
||||||
|
// Step 1: Replace all white spaces with underscores
|
||||||
|
|
||||||
|
// Replace all forms of whitespace (including non-standard ones) with underscores
|
||||||
|
let stringWithUnderscores = filename.replace(/[\s\uFEFF\xA0]+/g, "_");
|
||||||
|
|
||||||
|
// Remove all non-alphanumeric characters (except underscores)
|
||||||
|
let alphanumericString = stringWithUnderscores.replace(
|
||||||
|
/[^a-zA-Z0-9_]/g,
|
||||||
|
""
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fileExtension) {
|
||||||
|
filename = `${alphanumericString.trim()}.${fileExtension}`;
|
||||||
|
} else {
|
||||||
|
filename = alphanumericString;
|
||||||
|
}
|
||||||
|
|
||||||
|
const categoryList = appendCategoryToList(
|
||||||
|
categoryListRef.current?.getSelectedCategories(),
|
||||||
|
"101"
|
||||||
|
);
|
||||||
|
const categoryString = `**${categoryListRef.current?.getCategoriesFetchString(categoryList)}**`;
|
||||||
|
let metadescription = categoryString + fullDescription.slice(0, 150);
|
||||||
|
|
||||||
|
const requestBodyVideo: any = {
|
||||||
|
action: "PUBLISH_QDN_RESOURCE",
|
||||||
|
name: name,
|
||||||
|
service: "FILE",
|
||||||
|
file,
|
||||||
|
title: title.slice(0, 50),
|
||||||
|
description: metadescription,
|
||||||
|
identifier,
|
||||||
|
filename,
|
||||||
|
tag1: QSUPPORT_FILE_BASE,
|
||||||
|
};
|
||||||
|
listOfPublishes.push(requestBodyVideo);
|
||||||
|
fileReferences.push({
|
||||||
|
filename: file.name,
|
||||||
|
identifier,
|
||||||
|
name,
|
||||||
|
service: "FILE",
|
||||||
|
mimetype: file.type,
|
||||||
|
size: file.size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const idMeta = uid();
|
||||||
|
const identifier = `${QSUPPORT_FILE_BASE}${sanitizeTitle.slice(0, 30)}_${idMeta}`;
|
||||||
|
|
||||||
|
const categoryList = appendCategoryToList(
|
||||||
|
categoryListRef.current?.getSelectedCategories(),
|
||||||
|
"101"
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileObject: any = {
|
||||||
|
title,
|
||||||
|
version: 1,
|
||||||
|
fullDescription,
|
||||||
|
htmlDescription: description,
|
||||||
|
commentsId: `${QSUPPORT_FILE_BASE}_cm_${idMeta}`,
|
||||||
|
...categoryListRef.current?.categoriesToObject(categoryList),
|
||||||
|
files: fileReferences,
|
||||||
|
images: imagePublisherRef?.current?.getImageArray(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const categoryString = `**${categoryListRef.current?.getCategoriesFetchString(categoryList)}**`;
|
||||||
|
let metadescription = categoryString + fullDescription.slice(0, 150);
|
||||||
|
|
||||||
|
const fileObjectToBase64 = await objectToBase64(fileObject);
|
||||||
|
// Description is obtained from raw data
|
||||||
|
const requestBodyJson: any = {
|
||||||
|
action: "PUBLISH_QDN_RESOURCE",
|
||||||
|
name: name,
|
||||||
|
service: "DOCUMENT",
|
||||||
|
data64: fileObjectToBase64,
|
||||||
|
title: title.slice(0, 50),
|
||||||
|
description: metadescription,
|
||||||
|
identifier: identifier + "_metadata",
|
||||||
|
tag1: QSUPPORT_FILE_BASE,
|
||||||
|
filename: `video_metadata.json`,
|
||||||
|
};
|
||||||
|
listOfPublishes.push(requestBodyJson);
|
||||||
|
|
||||||
|
const multiplePublish = {
|
||||||
|
action: "PUBLISH_MULTIPLE_QDN_RESOURCES",
|
||||||
|
resources: [...listOfPublishes],
|
||||||
|
};
|
||||||
|
setPublishes(multiplePublish);
|
||||||
|
setIsOpenMultiplePublish(true);
|
||||||
|
} catch (error: any) {
|
||||||
|
let notificationObj: any = null;
|
||||||
|
if (typeof error === "string") {
|
||||||
|
notificationObj = {
|
||||||
|
msg: error || "Failed to publish issue",
|
||||||
|
alertType: "error",
|
||||||
|
};
|
||||||
|
} else if (typeof error?.error === "string") {
|
||||||
|
notificationObj = {
|
||||||
|
msg: error?.error || "Failed to publish issue",
|
||||||
|
alertType: "error",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
notificationObj = {
|
||||||
|
msg: error?.message || "Failed to publish issue",
|
||||||
|
alertType: "error",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!notificationObj) return;
|
||||||
|
dispatch(setNotification(notificationObj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{username && (
|
||||||
|
<>
|
||||||
|
{editId ? null : (
|
||||||
|
<StyledButton
|
||||||
|
color="primary"
|
||||||
|
startIcon={<AddBoxIcon />}
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Open an Issue
|
||||||
|
</StyledButton>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
open={isOpen}
|
||||||
|
aria-labelledby="modal-title"
|
||||||
|
aria-describedby="modal-description"
|
||||||
|
>
|
||||||
|
<ModalBody>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<NewCrowdfundTitle>Issue</NewCrowdfundTitle>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{step === "videos" && (
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
{...getRootProps()}
|
||||||
|
sx={{
|
||||||
|
border: "1px dashed gray",
|
||||||
|
padding: 2,
|
||||||
|
textAlign: "center",
|
||||||
|
marginBottom: 2,
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
<Typography>
|
||||||
|
Publish files related to issue (Optional)
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{files.map((file, index) => {
|
||||||
|
return (
|
||||||
|
<React.Fragment key={index}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography>{file?.file?.name}</Typography>
|
||||||
|
<RemoveIcon
|
||||||
|
onClick={() => {
|
||||||
|
setFiles(prev => {
|
||||||
|
const copyPrev = [...prev];
|
||||||
|
copyPrev.splice(index, 1);
|
||||||
|
return copyPrev;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "20px",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CategoryList
|
||||||
|
categoryData={allCategoryData}
|
||||||
|
ref={categoryListRef}
|
||||||
|
columns={3}
|
||||||
|
excludeCategories={SupportState}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<ImagePublisher ref={imagePublisherRef} />
|
||||||
|
<CustomInputField
|
||||||
|
name="title"
|
||||||
|
label="Title of Issue"
|
||||||
|
variant="filled"
|
||||||
|
value={title}
|
||||||
|
onChange={e => {
|
||||||
|
const value = e.target.value;
|
||||||
|
const formattedValue = value.replace(titleFormatter, "");
|
||||||
|
setTitle(formattedValue);
|
||||||
|
}}
|
||||||
|
inputProps={{ maxLength: 180 }}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "18px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Description of Issue
|
||||||
|
</Typography>
|
||||||
|
<TextEditor
|
||||||
|
inlineContent={description}
|
||||||
|
setInlineContent={value => {
|
||||||
|
setDescription(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<ActionButtonRow>
|
||||||
|
<ActionButton
|
||||||
|
onClick={() => {
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</ActionButton>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "20px",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ActionButton
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => {
|
||||||
|
publishQDNResource();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Publish
|
||||||
|
</ActionButton>
|
||||||
|
</Box>
|
||||||
|
</ActionButtonRow>
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{isOpenMultiplePublish && (
|
||||||
|
<MultiplePublish
|
||||||
|
isOpen={isOpenMultiplePublish}
|
||||||
|
onError={messageNotification => {
|
||||||
|
setIsOpenMultiplePublish(false);
|
||||||
|
setPublishes(null);
|
||||||
|
if (messageNotification) {
|
||||||
|
dispatch(
|
||||||
|
setNotification({
|
||||||
|
msg: messageNotification,
|
||||||
|
alertType: "error",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onSubmit={() => {
|
||||||
|
setIsOpenMultiplePublish(false);
|
||||||
|
setIsOpen(false);
|
||||||
|
setFiles([]);
|
||||||
|
setStep("videos");
|
||||||
|
setPlaylistCoverImage(null);
|
||||||
|
setPlaylistTitle("");
|
||||||
|
setPlaylistDescription("");
|
||||||
|
setSelectedCategory(null);
|
||||||
|
setSelectedSubCategory(null);
|
||||||
|
setPlaylistSetting(null);
|
||||||
|
categoryListRef.current?.clearCategories();
|
||||||
|
dispatch(
|
||||||
|
setNotification({
|
||||||
|
msg: "Issue published",
|
||||||
|
alertType: "success",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
publishes={publishes}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
585
src/components/PublishIssue/Upload-styles.tsx
Normal file
@ -0,0 +1,585 @@
|
|||||||
|
import { styled } from "@mui/system";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Grid,
|
||||||
|
Rating,
|
||||||
|
Select,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternate";
|
||||||
|
import { TimesSVG } from "../../assets/svgs/TimesSVG";
|
||||||
|
|
||||||
|
export const DoubleLine = styled(Typography)`
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
overflow: hidden;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const MainContainer = styled(Grid)({
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
justifyContent: "center",
|
||||||
|
margin: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MainCol = styled(Grid)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
width: "100%",
|
||||||
|
padding: "20px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CreateContainer = styled(Box)(({ theme }) => ({
|
||||||
|
position: "fixed",
|
||||||
|
bottom: "20px",
|
||||||
|
right: "20px",
|
||||||
|
cursor: "pointer",
|
||||||
|
background: theme.palette.background.default,
|
||||||
|
width: "50px",
|
||||||
|
height: "50px",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
borderRadius: "50%",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const ModalBody = styled(Box)(({ theme }) => ({
|
||||||
|
position: "absolute",
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
borderRadius: "4px",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
width: "75%",
|
||||||
|
maxWidth: "900px",
|
||||||
|
padding: "15px 35px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "17px",
|
||||||
|
overflowY: "auto",
|
||||||
|
maxHeight: "95vh",
|
||||||
|
boxShadow:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)"
|
||||||
|
: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px",
|
||||||
|
"&::-webkit-scrollbar-track": {
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar-track:hover": {
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar": {
|
||||||
|
width: "16px",
|
||||||
|
height: "10px",
|
||||||
|
backgroundColor: theme.palette.mode === "light" ? "#f6f8fa" : "#292d3e",
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar-thumb": {
|
||||||
|
backgroundColor: theme.palette.mode === "light" ? "#d3d9e1" : "#575757",
|
||||||
|
borderRadius: "8px",
|
||||||
|
backgroundClip: "content-box",
|
||||||
|
border: "4px solid transparent",
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar-thumb:hover": {
|
||||||
|
backgroundColor: theme.palette.mode === "light" ? "#b7bcc4" : "#474646",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const NewCrowdfundTitle = styled(Typography)(({ theme }) => ({
|
||||||
|
fontWeight: 400,
|
||||||
|
fontFamily: "Raleway",
|
||||||
|
fontSize: "25px",
|
||||||
|
userSelect: "none",
|
||||||
|
}));
|
||||||
|
export const NewCrowdFundFont = styled(Typography)(({ theme }) => ({
|
||||||
|
fontWeight: 400,
|
||||||
|
fontFamily: "Raleway",
|
||||||
|
fontSize: "18px",
|
||||||
|
userSelect: "none",
|
||||||
|
}));
|
||||||
|
export const NewCrowdfundTimeDescription = styled(Typography)(({ theme }) => ({
|
||||||
|
fontWeight: 400,
|
||||||
|
fontFamily: "Raleway",
|
||||||
|
fontSize: "18px",
|
||||||
|
userSelect: "none",
|
||||||
|
fontStyle: "italic",
|
||||||
|
textDecoration: "underline",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CustomInputField = styled(TextField)(({ theme }) => ({
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "19px",
|
||||||
|
letterSpacing: "0px",
|
||||||
|
fontWeight: 400,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
borderColor: theme.palette.background.paper,
|
||||||
|
"& label": {
|
||||||
|
color: theme.palette.mode === "light" ? "#808183" : "#edeef0",
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "19px",
|
||||||
|
letterSpacing: "0px",
|
||||||
|
fontWeight: 400,
|
||||||
|
},
|
||||||
|
"& label.Mui-focused": {
|
||||||
|
color: theme.palette.mode === "light" ? "#A0AAB4" : "#d7d8da",
|
||||||
|
},
|
||||||
|
"& .MuiInput-underline:after": {
|
||||||
|
borderBottomColor: theme.palette.mode === "light" ? "#B2BAC2" : "#c9cccf",
|
||||||
|
},
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
"& fieldset": {
|
||||||
|
borderColor: "#E0E3E7",
|
||||||
|
},
|
||||||
|
"&:hover fieldset": {
|
||||||
|
borderColor: "#B2BAC2",
|
||||||
|
},
|
||||||
|
"&.Mui-focused fieldset": {
|
||||||
|
borderColor: "#6F7E8C",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"& .MuiInputBase-root": {
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "19px",
|
||||||
|
letterSpacing: "0px",
|
||||||
|
fontWeight: 400,
|
||||||
|
},
|
||||||
|
"& [class$='-MuiFilledInput-root']": {
|
||||||
|
padding: "30px 12px 8px",
|
||||||
|
},
|
||||||
|
"& .MuiFilledInput-root:after": {
|
||||||
|
borderBottomColor: theme.palette.secondary.main,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundTitle = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Copse",
|
||||||
|
letterSpacing: "1px",
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: "20px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundSubTitleRow = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "100%",
|
||||||
|
flexDirection: "row",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CrowdfundSubTitle = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Copse",
|
||||||
|
letterSpacing: "1px",
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: "17px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
borderBottom: `1px solid ${theme.palette.text.primary}`,
|
||||||
|
paddingBottom: "1.5px",
|
||||||
|
width: "fit-content",
|
||||||
|
textDecoration: "none",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundDescription = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Raleway",
|
||||||
|
fontSize: "16px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const Spacer = ({ height }: any) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: height,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StyledCardHeaderComment = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
gap: "5px",
|
||||||
|
padding: "7px 0px",
|
||||||
|
});
|
||||||
|
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 AuthorTextComment = styled(Typography)({
|
||||||
|
fontFamily: "Raleway, sans-serif",
|
||||||
|
fontSize: "16px",
|
||||||
|
lineHeight: "1.2",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AddLogoIcon = styled(AddPhotoAlternateIcon)(({ theme }) => ({
|
||||||
|
color: "#fff",
|
||||||
|
height: "25px",
|
||||||
|
width: "auto",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CoverImagePreview = styled("img")(({ theme }) => ({
|
||||||
|
width: "100px",
|
||||||
|
height: "100px",
|
||||||
|
objectFit: "contain",
|
||||||
|
userSelect: "none",
|
||||||
|
borderRadius: "3px",
|
||||||
|
marginBottom: "10px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const LogoPreviewRow = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const TimesIcon = styled(TimesSVG)(({ theme }) => ({
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
borderRadius: "50%",
|
||||||
|
padding: "5px",
|
||||||
|
transition: "all 0.2s ease-in-out",
|
||||||
|
"&:hover": {
|
||||||
|
cursor: "pointer",
|
||||||
|
scale: "1.1",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundCardTitle = styled(DoubleLine)(({ theme }) => ({
|
||||||
|
fontFamily: "Montserrat",
|
||||||
|
fontSize: "24px",
|
||||||
|
letterSpacing: "-0.3px",
|
||||||
|
userSelect: "none",
|
||||||
|
marginBottom: "auto",
|
||||||
|
textAlign: "center",
|
||||||
|
"@media (max-width: 650px)": {
|
||||||
|
fontSize: "18px",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundUploadDate = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Montserrat",
|
||||||
|
fontSize: "12px",
|
||||||
|
letterSpacing: "0.2px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CATContainer = styled(Box)(({ theme }) => ({
|
||||||
|
position: "relative",
|
||||||
|
display: "flex",
|
||||||
|
padding: "15px",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "20px",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "100%",
|
||||||
|
alignItems: "center",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const AddCrowdFundButton = styled(Button)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
textTransform: "none",
|
||||||
|
padding: "10px 25px",
|
||||||
|
fontSize: "15px",
|
||||||
|
gap: "8px",
|
||||||
|
color: "#ffffff",
|
||||||
|
backgroundColor:
|
||||||
|
theme.palette.mode === "dark" ? theme.palette.primary.main : "#2a9a86",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "5px",
|
||||||
|
transition: "all 0.3s ease-in-out",
|
||||||
|
"&:hover": {
|
||||||
|
cursor: "pointer",
|
||||||
|
backgroundColor:
|
||||||
|
theme.palette.mode === "dark" ? theme.palette.primary.dark : "#217e6d",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const EditCrowdFundButton = styled(Button)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
textTransform: "none",
|
||||||
|
padding: "5px 12px",
|
||||||
|
gap: "8px",
|
||||||
|
color: "#ffffff",
|
||||||
|
backgroundColor:
|
||||||
|
theme.palette.mode === "dark" ? theme.palette.primary.main : "#2a9a86",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "5px",
|
||||||
|
transition: "all 0.3s ease-in-out",
|
||||||
|
"&:hover": {
|
||||||
|
cursor: "pointer",
|
||||||
|
backgroundColor:
|
||||||
|
theme.palette.mode === "dark" ? theme.palette.primary.dark : "#217e6d",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundListWrapper = styled(Box)(({ theme }) => ({
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: "0px",
|
||||||
|
background: theme.palette.background.default,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundTitleRow = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "100%",
|
||||||
|
gap: "10px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundPageTitle = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Copse",
|
||||||
|
fontSize: "35px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: "1px",
|
||||||
|
userSelect: "none",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundStatusRow = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "21px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: 0,
|
||||||
|
border: `1px solid ${theme.palette.text.primary}`,
|
||||||
|
borderRadius: "8px",
|
||||||
|
padding: "15px 25px",
|
||||||
|
userSelect: "none",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundDescriptionRow = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "100%",
|
||||||
|
fontFamily: "Montserrat",
|
||||||
|
fontSize: "18px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AboutMyCrowdfund = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Copse",
|
||||||
|
fontSize: "23px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: "1px",
|
||||||
|
userSelect: "none",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundInlineContentRow = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "100%",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CrowdfundInlineContent = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "19px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: 0,
|
||||||
|
userSelect: "none",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundAccordion = styled(Accordion)(({ theme }) => ({
|
||||||
|
backgroundColor: theme.palette.primary.light,
|
||||||
|
"& .Mui-expanded": {
|
||||||
|
minHeight: "auto !important",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundAccordionSummary = styled(AccordionSummary)({
|
||||||
|
height: "50px",
|
||||||
|
"& .Mui-expanded": {
|
||||||
|
margin: "0px !important",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CrowdfundAccordionFont = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "20px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: "0px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundAccordionDetails = styled(AccordionDetails)({
|
||||||
|
padding: "0px 16px 16px 16px",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AddCoverImageButton = styled(Button)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
fontFamily: "Montserrat",
|
||||||
|
fontSize: "16px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: "0.2px",
|
||||||
|
color: "white",
|
||||||
|
gap: "5px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CoverImage = styled("img")({
|
||||||
|
width: "100%",
|
||||||
|
height: "250px",
|
||||||
|
objectFit: "cover",
|
||||||
|
objectPosition: "center",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ActionButtonRow = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
width: "100%",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ActionButton = styled(Button)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
fontFamily: "Montserrat",
|
||||||
|
fontSize: "16px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: "0.2px",
|
||||||
|
color: "white",
|
||||||
|
gap: "5px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const BackToHomeButton = styled(Button)(({ theme }) => ({
|
||||||
|
position: "absolute",
|
||||||
|
top: "20px",
|
||||||
|
left: "20px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
fontFamily: "Montserrat",
|
||||||
|
fontSize: "13px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: "0.2px",
|
||||||
|
color: "white",
|
||||||
|
gap: "5px",
|
||||||
|
padding: "5px 10px",
|
||||||
|
backgroundColor: theme.palette.secondary.main,
|
||||||
|
transition: "all 0.3s ease-in-out",
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: theme.palette.secondary.dark,
|
||||||
|
cursor: "pointer",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CrowdfundLoaderRow = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: "10px",
|
||||||
|
padding: "10px",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const RatingContainer = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "1px 5px",
|
||||||
|
borderRadius: "5px",
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
transition: "all 0.3s ease-in-out",
|
||||||
|
"&:hover": {
|
||||||
|
cursor: "pointer",
|
||||||
|
backgroundColor: "#e4ddddac",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const StyledRating = styled(Rating)({
|
||||||
|
fontSize: "28px",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const NoReviewsFont = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: 0,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StyledButton = styled(Button)(({ theme }) => ({
|
||||||
|
fontWeight: 600,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
fontFamily: "Cairo",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CustomSelect = styled(Select)(({ theme }) => ({
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "19px",
|
||||||
|
letterSpacing: "0px",
|
||||||
|
fontWeight: 400,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
"& .MuiSelect-select": {
|
||||||
|
padding: "12px",
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "19px",
|
||||||
|
letterSpacing: "0px",
|
||||||
|
fontWeight: 400,
|
||||||
|
borderRadius: theme.shape.borderRadius, // Match border radius
|
||||||
|
},
|
||||||
|
"&:before": {
|
||||||
|
// Underline style
|
||||||
|
borderBottomColor: theme.palette.mode === "light" ? "#B2BAC2" : "#c9cccf",
|
||||||
|
},
|
||||||
|
"&:after": {
|
||||||
|
// Underline style when focused
|
||||||
|
borderBottomColor: theme.palette.secondary.main,
|
||||||
|
},
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
"& fieldset": {
|
||||||
|
borderColor: "#E0E3E7",
|
||||||
|
},
|
||||||
|
"&:hover fieldset": {
|
||||||
|
borderColor: "#B2BAC2",
|
||||||
|
},
|
||||||
|
"&.Mui-focused fieldset": {
|
||||||
|
borderColor: "#6F7E8C",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"& .MuiInputBase-root": {
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "19px",
|
||||||
|
letterSpacing: "0px",
|
||||||
|
fontWeight: 400,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
},
|
||||||
|
}));
|
109
src/components/ResponsiveImage.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import React, { useState, useEffect, CSSProperties } from 'react'
|
||||||
|
import Skeleton from '@mui/material/Skeleton'
|
||||||
|
import { Box } from '@mui/material'
|
||||||
|
|
||||||
|
interface ResponsiveImageProps {
|
||||||
|
src: string
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
alt?: string
|
||||||
|
className?: string
|
||||||
|
style?: CSSProperties
|
||||||
|
}
|
||||||
|
|
||||||
|
const ResponsiveImage: React.FC<ResponsiveImageProps> = ({
|
||||||
|
src,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
alt,
|
||||||
|
className,
|
||||||
|
style
|
||||||
|
}) => {
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const aspectRatio = (height / width) * 100
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const imageStyle: CSSProperties = {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
objectFit: 'cover'
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapperStyle: CSSProperties = {
|
||||||
|
position: 'relative',
|
||||||
|
paddingBottom: `${aspectRatio}%`,
|
||||||
|
overflow: 'hidden',
|
||||||
|
...style
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: '2px',
|
||||||
|
maxHeight: '50%'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
{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: '100%',
|
||||||
|
borderRadius: '8px',
|
||||||
|
visibility: loading ? 'hidden' : 'visible',
|
||||||
|
position: loading ? 'absolute' : 'unset',
|
||||||
|
objectFit: 'contain'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</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
|
67
src/components/StatsData.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { styled } from "@mui/system";
|
||||||
|
import { Grid } from "@mui/material";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { RootState } from "../state/store.ts";
|
||||||
|
import { useFetchFiles } from "../hooks/useFetchFiles.tsx";
|
||||||
|
|
||||||
|
export const StatsData = () => {
|
||||||
|
const StatsCol = styled(Grid)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
width: "100%",
|
||||||
|
padding: "20px 0px",
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const {
|
||||||
|
getFiles,
|
||||||
|
checkAndUpdateFile,
|
||||||
|
getFile,
|
||||||
|
hashMapFiles,
|
||||||
|
getNewFiles,
|
||||||
|
checkNewFiles,
|
||||||
|
getFilesFiltered,
|
||||||
|
getFilesCount,
|
||||||
|
} = useFetchFiles();
|
||||||
|
|
||||||
|
const totalIssuesPublished = useSelector(
|
||||||
|
(state: RootState) => state.global.totalFilesPublished
|
||||||
|
);
|
||||||
|
const totalNamesPublished = useSelector(
|
||||||
|
(state: RootState) => state.global.totalNamesPublished
|
||||||
|
);
|
||||||
|
const issuesPerNamePublished = useSelector(
|
||||||
|
(state: RootState) => state.global.filesPerNamePublished
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getFilesCount();
|
||||||
|
}, [getFilesCount]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
totalIssuesPublished > 0 && (
|
||||||
|
<StatsCol>
|
||||||
|
<div>
|
||||||
|
Issues Published:{" "}
|
||||||
|
<span style={{ fontWeight: "bold" }}>
|
||||||
|
{totalIssuesPublished || ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Publishers:{" "}
|
||||||
|
<span style={{ fontWeight: "bold" }}>
|
||||||
|
{totalNamesPublished || ""}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Average:{" "}
|
||||||
|
<span style={{ fontWeight: "bold" }}>
|
||||||
|
{issuesPerNamePublished > 0 &&
|
||||||
|
Number(issuesPerNamePublished).toFixed(0)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</StatsCol>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,28 @@
|
|||||||
|
import { styled } from '@mui/system';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Modal,
|
||||||
|
Typography
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
export const StyledModal = styled(Modal)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const ModalContent = styled(Box)(({ theme }) => ({
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
padding: theme.spacing(4),
|
||||||
|
borderRadius: theme.spacing(1),
|
||||||
|
width: '40%',
|
||||||
|
'&:focus': {
|
||||||
|
outline: 'none'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const ModalText = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Raleway",
|
||||||
|
fontSize: "25px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}));
|
100
src/components/common/BlockedNamesModal/BlockedNamesModal.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Modal,
|
||||||
|
Typography,
|
||||||
|
SelectChangeEvent,
|
||||||
|
ListItem,
|
||||||
|
List,
|
||||||
|
useTheme
|
||||||
|
} from "@mui/material";
|
||||||
|
import {
|
||||||
|
StyledModal,
|
||||||
|
ModalContent,
|
||||||
|
ModalText
|
||||||
|
} from "./BlockedNamesModal-styles";
|
||||||
|
|
||||||
|
interface PostModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BlockedNamesModal: React.FC<PostModalProps> = ({
|
||||||
|
open,
|
||||||
|
onClose
|
||||||
|
}) => {
|
||||||
|
const [blockedNames, setBlockedNames] = useState<string[]>([]);
|
||||||
|
const theme = useTheme();
|
||||||
|
const getBlockedNames = React.useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const listName = `blockedNames`;
|
||||||
|
const response = await qortalRequest({
|
||||||
|
action: "GET_LIST_ITEMS",
|
||||||
|
list_name: listName
|
||||||
|
});
|
||||||
|
setBlockedNames(response);
|
||||||
|
} catch (error) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
getBlockedNames();
|
||||||
|
}, [getBlockedNames]);
|
||||||
|
|
||||||
|
const removeFromBlockList = async (name: string) => {
|
||||||
|
try {
|
||||||
|
const response = await qortalRequest({
|
||||||
|
action: "DELETE_LIST_ITEM",
|
||||||
|
list_name: "blockedNames",
|
||||||
|
item: name
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response === true) {
|
||||||
|
setBlockedNames((prev) => prev.filter((n) => n !== name));
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledModal open={open} onClose={onClose}>
|
||||||
|
<ModalContent>
|
||||||
|
<ModalText>Manage blocked names</ModalText>
|
||||||
|
<List
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
flex: "1",
|
||||||
|
overflow: "auto"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{blockedNames.map((name, index) => (
|
||||||
|
<ListItem
|
||||||
|
key={name + index}
|
||||||
|
sx={{
|
||||||
|
display: "flex"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography>{name}</Typography>
|
||||||
|
<Button
|
||||||
|
sx={{
|
||||||
|
backgroundColor: theme.palette.primary.light,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
fontFamily: "Raleway"
|
||||||
|
}}
|
||||||
|
onClick={() => removeFromBlockList(name)}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
<Button variant="contained" color="primary" onClick={onClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalContent>
|
||||||
|
</StyledModal>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,9 @@
|
|||||||
|
import { styled } from "@mui/system";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
|
||||||
|
export const CategoryContainer = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: "5px",
|
||||||
|
}));
|
320
src/components/common/CategoryList/CategoryList.tsx
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
OutlinedInput,
|
||||||
|
Select,
|
||||||
|
SelectChangeEvent,
|
||||||
|
SxProps,
|
||||||
|
Theme,
|
||||||
|
} from "@mui/material";
|
||||||
|
|
||||||
|
import React, { useEffect, useImperativeHandle, useState } from "react";
|
||||||
|
import { CategoryContainer } from "./CategoryList-styles.tsx";
|
||||||
|
import { allCategoryData } from "../../../constants/Categories/1stCategories.ts";
|
||||||
|
import { log } from "../../../constants/Misc.ts";
|
||||||
|
|
||||||
|
export interface Category {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Categories {
|
||||||
|
[key: number]: Category[];
|
||||||
|
}
|
||||||
|
export interface CategoryData {
|
||||||
|
category: Category[];
|
||||||
|
subCategories: Categories[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListDirection = "column" | "row";
|
||||||
|
|
||||||
|
interface CategoryListProps {
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
|
categoryData: CategoryData;
|
||||||
|
initialCategories?: string[];
|
||||||
|
columns?: number;
|
||||||
|
afterChange?: (categories: string[]) => void;
|
||||||
|
excludeCategories?: Category[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CategoryListRef = {
|
||||||
|
getSelectedCategories: () => string[];
|
||||||
|
setSelectedCategories: (arr: string[]) => void;
|
||||||
|
clearCategories: () => void;
|
||||||
|
getCategoriesFetchString: (categories?: string[]) => string;
|
||||||
|
categoriesToObject: (categories?: string[]) => object;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CategoryList = React.forwardRef<
|
||||||
|
CategoryListRef,
|
||||||
|
CategoryListProps
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
sx,
|
||||||
|
categoryData,
|
||||||
|
initialCategories,
|
||||||
|
columns = 1,
|
||||||
|
afterChange,
|
||||||
|
excludeCategories,
|
||||||
|
}: CategoryListProps,
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const categoriesLength = categoryData.subCategories.length + 1;
|
||||||
|
|
||||||
|
let emptyCategories: string[] = [];
|
||||||
|
for (let i = 0; i < categoriesLength; i++) emptyCategories.push("");
|
||||||
|
|
||||||
|
const [selectedCategories, setSelectedCategories] = useState<string[]>(
|
||||||
|
initialCategories || emptyCategories
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialCategories) setSelectedCategories(initialCategories);
|
||||||
|
}, [initialCategories]);
|
||||||
|
|
||||||
|
const updateCategories = (categories: string[]) => {
|
||||||
|
setSelectedCategories(categories);
|
||||||
|
if (afterChange) afterChange(categories);
|
||||||
|
};
|
||||||
|
const categoriesToObject = (categories: string[]) => {
|
||||||
|
let categoriesObject = {};
|
||||||
|
categories.map((category, index) => {
|
||||||
|
if (index === 0) categoriesObject["category"] = category;
|
||||||
|
else if (index === 1) categoriesObject["subcategory"] = category;
|
||||||
|
else categoriesObject[`subcategory${index}`] = category;
|
||||||
|
});
|
||||||
|
if (log) console.log("categoriesObject is: ", categoriesObject);
|
||||||
|
return categoriesObject;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearCategories = () => {
|
||||||
|
updateCategories(emptyCategories);
|
||||||
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
getSelectedCategories: () => {
|
||||||
|
return selectedCategories;
|
||||||
|
},
|
||||||
|
setSelectedCategories: categories => {
|
||||||
|
if (log) console.log("setSelectedCategories: ", categories);
|
||||||
|
updateCategories(categories);
|
||||||
|
},
|
||||||
|
clearCategories,
|
||||||
|
getCategoriesFetchString: (categories?: string[]) =>
|
||||||
|
getCategoriesFetchString(categories || selectedCategories),
|
||||||
|
categoriesToObject: (categories?: string[]) =>
|
||||||
|
categoriesToObject(categories || selectedCategories),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const selectCategory = (optionId: string, index: number) => {
|
||||||
|
const isMainCategory = index === 0;
|
||||||
|
const subCategoryIndex = index - 1;
|
||||||
|
let selectedOption: Category | undefined;
|
||||||
|
if (isMainCategory)
|
||||||
|
selectedOption = categoryData.category.find(
|
||||||
|
option => option.id === +optionId
|
||||||
|
);
|
||||||
|
else {
|
||||||
|
const subCategoryLevel = categoryData.subCategories[subCategoryIndex];
|
||||||
|
const parentCategory = selectedCategories[subCategoryIndex];
|
||||||
|
const subCategory = subCategoryLevel[parentCategory];
|
||||||
|
|
||||||
|
selectedOption = subCategory.find(option => option.id === +optionId);
|
||||||
|
}
|
||||||
|
const newSelectedCategories: string[] = selectedCategories.map(
|
||||||
|
(category, categoryIndex) => {
|
||||||
|
if (index > categoryIndex) return category;
|
||||||
|
else if (index === categoryIndex) return selectedOption.id.toString();
|
||||||
|
else return "";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
updateCategories(newSelectedCategories);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectCategoryEvent = (event: SelectChangeEvent, index: number) => {
|
||||||
|
const optionId = event.target.value;
|
||||||
|
selectCategory(optionId, index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const categorySelectSX = {
|
||||||
|
// Target the input field
|
||||||
|
".MuiSelect-select": {
|
||||||
|
fontSize: "16px", // Change font size for the selected value
|
||||||
|
padding: "10px 5px 15px 15px;",
|
||||||
|
},
|
||||||
|
// Target the dropdown icon
|
||||||
|
".MuiSelect-icon": {
|
||||||
|
fontSize: "20px", // Adjust if needed
|
||||||
|
},
|
||||||
|
// Target the dropdown menu
|
||||||
|
"& .MuiMenu-paper": {
|
||||||
|
".MuiMenuItem-root": {
|
||||||
|
fontSize: "14px", // Change font size for the menu items
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const fillMenu = (category: Categories, index: number) => {
|
||||||
|
const subCategoryIndex = selectedCategories[index];
|
||||||
|
if (log) console.log("selected categories: ", selectedCategories);
|
||||||
|
if (log) console.log("index is: ", index);
|
||||||
|
if (log) console.log("subCategoryIndex is: ", subCategoryIndex);
|
||||||
|
if (log) console.log("category is: ", category);
|
||||||
|
if (log)
|
||||||
|
console.log(
|
||||||
|
"subCategoryIndex within category: ",
|
||||||
|
selectedCategories[subCategoryIndex]
|
||||||
|
);
|
||||||
|
if (log) console.log("categoryData: ", categoryData);
|
||||||
|
|
||||||
|
const menuToFill = category[subCategoryIndex];
|
||||||
|
if (menuToFill)
|
||||||
|
return menuToFill.map(option => (
|
||||||
|
<MenuItem key={option.id} value={option.id}>
|
||||||
|
{option.name}
|
||||||
|
</MenuItem>
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasSubCategory = (category: Categories, index: number) => {
|
||||||
|
const subCategoryIndex = selectedCategories[index];
|
||||||
|
const subCategory = category[subCategoryIndex];
|
||||||
|
if (excludeCategories && subCategory === excludeCategories) return false;
|
||||||
|
return subCategory && subCategoryIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CategoryContainer sx={{ width: "100%", ...sx }}>
|
||||||
|
<FormControl sx={{ width: "100%" }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(" + columns + ", 1fr)",
|
||||||
|
width: "100%",
|
||||||
|
gap: "20px",
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: "30px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl fullWidth sx={{ marginBottom: 1 }}>
|
||||||
|
<InputLabel
|
||||||
|
sx={{
|
||||||
|
fontSize: "16px",
|
||||||
|
}}
|
||||||
|
id="Category-1"
|
||||||
|
>
|
||||||
|
Category
|
||||||
|
</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="Category 1"
|
||||||
|
input={<OutlinedInput label="Category 1" />}
|
||||||
|
value={selectedCategories[0] || ""}
|
||||||
|
onChange={e => {
|
||||||
|
selectCategoryEvent(e, 0);
|
||||||
|
}}
|
||||||
|
sx={categorySelectSX}
|
||||||
|
>
|
||||||
|
{categoryData.category.map(option => (
|
||||||
|
<MenuItem key={option.id} value={option.id}>
|
||||||
|
{option.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
{categoryData.subCategories.map(
|
||||||
|
(category, index) =>
|
||||||
|
hasSubCategory(category, index) && (
|
||||||
|
<FormControl
|
||||||
|
fullWidth
|
||||||
|
sx={{
|
||||||
|
marginBottom: 1,
|
||||||
|
}}
|
||||||
|
key={selectedCategories[index] + index}
|
||||||
|
>
|
||||||
|
<InputLabel
|
||||||
|
sx={{
|
||||||
|
fontSize: "16px",
|
||||||
|
}}
|
||||||
|
id={`Category-${index + 2}`}
|
||||||
|
>
|
||||||
|
{`Category-${index + 2}`}
|
||||||
|
</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId={`Category ${index + 2}`}
|
||||||
|
input={<OutlinedInput label={`Category ${index + 2}`} />}
|
||||||
|
value={selectedCategories[index + 1] || ""}
|
||||||
|
onChange={e => {
|
||||||
|
selectCategoryEvent(e, index + 1);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
// Target the input field
|
||||||
|
".MuiSelect-select": {
|
||||||
|
fontSize: "16px", // Change font size for the selected value
|
||||||
|
padding: "10px 5px 15px 15px;",
|
||||||
|
},
|
||||||
|
// Target the dropdown icon
|
||||||
|
".MuiSelect-icon": {
|
||||||
|
fontSize: "20px", // Adjust if needed
|
||||||
|
},
|
||||||
|
// Target the dropdown menu
|
||||||
|
"& .MuiMenu-paper": {
|
||||||
|
".MuiMenuItem-root": {
|
||||||
|
fontSize: "14px", // Change font size for the menu items
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{fillMenu(category, index)}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</FormControl>
|
||||||
|
</CategoryContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getCategoriesFetchString = (categories: string[]) => {
|
||||||
|
let fetchString = "";
|
||||||
|
categories.map((category, index) => {
|
||||||
|
if (category) {
|
||||||
|
if (index === 0) fetchString += `cat:${category}`;
|
||||||
|
else if (index === 1) fetchString += `;sub:${category}`;
|
||||||
|
else fetchString += `;sub${index}:${category}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (log) console.log("categoriesAsDescription: ", fetchString);
|
||||||
|
return fetchString;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appendCategoryToList = (
|
||||||
|
categories: string[],
|
||||||
|
appendedCategoryID: string
|
||||||
|
) => {
|
||||||
|
const filteredCategories = categories.filter(
|
||||||
|
categoryString => categoryString.length > 0
|
||||||
|
);
|
||||||
|
filteredCategories.push(appendedCategoryID);
|
||||||
|
return filteredCategories;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCategoriesFromObject = (editFileProperties: any) => {
|
||||||
|
const categoryList: string[] = [];
|
||||||
|
const categoryCount = allCategoryData.subCategories.length + 1;
|
||||||
|
|
||||||
|
for (let i = 0; i < categoryCount; i++) {
|
||||||
|
if (i === 0 && editFileProperties.category)
|
||||||
|
categoryList.push(editFileProperties.category);
|
||||||
|
else if (i === 1 && editFileProperties.subcategory)
|
||||||
|
categoryList.push(editFileProperties.subcategory);
|
||||||
|
else categoryList.push(editFileProperties[`subcategory${i}`] || "");
|
||||||
|
}
|
||||||
|
return categoryList;
|
||||||
|
};
|
295
src/components/common/Comments/Comment.tsx
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
Typography,
|
||||||
|
useTheme,
|
||||||
|
} from "@mui/material";
|
||||||
|
import React, { useCallback, useState, useEffect } from "react";
|
||||||
|
import { CommentEditor } from "./CommentEditor";
|
||||||
|
import {
|
||||||
|
CardContentContainerComment,
|
||||||
|
CommentActionButtonRow,
|
||||||
|
CommentDateText,
|
||||||
|
EditReplyButton,
|
||||||
|
StyledCardComment,
|
||||||
|
} from "./Comments-styles";
|
||||||
|
import { StyledCardHeaderComment } from "./Comments-styles";
|
||||||
|
import { StyledCardColComment } from "./Comments-styles";
|
||||||
|
import { AuthorTextComment } from "./Comments-styles";
|
||||||
|
import {
|
||||||
|
StyledCardContentComment,
|
||||||
|
LoadMoreCommentsButton as CommentActionButton,
|
||||||
|
} from "./Comments-styles";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { RootState } from "../../../state/store";
|
||||||
|
import Portal from "../Portal";
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
<CommentActionButtonRow>
|
||||||
|
<CommentActionButton
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => setIsReplying(true)}
|
||||||
|
>
|
||||||
|
reply
|
||||||
|
</CommentActionButton>
|
||||||
|
{user?.name === comment?.name && (
|
||||||
|
<CommentActionButton
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => setCurrentEdit(comment)}
|
||||||
|
>
|
||||||
|
edit
|
||||||
|
</CommentActionButton>
|
||||||
|
)}
|
||||||
|
{isReplying && (
|
||||||
|
<CommentActionButton
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => {
|
||||||
|
setIsReplying(false);
|
||||||
|
setIsEditing(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
close
|
||||||
|
</CommentActionButton>
|
||||||
|
)}
|
||||||
|
</CommentActionButtonRow>
|
||||||
|
</Box>
|
||||||
|
</CommentCard>
|
||||||
|
|
||||||
|
<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 {
|
||||||
|
const url = await qortalRequest({
|
||||||
|
action: "GET_QDN_RESOURCE_URL",
|
||||||
|
name: author,
|
||||||
|
service: "THUMBNAIL",
|
||||||
|
identifier: "qortal_avatar",
|
||||||
|
});
|
||||||
|
|
||||||
|
setAvatarUrl(url);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getAvatar(name);
|
||||||
|
}, [name]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardContentContainerComment>
|
||||||
|
<StyledCardHeaderComment
|
||||||
|
sx={{
|
||||||
|
"& .MuiCardHeader-content": {
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Avatar
|
||||||
|
src={avatarUrl}
|
||||||
|
alt={`${name}'s avatar`}
|
||||||
|
sx={{ width: "35px", height: "35px" }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<StyledCardColComment>
|
||||||
|
<AuthorTextComment>{name}</AuthorTextComment>
|
||||||
|
</StyledCardColComment>
|
||||||
|
</StyledCardHeaderComment>
|
||||||
|
<StyledCardContentComment>
|
||||||
|
<StyledCardComment>{message}</StyledCardComment>
|
||||||
|
</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 && (
|
||||||
|
<CommentDateText>
|
||||||
|
{formatDate(+reply?.created)}
|
||||||
|
</CommentDateText>
|
||||||
|
)}
|
||||||
|
{user?.name === reply?.name ? (
|
||||||
|
<EditReplyButton
|
||||||
|
size="small"
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => setCurrentEdit(reply)}
|
||||||
|
sx={{}}
|
||||||
|
>
|
||||||
|
edit
|
||||||
|
</EditReplyButton>
|
||||||
|
) : (
|
||||||
|
<Box />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</CommentCard>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
{children}
|
||||||
|
</CardContentContainerComment>
|
||||||
|
);
|
||||||
|
};
|
254
src/components/common/Comments/CommentEditor.tsx
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
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";
|
||||||
|
import {
|
||||||
|
CommentInput,
|
||||||
|
CommentInputContainer,
|
||||||
|
SubmitCommentButton,
|
||||||
|
} from "./Comments-styles";
|
||||||
|
import { QSUPPORT_COMMENT_BASE } from "../../../constants/Identifiers.ts";
|
||||||
|
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);
|
||||||
|
|
||||||
|
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: any = 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 = `${QSUPPORT_COMMENT_BASE}${postId.slice(-12)}_base_${id}`;
|
||||||
|
let idForNotification = identifier;
|
||||||
|
|
||||||
|
if (isReply && commentId) {
|
||||||
|
const removeBaseCommentId = commentId;
|
||||||
|
removeBaseCommentId.replace("_base_", "");
|
||||||
|
identifier = `${QSUPPORT_COMMENT_BASE}${postId.slice(
|
||||||
|
-12
|
||||||
|
)}_reply_${removeBaseCommentId.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) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommentInputContainer>
|
||||||
|
<CommentInput
|
||||||
|
id="standard-multiline-flexible"
|
||||||
|
label="Your comment"
|
||||||
|
multiline
|
||||||
|
maxRows={4}
|
||||||
|
variant="filled"
|
||||||
|
value={value}
|
||||||
|
inputProps={{
|
||||||
|
maxLength: 200,
|
||||||
|
}}
|
||||||
|
InputLabelProps={{ style: { fontSize: "18px" } }}
|
||||||
|
onChange={e => setValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SubmitCommentButton variant="contained" onClick={handleSubmit}>
|
||||||
|
{isReply ? "Submit reply" : isEdit ? "Edit" : "Submit comment"}
|
||||||
|
</SubmitCommentButton>
|
||||||
|
</CommentInputContainer>
|
||||||
|
);
|
||||||
|
};
|
281
src/components/common/Comments/CommentSection.tsx
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
|
import { CommentEditor } from "./CommentEditor";
|
||||||
|
import { Comment } from "./Comment";
|
||||||
|
import { CircularProgress } from "@mui/material";
|
||||||
|
import { styled } from "@mui/system";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { RootState } from "../../../state/store";
|
||||||
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
CommentContainer,
|
||||||
|
CommentEditorContainer,
|
||||||
|
CommentsContainer,
|
||||||
|
LoadMoreCommentsButton,
|
||||||
|
LoadMoreCommentsButtonRow,
|
||||||
|
NoCommentsRow,
|
||||||
|
} from "./Comments-styles";
|
||||||
|
import { QSUPPORT_COMMENT_BASE } from "../../../constants/Identifiers.ts";
|
||||||
|
import {
|
||||||
|
CrowdfundSubTitle,
|
||||||
|
CrowdfundSubTitleRow,
|
||||||
|
} from "../../PublishIssue/Upload-styles.tsx";
|
||||||
|
|
||||||
|
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 [loadingComments, setLoadingComments] = useState<boolean>(false);
|
||||||
|
|
||||||
|
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 getReplies = useCallback(
|
||||||
|
async (commentId, postId) => {
|
||||||
|
const offset = 0;
|
||||||
|
|
||||||
|
const removeBaseCommentId = commentId.replace("_base_", "");
|
||||||
|
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=${QSUPPORT_COMMENT_BASE}${postId.slice(
|
||||||
|
-12
|
||||||
|
)}_reply_${removeBaseCommentId.slice(
|
||||||
|
-6
|
||||||
|
)}&limit=0&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();
|
||||||
|
const 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return comments;
|
||||||
|
},
|
||||||
|
[postId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getComments = useCallback(
|
||||||
|
async (isNewMessages?: boolean, numberOfComments?: number) => {
|
||||||
|
try {
|
||||||
|
setLoadingComments(true);
|
||||||
|
let offset = 0;
|
||||||
|
if (isNewMessages && numberOfComments) {
|
||||||
|
offset = numberOfComments;
|
||||||
|
}
|
||||||
|
const url = `/arbitrary/resources/search?mode=ALL&service=BLOG_COMMENT&query=${QSUPPORT_COMMENT_BASE}${postId.slice(
|
||||||
|
-12
|
||||||
|
)}_base_&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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const res = await getReplies(comment.identifier, postId);
|
||||||
|
comments = [...comments, ...res];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isNewMessages) {
|
||||||
|
setListComments(prev => [...prev, ...comments]);
|
||||||
|
setNewMessages(0);
|
||||||
|
} else {
|
||||||
|
setListComments(comments);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setLoadingComments(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[postId]
|
||||||
|
);
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Panel>
|
||||||
|
<CrowdfundSubTitleRow>
|
||||||
|
<CrowdfundSubTitle>Comments</CrowdfundSubTitle>
|
||||||
|
</CrowdfundSubTitleRow>
|
||||||
|
<CommentsContainer>
|
||||||
|
{loadingComments ? (
|
||||||
|
<NoCommentsRow>
|
||||||
|
<CircularProgress />
|
||||||
|
</NoCommentsRow>
|
||||||
|
) : listComments.length === 0 ? (
|
||||||
|
<NoCommentsRow>
|
||||||
|
There are no comments yet. Be the first to comment!
|
||||||
|
</NoCommentsRow>
|
||||||
|
) : (
|
||||||
|
<CommentContainer>
|
||||||
|
{structuredCommentList.map((comment: any) => {
|
||||||
|
return (
|
||||||
|
<Comment
|
||||||
|
key={comment?.identifier}
|
||||||
|
comment={comment}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
postId={postId}
|
||||||
|
postName={postName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CommentContainer>
|
||||||
|
)}
|
||||||
|
{listComments.length > 20 && (
|
||||||
|
<LoadMoreCommentsButtonRow>
|
||||||
|
<LoadMoreCommentsButton
|
||||||
|
onClick={() => {
|
||||||
|
getComments(
|
||||||
|
true,
|
||||||
|
listComments.filter(
|
||||||
|
item => !item.identifier.includes("_reply_")
|
||||||
|
).length
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
Load More Comments
|
||||||
|
</LoadMoreCommentsButton>
|
||||||
|
</LoadMoreCommentsButtonRow>
|
||||||
|
)}
|
||||||
|
</CommentsContainer>
|
||||||
|
<CommentEditorContainer>
|
||||||
|
<CommentEditor
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
postId={postId}
|
||||||
|
postName={postName}
|
||||||
|
/>
|
||||||
|
</CommentEditorContainer>
|
||||||
|
</Panel>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
281
src/components/common/Comments/Comments-styles.tsx
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
import { styled } from "@mui/system";
|
||||||
|
import { Card, Box, Typography, Button, TextField } 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" ? "#a9d9d038" : "#c3abe414",
|
||||||
|
border: `1px solid ${theme.palette.primary.main}`,
|
||||||
|
margin: "0px",
|
||||||
|
padding: "8px 15px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
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: "7px",
|
||||||
|
padding: "9px 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 StyledCardComment = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
letterSpacing: 0,
|
||||||
|
fontWeight: 400,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
fontSize: "19px",
|
||||||
|
wordBreak: "break-word"
|
||||||
|
}));
|
||||||
|
|
||||||
|
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)(({ theme }) => ({
|
||||||
|
fontFamily: "Montserrat, sans-serif",
|
||||||
|
fontSize: "17px",
|
||||||
|
letterSpacing: "0.3px",
|
||||||
|
fontWeight: 400,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
}));
|
||||||
|
|
||||||
|
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)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CommentsContainer = styled(Box)({
|
||||||
|
width: "90%",
|
||||||
|
maxWidth: "1000px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
flex: "1",
|
||||||
|
overflow: "auto",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CommentContainer = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
margin: "25px 0px 50px 0px",
|
||||||
|
maxWidth: "100%",
|
||||||
|
width: "100%",
|
||||||
|
gap: "10px",
|
||||||
|
padding: "0px 5px",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const NoCommentsRow = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
flex: "1",
|
||||||
|
padding: "10px 0px",
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
letterSpacing: 0,
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: "18px",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const LoadMoreCommentsButtonRow = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const EditReplyButton = styled(Button)(({ theme }) => ({
|
||||||
|
width: "30px",
|
||||||
|
alignSelf: "flex-end",
|
||||||
|
background: theme.palette.primary.light,
|
||||||
|
color: "#ffffff",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const LoadMoreCommentsButton = styled(Button)(({ theme }) => ({
|
||||||
|
fontFamily: "Montserrat",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: "0.2px",
|
||||||
|
fontSize: "15px",
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
color: "#ffffff",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CommentActionButtonRow = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "5px",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CommentEditorContainer = styled(Box)({
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CommentDateText = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
letterSpacing: 0,
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: "13px",
|
||||||
|
marginLeft: "5px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CommentInputContainer = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
marginTop: "15px",
|
||||||
|
width: "90%",
|
||||||
|
maxWidth: "1000px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
gap: "10px",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: "25px",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CommentInput = styled(TextField)(({ theme }) => ({
|
||||||
|
backgroundColor: theme.palette.mode === "light" ? "#a9d9d01d" : "#c3abe4a",
|
||||||
|
border: `1px solid ${theme.palette.primary.main}`,
|
||||||
|
width: "100%",
|
||||||
|
borderRadius: "8px",
|
||||||
|
'& [class$="-MuiFilledInput-root"]': {
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
letterSpacing: 0,
|
||||||
|
fontWeight: 400,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
fontSize: "19px",
|
||||||
|
minHeight: "100px",
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
"&:before": {
|
||||||
|
borderBottom: "none",
|
||||||
|
"&:hover": {
|
||||||
|
borderBottom: "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"&:hover": {
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
"&:before": {
|
||||||
|
borderBottom: "none",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const SubmitCommentButton = styled(Button)(({ theme }) => ({
|
||||||
|
fontFamily: "Montserrat",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: "0.2px",
|
||||||
|
fontSize: "15px",
|
||||||
|
backgroundColor: theme.palette.primary.main,
|
||||||
|
color: "#ffffff",
|
||||||
|
width: "75%",
|
||||||
|
}));
|
72
src/components/common/ConsentModal.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
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-share-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">
|
||||||
|
Q-Share is currently in its first version and as such there could be
|
||||||
|
some bugs. The Qortal community, along with its development team and
|
||||||
|
the creators of this application, cannot be held accountable for any
|
||||||
|
content published or displayed. Also, they are not responsible for
|
||||||
|
any loss of coin due to either bad actors or bugs in the
|
||||||
|
application. Furthermore, they bear no responsibility for any data
|
||||||
|
loss that may occur as a result of using this application. Finally, they bear no responsibility for any of the content uploaded by users.
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
204
src/components/common/DownloadTaskManager.tsx
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionDetails,
|
||||||
|
AccordionSummary,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
LinearProgress,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemIcon,
|
||||||
|
Popover,
|
||||||
|
Typography,
|
||||||
|
useTheme
|
||||||
|
} from '@mui/material'
|
||||||
|
import { Movie } from '@mui/icons-material'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { RootState } from '../../state/store'
|
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
|
import { DownloadingLight } from '../../assets/svgs/DownloadingLight'
|
||||||
|
import { DownloadedLight } from '../../assets/svgs/DownloadedLight'
|
||||||
|
import AttachFileIcon from "@mui/icons-material/AttachFile";
|
||||||
|
|
||||||
|
export const DownloadTaskManager: React.FC = () => {
|
||||||
|
const { downloads } = useSelector((state: RootState) => state.global)
|
||||||
|
const location = useLocation()
|
||||||
|
const theme = useTheme()
|
||||||
|
const [visible, setVisible] = useState(false)
|
||||||
|
const [hidden, setHidden] = useState(true)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
|
|
||||||
|
const [openDownload, setOpenDownload] = useState<boolean>(false);
|
||||||
|
|
||||||
|
|
||||||
|
const handleClick = (event?: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
const target = event?.currentTarget as unknown as HTMLButtonElement | null;
|
||||||
|
setAnchorEl(target);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseDownload = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
setOpenDownload(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Simulate downloads for demo purposes
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
setTimeout(() => {
|
||||||
|
setHidden(true)
|
||||||
|
setVisible(false)
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
}, [visible])
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Object.keys(downloads).length === 0) return
|
||||||
|
setVisible(true)
|
||||||
|
setHidden(false)
|
||||||
|
}, [downloads])
|
||||||
|
|
||||||
|
|
||||||
|
if (
|
||||||
|
!downloads ||
|
||||||
|
Object.keys(downloads).length === 0
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
let downloadInProgress = false
|
||||||
|
if(Object.keys(downloads).find((key)=> (downloads[key]?.status?.status !== 'READY' && downloads[key]?.status?.status !== 'DOWNLOADED'))){
|
||||||
|
downloadInProgress = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Button onClick={(e: any) => {
|
||||||
|
handleClick(e);
|
||||||
|
setOpenDownload(true);
|
||||||
|
}}>
|
||||||
|
{downloadInProgress ? (
|
||||||
|
<DownloadingLight height='24px' width='24px' className='download-icon' />
|
||||||
|
) : (
|
||||||
|
<DownloadedLight height='24px' width='24px' />
|
||||||
|
)}
|
||||||
|
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
id={"download-popover"}
|
||||||
|
open={openDownload}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={handleCloseDownload}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: "bottom",
|
||||||
|
horizontal: "left"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<List
|
||||||
|
sx={{
|
||||||
|
maxHeight: '50vh',
|
||||||
|
overflow: 'auto',
|
||||||
|
width: '250px',
|
||||||
|
gap: '5px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Object.keys(downloads)
|
||||||
|
.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={() => {
|
||||||
|
const id = downloadObj?.properties?.jsonId
|
||||||
|
if (!id) return
|
||||||
|
|
||||||
|
navigate(
|
||||||
|
`/share/${downloadObj?.properties?.name}/${id}`
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ListItemIcon>
|
||||||
|
|
||||||
|
<AttachFileIcon 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,
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{downloadObj?.identifier}
|
||||||
|
</Typography>
|
||||||
|
</ListItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
446
src/components/common/FileElement.tsx
Normal file
@ -0,0 +1,446 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { styled, useTheme } from "@mui/material/styles";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { CircularProgress } from "@mui/material";
|
||||||
|
import AttachFileIcon from "@mui/icons-material/AttachFile";
|
||||||
|
import { MyContext } from "../../wrappers/DownloadWrapper";
|
||||||
|
import { RootState } from "../../state/store";
|
||||||
|
import { setNotification } from "../../state/features/notificationsSlice";
|
||||||
|
|
||||||
|
const Widget = styled("div")(({ theme }) => ({
|
||||||
|
padding: 8,
|
||||||
|
borderRadius: 10,
|
||||||
|
maxWidth: 350,
|
||||||
|
position: "relative",
|
||||||
|
zIndex: 1,
|
||||||
|
backdropFilter: "blur(40px)",
|
||||||
|
background: "skyblue",
|
||||||
|
transition: "0.2s all",
|
||||||
|
"&:hover": {
|
||||||
|
opacity: 0.75,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const CoverImage = styled("div")({
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
objectFit: "cover",
|
||||||
|
overflow: "hidden",
|
||||||
|
flexShrink: 0,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: "rgba(0,0,0,0.08)",
|
||||||
|
"& > img": {
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IAudioElement {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
author?: string;
|
||||||
|
fileInfo?: any;
|
||||||
|
postId?: string;
|
||||||
|
user?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
mimeType?: string;
|
||||||
|
disable?: boolean;
|
||||||
|
mode?: string;
|
||||||
|
otherUser?: string;
|
||||||
|
customStyles?: any;
|
||||||
|
jsonId: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,
|
||||||
|
children,
|
||||||
|
mimeType,
|
||||||
|
disable,
|
||||||
|
customStyles,
|
||||||
|
jsonId
|
||||||
|
}: IAudioElement) {
|
||||||
|
const { downloadVideo } = React.useContext(MyContext);
|
||||||
|
const [startedDownload, setStartedDownload] = React.useState<boolean>(false)
|
||||||
|
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||||
|
const [fileProperties, setFileProperties] = React.useState<any>(null);
|
||||||
|
const [downloadLoader, setDownloadLoader] = React.useState<any>(false);
|
||||||
|
const downloads = useSelector((state: RootState) => state.global?.downloads);
|
||||||
|
const status = React.useRef<null | string>(null)
|
||||||
|
|
||||||
|
const hasCommencedDownload = React.useRef(false);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const reDownload = React.useRef<boolean>(false)
|
||||||
|
const isFetchingProperties = React.useRef<boolean>(false)
|
||||||
|
const download = React.useMemo(() => {
|
||||||
|
if (!downloads || !fileInfo?.identifier) return {};
|
||||||
|
const findDownload = downloads[fileInfo?.identifier];
|
||||||
|
|
||||||
|
if (!findDownload) return {};
|
||||||
|
return findDownload;
|
||||||
|
}, [downloads, fileInfo]);
|
||||||
|
|
||||||
|
const resourceStatus = React.useMemo(() => {
|
||||||
|
return download?.status || {};
|
||||||
|
}, [download]);
|
||||||
|
|
||||||
|
const retryDownload = React.useRef(0);
|
||||||
|
|
||||||
|
const handlePlay = async () => {
|
||||||
|
if (disable) return;
|
||||||
|
hasCommencedDownload.current = true;
|
||||||
|
setStartedDownload(true)
|
||||||
|
if (
|
||||||
|
resourceStatus?.status === "READY"
|
||||||
|
) {
|
||||||
|
if (downloadLoader) return;
|
||||||
|
|
||||||
|
setDownloadLoader(true);
|
||||||
|
let filename = download?.properties?.filename
|
||||||
|
let mimeType = download?.properties?.type
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { name, service, identifier } = fileInfo;
|
||||||
|
|
||||||
|
const res = await qortalRequest({
|
||||||
|
action: "GET_QDN_RESOURCE_PROPERTIES",
|
||||||
|
name: name,
|
||||||
|
service: service,
|
||||||
|
identifier: identifier,
|
||||||
|
});
|
||||||
|
filename = res?.filename || filename;
|
||||||
|
mimeType = res?.mimeType || mimeType;
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { name, service, identifier } = fileInfo;
|
||||||
|
|
||||||
|
const url = `/arbitrary/${service}/${name}/${identifier}`;
|
||||||
|
fetch(url)
|
||||||
|
.then(response => response.blob())
|
||||||
|
.then(async blob => {
|
||||||
|
await qortalRequest({
|
||||||
|
action: "SAVE_FILE",
|
||||||
|
blob,
|
||||||
|
filename: filename,
|
||||||
|
mimeType,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Error fetching the video:", error);
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
let notificationObj: any = null;
|
||||||
|
if (typeof error === "string") {
|
||||||
|
notificationObj = {
|
||||||
|
msg: error || "Failed to send message",
|
||||||
|
alertType: "error",
|
||||||
|
};
|
||||||
|
} else if (typeof error?.error === "string") {
|
||||||
|
notificationObj = {
|
||||||
|
msg: error?.error || "Failed to send message",
|
||||||
|
alertType: "error",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
notificationObj = {
|
||||||
|
msg: error?.message || "Failed to send message",
|
||||||
|
alertType: "error",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!notificationObj) return;
|
||||||
|
dispatch(setNotification(notificationObj));
|
||||||
|
} finally {
|
||||||
|
setDownloadLoader(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, service, identifier } = fileInfo;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
downloadVideo({
|
||||||
|
name,
|
||||||
|
service,
|
||||||
|
identifier,
|
||||||
|
properties: {
|
||||||
|
...fileInfo,
|
||||||
|
jsonId
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const refetch = React.useCallback(async () => {
|
||||||
|
if (!fileInfo) return
|
||||||
|
try {
|
||||||
|
const { name, service, identifier } = fileInfo;
|
||||||
|
isFetchingProperties.current = true
|
||||||
|
await qortalRequest({
|
||||||
|
action: 'GET_QDN_RESOURCE_PROPERTIES',
|
||||||
|
name,
|
||||||
|
service,
|
||||||
|
identifier
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
isFetchingProperties.current = false
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [fileInfo])
|
||||||
|
|
||||||
|
const refetchInInterval = ()=> {
|
||||||
|
try {
|
||||||
|
const interval = setInterval(()=> {
|
||||||
|
if(status?.current === 'DOWNLOADED'){
|
||||||
|
refetch()
|
||||||
|
}
|
||||||
|
if(status?.current === 'READY'){
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, 7500)
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if(resourceStatus?.status){
|
||||||
|
status.current = resourceStatus?.status
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
resourceStatus?.status === 'DOWNLOADED' &&
|
||||||
|
reDownload?.current === false
|
||||||
|
) {
|
||||||
|
refetchInInterval()
|
||||||
|
reDownload.current = true
|
||||||
|
}
|
||||||
|
}, [resourceStatus])
|
||||||
|
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (
|
||||||
|
resourceStatus?.status === "READY" &&
|
||||||
|
download?.url &&
|
||||||
|
download?.properties?.filename &&
|
||||||
|
hasCommencedDownload.current
|
||||||
|
) {
|
||||||
|
setIsLoading(false);
|
||||||
|
dispatch(
|
||||||
|
setNotification({
|
||||||
|
msg: "Download completed. Click to save file",
|
||||||
|
alertType: "info",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [resourceStatus, download]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
onClick={handlePlay}
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
overflow: "hidden",
|
||||||
|
position: "relative",
|
||||||
|
cursor: "pointer",
|
||||||
|
...(customStyles || {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
position: "relative",
|
||||||
|
gap: "7px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}{" "}
|
||||||
|
{((resourceStatus.status && resourceStatus?.status !== "READY") ||
|
||||||
|
isLoading) && startedDownload ? (
|
||||||
|
<>
|
||||||
|
<CircularProgress color="secondary" size={14} />
|
||||||
|
<Typography variant="body2">{`${Math.round(
|
||||||
|
resourceStatus?.percentLoaded || 0
|
||||||
|
).toFixed(0)}% loaded`}</Typography>
|
||||||
|
</>
|
||||||
|
) : 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) && startedDownload && (
|
||||||
|
<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?.properties?.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>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
import { Box, Button } from "@mui/material";
|
||||||
|
import { styled } from "@mui/system";
|
||||||
|
import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternate";
|
||||||
|
import { TimesSVG } from "./TimesSVG.tsx";
|
||||||
|
|
||||||
|
export const AddCoverImageButton = styled(Button)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
fontFamily: "Montserrat",
|
||||||
|
fontSize: "16px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: "0.2px",
|
||||||
|
color: "white",
|
||||||
|
gap: "5px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const AddLogoIcon = styled(AddPhotoAlternateIcon)(({ theme }) => ({
|
||||||
|
color: "#fff",
|
||||||
|
height: "25px",
|
||||||
|
width: "auto",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const LogoPreviewRow = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CoverImagePreview = styled("img")(({ theme }) => ({
|
||||||
|
width: "100px",
|
||||||
|
height: "100px",
|
||||||
|
objectFit: "contain",
|
||||||
|
userSelect: "none",
|
||||||
|
borderRadius: "3px",
|
||||||
|
marginBottom: "10px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const TimesIcon = styled(TimesSVG)(({ theme }) => ({
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
borderRadius: "50%",
|
||||||
|
padding: "5px",
|
||||||
|
transition: "all 0.2s ease-in-out",
|
||||||
|
"&:hover": {
|
||||||
|
cursor: "pointer",
|
||||||
|
scale: "1.1",
|
||||||
|
},
|
||||||
|
}));
|
62
src/components/common/ImagePublisher/ImagePublisher.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import ImageUploader from "./ImageUploader.tsx";
|
||||||
|
import React, { useImperativeHandle, useState } from "react";
|
||||||
|
import {
|
||||||
|
AddCoverImageButton,
|
||||||
|
AddLogoIcon,
|
||||||
|
CoverImagePreview,
|
||||||
|
LogoPreviewRow,
|
||||||
|
TimesIcon,
|
||||||
|
} from "./ImagePublisher-styles.tsx";
|
||||||
|
import { useTheme } from "@mui/material";
|
||||||
|
|
||||||
|
export type ImagePublisherRef = {
|
||||||
|
getImageArray: () => string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ImagePublisherProps {
|
||||||
|
initialImages?: string[];
|
||||||
|
}
|
||||||
|
export const ImagePublisher = React.forwardRef<
|
||||||
|
ImagePublisherRef,
|
||||||
|
ImagePublisherProps
|
||||||
|
>(({ initialImages }: ImagePublisherProps, ref) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [imageArray, setImageArray] = useState<string[]>(initialImages || []);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
getImageArray: () => {
|
||||||
|
return imageArray;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{imageArray.length === 0 ? (
|
||||||
|
<ImageUploader onPick={(img: string[]) => setImageArray(img)}>
|
||||||
|
<AddCoverImageButton variant="contained">
|
||||||
|
Add Images
|
||||||
|
<AddLogoIcon
|
||||||
|
sx={{
|
||||||
|
height: "25px",
|
||||||
|
width: "auto",
|
||||||
|
}}
|
||||||
|
></AddLogoIcon>
|
||||||
|
</AddCoverImageButton>
|
||||||
|
</ImageUploader>
|
||||||
|
) : (
|
||||||
|
<LogoPreviewRow>
|
||||||
|
{imageArray.map(
|
||||||
|
image =>
|
||||||
|
image && <CoverImagePreview src={image} alt="logo" key={image} />
|
||||||
|
)}
|
||||||
|
<TimesIcon
|
||||||
|
color={theme.palette.text.primary}
|
||||||
|
onClickFunc={() => setImageArray([])}
|
||||||
|
height={"32"}
|
||||||
|
width={"32"}
|
||||||
|
></TimesIcon>
|
||||||
|
</LogoPreviewRow>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
109
src/components/common/ImagePublisher/ImageUploader.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import React, { useCallback } from "react";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
import {
|
||||||
|
DropzoneInputProps,
|
||||||
|
DropzoneRootProps,
|
||||||
|
useDropzone,
|
||||||
|
} from "react-dropzone";
|
||||||
|
import Compressor from "compressorjs";
|
||||||
|
import { setNotification } from "../../../state/features/notificationsSlice.ts";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ImageUploader: React.FC<ImageUploaderProps> = ({
|
||||||
|
children,
|
||||||
|
onPick,
|
||||||
|
}) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const imageLimit = 3;
|
||||||
|
|
||||||
|
const compressImages = async (images: File[]) => {
|
||||||
|
const promises = images.map(image => {
|
||||||
|
return new Promise<File | Blob>(resolve => {
|
||||||
|
new Compressor(image, {
|
||||||
|
quality: 0.6,
|
||||||
|
maxWidth: 1200,
|
||||||
|
mimeType: "image/webp",
|
||||||
|
success(result) {
|
||||||
|
const file = new File([result], "name", {
|
||||||
|
type: "image/webp",
|
||||||
|
});
|
||||||
|
resolve(result);
|
||||||
|
},
|
||||||
|
error(err) {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return await Promise.all(promises);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDrop = useCallback(
|
||||||
|
async (acceptedFiles: File[]) => {
|
||||||
|
if (acceptedFiles.length > imageLimit) {
|
||||||
|
const notificationObj = {
|
||||||
|
msg: `Only ${imageLimit} images can be published`,
|
||||||
|
alertType: "error",
|
||||||
|
};
|
||||||
|
dispatch(setNotification(notificationObj));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const compressedImages = await compressImages(acceptedFiles);
|
||||||
|
if (!compressedImages) return;
|
||||||
|
|
||||||
|
const base64Iamges = await Promise.all(
|
||||||
|
compressedImages.map(image => toBase64(image as File))
|
||||||
|
);
|
||||||
|
|
||||||
|
onPick(base64Iamges 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;
|
28
src/components/common/ImagePublisher/TimesSVG.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
export interface IconTypes {
|
||||||
|
color?: string;
|
||||||
|
height: string;
|
||||||
|
width: string;
|
||||||
|
className?: string;
|
||||||
|
onClickFunc?: (e?: any) => void;
|
||||||
|
}
|
||||||
|
export const TimesSVG: React.FC<IconTypes> = ({
|
||||||
|
color,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
className,
|
||||||
|
onClickFunc,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
onClick={onClickFunc}
|
||||||
|
className={className}
|
||||||
|
fill={color}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
height={height}
|
||||||
|
viewBox="0 -960 960 960"
|
||||||
|
width={width}
|
||||||
|
>
|
||||||
|
<path d="m249-207-42-42 231-231-231-231 42-42 231 231 231-231 42 42-231 231 231 231-42 42-231-231-231 231Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
48
src/components/common/LazyLoad.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react'
|
||||||
|
import { useInView } from 'react-intersection-observer'
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onLoadMore: () => Promise<void>
|
||||||
|
isLoading?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const LazyLoad: React.FC<Props> = ({ onLoadMore, isLoading }) => {
|
||||||
|
const [isFetching, setIsFetching] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const firstLoad = useRef(false)
|
||||||
|
const [ref, inView] = useInView({
|
||||||
|
threshold: 0.7
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inView) {
|
||||||
|
setIsFetching(true)
|
||||||
|
onLoadMore().finally(() => {
|
||||||
|
setIsFetching(false)
|
||||||
|
firstLoad.current = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [inView])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
minHeight: '25px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
visibility: (isFetching || isLoading) ? 'visible' : 'hidden'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LazyLoad
|
225
src/components/common/MultiplePublish/MultiplePublishAll.tsx
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
Modal,
|
||||||
|
Typography,
|
||||||
|
useTheme,
|
||||||
|
} from "@mui/material";
|
||||||
|
import React, { useCallback, useEffect, useState, useRef } from "react";
|
||||||
|
import { CircleSVG } from "../../../assets/svgs/CircleSVG";
|
||||||
|
import { EmptyCircleSVG } from "../../../assets/svgs/EmptyCircleSVG";
|
||||||
|
import { styled } from "@mui/system";
|
||||||
|
|
||||||
|
interface Publish {
|
||||||
|
resources: any[];
|
||||||
|
action: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MultiplePublishProps {
|
||||||
|
publishes: Publish;
|
||||||
|
isOpen: boolean;
|
||||||
|
onSubmit: () => void;
|
||||||
|
onError: (message?: string) => void;
|
||||||
|
}
|
||||||
|
export const MultiplePublish = ({
|
||||||
|
publishes,
|
||||||
|
isOpen,
|
||||||
|
onSubmit,
|
||||||
|
onError,
|
||||||
|
}: MultiplePublishProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const listOfSuccessfulPublishesRef = useRef([]);
|
||||||
|
const [listOfSuccessfulPublishes, setListOfSuccessfulPublishes] = useState<
|
||||||
|
any[]
|
||||||
|
>([]);
|
||||||
|
const [listOfUnsuccessfulPublishes, setListOfUnSuccessfulPublishes] =
|
||||||
|
useState<any[]>([]);
|
||||||
|
const [currentlyInPublish, setCurrentlyInPublish] = useState(null);
|
||||||
|
const hasStarted = useRef(false);
|
||||||
|
const publish = useCallback(async (pub: any) => {
|
||||||
|
const lengthOfResources = pub?.resources?.length;
|
||||||
|
const lengthOfTimeout = lengthOfResources * 30000;
|
||||||
|
return await qortalRequestWithTimeout(pub, lengthOfTimeout);
|
||||||
|
}, []);
|
||||||
|
const [isPublishing, setIsPublishing] = useState(true);
|
||||||
|
|
||||||
|
const handlePublish = useCallback(
|
||||||
|
async (pub: any) => {
|
||||||
|
try {
|
||||||
|
setCurrentlyInPublish(pub?.identifier);
|
||||||
|
setIsPublishing(true);
|
||||||
|
const res = await publish(pub);
|
||||||
|
|
||||||
|
onSubmit();
|
||||||
|
setListOfUnSuccessfulPublishes([]);
|
||||||
|
} catch (error: any) {
|
||||||
|
const unsuccessfulPublishes = error?.error?.unsuccessfulPublishes || [];
|
||||||
|
if (error?.error === "User declined request") {
|
||||||
|
onError();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error?.error === "The request timed out") {
|
||||||
|
onError("The request timed out");
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unsuccessfulPublishes?.length > 0) {
|
||||||
|
setListOfUnSuccessfulPublishes(unsuccessfulPublishes);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsPublishing(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[publish]
|
||||||
|
);
|
||||||
|
|
||||||
|
const retry = () => {
|
||||||
|
let newlistOfMultiplePublishes: any[] = [];
|
||||||
|
listOfUnsuccessfulPublishes?.forEach(item => {
|
||||||
|
const findPub = publishes?.resources.find(
|
||||||
|
(res: any) => res?.identifier === item.identifier
|
||||||
|
);
|
||||||
|
if (findPub) {
|
||||||
|
newlistOfMultiplePublishes.push(findPub);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const multiplePublish = {
|
||||||
|
...publishes,
|
||||||
|
resources: newlistOfMultiplePublishes,
|
||||||
|
};
|
||||||
|
handlePublish(multiplePublish);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startPublish = useCallback(
|
||||||
|
async (pubs: any) => {
|
||||||
|
await handlePublish(pubs);
|
||||||
|
},
|
||||||
|
[handlePublish, onSubmit, listOfSuccessfulPublishes, publishes]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (publishes && !hasStarted.current) {
|
||||||
|
hasStarted.current = true;
|
||||||
|
startPublish(publishes);
|
||||||
|
}
|
||||||
|
}, [startPublish, publishes, listOfSuccessfulPublishes]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={isOpen}
|
||||||
|
aria-labelledby="modal-title"
|
||||||
|
aria-describedby="modal-description"
|
||||||
|
>
|
||||||
|
<ModalBody
|
||||||
|
sx={{
|
||||||
|
minHeight: "50vh",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{publishes?.resources?.map((publish: any) => {
|
||||||
|
const unpublished = listOfUnsuccessfulPublishes.map(
|
||||||
|
item => item?.identifier
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "20px",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
key={publish?.identifier}
|
||||||
|
>
|
||||||
|
<Typography>{publish?.identifier}</Typography>
|
||||||
|
{!isPublishing && hasStarted.current ? (
|
||||||
|
<>
|
||||||
|
{!unpublished.includes(publish.identifier) ? (
|
||||||
|
<CircleSVG
|
||||||
|
color={theme.palette.text.primary}
|
||||||
|
height="24px"
|
||||||
|
width="24px"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<EmptyCircleSVG
|
||||||
|
color={theme.palette.text.primary}
|
||||||
|
height="24px"
|
||||||
|
width="24px"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<CircularProgress size={16} color="secondary" />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{!isPublishing && listOfUnsuccessfulPublishes.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
marginTop: "20px",
|
||||||
|
fontSize: "16px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Some files were not published. Please try again. It's important
|
||||||
|
that all the files get published. Maybe wait a couple minutes if
|
||||||
|
the error keeps occurring
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={() => {
|
||||||
|
retry();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ModalBody>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ModalBody = styled(Box)(({ theme }) => ({
|
||||||
|
position: "absolute",
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
borderRadius: "4px",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
width: "75%",
|
||||||
|
maxWidth: "900px",
|
||||||
|
padding: "15px 35px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "17px",
|
||||||
|
overflowY: "auto",
|
||||||
|
maxHeight: "95vh",
|
||||||
|
boxShadow:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)"
|
||||||
|
: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px",
|
||||||
|
"&::-webkit-scrollbar-track": {
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar-track:hover": {
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar": {
|
||||||
|
width: "16px",
|
||||||
|
height: "10px",
|
||||||
|
backgroundColor: theme.palette.mode === "light" ? "#f6f8fa" : "#292d3e",
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar-thumb": {
|
||||||
|
backgroundColor: theme.palette.mode === "light" ? "#d3d9e1" : "#575757",
|
||||||
|
borderRadius: "8px",
|
||||||
|
backgroundClip: "content-box",
|
||||||
|
border: "4px solid transparent",
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar-thumb:hover": {
|
||||||
|
backgroundColor: theme.palette.mode === "light" ? "#b7bcc4" : "#474646",
|
||||||
|
},
|
||||||
|
}));
|
86
src/components/common/Notification/Notification.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
|
import { toast, ToastContainer, Zoom, Slide } from 'react-toastify'
|
||||||
|
import { removeNotification } from '../../../state/features/notificationsSlice'
|
||||||
|
import 'react-toastify/dist/ReactToastify.css'
|
||||||
|
import { RootState } from '../../../state/store'
|
||||||
|
|
||||||
|
const Notification = () => {
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
|
||||||
|
const { alertTypes } = useSelector((state: RootState) => state.notifications)
|
||||||
|
|
||||||
|
if (alertTypes.alertError) {
|
||||||
|
toast.error(`❌ ${alertTypes?.alertError}`, {
|
||||||
|
position: 'bottom-right',
|
||||||
|
autoClose: 4000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
icon: false
|
||||||
|
})
|
||||||
|
dispatch(removeNotification())
|
||||||
|
}
|
||||||
|
if (alertTypes.alertSuccess) {
|
||||||
|
toast.success(`✔️ ${alertTypes?.alertSuccess}`, {
|
||||||
|
position: 'bottom-right',
|
||||||
|
autoClose: 4000,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
icon: false
|
||||||
|
})
|
||||||
|
dispatch(removeNotification())
|
||||||
|
}
|
||||||
|
if (alertTypes.alertInfo) {
|
||||||
|
toast.info(`${alertTypes?.alertInfo}`, {
|
||||||
|
position: 'top-right',
|
||||||
|
autoClose: 1300,
|
||||||
|
hideProgressBar: false,
|
||||||
|
closeOnClick: true,
|
||||||
|
pauseOnHover: true,
|
||||||
|
draggable: true,
|
||||||
|
progress: undefined,
|
||||||
|
theme: 'light'
|
||||||
|
})
|
||||||
|
dispatch(removeNotification())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alertTypes.alertInfo) {
|
||||||
|
return (
|
||||||
|
<ToastContainer
|
||||||
|
position="top-right"
|
||||||
|
autoClose={2000}
|
||||||
|
hideProgressBar={false}
|
||||||
|
newestOnTop={false}
|
||||||
|
closeOnClick
|
||||||
|
rtl={false}
|
||||||
|
pauseOnFocusLoss
|
||||||
|
draggable
|
||||||
|
pauseOnHover
|
||||||
|
theme="light"
|
||||||
|
toastStyle={{ fontSize: '16px' }}
|
||||||
|
transition={Slide}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastContainer
|
||||||
|
transition={Zoom}
|
||||||
|
position="bottom-right"
|
||||||
|
autoClose={false}
|
||||||
|
hideProgressBar={false}
|
||||||
|
newestOnTop={false}
|
||||||
|
closeOnClick
|
||||||
|
rtl={false}
|
||||||
|
draggable
|
||||||
|
pauseOnHover
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Notification
|
43
src/components/common/PageLoader.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress';
|
||||||
|
import Box from '@mui/system/Box';
|
||||||
|
import { useTheme } from '@mui/material'
|
||||||
|
|
||||||
|
interface PageLoaderProps {
|
||||||
|
size?: number
|
||||||
|
thickness?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const PageLoader: React.FC<PageLoaderProps> = ({
|
||||||
|
size = 40,
|
||||||
|
thickness = 5
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '100vh',
|
||||||
|
width: '100%',
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.25)',
|
||||||
|
zIndex: 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
|
40
src/components/common/TextEditor/DisplayHtml.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import DOMPurify from "dompurify";
|
||||||
|
import "react-quill/dist/quill.snow.css";
|
||||||
|
import "react-quill/dist/quill.core.css";
|
||||||
|
import "react-quill/dist/quill.bubble.css";
|
||||||
|
import { convertQortalLinks } from "./utils";
|
||||||
|
import { Box, styled } from "@mui/material";
|
||||||
|
|
||||||
|
|
||||||
|
const CrowdfundInlineContent = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontSize: "19px",
|
||||||
|
fontWeight: 400,
|
||||||
|
letterSpacing: 0,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
width: '100%'
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const DisplayHtml = ({ html }) => {
|
||||||
|
const cleanContent = useMemo(() => {
|
||||||
|
if (!html) return null;
|
||||||
|
|
||||||
|
const sanitize: string = DOMPurify.sanitize(html, {
|
||||||
|
USE_PROFILES: { html: true },
|
||||||
|
});
|
||||||
|
const anchorQortal = convertQortalLinks(sanitize);
|
||||||
|
return anchorQortal;
|
||||||
|
}, [html]);
|
||||||
|
|
||||||
|
if (!cleanContent) return null;
|
||||||
|
return (
|
||||||
|
<CrowdfundInlineContent>
|
||||||
|
<div
|
||||||
|
className="ql-editor"
|
||||||
|
dangerouslySetInnerHTML={{ __html: cleanContent }}
|
||||||
|
/>
|
||||||
|
</CrowdfundInlineContent>
|
||||||
|
);
|
||||||
|
};
|
38
src/components/common/TextEditor/TextEditor.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactQuill, { Quill } from "react-quill";
|
||||||
|
import "react-quill/dist/quill.snow.css";
|
||||||
|
import ImageResize from "quill-image-resize-module-react";
|
||||||
|
|
||||||
|
Quill.register("modules/imageResize", ImageResize);
|
||||||
|
|
||||||
|
const modules = {
|
||||||
|
imageResize: {
|
||||||
|
parchment: Quill.import("parchment"),
|
||||||
|
modules: ["Resize", "DisplaySize"],
|
||||||
|
},
|
||||||
|
toolbar: [
|
||||||
|
["bold", "italic", "underline", "strike"], // styled text
|
||||||
|
["blockquote", "code-block"], // blocks
|
||||||
|
[{ header: 1 }, { header: 2 }], // custom button values
|
||||||
|
[{ list: "ordered" }, { list: "bullet" }], // lists
|
||||||
|
[{ script: "sub" }, { script: "super" }], // superscript/subscript
|
||||||
|
[{ indent: "-1" }, { indent: "+1" }], // outdent/indent
|
||||||
|
[{ direction: "rtl" }], // text direction
|
||||||
|
[{ size: ["small", false, "large", "huge"] }], // custom dropdown
|
||||||
|
[{ header: [1, 2, 3, 4, 5, 6, false] }], // custom button values
|
||||||
|
[{ color: [] }, { background: [] }], // dropdown with defaults
|
||||||
|
[{ font: [] }], // font family
|
||||||
|
[{ align: [] }], // text align
|
||||||
|
["clean"], // remove formatting
|
||||||
|
],
|
||||||
|
};
|
||||||
|
export const TextEditor = ({ inlineContent, setInlineContent }) => {
|
||||||
|
return (
|
||||||
|
<ReactQuill
|
||||||
|
theme="snow"
|
||||||
|
value={inlineContent}
|
||||||
|
onChange={setInlineContent}
|
||||||
|
modules={modules}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
26
src/components/common/TextEditor/utils.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
export function convertQortalLinks(inputHtml) {
|
||||||
|
// Regular expression to match 'qortal://...' URLs.
|
||||||
|
// This will stop at the first whitespace, comma, or HTML tag
|
||||||
|
var regex = /(qortal:\/\/[^\s,<]+)/g;
|
||||||
|
|
||||||
|
// Replace matches in inputHtml with formatted anchor tag
|
||||||
|
var outputHtml = inputHtml.replace(regex, function (match) {
|
||||||
|
return `<a href="${match}" className="qortal-link">${match}</a>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return outputHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractTextFromHTML(htmlString: any, length = 150) {
|
||||||
|
// Create a temporary DOM element
|
||||||
|
const tempDiv = document.createElement("div");
|
||||||
|
// Replace br tags and block-level tags with a space before setting the HTML content
|
||||||
|
const htmlWithSpaces = htmlString.replace(/<\/?(br|p|div|h[1-6]|ul|ol|li|blockquote)[^>]*>/gi, ' ');
|
||||||
|
tempDiv.innerHTML = htmlWithSpaces;
|
||||||
|
// Extract the text content
|
||||||
|
let text = tempDiv.textContent || tempDiv.innerText || "";
|
||||||
|
// Replace multiple spaces with a single space and trim
|
||||||
|
text = text.replace(/\s+/g, ' ').trim();
|
||||||
|
// Slice the text to the desired length
|
||||||
|
return text.slice(0, length);
|
||||||
|
}
|
857
src/components/common/VideoPlayer.tsx
Normal file
@ -0,0 +1,857 @@
|
|||||||
|
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'
|
||||||
|
import { setVideoPlaying } from '../../state/features/globalSlice'
|
||||||
|
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;
|
||||||
|
max-height: calc(100vh - 150px);
|
||||||
|
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
|
||||||
|
customStyle?: any
|
||||||
|
user?: string
|
||||||
|
jsonId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||||
|
poster,
|
||||||
|
name,
|
||||||
|
identifier,
|
||||||
|
service,
|
||||||
|
autoplay = true,
|
||||||
|
from = null,
|
||||||
|
customStyle = {},
|
||||||
|
user = '',
|
||||||
|
jsonId = ''
|
||||||
|
}) => {
|
||||||
|
const dispatch = useDispatch()
|
||||||
|
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 videoPlaying = useSelector((state: RootState) => state.global.videoPlaying);
|
||||||
|
const reDownload = useRef<boolean>(false)
|
||||||
|
const isFetchingProperties = useRef<boolean>(false)
|
||||||
|
|
||||||
|
const status = useRef<null | string>(null)
|
||||||
|
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 refetch = React.useCallback(async () => {
|
||||||
|
if (!name || !identifier || !service || isFetchingProperties.current) return
|
||||||
|
try {
|
||||||
|
isFetchingProperties.current = true
|
||||||
|
await qortalRequest({
|
||||||
|
action: 'GET_QDN_RESOURCE_PROPERTIES',
|
||||||
|
name,
|
||||||
|
service,
|
||||||
|
identifier
|
||||||
|
})
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
isFetchingProperties.current = false
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [identifier, name, service])
|
||||||
|
|
||||||
|
|
||||||
|
const toggleRef = useRef<any>(null)
|
||||||
|
const { downloadVideo } = useContext(MyContext)
|
||||||
|
const togglePlay = async () => {
|
||||||
|
if (!videoRef.current) return
|
||||||
|
setStartPlay(true)
|
||||||
|
if (!src || resourceStatus?.status !== 'READY') {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(()=> {
|
||||||
|
if(videoPlaying && videoPlaying.id === identifier && src && videoRef?.current){
|
||||||
|
handleCanPlay()
|
||||||
|
videoRef.current.volume = videoPlaying.volume
|
||||||
|
videoRef.current.currentTime = videoPlaying.currentTime
|
||||||
|
videoRef.current.play()
|
||||||
|
setPlaying(true)
|
||||||
|
setStartPlay(true)
|
||||||
|
dispatch(setVideoPlaying(null))
|
||||||
|
}
|
||||||
|
}, [videoPlaying, identifier, src])
|
||||||
|
|
||||||
|
const handleCanPlay = () => {
|
||||||
|
setIsLoading(false)
|
||||||
|
setCanPlay(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSrc = React.useCallback(async () => {
|
||||||
|
if (!name || !identifier || !service || !jsonId || !user) return
|
||||||
|
try {
|
||||||
|
downloadVideo({
|
||||||
|
name,
|
||||||
|
service,
|
||||||
|
identifier,
|
||||||
|
properties: {
|
||||||
|
jsonId,
|
||||||
|
user
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
}, [identifier, name, service, jsonId, user])
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
dispatch(setVideoPlaying(videoElement))
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const refetchInInterval = ()=> {
|
||||||
|
try {
|
||||||
|
const interval = setInterval(()=> {
|
||||||
|
if(status?.current === 'DOWNLOADED'){
|
||||||
|
refetch()
|
||||||
|
}
|
||||||
|
if(status?.current === 'READY'){
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
}, 7500)
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(resourceStatus?.status){
|
||||||
|
status.current = resourceStatus?.status
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
resourceStatus?.status === 'DOWNLOADED' &&
|
||||||
|
reDownload?.current === false
|
||||||
|
) {
|
||||||
|
refetchInInterval()
|
||||||
|
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()
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<Box
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
left={0}
|
||||||
|
right={0}
|
||||||
|
bottom={resourceStatus?.status === 'READY' ? '55px ' : 0}
|
||||||
|
display="flex"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
zIndex={25}
|
||||||
|
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 === 'NOT_PUBLISHED' && (
|
||||||
|
<>Video file was not published. Please inform the publisher!</>
|
||||||
|
)}
|
||||||
|
{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)}
|
||||||
|
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>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
|
||||||
|
dispatch(setVideoPlaying(null))
|
||||||
|
togglePlay()
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlayArrow
|
||||||
|
sx={{
|
||||||
|
width: '50px',
|
||||||
|
height: '50px',
|
||||||
|
color: 'white'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<VideoElement
|
||||||
|
id={identifier}
|
||||||
|
ref={videoRef}
|
||||||
|
src={!startPlay ? '' : resourceStatus?.status === 'READY' ? src : ''}
|
||||||
|
poster={!startPlay ? 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}
|
||||||
|
sx={{
|
||||||
|
maxWidth: '100px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
</VideoContainer>
|
||||||
|
)
|
||||||
|
}
|
648
src/components/common/VideoPlayerGlobal.tsx
Normal file
@ -0,0 +1,648 @@
|
|||||||
|
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import { Box, IconButton, Slider, useTheme } 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 CloseIcon from '@mui/icons-material/Close';
|
||||||
|
|
||||||
|
import { Menu, MenuItem } from '@mui/material'
|
||||||
|
import { MoreVert as MoreIcon } from '@mui/icons-material'
|
||||||
|
import { setVideoPlaying } from '../../state/features/globalSlice'
|
||||||
|
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;
|
||||||
|
max-height: calc(100vh - 150px);
|
||||||
|
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
|
||||||
|
customStyle?: any
|
||||||
|
user?: string
|
||||||
|
jsonId?: string
|
||||||
|
element?: null | any
|
||||||
|
checkIfDrag?: ()=> boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const VideoPlayerGlobal: React.FC<VideoPlayerProps> = ({
|
||||||
|
poster,
|
||||||
|
name,
|
||||||
|
identifier,
|
||||||
|
service,
|
||||||
|
autoplay = true,
|
||||||
|
from = null,
|
||||||
|
customStyle = {},
|
||||||
|
user = '',
|
||||||
|
jsonId = '',
|
||||||
|
element,
|
||||||
|
checkIfDrag
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
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 dispatch = useDispatch()
|
||||||
|
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 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(checkIfDrag && checkIfDrag()) return
|
||||||
|
if (!videoRef.current) return
|
||||||
|
if (playing) {
|
||||||
|
videoRef.current.pause()
|
||||||
|
} else {
|
||||||
|
videoRef.current.play()
|
||||||
|
}
|
||||||
|
setPlaying((prev)=> !prev)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 handleCanPlay = () => {
|
||||||
|
setIsLoading(false)
|
||||||
|
setCanPlay(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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 src = videoRef.current.src
|
||||||
|
const currentTime = videoRef.current.currentTime
|
||||||
|
videoRef.current.src = src
|
||||||
|
videoRef.current.load()
|
||||||
|
videoRef.current.currentTime = currentTime
|
||||||
|
if (playing) {
|
||||||
|
videoRef.current.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(()=> {
|
||||||
|
if(element){
|
||||||
|
let oldElement = document.getElementById('videoPlayer');
|
||||||
|
if(oldElement && oldElement?.parentNode){
|
||||||
|
oldElement?.parentNode.replaceChild(element, oldElement);
|
||||||
|
videoRef.current = element
|
||||||
|
setPlaying(true)
|
||||||
|
setCanPlay(true)
|
||||||
|
setStartPlay(true)
|
||||||
|
videoRef?.current?.addEventListener('click', ()=> {})
|
||||||
|
videoRef?.current?.addEventListener('timeupdate', updateProgress)
|
||||||
|
videoRef?.current?.addEventListener('ended', handleEnded)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}, [element])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VideoContainer
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyUp={keyboardShortcutsUp}
|
||||||
|
onKeyDown={keyboardShortcutsDown}
|
||||||
|
style={{
|
||||||
|
padding: from === 'create' ? '8px' : 0,
|
||||||
|
zIndex: 1000,
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="closePlayer">
|
||||||
|
|
||||||
|
<CloseIcon onClick={()=> {
|
||||||
|
dispatch(setVideoPlaying(null))
|
||||||
|
}} sx={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
backgroundColor: 'rgba(0,0,0,.5)'
|
||||||
|
}}></CloseIcon>
|
||||||
|
</div>
|
||||||
|
<div onClick={togglePlay}>
|
||||||
|
<VideoElement
|
||||||
|
id="videoPlayer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<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}
|
||||||
|
sx={{
|
||||||
|
maxWidth: '100px'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
</VideoContainer>
|
||||||
|
)
|
||||||
|
}
|
121
src/components/layout/Navbar/Navbar-styles.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import { AppBar, Button, Typography, Box } from "@mui/material";
|
||||||
|
import { styled } from "@mui/system";
|
||||||
|
import { LightModeSVG } from "../../../assets/svgs/LightModeSVG";
|
||||||
|
import { DarkModeSVG } from "../../../assets/svgs/DarkModeSVG";
|
||||||
|
|
||||||
|
export const CustomAppBar = styled(AppBar)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
width: "100%",
|
||||||
|
padding: "5px 16px",
|
||||||
|
backgroundImage: "none",
|
||||||
|
borderBottom: `1px solid ${theme.palette.primary.light}`,
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
[theme.breakpoints.only("xs")]: {
|
||||||
|
gap: "15px",
|
||||||
|
},
|
||||||
|
height: "100px",
|
||||||
|
}));
|
||||||
|
export const LogoContainer = styled("div")({
|
||||||
|
cursor: "pointer",
|
||||||
|
height: "100%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CustomTitle = styled(Typography)({
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "#000000",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AuthenticateButton = styled(Button)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "8px 15px",
|
||||||
|
borderRadius: "40px",
|
||||||
|
gap: "4px",
|
||||||
|
backgroundColor: theme.palette.secondary.main,
|
||||||
|
color: "#fff",
|
||||||
|
fontFamily: "Raleway",
|
||||||
|
transition: "all 0.3s ease-in-out",
|
||||||
|
boxShadow: "none",
|
||||||
|
"&:hover": {
|
||||||
|
cursor: "pointer",
|
||||||
|
boxShadow: "rgba(0, 0, 0, 0.15) 1.95px 1.95px 2.6px;",
|
||||||
|
backgroundColor: theme.palette.secondary.dark,
|
||||||
|
filter: "brightness(1.1)",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const AvatarContainer = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
"&:hover": {
|
||||||
|
cursor: "pointer",
|
||||||
|
"& #expand-icon": {
|
||||||
|
transition: "all 0.3s ease-in-out",
|
||||||
|
filter: "brightness(0.7)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DropdownContainer = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "5px",
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
padding: "10px 15px",
|
||||||
|
transition: "all 0.4s ease-in-out",
|
||||||
|
"&:hover": {
|
||||||
|
cursor: "pointer",
|
||||||
|
filter:
|
||||||
|
theme.palette.mode === "light" ? "brightness(0.95)" : "brightness(1.1)",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const DropdownText = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Raleway",
|
||||||
|
fontSize: "16px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const NavbarName = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Raleway",
|
||||||
|
fontSize: "18px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
margin: "0 10px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const ThemeSelectRow = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "5px",
|
||||||
|
flexBasis: 0,
|
||||||
|
height: "100%",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const LightModeIcon = styled(LightModeSVG)(({ theme }) => ({
|
||||||
|
transition: "all 0.1s ease-in-out",
|
||||||
|
"&:hover": {
|
||||||
|
cursor: "pointer",
|
||||||
|
filter:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "drop-shadow(0px 4px 6px rgba(255, 255, 255, 0.6))"
|
||||||
|
: "drop-shadow(0px 4px 6px rgba(99, 88, 88, 0.1))",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const DarkModeIcon = styled(DarkModeSVG)(({ theme }) => ({
|
||||||
|
transition: "all 0.1s ease-in-out",
|
||||||
|
"&:hover": {
|
||||||
|
cursor: "pointer",
|
||||||
|
filter:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "drop-shadow(0px 4px 6px rgba(255, 255, 255, 0.6))"
|
||||||
|
: "drop-shadow(0px 4px 6px rgba(99, 88, 88, 0.1))",
|
||||||
|
},
|
||||||
|
}));
|
312
src/components/layout/Navbar/Navbar.tsx
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
import React, { useRef, useState } from "react";
|
||||||
|
import { Box, Input, Popover, Typography, useTheme } from "@mui/material";
|
||||||
|
import { BlockedNamesModal } from "../../common/BlockedNamesModal/BlockedNamesModal";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AvatarContainer,
|
||||||
|
CustomAppBar,
|
||||||
|
DropdownContainer,
|
||||||
|
DropdownText,
|
||||||
|
LogoContainer,
|
||||||
|
NavbarName,
|
||||||
|
ThemeSelectRow,
|
||||||
|
} from "./Navbar-styles";
|
||||||
|
import { AccountCircleSVG } from "../../../assets/svgs/AccountCircleSVG";
|
||||||
|
import BackspaceIcon from "@mui/icons-material/Backspace";
|
||||||
|
|
||||||
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||||
|
import PersonOffIcon from "@mui/icons-material/PersonOff";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import SearchIcon from "@mui/icons-material/Search";
|
||||||
|
|
||||||
|
import { DownloadTaskManager } from "../../common/DownloadTaskManager";
|
||||||
|
import QSupportLogo from "../../../assets/img/Q-SupportIcon.webp";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import {
|
||||||
|
addFilteredFiles,
|
||||||
|
setFilterValue,
|
||||||
|
setIsFiltering,
|
||||||
|
} from "../../../state/features/fileSlice.ts";
|
||||||
|
import { RootState } from "../../../state/store";
|
||||||
|
import { useWindowSize } from "../../../hooks/useWindowSize";
|
||||||
|
import { PublishIssue } from "../../PublishIssue/PublishIssue.tsx";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
userName: string | null;
|
||||||
|
userAvatar: string;
|
||||||
|
authenticate: () => void;
|
||||||
|
setTheme: (val: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NavBar: React.FC<Props> = ({
|
||||||
|
isAuthenticated,
|
||||||
|
userName,
|
||||||
|
userAvatar,
|
||||||
|
authenticate,
|
||||||
|
setTheme,
|
||||||
|
}) => {
|
||||||
|
const windowSize = useWindowSize();
|
||||||
|
const searchValRef = useRef("");
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const theme = useTheme();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [openUserDropdown, setOpenUserDropdown] = useState<boolean>(false);
|
||||||
|
const [isOpenBlockedNamesModal, setIsOpenBlockedNamesModal] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
|
||||||
|
const [anchorElNotification, setAnchorElNotification] =
|
||||||
|
React.useState<HTMLButtonElement | null>(null);
|
||||||
|
const filterValue = useSelector((state: RootState) => state.file.filterValue);
|
||||||
|
|
||||||
|
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 openPopover = Boolean(anchorElNotification);
|
||||||
|
const idNotification = openPopover
|
||||||
|
? "simple-popover-notification"
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const handleCloseUserDropdown = () => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
setOpenUserDropdown(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCloseBlockedNames = () => {
|
||||||
|
setIsOpenBlockedNamesModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CustomAppBar position="sticky" elevation={2}>
|
||||||
|
<ThemeSelectRow>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
height: "100%",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LogoContainer
|
||||||
|
onClick={() => {
|
||||||
|
navigate("/");
|
||||||
|
dispatch(setIsFiltering(false));
|
||||||
|
dispatch(setFilterValue(""));
|
||||||
|
dispatch(addFilteredFiles([]));
|
||||||
|
searchValRef.current = "";
|
||||||
|
if (!inputRef.current) return;
|
||||||
|
inputRef.current.value = "";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={QSupportLogo}
|
||||||
|
style={{
|
||||||
|
width: "auto",
|
||||||
|
height: "100px",
|
||||||
|
padding: "2px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</LogoContainer>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontSize: "30px",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Welcome to Q-Support
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</ThemeSelectRow>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Popover
|
||||||
|
id={idNotification}
|
||||||
|
open={openPopover}
|
||||||
|
anchorEl={anchorElNotification}
|
||||||
|
onClose={closeNotificationPopover}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: "bottom",
|
||||||
|
horizontal: "left",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 1,
|
||||||
|
padding: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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(addFilteredFiles([]));
|
||||||
|
searchValRef.current = "";
|
||||||
|
if (!inputRef.current) return;
|
||||||
|
inputRef.current.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigate("/");
|
||||||
|
dispatch(setIsFiltering(true));
|
||||||
|
dispatch(addFilteredFiles([]));
|
||||||
|
dispatch(setFilterValue(searchValRef.current));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="Search"
|
||||||
|
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(addFilteredFiles([]));
|
||||||
|
searchValRef.current = "";
|
||||||
|
if (!inputRef.current) return;
|
||||||
|
inputRef.current.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigate("/");
|
||||||
|
dispatch(setIsFiltering(true));
|
||||||
|
dispatch(addFilteredFiles([]));
|
||||||
|
dispatch(setFilterValue(searchValRef.current));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<BackspaceIcon
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(setIsFiltering(false));
|
||||||
|
dispatch(setFilterValue(""));
|
||||||
|
dispatch(addFilteredFiles([]));
|
||||||
|
searchValRef.current = "";
|
||||||
|
if (!inputRef.current) return;
|
||||||
|
inputRef.current.value = "";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<DownloadTaskManager />
|
||||||
|
{isAuthenticated && userName && (
|
||||||
|
<>
|
||||||
|
<AvatarContainer
|
||||||
|
onClick={(e: any) => {
|
||||||
|
handleClick(e);
|
||||||
|
setOpenUserDropdown(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<NavbarName>{userName}</NavbarName>
|
||||||
|
{!userAvatar ? (
|
||||||
|
<AccountCircleSVG
|
||||||
|
color={theme.palette.text.primary}
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
src={userAvatar}
|
||||||
|
alt="User Avatar"
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
style={{
|
||||||
|
borderRadius: "50%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ExpandMoreIcon id="expand-icon" sx={{ color: "#ACB6BF" }} />
|
||||||
|
</AvatarContainer>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<AvatarContainer>
|
||||||
|
{isAuthenticated && userName && (
|
||||||
|
<>
|
||||||
|
<PublishIssue />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AvatarContainer>
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
id={"user-popover"}
|
||||||
|
open={openUserDropdown}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={handleCloseUserDropdown}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: "bottom",
|
||||||
|
horizontal: "left",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownContainer
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpenBlockedNamesModal(true);
|
||||||
|
handleCloseUserDropdown();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PersonOffIcon
|
||||||
|
sx={{
|
||||||
|
color: "#e35050",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<DropdownText>Blocked Names</DropdownText>
|
||||||
|
</DropdownContainer>
|
||||||
|
</Popover>
|
||||||
|
{isOpenBlockedNamesModal && (
|
||||||
|
<BlockedNamesModal
|
||||||
|
open={isOpenBlockedNamesModal}
|
||||||
|
onClose={onCloseBlockedNames}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</CustomAppBar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavBar;
|
48
src/constants/Categories/1stCategories.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import audioIcon from "../../assets/icons/audio.webp";
|
||||||
|
import bookIcon from "../../assets/icons/book.webp";
|
||||||
|
import documentIcon from "../../assets/icons/document.webp";
|
||||||
|
import gamingIcon from "../../assets/icons/gaming.webp";
|
||||||
|
import imageIcon from "../../assets/icons/image.webp";
|
||||||
|
import softwareIcon from "../../assets/icons/software.webp";
|
||||||
|
import unknownIcon from "../../assets/icons/unknown.webp";
|
||||||
|
import videoIcon from "../../assets/icons/video.webp";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Categories,
|
||||||
|
Category,
|
||||||
|
CategoryData,
|
||||||
|
} from "../../components/common/CategoryList/CategoryList.tsx";
|
||||||
|
import {
|
||||||
|
getAllCategoriesWithIcons,
|
||||||
|
sortCategory,
|
||||||
|
} from "./CategoryFunctions.ts";
|
||||||
|
import { QappCategories, SupportState } from "./2ndCategories.ts";
|
||||||
|
|
||||||
|
export const firstCategories: Category[] = [
|
||||||
|
{ id: 1, name: "Core" },
|
||||||
|
{ id: 2, name: "UI" },
|
||||||
|
{ id: 3, name: "Q-Apps" },
|
||||||
|
{ id: 4, name: "Website" },
|
||||||
|
{ id: 5, name: "Marketing" },
|
||||||
|
{ id: 99, name: "Other" },
|
||||||
|
];
|
||||||
|
export const secondCategories: Categories = {
|
||||||
|
1: SupportState,
|
||||||
|
2: SupportState,
|
||||||
|
3: QappCategories,
|
||||||
|
4: SupportState,
|
||||||
|
5: SupportState,
|
||||||
|
99: SupportState,
|
||||||
|
};
|
||||||
|
|
||||||
|
export let thirdCategories: Categories = {};
|
||||||
|
QappCategories.map(
|
||||||
|
supportStateCategory =>
|
||||||
|
(thirdCategories[supportStateCategory.id] = SupportState)
|
||||||
|
);
|
||||||
|
export const allCategoryData: CategoryData = {
|
||||||
|
category: firstCategories,
|
||||||
|
subCategories: [secondCategories, thirdCategories],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const iconCategories = getAllCategoriesWithIcons();
|
23
src/constants/Categories/2ndCategories.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import OpenIcon from "../../assets/icons/OpenIcon.png";
|
||||||
|
import ClosedIcon from "../../assets/icons/ClosedIcon.png";
|
||||||
|
import InProgressIcon from "../../assets/icons/InProgressIcon.png";
|
||||||
|
import CompleteIcon from "../../assets/icons/CompleteIcon.png";
|
||||||
|
|
||||||
|
export const SupportState = [
|
||||||
|
{ id: 101, name: "Open", icon: OpenIcon },
|
||||||
|
{ id: 102, name: "Closed", icon: ClosedIcon },
|
||||||
|
{ id: 103, name: "In Progress", icon: InProgressIcon },
|
||||||
|
{ id: 104, name: "Complete", icon: CompleteIcon },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const QappCategories = [
|
||||||
|
{ id: 301, name: "Q-Blog" },
|
||||||
|
{ id: 302, name: "Q-Mail" },
|
||||||
|
{ id: 303, name: "Q-Shop" },
|
||||||
|
{ id: 304, name: "Q-Fund" },
|
||||||
|
{ id: 305, name: "Ear-Bump" },
|
||||||
|
{ id: 306, name: "Q-Tube" },
|
||||||
|
{ id: 307, name: "Q-Share" },
|
||||||
|
{ id: 308, name: "Q-Support" },
|
||||||
|
{ id: 399, name: "Other" },
|
||||||
|
];
|
0
src/constants/Categories/3rdCategories.ts
Normal file
91
src/constants/Categories/CategoryFunctions.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import {
|
||||||
|
Category,
|
||||||
|
getCategoriesFromObject,
|
||||||
|
} from "../../components/common/CategoryList/CategoryList.tsx";
|
||||||
|
import { allCategoryData, iconCategories } from "./1stCategories.ts";
|
||||||
|
|
||||||
|
export const sortCategory = (a: Category, b: Category) => {
|
||||||
|
if (a.name === "Other") return 1;
|
||||||
|
else if (b.name === "Other") return -1;
|
||||||
|
else return a.name.localeCompare(b.name);
|
||||||
|
};
|
||||||
|
type Direction = "forward" | "backward";
|
||||||
|
const findCategory = (categoryID: number) => {
|
||||||
|
return allCategoryData.category.find(category => {
|
||||||
|
return category.id === categoryID;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const findSubCategory = (
|
||||||
|
categoryID: number,
|
||||||
|
direction: Direction = "forward"
|
||||||
|
) => {
|
||||||
|
const subCategoriesList = allCategoryData.subCategories;
|
||||||
|
if (direction === "backward") subCategoriesList.reverse();
|
||||||
|
|
||||||
|
for (const subCategories of subCategoriesList) {
|
||||||
|
for (const subCategoryID in subCategories) {
|
||||||
|
const returnValue = subCategories[subCategoryID].find(categoryObj => {
|
||||||
|
return categoryObj.id === categoryID;
|
||||||
|
});
|
||||||
|
if (returnValue) return returnValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export const findCategoryData = (
|
||||||
|
categoryID: number,
|
||||||
|
direction: Direction = "forward"
|
||||||
|
) => {
|
||||||
|
return direction === "forward"
|
||||||
|
? findCategory(categoryID) || findSubCategory(categoryID, "forward")
|
||||||
|
: findSubCategory(categoryID, "backward") || findCategory(categoryID);
|
||||||
|
};
|
||||||
|
export const findAllCategoryData = (
|
||||||
|
categories: string[],
|
||||||
|
direction: Direction = "forward"
|
||||||
|
) => {
|
||||||
|
let foundIcons: Category[] = [];
|
||||||
|
if (direction === "backward") categories.reverse();
|
||||||
|
|
||||||
|
categories.map(category => {
|
||||||
|
if (category) {
|
||||||
|
const icon = findCategoryData(+category, "backward");
|
||||||
|
if (icon) foundIcons.push(icon);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return foundIcons;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCategoriesWithIcons = (categories: Category[]) => {
|
||||||
|
return categories.filter(category => {
|
||||||
|
return category.icon;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAllCategoriesWithIcons = () => {
|
||||||
|
const categoriesWithIcons: Category[] = [];
|
||||||
|
|
||||||
|
allCategoryData.category.map(category => {
|
||||||
|
if (category.icon) categoriesWithIcons.push(category);
|
||||||
|
});
|
||||||
|
const subCategoriesList = allCategoryData.subCategories;
|
||||||
|
|
||||||
|
for (const subCategories of subCategoriesList) {
|
||||||
|
for (const subCategoryID in subCategories) {
|
||||||
|
const categoryWithIcon = subCategories[subCategoryID].map(categoryObj => {
|
||||||
|
if (categoryObj.icon) categoriesWithIcons.push(categoryObj);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return categoriesWithIcons;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getIconsFromObject = (fileObj: any) => {
|
||||||
|
const categories = getCategoriesFromObject(fileObj);
|
||||||
|
const icons = categories
|
||||||
|
.map(categoryID => {
|
||||||
|
return iconCategories.find(category => category.id === +categoryID)?.icon;
|
||||||
|
})
|
||||||
|
.reverse();
|
||||||
|
|
||||||
|
return icons.find(icon => icon !== undefined);
|
||||||
|
};
|
13
src/constants/Identifiers.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
const useTestIdentifiers = false;
|
||||||
|
|
||||||
|
export const QSUPPORT_FILE_BASE = useTestIdentifiers
|
||||||
|
? "MYTEST_support_issue_"
|
||||||
|
: "q_support_issue_";
|
||||||
|
|
||||||
|
export const QSUPPORT_PLAYLIST_BASE = useTestIdentifiers
|
||||||
|
? "MYTEST_support_playlist_"
|
||||||
|
: "q_support_playlist_";
|
||||||
|
|
||||||
|
export const QSUPPORT_COMMENT_BASE = useTestIdentifiers
|
||||||
|
? "qcomment_v1_MYTEST_support_"
|
||||||
|
: "qcomment_v1_q_support_";
|
5
src/constants/Misc.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const minPriceSuperlike = 10;
|
||||||
|
export const titleFormatter = /[^a-zA-Z0-9\s-_!?()&'",.;:|—~@#$%^*+=<>]/g;
|
||||||
|
export const titleFormatterOnSave = /[^a-zA-Z0-9\s-_!()&',.;—~@#$%^+=]/g;
|
||||||
|
|
||||||
|
export const log = false;
|
62
src/global.d.ts
vendored
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
// 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
|
||||||
|
encryptedData?: string
|
||||||
|
name?: string
|
||||||
|
mode?: string
|
||||||
|
query?: string
|
||||||
|
excludeBlocked?: boolean
|
||||||
|
exactMatchNames?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
}
|
421
src/hooks/useFetchFiles.tsx
Normal file
@ -0,0 +1,421 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import {
|
||||||
|
addFiles,
|
||||||
|
addToHashMap,
|
||||||
|
setCountNewFiles,
|
||||||
|
upsertFiles,
|
||||||
|
upsertFilesBeginning,
|
||||||
|
Video,
|
||||||
|
upsertFilteredFiles,
|
||||||
|
} from "../state/features/fileSlice.ts";
|
||||||
|
import {
|
||||||
|
setIsLoadingGlobal,
|
||||||
|
setUserAvatarHash,
|
||||||
|
setTotalFilesPublished,
|
||||||
|
setTotalNamesPublished,
|
||||||
|
setFilesPerNamePublished,
|
||||||
|
} from "../state/features/globalSlice";
|
||||||
|
import { RootState } from "../state/store";
|
||||||
|
import { fetchAndEvaluateVideos } from "../utils/fetchVideos";
|
||||||
|
import {
|
||||||
|
QSUPPORT_PLAYLIST_BASE,
|
||||||
|
QSUPPORT_FILE_BASE,
|
||||||
|
} from "../constants/Identifiers.ts";
|
||||||
|
import { RequestQueue } from "../utils/queue";
|
||||||
|
import { queue } from "../wrappers/GlobalWrapper";
|
||||||
|
import { getCategoriesFetchString } from "../components/common/CategoryList/CategoryList.tsx";
|
||||||
|
|
||||||
|
export const useFetchFiles = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const hashMapFiles = useSelector(
|
||||||
|
(state: RootState) => state.file.hashMapFiles
|
||||||
|
);
|
||||||
|
const videos = useSelector((state: RootState) => state.file.files);
|
||||||
|
const userAvatarHash = useSelector(
|
||||||
|
(state: RootState) => state.global.userAvatarHash
|
||||||
|
);
|
||||||
|
const filteredVideos = useSelector(
|
||||||
|
(state: RootState) => state.file.filteredFiles
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalFilesPublished = useSelector(
|
||||||
|
(state: RootState) => state.global.totalFilesPublished
|
||||||
|
);
|
||||||
|
const totalNamesPublished = useSelector(
|
||||||
|
(state: RootState) => state.global.totalNamesPublished
|
||||||
|
);
|
||||||
|
const filesPerNamePublished = useSelector(
|
||||||
|
(state: RootState) => state.global.filesPerNamePublished
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkAndUpdateFile = React.useCallback(
|
||||||
|
(video: Video) => {
|
||||||
|
const existingVideo = hashMapFiles[video.id];
|
||||||
|
if (!existingVideo) {
|
||||||
|
return true;
|
||||||
|
} else if (
|
||||||
|
video?.updated &&
|
||||||
|
existingVideo?.updated &&
|
||||||
|
(!existingVideo?.updated || video?.updated) > existingVideo?.updated
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[hashMapFiles]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getAvatar = React.useCallback(async (author: string) => {
|
||||||
|
try {
|
||||||
|
let url = await qortalRequest({
|
||||||
|
action: "GET_QDN_RESOURCE_URL",
|
||||||
|
name: author,
|
||||||
|
service: "THUMBNAIL",
|
||||||
|
identifier: "qortal_avatar",
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
setUserAvatarHash({
|
||||||
|
name: author,
|
||||||
|
url,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} catch (error) {}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getFile = async (
|
||||||
|
user: string,
|
||||||
|
videoId: string,
|
||||||
|
content: any,
|
||||||
|
retries: number = 0
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const res = await fetchAndEvaluateVideos({
|
||||||
|
user,
|
||||||
|
videoId,
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(addToHashMap(res));
|
||||||
|
} catch (error) {
|
||||||
|
retries = retries + 1;
|
||||||
|
if (retries < 2) {
|
||||||
|
// 3 is the maximum number of retries here, you can adjust it to your needs
|
||||||
|
queue.push(() => getFile(user, videoId, content, retries + 1));
|
||||||
|
} else {
|
||||||
|
console.error("Failed to get video after 3 attempts", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNewFiles = React.useCallback(async () => {
|
||||||
|
try {
|
||||||
|
dispatch(setIsLoadingGlobal(true));
|
||||||
|
|
||||||
|
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QSUPPORT_FILE_BASE}&limit=20&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const responseData = await response.json();
|
||||||
|
|
||||||
|
// const responseData = await qortalRequest({
|
||||||
|
// action: "SEARCH_QDN_RESOURCES",
|
||||||
|
// mode: "ALL",
|
||||||
|
// service: "DOCUMENT",
|
||||||
|
// query: "${QTUBE_VIDEO_BASE}",
|
||||||
|
// limit: 20,
|
||||||
|
// includeMetadata: true,
|
||||||
|
// reverse: true,
|
||||||
|
// excludeBlocked: true,
|
||||||
|
// exactMatchNames: true,
|
||||||
|
// name: names
|
||||||
|
// })
|
||||||
|
const latestVideo = videos[0];
|
||||||
|
if (!latestVideo) return;
|
||||||
|
const findVideo = responseData?.findIndex(
|
||||||
|
(item: any) => item?.identifier === latestVideo?.id
|
||||||
|
);
|
||||||
|
let fetchAll = responseData;
|
||||||
|
let willFetchAll = true;
|
||||||
|
if (findVideo !== -1) {
|
||||||
|
willFetchAll = false;
|
||||||
|
fetchAll = responseData.slice(0, findVideo);
|
||||||
|
}
|
||||||
|
|
||||||
|
const structureData = fetchAll.map((video: any): Video => {
|
||||||
|
return {
|
||||||
|
title: video?.metadata?.title,
|
||||||
|
category: video?.metadata?.category,
|
||||||
|
categoryName: video?.metadata?.categoryName,
|
||||||
|
tags: video?.metadata?.tags || [],
|
||||||
|
description: video?.metadata?.description,
|
||||||
|
created: video?.created,
|
||||||
|
updated: video?.updated,
|
||||||
|
user: video.name,
|
||||||
|
videoImage: "",
|
||||||
|
id: video.identifier,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (!willFetchAll) {
|
||||||
|
dispatch(upsertFilesBeginning(structureData));
|
||||||
|
}
|
||||||
|
if (willFetchAll) {
|
||||||
|
dispatch(addFiles(structureData));
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
dispatch(setCountNewFiles(0));
|
||||||
|
}, 1000);
|
||||||
|
for (const content of structureData) {
|
||||||
|
if (content.user && content.id) {
|
||||||
|
const res = checkAndUpdateFile(content);
|
||||||
|
if (res) {
|
||||||
|
queue.push(() => getFile(content.user, content.id, content));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
} finally {
|
||||||
|
dispatch(setIsLoadingGlobal(false));
|
||||||
|
}
|
||||||
|
}, [videos, hashMapFiles]);
|
||||||
|
|
||||||
|
const getFiles = React.useCallback(
|
||||||
|
async (
|
||||||
|
filters = {},
|
||||||
|
reset?: boolean,
|
||||||
|
resetFilers?: boolean,
|
||||||
|
limit?: number
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
name = "",
|
||||||
|
categories = [],
|
||||||
|
keywords = "",
|
||||||
|
type = "",
|
||||||
|
}: any = resetFilers ? {} : filters;
|
||||||
|
let offset = videos.length;
|
||||||
|
if (reset) {
|
||||||
|
offset = 0;
|
||||||
|
}
|
||||||
|
const videoLimit = limit || 50;
|
||||||
|
let defaultUrl = `/arbitrary/resources/search?mode=ALL&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}&limit=${videoLimit}`;
|
||||||
|
|
||||||
|
if (name) {
|
||||||
|
defaultUrl += `&name=${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categories.length > 0) {
|
||||||
|
defaultUrl += "&description=" + getCategoriesFetchString(categories);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keywords) {
|
||||||
|
defaultUrl = defaultUrl + `&query=${keywords}`;
|
||||||
|
}
|
||||||
|
if (type === "playlists") {
|
||||||
|
defaultUrl = defaultUrl + `&service=PLAYLIST`;
|
||||||
|
defaultUrl = defaultUrl + `&identifier=${QSUPPORT_PLAYLIST_BASE}`;
|
||||||
|
} else {
|
||||||
|
defaultUrl = defaultUrl + `&service=DOCUMENT`;
|
||||||
|
defaultUrl = defaultUrl + `&identifier=${QSUPPORT_FILE_BASE}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_BASE}&limit=${videoLimit}&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}`
|
||||||
|
const url = defaultUrl;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const responseData = await response.json();
|
||||||
|
|
||||||
|
// const responseData = await qortalRequest({
|
||||||
|
// action: "SEARCH_QDN_RESOURCES",
|
||||||
|
// mode: "ALL",
|
||||||
|
// service: "DOCUMENT",
|
||||||
|
// query: "${QTUBE_VIDEO_BASE}",
|
||||||
|
// limit: 20,
|
||||||
|
// includeMetadata: true,
|
||||||
|
// offset: offset,
|
||||||
|
// reverse: true,
|
||||||
|
// excludeBlocked: true,
|
||||||
|
// exactMatchNames: true,
|
||||||
|
// name: names
|
||||||
|
// })
|
||||||
|
const structureData = responseData.map((video: any): Video => {
|
||||||
|
return {
|
||||||
|
title: video?.metadata?.title,
|
||||||
|
service: video?.service,
|
||||||
|
category: video?.metadata?.category,
|
||||||
|
categoryName: video?.metadata?.categoryName,
|
||||||
|
tags: video?.metadata?.tags || [],
|
||||||
|
description: video?.metadata?.description,
|
||||||
|
created: video?.created,
|
||||||
|
updated: video?.updated,
|
||||||
|
user: video.name,
|
||||||
|
videoImage: "",
|
||||||
|
id: video.identifier,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if (reset) {
|
||||||
|
dispatch(addFiles(structureData));
|
||||||
|
} else {
|
||||||
|
dispatch(upsertFiles(structureData));
|
||||||
|
}
|
||||||
|
for (const content of structureData) {
|
||||||
|
if (content.user && content.id) {
|
||||||
|
const res = checkAndUpdateFile(content);
|
||||||
|
if (res) {
|
||||||
|
queue.push(() => getFile(content.user, content.id, content));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log({ error });
|
||||||
|
} finally {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[videos, hashMapFiles]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getFilesFiltered = React.useCallback(
|
||||||
|
async (filterValue: string) => {
|
||||||
|
try {
|
||||||
|
const offset = filteredVideos.length;
|
||||||
|
const replaceSpacesWithUnderscore = filterValue.replace(/ /g, "_");
|
||||||
|
|
||||||
|
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${replaceSpacesWithUnderscore}&identifier=${QSUPPORT_FILE_BASE}&limit=10&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const responseData = await response.json();
|
||||||
|
|
||||||
|
// const responseData = await qortalRequest({
|
||||||
|
// action: "SEARCH_QDN_RESOURCES",
|
||||||
|
// mode: "ALL",
|
||||||
|
// service: "DOCUMENT",
|
||||||
|
// query: replaceSpacesWithUnderscore,
|
||||||
|
// identifier: "${QTUBE_VIDEO_BASE}",
|
||||||
|
// limit: 20,
|
||||||
|
// includeMetadata: true,
|
||||||
|
// offset: offset,
|
||||||
|
// reverse: true,
|
||||||
|
// excludeBlocked: true,
|
||||||
|
// exactMatchNames: true,
|
||||||
|
// name: names
|
||||||
|
// })
|
||||||
|
const structureData = responseData.map((video: any): Video => {
|
||||||
|
return {
|
||||||
|
title: video?.metadata?.title,
|
||||||
|
category: video?.metadata?.category,
|
||||||
|
categoryName: video?.metadata?.categoryName,
|
||||||
|
tags: video?.metadata?.tags || [],
|
||||||
|
description: video?.metadata?.description,
|
||||||
|
created: video?.created,
|
||||||
|
updated: video?.updated,
|
||||||
|
user: video.name,
|
||||||
|
videoImage: "",
|
||||||
|
id: video.identifier,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
dispatch(upsertFilteredFiles(structureData));
|
||||||
|
|
||||||
|
for (const content of structureData) {
|
||||||
|
if (content.user && content.id) {
|
||||||
|
const res = checkAndUpdateFile(content);
|
||||||
|
if (res) {
|
||||||
|
queue.push(() => getFile(content.user, content.id, content));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
} finally {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[filteredVideos, hashMapFiles]
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkNewFiles = React.useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QSUPPORT_FILE_BASE}&limit=20&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const responseData = await response.json();
|
||||||
|
// const responseData = await qortalRequest({
|
||||||
|
// action: "SEARCH_QDN_RESOURCES",
|
||||||
|
// mode: "ALL",
|
||||||
|
// service: "DOCUMENT",
|
||||||
|
// query: "${QTUBE_VIDEO_BASE}",
|
||||||
|
// limit: 20,
|
||||||
|
// includeMetadata: true,
|
||||||
|
// reverse: true,
|
||||||
|
// excludeBlocked: true,
|
||||||
|
// exactMatchNames: true,
|
||||||
|
// name: names
|
||||||
|
// })
|
||||||
|
const latestVideo = videos[0];
|
||||||
|
if (!latestVideo) return;
|
||||||
|
const findVideo = responseData?.findIndex(
|
||||||
|
(item: any) => item?.identifier === latestVideo?.id
|
||||||
|
);
|
||||||
|
if (findVideo === -1) {
|
||||||
|
dispatch(setCountNewFiles(responseData.length));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newArray = responseData.slice(0, findVideo);
|
||||||
|
dispatch(setCountNewFiles(newArray.length));
|
||||||
|
return;
|
||||||
|
} catch (error) {}
|
||||||
|
}, [videos]);
|
||||||
|
|
||||||
|
const getFilesCount = React.useCallback(async () => {
|
||||||
|
try {
|
||||||
|
let url = `/arbitrary/resources/search?mode=ALL&includemetadata=false&limit=0&service=DOCUMENT&identifier=${QSUPPORT_FILE_BASE}`;
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const responseData = await response.json();
|
||||||
|
|
||||||
|
const totalFilesPublished = responseData.length;
|
||||||
|
const uniqueNames = new Set(responseData.map(video => video.name));
|
||||||
|
const totalNamesPublished = uniqueNames.size;
|
||||||
|
const filesPerNamePublished = (
|
||||||
|
totalFilesPublished / totalNamesPublished
|
||||||
|
).toFixed(2);
|
||||||
|
|
||||||
|
dispatch(setTotalFilesPublished(totalFilesPublished));
|
||||||
|
dispatch(setTotalNamesPublished(totalNamesPublished));
|
||||||
|
dispatch(setFilesPerNamePublished(filesPerNamePublished));
|
||||||
|
} catch (error) {
|
||||||
|
console.log({ error });
|
||||||
|
} finally {
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
getFiles,
|
||||||
|
checkAndUpdateFile,
|
||||||
|
getFile,
|
||||||
|
hashMapFiles,
|
||||||
|
getNewFiles,
|
||||||
|
checkNewFiles,
|
||||||
|
getFilesFiltered,
|
||||||
|
getFilesCount,
|
||||||
|
};
|
||||||
|
};
|
25
src/hooks/useWindowSize.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
export function useWindowSize() {
|
||||||
|
const [windowSize, setWindowSize] = useState<any>({
|
||||||
|
width: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleResize() {
|
||||||
|
setWindowSize({
|
||||||
|
width: window.innerWidth,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("resize", handleResize);
|
||||||
|
|
||||||
|
// Call handler right away so state gets updated with initial window size
|
||||||
|
handleResize();
|
||||||
|
|
||||||
|
// Remove event listener on cleanup
|
||||||
|
return () => window.removeEventListener("resize", handleResize);
|
||||||
|
}, []); // Empty array means that effect doesn't depend on any values from props or state, so it runs once when the component mounts, and never re-runs.
|
||||||
|
|
||||||
|
return windowSize;
|
||||||
|
}
|
229
src/index.css
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'Cambon Light';
|
||||||
|
src: url("./styles/fonts/Cambon-Light.ttf") format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Merriweather Sans';
|
||||||
|
src: url("./styles/fonts/Merriweather Sans.ttf") format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Karla';
|
||||||
|
src: url("./styles/fonts/Karla.ttf") format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Proxima Nova';
|
||||||
|
src: url("./styles/fonts/ProximaNova.otf") format("opentype");
|
||||||
|
}
|
||||||
|
|
||||||
|
@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: .75;
|
||||||
|
transition: .2s all;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-image {
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.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 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.download-icon {
|
||||||
|
transition: all 0.5s ease-in-out;
|
||||||
|
animation: downloadIconAnimation 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes downloadIconAnimation {
|
||||||
|
0% { transform: scale(1); fill: #fff; }
|
||||||
|
50% { transform: scale(1.2); fill: #3498db; }
|
||||||
|
100% { transform: scale(1); fill: #fff; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.closePlayer {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
width: 100%;
|
||||||
|
transition: all 0.3s;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
z-index: 8000;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* When the screen is 600px or less, display .myClassUnder600 and hide .myClassOver600 */
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
.myClassUnder600 {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 601px) {
|
||||||
|
.myClassOver600 {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ql-editor {
|
||||||
|
min-height: 100px;
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
.ql-editor img {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ql-container {
|
||||||
|
font-size: 16px
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-click {
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
.hover-click:hover {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
17
src/main.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App'
|
||||||
|
import './index.css'
|
||||||
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
interface CustomWindow extends Window {
|
||||||
|
_qdnBase: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const customWindow = window as unknown as CustomWindow
|
||||||
|
|
||||||
|
const baseUrl = customWindow?._qdnBase || ''
|
||||||
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
|
<BrowserRouter basename={baseUrl}>
|
||||||
|
<App />
|
||||||
|
<div id="modal-root" />
|
||||||
|
</BrowserRouter>
|
||||||
|
)
|
79
src/pages/Home/Channels.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { RootState } from "../../state/store";
|
||||||
|
import { Avatar, Box, Button, Typography, useTheme } from "@mui/material";
|
||||||
|
import { useFetchFiles } from "../../hooks/useFetchFiles.tsx";
|
||||||
|
import LazyLoad from "../../components/common/LazyLoad";
|
||||||
|
import {
|
||||||
|
BottomParent,
|
||||||
|
NameContainer,
|
||||||
|
VideoCard,
|
||||||
|
VideoCardName,
|
||||||
|
VideoCardTitle,
|
||||||
|
FileContainer,
|
||||||
|
VideoUploadDate,
|
||||||
|
} from "./FileList-styles.tsx";
|
||||||
|
import ResponsiveImage from "../../components/ResponsiveImage";
|
||||||
|
import { formatDate, formatTimestampSeconds } from "../../utils/time";
|
||||||
|
import { ChannelCard, ChannelTitle } from "./Home-styles";
|
||||||
|
|
||||||
|
interface VideoListProps {
|
||||||
|
mode?: string;
|
||||||
|
}
|
||||||
|
export const Channels = ({ mode }: VideoListProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const publishNames = useSelector(
|
||||||
|
(state: RootState) => state.global.publishNames
|
||||||
|
);
|
||||||
|
const userAvatarHash = useSelector(
|
||||||
|
(state: RootState) => state.global.userAvatarHash
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
minHeight: "50vh",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FileContainer>
|
||||||
|
{publishNames &&
|
||||||
|
publishNames?.slice(0, 10).map(name => {
|
||||||
|
let avatarUrl = "";
|
||||||
|
if (userAvatarHash[name]) {
|
||||||
|
avatarUrl = userAvatarHash[name];
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flex: 0,
|
||||||
|
alignItems: "center",
|
||||||
|
width: "auto",
|
||||||
|
position: "relative",
|
||||||
|
" @media (max-width: 450px)": {
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
key={name}
|
||||||
|
>
|
||||||
|
<ChannelCard
|
||||||
|
onClick={() => {
|
||||||
|
navigate(`/channel/${name}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ChannelTitle>{name}</ChannelTitle>
|
||||||
|
<ResponsiveImage src={avatarUrl} width={50} height={50} />
|
||||||
|
</ChannelCard>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</FileContainer>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
290
src/pages/Home/FileList-styles.tsx
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
import { styled } from "@mui/system";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Grid,
|
||||||
|
Typography,
|
||||||
|
Checkbox,
|
||||||
|
TextField,
|
||||||
|
InputLabel,
|
||||||
|
Autocomplete,
|
||||||
|
} from "@mui/material";
|
||||||
|
|
||||||
|
export const FileContainer = styled(Box)(({ theme }) => ({
|
||||||
|
position: "relative",
|
||||||
|
display: "flex",
|
||||||
|
padding: "15px",
|
||||||
|
flexDirection: "row",
|
||||||
|
gap: "20px",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
width: "100%",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StoresRow = styled(Grid)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
gap: "15px",
|
||||||
|
width: "auto",
|
||||||
|
position: "relative",
|
||||||
|
"@media (max-width: 450px)": {
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const VideoCard = styled(Grid)(({ theme }) => ({
|
||||||
|
position: "relative",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
height: "320px",
|
||||||
|
width: "300px",
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
borderRadius: "8px",
|
||||||
|
padding: "10px 15px",
|
||||||
|
gap: "20px",
|
||||||
|
cursor: "pointer",
|
||||||
|
border:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "none"
|
||||||
|
: `1px solid ${theme.palette.primary.light}`,
|
||||||
|
boxShadow:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)"
|
||||||
|
: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px",
|
||||||
|
transition: "all 0.3s ease-in-out",
|
||||||
|
"&:hover": {
|
||||||
|
boxShadow:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "0px 8px 10px 1px hsla(0,0%,0%,0.14), 0px 3px 14px 2px hsla(0,0%,0%,0.12), 0px 5px 5px -3px hsla(0,0%,0%,0.2)"
|
||||||
|
: "rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StoreCardInfo = styled(Grid)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "10px",
|
||||||
|
padding: "5px",
|
||||||
|
marginTop: "15px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const VideoImageContainer = styled(Grid)(({ theme }) => ({}));
|
||||||
|
|
||||||
|
export const VideoCardImage = styled("img")(({ theme }) => ({
|
||||||
|
maxWidth: "300px",
|
||||||
|
minWidth: "150px",
|
||||||
|
borderRadius: "5px",
|
||||||
|
height: "150px",
|
||||||
|
objectFit: "fill",
|
||||||
|
width: "266px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
const DoubleLine = styled(Typography)`
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
overflow: hidden;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const VideoCardTitle = styled(DoubleLine)(({ theme }) => ({
|
||||||
|
fontFamily: "Cairo",
|
||||||
|
fontSize: "16px",
|
||||||
|
letterSpacing: "0.4px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
}));
|
||||||
|
export const VideoCardName = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Cairo",
|
||||||
|
fontSize: "14px",
|
||||||
|
letterSpacing: "0.4px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
overflow: "hidden",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
width: "100%",
|
||||||
|
}));
|
||||||
|
export const VideoUploadDate = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Cairo",
|
||||||
|
fontSize: "12px",
|
||||||
|
letterSpacing: "0.4px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
}));
|
||||||
|
export const BottomParent = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
flexDirection: "column",
|
||||||
|
}));
|
||||||
|
export const VideoCardDescription = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Karla",
|
||||||
|
fontSize: "20px",
|
||||||
|
letterSpacing: "0px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StoreCardOwner = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Livvic",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
fontSize: "17px",
|
||||||
|
position: "absolute",
|
||||||
|
bottom: "5px",
|
||||||
|
right: "10px",
|
||||||
|
userSelect: "none",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const StoreCardYouOwn = styled(Box)(({ theme }) => ({
|
||||||
|
position: "absolute",
|
||||||
|
top: "5px",
|
||||||
|
right: "10px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "5px",
|
||||||
|
fontFamily: "Livvic",
|
||||||
|
fontSize: "15px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const MyStoresRow = styled(Grid)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
padding: "5px",
|
||||||
|
width: "100%",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const NameContainer = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
marginBottom: "10px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const MyStoresCard = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
width: "auto",
|
||||||
|
borderRadius: "4px",
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
padding: "5px 10px",
|
||||||
|
fontFamily: "Raleway",
|
||||||
|
fontSize: "18px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const MyStoresCheckbox = styled(Checkbox)(({ theme }) => ({
|
||||||
|
color: "#c0d4ff",
|
||||||
|
"&.Mui-checked": {
|
||||||
|
color: "#6596ff",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const FiltersCol = styled(Grid)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
height: "100%",
|
||||||
|
padding: "20px 15px",
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
borderTop: `1px solid ${theme.palette.background.paper}`,
|
||||||
|
borderRight: `1px solid ${theme.palette.background.paper}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const FiltersContainer = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const FiltersRow = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
width: "100%",
|
||||||
|
padding: "0 15px",
|
||||||
|
fontSize: "16px",
|
||||||
|
userSelect: "none",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const FiltersTitle = styled(Typography)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "5px",
|
||||||
|
margin: "20px 0",
|
||||||
|
fontFamily: "Raleway",
|
||||||
|
fontSize: "17px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const FiltersCheckbox = styled(Checkbox)(({ theme }) => ({
|
||||||
|
color: "#c0d4ff",
|
||||||
|
"&.Mui-checked": {
|
||||||
|
color: "#6596ff",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const FilterSelect = styled(Autocomplete)(({ theme }) => ({
|
||||||
|
"& #categories-select": {
|
||||||
|
padding: "7px",
|
||||||
|
},
|
||||||
|
"& .MuiSelect-placeholder": {
|
||||||
|
fontFamily: "Raleway",
|
||||||
|
fontSize: "17px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
},
|
||||||
|
"& MuiFormLabel-root": {
|
||||||
|
fontFamily: "Raleway",
|
||||||
|
fontSize: "17px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const FilterSelectMenuItems = styled(TextField)(({ theme }) => ({
|
||||||
|
fontFamily: "Raleway",
|
||||||
|
fontSize: "17px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const FiltersSubContainer = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "5px",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const FilterDropdownLabel = styled(InputLabel)(({ theme }) => ({
|
||||||
|
fontFamily: "Raleway",
|
||||||
|
fontSize: "16px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const IconsBox = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
gap: "3px",
|
||||||
|
position: "absolute",
|
||||||
|
top: "-20px",
|
||||||
|
right: "-5px",
|
||||||
|
transition: "all 0.3s ease-in-out",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BlockIconContainer = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
boxShadow: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;",
|
||||||
|
backgroundColor: "#fbfbfb",
|
||||||
|
color: "#c25252",
|
||||||
|
padding: "2px",
|
||||||
|
borderRadius: "3px",
|
||||||
|
transition: "all 0.3s ease-in-out",
|
||||||
|
"&:hover": {
|
||||||
|
cursor: "pointer",
|
||||||
|
transform: "scale(1.1)",
|
||||||
|
},
|
||||||
|
});
|
207
src/pages/Home/FileList.tsx
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
import { Avatar, Box, Skeleton, Tooltip } from "@mui/material";
|
||||||
|
import {
|
||||||
|
BlockIconContainer,
|
||||||
|
BottomParent,
|
||||||
|
FileContainer,
|
||||||
|
IconsBox,
|
||||||
|
NameContainer,
|
||||||
|
VideoCard,
|
||||||
|
VideoCardName,
|
||||||
|
VideoCardTitle,
|
||||||
|
VideoUploadDate,
|
||||||
|
} from "./FileList-styles.tsx";
|
||||||
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
|
import {
|
||||||
|
blockUser,
|
||||||
|
setEditFile,
|
||||||
|
Video,
|
||||||
|
} from "../../state/features/fileSlice.ts";
|
||||||
|
import BlockIcon from "@mui/icons-material/Block";
|
||||||
|
import AttachFileIcon from "@mui/icons-material/AttachFile";
|
||||||
|
import { formatBytes } from "../IssueContent/IssueContent.tsx";
|
||||||
|
import { formatDate } from "../../utils/time.ts";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { RootState } from "../../state/store.ts";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { getIconsFromObject } from "../../constants/Categories/CategoryFunctions.ts";
|
||||||
|
|
||||||
|
interface FileListProps {
|
||||||
|
files: Video[];
|
||||||
|
}
|
||||||
|
export const FileList = ({ files }: FileListProps) => {
|
||||||
|
const hashMapFiles = useSelector(
|
||||||
|
(state: RootState) => state.file.hashMapFiles
|
||||||
|
);
|
||||||
|
|
||||||
|
const [showIcons, setShowIcons] = useState(null);
|
||||||
|
const username = useSelector((state: RootState) => state.auth?.user?.name);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const blockUserFunc = async (user: string) => {
|
||||||
|
if (user === "Q-Share") return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await qortalRequest({
|
||||||
|
action: "ADD_LIST_ITEMS",
|
||||||
|
list_name: "blockedNames",
|
||||||
|
items: [user],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response === true) {
|
||||||
|
dispatch(blockUser(user));
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FileContainer>
|
||||||
|
{files.map((file: any, index: number) => {
|
||||||
|
const existingFile = hashMapFiles[file?.id];
|
||||||
|
let hasHash = false;
|
||||||
|
let fileObj = file;
|
||||||
|
if (existingFile) {
|
||||||
|
fileObj = existingFile;
|
||||||
|
hasHash = true;
|
||||||
|
}
|
||||||
|
const icon = getIconsFromObject(fileObj);
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
width: "100%",
|
||||||
|
height: "75px",
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
key={fileObj.id}
|
||||||
|
onMouseEnter={() => setShowIcons(fileObj.id)}
|
||||||
|
onMouseLeave={() => setShowIcons(null)}
|
||||||
|
>
|
||||||
|
{hasHash ? (
|
||||||
|
<>
|
||||||
|
<IconsBox
|
||||||
|
sx={{
|
||||||
|
opacity: showIcons === fileObj.id ? 1 : 0,
|
||||||
|
zIndex: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{fileObj?.user === username && (
|
||||||
|
<Tooltip title="Edit Issue Properties" placement="top">
|
||||||
|
<BlockIconContainer>
|
||||||
|
<EditIcon
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(setEditFile(fileObj));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</BlockIconContainer>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Tooltip title="Block user content" placement="top">
|
||||||
|
<BlockIconContainer>
|
||||||
|
<BlockIcon
|
||||||
|
onClick={() => {
|
||||||
|
blockUserFunc(fileObj?.user);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</BlockIconContainer>
|
||||||
|
</Tooltip>
|
||||||
|
</IconsBox>
|
||||||
|
<VideoCard
|
||||||
|
onClick={() => {
|
||||||
|
navigate(`/share/${fileObj?.user}/${fileObj?.id}`);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
gap: "25px",
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "25px",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon ? (
|
||||||
|
<img
|
||||||
|
src={icon}
|
||||||
|
width="50px"
|
||||||
|
style={{
|
||||||
|
borderRadius: "5px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<AttachFileIcon />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<VideoCardTitle
|
||||||
|
sx={{
|
||||||
|
width: "100px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatBytes(
|
||||||
|
fileObj?.files.reduce(
|
||||||
|
(acc, cur) => acc + (cur?.size || 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</VideoCardTitle>
|
||||||
|
<VideoCardTitle>{fileObj.title}</VideoCardTitle>
|
||||||
|
</Box>
|
||||||
|
<BottomParent>
|
||||||
|
<NameContainer
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
navigate(`/channel/${fileObj?.user}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
sx={{ height: 24, width: 24 }}
|
||||||
|
src={`/arbitrary/THUMBNAIL/${fileObj?.user}/qortal_avatar`}
|
||||||
|
alt={`${fileObj?.user}'s avatar`}
|
||||||
|
/>
|
||||||
|
<VideoCardName
|
||||||
|
sx={{
|
||||||
|
":hover": {
|
||||||
|
textDecoration: "underline",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{fileObj?.user}
|
||||||
|
</VideoCardName>
|
||||||
|
</NameContainer>
|
||||||
|
|
||||||
|
{fileObj?.created && (
|
||||||
|
<VideoUploadDate>
|
||||||
|
{formatDate(fileObj.created)}
|
||||||
|
</VideoUploadDate>
|
||||||
|
)}
|
||||||
|
</BottomParent>
|
||||||
|
</VideoCard>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Skeleton
|
||||||
|
variant="rectangular"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
paddingBottom: "10px",
|
||||||
|
objectFit: "contain",
|
||||||
|
visibility: "visible",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</FileContainer>
|
||||||
|
);
|
||||||
|
};
|
246
src/pages/Home/FileListComponentLevel.tsx
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { RootState } from "../../state/store";
|
||||||
|
import AttachFileIcon from "@mui/icons-material/AttachFile";
|
||||||
|
|
||||||
|
import { Avatar, Box, Skeleton, useTheme } from "@mui/material";
|
||||||
|
import { useFetchFiles } from "../../hooks/useFetchFiles.tsx";
|
||||||
|
import LazyLoad from "../../components/common/LazyLoad";
|
||||||
|
import {
|
||||||
|
BottomParent,
|
||||||
|
FileContainer,
|
||||||
|
NameContainer,
|
||||||
|
VideoCard,
|
||||||
|
VideoCardName,
|
||||||
|
VideoCardTitle,
|
||||||
|
VideoUploadDate,
|
||||||
|
} from "./FileList-styles.tsx";
|
||||||
|
import { formatDate } from "../../utils/time";
|
||||||
|
import { Video } from "../../state/features/fileSlice.ts";
|
||||||
|
import { queue } from "../../wrappers/GlobalWrapper";
|
||||||
|
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
|
||||||
|
import { formatBytes } from "../IssueContent/IssueContent.tsx";
|
||||||
|
import { getIconsFromObject } from "../../constants/Categories/CategoryFunctions.ts";
|
||||||
|
|
||||||
|
interface VideoListProps {
|
||||||
|
mode?: string;
|
||||||
|
}
|
||||||
|
export const FileListComponentLevel = ({ mode }: VideoListProps) => {
|
||||||
|
const { name: paramName } = useParams();
|
||||||
|
const theme = useTheme();
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
|
const firstFetch = useRef(false);
|
||||||
|
const afterFetch = useRef(false);
|
||||||
|
const hashMapVideos = useSelector(
|
||||||
|
(state: RootState) => state.file.hashMapFiles
|
||||||
|
);
|
||||||
|
|
||||||
|
const [videos, setVideos] = React.useState<Video[]>([]);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { getFile, getNewFiles, checkNewFiles, checkAndUpdateFile } =
|
||||||
|
useFetchFiles();
|
||||||
|
|
||||||
|
const getVideos = React.useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const offset = videos.length;
|
||||||
|
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QSUPPORT_FILE_BASE}_&limit=50&includemetadata=false&reverse=true&excludeblocked=true&name=${paramName}&exactmatchnames=true&offset=${offset}`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const responseData = await response.json();
|
||||||
|
|
||||||
|
const structureData = responseData.map((video: any): Video => {
|
||||||
|
return {
|
||||||
|
title: video?.metadata?.title,
|
||||||
|
category: video?.metadata?.category,
|
||||||
|
categoryName: video?.metadata?.categoryName,
|
||||||
|
tags: video?.metadata?.tags || [],
|
||||||
|
description: video?.metadata?.description,
|
||||||
|
created: video?.created,
|
||||||
|
updated: video?.updated,
|
||||||
|
user: video.name,
|
||||||
|
videoImage: "",
|
||||||
|
id: video.identifier,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const copiedVideos: Video[] = [...videos];
|
||||||
|
structureData.forEach((video: Video) => {
|
||||||
|
const index = videos.findIndex(p => p.id === video.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
copiedVideos[index] = video;
|
||||||
|
} else {
|
||||||
|
copiedVideos.push(video);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setVideos(copiedVideos);
|
||||||
|
|
||||||
|
for (const content of structureData) {
|
||||||
|
if (content.user && content.id) {
|
||||||
|
const res = checkAndUpdateFile(content);
|
||||||
|
if (res) {
|
||||||
|
queue.push(() => getFile(content.user, content.id, content));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
} finally {
|
||||||
|
}
|
||||||
|
}, [videos, hashMapVideos]);
|
||||||
|
|
||||||
|
const getVideosHandler = React.useCallback(async () => {
|
||||||
|
if (!firstFetch.current || !afterFetch.current) return;
|
||||||
|
await getVideos();
|
||||||
|
}, [getVideos]);
|
||||||
|
|
||||||
|
const getVideosHandlerMount = React.useCallback(async () => {
|
||||||
|
if (firstFetch.current) return;
|
||||||
|
firstFetch.current = true;
|
||||||
|
await getVideos();
|
||||||
|
afterFetch.current = true;
|
||||||
|
setIsLoading(false);
|
||||||
|
}, [getVideos]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!firstFetch.current) {
|
||||||
|
getVideosHandlerMount();
|
||||||
|
}
|
||||||
|
}, [getVideosHandlerMount]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FileContainer>
|
||||||
|
{videos.map((file: any, index: number) => {
|
||||||
|
const existingFile = hashMapVideos[file?.id];
|
||||||
|
let hasHash = false;
|
||||||
|
let fileObj = file;
|
||||||
|
if (existingFile) {
|
||||||
|
fileObj = existingFile;
|
||||||
|
hasHash = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const icon = getIconsFromObject(fileObj);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
width: "100%",
|
||||||
|
height: "75px",
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
key={fileObj.id}
|
||||||
|
>
|
||||||
|
{hasHash ? (
|
||||||
|
<>
|
||||||
|
<VideoCard
|
||||||
|
onClick={() => {
|
||||||
|
navigate(`/share/${fileObj?.user}/${fileObj?.id}`);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
gap: "25px",
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "25px",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon ? (
|
||||||
|
<img
|
||||||
|
src={icon}
|
||||||
|
width="50px"
|
||||||
|
style={{
|
||||||
|
borderRadius: "5px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<AttachFileIcon />
|
||||||
|
)}
|
||||||
|
<VideoCardTitle
|
||||||
|
sx={{
|
||||||
|
width: "100px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatBytes(
|
||||||
|
fileObj?.files.reduce(
|
||||||
|
(acc, cur) => acc + (cur?.size || 0),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</VideoCardTitle>
|
||||||
|
<VideoCardTitle>{fileObj.title}</VideoCardTitle>
|
||||||
|
</Box>
|
||||||
|
<BottomParent>
|
||||||
|
<NameContainer
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
navigate(`/channel/${fileObj?.user}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
sx={{ height: 24, width: 24 }}
|
||||||
|
src={`/arbitrary/THUMBNAIL/${fileObj?.user}/qortal_avatar`}
|
||||||
|
alt={`${fileObj?.user}'s avatar`}
|
||||||
|
/>
|
||||||
|
<VideoCardName
|
||||||
|
sx={{
|
||||||
|
":hover": {
|
||||||
|
textDecoration: "underline",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{fileObj?.user}
|
||||||
|
</VideoCardName>
|
||||||
|
</NameContainer>
|
||||||
|
|
||||||
|
{fileObj?.created && (
|
||||||
|
<VideoUploadDate>
|
||||||
|
{formatDate(fileObj.created)}
|
||||||
|
</VideoUploadDate>
|
||||||
|
)}
|
||||||
|
</BottomParent>
|
||||||
|
</VideoCard>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Skeleton
|
||||||
|
variant="rectangular"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
paddingBottom: "10px",
|
||||||
|
objectFit: "contain",
|
||||||
|
visibility: "visible",
|
||||||
|
borderRadius: "8px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</FileContainer>
|
||||||
|
<LazyLoad onLoadMore={getVideosHandler} isLoading={isLoading}></LazyLoad>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
87
src/pages/Home/Home-styles.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { styled } from "@mui/system";
|
||||||
|
import { Box, Grid, Typography, Checkbox } from "@mui/material";
|
||||||
|
|
||||||
|
|
||||||
|
export const SubtitleContainer = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: 'center',
|
||||||
|
margin: '10px 0px',
|
||||||
|
width: '100%'
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const Subtitle = styled(Typography)(({ theme }) => ({
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: '20px'
|
||||||
|
}));
|
||||||
|
|
||||||
|
const DoubleLine = styled(Typography)`
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
overflow: hidden;
|
||||||
|
`
|
||||||
|
export const ChannelTitle = styled(DoubleLine)(({ theme }) => ({
|
||||||
|
fontFamily: "Cairo",
|
||||||
|
fontSize: "20px",
|
||||||
|
letterSpacing: "0.4px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
marginBottom: 'auto',
|
||||||
|
textAlign: 'center'
|
||||||
|
}));
|
||||||
|
export const WelcomeTitle = styled(DoubleLine)(({ theme }) => ({
|
||||||
|
fontFamily: "Cairo",
|
||||||
|
fontSize: "24px",
|
||||||
|
letterSpacing: "0.4px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
textAlign: 'center'
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const WelcomeContainer = styled(Box)(({ theme }) => ({
|
||||||
|
position: 'fixed',
|
||||||
|
width: '90%',
|
||||||
|
height: '90%',
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
left: '50%',
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
zIndex: 500,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
export const ChannelCard = styled(Grid)(({ theme }) => ({
|
||||||
|
position: "relative",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: 'center',
|
||||||
|
height: "auto",
|
||||||
|
width: '300px',
|
||||||
|
minHeight: '130px',
|
||||||
|
backgroundColor: theme.palette.background.paper,
|
||||||
|
borderRadius: "8px",
|
||||||
|
padding: "10px 15px",
|
||||||
|
gap: "20px",
|
||||||
|
border:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "none"
|
||||||
|
: `1px solid ${theme.palette.primary.light}`,
|
||||||
|
boxShadow:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)"
|
||||||
|
: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px",
|
||||||
|
transition: "all 0.3s ease-in-out",
|
||||||
|
"&:hover": {
|
||||||
|
cursor: "pointer",
|
||||||
|
boxShadow:
|
||||||
|
theme.palette.mode === "dark"
|
||||||
|
? "0px 8px 10px 1px hsla(0,0%,0%,0.14), 0px 3px 14px 2px hsla(0,0%,0%,0.12), 0px 5px 5px -3px hsla(0,0%,0%,0.2)"
|
||||||
|
: "rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;"
|
||||||
|
}
|
||||||
|
}));
|
327
src/pages/Home/Home.tsx
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { RootState } from "../../state/store";
|
||||||
|
import { FileList } from "./FileList.tsx";
|
||||||
|
import { Box, Button, Grid, Input, useTheme } from "@mui/material";
|
||||||
|
import { useFetchFiles } from "../../hooks/useFetchFiles.tsx";
|
||||||
|
import LazyLoad from "../../components/common/LazyLoad";
|
||||||
|
import { FiltersCol, FiltersContainer } from "./FileList-styles.tsx";
|
||||||
|
import { SubtitleContainer } from "./Home-styles";
|
||||||
|
import {
|
||||||
|
changefilterName,
|
||||||
|
changefilterSearch,
|
||||||
|
changeFilterType,
|
||||||
|
} from "../../state/features/fileSlice.ts";
|
||||||
|
import { allCategoryData } from "../../constants/Categories/1stCategories.ts";
|
||||||
|
import {
|
||||||
|
CategoryList,
|
||||||
|
CategoryListRef,
|
||||||
|
} from "../../components/common/CategoryList/CategoryList.tsx";
|
||||||
|
import { StatsData } from "../../components/StatsData.tsx";
|
||||||
|
|
||||||
|
interface HomeProps {
|
||||||
|
mode?: string;
|
||||||
|
}
|
||||||
|
export const Home = ({ mode }: HomeProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const prevVal = useRef("");
|
||||||
|
const categoryListRef = useRef<CategoryListRef>(null);
|
||||||
|
const isFiltering = useSelector((state: RootState) => state.file.isFiltering);
|
||||||
|
const filterValue = useSelector((state: RootState) => state.file.filterValue);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
|
const filterType = useSelector((state: RootState) => state.file.filterType);
|
||||||
|
const totalFilesPublished = useSelector(
|
||||||
|
(state: RootState) => state.global.totalFilesPublished
|
||||||
|
);
|
||||||
|
const totalNamesPublished = useSelector(
|
||||||
|
(state: RootState) => state.global.totalNamesPublished
|
||||||
|
);
|
||||||
|
const filesPerNamePublished = useSelector(
|
||||||
|
(state: RootState) => state.global.filesPerNamePublished
|
||||||
|
);
|
||||||
|
const setFilterType = payload => {
|
||||||
|
dispatch(changeFilterType(payload));
|
||||||
|
};
|
||||||
|
const filterSearch = useSelector(
|
||||||
|
(state: RootState) => state.file.filterSearch
|
||||||
|
);
|
||||||
|
|
||||||
|
const setFilterSearch = payload => {
|
||||||
|
dispatch(changefilterSearch(payload));
|
||||||
|
};
|
||||||
|
const filterName = useSelector((state: RootState) => state.file.filterName);
|
||||||
|
|
||||||
|
const setFilterName = payload => {
|
||||||
|
dispatch(changefilterName(payload));
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFilterMode = useRef(false);
|
||||||
|
const firstFetch = useRef(false);
|
||||||
|
const afterFetch = useRef(false);
|
||||||
|
const isFetchingFiltered = useRef(false);
|
||||||
|
const isFetching = useRef(false);
|
||||||
|
|
||||||
|
const countNewFiles = useSelector(
|
||||||
|
(state: RootState) => state.file.countNewFiles
|
||||||
|
);
|
||||||
|
const userAvatarHash = useSelector(
|
||||||
|
(state: RootState) => state.global.userAvatarHash
|
||||||
|
);
|
||||||
|
|
||||||
|
const { files: globalVideos } = useSelector((state: RootState) => state.file);
|
||||||
|
|
||||||
|
const setSelectedCategoryFiles = payload => {};
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const filteredFiles = useSelector(
|
||||||
|
(state: RootState) => state.file.filteredFiles
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
getFiles,
|
||||||
|
checkAndUpdateFile,
|
||||||
|
getFile,
|
||||||
|
hashMapFiles,
|
||||||
|
getNewFiles,
|
||||||
|
checkNewFiles,
|
||||||
|
getFilesFiltered,
|
||||||
|
getFilesCount,
|
||||||
|
} = useFetchFiles();
|
||||||
|
|
||||||
|
const getFilesHandler = React.useCallback(
|
||||||
|
async (reset?: boolean, resetFilers?: boolean) => {
|
||||||
|
if (!firstFetch.current || !afterFetch.current) return;
|
||||||
|
if (isFetching.current) return;
|
||||||
|
isFetching.current = true;
|
||||||
|
const selectedCategories =
|
||||||
|
categoryListRef.current.getSelectedCategories() || [];
|
||||||
|
|
||||||
|
await getFiles(
|
||||||
|
{
|
||||||
|
name: filterName,
|
||||||
|
categories: selectedCategories,
|
||||||
|
keywords: filterSearch,
|
||||||
|
type: filterType,
|
||||||
|
},
|
||||||
|
reset,
|
||||||
|
resetFilers
|
||||||
|
);
|
||||||
|
isFetching.current = false;
|
||||||
|
},
|
||||||
|
[
|
||||||
|
getFiles,
|
||||||
|
filterValue,
|
||||||
|
getFilesFiltered,
|
||||||
|
isFiltering,
|
||||||
|
filterName,
|
||||||
|
filterSearch,
|
||||||
|
filterType,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchOnEnter = e => {
|
||||||
|
if (e.keyCode == 13) {
|
||||||
|
getFilesHandler(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isFiltering && filterValue !== prevVal?.current) {
|
||||||
|
prevVal.current = filterValue;
|
||||||
|
getFilesHandler();
|
||||||
|
}
|
||||||
|
}, [filterValue, isFiltering, filteredFiles, getFilesCount]);
|
||||||
|
|
||||||
|
const getFilesHandlerMount = React.useCallback(async () => {
|
||||||
|
if (firstFetch.current) return;
|
||||||
|
firstFetch.current = true;
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
await getFiles();
|
||||||
|
afterFetch.current = true;
|
||||||
|
isFetching.current = false;
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
}, [getFiles]);
|
||||||
|
|
||||||
|
let videos = globalVideos;
|
||||||
|
|
||||||
|
if (isFiltering) {
|
||||||
|
videos = filteredFiles;
|
||||||
|
isFilterMode.current = true;
|
||||||
|
} else {
|
||||||
|
isFilterMode.current = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// const interval = useRef<any>(null);
|
||||||
|
|
||||||
|
// const checkNewVideosFunc = useCallback(() => {
|
||||||
|
// let isCalling = false;
|
||||||
|
// interval.current = setInterval(async () => {
|
||||||
|
// if (isCalling || !firstFetch.current) return;
|
||||||
|
// isCalling = true;
|
||||||
|
// await checkNewVideos();
|
||||||
|
// isCalling = false;
|
||||||
|
// }, 30000); // 1 second interval
|
||||||
|
// }, [checkNewVideos]);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (isFiltering && interval.current) {
|
||||||
|
// clearInterval(interval.current);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// checkNewVideosFunc();
|
||||||
|
|
||||||
|
// return () => {
|
||||||
|
// if (interval?.current) {
|
||||||
|
// clearInterval(interval.current);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// }, [mode, checkNewVideosFunc, isFiltering]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
!firstFetch.current &&
|
||||||
|
!isFilterMode.current &&
|
||||||
|
globalVideos.length === 0
|
||||||
|
) {
|
||||||
|
isFetching.current = true;
|
||||||
|
getFilesHandlerMount();
|
||||||
|
} else {
|
||||||
|
firstFetch.current = true;
|
||||||
|
afterFetch.current = true;
|
||||||
|
}
|
||||||
|
}, [getFilesHandlerMount, globalVideos]);
|
||||||
|
|
||||||
|
const filtersToDefault = async () => {
|
||||||
|
setFilterType("videos");
|
||||||
|
setFilterSearch("");
|
||||||
|
setFilterName("");
|
||||||
|
categoryListRef.current?.clearCategories();
|
||||||
|
|
||||||
|
ReactDOM.flushSync(() => {
|
||||||
|
getFilesHandler(true, true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container sx={{ width: "100%" }}>
|
||||||
|
<FiltersCol item xs={12} md={2} sm={3}>
|
||||||
|
<FiltersContainer>
|
||||||
|
<StatsData />
|
||||||
|
<Input
|
||||||
|
id="standard-adornment-name"
|
||||||
|
onChange={e => {
|
||||||
|
setFilterSearch(e.target.value);
|
||||||
|
}}
|
||||||
|
onKeyDown={searchOnEnter}
|
||||||
|
value={filterSearch}
|
||||||
|
placeholder="Search"
|
||||||
|
sx={{
|
||||||
|
borderBottom: "1px solid white",
|
||||||
|
"&&:before": {
|
||||||
|
borderBottom: "none",
|
||||||
|
},
|
||||||
|
"&&:after": {
|
||||||
|
borderBottom: "none",
|
||||||
|
},
|
||||||
|
"&&:hover:before": {
|
||||||
|
borderBottom: "none",
|
||||||
|
},
|
||||||
|
"&&.Mui-focused:before": {
|
||||||
|
borderBottom: "none",
|
||||||
|
},
|
||||||
|
"&&.Mui-focused": {
|
||||||
|
outline: "none",
|
||||||
|
},
|
||||||
|
fontSize: "18px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
id="standard-adornment-name"
|
||||||
|
onChange={e => {
|
||||||
|
setFilterName(e.target.value);
|
||||||
|
}}
|
||||||
|
onKeyDown={searchOnEnter}
|
||||||
|
value={filterName}
|
||||||
|
placeholder="User's Name (Exact)"
|
||||||
|
sx={{
|
||||||
|
marginTop: "20px",
|
||||||
|
borderBottom: "1px solid white",
|
||||||
|
"&&:before": {
|
||||||
|
borderBottom: "none",
|
||||||
|
},
|
||||||
|
"&&:after": {
|
||||||
|
borderBottom: "none",
|
||||||
|
},
|
||||||
|
"&&:hover:before": {
|
||||||
|
borderBottom: "none",
|
||||||
|
},
|
||||||
|
"&&.Mui-focused:before": {
|
||||||
|
borderBottom: "none",
|
||||||
|
},
|
||||||
|
"&&.Mui-focused": {
|
||||||
|
outline: "none",
|
||||||
|
},
|
||||||
|
fontSize: "18px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CategoryList categoryData={allCategoryData} ref={categoryListRef} />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
filtersToDefault();
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
marginTop: "20px",
|
||||||
|
fontWeight: 1000,
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
reset
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
getFilesHandler(true);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
marginTop: "20px",
|
||||||
|
fontWeight: 1000,
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
|
</FiltersContainer>
|
||||||
|
</FiltersCol>
|
||||||
|
<Grid item xs={12} md={10} sm={9}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
marginTop: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SubtitleContainer
|
||||||
|
sx={{
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
paddingLeft: "15px",
|
||||||
|
width: "100%",
|
||||||
|
maxWidth: "1400px",
|
||||||
|
}}
|
||||||
|
></SubtitleContainer>
|
||||||
|
<FileList files={videos} />
|
||||||
|
<LazyLoad
|
||||||
|
onLoadMore={getFilesHandler}
|
||||||
|
isLoading={isLoading}
|
||||||
|
></LazyLoad>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
68
src/pages/IndividualProfile/IndividualProfile.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { FileListComponentLevel } from "../Home/FileListComponentLevel.tsx";
|
||||||
|
import { HeaderContainer, ProfileContainer } from "./Profile-styles";
|
||||||
|
import {
|
||||||
|
AuthorTextComment,
|
||||||
|
StyledCardColComment,
|
||||||
|
StyledCardHeaderComment,
|
||||||
|
} from "../IssueContent/IssueContent-styles.tsx";
|
||||||
|
import { Avatar, Box, useTheme } from "@mui/material";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { RootState } from "../../state/store";
|
||||||
|
|
||||||
|
export const IndividualProfile = () => {
|
||||||
|
const { name: paramName } = useParams();
|
||||||
|
|
||||||
|
const userAvatarHash = useSelector(
|
||||||
|
(state: RootState) => state.global.userAvatarHash
|
||||||
|
);
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const avatarUrl = useMemo(() => {
|
||||||
|
let url = "";
|
||||||
|
if (paramName && userAvatarHash[paramName]) {
|
||||||
|
url = userAvatarHash[paramName];
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}, [userAvatarHash, paramName]);
|
||||||
|
return (
|
||||||
|
<ProfileContainer>
|
||||||
|
<HeaderContainer>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StyledCardHeaderComment
|
||||||
|
sx={{
|
||||||
|
"& .MuiCardHeader-content": {
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Avatar
|
||||||
|
src={`/arbitrary/THUMBNAIL/${paramName}/qortal_avatar`}
|
||||||
|
alt={`${paramName}'s avatar`}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<StyledCardColComment>
|
||||||
|
<AuthorTextComment
|
||||||
|
color={
|
||||||
|
theme.palette.mode === "light"
|
||||||
|
? theme.palette.text.secondary
|
||||||
|
: "#d6e8ff"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{paramName}
|
||||||
|
</AuthorTextComment>
|
||||||
|
</StyledCardColComment>
|
||||||
|
</StyledCardHeaderComment>
|
||||||
|
</Box>
|
||||||
|
</HeaderContainer>
|
||||||
|
<FileListComponentLevel />
|
||||||
|
</ProfileContainer>
|
||||||
|
);
|
||||||
|
};
|
16
src/pages/IndividualProfile/Profile-styles.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { styled } from "@mui/system";
|
||||||
|
import { Box, Grid, Typography, Checkbox } from "@mui/material";
|
||||||
|
|
||||||
|
export const ProfileContainer = styled(Box)(({ theme }) => ({
|
||||||
|
position: "relative",
|
||||||
|
display: "flex",
|
||||||
|
width: "100%",
|
||||||
|
flexDirection: "column"
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const HeaderContainer = styled(Box)(({ theme }) => ({
|
||||||
|
position: "relative",
|
||||||
|
display: "flex",
|
||||||
|
width: "100%",
|
||||||
|
justifyContent: "center"
|
||||||
|
}));
|
89
src/pages/IssueContent/IssueContent-styles.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { styled } from "@mui/system";
|
||||||
|
import { Box, Typography } from "@mui/material";
|
||||||
|
|
||||||
|
export const FilePlayerContainer = styled(Box)(({ theme }) => ({
|
||||||
|
maxWidth: "95%",
|
||||||
|
width: "1000px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const FileTitle = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Raleway",
|
||||||
|
fontSize: "20px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const FileDescription = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Raleway",
|
||||||
|
fontSize: "16px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const ImageContainer = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const Spacer = ({ height }: any) => {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: height,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StyledCardHeaderComment = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
gap: "5px",
|
||||||
|
padding: "7px 0px",
|
||||||
|
});
|
||||||
|
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 AuthorTextComment = styled(Typography)({
|
||||||
|
fontFamily: "Raleway, sans-serif",
|
||||||
|
fontSize: "16px",
|
||||||
|
lineHeight: "1.2",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const FileAttachmentContainer = styled(Box)(({ theme }) => ({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "20px",
|
||||||
|
padding: "5px 10px",
|
||||||
|
border: `1px solid ${theme.palette.text.primary}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const FileAttachmentFont = styled(Typography)(({ theme }) => ({
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
fontSize: "16px",
|
||||||
|
letterSpacing: 0,
|
||||||
|
fontWeight: 400,
|
||||||
|
userSelect: "none",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}));
|
564
src/pages/IssueContent/IssueContent.tsx
Normal file
@ -0,0 +1,564 @@
|
|||||||
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { setIsLoadingGlobal } from "../../state/features/globalSlice";
|
||||||
|
import { Avatar, Box, Typography, useTheme } from "@mui/material";
|
||||||
|
import { RootState } from "../../state/store";
|
||||||
|
import { addToHashMap } from "../../state/features/fileSlice.ts";
|
||||||
|
import AttachFileIcon from "@mui/icons-material/AttachFile";
|
||||||
|
import DownloadIcon from "@mui/icons-material/Download";
|
||||||
|
import {
|
||||||
|
AuthorTextComment,
|
||||||
|
FileAttachmentContainer,
|
||||||
|
FileAttachmentFont,
|
||||||
|
FileDescription,
|
||||||
|
FilePlayerContainer,
|
||||||
|
FileTitle,
|
||||||
|
ImageContainer,
|
||||||
|
Spacer,
|
||||||
|
StyledCardColComment,
|
||||||
|
StyledCardHeaderComment,
|
||||||
|
} from "./IssueContent-styles.tsx";
|
||||||
|
import { formatDate } from "../../utils/time";
|
||||||
|
import { CommentSection } from "../../components/common/Comments/CommentSection";
|
||||||
|
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
|
||||||
|
import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml";
|
||||||
|
import FileElement from "../../components/common/FileElement";
|
||||||
|
import { allCategoryData } from "../../constants/Categories/1stCategories.ts";
|
||||||
|
import {
|
||||||
|
Category,
|
||||||
|
getCategoriesFromObject,
|
||||||
|
} from "../../components/common/CategoryList/CategoryList.tsx";
|
||||||
|
import { getIconsFromObject } from "../../constants/Categories/CategoryFunctions.ts";
|
||||||
|
|
||||||
|
export function formatBytes(bytes, decimals = 2) {
|
||||||
|
if (bytes === 0) return "0 Bytes";
|
||||||
|
|
||||||
|
const k = 1024;
|
||||||
|
const dm = decimals < 0 ? 0 : decimals;
|
||||||
|
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||||
|
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IssueContent = () => {
|
||||||
|
const { name, id } = useParams();
|
||||||
|
const [isExpandedDescription, setIsExpandedDescription] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
const [descriptionHeight, setDescriptionHeight] = useState<null | number>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [icon, setIcon] = useState<string>("");
|
||||||
|
const userAvatarHash = useSelector(
|
||||||
|
(state: RootState) => state.global.userAvatarHash
|
||||||
|
);
|
||||||
|
const contentRef = useRef(null);
|
||||||
|
|
||||||
|
const avatarUrl = useMemo(() => {
|
||||||
|
let url = "";
|
||||||
|
if (name && userAvatarHash[name]) {
|
||||||
|
url = userAvatarHash[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}, [userAvatarHash, name]);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const [fileData, setFileData] = useState<any>(null);
|
||||||
|
const [playlistData, setPlaylistData] = useState<any>(null);
|
||||||
|
|
||||||
|
const hashMapVideos = useSelector(
|
||||||
|
(state: RootState) => state.file.hashMapFiles
|
||||||
|
);
|
||||||
|
const videoReference = useMemo(() => {
|
||||||
|
if (!fileData) return null;
|
||||||
|
const { videoReference } = fileData;
|
||||||
|
if (
|
||||||
|
videoReference?.identifier &&
|
||||||
|
videoReference?.name &&
|
||||||
|
videoReference?.service
|
||||||
|
) {
|
||||||
|
return videoReference;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, [fileData]);
|
||||||
|
|
||||||
|
const videoCover = useMemo(() => {
|
||||||
|
if (!fileData) return null;
|
||||||
|
const { videoImage } = fileData;
|
||||||
|
return videoImage || null;
|
||||||
|
}, [fileData]);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const getVideoData = React.useCallback(async (name: string, id: string) => {
|
||||||
|
try {
|
||||||
|
if (!name || !id) return;
|
||||||
|
dispatch(setIsLoadingGlobal(true));
|
||||||
|
|
||||||
|
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QSUPPORT_FILE_BASE}&limit=1&includemetadata=true&reverse=true&excludeblocked=true&name=${name}&exactmatchnames=true&offset=0&identifier=${id}`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const responseDataSearch = await response.json();
|
||||||
|
|
||||||
|
if (responseDataSearch?.length > 0) {
|
||||||
|
let resourceData = responseDataSearch[0];
|
||||||
|
resourceData = {
|
||||||
|
title: resourceData?.metadata?.title,
|
||||||
|
category: resourceData?.metadata?.category,
|
||||||
|
categoryName: resourceData?.metadata?.categoryName,
|
||||||
|
tags: resourceData?.metadata?.tags || [],
|
||||||
|
description: resourceData?.metadata?.description,
|
||||||
|
created: resourceData?.created,
|
||||||
|
updated: resourceData?.updated,
|
||||||
|
user: resourceData.name,
|
||||||
|
videoImage: "",
|
||||||
|
id: resourceData.identifier,
|
||||||
|
};
|
||||||
|
|
||||||
|
const responseData = await qortalRequest({
|
||||||
|
action: "FETCH_QDN_RESOURCE",
|
||||||
|
name: name,
|
||||||
|
service: "DOCUMENT",
|
||||||
|
identifier: id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (responseData && !responseData.error) {
|
||||||
|
const combinedData = {
|
||||||
|
...resourceData,
|
||||||
|
...responseData,
|
||||||
|
};
|
||||||
|
setFileData(combinedData);
|
||||||
|
dispatch(addToHashMap(combinedData));
|
||||||
|
checkforPlaylist(name, id, combinedData?.code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
} finally {
|
||||||
|
dispatch(setIsLoadingGlobal(false));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const checkforPlaylist = React.useCallback(async (name, id, code) => {
|
||||||
|
try {
|
||||||
|
if (!name || !id || !code) return;
|
||||||
|
|
||||||
|
const url = `/arbitrary/resources/search?mode=ALL&service=PLAYLIST&description=c:${code}&limit=1&includemetadata=true&reverse=true&excludeblocked=true&name=${name}&exactmatchnames=true&offset=0`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const responseDataSearch = await response.json();
|
||||||
|
|
||||||
|
if (responseDataSearch?.length > 0) {
|
||||||
|
let resourceData = responseDataSearch[0];
|
||||||
|
resourceData = {
|
||||||
|
title: resourceData?.metadata?.title,
|
||||||
|
category: resourceData?.metadata?.category,
|
||||||
|
categoryName: resourceData?.metadata?.categoryName,
|
||||||
|
tags: resourceData?.metadata?.tags || [],
|
||||||
|
description: resourceData?.metadata?.description,
|
||||||
|
created: resourceData?.created,
|
||||||
|
updated: resourceData?.updated,
|
||||||
|
name: resourceData.name,
|
||||||
|
videoImage: "",
|
||||||
|
identifier: resourceData.identifier,
|
||||||
|
service: resourceData.service,
|
||||||
|
};
|
||||||
|
|
||||||
|
const responseData = await qortalRequest({
|
||||||
|
action: "FETCH_QDN_RESOURCE",
|
||||||
|
name: resourceData.name,
|
||||||
|
service: resourceData.service,
|
||||||
|
identifier: resourceData.identifier,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (responseData && !responseData.error) {
|
||||||
|
const combinedData = {
|
||||||
|
...resourceData,
|
||||||
|
...responseData,
|
||||||
|
};
|
||||||
|
const videos = [];
|
||||||
|
if (combinedData?.videos) {
|
||||||
|
for (const vid of combinedData.videos) {
|
||||||
|
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&identifier=${vid.identifier}&limit=1&includemetadata=true&reverse=true&name=${vid.name}&exactmatchnames=true&offset=0`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const responseDataSearchVid = await response.json();
|
||||||
|
|
||||||
|
if (responseDataSearchVid?.length > 0) {
|
||||||
|
let resourceData2 = responseDataSearchVid[0];
|
||||||
|
videos.push(resourceData2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
combinedData.videos = videos;
|
||||||
|
setPlaylistData(combinedData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (name && id) {
|
||||||
|
const existingVideo = hashMapVideos[id];
|
||||||
|
|
||||||
|
if (existingVideo) {
|
||||||
|
setFileData(existingVideo);
|
||||||
|
checkforPlaylist(name, id, existingVideo?.code);
|
||||||
|
} else {
|
||||||
|
getVideoData(name, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [id, name]);
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
// dispatch(setUserAvatarHash({
|
||||||
|
// name: author,
|
||||||
|
// url
|
||||||
|
// }))
|
||||||
|
// } catch (error) { }
|
||||||
|
// }, [])
|
||||||
|
|
||||||
|
// React.useEffect(() => {
|
||||||
|
// if (name && !avatarUrl) {
|
||||||
|
// const existingAvatar = userAvatarHash[name]
|
||||||
|
|
||||||
|
// if (existingAvatar) {
|
||||||
|
// setAvatarUrl(existingAvatar)
|
||||||
|
// } else {
|
||||||
|
// getAvatar(name)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
// }, [name, userAvatarHash])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (contentRef.current) {
|
||||||
|
const height = contentRef.current.offsetHeight;
|
||||||
|
if (height > 100) {
|
||||||
|
// Assuming 100px is your threshold
|
||||||
|
setDescriptionHeight(100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fileData) {
|
||||||
|
const icon = getIconsFromObject(fileData);
|
||||||
|
setIcon(icon);
|
||||||
|
}
|
||||||
|
}, [fileData]);
|
||||||
|
|
||||||
|
const categoriesDisplay = useMemo(() => {
|
||||||
|
if (fileData) {
|
||||||
|
const categoryList = getCategoriesFromObject(fileData);
|
||||||
|
|
||||||
|
const categoryNames = categoryList.map((categoryID, index) => {
|
||||||
|
let categoryName: Category;
|
||||||
|
if (index === 0) {
|
||||||
|
categoryName = allCategoryData.category.find(
|
||||||
|
item => item?.id === +categoryList[0]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const subCategories = allCategoryData.subCategories[index - 1];
|
||||||
|
const selectedSubCategory = subCategories[categoryList[index - 1]];
|
||||||
|
if (selectedSubCategory) {
|
||||||
|
categoryName = selectedSubCategory.find(
|
||||||
|
item => item?.id === +categoryList[index]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return categoryName?.name;
|
||||||
|
});
|
||||||
|
const filteredCategoryNames = categoryNames.filter(name => name);
|
||||||
|
let categoryDisplay = "";
|
||||||
|
const separator = " > ";
|
||||||
|
filteredCategoryNames.map((name, index) => {
|
||||||
|
categoryDisplay +=
|
||||||
|
index !== filteredCategoryNames.length - 1 ? name + separator : name;
|
||||||
|
});
|
||||||
|
return categoryDisplay;
|
||||||
|
}
|
||||||
|
return "no videodata";
|
||||||
|
}, [fileData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
flexDirection: "column",
|
||||||
|
padding: "20px 10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FilePlayerContainer
|
||||||
|
sx={{
|
||||||
|
marginBottom: "30px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Spacer height="15px" />
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon ? (
|
||||||
|
<img
|
||||||
|
src={icon}
|
||||||
|
width="50px"
|
||||||
|
style={{
|
||||||
|
borderRadius: "5px",
|
||||||
|
marginRight: "10px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<AttachFileIcon />
|
||||||
|
)}
|
||||||
|
<FileTitle
|
||||||
|
variant="h1"
|
||||||
|
color="textPrimary"
|
||||||
|
sx={{
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{fileData?.title}
|
||||||
|
</FileTitle>
|
||||||
|
</div>
|
||||||
|
{fileData?.created && (
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{
|
||||||
|
fontSize: "12px",
|
||||||
|
}}
|
||||||
|
color={theme.palette.text.primary}
|
||||||
|
>
|
||||||
|
{formatDate(fileData.created)}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Spacer height="15px" />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
navigate(`/channel/${name}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<StyledCardHeaderComment
|
||||||
|
sx={{
|
||||||
|
"& .MuiCardHeader-content": {
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<Avatar
|
||||||
|
src={`/arbitrary/THUMBNAIL/${name}/qortal_avatar`}
|
||||||
|
alt={`${name}'s avatar`}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<StyledCardColComment>
|
||||||
|
<AuthorTextComment
|
||||||
|
color={
|
||||||
|
theme.palette.mode === "light"
|
||||||
|
? theme.palette.text.secondary
|
||||||
|
: "#d6e8ff"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</AuthorTextComment>
|
||||||
|
</StyledCardColComment>
|
||||||
|
</StyledCardHeaderComment>
|
||||||
|
</Box>
|
||||||
|
<Spacer height="15px" />
|
||||||
|
<Box>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontWeight: "bold",
|
||||||
|
fontSize: "16px",
|
||||||
|
userSelect: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{categoriesDisplay}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<ImageContainer>
|
||||||
|
{fileData?.images &&
|
||||||
|
fileData.images.map(image => {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
width={`${1280 / fileData.images.length}px`}
|
||||||
|
height={"480px"}
|
||||||
|
style={{ marginRight: "10px", marginBottom: "10px" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ImageContainer>
|
||||||
|
<Spacer height="15px" />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
background: "#333333",
|
||||||
|
borderRadius: "5px",
|
||||||
|
padding: "5px",
|
||||||
|
width: "100%",
|
||||||
|
cursor: !descriptionHeight
|
||||||
|
? "default"
|
||||||
|
: isExpandedDescription
|
||||||
|
? "default"
|
||||||
|
: "pointer",
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
className={
|
||||||
|
!descriptionHeight ? "" : isExpandedDescription ? "" : "hover-click"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{descriptionHeight && !isExpandedDescription && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "0px",
|
||||||
|
right: "0px",
|
||||||
|
left: "0px",
|
||||||
|
bottom: "0px",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
if (isExpandedDescription) return;
|
||||||
|
setIsExpandedDescription(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Box
|
||||||
|
ref={contentRef}
|
||||||
|
sx={{
|
||||||
|
height: !descriptionHeight
|
||||||
|
? "auto"
|
||||||
|
: isExpandedDescription
|
||||||
|
? "auto"
|
||||||
|
: "100px",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{fileData?.htmlDescription ? (
|
||||||
|
<DisplayHtml html={fileData?.htmlDescription} />
|
||||||
|
) : (
|
||||||
|
<FileDescription
|
||||||
|
variant="body1"
|
||||||
|
color="textPrimary"
|
||||||
|
sx={{
|
||||||
|
cursor: "default",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{fileData?.fullDescription}
|
||||||
|
</FileDescription>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{descriptionHeight && (
|
||||||
|
<Typography
|
||||||
|
onClick={() => {
|
||||||
|
setIsExpandedDescription(prev => !prev);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
fontWeight: "bold",
|
||||||
|
fontSize: "16px",
|
||||||
|
cursor: "pointer",
|
||||||
|
paddingLeft: "15px",
|
||||||
|
paddingTop: "15px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isExpandedDescription ? "Show less" : "...more"}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "25px",
|
||||||
|
marginTop: "25px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{fileData?.files?.map((file, index) => {
|
||||||
|
return (
|
||||||
|
<FileAttachmentContainer
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
key={file.toString() + index}
|
||||||
|
>
|
||||||
|
<FileAttachmentFont>{file.filename}</FileAttachmentFont>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "25px",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FileAttachmentFont>
|
||||||
|
{formatBytes(file?.size || 0)}
|
||||||
|
</FileAttachmentFont>
|
||||||
|
<FileElement
|
||||||
|
fileInfo={{
|
||||||
|
...file,
|
||||||
|
filename: file?.filename,
|
||||||
|
mimeType: file?.mimetype,
|
||||||
|
}}
|
||||||
|
jsonId={id}
|
||||||
|
title={file?.filename}
|
||||||
|
customStyles={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "flex-end",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DownloadIcon />
|
||||||
|
</FileElement>
|
||||||
|
</Box>
|
||||||
|
</FileAttachmentContainer>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</FilePlayerContainer>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "20px",
|
||||||
|
width: "100%",
|
||||||
|
maxWidth: "1200px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CommentSection postId={id || ""} postName={name || ""} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
27
src/state/features/authSlice.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
|
||||||
|
interface AuthState {
|
||||||
|
user: {
|
||||||
|
address: string;
|
||||||
|
publicKey: string;
|
||||||
|
name?: string;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
const initialState: AuthState = {
|
||||||
|
user: null
|
||||||
|
};
|
||||||
|
|
||||||
|
export const authSlice = createSlice({
|
||||||
|
name: 'auth',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
addUser: (state, action) => {
|
||||||
|
state.user = action.payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { addUser } = authSlice.actions;
|
||||||
|
|
||||||
|
export default authSlice.reducer;
|
188
src/state/features/fileSlice.ts
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
|
import { RootState } from "../store";
|
||||||
|
|
||||||
|
interface GlobalState {
|
||||||
|
files: Video[];
|
||||||
|
filteredFiles: Video[];
|
||||||
|
hashMapFiles: Record<string, Video>;
|
||||||
|
countNewFiles: number;
|
||||||
|
isFiltering: boolean;
|
||||||
|
filterValue: string;
|
||||||
|
filterType: string;
|
||||||
|
filterSearch: string;
|
||||||
|
filterName: string;
|
||||||
|
selectedCategoryFiles: any[];
|
||||||
|
editFileProperties: any;
|
||||||
|
editPlaylistProperties: any;
|
||||||
|
}
|
||||||
|
const initialState: GlobalState = {
|
||||||
|
files: [],
|
||||||
|
filteredFiles: [],
|
||||||
|
hashMapFiles: {},
|
||||||
|
countNewFiles: 0,
|
||||||
|
isFiltering: false,
|
||||||
|
filterValue: "",
|
||||||
|
filterType: "videos",
|
||||||
|
filterSearch: "",
|
||||||
|
filterName: "",
|
||||||
|
selectedCategoryFiles: [null, null, null, null],
|
||||||
|
editFileProperties: null,
|
||||||
|
editPlaylistProperties: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Video {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
created: number | string;
|
||||||
|
user: string;
|
||||||
|
service?: string;
|
||||||
|
videoImage?: string;
|
||||||
|
id: string;
|
||||||
|
category?: string;
|
||||||
|
categoryName?: string;
|
||||||
|
tags?: string[];
|
||||||
|
updated?: number | string;
|
||||||
|
isValid?: boolean;
|
||||||
|
code?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fileSlice = createSlice({
|
||||||
|
name: "file",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setEditFile: (state, action) => {
|
||||||
|
state.editFileProperties = action.payload;
|
||||||
|
},
|
||||||
|
setEditPlaylist: (state, action) => {
|
||||||
|
state.editPlaylistProperties = action.payload;
|
||||||
|
},
|
||||||
|
changeFilterType: (state, action) => {
|
||||||
|
state.filterType = action.payload;
|
||||||
|
},
|
||||||
|
changefilterSearch: (state, action) => {
|
||||||
|
state.filterSearch = action.payload;
|
||||||
|
},
|
||||||
|
changefilterName: (state, action) => {
|
||||||
|
state.filterName = action.payload;
|
||||||
|
},
|
||||||
|
setCountNewFiles: (state, action) => {
|
||||||
|
state.countNewFiles = action.payload;
|
||||||
|
},
|
||||||
|
addFiles: (state, action) => {
|
||||||
|
state.files = action.payload;
|
||||||
|
},
|
||||||
|
addFilteredFiles: (state, action) => {
|
||||||
|
state.filteredFiles = action.payload;
|
||||||
|
},
|
||||||
|
removeFile: (state, action) => {
|
||||||
|
const idToDelete = action.payload;
|
||||||
|
state.files = state.files.filter(item => item.id !== idToDelete);
|
||||||
|
state.filteredFiles = state.filteredFiles.filter(
|
||||||
|
item => item.id !== idToDelete
|
||||||
|
);
|
||||||
|
},
|
||||||
|
addFileToBeginning: (state, action) => {
|
||||||
|
state.files.unshift(action.payload);
|
||||||
|
},
|
||||||
|
clearFileList: state => {
|
||||||
|
state.files = [];
|
||||||
|
},
|
||||||
|
updateFile: (state, action) => {
|
||||||
|
const { id } = action.payload;
|
||||||
|
const index = state.files.findIndex(video => video.id === id);
|
||||||
|
if (index !== -1) {
|
||||||
|
state.files[index] = { ...action.payload };
|
||||||
|
}
|
||||||
|
const index2 = state.filteredFiles.findIndex(video => video.id === id);
|
||||||
|
if (index2 !== -1) {
|
||||||
|
state.filteredFiles[index2] = { ...action.payload };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addToHashMap: (state, action) => {
|
||||||
|
const video = action.payload;
|
||||||
|
state.hashMapFiles[video.id] = video;
|
||||||
|
},
|
||||||
|
updateInHashMap: (state, action) => {
|
||||||
|
const { id } = action.payload;
|
||||||
|
const video = action.payload;
|
||||||
|
state.hashMapFiles[id] = { ...video };
|
||||||
|
},
|
||||||
|
removeFromHashMap: (state, action) => {
|
||||||
|
const idToDelete = action.payload;
|
||||||
|
delete state.hashMapFiles[idToDelete];
|
||||||
|
},
|
||||||
|
addArrayToHashMap: (state, action) => {
|
||||||
|
const videos = action.payload;
|
||||||
|
videos.forEach((video: Video) => {
|
||||||
|
state.hashMapFiles[video.id] = video;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
upsertFiles: (state, action) => {
|
||||||
|
action.payload.forEach((video: Video) => {
|
||||||
|
const index = state.files.findIndex(p => p.id === video.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
state.files[index] = video;
|
||||||
|
} else {
|
||||||
|
state.files.push(video);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
upsertFilteredFiles: (state, action) => {
|
||||||
|
action.payload.forEach((video: Video) => {
|
||||||
|
const index = state.filteredFiles.findIndex(p => p.id === video.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
state.filteredFiles[index] = video;
|
||||||
|
} else {
|
||||||
|
state.filteredFiles.push(video);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
upsertFilesBeginning: (state, action) => {
|
||||||
|
action.payload.reverse().forEach((video: Video) => {
|
||||||
|
const index = state.files.findIndex(p => p.id === video.id);
|
||||||
|
if (index !== -1) {
|
||||||
|
state.files[index] = video;
|
||||||
|
} else {
|
||||||
|
state.files.unshift(video);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setIsFiltering: (state, action) => {
|
||||||
|
state.isFiltering = action.payload;
|
||||||
|
},
|
||||||
|
setFilterValue: (state, action) => {
|
||||||
|
state.filterValue = action.payload;
|
||||||
|
},
|
||||||
|
blockUser: (state, action) => {
|
||||||
|
const username = action.payload;
|
||||||
|
state.files = state.files.filter(item => item.user !== username);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
setCountNewFiles,
|
||||||
|
addFiles,
|
||||||
|
addFilteredFiles,
|
||||||
|
removeFile,
|
||||||
|
addFileToBeginning,
|
||||||
|
updateFile,
|
||||||
|
addToHashMap,
|
||||||
|
updateInHashMap,
|
||||||
|
removeFromHashMap,
|
||||||
|
addArrayToHashMap,
|
||||||
|
upsertFiles,
|
||||||
|
upsertFilteredFiles,
|
||||||
|
upsertFilesBeginning,
|
||||||
|
setIsFiltering,
|
||||||
|
setFilterValue,
|
||||||
|
clearFileList,
|
||||||
|
changeFilterType,
|
||||||
|
changefilterSearch,
|
||||||
|
changefilterName,
|
||||||
|
blockUser,
|
||||||
|
setEditFile,
|
||||||
|
setEditPlaylist,
|
||||||
|
} = fileSlice.actions;
|
||||||
|
|
||||||
|
export default fileSlice.reducer;
|
79
src/state/features/globalSlice.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
interface GlobalState {
|
||||||
|
isLoadingGlobal: boolean;
|
||||||
|
downloads: any;
|
||||||
|
userAvatarHash: Record<string, string>;
|
||||||
|
publishNames: string[] | null;
|
||||||
|
videoPlaying: any | null;
|
||||||
|
totalFilesPublished: number;
|
||||||
|
totalNamesPublished: number;
|
||||||
|
filesPerNamePublished: number;
|
||||||
|
}
|
||||||
|
const initialState: GlobalState = {
|
||||||
|
isLoadingGlobal: false,
|
||||||
|
downloads: {},
|
||||||
|
userAvatarHash: {},
|
||||||
|
publishNames: null,
|
||||||
|
videoPlaying: null,
|
||||||
|
totalFilesPublished: null,
|
||||||
|
totalNamesPublished: null,
|
||||||
|
filesPerNamePublished: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const globalSlice = createSlice({
|
||||||
|
name: "global",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setIsLoadingGlobal: (state, action) => {
|
||||||
|
state.isLoadingGlobal = action.payload;
|
||||||
|
},
|
||||||
|
setAddToDownloads: (state, action) => {
|
||||||
|
const download = action.payload;
|
||||||
|
state.downloads[download.identifier] = download;
|
||||||
|
},
|
||||||
|
updateDownloads: (state, action) => {
|
||||||
|
const { identifier } = action.payload;
|
||||||
|
const download = action.payload;
|
||||||
|
state.downloads[identifier] = {
|
||||||
|
...state.downloads[identifier],
|
||||||
|
...download,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
setUserAvatarHash: (state, action) => {
|
||||||
|
const avatar = action.payload;
|
||||||
|
if (avatar?.name && avatar?.url) {
|
||||||
|
state.userAvatarHash[avatar?.name] = avatar?.url;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addPublishNames: (state, action) => {
|
||||||
|
state.publishNames = action.payload;
|
||||||
|
},
|
||||||
|
setVideoPlaying: (state, action) => {
|
||||||
|
state.videoPlaying = action.payload;
|
||||||
|
},
|
||||||
|
setTotalFilesPublished: (state, action) => {
|
||||||
|
state.totalFilesPublished = action.payload;
|
||||||
|
},
|
||||||
|
setTotalNamesPublished: (state, action) => {
|
||||||
|
state.totalNamesPublished = action.payload;
|
||||||
|
},
|
||||||
|
setFilesPerNamePublished: (state, action) => {
|
||||||
|
state.filesPerNamePublished = action.payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
setIsLoadingGlobal,
|
||||||
|
setAddToDownloads,
|
||||||
|
updateDownloads,
|
||||||
|
setUserAvatarHash,
|
||||||
|
addPublishNames,
|
||||||
|
setVideoPlaying,
|
||||||
|
setTotalFilesPublished,
|
||||||
|
setTotalNamesPublished,
|
||||||
|
setFilesPerNamePublished,
|
||||||
|
} = globalSlice.actions;
|
||||||
|
|
||||||
|
export default globalSlice.reducer;
|
73
src/state/features/notificationsSlice.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
|
interface AlertTypes {
|
||||||
|
alertSuccess: string
|
||||||
|
alertError: string
|
||||||
|
alertInfo: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface InitialState {
|
||||||
|
alertTypes: AlertTypes
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: InitialState = {
|
||||||
|
alertTypes: {
|
||||||
|
alertSuccess: '',
|
||||||
|
alertError: '',
|
||||||
|
alertInfo: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const notificationsSlice = createSlice({
|
||||||
|
name: "notifications",
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setNotification: (
|
||||||
|
state: InitialState,
|
||||||
|
action: PayloadAction<{ alertType: string; msg: string }>
|
||||||
|
) => {
|
||||||
|
if (action.payload.alertType === "success") {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
alertTypes: {
|
||||||
|
...state.alertTypes,
|
||||||
|
alertSuccess: action.payload.msg,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (action.payload.alertType === "error") {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
alertTypes: {
|
||||||
|
...state.alertTypes,
|
||||||
|
alertError: action.payload.msg,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else if (action.payload.alertType === "info") {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
alertTypes: {
|
||||||
|
...state.alertTypes,
|
||||||
|
alertInfo: action.payload.msg,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
removeNotification: (state: InitialState) => {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
alertTypes: {
|
||||||
|
...state.alertTypes,
|
||||||
|
alertSuccess: '',
|
||||||
|
alertError: '',
|
||||||
|
alertInfo: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setNotification, removeNotification } =
|
||||||
|
notificationsSlice.actions;
|
||||||
|
|
||||||
|
export default notificationsSlice.reducer;
|
27
src/state/store.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
|
import notificationsReducer from "./features/notificationsSlice";
|
||||||
|
import authReducer from "./features/authSlice";
|
||||||
|
import globalReducer from "./features/globalSlice";
|
||||||
|
import fileReducer from "./features/fileSlice.ts";
|
||||||
|
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer: {
|
||||||
|
notifications: notificationsReducer,
|
||||||
|
auth: authReducer,
|
||||||
|
global: globalReducer,
|
||||||
|
file: fileReducer,
|
||||||
|
},
|
||||||
|
middleware: getDefaultMiddleware =>
|
||||||
|
getDefaultMiddleware({
|
||||||
|
serializableCheck: false,
|
||||||
|
}),
|
||||||
|
preloadedState: undefined, // optional, can be any valid state object
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define the RootState type, which is the type of the entire Redux state tree.
|
||||||
|
// This is useful when you need to access the state in a component or elsewhere.
|
||||||
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
|
|
||||||
|
// Define the AppDispatch type, which is the type of the Redux store's dispatch function.
|
||||||
|
// This is useful when you need to dispatch an action in a component or elsewhere.
|
||||||
|
export type AppDispatch = typeof store.dispatch;
|