mirror of https://github.com/Qortal/q-tube
Justin Ferrari
10 months ago
commit
0b78f49386
99 changed files with 20696 additions and 0 deletions
@ -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" |
||||||
|
|
||||||
|
}, |
||||||
|
} |
@ -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 |
@ -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-Tube</title> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<div id="root"></div> |
||||||
|
<script type="module" src="/src/main.tsx"></script> |
||||||
|
</body> |
||||||
|
</html> |
@ -0,0 +1,47 @@ |
|||||||
|
{ |
||||||
|
"name": "qtube", |
||||||
|
"private": true, |
||||||
|
"version": "0.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", |
||||||
|
"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": "^4.3.2" |
||||||
|
} |
||||||
|
} |
After Width: | Height: | Size: 1.1 KiB |
@ -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; |
||||||
|
} |
||||||
|
|
@ -0,0 +1,39 @@ |
|||||||
|
import { useState } from "react"; |
||||||
|
import { Routes, Route } from "react-router-dom"; |
||||||
|
import { ThemeProvider } from "@mui/material/styles"; |
||||||
|
import { CssBaseline } from "@mui/material"; |
||||||
|
import { lightTheme, darkTheme } from "./styles/theme"; |
||||||
|
import { store } from "./state/store"; |
||||||
|
import { Provider } from "react-redux"; |
||||||
|
import GlobalWrapper from "./wrappers/GlobalWrapper"; |
||||||
|
import Notification from "./components/common/Notification/Notification"; |
||||||
|
import { Home } from "./pages/Home/Home"; |
||||||
|
import { VideoContent } from "./pages/VideoContent/VideoContent"; |
||||||
|
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="/video/:name/:id" element={<VideoContent />} /> |
||||||
|
<Route path="/channel/:name" element={<IndividualProfile />} /> |
||||||
|
</Routes> |
||||||
|
</GlobalWrapper> |
||||||
|
</DownloadWrapper> |
||||||
|
</ThemeProvider> |
||||||
|
</Provider> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export default App; |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 9.3 KiB |
@ -0,0 +1,25 @@ |
|||||||
|
interface AccountCircleSVGProps { |
||||||
|
color: string |
||||||
|
height: string |
||||||
|
width: string |
||||||
|
} |
||||||
|
|
||||||
|
export const AccountCircleSVG: React.FC<AccountCircleSVGProps> = ({ |
||||||
|
color, |
||||||
|
height, |
||||||
|
width |
||||||
|
}) => { |
||||||
|
return ( |
||||||
|
<svg |
||||||
|
xmlns="http://www.w3.org/2000/svg" |
||||||
|
height={height} |
||||||
|
viewBox="0 96 960 960" |
||||||
|
width={width} |
||||||
|
> |
||||||
|
<path |
||||||
|
fill={color} |
||||||
|
d="M222 801q63-44 125-67.5T480 710q71 0 133.5 23.5T739 801q44-54 62.5-109T820 576q0-145-97.5-242.5T480 236q-145 0-242.5 97.5T140 576q0 61 19 116t63 109Zm257.814-195Q422 606 382.5 566.314q-39.5-39.686-39.5-97.5t39.686-97.314q39.686-39.5 97.5-39.5t97.314 39.686q39.5 39.686 39.5 97.5T577.314 566.5q-39.686 39.5-97.5 39.5Zm.654 370Q398 976 325 944.5q-73-31.5-127.5-86t-86-127.266Q80 658.468 80 575.734T111.5 420.5q31.5-72.5 86-127t127.266-86q72.766-31.5 155.5-31.5T635.5 207.5q72.5 31.5 127 86t86 127.032q31.5 72.532 31.5 155T848.5 731q-31.5 73-86 127.5t-127.032 86q-72.532 31.5-155 31.5ZM480 916q55 0 107.5-16T691 844q-51-36-104-55t-107-19q-54 0-107 19t-104 55q51 40 103.5 56T480 916Zm0-370q34 0 55.5-21.5T557 469q0-34-21.5-55.5T480 392q-34 0-55.5 21.5T403 469q0 34 21.5 55.5T480 546Zm0-77Zm0 374Z" |
||||||
|
/> |
||||||
|
</svg> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,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> |
||||||
|
); |
||||||
|
}; |
@ -0,0 +1,23 @@ |
|||||||
|
import { IconTypes } from './IconTypes' |
||||||
|
|
||||||
|
export const DarkModeSVG: React.FC<IconTypes> = ({ |
||||||
|
color, |
||||||
|
height, |
||||||
|
width, |
||||||
|
className, |
||||||
|
onClickFunc |
||||||
|
}) => { |
||||||
|
return ( |
||||||
|
<svg |
||||||
|
className={className} |
||||||
|
onClick={onClickFunc} |
||||||
|
fill={color} |
||||||
|
xmlns="http://www.w3.org/2000/svg" |
||||||
|
height={height} |
||||||
|
viewBox="0 96 960 960" |
||||||
|
width={width} |
||||||
|
> |
||||||
|
<path d="M480 936q-150 0-255-105T120 576q0-150 105-255t255-105q8 0 17 .5t23 1.5q-36 32-56 79t-20 99q0 90 63 153t153 63q52 0 99-18.5t79-51.5q1 12 1.5 19.5t.5 14.5q0 150-105 255T480 936Zm0-60q109 0 190-67.5T771 650q-25 11-53.667 16.5Q688.667 672 660 672q-114.689 0-195.345-80.655Q384 510.689 384 396q0-24 5-51.5t18-62.5q-98 27-162.5 109.5T180 576q0 125 87.5 212.5T480 876Zm-4-297Z" /> |
||||||
|
</svg> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,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> |
||||||
|
) |
||||||
|
} |
@ -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> |
||||||
|
) |
||||||
|
} |
@ -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> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,22 @@ |
|||||||
|
import { IconTypes } from "./IconTypes"; |
||||||
|
export const ExpandMoreSVG: React.FC<IconTypes> = ({ |
||||||
|
color, |
||||||
|
height, |
||||||
|
width, |
||||||
|
className, |
||||||
|
onClickFunc |
||||||
|
}) => { |
||||||
|
return ( |
||||||
|
<svg |
||||||
|
onClick={onClickFunc} |
||||||
|
height={height} |
||||||
|
width={width} |
||||||
|
fill={color} |
||||||
|
className={className} |
||||||
|
xmlns="http://www.w3.org/2000/svg" |
||||||
|
viewBox="0 -960 960 960" |
||||||
|
> |
||||||
|
<path d="M480-345 240-585l43-43 197 198 197-197 43 43-240 239Z" /> |
||||||
|
</svg> |
||||||
|
); |
||||||
|
}; |
@ -0,0 +1,7 @@ |
|||||||
|
export interface IconTypes { |
||||||
|
color?: string; |
||||||
|
height: string; |
||||||
|
width: string; |
||||||
|
className?: string; |
||||||
|
onClickFunc?: (e?: any) => void; |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
import { IconTypes } from './IconTypes' |
||||||
|
|
||||||
|
export const LightModeSVG: React.FC<IconTypes> = ({ |
||||||
|
color, |
||||||
|
height, |
||||||
|
width, |
||||||
|
className, |
||||||
|
onClickFunc |
||||||
|
}) => { |
||||||
|
return ( |
||||||
|
<svg |
||||||
|
className={className} |
||||||
|
onClick={onClickFunc} |
||||||
|
fill={color} |
||||||
|
xmlns="http://www.w3.org/2000/svg" |
||||||
|
height={height} |
||||||
|
viewBox="0 96 960 960" |
||||||
|
width={width} |
||||||
|
> |
||||||
|
<path d="M479.765 716Q538 716 579 675.235q41-40.764 41-99Q620 518 579.235 477q-40.764-41-99-41Q422 436 381 476.765q-41 40.764-41 99Q340 634 380.765 675q40.764 41 99 41Zm.235 60q-83 0-141.5-58.5T280 576q0-83 58.5-141.5T480 376q83 0 141.5 58.5T680 576q0 83-58.5 141.5T480 776ZM70 606q-12.75 0-21.375-8.675Q40 588.649 40 575.825 40 563 48.625 554.5T70 546h100q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T170 606H70Zm720 0q-12.75 0-21.375-8.675-8.625-8.676-8.625-21.5 0-12.825 8.625-21.325T790 546h100q12.75 0 21.375 8.675 8.625 8.676 8.625 21.5 0 12.825-8.625 21.325T890 606H790ZM479.825 296Q467 296 458.5 287.375T450 266V166q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T510 166v100q0 12.75-8.675 21.375-8.676 8.625-21.5 8.625Zm0 720q-12.825 0-21.325-8.62-8.5-8.63-8.5-21.38V886q0-12.75 8.675-21.375 8.676-8.625 21.5-8.625 12.825 0 21.325 8.625T510 886v100q0 12.75-8.675 21.38-8.676 8.62-21.5 8.62ZM240 378l-57-56q-9-9-8.629-21.603.37-12.604 8.526-21.5 8.896-8.897 21.5-8.897Q217 270 226 279l56 57q8 9 8 21t-8 20.5q-8 8.5-20.5 8.5t-21.5-8Zm494 495-56-57q-8-9-8-21.375T678.5 774q8.5-9 20.5-9t21 9l57 56q9 9 8.629 21.603-.37 12.604-8.526 21.5-8.896 8.897-21.5 8.897Q743 882 734 873Zm-56-495q-9-9-9-21t9-21l56-57q9-9 21.603-8.629 12.604.37 21.5 8.526 8.897 8.896 8.897 21.5Q786 313 777 322l-57 56q-8 8-20.364 8-12.363 0-21.636-8ZM182.897 873.103q-8.897-8.896-8.897-21.5Q174 839 183 830l57-56q8.8-9 20.9-9 12.1 0 20.709 9Q291 783 291 795t-9 21l-56 57q-9 9-21.603 8.629-12.604-.37-21.5-8.526ZM480 576Z" /> |
||||||
|
</svg> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,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> |
||||||
|
|
||||||
|
); |
||||||
|
}; |
@ -0,0 +1,23 @@ |
|||||||
|
import { IconTypes } from "./IconTypes"; |
||||||
|
|
||||||
|
export const TimesSVG: React.FC<IconTypes> = ({ |
||||||
|
color, |
||||||
|
height, |
||||||
|
width, |
||||||
|
className, |
||||||
|
onClickFunc |
||||||
|
}) => { |
||||||
|
return ( |
||||||
|
<svg |
||||||
|
onClick={onClickFunc} |
||||||
|
className={className} |
||||||
|
fill={color} |
||||||
|
xmlns="http://www.w3.org/2000/svg" |
||||||
|
height={height} |
||||||
|
viewBox="0 -960 960 960" |
||||||
|
width={width} |
||||||
|
> |
||||||
|
<path d="m249-207-42-42 231-231-231-231 42-42 231 231 231-231 42 42-231 231 231 231-42 42-231-231-231 231Z" /> |
||||||
|
</svg> |
||||||
|
); |
||||||
|
}; |
@ -0,0 +1,617 @@ |
|||||||
|
import React, { useEffect, useMemo, useState } from "react"; |
||||||
|
import { |
||||||
|
AddCoverImageButton, |
||||||
|
AddLogoIcon, |
||||||
|
CoverImagePreview, |
||||||
|
CrowdfundActionButton, |
||||||
|
CrowdfundActionButtonRow, |
||||||
|
CustomInputField, |
||||||
|
CustomSelect, |
||||||
|
LogoPreviewRow, |
||||||
|
ModalBody, |
||||||
|
NewCrowdfundTitle, |
||||||
|
StyledButton, |
||||||
|
TimesIcon, |
||||||
|
} from "./Upload-styles"; |
||||||
|
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 AddBoxIcon from "@mui/icons-material/AddBox"; |
||||||
|
import { useDropzone } from "react-dropzone"; |
||||||
|
|
||||||
|
import { setNotification } from "../../state/features/notificationsSlice"; |
||||||
|
import { objectToBase64, uint8ArrayToBase64 } from "../../utils/toBase64"; |
||||||
|
import { RootState } from "../../state/store"; |
||||||
|
import { |
||||||
|
upsertVideosBeginning, |
||||||
|
addToHashMap, |
||||||
|
upsertVideos, |
||||||
|
setEditVideo, |
||||||
|
updateVideo, |
||||||
|
updateInHashMap, |
||||||
|
setEditPlaylist, |
||||||
|
} from "../../state/features/videoSlice"; |
||||||
|
import ImageUploader from "../common/ImageUploader"; |
||||||
|
import { QTUBE_PLAYLIST_BASE, QTUBE_VIDEO_BASE, categories, subCategories } from "../../constants"; |
||||||
|
import { Playlists } from "../Playlists/Playlists"; |
||||||
|
import { PlaylistListEdit } from "../PlaylistListEdit/PlaylistListEdit"; |
||||||
|
import { TextEditor } from "../common/TextEditor/TextEditor"; |
||||||
|
import { extractTextFromHTML } from "../common/TextEditor/utils"; |
||||||
|
|
||||||
|
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.video.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 = categories.find( |
||||||
|
(option) => option.id === +editVideoProperties.category |
||||||
|
); |
||||||
|
setSelectedCategoryVideos(selectedOption || null); |
||||||
|
} |
||||||
|
|
||||||
|
if ( |
||||||
|
editVideoProperties?.category && |
||||||
|
editVideoProperties?.subcategory && |
||||||
|
subCategories[+editVideoProperties?.category] |
||||||
|
) { |
||||||
|
const selectedOption = subCategories[ |
||||||
|
+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 = `${QTUBE_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 = `${QTUBE_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: QTUBE_VIDEO_BASE, |
||||||
|
}; |
||||||
|
|
||||||
|
await qortalRequest(requestBodyJson); |
||||||
|
if(isNew){ |
||||||
|
const objectToStore = { |
||||||
|
title: title.slice(0, 50), |
||||||
|
description: metadescription, |
||||||
|
id: identifier, |
||||||
|
service: "PLAYLIST", |
||||||
|
name: username, |
||||||
|
...playlistObject |
||||||
|
} |
||||||
|
dispatch( |
||||||
|
updateVideo(objectToStore) |
||||||
|
); |
||||||
|
dispatch( |
||||||
|
updateInHashMap(objectToStore) |
||||||
|
); |
||||||
|
} else { |
||||||
|
dispatch( |
||||||
|
updateVideo({ |
||||||
|
...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 = categories.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} |
||||||
|
> |
||||||
|
{categories.map((option) => ( |
||||||
|
<MenuItem key={option.id} value={option.id}> |
||||||
|
{option.name} |
||||||
|
</MenuItem> |
||||||
|
))} |
||||||
|
</Select> |
||||||
|
</FormControl> |
||||||
|
{selectedCategoryVideos && |
||||||
|
subCategories[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, |
||||||
|
subCategories[selectedCategoryVideos?.id] |
||||||
|
) |
||||||
|
} |
||||||
|
> |
||||||
|
{subCategories[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> |
||||||
|
<CoverImagePreview src={coverImage} alt="logo" /> |
||||||
|
<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> |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
@ -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, |
||||||
|
}, |
||||||
|
})); |
@ -0,0 +1,579 @@ |
|||||||
|
import React, { useEffect, useState } from "react"; |
||||||
|
import { |
||||||
|
AddCoverImageButton, |
||||||
|
AddLogoIcon, |
||||||
|
CoverImagePreview, |
||||||
|
CrowdfundActionButton, |
||||||
|
CrowdfundActionButtonRow, |
||||||
|
CustomInputField, |
||||||
|
CustomSelect, |
||||||
|
LogoPreviewRow, |
||||||
|
ModalBody, |
||||||
|
NewCrowdfundTitle, |
||||||
|
StyledButton, |
||||||
|
TimesIcon, |
||||||
|
} from "./Upload-styles"; |
||||||
|
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 AddBoxIcon from "@mui/icons-material/AddBox"; |
||||||
|
import { useDropzone } from "react-dropzone"; |
||||||
|
|
||||||
|
import { setNotification } from "../../state/features/notificationsSlice"; |
||||||
|
import { objectToBase64, uint8ArrayToBase64 } from "../../utils/toBase64"; |
||||||
|
import { RootState } from "../../state/store"; |
||||||
|
import { |
||||||
|
upsertVideosBeginning, |
||||||
|
addToHashMap, |
||||||
|
upsertVideos, |
||||||
|
setEditVideo, |
||||||
|
updateVideo, |
||||||
|
updateInHashMap, |
||||||
|
} from "../../state/features/videoSlice"; |
||||||
|
import ImageUploader from "../common/ImageUploader"; |
||||||
|
import { QTUBE_VIDEO_BASE, categories, subCategories } from "../../constants"; |
||||||
|
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublish"; |
||||||
|
import { TextEditor } from "../common/TextEditor/TextEditor"; |
||||||
|
import { extractTextFromHTML } from "../common/TextEditor/utils"; |
||||||
|
|
||||||
|
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 EditVideo = () => { |
||||||
|
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.video.editVideoProperties |
||||||
|
); |
||||||
|
const [publishes, setPublishes] = useState<any[]>([]); |
||||||
|
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 [selectedCategoryVideos, setSelectedCategoryVideos] = |
||||||
|
useState<any>(null); |
||||||
|
const [selectedSubCategoryVideos, setSelectedSubCategoryVideos] = |
||||||
|
useState<any>(null); |
||||||
|
|
||||||
|
const { getRootProps, getInputProps } = useDropzone({ |
||||||
|
accept: { |
||||||
|
"video/*": [], |
||||||
|
}, |
||||||
|
maxFiles: 1, |
||||||
|
maxSize: 419430400, // 400 MB in bytes
|
||||||
|
onDrop: (acceptedFiles, rejectedFiles) => { |
||||||
|
const firstFile = acceptedFiles[0]; |
||||||
|
|
||||||
|
setFile(firstFile); |
||||||
|
|
||||||
|
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 (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]);
|
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (editVideoProperties) { |
||||||
|
setTitle(editVideoProperties?.title || ""); |
||||||
|
if(editVideoProperties?.htmlDescription){ |
||||||
|
setDescription(editVideoProperties?.htmlDescription); |
||||||
|
|
||||||
|
} else if(editVideoProperties?.fullDescription) { |
||||||
|
const paragraph = `<p>${editVideoProperties?.fullDescription}</p>` |
||||||
|
setDescription(paragraph); |
||||||
|
|
||||||
|
} |
||||||
|
setCoverImage(editVideoProperties?.videoImage || ""); |
||||||
|
|
||||||
|
if (editVideoProperties?.category) { |
||||||
|
const selectedOption = categories.find( |
||||||
|
(option) => option.id === +editVideoProperties.category |
||||||
|
); |
||||||
|
setSelectedCategoryVideos(selectedOption || null); |
||||||
|
} |
||||||
|
|
||||||
|
if ( |
||||||
|
editVideoProperties?.category && |
||||||
|
editVideoProperties?.subcategory && |
||||||
|
subCategories[+editVideoProperties?.category] |
||||||
|
) { |
||||||
|
const selectedOption = subCategories[ |
||||||
|
+editVideoProperties?.category |
||||||
|
]?.find((option) => option.id === +editVideoProperties.subcategory); |
||||||
|
setSelectedSubCategoryVideos(selectedOption || null); |
||||||
|
} |
||||||
|
} |
||||||
|
}, [editVideoProperties]); |
||||||
|
|
||||||
|
const onClose = () => { |
||||||
|
dispatch(setEditVideo(null)); |
||||||
|
setVideoPropertiesToSetToRedux(null); |
||||||
|
setFile(null); |
||||||
|
setTitle(""); |
||||||
|
setDescription(""); |
||||||
|
setCoverImage(""); |
||||||
|
}; |
||||||
|
|
||||||
|
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 (editVideoProperties?.user !== username) { |
||||||
|
errorMsg = "Cannot publish another user's resource"; |
||||||
|
} |
||||||
|
|
||||||
|
if (errorMsg) { |
||||||
|
dispatch( |
||||||
|
setNotification({ |
||||||
|
msg: errorMsg, |
||||||
|
alertType: "error", |
||||||
|
}) |
||||||
|
); |
||||||
|
return; |
||||||
|
} |
||||||
|
let listOfPublishes = []; |
||||||
|
const category = selectedCategoryVideos.id; |
||||||
|
const subcategory = selectedSubCategoryVideos?.id || ""; |
||||||
|
|
||||||
|
const fullDescription = extractTextFromHTML(description) |
||||||
|
let fileExtension = "mp4"; |
||||||
|
const fileExtensionSplit = file?.name?.split("."); |
||||||
|
if (fileExtensionSplit?.length > 1) { |
||||||
|
fileExtension = fileExtensionSplit?.pop() || "mp4"; |
||||||
|
} |
||||||
|
|
||||||
|
let filename = title.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, |
||||||
|
"" |
||||||
|
); |
||||||
|
|
||||||
|
const videoObject: any = { |
||||||
|
title, |
||||||
|
version: editVideoProperties.version, |
||||||
|
htmlDescription: description, |
||||||
|
fullDescription, |
||||||
|
videoImage: coverImage, |
||||||
|
videoReference: editVideoProperties.videoReference, |
||||||
|
commentsId: editVideoProperties.commentsId, |
||||||
|
category, |
||||||
|
subcategory, |
||||||
|
code: editVideoProperties.code, |
||||||
|
videoType: file?.type || "video/mp4", |
||||||
|
filename: `${alphanumericString.trim()}.${fileExtension}` |
||||||
|
}; |
||||||
|
|
||||||
|
let metadescription = |
||||||
|
`**category:${category};subcategory:${subcategory};code:${editVideoProperties.code}**` + |
||||||
|
description.slice(0, 150); |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const crowdfundObjectToBase64 = await objectToBase64(videoObject); |
||||||
|
// Description is obtained from raw data
|
||||||
|
const requestBodyJson: any = { |
||||||
|
action: "PUBLISH_QDN_RESOURCE", |
||||||
|
name: username, |
||||||
|
service: "DOCUMENT", |
||||||
|
data64: crowdfundObjectToBase64, |
||||||
|
title: title.slice(0, 50), |
||||||
|
description: metadescription, |
||||||
|
identifier: editVideoProperties.id, |
||||||
|
tag1: QTUBE_VIDEO_BASE, |
||||||
|
filename: `video_metadata.json`, |
||||||
|
}; |
||||||
|
listOfPublishes.push(requestBodyJson); |
||||||
|
|
||||||
|
if (file && editVideoProperties.videoReference?.identifier) { |
||||||
|
const requestBodyVideo: any = { |
||||||
|
action: "PUBLISH_QDN_RESOURCE", |
||||||
|
name: username, |
||||||
|
service: "VIDEO", |
||||||
|
file, |
||||||
|
title: title.slice(0, 50), |
||||||
|
description: metadescription, |
||||||
|
identifier: editVideoProperties.videoReference?.identifier, |
||||||
|
tag1: QTUBE_VIDEO_BASE, |
||||||
|
filename: `${alphanumericString.trim()}.${fileExtension}` |
||||||
|
}; |
||||||
|
|
||||||
|
listOfPublishes.push(requestBodyVideo); |
||||||
|
} |
||||||
|
|
||||||
|
setPublishes(listOfPublishes); |
||||||
|
setIsOpenMultiplePublish(true); |
||||||
|
setVideoPropertiesToSetToRedux({ |
||||||
|
...editVideoProperties, |
||||||
|
...videoObject, |
||||||
|
}); |
||||||
|
} 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 = categories.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); |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<Modal |
||||||
|
open={!!editVideoProperties} |
||||||
|
aria-labelledby="modal-title" |
||||||
|
aria-describedby="modal-description" |
||||||
|
> |
||||||
|
<ModalBody> |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: "flex", |
||||||
|
alignItems: "center", |
||||||
|
justifyContent: "space-between", |
||||||
|
}} |
||||||
|
> |
||||||
|
<NewCrowdfundTitle>Update Video properties</NewCrowdfundTitle> |
||||||
|
</Box> |
||||||
|
<> |
||||||
|
<Box |
||||||
|
{...getRootProps()} |
||||||
|
sx={{ |
||||||
|
border: "1px dashed gray", |
||||||
|
padding: 2, |
||||||
|
textAlign: "center", |
||||||
|
marginBottom: 2, |
||||||
|
cursor: "pointer", |
||||||
|
}} |
||||||
|
> |
||||||
|
<input {...getInputProps()} /> |
||||||
|
<Typography>Click to update video file - optional</Typography> |
||||||
|
</Box> |
||||||
|
<Typography |
||||||
|
sx={{ |
||||||
|
marginBottom: "10px", |
||||||
|
}} |
||||||
|
> |
||||||
|
{file?.name} |
||||||
|
</Typography> |
||||||
|
<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} |
||||||
|
> |
||||||
|
{categories.map((option) => ( |
||||||
|
<MenuItem key={option.id} value={option.id}> |
||||||
|
{option.name} |
||||||
|
</MenuItem> |
||||||
|
))} |
||||||
|
</Select> |
||||||
|
</FormControl> |
||||||
|
{selectedCategoryVideos && |
||||||
|
subCategories[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, |
||||||
|
subCategories[selectedCategoryVideos?.id] |
||||||
|
) |
||||||
|
} |
||||||
|
> |
||||||
|
{subCategories[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> |
||||||
|
<CoverImagePreview src={coverImage} alt="logo" /> |
||||||
|
<TimesIcon |
||||||
|
color={theme.palette.text.primary} |
||||||
|
onClickFunc={() => setCoverImage("")} |
||||||
|
height={"32"} |
||||||
|
width={"32"} |
||||||
|
></TimesIcon> |
||||||
|
</LogoPreviewRow> |
||||||
|
)} |
||||||
|
<CustomInputField |
||||||
|
name="title" |
||||||
|
label="Title of video" |
||||||
|
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 |
||||||
|
/> |
||||||
|
<Typography sx={{ |
||||||
|
fontSize: '18px' |
||||||
|
}}>Description of video</Typography> |
||||||
|
<TextEditor inlineContent={description} setInlineContent={(value)=> { |
||||||
|
setDescription(value) |
||||||
|
}} /> |
||||||
|
{/* <CustomInputField |
||||||
|
name="description" |
||||||
|
label="Describe your video in a few words" |
||||||
|
variant="filled" |
||||||
|
value={description} |
||||||
|
onChange={(e) => setDescription(e.target.value)} |
||||||
|
inputProps={{ maxLength: 10000 }} |
||||||
|
multiline |
||||||
|
maxRows={3} |
||||||
|
required |
||||||
|
/> */} |
||||||
|
</React.Fragment> |
||||||
|
</> |
||||||
|
|
||||||
|
<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} |
||||||
|
onSubmit={() => { |
||||||
|
setIsOpenMultiplePublish(false); |
||||||
|
const clonedCopy = structuredClone(videoPropertiesToSetToRedux); |
||||||
|
dispatch(updateVideo(clonedCopy)); |
||||||
|
dispatch(updateInHashMap(clonedCopy)); |
||||||
|
dispatch( |
||||||
|
setNotification({ |
||||||
|
msg: "Video updated", |
||||||
|
alertType: "success", |
||||||
|
}) |
||||||
|
); |
||||||
|
onClose(); |
||||||
|
}} |
||||||
|
publishes={publishes} |
||||||
|
/> |
||||||
|
)} |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
@ -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, |
||||||
|
}, |
||||||
|
})); |
@ -0,0 +1,210 @@ |
|||||||
|
import React, { useState } from "react"; |
||||||
|
import { CardContentContainerComment } from "../common/Comments/Comments-styles"; |
||||||
|
import { |
||||||
|
CrowdfundSubTitle, |
||||||
|
CrowdfundSubTitleRow, |
||||||
|
} from "../UploadVideo/Upload-styles"; |
||||||
|
import { Box, Button, Input, Typography, useTheme } from "@mui/material"; |
||||||
|
import { useNavigate } from "react-router-dom"; |
||||||
|
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; |
||||||
|
import { removeVideo } from "../../state/features/videoSlice"; |
||||||
|
import AddIcon from '@mui/icons-material/Add'; |
||||||
|
import { QTUBE_VIDEO_BASE } from "../../constants"; |
||||||
|
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=${QTUBE_VIDEO_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> |
||||||
|
|
||||||
|
); |
||||||
|
}; |
@ -0,0 +1,66 @@ |
|||||||
|
import React from 'react' |
||||||
|
import { CardContentContainerComment } from '../common/Comments/Comments-styles' |
||||||
|
import { CrowdfundSubTitle, CrowdfundSubTitleRow } from '../UploadVideo/Upload-styles' |
||||||
|
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> |
||||||
|
|
||||||
|
) |
||||||
|
} |
@ -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 |
@ -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, |
||||||
|
}, |
||||||
|
})); |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,28 @@ |
|||||||
|
import { styled } from '@mui/system'; |
||||||
|
import { |
||||||
|
Box, |
||||||
|
Modal, |
||||||
|
Typography |
||||||
|
} from '@mui/material'; |
||||||
|
|
||||||
|
export const StyledModal = styled(Modal)(({ theme }) => ({ |
||||||
|
display: 'flex', |
||||||
|
alignItems: 'center', |
||||||
|
justifyContent: 'center' |
||||||
|
})) |
||||||
|
|
||||||
|
export const ModalContent = styled(Box)(({ theme }) => ({ |
||||||
|
backgroundColor: theme.palette.primary.main, |
||||||
|
padding: theme.spacing(4), |
||||||
|
borderRadius: theme.spacing(1), |
||||||
|
width: '40%', |
||||||
|
'&:focus': { |
||||||
|
outline: 'none' |
||||||
|
} |
||||||
|
})) |
||||||
|
|
||||||
|
export const ModalText = styled(Typography)(({ theme }) => ({ |
||||||
|
fontFamily: "Raleway", |
||||||
|
fontSize: "25px", |
||||||
|
color: theme.palette.text.primary, |
||||||
|
})); |
@ -0,0 +1,100 @@ |
|||||||
|
import React, { useState } from "react"; |
||||||
|
import { |
||||||
|
Box, |
||||||
|
Button, |
||||||
|
Modal, |
||||||
|
Typography, |
||||||
|
SelectChangeEvent, |
||||||
|
ListItem, |
||||||
|
List, |
||||||
|
useTheme |
||||||
|
} from "@mui/material"; |
||||||
|
import { |
||||||
|
StyledModal, |
||||||
|
ModalContent, |
||||||
|
ModalText |
||||||
|
} from "./BlockedNamesModal-styles"; |
||||||
|
|
||||||
|
interface PostModalProps { |
||||||
|
open: boolean; |
||||||
|
onClose: () => void; |
||||||
|
} |
||||||
|
|
||||||
|
export const BlockedNamesModal: React.FC<PostModalProps> = ({ |
||||||
|
open, |
||||||
|
onClose |
||||||
|
}) => { |
||||||
|
const [blockedNames, setBlockedNames] = useState<string[]>([]); |
||||||
|
const theme = useTheme(); |
||||||
|
const getBlockedNames = React.useCallback(async () => { |
||||||
|
try { |
||||||
|
const listName = `blockedNames`; |
||||||
|
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,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> |
||||||
|
); |
||||||
|
}; |
@ -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 { COMMENT_BASE } from "../../../constants"; |
||||||
|
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 = `${COMMENT_BASE}${postId.slice(-12)}_base_${id}`; |
||||||
|
let idForNotification = identifier; |
||||||
|
|
||||||
|
if (isReply && commentId) { |
||||||
|
const removeBaseCommentId = commentId; |
||||||
|
removeBaseCommentId.replace("_base_", ""); |
||||||
|
identifier = `${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> |
||||||
|
); |
||||||
|
}; |
@ -0,0 +1,279 @@ |
|||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; |
||||||
|
import { CommentEditor } from "./CommentEditor"; |
||||||
|
import { Comment } from "./Comment"; |
||||||
|
import { Box, Button, CircularProgress, useTheme } from "@mui/material"; |
||||||
|
import { styled } from "@mui/system"; |
||||||
|
import { useSelector } from "react-redux"; |
||||||
|
import { RootState } from "../../../state/store"; |
||||||
|
import { useNavigate, useLocation } from "react-router-dom"; |
||||||
|
import { |
||||||
|
CommentContainer, |
||||||
|
CommentEditorContainer, |
||||||
|
CommentsContainer, |
||||||
|
LoadMoreCommentsButton, |
||||||
|
LoadMoreCommentsButtonRow, |
||||||
|
NoCommentsRow, |
||||||
|
} from "./Comments-styles"; |
||||||
|
import { COMMENT_BASE } from "../../../constants"; |
||||||
|
import { CrowdfundSubTitle, CrowdfundSubTitleRow } from "../../UploadVideo/Upload-styles"; |
||||||
|
|
||||||
|
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=${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=${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> |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
@ -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%", |
||||||
|
})); |
@ -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-tube-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-Tube 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> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,203 @@ |
|||||||
|
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' |
||||||
|
|
||||||
|
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( |
||||||
|
`/video/${downloadObj?.properties?.user}/${id}` |
||||||
|
) |
||||||
|
}} |
||||||
|
> |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
width: '100%', |
||||||
|
display: 'flex', |
||||||
|
alignItems: 'center', |
||||||
|
justifyContent: 'space-between' |
||||||
|
}} |
||||||
|
> |
||||||
|
<ListItemIcon> |
||||||
|
{service === 'VIDEO' && ( |
||||||
|
<Movie sx={{ color: theme.palette.text.primary }} /> |
||||||
|
)} |
||||||
|
</ListItemIcon> |
||||||
|
|
||||||
|
<Box |
||||||
|
sx={{ width: '100px', marginLeft: 1, marginRight: 1 }} |
||||||
|
> |
||||||
|
<LinearProgress |
||||||
|
variant="determinate" |
||||||
|
value={progress} |
||||||
|
sx={{ |
||||||
|
borderRadius: '5px', |
||||||
|
color: theme.palette.secondary.main |
||||||
|
}} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
<Typography |
||||||
|
sx={{ |
||||||
|
fontFamily: 'Arial', |
||||||
|
color: theme.palette.text.primary |
||||||
|
}} |
||||||
|
variant="caption" |
||||||
|
> |
||||||
|
{`${progress?.toFixed(0)}%`}{' '} |
||||||
|
{status && status === 'REFETCHING' && '- refetching'} |
||||||
|
{status && status === 'DOWNLOADED' && '- building'} |
||||||
|
</Typography> |
||||||
|
</Box> |
||||||
|
<Typography |
||||||
|
sx={{ |
||||||
|
fontSize: '10px', |
||||||
|
width: '100%', |
||||||
|
textAlign: 'end', |
||||||
|
fontFamily: 'Arial', |
||||||
|
color: theme.palette.text.primary, |
||||||
|
textOverflow: 'ellipsis', |
||||||
|
whiteSpace: 'nowrap', |
||||||
|
overflow: 'hidden', |
||||||
|
}} |
||||||
|
> |
||||||
|
{downloadObj?.identifier} |
||||||
|
</Typography> |
||||||
|
</ListItem> |
||||||
|
) |
||||||
|
})} |
||||||
|
</List> |
||||||
|
</Popover> |
||||||
|
|
||||||
|
</Box> |
||||||
|
|
||||||
|
) |
||||||
|
} |
@ -0,0 +1,433 @@ |
|||||||
|
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; |
||||||
|
} |
||||||
|
|
||||||
|
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, |
||||||
|
}: 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 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, |
||||||
|
}, |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
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(resourceStatus?.current === 'DOWNLOADED'){ |
||||||
|
refetch() |
||||||
|
} |
||||||
|
if(resourceStatus?.current === 'READY'){ |
||||||
|
clearInterval(interval); |
||||||
|
} |
||||||
|
|
||||||
|
}, 7500) |
||||||
|
} catch (error) { |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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", |
||||||
|
}) |
||||||
|
); |
||||||
|
} else if ( |
||||||
|
resourceStatus?.status === 'DOWNLOADED' && |
||||||
|
reDownload?.current === false |
||||||
|
) { |
||||||
|
refetchInInterval() |
||||||
|
reDownload.current = true |
||||||
|
} |
||||||
|
}, [resourceStatus, download]); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Box |
||||||
|
onClick={handlePlay} |
||||||
|
sx={{ |
||||||
|
width: "100%", |
||||||
|
overflow: "hidden", |
||||||
|
position: "relative", |
||||||
|
cursor: "pointer", |
||||||
|
...(customStyles || {}), |
||||||
|
}} |
||||||
|
> |
||||||
|
{children && ( |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: "flex", |
||||||
|
alignItems: "center", |
||||||
|
position: "relative", |
||||||
|
gap: "7px", |
||||||
|
}} |
||||||
|
> |
||||||
|
{children}{" "} |
||||||
|
{((resourceStatus.status && resourceStatus?.status !== "READY") || |
||||||
|
isLoading) && startedDownload ? ( |
||||||
|
<> |
||||||
|
<CircularProgress color="secondary" size={14} /> |
||||||
|
<Typography 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,89 @@ |
|||||||
|
import React, { useCallback } from 'react' |
||||||
|
import { Box, Button, TextField, Typography, Modal } from '@mui/material' |
||||||
|
import { |
||||||
|
useDropzone, |
||||||
|
DropzoneRootProps, |
||||||
|
DropzoneInputProps |
||||||
|
} from 'react-dropzone' |
||||||
|
import Compressor from 'compressorjs' |
||||||
|
|
||||||
|
const toBase64 = (file: File): Promise<string | ArrayBuffer | null> => |
||||||
|
new Promise((resolve, reject) => { |
||||||
|
const reader = new FileReader() |
||||||
|
reader.readAsDataURL(file) |
||||||
|
reader.onload = () => resolve(reader.result) |
||||||
|
reader.onerror = (error) => { |
||||||
|
reject(error) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
interface ImageUploaderProps { |
||||||
|
children: React.ReactNode |
||||||
|
onPick: (base64Img: string) => void |
||||||
|
} |
||||||
|
|
||||||
|
const ImageUploader: React.FC<ImageUploaderProps> = ({ children, onPick }) => { |
||||||
|
const onDrop = useCallback( |
||||||
|
async (acceptedFiles: File[]) => { |
||||||
|
if (acceptedFiles.length > 1) { |
||||||
|
return |
||||||
|
} |
||||||
|
let compressedFile: File | undefined |
||||||
|
|
||||||
|
try { |
||||||
|
const image = acceptedFiles[0] |
||||||
|
await new Promise<void>((resolve) => { |
||||||
|
new Compressor(image, { |
||||||
|
quality: 0.6, |
||||||
|
maxWidth: 1200, |
||||||
|
mimeType: 'image/webp', |
||||||
|
success(result) { |
||||||
|
const file = new File([result], 'name', { |
||||||
|
type: 'image/webp' |
||||||
|
}) |
||||||
|
compressedFile = file |
||||||
|
resolve() |
||||||
|
}, |
||||||
|
error(err) {} |
||||||
|
}) |
||||||
|
}) |
||||||
|
if (!compressedFile) return |
||||||
|
const base64Img = await toBase64(compressedFile) |
||||||
|
|
||||||
|
onPick(base64Img as string) |
||||||
|
} catch (error) { |
||||||
|
console.error(error) |
||||||
|
} |
||||||
|
}, |
||||||
|
[onPick] |
||||||
|
) |
||||||
|
|
||||||
|
const { |
||||||
|
getRootProps, |
||||||
|
getInputProps, |
||||||
|
isDragActive |
||||||
|
}: { |
||||||
|
getRootProps: () => DropzoneRootProps |
||||||
|
getInputProps: () => DropzoneInputProps |
||||||
|
isDragActive: boolean |
||||||
|
} = useDropzone({ |
||||||
|
onDrop, |
||||||
|
accept: { |
||||||
|
'image/*': [] |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
return ( |
||||||
|
<Box |
||||||
|
{...getRootProps()} |
||||||
|
sx={{ |
||||||
|
display: 'flex' |
||||||
|
}} |
||||||
|
> |
||||||
|
<input {...getInputProps()} /> |
||||||
|
{children} |
||||||
|
</Box> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default ImageUploader |
@ -0,0 +1,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 |
@ -0,0 +1,136 @@ |
|||||||
|
/* 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 { ModalBody } from "../../UploadVideo/Upload-styles"; |
||||||
|
import { CircleSVG } from "../../../assets/svgs/CircleSVG"; |
||||||
|
import { EmptyCircleSVG } from "../../../assets/svgs/EmptyCircleSVG"; |
||||||
|
|
||||||
|
export const MultiplePublish = ({ publishes, isOpen, onSubmit }) => { |
||||||
|
const theme = useTheme(); |
||||||
|
const listOfSuccessfulPublishesRef = useRef([]) |
||||||
|
const [listOfSuccessfulPublishes, setListOfSuccessfulPublishes] = useState< |
||||||
|
any[] |
||||||
|
>([]); |
||||||
|
const [currentlyInPublish, setCurrentlyInPublish] = useState(null); |
||||||
|
const hasStarted = useRef(false); |
||||||
|
const publish = useCallback(async (pub: any) => { |
||||||
|
await qortalRequest(pub); |
||||||
|
}, []); |
||||||
|
const [isPublishing, setIsPublishing] = useState(true) |
||||||
|
|
||||||
|
const handlePublish = useCallback( |
||||||
|
async (pub: any) => { |
||||||
|
try { |
||||||
|
setCurrentlyInPublish(pub?.identifier); |
||||||
|
|
||||||
|
await publish(pub); |
||||||
|
|
||||||
|
setListOfSuccessfulPublishes((prev: any) => [...prev, pub?.identifier]); |
||||||
|
listOfSuccessfulPublishesRef.current = [...listOfSuccessfulPublishesRef.current, pub?.identifier] |
||||||
|
} catch (error) { |
||||||
|
console.log({ error }); |
||||||
|
await new Promise<void>((res) => { |
||||||
|
setTimeout(() => { |
||||||
|
res(); |
||||||
|
}, 5000); |
||||||
|
}); |
||||||
|
// await handlePublish(pub);
|
||||||
|
} |
||||||
|
}, |
||||||
|
[publish] |
||||||
|
); |
||||||
|
|
||||||
|
const startPublish = useCallback( |
||||||
|
async (pubs: any) => { |
||||||
|
setIsPublishing(true) |
||||||
|
const filterPubs = pubs.filter((pub)=> !listOfSuccessfulPublishesRef.current.includes(pub.identifier)) |
||||||
|
for (const pub of filterPubs) { |
||||||
|
await handlePublish(pub); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
if(listOfSuccessfulPublishesRef.current.length === pubs.length){ |
||||||
|
onSubmit() |
||||||
|
} |
||||||
|
setIsPublishing(false) |
||||||
|
}, |
||||||
|
[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.map((publish: any) => { |
||||||
|
return ( |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: "flex", |
||||||
|
gap: "20px", |
||||||
|
justifyContent: "space-between", |
||||||
|
alignItems: "center", |
||||||
|
}} |
||||||
|
> |
||||||
|
<Typography>{publish?.title}</Typography> |
||||||
|
{publish?.identifier === currentlyInPublish ? ( |
||||||
|
<CircularProgress |
||||||
|
size={20} |
||||||
|
thickness={2} |
||||||
|
sx={{ |
||||||
|
color: theme.palette.secondary.main, |
||||||
|
}} |
||||||
|
/> |
||||||
|
) : listOfSuccessfulPublishes.includes(publish.identifier) ? ( |
||||||
|
<CircleSVG |
||||||
|
color={theme.palette.text.primary} |
||||||
|
height="24px" |
||||||
|
width="24px" |
||||||
|
/> |
||||||
|
) : ( |
||||||
|
<EmptyCircleSVG |
||||||
|
color={theme.palette.text.primary} |
||||||
|
height="24px" |
||||||
|
width="24px" |
||||||
|
/> |
||||||
|
)} |
||||||
|
</Box> |
||||||
|
); |
||||||
|
})} |
||||||
|
{!isPublishing && listOfSuccessfulPublishes.length !== publishes.length && ( |
||||||
|
<> |
||||||
|
<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 onClick={()=> { |
||||||
|
startPublish(publishes) |
||||||
|
}}>Try again</Button> |
||||||
|
</> |
||||||
|
)} |
||||||
|
|
||||||
|
</ModalBody> |
||||||
|
</Modal> |
||||||
|
); |
||||||
|
}; |
@ -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; |
@ -0,0 +1,25 @@ |
|||||||
|
import React, { useEffect, useState } from 'react' |
||||||
|
import { createPortal } from 'react-dom' |
||||||
|
|
||||||
|
interface PortalProps { |
||||||
|
children: React.ReactNode |
||||||
|
} |
||||||
|
|
||||||
|
const Portal: React.FC<PortalProps> = ({ children }) => { |
||||||
|
const [mounted, setMounted] = useState(false) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
setMounted(true) |
||||||
|
|
||||||
|
return () => setMounted(false) |
||||||
|
}, []) |
||||||
|
|
||||||
|
return mounted |
||||||
|
? createPortal( |
||||||
|
children, |
||||||
|
document.querySelector('#modal-root') as HTMLElement |
||||||
|
) |
||||||
|
: null |
||||||
|
} |
||||||
|
|
||||||
|
export default Portal |
@ -0,0 +1,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> |
||||||
|
); |
||||||
|
}; |
@ -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} |
||||||
|
/> |
||||||
|
); |
||||||
|
}; |
@ -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); |
||||||
|
} |
@ -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> |
||||||
|
) |
||||||
|
} |
@ -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> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,125 @@ |
|||||||
|
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: '55px' |
||||||
|
})); |
||||||
|
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))" |
||||||
|
} |
||||||
|
})); |
@ -0,0 +1,443 @@ |
|||||||
|
import React, { useState, useRef } from "react"; |
||||||
|
import { Box, Button, Input, Popover, useTheme } from "@mui/material"; |
||||||
|
import ExitToAppIcon from "@mui/icons-material/ExitToApp"; |
||||||
|
import { BlockedNamesModal } from "../../common/BlockedNamesModal/BlockedNamesModal"; |
||||||
|
import AddBoxIcon from "@mui/icons-material/AddBox"; |
||||||
|
|
||||||
|
import { |
||||||
|
AvatarContainer, |
||||||
|
CustomAppBar, |
||||||
|
DropdownContainer, |
||||||
|
DropdownText, |
||||||
|
AuthenticateButton, |
||||||
|
NavbarName, |
||||||
|
LightModeIcon, |
||||||
|
DarkModeIcon, |
||||||
|
ThemeSelectRow, |
||||||
|
LogoContainer, |
||||||
|
} 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 Logo from "../../../assets/img/logo.png"; |
||||||
|
import { useDispatch, useSelector } from "react-redux"; |
||||||
|
import { |
||||||
|
addFilteredVideos, |
||||||
|
setEditPlaylist, |
||||||
|
setFilterValue, |
||||||
|
setIsFiltering, |
||||||
|
} from "../../../state/features/videoSlice"; |
||||||
|
import { RootState } from "../../../state/store"; |
||||||
|
import { useWindowSize } from "../../../hooks/useWindowSize"; |
||||||
|
import { UploadVideo } from "../../UploadVideo/UploadVideo"; |
||||||
|
import { StyledButton } from "../../UploadVideo/Upload-styles"; |
||||||
|
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.video.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> |
||||||
|
<LogoContainer |
||||||
|
onClick={() => { |
||||||
|
navigate("/"); |
||||||
|
dispatch(setIsFiltering(false)); |
||||||
|
dispatch(setFilterValue("")); |
||||||
|
dispatch(addFilteredVideos([])); |
||||||
|
searchValRef.current = ""; |
||||||
|
if (!inputRef.current) return; |
||||||
|
inputRef.current.value = ""; |
||||||
|
}} |
||||||
|
> |
||||||
|
<img |
||||||
|
src={Logo} |
||||||
|
style={{ |
||||||
|
width: "auto", |
||||||
|
height: "55px", |
||||||
|
padding: "2px", |
||||||
|
}} |
||||||
|
/> |
||||||
|
</LogoContainer> |
||||||
|
</ThemeSelectRow> |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: "flex", |
||||||
|
alignItems: "center", |
||||||
|
gap: "10px", |
||||||
|
}} |
||||||
|
> |
||||||
|
{/* {windowSize.width <= 600 ? ( |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: 'flex', |
||||||
|
alignItems: 'center', |
||||||
|
gap: 1 |
||||||
|
}} |
||||||
|
className="myClassOver600" |
||||||
|
|
||||||
|
|
||||||
|
> |
||||||
|
<Box onClick={openNotificationPopover}> |
||||||
|
<SearchIcon |
||||||
|
sx={{ |
||||||
|
cursor: 'pointer', |
||||||
|
display: 'flex' |
||||||
|
}} |
||||||
|
|
||||||
|
/> |
||||||
|
</Box> |
||||||
|
{filterValue && ( |
||||||
|
<BackspaceIcon |
||||||
|
sx={{ |
||||||
|
cursor: 'pointer' |
||||||
|
}} |
||||||
|
onClick={() => { |
||||||
|
dispatch(setIsFiltering(false)) |
||||||
|
dispatch(setFilterValue('')) |
||||||
|
dispatch(addFilteredVideos([])) |
||||||
|
searchValRef.current = '' |
||||||
|
if (!inputRef.current) return |
||||||
|
inputRef.current.value = '' |
||||||
|
}} |
||||||
|
/> |
||||||
|
)} |
||||||
|
|
||||||
|
</Box> |
||||||
|
): ( |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: 'flex', |
||||||
|
alignItems: 'center', |
||||||
|
gap: 1 |
||||||
|
}} |
||||||
|
className="myClassUnder600" |
||||||
|
> |
||||||
|
<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(addFilteredVideos([])) |
||||||
|
searchValRef.current = '' |
||||||
|
if (!inputRef.current) return |
||||||
|
inputRef.current.value = '' |
||||||
|
return |
||||||
|
} |
||||||
|
navigate('/') |
||||||
|
dispatch(setIsFiltering(true)) |
||||||
|
dispatch(addFilteredVideos([])) |
||||||
|
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(addFilteredVideos([])) |
||||||
|
searchValRef.current = '' |
||||||
|
if (!inputRef.current) return |
||||||
|
inputRef.current.value = '' |
||||||
|
return |
||||||
|
} |
||||||
|
navigate('/') |
||||||
|
dispatch(setIsFiltering(true)) |
||||||
|
dispatch(addFilteredVideos([])) |
||||||
|
dispatch(setFilterValue(searchValRef.current)) |
||||||
|
}} |
||||||
|
/> |
||||||
|
{filterValue && ( |
||||||
|
<BackspaceIcon |
||||||
|
sx={{ |
||||||
|
cursor: 'pointer' |
||||||
|
}} |
||||||
|
onClick={() => { |
||||||
|
dispatch(setIsFiltering(false)) |
||||||
|
dispatch(setFilterValue('')) |
||||||
|
dispatch(addFilteredVideos([])) |
||||||
|
searchValRef.current = '' |
||||||
|
if (!inputRef.current) return |
||||||
|
inputRef.current.value = '' |
||||||
|
}} |
||||||
|
/> |
||||||
|
)} |
||||||
|
|
||||||
|
</Box> |
||||||
|
)} */} |
||||||
|
|
||||||
|
<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(addFilteredVideos([])); |
||||||
|
searchValRef.current = ""; |
||||||
|
if (!inputRef.current) return; |
||||||
|
inputRef.current.value = ""; |
||||||
|
return; |
||||||
|
} |
||||||
|
navigate("/"); |
||||||
|
dispatch(setIsFiltering(true)); |
||||||
|
dispatch(addFilteredVideos([])); |
||||||
|
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(addFilteredVideos([])); |
||||||
|
searchValRef.current = ""; |
||||||
|
if (!inputRef.current) return; |
||||||
|
inputRef.current.value = ""; |
||||||
|
return; |
||||||
|
} |
||||||
|
navigate("/"); |
||||||
|
dispatch(setIsFiltering(true)); |
||||||
|
dispatch(addFilteredVideos([])); |
||||||
|
dispatch(setFilterValue(searchValRef.current)); |
||||||
|
}} |
||||||
|
/> |
||||||
|
<BackspaceIcon |
||||||
|
sx={{ |
||||||
|
cursor: "pointer", |
||||||
|
}} |
||||||
|
onClick={() => { |
||||||
|
dispatch(setIsFiltering(false)); |
||||||
|
dispatch(setFilterValue("")); |
||||||
|
dispatch(addFilteredVideos([])); |
||||||
|
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 && ( |
||||||
|
<> |
||||||
|
<UploadVideo /> |
||||||
|
<StyledButton |
||||||
|
color="primary" |
||||||
|
startIcon={<AddBoxIcon />} |
||||||
|
onClick={() => { |
||||||
|
dispatch(setEditPlaylist({mode: 'new'})) |
||||||
|
}} |
||||||
|
> |
||||||
|
create playlist |
||||||
|
</StyledButton> |
||||||
|
</> |
||||||
|
)} |
||||||
|
|
||||||
|
|
||||||
|
</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; |
@ -0,0 +1,89 @@ |
|||||||
|
const useTestIdentifiers = false; |
||||||
|
|
||||||
|
export const QTUBE_VIDEO_BASE = useTestIdentifiers |
||||||
|
? "MYTEST_vid_" |
||||||
|
: "qtube_vid_"; |
||||||
|
|
||||||
|
export const QTUBE_PLAYLIST_BASE = useTestIdentifiers |
||||||
|
? "MYTEST_playlist_" |
||||||
|
: "qtube_playlist_"; |
||||||
|
|
||||||
|
export const COMMENT_BASE = useTestIdentifiers |
||||||
|
? "qcomment_v1_MYTEST_" |
||||||
|
: "qcomment_v1_qtube_"; |
||||||
|
|
||||||
|
interface SubCategory { |
||||||
|
id: number; |
||||||
|
name: string; |
||||||
|
} |
||||||
|
|
||||||
|
interface CategoryMap { |
||||||
|
[key: number]: SubCategory[]; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
export const categories = [ |
||||||
|
{"id": 1, "name": "Movies"}, |
||||||
|
{"id": 2, "name": "Series"}, |
||||||
|
{"id": 3, "name": "Music"}, |
||||||
|
{"id": 4, "name": "Education"}, |
||||||
|
{"id": 5, "name": "Lifestyle"}, |
||||||
|
{"id": 6, "name": "Gaming"}, |
||||||
|
{"id": 7, "name": "Technology"}, |
||||||
|
{"id": 8, "name": "Sports"}, |
||||||
|
{"id": 9, "name": "News & Politics"}, |
||||||
|
{"id": 10, "name": "Cooking & Food"}, |
||||||
|
{"id": 11, "name": "Animation"}, |
||||||
|
{"id": 12, "name": "Science"}, |
||||||
|
{"id": 13, "name": "Health & Wellness"}, |
||||||
|
{"id": 14, "name": "DIY & Crafts"}, |
||||||
|
{"id": 15, "name": "Kids & Family"}, |
||||||
|
{"id": 16, "name": "Comedy"}, |
||||||
|
{"id": 17, "name": "Travel & Adventure"}, |
||||||
|
{"id": 18, "name": "Art & Design"}, |
||||||
|
{"id": 19, "name": "Nature & Environment"}, |
||||||
|
{"id": 20, "name": "Business & Finance"}, |
||||||
|
{"id": 21, "name": "Personal Development"}, |
||||||
|
{"id": 22, "name": "Other"}, |
||||||
|
{"id": 23, "name": "History"}, |
||||||
|
] |
||||||
|
|
||||||
|
|
||||||
|
export const subCategories: CategoryMap = { |
||||||
|
1: [ // Movies
|
||||||
|
{"id": 101, "name": "Action & Adventure"}, |
||||||
|
{"id": 102, "name": "Comedy"}, |
||||||
|
{"id": 103, "name": "Drama"}, |
||||||
|
{"id": 104, "name": "Fantasy & Science Fiction"}, |
||||||
|
{"id": 105, "name": "Horror & Thriller"}, |
||||||
|
{"id": 106, "name": "Documentaries"}, |
||||||
|
{"id": 107, "name": "Animated"}, |
||||||
|
{"id": 108, "name": "Family & Kids"}, |
||||||
|
{"id": 109, "name": "Romance"}, |
||||||
|
{"id": 110, "name": "Mystery & Crime"}, |
||||||
|
{"id": 111, "name": "Historical & War"}, |
||||||
|
{"id": 112, "name": "Musicals & Music Films"}, |
||||||
|
{"id": 113, "name": "Indie Films"}, |
||||||
|
{"id": 114, "name": "International Films"}, |
||||||
|
{"id": 115, "name": "Biographies & True Stories"}, |
||||||
|
{"id": 116, "name": "Other"} |
||||||
|
], |
||||||
|
2: [ // Series
|
||||||
|
{"id": 201, "name": "Dramas"}, |
||||||
|
{"id": 202, "name": "Comedies"}, |
||||||
|
{"id": 203, "name": "Reality & Competition"}, |
||||||
|
{"id": 204, "name": "Documentaries & Docuseries"}, |
||||||
|
{"id": 205, "name": "Sci-Fi & Fantasy"}, |
||||||
|
{"id": 206, "name": "Crime & Mystery"}, |
||||||
|
{"id": 207, "name": "Animated Series"}, |
||||||
|
{"id": 208, "name": "Kids & Family"}, |
||||||
|
{"id": 209, "name": "Historical & Period Pieces"}, |
||||||
|
{"id": 210, "name": "Action & Adventure"}, |
||||||
|
{"id": 211, "name": "Horror & Thriller"}, |
||||||
|
{"id": 212, "name": "Romance"}, |
||||||
|
{"id": 213, "name": "Anthologies"}, |
||||||
|
{"id": 214, "name": "International Series"}, |
||||||
|
{"id": 215, "name": "Miniseries"}, |
||||||
|
{"id": 216, "name": "Other"} |
||||||
|
] |
||||||
|
} |
@ -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> |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,377 @@ |
|||||||
|
import React from 'react' |
||||||
|
import { useDispatch, useSelector } from 'react-redux' |
||||||
|
import { |
||||||
|
addVideos, |
||||||
|
addToHashMap, |
||||||
|
setCountNewVideos, |
||||||
|
upsertVideos, |
||||||
|
upsertVideosBeginning, |
||||||
|
Video, |
||||||
|
upsertFilteredVideos |
||||||
|
} from '../state/features/videoSlice' |
||||||
|
import { |
||||||
|
setIsLoadingGlobal, setUserAvatarHash |
||||||
|
} from '../state/features/globalSlice' |
||||||
|
import { RootState } from '../state/store' |
||||||
|
import { fetchAndEvaluateVideos } from '../utils/fetchVideos' |
||||||
|
import { QTUBE_PLAYLIST_BASE, QTUBE_VIDEO_BASE } from '../constants' |
||||||
|
import { RequestQueue } from '../utils/queue' |
||||||
|
import { queue } from '../wrappers/GlobalWrapper' |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const useFetchVideos = () => { |
||||||
|
const dispatch = useDispatch() |
||||||
|
const hashMapVideos = useSelector( |
||||||
|
(state: RootState) => state.video.hashMapVideos |
||||||
|
) |
||||||
|
const videos = useSelector((state: RootState) => state.video.videos) |
||||||
|
const userAvatarHash = useSelector( |
||||||
|
(state: RootState) => state.global.userAvatarHash |
||||||
|
) |
||||||
|
const filteredVideos = useSelector( |
||||||
|
(state: RootState) => state.video.filteredVideos |
||||||
|
) |
||||||
|
|
||||||
|
const checkAndUpdateVideo = React.useCallback( |
||||||
|
(video: Video) => { |
||||||
|
const existingVideo = hashMapVideos[video.id] |
||||||
|
if (!existingVideo) { |
||||||
|
return true |
||||||
|
} else if ( |
||||||
|
video?.updated && |
||||||
|
existingVideo?.updated && |
||||||
|
(!existingVideo?.updated || video?.updated) > existingVideo?.updated |
||||||
|
) { |
||||||
|
return true |
||||||
|
} else { |
||||||
|
return false |
||||||
|
} |
||||||
|
}, |
||||||
|
[hashMapVideos] |
||||||
|
) |
||||||
|
|
||||||
|
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 getVideo = 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(() => getVideo(user, videoId, content, retries + 1)); |
||||||
|
} else { |
||||||
|
console.error('Failed to get video after 3 attempts', error); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const getNewVideos = React.useCallback(async () => { |
||||||
|
try { |
||||||
|
dispatch(setIsLoadingGlobal(true)) |
||||||
|
|
||||||
|
|
||||||
|
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_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(upsertVideosBeginning(structureData)) |
||||||
|
} |
||||||
|
if (willFetchAll) { |
||||||
|
dispatch(addVideos(structureData)) |
||||||
|
} |
||||||
|
setTimeout(()=> { |
||||||
|
dispatch(setCountNewVideos(0)) |
||||||
|
}, 1000) |
||||||
|
for (const content of structureData) { |
||||||
|
if (content.user && content.id) { |
||||||
|
const res = checkAndUpdateVideo(content) |
||||||
|
if (res) { |
||||||
|
queue.push(() => getVideo(content.user, content.id, content)); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
} finally { |
||||||
|
dispatch(setIsLoadingGlobal(false)) |
||||||
|
} |
||||||
|
}, [videos, hashMapVideos]) |
||||||
|
|
||||||
|
const getVideos = React.useCallback(async (filters = {}, reset?:boolean, resetFilers?: boolean,limit?: number) => { |
||||||
|
try { |
||||||
|
const {name = '',
|
||||||
|
category = '',
|
||||||
|
subcategory = '',
|
||||||
|
keywords = '',
|
||||||
|
type = '' }: any = resetFilers ? {} : filters |
||||||
|
let offset = videos.length |
||||||
|
if(reset){ |
||||||
|
offset = 0 |
||||||
|
} |
||||||
|
const videoLimit = limit || 20 |
||||||
|
|
||||||
|
let defaultUrl = `/arbitrary/resources/search?mode=ALL&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}&limit=${videoLimit}` |
||||||
|
|
||||||
|
|
||||||
|
if(name){ |
||||||
|
defaultUrl = defaultUrl + `&name=${name}` |
||||||
|
} |
||||||
|
if(category){ |
||||||
|
if(!subcategory){ |
||||||
|
defaultUrl = defaultUrl + `&description=category:${category}` |
||||||
|
|
||||||
|
} else { |
||||||
|
defaultUrl = defaultUrl + `&description=category:${category};subcategory:${subcategory}` |
||||||
|
} |
||||||
|
} |
||||||
|
if(keywords){ |
||||||
|
defaultUrl = defaultUrl + `&query=${keywords}` |
||||||
|
} |
||||||
|
if(type === 'playlists'){ |
||||||
|
defaultUrl = defaultUrl + `&service=PLAYLIST` |
||||||
|
defaultUrl = defaultUrl + `&identifier=${QTUBE_PLAYLIST_BASE}` |
||||||
|
|
||||||
|
} else { |
||||||
|
defaultUrl = defaultUrl + `&service=DOCUMENT` |
||||||
|
defaultUrl = defaultUrl + `&identifier=${QTUBE_VIDEO_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(addVideos(structureData)) |
||||||
|
|
||||||
|
} else { |
||||||
|
dispatch(upsertVideos(structureData)) |
||||||
|
|
||||||
|
} |
||||||
|
for (const content of structureData) { |
||||||
|
if (content.user && content.id) { |
||||||
|
const res = checkAndUpdateVideo(content) |
||||||
|
if (res) { |
||||||
|
queue.push(() => getVideo(content.user, content.id, content)); |
||||||
|
|
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
console.log({error}) |
||||||
|
} finally { |
||||||
|
|
||||||
|
} |
||||||
|
}, [videos, hashMapVideos]) |
||||||
|
|
||||||
|
const getVideosFiltered = 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=${QTUBE_VIDEO_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(upsertFilteredVideos(structureData)) |
||||||
|
|
||||||
|
for (const content of structureData) { |
||||||
|
if (content.user && content.id) { |
||||||
|
const res = checkAndUpdateVideo(content) |
||||||
|
if (res) { |
||||||
|
queue.push(() => getVideo(content.user, content.id, content)); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
} finally { |
||||||
|
|
||||||
|
} |
||||||
|
}, [filteredVideos, hashMapVideos]) |
||||||
|
|
||||||
|
const checkNewVideos = React.useCallback(async () => { |
||||||
|
try { |
||||||
|
|
||||||
|
|
||||||
|
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_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(setCountNewVideos(responseData.length)) |
||||||
|
return |
||||||
|
} |
||||||
|
const newArray = responseData.slice(0, findVideo) |
||||||
|
dispatch(setCountNewVideos(newArray.length)) |
||||||
|
return |
||||||
|
} catch (error) {} |
||||||
|
}, [videos]) |
||||||
|
|
||||||
|
|
||||||
|
return { |
||||||
|
getVideos, |
||||||
|
checkAndUpdateVideo, |
||||||
|
getVideo, |
||||||
|
hashMapVideos, |
||||||
|
getNewVideos, |
||||||
|
checkNewVideos, |
||||||
|
getVideosFiltered |
||||||
|
} |
||||||
|
} |
@ -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; |
||||||
|
} |
@ -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; |
||||||
|
} |
@ -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> |
||||||
|
) |
@ -0,0 +1,77 @@ |
|||||||
|
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 { useFetchVideos } from '../../hooks/useFetchVideos' |
||||||
|
import LazyLoad from '../../components/common/LazyLoad' |
||||||
|
import { BottomParent, NameContainer, VideoCard, VideoCardName, VideoCardTitle, VideoContainer, VideoUploadDate } from './VideoList-styles' |
||||||
|
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' |
||||||
|
}}> |
||||||
|
<VideoContainer> |
||||||
|
{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> |
||||||
|
) |
||||||
|
})} |
||||||
|
</VideoContainer> |
||||||
|
</Box> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
|
@ -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;" |
||||||
|
} |
||||||
|
})); |
@ -0,0 +1,15 @@ |
|||||||
|
import React from 'react' |
||||||
|
import { VideoList } from './VideoList' |
||||||
|
|
||||||
|
import { useSelector } from 'react-redux' |
||||||
|
import { RootState } from '../../state/store' |
||||||
|
|
||||||
|
export const Home = () => { |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<VideoList /> |
||||||
|
</> |
||||||
|
|
||||||
|
) |
||||||
|
} |
@ -0,0 +1,284 @@ |
|||||||
|
import { styled } from "@mui/system"; |
||||||
|
import { Box, Grid, Typography, Checkbox, TextField, InputLabel, Autocomplete } from "@mui/material"; |
||||||
|
|
||||||
|
export const VideoContainer = styled(Grid)(({ 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", |
||||||
|
marginBottom: 'auto' |
||||||
|
})); |
||||||
|
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: '12px', |
||||||
|
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: '5px', |
||||||
|
borderRadius: '3px', |
||||||
|
transition: 'all 0.3s ease-in-out', |
||||||
|
"&:hover": { |
||||||
|
cursor: 'pointer', |
||||||
|
transform: "scale(1.1)", |
||||||
|
} |
||||||
|
}) |
@ -0,0 +1,792 @@ |
|||||||
|
import React, { useCallback, useEffect, useRef, useState } from "react"; |
||||||
|
import { useNavigate } from "react-router-dom"; |
||||||
|
import ReactDOM from "react-dom"; |
||||||
|
import { useSelector, useDispatch } from "react-redux"; |
||||||
|
import { RootState } from "../../state/store"; |
||||||
|
import { |
||||||
|
Avatar, |
||||||
|
Box, |
||||||
|
Button, |
||||||
|
FormControl, |
||||||
|
Grid, |
||||||
|
Input, |
||||||
|
InputLabel, |
||||||
|
MenuItem, |
||||||
|
OutlinedInput, |
||||||
|
Select, |
||||||
|
SelectChangeEvent, |
||||||
|
Tooltip, |
||||||
|
Typography, |
||||||
|
useTheme, |
||||||
|
} from "@mui/material"; |
||||||
|
import { useFetchVideos } from "../../hooks/useFetchVideos"; |
||||||
|
import LazyLoad from "../../components/common/LazyLoad"; |
||||||
|
import { |
||||||
|
BlockIconContainer, |
||||||
|
BottomParent, |
||||||
|
FilterSelect, |
||||||
|
FiltersCheckbox, |
||||||
|
FiltersCol, |
||||||
|
FiltersContainer, |
||||||
|
FiltersRow, |
||||||
|
FiltersSubContainer, |
||||||
|
FiltersTitle, |
||||||
|
IconsBox, |
||||||
|
NameContainer, |
||||||
|
VideoCard, |
||||||
|
VideoCardName, |
||||||
|
VideoCardTitle, |
||||||
|
VideoContainer, |
||||||
|
VideoUploadDate, |
||||||
|
} from "./VideoList-styles"; |
||||||
|
import ResponsiveImage from "../../components/ResponsiveImage"; |
||||||
|
import { formatDate, formatTimestampSeconds } from "../../utils/time"; |
||||||
|
import { Subtitle, SubtitleContainer } from "./Home-styles"; |
||||||
|
import { ExpandMoreSVG } from "../../assets/svgs/ExpandMoreSVG"; |
||||||
|
import { |
||||||
|
addVideos, |
||||||
|
blockUser, |
||||||
|
changeFilterType, |
||||||
|
changeSelectedCategoryVideos, |
||||||
|
changeSelectedSubCategoryVideos, |
||||||
|
changefilterName, |
||||||
|
changefilterSearch, |
||||||
|
clearVideoList, |
||||||
|
setEditPlaylist, |
||||||
|
setEditVideo, |
||||||
|
} from "../../state/features/videoSlice"; |
||||||
|
import { categories, subCategories } from "../../constants"; |
||||||
|
import { Playlists } from "../../components/Playlists/Playlists"; |
||||||
|
import { PlaylistSVG } from "../../assets/svgs/PlaylistSVG"; |
||||||
|
import BlockIcon from "@mui/icons-material/Block"; |
||||||
|
import EditIcon from '@mui/icons-material/Edit'; |
||||||
|
|
||||||
|
interface VideoListProps { |
||||||
|
mode?: string; |
||||||
|
} |
||||||
|
export const VideoList = ({ mode }: VideoListProps) => { |
||||||
|
const theme = useTheme(); |
||||||
|
const prevVal = useRef(""); |
||||||
|
const isFiltering = useSelector( |
||||||
|
(state: RootState) => state.video.isFiltering |
||||||
|
); |
||||||
|
const filterValue = useSelector( |
||||||
|
(state: RootState) => state.video.filterValue |
||||||
|
); |
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false); |
||||||
|
const [showIcons, setShowIcons] = useState(null); |
||||||
|
|
||||||
|
const filterType = useSelector((state: RootState) => state.video.filterType); |
||||||
|
|
||||||
|
const setFilterType = (payload) => { |
||||||
|
dispatch(changeFilterType(payload)); |
||||||
|
}; |
||||||
|
const filterSearch = useSelector( |
||||||
|
(state: RootState) => state.video.filterSearch |
||||||
|
); |
||||||
|
|
||||||
|
const setFilterSearch = (payload) => { |
||||||
|
dispatch(changefilterSearch(payload)); |
||||||
|
}; |
||||||
|
const filterName = useSelector((state: RootState) => state.video.filterName); |
||||||
|
|
||||||
|
const setFilterName = (payload) => { |
||||||
|
dispatch(changefilterName(payload)); |
||||||
|
}; |
||||||
|
|
||||||
|
const selectedCategoryVideos = useSelector( |
||||||
|
(state: RootState) => state.video.selectedCategoryVideos |
||||||
|
); |
||||||
|
|
||||||
|
const setSelectedCategoryVideos = (payload) => { |
||||||
|
dispatch(changeSelectedCategoryVideos(payload)); |
||||||
|
}; |
||||||
|
const selectedSubCategoryVideos = useSelector( |
||||||
|
(state: RootState) => state.video.selectedSubCategoryVideos |
||||||
|
); |
||||||
|
|
||||||
|
const setSelectedSubCategoryVideos = (payload) => { |
||||||
|
dispatch(changeSelectedSubCategoryVideos(payload)); |
||||||
|
}; |
||||||
|
|
||||||
|
const dispatch = useDispatch(); |
||||||
|
const filteredVideos = useSelector( |
||||||
|
(state: RootState) => state.video.filteredVideos |
||||||
|
); |
||||||
|
const username = useSelector((state: RootState) => state.auth?.user?.name); |
||||||
|
const isFilterMode = useRef(false); |
||||||
|
const firstFetch = useRef(false); |
||||||
|
const afterFetch = useRef(false); |
||||||
|
const isFetchingFiltered = useRef(false); |
||||||
|
const isFetching = useRef(false); |
||||||
|
const hashMapVideos = useSelector( |
||||||
|
(state: RootState) => state.video.hashMapVideos |
||||||
|
); |
||||||
|
|
||||||
|
const countNewVideos = useSelector( |
||||||
|
(state: RootState) => state.video.countNewVideos |
||||||
|
); |
||||||
|
const userAvatarHash = useSelector( |
||||||
|
(state: RootState) => state.global.userAvatarHash |
||||||
|
); |
||||||
|
|
||||||
|
const { videos: globalVideos } = useSelector( |
||||||
|
(state: RootState) => state.video |
||||||
|
); |
||||||
|
const navigate = useNavigate(); |
||||||
|
const { getVideos, getNewVideos, checkNewVideos, getVideosFiltered } = |
||||||
|
useFetchVideos(); |
||||||
|
|
||||||
|
const getVideosHandler = React.useCallback( |
||||||
|
async (reset?: boolean, resetFilers?: boolean) => { |
||||||
|
|
||||||
|
|
||||||
|
if (!firstFetch.current || !afterFetch.current) return; |
||||||
|
if (isFetching.current) return; |
||||||
|
isFetching.current = true; |
||||||
|
await getVideos( |
||||||
|
{ |
||||||
|
name: filterName, |
||||||
|
category: selectedCategoryVideos?.id, |
||||||
|
subcategory: selectedSubCategoryVideos?.id, |
||||||
|
keywords: filterSearch, |
||||||
|
type: filterType, |
||||||
|
}, |
||||||
|
reset ? true : false, |
||||||
|
resetFilers |
||||||
|
); |
||||||
|
isFetching.current = false; |
||||||
|
}, |
||||||
|
[ |
||||||
|
getVideos, |
||||||
|
filterValue, |
||||||
|
getVideosFiltered, |
||||||
|
isFiltering, |
||||||
|
filterName, |
||||||
|
selectedCategoryVideos, |
||||||
|
selectedSubCategoryVideos, |
||||||
|
filterSearch, |
||||||
|
filterType, |
||||||
|
] |
||||||
|
); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (isFiltering && filterValue !== prevVal?.current) { |
||||||
|
prevVal.current = filterValue; |
||||||
|
getVideosHandler(); |
||||||
|
} |
||||||
|
}, [filterValue, isFiltering, filteredVideos]); |
||||||
|
|
||||||
|
const getVideosHandlerMount = React.useCallback(async () => { |
||||||
|
if (firstFetch.current) return; |
||||||
|
firstFetch.current = true; |
||||||
|
setIsLoading(true); |
||||||
|
|
||||||
|
await getVideos(); |
||||||
|
afterFetch.current = true; |
||||||
|
isFetching.current = false; |
||||||
|
|
||||||
|
setIsLoading(false); |
||||||
|
}, [getVideos]); |
||||||
|
|
||||||
|
let videos = globalVideos; |
||||||
|
|
||||||
|
if (isFiltering) { |
||||||
|
videos = filteredVideos; |
||||||
|
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; |
||||||
|
getVideosHandlerMount(); |
||||||
|
} else { |
||||||
|
firstFetch.current = true; |
||||||
|
afterFetch.current = true; |
||||||
|
} |
||||||
|
}, [getVideosHandlerMount, globalVideos]); |
||||||
|
|
||||||
|
const filtersToDefault = async () => { |
||||||
|
setFilterType("videos"); |
||||||
|
setFilterSearch(""); |
||||||
|
setFilterName(""); |
||||||
|
setSelectedCategoryVideos(null); |
||||||
|
setSelectedSubCategoryVideos(null); |
||||||
|
|
||||||
|
ReactDOM.flushSync(() => { |
||||||
|
getVideosHandler(true, true); |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
const handleOptionCategoryChangeVideos = ( |
||||||
|
event: SelectChangeEvent<string> |
||||||
|
) => { |
||||||
|
const optionId = event.target.value; |
||||||
|
const selectedOption = categories.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 blockUserFunc = async (user: string) => { |
||||||
|
if (user === "Q-Tube") return; |
||||||
|
|
||||||
|
try { |
||||||
|
const response = await qortalRequest({ |
||||||
|
action: "ADD_LIST_ITEMS", |
||||||
|
list_name: "blockedNames", |
||||||
|
items: [user], |
||||||
|
}); |
||||||
|
|
||||||
|
if (response === true) { |
||||||
|
dispatch(blockUser(user)) |
||||||
|
} |
||||||
|
} catch (error) {} |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Grid container sx={{ width: "100%" }}> |
||||||
|
<FiltersCol item xs={12} md={2} sm={3}> |
||||||
|
<FiltersContainer> |
||||||
|
<Input |
||||||
|
id="standard-adornment-name" |
||||||
|
onChange={(e) => { |
||||||
|
setFilterSearch(e.target.value); |
||||||
|
}} |
||||||
|
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); |
||||||
|
}} |
||||||
|
value={filterName} |
||||||
|
placeholder="User's name" |
||||||
|
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", |
||||||
|
}} |
||||||
|
/> |
||||||
|
<FiltersTitle> |
||||||
|
Categories |
||||||
|
<ExpandMoreSVG |
||||||
|
color={theme.palette.text.primary} |
||||||
|
height={"22"} |
||||||
|
width={"22"} |
||||||
|
/> |
||||||
|
</FiltersTitle> |
||||||
|
<FiltersSubContainer> |
||||||
|
<FormControl sx={{ width: "100%" }}> |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: "flex", |
||||||
|
gap: "20px", |
||||||
|
alignItems: "center", |
||||||
|
flexDirection: "column", |
||||||
|
}} |
||||||
|
> |
||||||
|
<FormControl fullWidth sx={{ marginBottom: 1 }}> |
||||||
|
<InputLabel |
||||||
|
sx={{ |
||||||
|
fontSize: "16px", |
||||||
|
}} |
||||||
|
id="Category" |
||||||
|
> |
||||||
|
Category |
||||||
|
</InputLabel> |
||||||
|
<Select |
||||||
|
labelId="Category" |
||||||
|
input={<OutlinedInput label="Category" />} |
||||||
|
value={selectedCategoryVideos?.id || ""} |
||||||
|
onChange={handleOptionCategoryChangeVideos} |
||||||
|
sx={{ |
||||||
|
// 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
|
||||||
|
}, |
||||||
|
}, |
||||||
|
}} |
||||||
|
> |
||||||
|
{categories.map((option) => ( |
||||||
|
<MenuItem key={option.id} value={option.id}> |
||||||
|
{option.name} |
||||||
|
</MenuItem> |
||||||
|
))} |
||||||
|
</Select> |
||||||
|
</FormControl> |
||||||
|
{selectedCategoryVideos && |
||||||
|
subCategories[selectedCategoryVideos?.id] && ( |
||||||
|
<FormControl fullWidth sx={{ marginBottom: 2 }}> |
||||||
|
<InputLabel |
||||||
|
sx={{ |
||||||
|
fontSize: "16px", |
||||||
|
}} |
||||||
|
id="Sub-Category" |
||||||
|
> |
||||||
|
Sub-Category |
||||||
|
</InputLabel> |
||||||
|
<Select |
||||||
|
labelId="Sub-Category" |
||||||
|
input={<OutlinedInput label="Sub-Category" />} |
||||||
|
value={selectedSubCategoryVideos?.id || ""} |
||||||
|
onChange={(e) => |
||||||
|
handleOptionSubCategoryChangeVideos( |
||||||
|
e, |
||||||
|
subCategories[selectedCategoryVideos?.id] |
||||||
|
) |
||||||
|
} |
||||||
|
sx={{ |
||||||
|
// 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
|
||||||
|
}, |
||||||
|
}, |
||||||
|
}} |
||||||
|
> |
||||||
|
{subCategories[selectedCategoryVideos.id].map( |
||||||
|
(option) => ( |
||||||
|
<MenuItem key={option.id} value={option.id}> |
||||||
|
{option.name} |
||||||
|
</MenuItem> |
||||||
|
) |
||||||
|
)} |
||||||
|
</Select> |
||||||
|
</FormControl> |
||||||
|
)} |
||||||
|
</Box> |
||||||
|
</FormControl> |
||||||
|
</FiltersSubContainer> |
||||||
|
<FiltersTitle> |
||||||
|
Type |
||||||
|
<ExpandMoreSVG |
||||||
|
color={theme.palette.text.primary} |
||||||
|
height={"22"} |
||||||
|
width={"22"} |
||||||
|
/> |
||||||
|
</FiltersTitle> |
||||||
|
<FiltersSubContainer> |
||||||
|
<FiltersRow> |
||||||
|
Videos |
||||||
|
<FiltersCheckbox |
||||||
|
checked={filterType === "videos"} |
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { |
||||||
|
setFilterType("videos"); |
||||||
|
}} |
||||||
|
inputProps={{ "aria-label": "controlled" }} |
||||||
|
/> |
||||||
|
</FiltersRow> |
||||||
|
<FiltersRow> |
||||||
|
Playlists |
||||||
|
<FiltersCheckbox |
||||||
|
checked={filterType === "playlists"} |
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { |
||||||
|
setFilterType("playlists"); |
||||||
|
}} |
||||||
|
inputProps={{ "aria-label": "controlled" }} |
||||||
|
/> |
||||||
|
</FiltersRow> |
||||||
|
</FiltersSubContainer> |
||||||
|
<Button |
||||||
|
onClick={() => { |
||||||
|
filtersToDefault(); |
||||||
|
}} |
||||||
|
sx={{ |
||||||
|
marginTop: "20px", |
||||||
|
}} |
||||||
|
variant="contained" |
||||||
|
> |
||||||
|
reset |
||||||
|
</Button> |
||||||
|
<Button |
||||||
|
onClick={() => { |
||||||
|
getVideosHandler(true); |
||||||
|
}} |
||||||
|
sx={{ |
||||||
|
marginTop: "20px", |
||||||
|
}} |
||||||
|
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", |
||||||
|
}} |
||||||
|
> |
||||||
|
{/* <Subtitle sx={{ |
||||||
|
textAlign: 'start', |
||||||
|
fontSize: '18px' |
||||||
|
}}> |
||||||
|
{!isFiltering ? 'Recently Published Videos': 'Search'} |
||||||
|
</Subtitle> */} |
||||||
|
</SubtitleContainer> |
||||||
|
{/* { countNewVideos > 0 && !isFiltering && ( |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: 'flex', |
||||||
|
alignItems: 'center', |
||||||
|
justifyContent: 'center' |
||||||
|
}} |
||||||
|
> |
||||||
|
<Typography> |
||||||
|
{countNewVideos === 1 |
||||||
|
? `There is ${countNewVideos} new video` |
||||||
|
: `There are ${countNewVideos} new videos`} |
||||||
|
</Typography> |
||||||
|
<Button |
||||||
|
sx={{ |
||||||
|
backgroundColor: theme.palette.primary.light, |
||||||
|
color: theme.palette.text.primary, |
||||||
|
fontFamily: 'Arial' |
||||||
|
}} |
||||||
|
onClick={()=> { |
||||||
|
getNewVideos() |
||||||
|
}} |
||||||
|
> |
||||||
|
Load new Posts |
||||||
|
</Button> |
||||||
|
</Box> |
||||||
|
)} */} |
||||||
|
<VideoContainer> |
||||||
|
{videos.map((video: any, index: number) => { |
||||||
|
const existingVideo = hashMapVideos[video?.id]; |
||||||
|
let hasHash = false; |
||||||
|
let videoObj = video; |
||||||
|
if (existingVideo) { |
||||||
|
videoObj = existingVideo; |
||||||
|
hasHash = true; |
||||||
|
} |
||||||
|
|
||||||
|
let avatarUrl = ""; |
||||||
|
if (userAvatarHash[videoObj?.user]) { |
||||||
|
avatarUrl = userAvatarHash[videoObj?.user]; |
||||||
|
} |
||||||
|
|
||||||
|
if (hasHash && !videoObj?.videoImage && !videoObj?.image) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
const isPlaylist = videoObj?.service === "PLAYLIST"; |
||||||
|
|
||||||
|
|
||||||
|
if (isPlaylist) { |
||||||
|
return ( |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: "flex", |
||||||
|
flex: 0, |
||||||
|
alignItems: "center", |
||||||
|
width: "auto", |
||||||
|
position: "relative", |
||||||
|
" @media (max-width: 450px)": { |
||||||
|
width: "100%", |
||||||
|
}, |
||||||
|
}} |
||||||
|
onMouseEnter={() => setShowIcons(videoObj.id)} |
||||||
|
onMouseLeave={() => setShowIcons(null)} |
||||||
|
key={videoObj.id} |
||||||
|
> |
||||||
|
|
||||||
|
<IconsBox |
||||||
|
sx={{ |
||||||
|
opacity: showIcons === videoObj.id ? 1 : 0, |
||||||
|
zIndex: 2, |
||||||
|
}} |
||||||
|
> |
||||||
|
{videoObj?.user === username && ( |
||||||
|
<Tooltip title="Edit playlist" placement="top"> |
||||||
|
<BlockIconContainer> |
||||||
|
<EditIcon |
||||||
|
onClick={() => { |
||||||
|
dispatch(setEditPlaylist(videoObj)); |
||||||
|
}} |
||||||
|
/> |
||||||
|
</BlockIconContainer> |
||||||
|
</Tooltip> |
||||||
|
)} |
||||||
|
|
||||||
|
<Tooltip title="Block user content" placement="top"> |
||||||
|
<BlockIconContainer> |
||||||
|
<BlockIcon |
||||||
|
onClick={() => { |
||||||
|
blockUserFunc(videoObj?.user); |
||||||
|
}} |
||||||
|
/> |
||||||
|
</BlockIconContainer> |
||||||
|
</Tooltip> |
||||||
|
</IconsBox> |
||||||
|
<VideoCard |
||||||
|
sx={{ |
||||||
|
cursor: !hasHash && 'default' |
||||||
|
}} |
||||||
|
onClick={() => { |
||||||
|
if(!hasHash) return |
||||||
|
navigate( |
||||||
|
`/video/${videoObj?.videos?.[0]?.name}/${videoObj?.videos?.[0]?.identifier}` |
||||||
|
); |
||||||
|
}} |
||||||
|
> |
||||||
|
<ResponsiveImage |
||||||
|
src={videoObj?.image} |
||||||
|
width={266} |
||||||
|
height={150} |
||||||
|
/> |
||||||
|
<VideoCardTitle>{videoObj?.title}</VideoCardTitle> |
||||||
|
<BottomParent> |
||||||
|
<NameContainer |
||||||
|
onClick={(e) => { |
||||||
|
e.stopPropagation(); |
||||||
|
navigate(`/channel/${videoObj?.user}`); |
||||||
|
}} |
||||||
|
> |
||||||
|
<Avatar |
||||||
|
sx={{ height: 24, width: 24 }} |
||||||
|
src={`/arbitrary/THUMBNAIL/${videoObj?.user}/qortal_avatar`} |
||||||
|
alt={`${videoObj?.user}'s avatar`} |
||||||
|
/> |
||||||
|
<VideoCardName |
||||||
|
sx={{ |
||||||
|
":hover": { |
||||||
|
textDecoration: "underline", |
||||||
|
}, |
||||||
|
}} |
||||||
|
> |
||||||
|
{videoObj?.user} |
||||||
|
</VideoCardName> |
||||||
|
</NameContainer> |
||||||
|
|
||||||
|
{videoObj?.created && ( |
||||||
|
<VideoUploadDate> |
||||||
|
{formatDate(videoObj.created)} |
||||||
|
</VideoUploadDate> |
||||||
|
)} |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: "flex", |
||||||
|
position: "absolute", |
||||||
|
bottom: "5px", |
||||||
|
right: "5px", |
||||||
|
}} |
||||||
|
> |
||||||
|
<PlaylistSVG |
||||||
|
color={theme.palette.text.primary} |
||||||
|
height="36px" |
||||||
|
width="36px" |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
</BottomParent> |
||||||
|
</VideoCard> |
||||||
|
</Box> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: "flex", |
||||||
|
flex: 0, |
||||||
|
alignItems: "center", |
||||||
|
width: "auto", |
||||||
|
position: "relative", |
||||||
|
" @media (max-width: 450px)": { |
||||||
|
width: "100%", |
||||||
|
}, |
||||||
|
}} |
||||||
|
key={videoObj.id} |
||||||
|
onMouseEnter={() => setShowIcons(videoObj.id)} |
||||||
|
onMouseLeave={() => setShowIcons(null)} |
||||||
|
> |
||||||
|
<IconsBox |
||||||
|
sx={{ |
||||||
|
opacity: showIcons === videoObj.id ? 1 : 0, |
||||||
|
zIndex: 2, |
||||||
|
}} |
||||||
|
> |
||||||
|
{videoObj?.user === username && (
|
||||||
|
<Tooltip title="Edit video properties" placement="top"> |
||||||
|
<BlockIconContainer> |
||||||
|
<EditIcon |
||||||
|
onClick={() => { |
||||||
|
dispatch(setEditVideo(videoObj)); |
||||||
|
}} |
||||||
|
/> |
||||||
|
</BlockIconContainer> |
||||||
|
</Tooltip> |
||||||
|
|
||||||
|
)} |
||||||
|
|
||||||
|
<Tooltip title="Block user content" placement="top"> |
||||||
|
<BlockIconContainer> |
||||||
|
<BlockIcon |
||||||
|
onClick={() => { |
||||||
|
blockUserFunc(videoObj?.user); |
||||||
|
}} |
||||||
|
/> |
||||||
|
</BlockIconContainer> |
||||||
|
</Tooltip> |
||||||
|
</IconsBox> |
||||||
|
<VideoCard |
||||||
|
onClick={() => { |
||||||
|
navigate(`/video/${videoObj?.user}/${videoObj?.id}`); |
||||||
|
}} |
||||||
|
> |
||||||
|
<ResponsiveImage |
||||||
|
src={videoObj.videoImage} |
||||||
|
width={266} |
||||||
|
height={150} |
||||||
|
/> |
||||||
|
<VideoCardTitle>{videoObj.title}</VideoCardTitle> |
||||||
|
<BottomParent> |
||||||
|
<NameContainer |
||||||
|
onClick={(e) => { |
||||||
|
e.stopPropagation(); |
||||||
|
navigate(`/channel/${videoObj?.user}`); |
||||||
|
}} |
||||||
|
> |
||||||
|
<Avatar |
||||||
|
sx={{ height: 24, width: 24 }} |
||||||
|
src={`/arbitrary/THUMBNAIL/${videoObj?.user}/qortal_avatar`} |
||||||
|
alt={`${videoObj?.user}'s avatar`} |
||||||
|
/> |
||||||
|
<VideoCardName |
||||||
|
sx={{ |
||||||
|
":hover": { |
||||||
|
textDecoration: "underline", |
||||||
|
}, |
||||||
|
}} |
||||||
|
> |
||||||
|
{videoObj?.user} |
||||||
|
</VideoCardName> |
||||||
|
</NameContainer> |
||||||
|
|
||||||
|
{videoObj?.created && ( |
||||||
|
<VideoUploadDate> |
||||||
|
{formatDate(videoObj.created)} |
||||||
|
</VideoUploadDate> |
||||||
|
)} |
||||||
|
</BottomParent> |
||||||
|
</VideoCard> |
||||||
|
</Box> |
||||||
|
); |
||||||
|
})} |
||||||
|
</VideoContainer> |
||||||
|
|
||||||
|
<LazyLoad |
||||||
|
onLoadMore={getVideosHandler} |
||||||
|
isLoading={isLoading} |
||||||
|
></LazyLoad> |
||||||
|
</Box> |
||||||
|
</Grid> |
||||||
|
</Grid> |
||||||
|
); |
||||||
|
}; |
@ -0,0 +1,205 @@ |
|||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react' |
||||||
|
import { useNavigate, useParams } from 'react-router-dom' |
||||||
|
import { useSelector } from 'react-redux' |
||||||
|
import { RootState } from '../../state/store' |
||||||
|
import { |
||||||
|
Avatar, |
||||||
|
Box, |
||||||
|
Button, |
||||||
|
Typography, |
||||||
|
useTheme |
||||||
|
} from '@mui/material' |
||||||
|
import { useFetchVideos } from '../../hooks/useFetchVideos' |
||||||
|
import LazyLoad from '../../components/common/LazyLoad' |
||||||
|
import { BottomParent, NameContainer, VideoCard, VideoCardName, VideoCardTitle, VideoContainer, VideoUploadDate } from './VideoList-styles' |
||||||
|
import ResponsiveImage from '../../components/ResponsiveImage' |
||||||
|
import { formatDate, formatTimestampSeconds } from '../../utils/time' |
||||||
|
import { Video } from '../../state/features/videoSlice' |
||||||
|
import { queue } from '../../wrappers/GlobalWrapper' |
||||||
|
import { QTUBE_VIDEO_BASE } from '../../constants' |
||||||
|
|
||||||
|
interface VideoListProps { |
||||||
|
mode?: string |
||||||
|
} |
||||||
|
export const VideoListComponentLevel = ({ 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.video.hashMapVideos |
||||||
|
) |
||||||
|
|
||||||
|
const countNewVideos = useSelector( |
||||||
|
(state: RootState) => state.video.countNewVideos |
||||||
|
) |
||||||
|
const userAvatarHash = useSelector( |
||||||
|
(state: RootState) => state.global.userAvatarHash |
||||||
|
) |
||||||
|
|
||||||
|
const [videos, setVideos] = React.useState<Video[]>([]) |
||||||
|
|
||||||
|
const navigate = useNavigate() |
||||||
|
const { |
||||||
|
getVideo, |
||||||
|
getNewVideos, |
||||||
|
checkNewVideos, |
||||||
|
checkAndUpdateVideo |
||||||
|
} = useFetchVideos() |
||||||
|
|
||||||
|
const getVideos = React.useCallback(async () => { |
||||||
|
try { |
||||||
|
const offset = videos.length
|
||||||
|
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QTUBE_VIDEO_BASE}_&limit=20&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 = checkAndUpdateVideo(content) |
||||||
|
if (res) { |
||||||
|
queue.push(() => getVideo(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' |
||||||
|
}}> |
||||||
|
|
||||||
|
<VideoContainer> |
||||||
|
{videos.map((video: any, index: number) => { |
||||||
|
const existingVideo = hashMapVideos[video.id] |
||||||
|
let hasHash = false |
||||||
|
let videoObj = video |
||||||
|
if (existingVideo) { |
||||||
|
videoObj = existingVideo |
||||||
|
hasHash = true |
||||||
|
} |
||||||
|
|
||||||
|
let avatarUrl = '' |
||||||
|
if(userAvatarHash[videoObj?.user]){ |
||||||
|
avatarUrl = userAvatarHash[videoObj?.user] |
||||||
|
} |
||||||
|
|
||||||
|
if(hasHash && (!videoObj?.videoImage || videoObj?.videoImage?.length < 50)){ |
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
return ( |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: 'flex', |
||||||
|
flex: 0, |
||||||
|
alignItems: 'center', |
||||||
|
width: 'auto', |
||||||
|
position: 'relative', |
||||||
|
' @media (max-width: 450px)': { |
||||||
|
width: '100%' |
||||||
|
} |
||||||
|
}} |
||||||
|
key={videoObj.id} |
||||||
|
> |
||||||
|
|
||||||
|
<VideoCard |
||||||
|
onClick={() => { |
||||||
|
navigate(`/video/${videoObj.user}/${videoObj.id}`) |
||||||
|
}} |
||||||
|
> |
||||||
|
<ResponsiveImage src={videoObj.videoImage} width={266} height={150}/> |
||||||
|
<VideoCardTitle>{videoObj.title}</VideoCardTitle> |
||||||
|
<BottomParent> |
||||||
|
<NameContainer> |
||||||
|
<Avatar sx={{height: 24, width: 24}} src={`/arbitrary/THUMBNAIL/${videoObj?.user}/qortal_avatar`} alt={`${videoObj.user}'s avatar`} /> |
||||||
|
<VideoCardName>{videoObj.user}</VideoCardName> |
||||||
|
</NameContainer> |
||||||
|
|
||||||
|
{videoObj?.created && ( |
||||||
|
<VideoUploadDate>{formatDate(videoObj.created)}</VideoUploadDate> |
||||||
|
)} |
||||||
|
|
||||||
|
</BottomParent> |
||||||
|
</VideoCard> |
||||||
|
|
||||||
|
|
||||||
|
</Box> |
||||||
|
) |
||||||
|
})} |
||||||
|
</VideoContainer> |
||||||
|
<LazyLoad onLoadMore={getVideosHandler} isLoading={isLoading}></LazyLoad> |
||||||
|
</Box> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
|
@ -0,0 +1,64 @@ |
|||||||
|
import React, { useMemo } from 'react' |
||||||
|
import { VideoListComponentLevel } from '../Home/VideoListComponentLevel' |
||||||
|
import { HeaderContainer, ProfileContainer } from './Profile-styles' |
||||||
|
import { AuthorTextComment, StyledCardColComment, StyledCardHeaderComment } from '../VideoContent/VideoContent-styles' |
||||||
|
import { Avatar, Box, useTheme } from '@mui/material' |
||||||
|
import { useParams } from 'react-router-dom' |
||||||
|
import { useSelector } from 'react-redux' |
||||||
|
import { setUserAvatarHash } from '../../state/features/globalSlice' |
||||||
|
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> |
||||||
|
<VideoListComponentLevel /> |
||||||
|
</ProfileContainer> |
||||||
|
|
||||||
|
) |
||||||
|
} |
@ -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" |
||||||
|
})); |
@ -0,0 +1,81 @@ |
|||||||
|
import { styled } from "@mui/system"; |
||||||
|
import { Box, Grid, Typography, Checkbox } from "@mui/material"; |
||||||
|
|
||||||
|
export const VideoPlayerContainer = styled(Box)(({ theme }) => ({ |
||||||
|
maxWidth: '95%', |
||||||
|
width: '1000px', |
||||||
|
display: 'flex', |
||||||
|
flexDirection: 'column', |
||||||
|
alignItems: 'flex-start', |
||||||
|
})); |
||||||
|
|
||||||
|
export const VideoTitle = styled(Typography)(({ theme }) => ({ |
||||||
|
fontFamily: "Raleway", |
||||||
|
fontSize: "20px", |
||||||
|
color: theme.palette.text.primary, |
||||||
|
userSelect: "none", |
||||||
|
wordBreak: "break-word" |
||||||
|
})); |
||||||
|
|
||||||
|
export const VideoDescription = 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 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' |
||||||
|
})); |
@ -0,0 +1,457 @@ |
|||||||
|
import React, { useState, useMemo, useRef, useEffect } 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 { VideoPlayer } from "../../components/common/VideoPlayer"; |
||||||
|
import { RootState } from "../../state/store"; |
||||||
|
import { addToHashMap } from "../../state/features/videoSlice"; |
||||||
|
import AttachFileIcon from "@mui/icons-material/AttachFile"; |
||||||
|
import DownloadIcon from "@mui/icons-material/Download"; |
||||||
|
|
||||||
|
import mockImg from "../../test/mockimg.jpg"; |
||||||
|
import { |
||||||
|
AuthorTextComment, |
||||||
|
FileAttachmentContainer, |
||||||
|
FileAttachmentFont, |
||||||
|
Spacer, |
||||||
|
StyledCardColComment, |
||||||
|
StyledCardHeaderComment, |
||||||
|
VideoDescription, |
||||||
|
VideoPlayerContainer, |
||||||
|
VideoTitle, |
||||||
|
} from "./VideoContent-styles"; |
||||||
|
import { setUserAvatarHash } from "../../state/features/globalSlice"; |
||||||
|
import { |
||||||
|
formatDate, |
||||||
|
formatDateSeconds, |
||||||
|
formatTimestampSeconds, |
||||||
|
} from "../../utils/time"; |
||||||
|
import { NavbarName } from "../../components/layout/Navbar/Navbar-styles"; |
||||||
|
import { CommentSection } from "../../components/common/Comments/CommentSection"; |
||||||
|
import { |
||||||
|
CrowdfundSubTitle, |
||||||
|
CrowdfundSubTitleRow, |
||||||
|
} from "../../components/UploadVideo/Upload-styles"; |
||||||
|
import { QTUBE_VIDEO_BASE } from "../../constants"; |
||||||
|
import { Playlists } from "../../components/Playlists/Playlists"; |
||||||
|
import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml"; |
||||||
|
import FileElement from "../../components/common/FileElement"; |
||||||
|
|
||||||
|
export const VideoContent = () => { |
||||||
|
const { name, id } = useParams(); |
||||||
|
const [isExpandedDescription, setIsExpandedDescription] = |
||||||
|
useState<boolean>(false); |
||||||
|
const [descriptionHeight, setDescriptionHeight] = |
||||||
|
useState<null | number>(null); |
||||||
|
|
||||||
|
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 [videoData, setVideoData] = useState<any>(null); |
||||||
|
const [playlistData, setPlaylistData] = useState<any>(null); |
||||||
|
|
||||||
|
const hashMapVideos = useSelector( |
||||||
|
(state: RootState) => state.video.hashMapVideos |
||||||
|
); |
||||||
|
const videoReference = useMemo(() => { |
||||||
|
if (!videoData) return null; |
||||||
|
const { videoReference } = videoData; |
||||||
|
if ( |
||||||
|
videoReference?.identifier && |
||||||
|
videoReference?.name && |
||||||
|
videoReference?.service |
||||||
|
) { |
||||||
|
return videoReference; |
||||||
|
} else { |
||||||
|
return null; |
||||||
|
} |
||||||
|
}, [videoData]); |
||||||
|
|
||||||
|
const videoCover = useMemo(() => { |
||||||
|
if (!videoData) return null; |
||||||
|
const { videoImage } = videoData; |
||||||
|
return videoImage || null; |
||||||
|
}, [videoData]); |
||||||
|
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=${QTUBE_VIDEO_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, |
||||||
|
}; |
||||||
|
|
||||||
|
setVideoData(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) { |
||||||
|
setVideoData(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; |
||||||
|
console.log({height}) |
||||||
|
if (height > 100) { // Assuming 100px is your threshold
|
||||||
|
setDescriptionHeight(100) |
||||||
|
} |
||||||
|
} |
||||||
|
}, [videoData]);
|
||||||
|
|
||||||
|
console.log({ videoData }); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: "flex", |
||||||
|
alignItems: "center", |
||||||
|
flexDirection: "column", |
||||||
|
padding: "20px 10px", |
||||||
|
}} |
||||||
|
> |
||||||
|
<VideoPlayerContainer |
||||||
|
sx={{ |
||||||
|
marginBottom: "30px", |
||||||
|
}} |
||||||
|
> |
||||||
|
{videoReference && ( |
||||||
|
<VideoPlayer |
||||||
|
name={videoReference?.name} |
||||||
|
service={videoReference?.service} |
||||||
|
identifier={videoReference?.identifier} |
||||||
|
user={name} |
||||||
|
jsonId={id} |
||||||
|
poster={videoCover || ""} |
||||||
|
/> |
||||||
|
)} |
||||||
|
|
||||||
|
<Spacer height="15px" /> |
||||||
|
<Box sx={{ |
||||||
|
width: '100%', |
||||||
|
display: 'flex', |
||||||
|
justifyContent: 'flex-end' |
||||||
|
}}> |
||||||
|
<FileAttachmentContainer> |
||||||
|
|
||||||
|
<FileAttachmentFont> |
||||||
|
save to disk |
||||||
|
</FileAttachmentFont> |
||||||
|
<FileElement |
||||||
|
fileInfo={{...videoReference, |
||||||
|
filename: videoData?.filename || videoData?.title?.slice(0,20) + '.mp4', |
||||||
|
mimeType: videoData?.videoType || '"video/mp4', |
||||||
|
|
||||||
|
}} |
||||||
|
title={videoData?.filename || videoData?.title?.slice(0,20)} |
||||||
|
customStyles={{ |
||||||
|
display: "flex", |
||||||
|
alignItems: "center", |
||||||
|
justifyContent: "flex-end", |
||||||
|
}} |
||||||
|
> |
||||||
|
<DownloadIcon /> |
||||||
|
</FileElement> |
||||||
|
</FileAttachmentContainer> |
||||||
|
</Box> |
||||||
|
<VideoTitle |
||||||
|
variant="h1" |
||||||
|
color="textPrimary" |
||||||
|
sx={{ |
||||||
|
textAlign: "center", |
||||||
|
}} |
||||||
|
> |
||||||
|
{videoData?.title} |
||||||
|
</VideoTitle> |
||||||
|
{videoData?.created && ( |
||||||
|
<Typography |
||||||
|
variant="h6" |
||||||
|
sx={{ |
||||||
|
fontSize: "12px", |
||||||
|
}} |
||||||
|
color={theme.palette.text.primary} |
||||||
|
> |
||||||
|
{formatDate(videoData.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 |
||||||
|
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", |
||||||
|
}} |
||||||
|
> |
||||||
|
{videoData?.htmlDescription ? ( |
||||||
|
<DisplayHtml html={videoData?.htmlDescription} /> |
||||||
|
) : ( |
||||||
|
<VideoDescription variant="body1" color="textPrimary" sx={{ |
||||||
|
cursor: 'default' |
||||||
|
}}> |
||||||
|
{videoData?.fullDescription} |
||||||
|
</VideoDescription> |
||||||
|
)} |
||||||
|
</Box> |
||||||
|
{descriptionHeight && ( |
||||||
|
<Typography |
||||||
|
onClick={() => { |
||||||
|
setIsExpandedDescription((prev) => !prev); |
||||||
|
}} |
||||||
|
sx={{ |
||||||
|
fontWeight: "bold", |
||||||
|
fontSize: "16px", |
||||||
|
cursor: "pointer", |
||||||
|
paddingLeft: "15px", |
||||||
|
paddingTop: "15px", |
||||||
|
}} |
||||||
|
> |
||||||
|
{isExpandedDescription ? "Show less" : "...more"} |
||||||
|
</Typography> |
||||||
|
)} |
||||||
|
|
||||||
|
</Box> |
||||||
|
</VideoPlayerContainer> |
||||||
|
|
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: "flex", |
||||||
|
gap: "20px", |
||||||
|
width: "100%", |
||||||
|
maxWidth: "1200px", |
||||||
|
}} |
||||||
|
> |
||||||
|
<CommentSection postId={id || ""} postName={name || ""} /> |
||||||
|
{playlistData && ( |
||||||
|
<Playlists playlistData={playlistData} currentVideoIdentifier={id} /> |
||||||
|
)} |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
); |
||||||
|
}; |
@ -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; |
@ -0,0 +1,62 @@ |
|||||||
|
import { createSlice } from '@reduxjs/toolkit' |
||||||
|
|
||||||
|
|
||||||
|
interface GlobalState { |
||||||
|
isLoadingGlobal: boolean |
||||||
|
downloads: any |
||||||
|
userAvatarHash: Record<string, string> |
||||||
|
publishNames: string[] | null |
||||||
|
videoPlaying: any | null |
||||||
|
} |
||||||
|
const initialState: GlobalState = { |
||||||
|
isLoadingGlobal: false, |
||||||
|
downloads: {}, |
||||||
|
userAvatarHash: {}, |
||||||
|
publishNames: null, |
||||||
|
videoPlaying: 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 |
||||||
|
}, |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
export const { |
||||||
|
setIsLoadingGlobal, |
||||||
|
setAddToDownloads, |
||||||
|
updateDownloads, |
||||||
|
setUserAvatarHash, |
||||||
|
addPublishNames, |
||||||
|
setVideoPlaying |
||||||
|
} = globalSlice.actions |
||||||
|
|
||||||
|
export default globalSlice.reducer |
@ -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; |
@ -0,0 +1,204 @@ |
|||||||
|
import { createSlice } from '@reduxjs/toolkit'; |
||||||
|
import { RootState } from '../store' |
||||||
|
|
||||||
|
|
||||||
|
interface GlobalState { |
||||||
|
videos: Video[] |
||||||
|
filteredVideos: Video[] |
||||||
|
hashMapVideos: Record<string, Video> |
||||||
|
countNewVideos: number |
||||||
|
isFiltering: boolean |
||||||
|
filterValue: string |
||||||
|
filterType: string |
||||||
|
filterSearch: string |
||||||
|
filterName: string |
||||||
|
selectedCategoryVideos: any |
||||||
|
selectedSubCategoryVideos: any |
||||||
|
editVideoProperties: any |
||||||
|
editPlaylistProperties: any |
||||||
|
} |
||||||
|
const initialState: GlobalState = { |
||||||
|
videos: [], |
||||||
|
filteredVideos: [], |
||||||
|
hashMapVideos: {}, |
||||||
|
countNewVideos: 0, |
||||||
|
isFiltering: false, |
||||||
|
filterValue: '', |
||||||
|
filterType: 'videos', |
||||||
|
filterSearch: '', |
||||||
|
filterName: '', |
||||||
|
selectedCategoryVideos: null, |
||||||
|
selectedSubCategoryVideos: null, |
||||||
|
editVideoProperties: 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 videoSlice = createSlice({ |
||||||
|
name: 'video', |
||||||
|
initialState, |
||||||
|
reducers: { |
||||||
|
setEditVideo: (state, action) => { |
||||||
|
state.editVideoProperties = 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 |
||||||
|
}, |
||||||
|
changeSelectedCategoryVideos: (state, action) => { |
||||||
|
state.selectedCategoryVideos = action.payload |
||||||
|
}, |
||||||
|
changeSelectedSubCategoryVideos: (state, action) => { |
||||||
|
state.selectedSubCategoryVideos = action.payload |
||||||
|
}, |
||||||
|
setCountNewVideos: (state, action) => { |
||||||
|
state.countNewVideos = action.payload |
||||||
|
}, |
||||||
|
addVideos: (state, action) => { |
||||||
|
state.videos = action.payload |
||||||
|
}, |
||||||
|
addFilteredVideos: (state, action) => { |
||||||
|
state.filteredVideos = action.payload |
||||||
|
}, |
||||||
|
removeVideo: (state, action) => { |
||||||
|
const idToDelete = action.payload |
||||||
|
state.videos = state.videos.filter((item) => item.id !== idToDelete) |
||||||
|
state.filteredVideos = state.filteredVideos.filter( |
||||||
|
(item) => item.id !== idToDelete |
||||||
|
) |
||||||
|
}, |
||||||
|
addVideoToBeginning: (state, action) => { |
||||||
|
state.videos.unshift(action.payload) |
||||||
|
}, |
||||||
|
clearVideoList: (state) => { |
||||||
|
state.videos = [] |
||||||
|
}, |
||||||
|
updateVideo: (state, action) => { |
||||||
|
const { id } = action.payload |
||||||
|
const index = state.videos.findIndex((video) => video.id === id) |
||||||
|
if (index !== -1) { |
||||||
|
state.videos[index] = { ...action.payload } |
||||||
|
} |
||||||
|
const index2 = state.filteredVideos.findIndex((video) => video.id === id) |
||||||
|
if (index2 !== -1) { |
||||||
|
state.filteredVideos[index2] = { ...action.payload } |
||||||
|
} |
||||||
|
}, |
||||||
|
addToHashMap: (state, action) => { |
||||||
|
const video = action.payload |
||||||
|
state.hashMapVideos[video.id] = video |
||||||
|
}, |
||||||
|
updateInHashMap: (state, action) => { |
||||||
|
const { id } = action.payload |
||||||
|
const video = action.payload |
||||||
|
state.hashMapVideos[id] = { ...video } |
||||||
|
}, |
||||||
|
removeFromHashMap: (state, action) => { |
||||||
|
const idToDelete = action.payload |
||||||
|
delete state.hashMapVideos[idToDelete] |
||||||
|
}, |
||||||
|
addArrayToHashMap: (state, action) => { |
||||||
|
const videos = action.payload |
||||||
|
videos.forEach((video: Video) => { |
||||||
|
state.hashMapVideos[video.id] = video |
||||||
|
}) |
||||||
|
}, |
||||||
|
upsertVideos: (state, action) => { |
||||||
|
action.payload.forEach((video: Video) => { |
||||||
|
const index = state.videos.findIndex((p) => p.id === video.id) |
||||||
|
if (index !== -1) { |
||||||
|
state.videos[index] = video |
||||||
|
} else { |
||||||
|
state.videos.push(video) |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
upsertFilteredVideos: (state, action) => { |
||||||
|
action.payload.forEach((video: Video) => { |
||||||
|
const index = state.filteredVideos.findIndex((p) => p.id === video.id) |
||||||
|
if (index !== -1) { |
||||||
|
state.filteredVideos[index] = video |
||||||
|
} else { |
||||||
|
state.filteredVideos.push(video) |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
upsertVideosBeginning: (state, action) => { |
||||||
|
action.payload.reverse().forEach((video: Video) => { |
||||||
|
const index = state.videos.findIndex((p) => p.id === video.id) |
||||||
|
if (index !== -1) { |
||||||
|
state.videos[index] = video |
||||||
|
} else { |
||||||
|
state.videos.unshift(video) |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
setIsFiltering: (state, action) => { |
||||||
|
state.isFiltering = action.payload |
||||||
|
}, |
||||||
|
setFilterValue: (state, action) => { |
||||||
|
state.filterValue = action.payload |
||||||
|
}, |
||||||
|
blockUser: (state, action) => { |
||||||
|
const username = action.payload |
||||||
|
|
||||||
|
state.videos = state.videos.filter((item) => item.user !== username) |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
export const { |
||||||
|
setCountNewVideos, |
||||||
|
addVideos, |
||||||
|
addFilteredVideos, |
||||||
|
removeVideo, |
||||||
|
addVideoToBeginning, |
||||||
|
updateVideo, |
||||||
|
addToHashMap, |
||||||
|
updateInHashMap, |
||||||
|
removeFromHashMap, |
||||||
|
addArrayToHashMap, |
||||||
|
upsertVideos, |
||||||
|
upsertFilteredVideos, |
||||||
|
upsertVideosBeginning, |
||||||
|
setIsFiltering, |
||||||
|
setFilterValue, |
||||||
|
clearVideoList, |
||||||
|
changeFilterType, |
||||||
|
changefilterSearch, |
||||||
|
changefilterName, |
||||||
|
changeSelectedCategoryVideos, |
||||||
|
changeSelectedSubCategoryVideos, |
||||||
|
blockUser, |
||||||
|
setEditVideo, |
||||||
|
setEditPlaylist |
||||||
|
} = videoSlice.actions |
||||||
|
|
||||||
|
export default videoSlice.reducer |
||||||
|
|
@ -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 videoReducer from './features/videoSlice' |
||||||
|
|
||||||
|
export const store = configureStore({ |
||||||
|
reducer: { |
||||||
|
notifications: notificationsReducer, |
||||||
|
auth: authReducer, |
||||||
|
global: globalReducer, |
||||||
|
video: videoReducer, |
||||||
|
}, |
||||||
|
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 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,184 @@ |
|||||||
|
import { createTheme } from "@mui/material/styles"; |
||||||
|
|
||||||
|
const commonThemeOptions = { |
||||||
|
typography: { |
||||||
|
fontFamily: [ |
||||||
|
"Cambon Light", |
||||||
|
"Raleway, sans-serif", |
||||||
|
"Karla", |
||||||
|
"Merriweather Sans", |
||||||
|
"Proxima Nova", |
||||||
|
"Oxygen", |
||||||
|
"Catamaran", |
||||||
|
"Cairo", |
||||||
|
"Arial" |
||||||
|
].join(","), |
||||||
|
h1: { |
||||||
|
fontSize: "2rem", |
||||||
|
fontWeight: 600 |
||||||
|
}, |
||||||
|
h2: { |
||||||
|
fontSize: "1.75rem", |
||||||
|
fontWeight: 500 |
||||||
|
}, |
||||||
|
h3: { |
||||||
|
fontSize: "1.5rem", |
||||||
|
fontWeight: 500 |
||||||
|
}, |
||||||
|
h4: { |
||||||
|
fontSize: "1.25rem", |
||||||
|
fontWeight: 500 |
||||||
|
}, |
||||||
|
h5: { |
||||||
|
fontSize: "1rem", |
||||||
|
fontWeight: 500 |
||||||
|
}, |
||||||
|
h6: { |
||||||
|
fontSize: "0.875rem", |
||||||
|
fontWeight: 500 |
||||||
|
}, |
||||||
|
body1: { |
||||||
|
fontSize: "23px", |
||||||
|
fontFamily: "Raleway", |
||||||
|
fontWeight: 400, |
||||||
|
lineHeight: 1.5, |
||||||
|
letterSpacing: "0.5px" |
||||||
|
}, |
||||||
|
|
||||||
|
body2: { |
||||||
|
fontSize: "18px", |
||||||
|
fontFamily: "Raleway, Arial", |
||||||
|
fontWeight: 400, |
||||||
|
lineHeight: 1.4, |
||||||
|
letterSpacing: "0.2px" |
||||||
|
} |
||||||
|
}, |
||||||
|
spacing: 8, |
||||||
|
shape: { |
||||||
|
borderRadius: 4 |
||||||
|
}, |
||||||
|
breakpoints: { |
||||||
|
values: { |
||||||
|
xs: 0, |
||||||
|
sm: 600, |
||||||
|
md: 900, |
||||||
|
lg: 1200, |
||||||
|
xl: 1536 |
||||||
|
} |
||||||
|
}, |
||||||
|
components: { |
||||||
|
MuiButton: { |
||||||
|
styleOverrides: { |
||||||
|
root: { |
||||||
|
backgroundColor: "inherit", |
||||||
|
transition: "filter 0.3s ease-in-out", |
||||||
|
"&:hover": { |
||||||
|
filter: "brightness(1.1)" |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
defaultProps: { |
||||||
|
disableElevation: true, |
||||||
|
disableRipple: true |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const lightTheme = createTheme({ |
||||||
|
...commonThemeOptions, |
||||||
|
palette: { |
||||||
|
mode: "light", |
||||||
|
primary: { |
||||||
|
main: "#ffffff", |
||||||
|
dark: "#F5F5F5", |
||||||
|
light: "#FCFCFC" |
||||||
|
}, |
||||||
|
secondary: { |
||||||
|
main: "#417Ed4", |
||||||
|
dark: "#3e74c1" |
||||||
|
}, |
||||||
|
background: { |
||||||
|
default: "#fcfcfc", |
||||||
|
paper: "#F5F5F5" |
||||||
|
}, |
||||||
|
text: { |
||||||
|
primary: "#000000", |
||||||
|
secondary: "#525252" |
||||||
|
} |
||||||
|
}, |
||||||
|
components: { |
||||||
|
MuiCard: { |
||||||
|
styleOverrides: { |
||||||
|
root: { |
||||||
|
boxShadow: |
||||||
|
"rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.06) 0px 1px 2px 0px;", |
||||||
|
borderRadius: "8px", |
||||||
|
transition: "all 0.3s ease-in-out", |
||||||
|
"&:hover": { |
||||||
|
cursor: "pointer", |
||||||
|
boxShadow: |
||||||
|
"rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
MuiIcon: { |
||||||
|
defaultProps: { |
||||||
|
style: { |
||||||
|
color: "#000000" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
const darkTheme = createTheme({ |
||||||
|
...commonThemeOptions, |
||||||
|
palette: { |
||||||
|
mode: "dark", |
||||||
|
primary: { |
||||||
|
main: "#2e3d60", |
||||||
|
dark: "#1a2744", |
||||||
|
light: "#353535" |
||||||
|
}, |
||||||
|
secondary: { |
||||||
|
main: "#417Ed4", |
||||||
|
dark: "#3e74c1" |
||||||
|
}, |
||||||
|
|
||||||
|
background: { |
||||||
|
default: "#111111", |
||||||
|
paper: "#1A1C1E" |
||||||
|
}, |
||||||
|
text: { |
||||||
|
primary: "#ffffff", |
||||||
|
secondary: "#b3b3b3" |
||||||
|
} |
||||||
|
}, |
||||||
|
components: { |
||||||
|
MuiCard: { |
||||||
|
styleOverrides: { |
||||||
|
root: { |
||||||
|
boxShadow: "none", |
||||||
|
borderRadius: "8px", |
||||||
|
transition: "all 0.3s ease-in-out", |
||||||
|
"&:hover": { |
||||||
|
cursor: "pointer", |
||||||
|
boxShadow: |
||||||
|
" 0px 3px 4px 0px hsla(0,0%,0%,0.14), 0px 3px 3px -2px hsla(0,0%,0%,0.12), 0px 1px 8px 0px hsla(0,0%,0%,0.2);" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
MuiIcon: { |
||||||
|
defaultProps: { |
||||||
|
style: { |
||||||
|
color: "#ffffff" |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
export { lightTheme, darkTheme }; |
After Width: | Height: | Size: 426 KiB |
After Width: | Height: | Size: 21 KiB |
@ -0,0 +1,7 @@ |
|||||||
|
export const checkStructure = (content: any) => { |
||||||
|
let isValid = true |
||||||
|
|
||||||
|
return isValid |
||||||
|
} |
||||||
|
|
||||||
|
|
@ -0,0 +1,14 @@ |
|||||||
|
export function extractTextFromSlate(nodes: any) { |
||||||
|
if(!Array.isArray(nodes)) return "" |
||||||
|
let text = ""; |
||||||
|
|
||||||
|
for (const node of nodes) { |
||||||
|
if (node.text) { |
||||||
|
text += node.text; |
||||||
|
} else if (node.children) { |
||||||
|
text += extractTextFromSlate(node.children); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return text; |
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
import { checkStructure } from './checkStructure' |
||||||
|
|
||||||
|
export const fetchAndEvaluateVideos = async (data: any) => { |
||||||
|
const getVideo = async () => { |
||||||
|
const { user, videoId, content } = data |
||||||
|
let obj: any = { |
||||||
|
...content, |
||||||
|
isValid: false |
||||||
|
} |
||||||
|
|
||||||
|
if (!user || !videoId) return obj |
||||||
|
|
||||||
|
try { |
||||||
|
|
||||||
|
const responseData = await qortalRequest({ |
||||||
|
action: 'FETCH_QDN_RESOURCE', |
||||||
|
name: user, |
||||||
|
service: content?.service || 'DOCUMENT', |
||||||
|
identifier: videoId |
||||||
|
}) |
||||||
|
if (checkStructure(responseData)) { |
||||||
|
obj = { |
||||||
|
...content, |
||||||
|
...responseData, |
||||||
|
isValid: true |
||||||
|
} |
||||||
|
} |
||||||
|
return obj |
||||||
|
} catch (error: any) { |
||||||
|
throw new Error(error?.message || 'error') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const res = await getVideo() |
||||||
|
return res |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
type QueueItem = { |
||||||
|
request: () => Promise<any>; |
||||||
|
resolve: (value: any | PromiseLike<any>) => void; |
||||||
|
reject: (reason?: any) => void; |
||||||
|
}; |
||||||
|
|
||||||
|
export class RequestQueue { |
||||||
|
private queue: QueueItem[]; |
||||||
|
private maxConcurrent: number; |
||||||
|
private currentConcurrent: number; |
||||||
|
|
||||||
|
constructor(maxConcurrent = 5) { |
||||||
|
this.queue = []; |
||||||
|
this.maxConcurrent = maxConcurrent; |
||||||
|
this.currentConcurrent = 0; |
||||||
|
} |
||||||
|
|
||||||
|
async push(request: () => Promise<any>): Promise<any> { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
this.queue.push({ |
||||||
|
request, |
||||||
|
resolve, |
||||||
|
reject, |
||||||
|
}); |
||||||
|
this.checkQueue(); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private checkQueue(): void { |
||||||
|
if (this.queue.length === 0 || this.currentConcurrent >= this.maxConcurrent) return; |
||||||
|
|
||||||
|
const { request, resolve, reject } = this.queue.shift() as QueueItem; |
||||||
|
this.currentConcurrent++; |
||||||
|
|
||||||
|
request() |
||||||
|
.then(resolve) |
||||||
|
.catch(reject) |
||||||
|
.finally(() => { |
||||||
|
this.currentConcurrent--; |
||||||
|
this.checkQueue(); |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,46 @@ |
|||||||
|
import moment from 'moment' |
||||||
|
|
||||||
|
export function formatTimestamp(timestamp: number): string { |
||||||
|
const now = moment() |
||||||
|
const timestampMoment = moment(timestamp) |
||||||
|
const elapsedTime = now.diff(timestampMoment, 'minutes') |
||||||
|
|
||||||
|
if (elapsedTime < 1) { |
||||||
|
return 'Just now' |
||||||
|
} else if (elapsedTime < 60) { |
||||||
|
return `${elapsedTime}m` |
||||||
|
} else if (elapsedTime < 1440) { |
||||||
|
return `${Math.floor(elapsedTime / 60)}h` |
||||||
|
} else { |
||||||
|
return timestampMoment.format('MMM D') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function formatTimestampSeconds(timestamp: number): string { |
||||||
|
const now = moment() |
||||||
|
const timestampMoment = moment.unix(timestamp) |
||||||
|
const elapsedTime = now.diff(timestampMoment, 'minutes') |
||||||
|
|
||||||
|
if (elapsedTime < 1) { |
||||||
|
return 'Just now' |
||||||
|
} else if (elapsedTime < 60) { |
||||||
|
return `${elapsedTime}m` |
||||||
|
} else if (elapsedTime < 1440) { |
||||||
|
return `${Math.floor(elapsedTime / 60)}h` |
||||||
|
} else { |
||||||
|
return timestampMoment.format('MMM D') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const formatDate = (unixTimestamp: number): string => { |
||||||
|
const date = moment(unixTimestamp, 'x').fromNow() |
||||||
|
|
||||||
|
return date |
||||||
|
} |
||||||
|
export const formatDateSeconds = (unixTimestamp: number): string => { |
||||||
|
const date = moment.unix(unixTimestamp).fromNow(); |
||||||
|
|
||||||
|
return date |
||||||
|
} |
@ -0,0 +1,174 @@ |
|||||||
|
export const toBase64 = (file: File): Promise<string | ArrayBuffer | null> => |
||||||
|
new Promise((resolve, reject) => { |
||||||
|
const reader = new FileReader() |
||||||
|
reader.readAsDataURL(file) |
||||||
|
|
||||||
|
reader.onload = () => { |
||||||
|
const result = reader.result |
||||||
|
reader.onload = null // remove onload handler
|
||||||
|
reader.onerror = null // remove onerror handler
|
||||||
|
resolve(result) |
||||||
|
} |
||||||
|
|
||||||
|
reader.onerror = (error) => { |
||||||
|
reader.onload = null // remove onload handler
|
||||||
|
reader.onerror = null // remove onerror handler
|
||||||
|
reject(error) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
export function objectToBase64(obj: any) { |
||||||
|
// Step 1: Convert the object to a JSON string
|
||||||
|
const jsonString = JSON.stringify(obj) |
||||||
|
|
||||||
|
// Step 2: Create a Blob from the JSON string
|
||||||
|
const blob = new Blob([jsonString], { type: 'application/json' }) |
||||||
|
|
||||||
|
// Step 3: Create a FileReader to read the Blob as a base64-encoded string
|
||||||
|
return new Promise<string>((resolve, reject) => { |
||||||
|
const reader = new FileReader() |
||||||
|
reader.onloadend = () => { |
||||||
|
if (typeof reader.result === 'string') { |
||||||
|
// Remove 'data:application/json;base64,' prefix
|
||||||
|
const base64 = reader.result.replace( |
||||||
|
'data:application/json;base64,', |
||||||
|
'' |
||||||
|
) |
||||||
|
resolve(base64) |
||||||
|
} else { |
||||||
|
reject(new Error('Failed to read the Blob as a base64-encoded string')) |
||||||
|
} |
||||||
|
} |
||||||
|
reader.onerror = () => { |
||||||
|
reject(reader.error) |
||||||
|
} |
||||||
|
reader.readAsDataURL(blob) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export function objectToUint8Array(obj: any) { |
||||||
|
// Convert the object to a JSON string
|
||||||
|
const jsonString = JSON.stringify(obj) |
||||||
|
|
||||||
|
// Encode the JSON string as a byte array using TextEncoder
|
||||||
|
const encoder = new TextEncoder() |
||||||
|
const byteArray = encoder.encode(jsonString) |
||||||
|
|
||||||
|
// Create a new Uint8Array and set its content to the encoded byte array
|
||||||
|
const uint8Array = new Uint8Array(byteArray) |
||||||
|
|
||||||
|
return uint8Array |
||||||
|
} |
||||||
|
|
||||||
|
export function uint8ArrayToBase64(uint8Array: Uint8Array): string { |
||||||
|
const length = uint8Array.length |
||||||
|
let binaryString = '' |
||||||
|
const chunkSize = 1024 * 1024 // Process 1MB at a time
|
||||||
|
|
||||||
|
for (let i = 0; i < length; i += chunkSize) { |
||||||
|
const chunkEnd = Math.min(i + chunkSize, length) |
||||||
|
const chunk = uint8Array.subarray(i, chunkEnd) |
||||||
|
binaryString += Array.from(chunk, (byte) => String.fromCharCode(byte)).join( |
||||||
|
'' |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
return btoa(binaryString) |
||||||
|
} |
||||||
|
|
||||||
|
export function objectToUint8ArrayFromResponse(obj: any) { |
||||||
|
const len = Object.keys(obj).length |
||||||
|
const result = new Uint8Array(len) |
||||||
|
|
||||||
|
for (let i = 0; i < len; i++) { |
||||||
|
result[i] = obj[i] |
||||||
|
} |
||||||
|
|
||||||
|
return result |
||||||
|
} |
||||||
|
// export function uint8ArrayToBase64(arrayBuffer: Uint8Array): string {
|
||||||
|
// let binary = ''
|
||||||
|
// const bytes = new Uint8Array(arrayBuffer)
|
||||||
|
// const len = bytes.length
|
||||||
|
|
||||||
|
// for (let i = 0; i < len; i++) {
|
||||||
|
// binary += String.fromCharCode(bytes[i])
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return btoa(binary)
|
||||||
|
// }
|
||||||
|
|
||||||
|
export function base64ToUint8Array(base64: string) { |
||||||
|
const binaryString = atob(base64) |
||||||
|
const len = binaryString.length |
||||||
|
const bytes = new Uint8Array(len) |
||||||
|
|
||||||
|
for (let i = 0; i < len; i++) { |
||||||
|
bytes[i] = binaryString.charCodeAt(i) |
||||||
|
} |
||||||
|
|
||||||
|
return bytes |
||||||
|
} |
||||||
|
|
||||||
|
export function uint8ArrayToObject(uint8Array: Uint8Array) { |
||||||
|
// Decode the byte array using TextDecoder
|
||||||
|
const decoder = new TextDecoder() |
||||||
|
const jsonString = decoder.decode(uint8Array) |
||||||
|
|
||||||
|
// Convert the JSON string back into an object
|
||||||
|
const obj = JSON.parse(jsonString) |
||||||
|
|
||||||
|
return obj |
||||||
|
} |
||||||
|
|
||||||
|
export function processFileInChunks(file: File): Promise<Uint8Array> { |
||||||
|
return new Promise( |
||||||
|
(resolve: (value: Uint8Array) => void, reject: (reason?: any) => void) => { |
||||||
|
const reader = new FileReader() |
||||||
|
|
||||||
|
reader.onload = function (event: ProgressEvent<FileReader>) { |
||||||
|
const arrayBuffer = event.target?.result as ArrayBuffer |
||||||
|
const uint8Array = new Uint8Array(arrayBuffer) |
||||||
|
resolve(uint8Array) |
||||||
|
} |
||||||
|
|
||||||
|
reader.onerror = function (error: ProgressEvent<FileReader>) { |
||||||
|
reject(error) |
||||||
|
} |
||||||
|
|
||||||
|
reader.readAsArrayBuffer(file) |
||||||
|
} |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
// export async function processFileInChunks(file: File, chunkSize = 1024 * 1024): Promise<Uint8Array> {
|
||||||
|
// const fileStream = file.stream();
|
||||||
|
// const reader = fileStream.getReader();
|
||||||
|
// const totalLength = file.size;
|
||||||
|
|
||||||
|
// if (totalLength <= 0 || isNaN(totalLength)) {
|
||||||
|
// throw new Error('Invalid file size');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const combinedArray = new Uint8Array(totalLength);
|
||||||
|
// let offset = 0;
|
||||||
|
|
||||||
|
// while (offset < totalLength) {
|
||||||
|
// const { value, done } = await reader.read();
|
||||||
|
|
||||||
|
// if (done) {
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const chunk = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
||||||
|
|
||||||
|
// // Set elements one by one instead of using combinedArray.set(chunk, offset)
|
||||||
|
// for (let i = 0; i < chunk.length; i++) {
|
||||||
|
// combinedArray[offset + i] = chunk[i];
|
||||||
|
// }
|
||||||
|
|
||||||
|
// offset += chunk.length;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return combinedArray;
|
||||||
|
// }
|
@ -0,0 +1,213 @@ |
|||||||
|
import React from 'react' |
||||||
|
import { useDispatch, useSelector } from 'react-redux' |
||||||
|
|
||||||
|
|
||||||
|
import { |
||||||
|
setAddToDownloads, |
||||||
|
updateDownloads |
||||||
|
} from '../state/features/globalSlice' |
||||||
|
|
||||||
|
import { DownloadTaskManager } from '../components/common/DownloadTaskManager' |
||||||
|
import { RootState } from '../state/store' |
||||||
|
|
||||||
|
interface Props { |
||||||
|
children: React.ReactNode |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
const defaultValues: MyContextInterface = { |
||||||
|
downloadVideo: () => {} |
||||||
|
} |
||||||
|
interface IDownloadVideoParams { |
||||||
|
name: string |
||||||
|
service: string |
||||||
|
identifier: string |
||||||
|
properties: any |
||||||
|
} |
||||||
|
interface MyContextInterface { |
||||||
|
downloadVideo: ({ |
||||||
|
name, |
||||||
|
service, |
||||||
|
identifier, |
||||||
|
properties |
||||||
|
}: IDownloadVideoParams) => void |
||||||
|
} |
||||||
|
export const MyContext = React.createContext<MyContextInterface>(defaultValues) |
||||||
|
|
||||||
|
const DownloadWrapper: React.FC<Props> = ({ children }) => { |
||||||
|
const dispatch = useDispatch() |
||||||
|
const downloads = useSelector((state: RootState) => state.global?.downloads); |
||||||
|
|
||||||
|
|
||||||
|
const fetchResource = async ({ name, service, identifier }: any) => { |
||||||
|
try { |
||||||
|
await qortalRequest({ |
||||||
|
action: 'GET_QDN_RESOURCE_PROPERTIES', |
||||||
|
name, |
||||||
|
service, |
||||||
|
identifier |
||||||
|
}) |
||||||
|
} catch (error) {} |
||||||
|
} |
||||||
|
|
||||||
|
const fetchVideoUrl = async ({ name, service, identifier }: any) => { |
||||||
|
try { |
||||||
|
fetchResource({ name, service, identifier }) |
||||||
|
let url = await qortalRequest({ |
||||||
|
action: 'GET_QDN_RESOURCE_URL', |
||||||
|
service: service, |
||||||
|
name: name, |
||||||
|
identifier: identifier |
||||||
|
}) |
||||||
|
if (url) { |
||||||
|
dispatch( |
||||||
|
updateDownloads({ |
||||||
|
name, |
||||||
|
service, |
||||||
|
identifier, |
||||||
|
url |
||||||
|
}) |
||||||
|
) |
||||||
|
} |
||||||
|
} catch (error) {} |
||||||
|
} |
||||||
|
|
||||||
|
const performDownload = ({ |
||||||
|
name, |
||||||
|
service, |
||||||
|
identifier, |
||||||
|
properties |
||||||
|
}: IDownloadVideoParams) => { |
||||||
|
if(downloads[identifier]) return |
||||||
|
dispatch( |
||||||
|
setAddToDownloads({ |
||||||
|
name, |
||||||
|
service, |
||||||
|
identifier, |
||||||
|
properties |
||||||
|
}) |
||||||
|
) |
||||||
|
|
||||||
|
let isCalling = false |
||||||
|
let percentLoaded = 0 |
||||||
|
let timer = 24 |
||||||
|
const intervalId = setInterval(async () => { |
||||||
|
if (isCalling) return |
||||||
|
isCalling = true |
||||||
|
const res = await qortalRequest({ |
||||||
|
action: 'GET_QDN_RESOURCE_STATUS', |
||||||
|
name: name, |
||||||
|
service: service, |
||||||
|
identifier: identifier |
||||||
|
}) |
||||||
|
if(res?.status === 'NOT_PUBLISHED'){ |
||||||
|
dispatch( |
||||||
|
updateDownloads({ |
||||||
|
name, |
||||||
|
service, |
||||||
|
identifier, |
||||||
|
status: res |
||||||
|
}) |
||||||
|
) |
||||||
|
clearInterval(intervalId) |
||||||
|
} |
||||||
|
isCalling = false |
||||||
|
if (res.localChunkCount) { |
||||||
|
if (res.percentLoaded) { |
||||||
|
if ( |
||||||
|
res.percentLoaded === percentLoaded && |
||||||
|
res.percentLoaded !== 100 |
||||||
|
) { |
||||||
|
timer = timer - 5 |
||||||
|
} else { |
||||||
|
timer = 24 |
||||||
|
} |
||||||
|
if (timer < 0) { |
||||||
|
timer = 24 |
||||||
|
isCalling = true |
||||||
|
dispatch( |
||||||
|
updateDownloads({ |
||||||
|
name, |
||||||
|
service, |
||||||
|
identifier, |
||||||
|
status: { |
||||||
|
...res, |
||||||
|
status: 'REFETCHING' |
||||||
|
} |
||||||
|
}) |
||||||
|
) |
||||||
|
setTimeout(() => { |
||||||
|
isCalling = false |
||||||
|
fetchResource({ |
||||||
|
name, |
||||||
|
service, |
||||||
|
identifier |
||||||
|
}) |
||||||
|
}, 25000) |
||||||
|
return |
||||||
|
} |
||||||
|
percentLoaded = res.percentLoaded |
||||||
|
} |
||||||
|
dispatch( |
||||||
|
updateDownloads({ |
||||||
|
name, |
||||||
|
service, |
||||||
|
identifier, |
||||||
|
status: res |
||||||
|
}) |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
// check if progress is 100% and clear interval if true
|
||||||
|
if (res?.status === 'READY') { |
||||||
|
clearInterval(intervalId) |
||||||
|
dispatch( |
||||||
|
updateDownloads({ |
||||||
|
name, |
||||||
|
service, |
||||||
|
identifier, |
||||||
|
status: res |
||||||
|
}) |
||||||
|
) |
||||||
|
} |
||||||
|
}, 5000) // 1 second interval
|
||||||
|
|
||||||
|
fetchVideoUrl({ |
||||||
|
name, |
||||||
|
service, |
||||||
|
identifier |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const downloadVideo = async ({ |
||||||
|
name, |
||||||
|
service, |
||||||
|
identifier, |
||||||
|
properties |
||||||
|
}: IDownloadVideoParams) => { |
||||||
|
try { |
||||||
|
|
||||||
|
|
||||||
|
performDownload({ |
||||||
|
name, |
||||||
|
service, |
||||||
|
identifier, |
||||||
|
properties |
||||||
|
}) |
||||||
|
return 'addedToList' |
||||||
|
} catch (error) { |
||||||
|
console.error(error) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<MyContext.Provider value={{ downloadVideo }}> |
||||||
|
{/* <DownloadTaskManager /> */} |
||||||
|
{children} |
||||||
|
</MyContext.Provider> |
||||||
|
</> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default DownloadWrapper |
@ -0,0 +1,173 @@ |
|||||||
|
import React, { |
||||||
|
useEffect, |
||||||
|
useState, |
||||||
|
useCallback, |
||||||
|
useRef, |
||||||
|
useMemo, |
||||||
|
} from "react"; |
||||||
|
import { useDispatch, useSelector } from "react-redux"; |
||||||
|
|
||||||
|
import { addUser } from "../state/features/authSlice"; |
||||||
|
import NavBar from "../components/layout/Navbar/Navbar"; |
||||||
|
import PageLoader from "../components/common/PageLoader"; |
||||||
|
import { RootState } from "../state/store"; |
||||||
|
import { setUserAvatarHash } from "../state/features/globalSlice"; |
||||||
|
import { VideoPlayerGlobal } from "../components/common/VideoPlayerGlobal"; |
||||||
|
import { Rnd } from "react-rnd"; |
||||||
|
import { RequestQueue } from "../utils/queue"; |
||||||
|
import { EditVideo } from "../components/EditVideo/EditVideo"; |
||||||
|
import { EditPlaylist } from "../components/EditPlaylist/EditPlaylist"; |
||||||
|
import ConsentModal from "../components/common/ConsentModal"; |
||||||
|
|
||||||
|
interface Props { |
||||||
|
children: React.ReactNode; |
||||||
|
setTheme: (val: string) => void; |
||||||
|
} |
||||||
|
|
||||||
|
let timer: number | null = null; |
||||||
|
|
||||||
|
export const queue = new RequestQueue(); |
||||||
|
|
||||||
|
const GlobalWrapper: React.FC<Props> = ({ children, setTheme }) => { |
||||||
|
const dispatch = useDispatch(); |
||||||
|
const isDragging = useRef(false); |
||||||
|
const [userAvatar, setUserAvatar] = useState<string>(""); |
||||||
|
const user = useSelector((state: RootState) => state.auth.user); |
||||||
|
const videoPlaying = useSelector( |
||||||
|
(state: RootState) => state.global.videoPlaying |
||||||
|
); |
||||||
|
const username = useMemo(() => { |
||||||
|
if (!user?.name) return ""; |
||||||
|
|
||||||
|
return user.name; |
||||||
|
}, [user]); |
||||||
|
const getAvatar = React.useCallback( |
||||||
|
async (author: string) => { |
||||||
|
try { |
||||||
|
const url = await qortalRequest({ |
||||||
|
action: "GET_QDN_RESOURCE_URL", |
||||||
|
name: author, |
||||||
|
service: "THUMBNAIL", |
||||||
|
identifier: "qortal_avatar", |
||||||
|
}); |
||||||
|
if (url) { |
||||||
|
setUserAvatar(url); |
||||||
|
dispatch( |
||||||
|
setUserAvatarHash({ |
||||||
|
name: author, |
||||||
|
url, |
||||||
|
}) |
||||||
|
); |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
/* empty */ |
||||||
|
} |
||||||
|
}, |
||||||
|
[dispatch] |
||||||
|
); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (!username) return; |
||||||
|
|
||||||
|
getAvatar(username); |
||||||
|
}, [username, getAvatar]); |
||||||
|
|
||||||
|
const { isLoadingGlobal } = useSelector((state: RootState) => state.global); |
||||||
|
|
||||||
|
async function getNameInfo(address: string) { |
||||||
|
const response = await qortalRequest({ |
||||||
|
action: "GET_ACCOUNT_NAMES", |
||||||
|
address: address, |
||||||
|
}); |
||||||
|
const nameData = response; |
||||||
|
|
||||||
|
if (nameData?.length > 0) { |
||||||
|
return nameData[0].name; |
||||||
|
} else { |
||||||
|
return ""; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const askForAccountInformation = React.useCallback(async () => { |
||||||
|
try { |
||||||
|
const account = await qortalRequest({ |
||||||
|
action: "GET_USER_ACCOUNT", |
||||||
|
}); |
||||||
|
|
||||||
|
const name = await getNameInfo(account.address); |
||||||
|
dispatch(addUser({ ...account, name })); |
||||||
|
} catch (error) { |
||||||
|
console.error(error); |
||||||
|
} |
||||||
|
}, [dispatch]); |
||||||
|
|
||||||
|
React.useEffect(() => { |
||||||
|
askForAccountInformation(); |
||||||
|
}, [askForAccountInformation]); |
||||||
|
|
||||||
|
const onDragStart = () => { |
||||||
|
timer = Date.now(); |
||||||
|
isDragging.current = true; |
||||||
|
}; |
||||||
|
|
||||||
|
const handleStopDrag = async () => { |
||||||
|
const time = Date.now(); |
||||||
|
if (timer && time - timer < 300) { |
||||||
|
isDragging.current = false; |
||||||
|
} else { |
||||||
|
isDragging.current = true; |
||||||
|
} |
||||||
|
}; |
||||||
|
const onDragStop = () => { |
||||||
|
handleStopDrag(); |
||||||
|
}; |
||||||
|
|
||||||
|
const checkIfDrag = useCallback(() => { |
||||||
|
return isDragging.current; |
||||||
|
}, []); |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
{isLoadingGlobal && <PageLoader />} |
||||||
|
<ConsentModal /> |
||||||
|
|
||||||
|
<NavBar |
||||||
|
setTheme={(val: string) => setTheme(val)} |
||||||
|
isAuthenticated={!!user?.name} |
||||||
|
userName={user?.name || ""} |
||||||
|
userAvatar={userAvatar} |
||||||
|
authenticate={askForAccountInformation} |
||||||
|
/> |
||||||
|
<EditVideo /> |
||||||
|
<EditPlaylist /> |
||||||
|
<Rnd |
||||||
|
onDragStart={onDragStart} |
||||||
|
onDragStop={onDragStop} |
||||||
|
style={{ |
||||||
|
display: videoPlaying ? "block" : "none", |
||||||
|
position: "fixed", |
||||||
|
height: "auto", |
||||||
|
width: 350, |
||||||
|
zIndex: 1000, |
||||||
|
maxWidth: 800, |
||||||
|
}} |
||||||
|
default={{ |
||||||
|
x: 0, |
||||||
|
y: 60, |
||||||
|
width: 350, |
||||||
|
height: "auto", |
||||||
|
}} |
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
onDrag={() => {}} |
||||||
|
> |
||||||
|
{videoPlaying && ( |
||||||
|
<VideoPlayerGlobal checkIfDrag={checkIfDrag} element={videoPlaying} /> |
||||||
|
)} |
||||||
|
</Rnd> |
||||||
|
|
||||||
|
{children} |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default GlobalWrapper; |
@ -0,0 +1,26 @@ |
|||||||
|
{ |
||||||
|
"compilerOptions": { |
||||||
|
"target": "ESNext", |
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"], |
||||||
|
"module": "ESNext", |
||||||
|
"skipLibCheck": true, |
||||||
|
"noImplicitAny": false, |
||||||
|
|
||||||
|
/* Bundler mode */ |
||||||
|
"moduleResolution": "bundler", |
||||||
|
"allowImportingTsExtensions": true, |
||||||
|
"resolveJsonModule": true, |
||||||
|
"isolatedModules": true, |
||||||
|
"noEmit": true, |
||||||
|
"jsx": "react-jsx", |
||||||
|
|
||||||
|
/* Linting */ |
||||||
|
"strict": false, |
||||||
|
"noUnusedLocals": false, |
||||||
|
"noUnusedParameters": false, |
||||||
|
"noFallthroughCasesInSwitch": true, |
||||||
|
"strictNullChecks": false, |
||||||
|
}, |
||||||
|
"include": ["src"], |
||||||
|
"references": [{ "path": "./tsconfig.node.json" }] |
||||||
|
} |
@ -0,0 +1,10 @@ |
|||||||
|
{ |
||||||
|
"compilerOptions": { |
||||||
|
"composite": true, |
||||||
|
"skipLibCheck": true, |
||||||
|
"module": "ESNext", |
||||||
|
"moduleResolution": "bundler", |
||||||
|
"allowSyntheticDefaultImports": true |
||||||
|
}, |
||||||
|
"include": ["vite.config.ts"] |
||||||
|
} |
Loading…
Reference in new issue