@ -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="/share/: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 |
After Width: | Height: | Size: 13 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,765 @@ |
|||||||
|
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 RemoveIcon from "@mui/icons-material/Remove"; |
||||||
|
|
||||||
|
import ShortUniqueId from "short-unique-id"; |
||||||
|
import { useDispatch, useSelector } from "react-redux"; |
||||||
|
import AddBoxIcon from "@mui/icons-material/AddBox"; |
||||||
|
import { useDropzone } from "react-dropzone"; |
||||||
|
|
||||||
|
import { setNotification } from "../../state/features/notificationsSlice"; |
||||||
|
import { objectToBase64, 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, subCategories2, |
||||||
|
subCategories3, } 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; |
||||||
|
identifier?:string; |
||||||
|
filename?: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 [files, setFiles] = useState<VideoFile[]>([]); |
||||||
|
|
||||||
|
const [selectedCategoryVideos, setSelectedCategoryVideos] = |
||||||
|
useState<any>(null); |
||||||
|
const [selectedSubCategoryVideos, setSelectedSubCategoryVideos] = |
||||||
|
useState<any>(null); |
||||||
|
const [selectedSubCategoryVideos2, setSelectedSubCategoryVideos2] = |
||||||
|
useState<any>(null); |
||||||
|
const [selectedSubCategoryVideos3, setSelectedSubCategoryVideos3] = |
||||||
|
useState<any>(null); |
||||||
|
|
||||||
|
const { getRootProps, getInputProps } = useDropzone({ |
||||||
|
maxFiles: 10, |
||||||
|
maxSize: 419430400, // 400 MB in bytes
|
||||||
|
onDrop: (acceptedFiles, rejectedFiles) => { |
||||||
|
const formatArray = acceptedFiles.map((item) => { |
||||||
|
return { |
||||||
|
file: item, |
||||||
|
title: "", |
||||||
|
description: "", |
||||||
|
coverImage: "", |
||||||
|
}; |
||||||
|
}); |
||||||
|
|
||||||
|
setFiles((prev) => [...prev, ...formatArray]); |
||||||
|
|
||||||
|
let errorString = null; |
||||||
|
rejectedFiles.forEach(({ file, errors }) => { |
||||||
|
errors.forEach((error) => { |
||||||
|
if (error.code === "file-too-large") { |
||||||
|
errorString = "File must be under 400mb"; |
||||||
|
} |
||||||
|
console.log(`Error with file ${file.name}: ${error.message}`); |
||||||
|
}); |
||||||
|
}); |
||||||
|
if (errorString) { |
||||||
|
const notificationObj = { |
||||||
|
msg: errorString, |
||||||
|
alertType: "error", |
||||||
|
}; |
||||||
|
|
||||||
|
dispatch(setNotification(notificationObj)); |
||||||
|
} |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (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 || ""); |
||||||
|
setFiles(editVideoProperties?.files || []) |
||||||
|
if(editVideoProperties?.htmlDescription){ |
||||||
|
setDescription(editVideoProperties?.htmlDescription); |
||||||
|
|
||||||
|
} else if(editVideoProperties?.fullDescription) { |
||||||
|
const paragraph = `<p>${editVideoProperties?.fullDescription}</p>` |
||||||
|
setDescription(paragraph); |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
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?.category && |
||||||
|
editVideoProperties?.subcategory2 && |
||||||
|
subCategories2[+editVideoProperties?.subcategory] |
||||||
|
) { |
||||||
|
const selectedOption = subCategories2[ |
||||||
|
+editVideoProperties?.subcategory |
||||||
|
]?.find((option) => option.id === +editVideoProperties.subcategory2); |
||||||
|
setSelectedSubCategoryVideos2(selectedOption || null); |
||||||
|
} |
||||||
|
if ( |
||||||
|
editVideoProperties?.category && |
||||||
|
editVideoProperties?.subcategory3 && |
||||||
|
subCategories3[+editVideoProperties?.subcategory2] |
||||||
|
) { |
||||||
|
|
||||||
|
const selectedOption = subCategories3[ |
||||||
|
+editVideoProperties?.subcategory2 |
||||||
|
]?.find((option) => option.id === +editVideoProperties.subcategory3); |
||||||
|
setSelectedSubCategoryVideos3(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 (!selectedCategoryVideos) throw new Error("Please select a category"); |
||||||
|
if (!editVideoProperties) return; |
||||||
|
if (!userAddress) throw new Error("Unable to locate user address"); |
||||||
|
if(files.length === 0) throw new Error("Add at least one file"); |
||||||
|
|
||||||
|
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 fileReferences = [] |
||||||
|
|
||||||
|
let listOfPublishes = []; |
||||||
|
const fullDescription = extractTextFromHTML(description); |
||||||
|
|
||||||
|
const category = selectedCategoryVideos.id; |
||||||
|
const subcategory = selectedSubCategoryVideos?.id || ""; |
||||||
|
const subcategory2 = selectedSubCategoryVideos2?.id || ""; |
||||||
|
const subcategory3 = selectedSubCategoryVideos3?.id || ""; |
||||||
|
const sanitizeTitle = title |
||||||
|
.replace(/[^a-zA-Z0-9\s-]/g, "") |
||||||
|
.replace(/\s+/g, "-") |
||||||
|
.replace(/-+/g, "-") |
||||||
|
.trim() |
||||||
|
.toLowerCase(); |
||||||
|
|
||||||
|
|
||||||
|
for (const publish of files) { |
||||||
|
if(publish?.identifier){ |
||||||
|
fileReferences.push(publish) |
||||||
|
continue |
||||||
|
} |
||||||
|
const file = publish.file; |
||||||
|
const id = uid(); |
||||||
|
|
||||||
|
const identifier = `${QTUBE_VIDEO_BASE}${sanitizeTitle.slice(0, 30)}_${id}`; |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let fileExtension = ""; |
||||||
|
const fileExtensionSplit = file?.name?.split("."); |
||||||
|
if (fileExtensionSplit?.length > 1) { |
||||||
|
fileExtension = fileExtensionSplit?.pop() || ""; |
||||||
|
} |
||||||
|
let firstPartName = fileExtensionSplit[0] |
||||||
|
|
||||||
|
let filename = firstPartName.slice(0, 15); |
||||||
|
|
||||||
|
// Step 1: Replace all white spaces with underscores
|
||||||
|
|
||||||
|
// Replace all forms of whitespace (including non-standard ones) with underscores
|
||||||
|
let stringWithUnderscores = filename.replace(/[\s\uFEFF\xA0]+/g, "_"); |
||||||
|
|
||||||
|
// Remove all non-alphanumeric characters (except underscores)
|
||||||
|
let alphanumericString = stringWithUnderscores.replace( |
||||||
|
/[^a-zA-Z0-9_]/g, |
||||||
|
"" |
||||||
|
); |
||||||
|
|
||||||
|
if(fileExtension){ |
||||||
|
filename = `${alphanumericString.trim()}.${fileExtension}` |
||||||
|
} else { |
||||||
|
filename = alphanumericString |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let metadescription = |
||||||
|
`**cat:${category};sub:${subcategory};sub2:${subcategory2};sub3:${subcategory3}**` + |
||||||
|
fullDescription.slice(0, 150); |
||||||
|
|
||||||
|
const requestBodyVideo: any = { |
||||||
|
action: "PUBLISH_QDN_RESOURCE", |
||||||
|
name: name, |
||||||
|
service: "FILE", |
||||||
|
file, |
||||||
|
title: title.slice(0, 50), |
||||||
|
description: metadescription, |
||||||
|
identifier, |
||||||
|
filename, |
||||||
|
tag1: QTUBE_VIDEO_BASE, |
||||||
|
}; |
||||||
|
listOfPublishes.push(requestBodyVideo); |
||||||
|
fileReferences.push({ |
||||||
|
filename: file.name, |
||||||
|
identifier, |
||||||
|
name, |
||||||
|
service: 'FILE', |
||||||
|
mimetype: file.type, |
||||||
|
size: file.size |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const fileObject: any = { |
||||||
|
title, |
||||||
|
version: editVideoProperties.version, |
||||||
|
fullDescription, |
||||||
|
htmlDescription: description, |
||||||
|
commentsId: editVideoProperties.commentsId, |
||||||
|
category, |
||||||
|
subcategory, |
||||||
|
subcategory2, |
||||||
|
subcategory3, |
||||||
|
files: fileReferences |
||||||
|
}; |
||||||
|
|
||||||
|
let metadescription = |
||||||
|
`**cat:${category};sub:${subcategory};sub2:${subcategory2}**` + |
||||||
|
fullDescription.slice(0, 150); |
||||||
|
|
||||||
|
const crowdfundObjectToBase64 = await objectToBase64(fileObject); |
||||||
|
// Description is obtained from raw data
|
||||||
|
|
||||||
|
const requestBodyJson: any = { |
||||||
|
action: "PUBLISH_QDN_RESOURCE", |
||||||
|
name: name, |
||||||
|
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); |
||||||
|
|
||||||
|
setPublishes(listOfPublishes); |
||||||
|
setIsOpenMultiplePublish(true); |
||||||
|
setVideoPropertiesToSetToRedux({ |
||||||
|
...editVideoProperties, |
||||||
|
...fileObject, |
||||||
|
}); |
||||||
|
} catch (error: any) { |
||||||
|
let notificationObj: any = null; |
||||||
|
if (typeof error === "string") { |
||||||
|
notificationObj = { |
||||||
|
msg: error || "Failed to publish update", |
||||||
|
alertType: "error", |
||||||
|
}; |
||||||
|
} else if (typeof error?.error === "string") { |
||||||
|
notificationObj = { |
||||||
|
msg: error?.error || "Failed to publish update", |
||||||
|
alertType: "error", |
||||||
|
}; |
||||||
|
} else { |
||||||
|
notificationObj = { |
||||||
|
msg: error?.message || "Failed to publish update", |
||||||
|
alertType: "error", |
||||||
|
}; |
||||||
|
} |
||||||
|
if (!notificationObj) return; |
||||||
|
dispatch(setNotification(notificationObj)); |
||||||
|
|
||||||
|
throw new Error("Failed to publish update"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const handleOnchange = (index: number, type: string, value: string) => { |
||||||
|
// setFiles((prev) => {
|
||||||
|
// let formattedValue = value
|
||||||
|
// console.log({type})
|
||||||
|
// if(type === 'title'){
|
||||||
|
// formattedValue = value.replace(/[^a-zA-Z0-9\s]/g, "")
|
||||||
|
// }
|
||||||
|
// const copyFiles = [...prev];
|
||||||
|
// copyFiles[index] = {
|
||||||
|
// ...copyFiles[index],
|
||||||
|
// [type]: formattedValue,
|
||||||
|
// };
|
||||||
|
// return copyFiles;
|
||||||
|
// });
|
||||||
|
}; |
||||||
|
|
||||||
|
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 handleOptionSubCategoryChangeVideos2 = ( |
||||||
|
event: SelectChangeEvent<string>, |
||||||
|
subcategories: any[] |
||||||
|
) => { |
||||||
|
const optionId = event.target.value; |
||||||
|
const selectedOption = subcategories.find( |
||||||
|
(option) => option.id === +optionId |
||||||
|
); |
||||||
|
setSelectedSubCategoryVideos2(selectedOption || null); |
||||||
|
}; |
||||||
|
|
||||||
|
const handleOptionSubCategoryChangeVideos3 = ( |
||||||
|
event: SelectChangeEvent<string>, |
||||||
|
subcategories: any[] |
||||||
|
) => { |
||||||
|
const optionId = event.target.value; |
||||||
|
const selectedOption = subcategories.find( |
||||||
|
(option) => option.id === +optionId |
||||||
|
); |
||||||
|
setSelectedSubCategoryVideos3(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 share</NewCrowdfundTitle> |
||||||
|
</Box> |
||||||
|
<> |
||||||
|
<Box |
||||||
|
{...getRootProps()} |
||||||
|
sx={{ |
||||||
|
border: "1px dashed gray", |
||||||
|
padding: 2, |
||||||
|
textAlign: "center", |
||||||
|
marginBottom: 2, |
||||||
|
cursor: "pointer", |
||||||
|
}} |
||||||
|
> |
||||||
|
<input {...getInputProps()} /> |
||||||
|
<Typography>Click to add more files</Typography> |
||||||
|
</Box> |
||||||
|
{files.map((file, index) => { |
||||||
|
const isExistingFile = !!file?.identifier |
||||||
|
return ( |
||||||
|
<React.Fragment key={index}> |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: "flex", |
||||||
|
justifyContent: "space-between", |
||||||
|
alignItems: "center", |
||||||
|
}} |
||||||
|
> |
||||||
|
<Typography>{isExistingFile? file.filename : file?.file?.name}</Typography> |
||||||
|
<RemoveIcon |
||||||
|
onClick={() => { |
||||||
|
setFiles((prev) => { |
||||||
|
const copyPrev = [...prev]; |
||||||
|
copyPrev.splice(index, 1); |
||||||
|
return copyPrev; |
||||||
|
}); |
||||||
|
}} |
||||||
|
sx={{ |
||||||
|
cursor: "pointer", |
||||||
|
}} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
</React.Fragment> |
||||||
|
); |
||||||
|
})} |
||||||
|
|
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: "flex", |
||||||
|
gap: "20px", |
||||||
|
alignItems: "flex-start", |
||||||
|
}} |
||||||
|
> |
||||||
|
{files?.length > 0 && ( |
||||||
|
<> |
||||||
|
<Box sx={{ |
||||||
|
display: 'flex', |
||||||
|
flexDirection: 'column', |
||||||
|
gap: '20px', |
||||||
|
width: '50%' |
||||||
|
}}> |
||||||
|
<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> |
||||||
|
</Box> |
||||||
|
{selectedCategoryVideos && ( |
||||||
|
<> |
||||||
|
<Box sx={{ |
||||||
|
display: 'flex', |
||||||
|
flexDirection: 'column', |
||||||
|
gap: '20px', |
||||||
|
width: '50%' |
||||||
|
}}> |
||||||
|
|
||||||
|
|
||||||
|
{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> |
||||||
|
)} |
||||||
|
{selectedSubCategoryVideos && |
||||||
|
subCategories2[selectedSubCategoryVideos?.id] && ( |
||||||
|
<FormControl fullWidth sx={{ marginBottom: 2 }}> |
||||||
|
<InputLabel id="Category"> |
||||||
|
Select a Sub-sub-Category |
||||||
|
</InputLabel> |
||||||
|
<Select |
||||||
|
labelId="Sub-Category" |
||||||
|
input={ |
||||||
|
<OutlinedInput label="Select a Sub-sub-Category" /> |
||||||
|
} |
||||||
|
value={selectedSubCategoryVideos2?.id || ""} |
||||||
|
onChange={(e) => |
||||||
|
handleOptionSubCategoryChangeVideos2( |
||||||
|
e, |
||||||
|
subCategories2[selectedSubCategoryVideos?.id] |
||||||
|
) |
||||||
|
} |
||||||
|
> |
||||||
|
{subCategories2[selectedSubCategoryVideos.id].map( |
||||||
|
(option) => ( |
||||||
|
<MenuItem key={option.id} value={option.id}> |
||||||
|
{option.name} |
||||||
|
</MenuItem> |
||||||
|
) |
||||||
|
)} |
||||||
|
</Select> |
||||||
|
</FormControl> |
||||||
|
)} |
||||||
|
{selectedSubCategoryVideos2 && |
||||||
|
subCategories3[selectedSubCategoryVideos2?.id] && ( |
||||||
|
<FormControl fullWidth sx={{ marginBottom: 2 }}> |
||||||
|
<InputLabel id="Category"> |
||||||
|
Select a Sub-3x-subCategory |
||||||
|
</InputLabel> |
||||||
|
<Select |
||||||
|
labelId="Sub-Category" |
||||||
|
input={ |
||||||
|
<OutlinedInput label="Select a Sub-3x-Category" /> |
||||||
|
} |
||||||
|
value={selectedSubCategoryVideos3?.id || ""} |
||||||
|
onChange={(e) => |
||||||
|
handleOptionSubCategoryChangeVideos3( |
||||||
|
e, |
||||||
|
subCategories3[selectedSubCategoryVideos2?.id] |
||||||
|
) |
||||||
|
} |
||||||
|
> |
||||||
|
{subCategories3[selectedSubCategoryVideos2.id].map( |
||||||
|
(option) => ( |
||||||
|
<MenuItem key={option.id} value={option.id}> |
||||||
|
{option.name} |
||||||
|
</MenuItem> |
||||||
|
) |
||||||
|
)} |
||||||
|
</Select> |
||||||
|
</FormControl> |
||||||
|
)} |
||||||
|
</Box> |
||||||
|
</> |
||||||
|
)} |
||||||
|
|
||||||
|
</> |
||||||
|
)} |
||||||
|
</Box> |
||||||
|
{files?.length > 0 && ( |
||||||
|
<> |
||||||
|
<CustomInputField |
||||||
|
name="title" |
||||||
|
label="Title of share" |
||||||
|
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 share |
||||||
|
</Typography> |
||||||
|
<TextEditor |
||||||
|
inlineContent={description} |
||||||
|
setInlineContent={(value) => { |
||||||
|
setDescription(value); |
||||||
|
}} |
||||||
|
/> |
||||||
|
</> |
||||||
|
)} |
||||||
|
</> |
||||||
|
|
||||||
|
<CrowdfundActionButtonRow> |
||||||
|
<CrowdfundActionButton |
||||||
|
onClick={() => { |
||||||
|
onClose(); |
||||||
|
}} |
||||||
|
variant="contained" |
||||||
|
color="error" |
||||||
|
> |
||||||
|
Cancel |
||||||
|
</CrowdfundActionButton> |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: "flex", |
||||||
|
gap: "20px", |
||||||
|
alignItems: "center", |
||||||
|
}} |
||||||
|
> |
||||||
|
<CrowdfundActionButton |
||||||
|
variant="contained" |
||||||
|
onClick={() => { |
||||||
|
publishQDNResource(); |
||||||
|
}} |
||||||
|
> |
||||||
|
Publish |
||||||
|
</CrowdfundActionButton> |
||||||
|
</Box> |
||||||
|
</CrowdfundActionButtonRow> |
||||||
|
</ModalBody> |
||||||
|
</Modal> |
||||||
|
{isOpenMultiplePublish && ( |
||||||
|
<MultiplePublish |
||||||
|
isOpen={isOpenMultiplePublish} |
||||||
|
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,587 @@ |
|||||||
|
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, |
||||||
|
fontFamily: "Cairo" |
||||||
|
})) |
||||||
|
|
||||||
|
export const CustomSelect = styled(Select)(({ theme }) => ({ |
||||||
|
fontFamily: "Mulish", |
||||||
|
fontSize: "19px", |
||||||
|
letterSpacing: "0px", |
||||||
|
fontWeight: 400, |
||||||
|
color: theme.palette.text.primary, |
||||||
|
backgroundColor: theme.palette.background.default, |
||||||
|
'& .MuiSelect-select': { |
||||||
|
padding: '12px', |
||||||
|
fontFamily: "Mulish", |
||||||
|
fontSize: "19px", |
||||||
|
letterSpacing: "0px", |
||||||
|
fontWeight: 400, |
||||||
|
borderRadius: theme.shape.borderRadius, // Match border radius
|
||||||
|
}, |
||||||
|
'&:before': { |
||||||
|
// Underline style
|
||||||
|
borderBottomColor: theme.palette.mode === "light" ? "#B2BAC2" : "#c9cccf", |
||||||
|
}, |
||||||
|
'&:after': { |
||||||
|
// Underline style when focused
|
||||||
|
borderBottomColor: theme.palette.secondary.main, |
||||||
|
}, |
||||||
|
'& .MuiOutlinedInput-root': { |
||||||
|
'& fieldset': { |
||||||
|
borderColor: "#E0E3E7", |
||||||
|
}, |
||||||
|
'&:hover fieldset': { |
||||||
|
borderColor: "#B2BAC2", |
||||||
|
}, |
||||||
|
'&.Mui-focused fieldset': { |
||||||
|
borderColor: "#6F7E8C", |
||||||
|
}, |
||||||
|
}, |
||||||
|
'& .MuiInputBase-root': { |
||||||
|
fontFamily: "Mulish", |
||||||
|
fontSize: "19px", |
||||||
|
letterSpacing: "0px", |
||||||
|
fontWeight: 400, |
||||||
|
color: theme.palette.text.primary, |
||||||
|
}, |
||||||
|
})); |
@ -0,0 +1,687 @@ |
|||||||
|
import React, { useEffect, useState } from "react"; |
||||||
|
import { |
||||||
|
AddCoverImageButton, |
||||||
|
AddLogoIcon, |
||||||
|
CoverImagePreview, |
||||||
|
CrowdfundActionButton, |
||||||
|
CrowdfundActionButtonRow, |
||||||
|
CustomInputField, |
||||||
|
CustomSelect, |
||||||
|
LogoPreviewRow, |
||||||
|
ModalBody, |
||||||
|
NewCrowdfundTitle, |
||||||
|
StyledButton, |
||||||
|
TimesIcon, |
||||||
|
} from "./Upload-styles"; |
||||||
|
import { |
||||||
|
Box, |
||||||
|
Button, |
||||||
|
FormControl, |
||||||
|
Input, |
||||||
|
InputLabel, |
||||||
|
MenuItem, |
||||||
|
Modal, |
||||||
|
OutlinedInput, |
||||||
|
Select, |
||||||
|
SelectChangeEvent, |
||||||
|
Typography, |
||||||
|
useTheme, |
||||||
|
} from "@mui/material"; |
||||||
|
import RemoveIcon from "@mui/icons-material/Remove"; |
||||||
|
import ShortUniqueId from "short-unique-id"; |
||||||
|
import { useDispatch, useSelector } from "react-redux"; |
||||||
|
import AddBoxIcon from "@mui/icons-material/AddBox"; |
||||||
|
import { useDropzone } from "react-dropzone"; |
||||||
|
import AddIcon from "@mui/icons-material/Add"; |
||||||
|
|
||||||
|
import { setNotification } from "../../state/features/notificationsSlice"; |
||||||
|
import { objectToBase64, uint8ArrayToBase64 } from "../../utils/toBase64"; |
||||||
|
import { RootState } from "../../state/store"; |
||||||
|
import { |
||||||
|
upsertVideosBeginning, |
||||||
|
addToHashMap, |
||||||
|
upsertVideos, |
||||||
|
} from "../../state/features/videoSlice"; |
||||||
|
import ImageUploader from "../common/ImageUploader"; |
||||||
|
import { |
||||||
|
QTUBE_PLAYLIST_BASE, |
||||||
|
QTUBE_VIDEO_BASE, |
||||||
|
categories, |
||||||
|
subCategories, |
||||||
|
subCategories2, |
||||||
|
subCategories3, |
||||||
|
} from "../../constants"; |
||||||
|
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublish"; |
||||||
|
import { |
||||||
|
CrowdfundSubTitle, |
||||||
|
CrowdfundSubTitleRow, |
||||||
|
} from "../EditPlaylist/Upload-styles"; |
||||||
|
import { CardContentContainerComment } from "../common/Comments/Comments-styles"; |
||||||
|
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 UploadVideo = ({ editId, editContent }: NewCrowdfundProps) => { |
||||||
|
const theme = useTheme(); |
||||||
|
const dispatch = useDispatch(); |
||||||
|
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false); |
||||||
|
const username = useSelector((state: RootState) => state.auth?.user?.name); |
||||||
|
const userAddress = useSelector( |
||||||
|
(state: RootState) => state.auth?.user?.address |
||||||
|
); |
||||||
|
const [files, setFiles] = useState<VideoFile[]>([]); |
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState<boolean>(false); |
||||||
|
const [title, setTitle] = useState<string>(""); |
||||||
|
const [description, setDescription] = useState<string>(""); |
||||||
|
const [step, setStep] = useState<string>("videos"); |
||||||
|
const [playlistCoverImage, setPlaylistCoverImage] = useState<null | string>( |
||||||
|
null |
||||||
|
); |
||||||
|
const [selectExistingPlaylist, setSelectExistingPlaylist] = |
||||||
|
useState<any>(null); |
||||||
|
const [playlistTitle, setPlaylistTitle] = useState<string>(""); |
||||||
|
const [playlistDescription, setPlaylistDescription] = useState<string>(""); |
||||||
|
const [selectedCategory, setSelectedCategory] = useState<any>(null); |
||||||
|
const [selectedSubCategory, setSelectedSubCategory] = useState<any>(null); |
||||||
|
|
||||||
|
const [selectedCategoryVideos, setSelectedCategoryVideos] = |
||||||
|
useState<any>(null); |
||||||
|
const [selectedSubCategoryVideos, setSelectedSubCategoryVideos] = |
||||||
|
useState<any>(null); |
||||||
|
const [selectedSubCategoryVideos2, setSelectedSubCategoryVideos2] = |
||||||
|
useState<any>(null); |
||||||
|
const [selectedSubCategoryVideos3, setSelectedSubCategoryVideos3] = |
||||||
|
useState<any>(null); |
||||||
|
|
||||||
|
const [playlistSetting, setPlaylistSetting] = useState<null | string>(null); |
||||||
|
const [publishes, setPublishes] = useState<any[]>([]); |
||||||
|
const { getRootProps, getInputProps } = useDropzone({ |
||||||
|
maxFiles: 10, |
||||||
|
maxSize: 419430400, // 400 MB in bytes
|
||||||
|
onDrop: (acceptedFiles, rejectedFiles) => { |
||||||
|
const formatArray = acceptedFiles.map((item) => { |
||||||
|
return { |
||||||
|
file: item, |
||||||
|
title: "", |
||||||
|
description: "", |
||||||
|
coverImage: "", |
||||||
|
}; |
||||||
|
}); |
||||||
|
|
||||||
|
setFiles((prev) => [...prev, ...formatArray]); |
||||||
|
|
||||||
|
let errorString = null; |
||||||
|
rejectedFiles.forEach(({ file, errors }) => { |
||||||
|
errors.forEach((error) => { |
||||||
|
if (error.code === "file-too-large") { |
||||||
|
errorString = "File must be under 400mb"; |
||||||
|
} |
||||||
|
console.log(`Error with file ${file.name}: ${error.message}`); |
||||||
|
}); |
||||||
|
}); |
||||||
|
if (errorString) { |
||||||
|
const notificationObj = { |
||||||
|
msg: errorString, |
||||||
|
alertType: "error", |
||||||
|
}; |
||||||
|
|
||||||
|
dispatch(setNotification(notificationObj)); |
||||||
|
} |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (editContent) { |
||||||
|
} |
||||||
|
}, [editContent]); |
||||||
|
|
||||||
|
const onClose = () => { |
||||||
|
setIsOpen(false); |
||||||
|
}; |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async function publishQDNResource() { |
||||||
|
|
||||||
|
try { |
||||||
|
if (!userAddress) throw new Error("Unable to locate user address"); |
||||||
|
|
||||||
|
if (!title) throw new Error("Please enter a title"); |
||||||
|
if (!description) throw new Error("Please enter a description"); |
||||||
|
if (!selectedCategoryVideos) throw new Error("Please select a category"); |
||||||
|
if(files.length === 0) throw new Error("Add at least one file"); |
||||||
|
let errorMsg = ""; |
||||||
|
let name = ""; |
||||||
|
if (username) { |
||||||
|
name = username; |
||||||
|
} |
||||||
|
if (!name) { |
||||||
|
errorMsg = |
||||||
|
"Cannot publish without access to your name. Please authenticate."; |
||||||
|
} |
||||||
|
|
||||||
|
if (editId && editContent?.user !== name) { |
||||||
|
errorMsg = "Cannot publish another user's resource"; |
||||||
|
} |
||||||
|
|
||||||
|
if (errorMsg) { |
||||||
|
dispatch( |
||||||
|
setNotification({ |
||||||
|
msg: errorMsg, |
||||||
|
alertType: "error", |
||||||
|
}) |
||||||
|
); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
let fileReferences = [] |
||||||
|
|
||||||
|
let listOfPublishes = []; |
||||||
|
|
||||||
|
const fullDescription = extractTextFromHTML(description); |
||||||
|
const category = selectedCategoryVideos.id; |
||||||
|
const subcategory = selectedSubCategoryVideos?.id || ""; |
||||||
|
const subcategory2 = selectedSubCategoryVideos2?.id || ""; |
||||||
|
const subcategory3 = selectedSubCategoryVideos3?.id || ""; |
||||||
|
|
||||||
|
const sanitizeTitle = title |
||||||
|
.replace(/[^a-zA-Z0-9\s-]/g, "") |
||||||
|
.replace(/\s+/g, "-") |
||||||
|
.replace(/-+/g, "-") |
||||||
|
.trim() |
||||||
|
.toLowerCase(); |
||||||
|
|
||||||
|
for (const publish of files) { |
||||||
|
|
||||||
|
const file = publish.file; |
||||||
|
const id = uid(); |
||||||
|
|
||||||
|
const identifier = `${QTUBE_VIDEO_BASE}${sanitizeTitle.slice(0, 30)}_${id}`; |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let fileExtension = ""; |
||||||
|
const fileExtensionSplit = file?.name?.split("."); |
||||||
|
if (fileExtensionSplit?.length > 1) { |
||||||
|
fileExtension = fileExtensionSplit?.pop() || ""; |
||||||
|
} |
||||||
|
let firstPartName = fileExtensionSplit[0] |
||||||
|
|
||||||
|
let filename = firstPartName.slice(0, 15); |
||||||
|
|
||||||
|
// Step 1: Replace all white spaces with underscores
|
||||||
|
|
||||||
|
// Replace all forms of whitespace (including non-standard ones) with underscores
|
||||||
|
let stringWithUnderscores = filename.replace(/[\s\uFEFF\xA0]+/g, "_"); |
||||||
|
|
||||||
|
// Remove all non-alphanumeric characters (except underscores)
|
||||||
|
let alphanumericString = stringWithUnderscores.replace( |
||||||
|
/[^a-zA-Z0-9_]/g, |
||||||
|
"" |
||||||
|
); |
||||||
|
|
||||||
|
if(fileExtension){ |
||||||
|
filename = `${alphanumericString.trim()}.${fileExtension}` |
||||||
|
} else { |
||||||
|
filename = alphanumericString |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let metadescription = |
||||||
|
`**cat:${category};sub:${subcategory};sub2:${subcategory2};sub3:${subcategory3}**` + |
||||||
|
fullDescription.slice(0, 150); |
||||||
|
|
||||||
|
|
||||||
|
const requestBodyVideo: any = { |
||||||
|
action: "PUBLISH_QDN_RESOURCE", |
||||||
|
name: name, |
||||||
|
service: "FILE", |
||||||
|
file, |
||||||
|
title: title.slice(0, 50), |
||||||
|
description: metadescription, |
||||||
|
identifier, |
||||||
|
filename, |
||||||
|
tag1: QTUBE_VIDEO_BASE, |
||||||
|
}; |
||||||
|
listOfPublishes.push(requestBodyVideo); |
||||||
|
fileReferences.push({ |
||||||
|
filename: file.name, |
||||||
|
identifier, |
||||||
|
name, |
||||||
|
service: 'FILE', |
||||||
|
mimetype: file.type, |
||||||
|
size: file.size |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const idMeta = uid(); |
||||||
|
const identifier = `${QTUBE_VIDEO_BASE}${sanitizeTitle.slice(0, 30)}_${idMeta}`; |
||||||
|
const fileObject: any = { |
||||||
|
title, |
||||||
|
version: 1, |
||||||
|
fullDescription, |
||||||
|
htmlDescription: description, |
||||||
|
commentsId: `${QTUBE_VIDEO_BASE}_cm_${idMeta}`, |
||||||
|
category, |
||||||
|
subcategory, |
||||||
|
subcategory2, |
||||||
|
subcategory3, |
||||||
|
files: fileReferences |
||||||
|
}; |
||||||
|
|
||||||
|
let metadescription = |
||||||
|
`**cat:${category};sub:${subcategory};sub2:${subcategory2}**` + |
||||||
|
fullDescription.slice(0, 150); |
||||||
|
|
||||||
|
const crowdfundObjectToBase64 = await objectToBase64(fileObject); |
||||||
|
// Description is obtained from raw data
|
||||||
|
const requestBodyJson: any = { |
||||||
|
action: "PUBLISH_QDN_RESOURCE", |
||||||
|
name: name, |
||||||
|
service: "DOCUMENT", |
||||||
|
data64: crowdfundObjectToBase64, |
||||||
|
title: title.slice(0, 50), |
||||||
|
description: metadescription, |
||||||
|
identifier: identifier + "_metadata", |
||||||
|
tag1: QTUBE_VIDEO_BASE, |
||||||
|
filename: `video_metadata.json`, |
||||||
|
}; |
||||||
|
listOfPublishes.push(requestBodyJson); |
||||||
|
setPublishes(listOfPublishes); |
||||||
|
setIsOpenMultiplePublish(true); |
||||||
|
} catch (error: any) { |
||||||
|
let notificationObj: any = null; |
||||||
|
if (typeof error === "string") { |
||||||
|
notificationObj = { |
||||||
|
msg: error || "Failed to publish share", |
||||||
|
alertType: "error", |
||||||
|
}; |
||||||
|
} else if (typeof error?.error === "string") { |
||||||
|
notificationObj = { |
||||||
|
msg: error?.error || "Failed to publish share", |
||||||
|
alertType: "error", |
||||||
|
}; |
||||||
|
} else { |
||||||
|
notificationObj = { |
||||||
|
msg: error?.message || "Failed to publish share", |
||||||
|
alertType: "error", |
||||||
|
}; |
||||||
|
} |
||||||
|
if (!notificationObj) return; |
||||||
|
dispatch(setNotification(notificationObj)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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 handleOptionSubCategoryChangeVideos2 = ( |
||||||
|
event: SelectChangeEvent<string>, |
||||||
|
subcategories: any[] |
||||||
|
) => { |
||||||
|
const optionId = event.target.value; |
||||||
|
const selectedOption = subcategories.find( |
||||||
|
(option) => option.id === +optionId |
||||||
|
); |
||||||
|
setSelectedSubCategoryVideos2(selectedOption || null); |
||||||
|
}; |
||||||
|
|
||||||
|
const handleOptionSubCategoryChangeVideos3 = ( |
||||||
|
event: SelectChangeEvent<string>, |
||||||
|
subcategories: any[] |
||||||
|
) => { |
||||||
|
const optionId = event.target.value; |
||||||
|
const selectedOption = subcategories.find( |
||||||
|
(option) => option.id === +optionId |
||||||
|
); |
||||||
|
setSelectedSubCategoryVideos3(selectedOption || null); |
||||||
|
}; |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
{username && ( |
||||||
|
<> |
||||||
|
{editId ? null : ( |
||||||
|
<StyledButton |
||||||
|
color="primary" |
||||||
|
startIcon={<AddBoxIcon />} |
||||||
|
onClick={() => { |
||||||
|
setIsOpen(true); |
||||||
|
}} |
||||||
|
> |
||||||
|
share |
||||||
|
</StyledButton> |
||||||
|
)} |
||||||
|
</> |
||||||
|
)} |
||||||
|
|
||||||
|
<Modal |
||||||
|
open={isOpen} |
||||||
|
aria-labelledby="modal-title" |
||||||
|
aria-describedby="modal-description" |
||||||
|
> |
||||||
|
<ModalBody> |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: "flex", |
||||||
|
alignItems: "center", |
||||||
|
justifyContent: "space-between", |
||||||
|
}} |
||||||
|
> |
||||||
|
|
||||||
|
<NewCrowdfundTitle>Share</NewCrowdfundTitle> |
||||||
|
|
||||||
|
</Box> |
||||||
|
|
||||||
|
{step === "videos" && ( |
||||||
|
<> |
||||||
|
<Box |
||||||
|
{...getRootProps()} |
||||||
|
sx={{ |
||||||
|
border: "1px dashed gray", |
||||||
|
padding: 2, |
||||||
|
textAlign: "center", |
||||||
|
marginBottom: 2, |
||||||
|
cursor: "pointer", |
||||||
|
}} |
||||||
|
> |
||||||
|
<input {...getInputProps()} /> |
||||||
|
<Typography> |
||||||
|
Drag and drop files here or click to select files |
||||||
|
</Typography> |
||||||
|
</Box> |
||||||
|
{files.map((file, index) => { |
||||||
|
return ( |
||||||
|
<React.Fragment key={index}> |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: "flex", |
||||||
|
justifyContent: "space-between", |
||||||
|
alignItems: "center", |
||||||
|
}} |
||||||
|
> |
||||||
|
<Typography>{file?.file?.name}</Typography> |
||||||
|
<RemoveIcon |
||||||
|
onClick={() => { |
||||||
|
setFiles((prev) => { |
||||||
|
const copyPrev = [...prev]; |
||||||
|
copyPrev.splice(index, 1); |
||||||
|
return copyPrev; |
||||||
|
}); |
||||||
|
}} |
||||||
|
sx={{ |
||||||
|
cursor: "pointer", |
||||||
|
}} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
</React.Fragment> |
||||||
|
); |
||||||
|
})} |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: "flex", |
||||||
|
gap: "20px", |
||||||
|
alignItems: "flex-start", |
||||||
|
}} |
||||||
|
> |
||||||
|
{files?.length > 0 && ( |
||||||
|
<> |
||||||
|
<Box sx={{ |
||||||
|
display: 'flex', |
||||||
|
flexDirection: 'column', |
||||||
|
gap: '20px', |
||||||
|
width: '50%' |
||||||
|
}}> |
||||||
|
<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> |
||||||
|
</Box> |
||||||
|
{selectedCategoryVideos && ( |
||||||
|
<> |
||||||
|
<Box sx={{ |
||||||
|
display: 'flex', |
||||||
|
flexDirection: 'column', |
||||||
|
gap: '20px', |
||||||
|
width: '50%' |
||||||
|
}}> |
||||||
|
|
||||||
|
|
||||||
|
{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> |
||||||
|
)} |
||||||
|
{selectedSubCategoryVideos && |
||||||
|
subCategories2[selectedSubCategoryVideos?.id] && ( |
||||||
|
<FormControl fullWidth sx={{ marginBottom: 2 }}> |
||||||
|
<InputLabel id="Category"> |
||||||
|
Select a Sub-sub-Category |
||||||
|
</InputLabel> |
||||||
|
<Select |
||||||
|
labelId="Sub-Category" |
||||||
|
input={ |
||||||
|
<OutlinedInput label="Select a Sub-sub-Category" /> |
||||||
|
} |
||||||
|
value={selectedSubCategoryVideos2?.id || ""} |
||||||
|
onChange={(e) => |
||||||
|
handleOptionSubCategoryChangeVideos2( |
||||||
|
e, |
||||||
|
subCategories2[selectedSubCategoryVideos?.id] |
||||||
|
) |
||||||
|
} |
||||||
|
> |
||||||
|
{subCategories2[selectedSubCategoryVideos.id].map( |
||||||
|
(option) => ( |
||||||
|
<MenuItem key={option.id} value={option.id}> |
||||||
|
{option.name} |
||||||
|
</MenuItem> |
||||||
|
) |
||||||
|
)} |
||||||
|
</Select> |
||||||
|
</FormControl> |
||||||
|
)} |
||||||
|
{selectedSubCategoryVideos2 && |
||||||
|
subCategories3[selectedSubCategoryVideos2?.id] && ( |
||||||
|
<FormControl fullWidth sx={{ marginBottom: 2 }}> |
||||||
|
<InputLabel id="Category"> |
||||||
|
Select a Sub-3x-subCategory |
||||||
|
</InputLabel> |
||||||
|
<Select |
||||||
|
labelId="Sub-Category" |
||||||
|
input={ |
||||||
|
<OutlinedInput label="Select a Sub-3x-Category" /> |
||||||
|
} |
||||||
|
value={selectedSubCategoryVideos3?.id || ""} |
||||||
|
onChange={(e) => |
||||||
|
handleOptionSubCategoryChangeVideos3( |
||||||
|
e, |
||||||
|
subCategories3[selectedSubCategoryVideos2?.id] |
||||||
|
) |
||||||
|
} |
||||||
|
> |
||||||
|
{subCategories3[selectedSubCategoryVideos2.id].map( |
||||||
|
(option) => ( |
||||||
|
<MenuItem key={option.id} value={option.id}> |
||||||
|
{option.name} |
||||||
|
</MenuItem> |
||||||
|
) |
||||||
|
)} |
||||||
|
</Select> |
||||||
|
</FormControl> |
||||||
|
)} |
||||||
|
</Box> |
||||||
|
</> |
||||||
|
)} |
||||||
|
|
||||||
|
</> |
||||||
|
)} |
||||||
|
</Box> |
||||||
|
{files?.length > 0 && ( |
||||||
|
<> |
||||||
|
<CustomInputField |
||||||
|
name="title" |
||||||
|
label="Title of share" |
||||||
|
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 share |
||||||
|
</Typography> |
||||||
|
<TextEditor |
||||||
|
inlineContent={description} |
||||||
|
setInlineContent={(value) => { |
||||||
|
setDescription(value); |
||||||
|
}} |
||||||
|
/> |
||||||
|
</> |
||||||
|
)} |
||||||
|
|
||||||
|
</> |
||||||
|
)} |
||||||
|
<CrowdfundActionButtonRow> |
||||||
|
<CrowdfundActionButton |
||||||
|
onClick={() => { |
||||||
|
onClose(); |
||||||
|
}} |
||||||
|
variant="contained" |
||||||
|
color="error" |
||||||
|
> |
||||||
|
Cancel |
||||||
|
</CrowdfundActionButton> |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: "flex", |
||||||
|
gap: "20px", |
||||||
|
alignItems: "center", |
||||||
|
}} |
||||||
|
> |
||||||
|
<CrowdfundActionButton |
||||||
|
variant="contained" |
||||||
|
onClick={() => { |
||||||
|
publishQDNResource(); |
||||||
|
}} |
||||||
|
> |
||||||
|
Publish |
||||||
|
</CrowdfundActionButton> |
||||||
|
</Box> |
||||||
|
</CrowdfundActionButtonRow> |
||||||
|
</ModalBody> |
||||||
|
</Modal> |
||||||
|
|
||||||
|
{isOpenMultiplePublish && ( |
||||||
|
<MultiplePublish |
||||||
|
isOpen={isOpenMultiplePublish} |
||||||
|
onSubmit={() => { |
||||||
|
setIsOpenMultiplePublish(false); |
||||||
|
setIsOpen(false); |
||||||
|
setFiles([]); |
||||||
|
setStep("videos"); |
||||||
|
setPlaylistCoverImage(null); |
||||||
|
setPlaylistTitle(""); |
||||||
|
setPlaylistDescription(""); |
||||||
|
setSelectedCategory(null); |
||||||
|
setSelectedSubCategory(null); |
||||||
|
setSelectedCategoryVideos(null); |
||||||
|
setSelectedSubCategoryVideos(null); |
||||||
|
setPlaylistSetting(null); |
||||||
|
dispatch( |
||||||
|
setNotification({ |
||||||
|
msg: "Videos published", |
||||||
|
alertType: "success", |
||||||
|
}) |
||||||
|
); |
||||||
|
}} |
||||||
|
publishes={publishes} |
||||||
|
/> |
||||||
|
)} |
||||||
|
</> |
||||||
|
); |
||||||
|
}; |
@ -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,204 @@ |
|||||||
|
import React, { useState, useEffect } from 'react' |
||||||
|
import { |
||||||
|
Accordion, |
||||||
|
AccordionDetails, |
||||||
|
AccordionSummary, |
||||||
|
Box, |
||||||
|
Button, |
||||||
|
LinearProgress, |
||||||
|
List, |
||||||
|
ListItem, |
||||||
|
ListItemIcon, |
||||||
|
Popover, |
||||||
|
Typography, |
||||||
|
useTheme |
||||||
|
} from '@mui/material' |
||||||
|
import { Movie } from '@mui/icons-material' |
||||||
|
import { useSelector } from 'react-redux' |
||||||
|
import { RootState } from '../../state/store' |
||||||
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore' |
||||||
|
import { useLocation, useNavigate } from 'react-router-dom' |
||||||
|
import { DownloadingLight } from '../../assets/svgs/DownloadingLight' |
||||||
|
import { DownloadedLight } from '../../assets/svgs/DownloadedLight' |
||||||
|
import AttachFileIcon from "@mui/icons-material/AttachFile"; |
||||||
|
|
||||||
|
export const DownloadTaskManager: React.FC = () => { |
||||||
|
const { downloads } = useSelector((state: RootState) => state.global) |
||||||
|
const location = useLocation() |
||||||
|
const theme = useTheme() |
||||||
|
const [visible, setVisible] = useState(false) |
||||||
|
const [hidden, setHidden] = useState(true) |
||||||
|
const navigate = useNavigate() |
||||||
|
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null); |
||||||
|
|
||||||
|
|
||||||
|
const [openDownload, setOpenDownload] = useState<boolean>(false); |
||||||
|
|
||||||
|
|
||||||
|
const handleClick = (event?: React.MouseEvent<HTMLDivElement>) => { |
||||||
|
const target = event?.currentTarget as unknown as HTMLButtonElement | null; |
||||||
|
setAnchorEl(target); |
||||||
|
}; |
||||||
|
|
||||||
|
const handleCloseDownload = () => { |
||||||
|
setAnchorEl(null); |
||||||
|
setOpenDownload(false); |
||||||
|
}; |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
// Simulate downloads for demo purposes
|
||||||
|
|
||||||
|
if (visible) { |
||||||
|
setTimeout(() => { |
||||||
|
setHidden(true) |
||||||
|
setVisible(false) |
||||||
|
}, 3000) |
||||||
|
} |
||||||
|
}, [visible]) |
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (Object.keys(downloads).length === 0) return |
||||||
|
setVisible(true) |
||||||
|
setHidden(false) |
||||||
|
}, [downloads]) |
||||||
|
|
||||||
|
|
||||||
|
if ( |
||||||
|
!downloads || |
||||||
|
Object.keys(downloads).length === 0 |
||||||
|
) |
||||||
|
return null |
||||||
|
|
||||||
|
|
||||||
|
let downloadInProgress = false |
||||||
|
if(Object.keys(downloads).find((key)=> (downloads[key]?.status?.status !== 'READY' && downloads[key]?.status?.status !== 'DOWNLOADED'))){ |
||||||
|
downloadInProgress = true |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<Box> |
||||||
|
<Button onClick={(e: any) => { |
||||||
|
handleClick(e); |
||||||
|
setOpenDownload(true); |
||||||
|
}}> |
||||||
|
{downloadInProgress ? ( |
||||||
|
<DownloadingLight height='24px' width='24px' className='download-icon' /> |
||||||
|
) : ( |
||||||
|
<DownloadedLight height='24px' width='24px' /> |
||||||
|
)} |
||||||
|
|
||||||
|
</Button> |
||||||
|
|
||||||
|
<Popover |
||||||
|
id={"download-popover"} |
||||||
|
open={openDownload} |
||||||
|
anchorEl={anchorEl} |
||||||
|
onClose={handleCloseDownload} |
||||||
|
anchorOrigin={{ |
||||||
|
vertical: "bottom", |
||||||
|
horizontal: "left" |
||||||
|
}} |
||||||
|
> |
||||||
|
<List |
||||||
|
sx={{ |
||||||
|
maxHeight: '50vh', |
||||||
|
overflow: 'auto', |
||||||
|
width: '250px', |
||||||
|
gap: '5px', |
||||||
|
display: 'flex', |
||||||
|
flexDirection: 'column', |
||||||
|
|
||||||
|
}} |
||||||
|
> |
||||||
|
{Object.keys(downloads) |
||||||
|
.map((download: any) => { |
||||||
|
const downloadObj = downloads[download] |
||||||
|
const progress = downloads[download]?.status?.percentLoaded || 0 |
||||||
|
const status = downloads[download]?.status?.status |
||||||
|
const service = downloads[download]?.service |
||||||
|
return ( |
||||||
|
<ListItem |
||||||
|
key={downloadObj?.identifier} |
||||||
|
sx={{ |
||||||
|
display: 'flex', |
||||||
|
flexDirection: 'column', |
||||||
|
width: '100%', |
||||||
|
justifyContent: 'center', |
||||||
|
background: theme.palette.primary.main, |
||||||
|
color: theme.palette.text.primary, |
||||||
|
cursor: 'pointer', |
||||||
|
padding: '2px', |
||||||
|
|
||||||
|
}} |
||||||
|
onClick={() => { |
||||||
|
const id = downloadObj?.properties?.jsonId |
||||||
|
if (!id) return |
||||||
|
|
||||||
|
navigate( |
||||||
|
`/share/${downloadObj?.properties?.name}/${id}` |
||||||
|
) |
||||||
|
}} |
||||||
|
> |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
width: '100%', |
||||||
|
display: 'flex', |
||||||
|
alignItems: 'center', |
||||||
|
justifyContent: 'space-between' |
||||||
|
}} |
||||||
|
> |
||||||
|
<ListItemIcon> |
||||||
|
|
||||||
|
<AttachFileIcon sx={{ color: theme.palette.text.primary }} /> |
||||||
|
|
||||||
|
</ListItemIcon> |
||||||
|
|
||||||
|
<Box |
||||||
|
sx={{ width: '100px', marginLeft: 1, marginRight: 1 }} |
||||||
|
> |
||||||
|
<LinearProgress |
||||||
|
variant="determinate" |
||||||
|
value={progress} |
||||||
|
sx={{ |
||||||
|
borderRadius: '5px', |
||||||
|
color: theme.palette.secondary.main |
||||||
|
}} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
<Typography |
||||||
|
sx={{ |
||||||
|
fontFamily: 'Arial', |
||||||
|
color: theme.palette.text.primary |
||||||
|
}} |
||||||
|
variant="caption" |
||||||
|
> |
||||||
|
{`${progress?.toFixed(0)}%`}{' '} |
||||||
|
{status && status === 'REFETCHING' && '- refetching'} |
||||||
|
{status && status === 'DOWNLOADED' && '- building'} |
||||||
|
</Typography> |
||||||
|
</Box> |
||||||
|
<Typography |
||||||
|
sx={{ |
||||||
|
fontSize: '10px', |
||||||
|
width: '100%', |
||||||
|
textAlign: 'end', |
||||||
|
fontFamily: 'Arial', |
||||||
|
color: theme.palette.text.primary, |
||||||
|
textOverflow: 'ellipsis', |
||||||
|
whiteSpace: 'nowrap', |
||||||
|
overflow: 'hidden', |
||||||
|
}} |
||||||
|
> |
||||||
|
{downloadObj?.identifier} |
||||||
|
</Typography> |
||||||
|
</ListItem> |
||||||
|
) |
||||||
|
})} |
||||||
|
</List> |
||||||
|
</Popover> |
||||||
|
|
||||||
|
</Box> |
||||||
|
|
||||||
|
) |
||||||
|
} |
@ -0,0 +1,436 @@ |
|||||||
|
import * as React from "react"; |
||||||
|
import { styled, useTheme } from "@mui/material/styles"; |
||||||
|
import Box from "@mui/material/Box"; |
||||||
|
import Typography from "@mui/material/Typography"; |
||||||
|
import { useDispatch, useSelector } from "react-redux"; |
||||||
|
import { CircularProgress } from "@mui/material"; |
||||||
|
import AttachFileIcon from "@mui/icons-material/AttachFile"; |
||||||
|
import { MyContext } from "../../wrappers/DownloadWrapper"; |
||||||
|
import { RootState } from "../../state/store"; |
||||||
|
import { setNotification } from "../../state/features/notificationsSlice"; |
||||||
|
|
||||||
|
const Widget = styled("div")(({ theme }) => ({ |
||||||
|
padding: 8, |
||||||
|
borderRadius: 10, |
||||||
|
maxWidth: 350, |
||||||
|
position: "relative", |
||||||
|
zIndex: 1, |
||||||
|
backdropFilter: "blur(40px)", |
||||||
|
background: "skyblue", |
||||||
|
transition: "0.2s all", |
||||||
|
"&:hover": { |
||||||
|
opacity: 0.75, |
||||||
|
}, |
||||||
|
})); |
||||||
|
|
||||||
|
const CoverImage = styled("div")({ |
||||||
|
width: 40, |
||||||
|
height: 40, |
||||||
|
objectFit: "cover", |
||||||
|
overflow: "hidden", |
||||||
|
flexShrink: 0, |
||||||
|
borderRadius: 8, |
||||||
|
backgroundColor: "rgba(0,0,0,0.08)", |
||||||
|
"& > img": { |
||||||
|
width: "100%", |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
interface IAudioElement { |
||||||
|
title: string; |
||||||
|
description?: string; |
||||||
|
author?: string; |
||||||
|
fileInfo?: any; |
||||||
|
postId?: string; |
||||||
|
user?: string; |
||||||
|
children?: React.ReactNode; |
||||||
|
mimeType?: string; |
||||||
|
disable?: boolean; |
||||||
|
mode?: string; |
||||||
|
otherUser?: string; |
||||||
|
customStyles?: any; |
||||||
|
jsonId:string; |
||||||
|
} |
||||||
|
|
||||||
|
interface CustomWindow extends Window { |
||||||
|
showSaveFilePicker: any; // Replace 'any' with the appropriate type if you know it
|
||||||
|
} |
||||||
|
|
||||||
|
const customWindow = window as unknown as CustomWindow; |
||||||
|
|
||||||
|
export default function FileElement({ |
||||||
|
title, |
||||||
|
description, |
||||||
|
author, |
||||||
|
fileInfo, |
||||||
|
children, |
||||||
|
mimeType, |
||||||
|
disable, |
||||||
|
customStyles, |
||||||
|
jsonId |
||||||
|
}: IAudioElement) { |
||||||
|
const { downloadVideo } = React.useContext(MyContext); |
||||||
|
const [startedDownload, setStartedDownload] = React.useState<boolean>(false) |
||||||
|
const [isLoading, setIsLoading] = React.useState<boolean>(false); |
||||||
|
const [fileProperties, setFileProperties] = React.useState<any>(null); |
||||||
|
const [downloadLoader, setDownloadLoader] = React.useState<any>(false); |
||||||
|
const downloads = useSelector((state: RootState) => state.global?.downloads); |
||||||
|
const hasCommencedDownload = React.useRef(false); |
||||||
|
const dispatch = useDispatch(); |
||||||
|
const reDownload = React.useRef<boolean>(false) |
||||||
|
const isFetchingProperties = React.useRef<boolean>(false) |
||||||
|
const download = React.useMemo(() => { |
||||||
|
if (!downloads || !fileInfo?.identifier) return {}; |
||||||
|
const findDownload = downloads[fileInfo?.identifier]; |
||||||
|
|
||||||
|
if (!findDownload) return {}; |
||||||
|
return findDownload; |
||||||
|
}, [downloads, fileInfo]); |
||||||
|
|
||||||
|
const resourceStatus = React.useMemo(() => { |
||||||
|
return download?.status || {}; |
||||||
|
}, [download]); |
||||||
|
|
||||||
|
const retryDownload = React.useRef(0); |
||||||
|
|
||||||
|
const handlePlay = async () => { |
||||||
|
if (disable) return; |
||||||
|
hasCommencedDownload.current = true; |
||||||
|
setStartedDownload(true) |
||||||
|
if ( |
||||||
|
resourceStatus?.status === "READY" |
||||||
|
) { |
||||||
|
if (downloadLoader) return; |
||||||
|
|
||||||
|
setDownloadLoader(true); |
||||||
|
let filename = download?.properties?.filename |
||||||
|
let mimeType = download?.properties?.type |
||||||
|
|
||||||
|
try { |
||||||
|
const { name, service, identifier } = fileInfo; |
||||||
|
|
||||||
|
const res = await qortalRequest({ |
||||||
|
action: "GET_QDN_RESOURCE_PROPERTIES", |
||||||
|
name: name, |
||||||
|
service: service, |
||||||
|
identifier: identifier, |
||||||
|
}); |
||||||
|
filename = res?.filename || filename; |
||||||
|
mimeType = res?.mimeType || mimeType; |
||||||
|
} catch (error) { |
||||||
|
|
||||||
|
} |
||||||
|
try { |
||||||
|
const { name, service, identifier } = fileInfo; |
||||||
|
|
||||||
|
const url = `/arbitrary/${service}/${name}/${identifier}`; |
||||||
|
fetch(url) |
||||||
|
.then(response => response.blob()) |
||||||
|
.then(async blob => { |
||||||
|
await qortalRequest({ |
||||||
|
action: "SAVE_FILE", |
||||||
|
blob, |
||||||
|
filename: filename, |
||||||
|
mimeType, |
||||||
|
}); |
||||||
|
}) |
||||||
|
.catch(error => { |
||||||
|
console.error("Error fetching the video:", error); |
||||||
|
}); |
||||||
|
} catch (error: any) { |
||||||
|
let notificationObj: any = null; |
||||||
|
if (typeof error === "string") { |
||||||
|
notificationObj = { |
||||||
|
msg: error || "Failed to send message", |
||||||
|
alertType: "error", |
||||||
|
}; |
||||||
|
} else if (typeof error?.error === "string") { |
||||||
|
notificationObj = { |
||||||
|
msg: error?.error || "Failed to send message", |
||||||
|
alertType: "error", |
||||||
|
}; |
||||||
|
} else { |
||||||
|
notificationObj = { |
||||||
|
msg: error?.message || "Failed to send message", |
||||||
|
alertType: "error", |
||||||
|
}; |
||||||
|
} |
||||||
|
if (!notificationObj) return; |
||||||
|
dispatch(setNotification(notificationObj)); |
||||||
|
} finally { |
||||||
|
setDownloadLoader(false); |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const { name, service, identifier } = fileInfo; |
||||||
|
|
||||||
|
setIsLoading(true); |
||||||
|
downloadVideo({ |
||||||
|
name, |
||||||
|
service, |
||||||
|
identifier, |
||||||
|
properties: { |
||||||
|
...fileInfo, |
||||||
|
jsonId |
||||||
|
}, |
||||||
|
}); |
||||||
|
}; |
||||||
|
|
||||||
|
const refetch = React.useCallback(async () => { |
||||||
|
if (!fileInfo) return |
||||||
|
try { |
||||||
|
const { name, service, identifier } = fileInfo; |
||||||
|
isFetchingProperties.current = true |
||||||
|
await qortalRequest({ |
||||||
|
action: 'GET_QDN_RESOURCE_PROPERTIES', |
||||||
|
name, |
||||||
|
service, |
||||||
|
identifier |
||||||
|
}) |
||||||
|
|
||||||
|
} catch (error) { |
||||||
|
|
||||||
|
} finally { |
||||||
|
isFetchingProperties.current = false |
||||||
|
} |
||||||
|
|
||||||
|
}, [fileInfo]) |
||||||
|
|
||||||
|
const refetchInInterval = ()=> { |
||||||
|
try { |
||||||
|
const interval = setInterval(()=> { |
||||||
|
if(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,434 @@ |
|||||||
|
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 QShareLogo from "../../../assets/img/q-share-icon.webp"; |
||||||
|
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={QShareLogo} |
||||||
|
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 /> |
||||||
|
</> |
||||||
|
)} |
||||||
|
|
||||||
|
|
||||||
|
</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,220 @@ |
|||||||
|
const useTestIdentifiers = true; |
||||||
|
|
||||||
|
export const QTUBE_VIDEO_BASE = useTestIdentifiers |
||||||
|
? "MYTEST_share_vid_" |
||||||
|
: "qshare_file_"; |
||||||
|
|
||||||
|
export const QTUBE_PLAYLIST_BASE = useTestIdentifiers |
||||||
|
? "MYTEST_share_playlist_" |
||||||
|
: "qshare_playlist_"; |
||||||
|
|
||||||
|
export const COMMENT_BASE = useTestIdentifiers |
||||||
|
? "qcomment_v1_MYTEST_" |
||||||
|
: "qcomment_v1_qshare_"; |
||||||
|
|
||||||
|
interface SubCategory { |
||||||
|
id: number; |
||||||
|
name: string; |
||||||
|
} |
||||||
|
|
||||||
|
interface CategoryMap { |
||||||
|
[key: number]: SubCategory[]; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
export const categories = [ |
||||||
|
{"id": 1, "name": "Software"}, |
||||||
|
{"id": 2, "name": "Gaming"}, |
||||||
|
{"id": 3, "name": "Media"} |
||||||
|
]; |
||||||
|
|
||||||
|
export const subCategories: CategoryMap = { |
||||||
|
1: [ |
||||||
|
{"id": 101, "name": "OS"}, |
||||||
|
{"id": 102, "name": "Application"}, |
||||||
|
{"id": 103, "name": "Source Code"}, |
||||||
|
{"id": 104, "name": "Other"} |
||||||
|
], |
||||||
|
2: [ |
||||||
|
{"id": 201, "name": "NES"}, |
||||||
|
{"id": 202, "name": "SNES"}, |
||||||
|
{"id": 203, "name": "PC"}, |
||||||
|
{"id": 204, "name": "Other Gaming Systems"} |
||||||
|
], |
||||||
|
3: [ |
||||||
|
{"id": 301, "name": "Audio"}, |
||||||
|
{"id": 302, "name": "Video"}, |
||||||
|
{"id": 303, "name": "Image"}, |
||||||
|
{"id": 304, "name": "Document"}, |
||||||
|
{"id": 305, "name": "Other Media Formats"} |
||||||
|
] |
||||||
|
}; |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const subCategories2: CategoryMap = { |
||||||
|
201: [ // NES
|
||||||
|
{"id": 20101, "name": "ROM"}, |
||||||
|
{"id": 20102, "name": "Romhack"}, |
||||||
|
{"id": 20103, "name": "Emulator"}, |
||||||
|
], |
||||||
|
202: [ // SNES
|
||||||
|
{"id": 20201, "name": "ROM"}, |
||||||
|
{"id": 20202, "name": "Romhack"}, |
||||||
|
{"id": 20203, "name": "Emulator"}, |
||||||
|
], |
||||||
|
301: [ // Audio
|
||||||
|
{"id": 30101, "name": "Music"}, |
||||||
|
{"id": 30102, "name": "Podcasts"}, |
||||||
|
{"id": 30103, "name": "Audiobooks"}, |
||||||
|
{"id": 30104, "name": "Sound Effects"}, |
||||||
|
{"id": 30105, "name": "Lectures & Speeches"}, |
||||||
|
{"id": 30106, "name": "Radio Shows"}, |
||||||
|
{"id": 30107, "name": "Ambient Sounds"}, |
||||||
|
{"id": 30108, "name": "Language Learning Material"}, |
||||||
|
{"id": 30109, "name": "Comedy & Satire"}, |
||||||
|
{"id": 30110, "name": "Documentaries"}, |
||||||
|
{"id": 30111, "name": "Guided Meditations & Yoga"}, |
||||||
|
{"id": 30112, "name": "Live Performances"}, |
||||||
|
{"id": 30113, "name": "Nature Sounds"}, |
||||||
|
{"id": 30114, "name": "Soundtracks"}, |
||||||
|
{"id": 30115, "name": "Interviews"} |
||||||
|
], |
||||||
|
302: [ // Under Video
|
||||||
|
{"id": 30201, "name": "Movies"}, |
||||||
|
{"id": 30202, "name": "Series"}, |
||||||
|
{"id": 30203, "name": "Music"}, |
||||||
|
{"id": 30204, "name": "Education"}, |
||||||
|
{"id": 30205, "name": "Lifestyle"}, |
||||||
|
{"id": 30206, "name": "Gaming"}, |
||||||
|
{"id": 30207, "name": "Technology"}, |
||||||
|
{"id": 30208, "name": "Sports"}, |
||||||
|
{"id": 30209, "name": "News & Politics"}, |
||||||
|
{"id": 30210, "name": "Cooking & Food"}, |
||||||
|
{"id": 30211, "name": "Animation"}, |
||||||
|
{"id": 30212, "name": "Science"}, |
||||||
|
{"id": 30213, "name": "Health & Wellness"}, |
||||||
|
{"id": 30214, "name": "DIY & Crafts"}, |
||||||
|
{"id": 30215, "name": "Kids & Family"}, |
||||||
|
{"id": 30216, "name": "Comedy"}, |
||||||
|
{"id": 30217, "name": "Travel & Adventure"}, |
||||||
|
{"id": 30218, "name": "Art & Design"}, |
||||||
|
{"id": 30219, "name": "Nature & Environment"}, |
||||||
|
{"id": 30220, "name": "Business & Finance"}, |
||||||
|
{"id": 30221, "name": "Personal Development"}, |
||||||
|
{"id": 30222, "name": "Other"}, |
||||||
|
{"id": 30223, "name": "History"} |
||||||
|
], |
||||||
|
303: [ // Image
|
||||||
|
{"id": 30301, "name": "Nature"}, |
||||||
|
{"id": 30302, "name": "Urban & Cityscapes"}, |
||||||
|
{"id": 30303, "name": "People & Portraits"}, |
||||||
|
{"id": 30304, "name": "Art & Abstract"}, |
||||||
|
{"id": 30305, "name": "Travel & Adventure"}, |
||||||
|
{"id": 30306, "name": "Animals & Wildlife"}, |
||||||
|
{"id": 30307, "name": "Sports & Action"}, |
||||||
|
{"id": 30308, "name": "Food & Cuisine"}, |
||||||
|
{"id": 30309, "name": "Fashion & Beauty"}, |
||||||
|
{"id": 30310, "name": "Technology & Science"}, |
||||||
|
{"id": 30311, "name": "Historical & Cultural"}, |
||||||
|
{"id": 30312, "name": "Aerial & Drone"}, |
||||||
|
{"id": 30313, "name": "Black & White"}, |
||||||
|
{"id": 30314, "name": "Events & Celebrations"}, |
||||||
|
{"id": 30315, "name": "Business & Corporate"}, |
||||||
|
{"id": 30316, "name": "Health & Wellness"}, |
||||||
|
{"id": 30317, "name": "Transportation & Vehicles"}, |
||||||
|
{"id": 30318, "name": "Still Life & Objects"}, |
||||||
|
{"id": 30319, "name": "Architecture & Buildings"}, |
||||||
|
{"id": 30320, "name": "Landscapes & Seascapes"} |
||||||
|
], |
||||||
|
304: [ // Document
|
||||||
|
{"id": 30401, "name": "PDF"}, |
||||||
|
{"id": 30402, "name": "Word Document"}, |
||||||
|
{"id": 30403, "name": "Spreadsheet"}, |
||||||
|
{"id": 30404, "name": "Powerpoint"}, |
||||||
|
{"id": 30405, "name": "Books"} |
||||||
|
] |
||||||
|
}; |
||||||
|
|
||||||
|
|
||||||
|
export const subCategories3: CategoryMap = { |
||||||
|
30201: [ // Under Movies
|
||||||
|
{"id": 3020101, "name": "Action & Adventure"}, |
||||||
|
{"id": 3020102, "name": "Comedy"}, |
||||||
|
{"id": 3020103, "name": "Drama"}, |
||||||
|
{"id": 3020104, "name": "Fantasy & Science Fiction"}, |
||||||
|
{"id": 3020105, "name": "Horror & Thriller"}, |
||||||
|
{"id": 3020106, "name": "Documentaries"}, |
||||||
|
{"id": 3020107, "name": "Animated"}, |
||||||
|
{"id": 3020108, "name": "Family & Kids"}, |
||||||
|
{"id": 3020109, "name": "Romance"}, |
||||||
|
{"id": 3020110, "name": "Mystery & Crime"}, |
||||||
|
{"id": 3020111, "name": "Historical & War"}, |
||||||
|
{"id": 3020112, "name": "Musicals & Music Films"}, |
||||||
|
{"id": 3020113, "name": "Indie Films"}, |
||||||
|
{"id": 3020114, "name": "International Films"}, |
||||||
|
{"id": 3020115, "name": "Biographies & True Stories"}, |
||||||
|
{"id": 3020116, "name": "Other"} |
||||||
|
], |
||||||
|
30202: [ // Under Series
|
||||||
|
{"id": 3020201, "name": "Dramas"}, |
||||||
|
{"id": 3020202, "name": "Comedies"}, |
||||||
|
{"id": 3020203, "name": "Reality & Competition"}, |
||||||
|
{"id": 3020204, "name": "Documentaries & Docuseries"}, |
||||||
|
{"id": 3020205, "name": "Sci-Fi & Fantasy"}, |
||||||
|
{"id": 3020206, "name": "Crime & Mystery"}, |
||||||
|
{"id": 3020207, "name": "Animated Series"}, |
||||||
|
{"id": 3020208, "name": "Kids & Family"}, |
||||||
|
{"id": 3020209, "name": "Historical & Period Pieces"}, |
||||||
|
{"id": 3020210, "name": "Action & Adventure"}, |
||||||
|
{"id": 3020211, "name": "Horror & Thriller"}, |
||||||
|
{"id": 3020212, "name": "Romance"}, |
||||||
|
{"id": 3020213, "name": "Anthologies"}, |
||||||
|
{"id": 3020214, "name": "International Series"}, |
||||||
|
{"id": 3020215, "name": "Miniseries"}, |
||||||
|
{"id": 3020216, "name": "Other"} |
||||||
|
], |
||||||
|
30405: [ // Under Books
|
||||||
|
{"id": 3040501, "name": "Fiction"}, |
||||||
|
{"id": 3040502, "name": "Non-Fiction"}, |
||||||
|
{"id": 3040503, "name": "Science Fiction & Fantasy"}, |
||||||
|
{"id": 3040504, "name": "Biographies & Memoirs"}, |
||||||
|
{"id": 3040505, "name": "Children's Books"}, |
||||||
|
{"id": 3040506, "name": "Educational"}, |
||||||
|
{"id": 3040507, "name": "Self-Help"}, |
||||||
|
{"id": 3040508, "name": "Cookbooks, Food & Wine"}, |
||||||
|
{"id": 3040509, "name": "Mystery & Thriller"}, |
||||||
|
{"id": 3040510, "name": "History"}, |
||||||
|
{"id": 3040511, "name": "Poetry"}, |
||||||
|
{"id": 3040512, "name": "Art & Photography"}, |
||||||
|
{"id": 3040513, "name": "Religion & Spirituality"}, |
||||||
|
{"id": 3040514, "name": "Travel"}, |
||||||
|
{"id": 3040515, "name": "Comics & Graphic Novels"}, |
||||||
|
|
||||||
|
], |
||||||
|
30101: [ // Under Music
|
||||||
|
{"id": 3010101, "name": "Rock"}, |
||||||
|
{"id": 3010102, "name": "Pop"}, |
||||||
|
{"id": 3010103, "name": "Classical"}, |
||||||
|
{"id": 3010104, "name": "Jazz"}, |
||||||
|
{"id": 3010105, "name": "Electronic"}, |
||||||
|
{"id": 3010106, "name": "Country"}, |
||||||
|
{"id": 3010107, "name": "Hip Hop/Rap"}, |
||||||
|
{"id": 3010108, "name": "Blues"}, |
||||||
|
{"id": 3010109, "name": "R&B/Soul"}, |
||||||
|
{"id": 3010110, "name": "Reggae"}, |
||||||
|
{"id": 3010111, "name": "Folk"}, |
||||||
|
{"id": 3010112, "name": "Metal"}, |
||||||
|
{"id": 3010113, "name": "World Music"}, |
||||||
|
{"id": 3010114, "name": "Latin"}, |
||||||
|
{"id": 3010115, "name": "Indie"}, |
||||||
|
{"id": 3010116, "name": "Punk"}, |
||||||
|
{"id": 3010117, "name": "Soundtracks"}, |
||||||
|
{"id": 3010118, "name": "Children's Music"}, |
||||||
|
{"id": 3010119, "name": "New Age"}, |
||||||
|
{"id": 3010120, "name": "Classical Crossover"} |
||||||
|
] |
||||||
|
|
||||||
|
|
||||||
|
}; |
@ -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,393 @@ |
|||||||
|
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 = '',
|
||||||
|
subcategory2 = '',
|
||||||
|
subcategory3 = '',
|
||||||
|
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 += `&name=${name}`; |
||||||
|
} |
||||||
|
|
||||||
|
if (category) { |
||||||
|
// Start with the category
|
||||||
|
let description = `cat:${category}`; |
||||||
|
|
||||||
|
// Check and append subcategory
|
||||||
|
if (subcategory) { |
||||||
|
description += `;sub:${subcategory}`; |
||||||
|
} |
||||||
|
|
||||||
|
// Check and append subcategory2
|
||||||
|
if (subcategory2) { |
||||||
|
description += `;sub2:${subcategory2}`; |
||||||
|
} |
||||||
|
|
||||||
|
// Check and append subcategory3
|
||||||
|
if (subcategory3) { |
||||||
|
description += `;sub3:${subcategory3}`; |
||||||
|
} |
||||||
|
|
||||||
|
// Append the description to the URL
|
||||||
|
defaultUrl += `&description=${description}`; |
||||||
|
} |
||||||
|
|
||||||
|
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,283 @@ |
|||||||
|
import { styled } from "@mui/system"; |
||||||
|
import { Box, Grid, Typography, Checkbox, TextField, InputLabel, Autocomplete } from "@mui/material"; |
||||||
|
|
||||||
|
export const VideoContainer = styled(Box)(({ theme }) => ({ |
||||||
|
position: "relative", |
||||||
|
display: "flex", |
||||||
|
padding: "15px", |
||||||
|
flexDirection: "row", |
||||||
|
gap: "20px", |
||||||
|
flexWrap: "wrap", |
||||||
|
justifyContent: "flex-start", |
||||||
|
width: '100%' |
||||||
|
})); |
||||||
|
|
||||||
|
export const StoresRow = styled(Grid)(({ theme }) => ({ |
||||||
|
display: "flex", |
||||||
|
flexDirection: "column", |
||||||
|
alignItems: "center", |
||||||
|
justifyContent: "flex-start", |
||||||
|
gap: "15px", |
||||||
|
width: "auto", |
||||||
|
position: "relative", |
||||||
|
"@media (max-width: 450px)": { |
||||||
|
width: "100%" |
||||||
|
} |
||||||
|
})); |
||||||
|
|
||||||
|
export const VideoCard = styled(Grid)(({ theme }) => ({ |
||||||
|
position: "relative", |
||||||
|
display: "flex", |
||||||
|
flexDirection: "column", |
||||||
|
height: "320px", |
||||||
|
width: '300px', |
||||||
|
backgroundColor: theme.palette.background.paper, |
||||||
|
borderRadius: "8px", |
||||||
|
padding: "10px 15px", |
||||||
|
gap: "20px", |
||||||
|
cursor: "pointer", |
||||||
|
border: |
||||||
|
theme.palette.mode === "dark" |
||||||
|
? "none" |
||||||
|
: `1px solid ${theme.palette.primary.light}`, |
||||||
|
boxShadow: |
||||||
|
theme.palette.mode === "dark" |
||||||
|
? "0px 4px 5px 0px hsla(0,0%,0%,0.14), 0px 1px 10px 0px hsla(0,0%,0%,0.12), 0px 2px 4px -1px hsla(0,0%,0%,0.2)" |
||||||
|
: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px", |
||||||
|
transition: "all 0.3s ease-in-out", |
||||||
|
"&:hover": { |
||||||
|
boxShadow: |
||||||
|
theme.palette.mode === "dark" |
||||||
|
? "0px 8px 10px 1px hsla(0,0%,0%,0.14), 0px 3px 14px 2px hsla(0,0%,0%,0.12), 0px 5px 5px -3px hsla(0,0%,0%,0.2)" |
||||||
|
: "rgba(0, 0, 0, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;" |
||||||
|
} |
||||||
|
})); |
||||||
|
|
||||||
|
export const StoreCardInfo = styled(Grid)(({ theme }) => ({ |
||||||
|
display: "flex", |
||||||
|
flexDirection: "column", |
||||||
|
gap: "10px", |
||||||
|
padding: "5px", |
||||||
|
marginTop: "15px" |
||||||
|
})); |
||||||
|
|
||||||
|
export const VideoImageContainer = styled(Grid)(({ theme }) => ({})); |
||||||
|
|
||||||
|
export const VideoCardImage = styled("img")(({ theme }) => ({ |
||||||
|
maxWidth: "300px", |
||||||
|
minWidth: "150px", |
||||||
|
borderRadius: "5px", |
||||||
|
height: '150px', |
||||||
|
objectFit: 'fill', |
||||||
|
width: '266px', |
||||||
|
})); |
||||||
|
|
||||||
|
const DoubleLine = styled(Typography)` |
||||||
|
display: -webkit-box; |
||||||
|
-webkit-box-orient: vertical; |
||||||
|
-webkit-line-clamp: 2; |
||||||
|
overflow: hidden; |
||||||
|
` |
||||||
|
|
||||||
|
export const VideoCardTitle = styled(DoubleLine)(({ theme }) => ({ |
||||||
|
fontFamily: "Cairo", |
||||||
|
fontSize: "16px", |
||||||
|
letterSpacing: "0.4px", |
||||||
|
color: theme.palette.text.primary, |
||||||
|
userSelect: "none" |
||||||
|
})); |
||||||
|
export const VideoCardName = styled(Typography)(({ theme }) => ({ |
||||||
|
fontFamily: "Cairo", |
||||||
|
fontSize: "14px", |
||||||
|
letterSpacing: "0.4px", |
||||||
|
color: theme.palette.text.primary, |
||||||
|
userSelect: "none", |
||||||
|
overflow: "hidden", |
||||||
|
whiteSpace: "nowrap", |
||||||
|
textOverflow: "ellipsis", |
||||||
|
width: "100%", |
||||||
|
})); |
||||||
|
export const VideoUploadDate = styled(Typography)(({ theme }) => ({ |
||||||
|
fontFamily: "Cairo", |
||||||
|
fontSize: "12px", |
||||||
|
letterSpacing: "0.4px", |
||||||
|
color: theme.palette.text.primary, |
||||||
|
userSelect: "none" |
||||||
|
})); |
||||||
|
export const BottomParent = styled(Box)(({ theme }) => ({ |
||||||
|
display: 'flex', |
||||||
|
alignItems: 'flex-start', |
||||||
|
flexDirection: 'column' |
||||||
|
})); |
||||||
|
export const VideoCardDescription = styled(Typography)(({ theme }) => ({ |
||||||
|
fontFamily: "Karla", |
||||||
|
fontSize: "20px", |
||||||
|
letterSpacing: "0px", |
||||||
|
color: theme.palette.text.primary, |
||||||
|
userSelect: "none" |
||||||
|
})); |
||||||
|
|
||||||
|
export const StoreCardOwner = styled(Typography)(({ theme }) => ({ |
||||||
|
fontFamily: "Livvic", |
||||||
|
color: theme.palette.text.primary, |
||||||
|
fontSize: "17px", |
||||||
|
position: "absolute", |
||||||
|
bottom: "5px", |
||||||
|
right: "10px", |
||||||
|
userSelect: "none" |
||||||
|
})); |
||||||
|
|
||||||
|
export const StoreCardYouOwn = styled(Box)(({ theme }) => ({ |
||||||
|
position: "absolute", |
||||||
|
top: "5px", |
||||||
|
right: "10px", |
||||||
|
display: "flex", |
||||||
|
alignItems: "center", |
||||||
|
gap: "5px", |
||||||
|
fontFamily: "Livvic", |
||||||
|
fontSize: "15px", |
||||||
|
color: theme.palette.text.primary |
||||||
|
})); |
||||||
|
|
||||||
|
export const MyStoresRow = styled(Grid)(({ theme }) => ({ |
||||||
|
display: "flex", |
||||||
|
flexDirection: "row", |
||||||
|
justifyContent: "flex-end", |
||||||
|
padding: "5px", |
||||||
|
width: "100%" |
||||||
|
})); |
||||||
|
|
||||||
|
export const NameContainer = styled(Box)(({ theme }) => ({ |
||||||
|
display: "flex", |
||||||
|
flexDirection: "row", |
||||||
|
justifyContent: "flex-start", |
||||||
|
alignItems: 'center', |
||||||
|
gap: '10px', |
||||||
|
marginBottom: '10px' |
||||||
|
})); |
||||||
|
|
||||||
|
export const MyStoresCard = styled(Box)(({ theme }) => ({ |
||||||
|
display: "flex", |
||||||
|
flexDirection: "row", |
||||||
|
alignItems: "center", |
||||||
|
width: "auto", |
||||||
|
borderRadius: "4px", |
||||||
|
backgroundColor: theme.palette.background.paper, |
||||||
|
padding: "5px 10px", |
||||||
|
fontFamily: "Raleway", |
||||||
|
fontSize: "18px", |
||||||
|
color: theme.palette.text.primary |
||||||
|
})); |
||||||
|
|
||||||
|
export const MyStoresCheckbox = styled(Checkbox)(({ theme }) => ({ |
||||||
|
color: "#c0d4ff", |
||||||
|
"&.Mui-checked": { |
||||||
|
color: "#6596ff" |
||||||
|
} |
||||||
|
})); |
||||||
|
|
||||||
|
export const FiltersCol = styled(Grid)(({ theme }) => ({ |
||||||
|
display: "flex", |
||||||
|
flexDirection: "column", |
||||||
|
height: "100%", |
||||||
|
padding: "20px 15px", |
||||||
|
backgroundColor: theme.palette.background.default, |
||||||
|
borderTop: `1px solid ${theme.palette.background.paper}`, |
||||||
|
borderRight: `1px solid ${theme.palette.background.paper}` |
||||||
|
})); |
||||||
|
|
||||||
|
export const FiltersContainer = styled(Box)(({ theme }) => ({ |
||||||
|
display: "flex", |
||||||
|
flexDirection: "column", |
||||||
|
justifyContent: "space-between" |
||||||
|
})); |
||||||
|
|
||||||
|
export const FiltersRow = styled(Box)(({ theme }) => ({ |
||||||
|
display: "flex", |
||||||
|
alignItems: "center", |
||||||
|
justifyContent: "space-between", |
||||||
|
width: "100%", |
||||||
|
padding: "0 15px", |
||||||
|
fontSize: "16px", |
||||||
|
userSelect: "none" |
||||||
|
})); |
||||||
|
|
||||||
|
export const FiltersTitle = styled(Typography)(({ theme }) => ({ |
||||||
|
display: "flex", |
||||||
|
alignItems: "center", |
||||||
|
gap: "5px", |
||||||
|
margin: "20px 0", |
||||||
|
fontFamily: "Raleway", |
||||||
|
fontSize: "17px", |
||||||
|
color: theme.palette.text.primary, |
||||||
|
userSelect: "none" |
||||||
|
})); |
||||||
|
|
||||||
|
export const FiltersCheckbox = styled(Checkbox)(({ theme }) => ({ |
||||||
|
color: "#c0d4ff", |
||||||
|
"&.Mui-checked": { |
||||||
|
color: "#6596ff" |
||||||
|
} |
||||||
|
})); |
||||||
|
|
||||||
|
export const FilterSelect = styled(Autocomplete)(({ theme }) => ({ |
||||||
|
"& #categories-select": { |
||||||
|
padding: "7px" |
||||||
|
}, |
||||||
|
"& .MuiSelect-placeholder": { |
||||||
|
fontFamily: "Raleway", |
||||||
|
fontSize: "17px", |
||||||
|
color: theme.palette.text.primary, |
||||||
|
userSelect: "none" |
||||||
|
}, |
||||||
|
"& MuiFormLabel-root": { |
||||||
|
fontFamily: "Raleway", |
||||||
|
fontSize: "17px", |
||||||
|
color: theme.palette.text.primary, |
||||||
|
userSelect: "none" |
||||||
|
} |
||||||
|
})); |
||||||
|
|
||||||
|
export const FilterSelectMenuItems = styled(TextField)(({ theme }) => ({ |
||||||
|
fontFamily: "Raleway", |
||||||
|
fontSize: "17px", |
||||||
|
color: theme.palette.text.primary, |
||||||
|
userSelect: "none" |
||||||
|
})); |
||||||
|
|
||||||
|
|
||||||
|
export const FiltersSubContainer = styled(Box)(({ theme }) => ({ |
||||||
|
display: "flex", |
||||||
|
alignItems: "center", |
||||||
|
flexDirection: "column", |
||||||
|
gap: "5px" |
||||||
|
})); |
||||||
|
|
||||||
|
export const FilterDropdownLabel = styled(InputLabel)(({ theme }) => ({ |
||||||
|
fontFamily: "Raleway", |
||||||
|
fontSize: "16px", |
||||||
|
color: theme.palette.text.primary |
||||||
|
})); |
||||||
|
|
||||||
|
export const IconsBox = styled(Box)({ |
||||||
|
display: 'flex', |
||||||
|
gap: "3px", |
||||||
|
position: 'absolute', |
||||||
|
top: '-20px', |
||||||
|
right: '-5px', |
||||||
|
transition: 'all 0.3s ease-in-out', |
||||||
|
}); |
||||||
|
|
||||||
|
export const BlockIconContainer = styled(Box)({ |
||||||
|
display: 'flex', |
||||||
|
boxShadow: "rgba(99, 99, 99, 0.2) 0px 2px 8px 0px;", |
||||||
|
backgroundColor: '#fbfbfb', |
||||||
|
color: "#c25252", |
||||||
|
padding: '2px', |
||||||
|
borderRadius: '3px', |
||||||
|
transition: 'all 0.3s ease-in-out', |
||||||
|
"&:hover": { |
||||||
|
cursor: 'pointer', |
||||||
|
transform: "scale(1.1)", |
||||||
|
} |
||||||
|
}) |
@ -0,0 +1,826 @@ |
|||||||
|
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 AttachFileIcon from '@mui/icons-material/AttachFile'; |
||||||
|
import { |
||||||
|
Avatar, |
||||||
|
Box, |
||||||
|
Button, |
||||||
|
FormControl, |
||||||
|
Grid, |
||||||
|
Input, |
||||||
|
InputLabel, |
||||||
|
MenuItem, |
||||||
|
OutlinedInput, |
||||||
|
Select, |
||||||
|
SelectChangeEvent, |
||||||
|
Skeleton, |
||||||
|
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, |
||||||
|
changeSelectedSubCategoryVideos2, |
||||||
|
changeSelectedSubCategoryVideos3, |
||||||
|
changefilterName, |
||||||
|
changefilterSearch, |
||||||
|
clearVideoList, |
||||||
|
setEditPlaylist, |
||||||
|
setEditVideo, |
||||||
|
} from "../../state/features/videoSlice"; |
||||||
|
import { categories, subCategories, subCategories2, subCategories3 } 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'; |
||||||
|
import { formatBytes } from "../VideoContent/VideoContent"; |
||||||
|
|
||||||
|
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 selectedSubCategoryVideos2 = useSelector( |
||||||
|
(state: RootState) => state.video.selectedSubCategoryVideos2 |
||||||
|
); |
||||||
|
const selectedSubCategoryVideos3 = useSelector( |
||||||
|
(state: RootState) => state.video.selectedSubCategoryVideos3 |
||||||
|
); |
||||||
|
|
||||||
|
const setSelectedSubCategoryVideos = (payload) => { |
||||||
|
dispatch(changeSelectedSubCategoryVideos(payload)); |
||||||
|
}; |
||||||
|
const setSelectedSubCategoryVideos2 = (payload) => { |
||||||
|
dispatch(changeSelectedSubCategoryVideos2(payload)); |
||||||
|
}; |
||||||
|
const setSelectedSubCategoryVideos3 = (payload) => { |
||||||
|
dispatch(changeSelectedSubCategoryVideos3(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; |
||||||
|
console.log({ |
||||||
|
category: selectedCategoryVideos?.id, |
||||||
|
subcategory: selectedSubCategoryVideos?.id, |
||||||
|
subcategory2: selectedSubCategoryVideos2?.id, |
||||||
|
subcategory3: selectedSubCategoryVideos3?.id, |
||||||
|
}) |
||||||
|
await getVideos( |
||||||
|
{ |
||||||
|
name: filterName, |
||||||
|
category: selectedCategoryVideos?.id, |
||||||
|
subcategory: selectedSubCategoryVideos?.id, |
||||||
|
subcategory2: selectedSubCategoryVideos2?.id, |
||||||
|
subcategory3: selectedSubCategoryVideos3?.id, |
||||||
|
keywords: filterSearch, |
||||||
|
type: filterType, |
||||||
|
}, |
||||||
|
reset ? true : false, |
||||||
|
resetFilers |
||||||
|
); |
||||||
|
isFetching.current = false; |
||||||
|
}, |
||||||
|
[ |
||||||
|
getVideos, |
||||||
|
filterValue, |
||||||
|
getVideosFiltered, |
||||||
|
isFiltering, |
||||||
|
filterName, |
||||||
|
selectedCategoryVideos, |
||||||
|
selectedSubCategoryVideos, |
||||||
|
selectedSubCategoryVideos2, |
||||||
|
selectedSubCategoryVideos3, |
||||||
|
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 handleOptionSubCategoryChangeVideos2 = ( |
||||||
|
event: SelectChangeEvent<string>, |
||||||
|
subcategories: any[] |
||||||
|
) => { |
||||||
|
const optionId = event.target.value; |
||||||
|
const selectedOption = subcategories.find( |
||||||
|
(option) => option.id === +optionId |
||||||
|
); |
||||||
|
setSelectedSubCategoryVideos2(selectedOption || null); |
||||||
|
}; |
||||||
|
const handleOptionSubCategoryChangeVideos3 = ( |
||||||
|
event: SelectChangeEvent<string>, |
||||||
|
subcategories: any[] |
||||||
|
) => { |
||||||
|
const optionId = event.target.value; |
||||||
|
const selectedOption = subcategories.find( |
||||||
|
(option) => option.id === +optionId |
||||||
|
); |
||||||
|
setSelectedSubCategoryVideos3(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> |
||||||
|
)} |
||||||
|
{selectedSubCategoryVideos && |
||||||
|
subCategories2[selectedSubCategoryVideos?.id] && ( |
||||||
|
<FormControl fullWidth sx={{ marginBottom: 2 }}> |
||||||
|
<InputLabel |
||||||
|
sx={{ |
||||||
|
fontSize: "16px", |
||||||
|
}} |
||||||
|
id="Sub-2x-Category" |
||||||
|
> |
||||||
|
Sub-2x-Category |
||||||
|
</InputLabel> |
||||||
|
<Select |
||||||
|
labelId="Sub-2x-Category" |
||||||
|
input={<OutlinedInput label="Sub-2x-Category" />} |
||||||
|
value={selectedSubCategoryVideos2?.id || ""} |
||||||
|
onChange={(e) => |
||||||
|
handleOptionSubCategoryChangeVideos2( |
||||||
|
e, |
||||||
|
subCategories2[selectedSubCategoryVideos?.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
|
||||||
|
}, |
||||||
|
}, |
||||||
|
}} |
||||||
|
> |
||||||
|
{subCategories2[selectedSubCategoryVideos.id].map( |
||||||
|
(option) => ( |
||||||
|
<MenuItem key={option.id} value={option.id}> |
||||||
|
{option.name} |
||||||
|
</MenuItem> |
||||||
|
) |
||||||
|
)} |
||||||
|
</Select> |
||||||
|
</FormControl> |
||||||
|
)} |
||||||
|
{selectedSubCategoryVideos2 && |
||||||
|
subCategories3[selectedSubCategoryVideos2?.id] && ( |
||||||
|
<FormControl fullWidth sx={{ marginBottom: 2 }}> |
||||||
|
<InputLabel |
||||||
|
sx={{ |
||||||
|
fontSize: "16px", |
||||||
|
}} |
||||||
|
id="Sub-2x-Category" |
||||||
|
> |
||||||
|
Sub-3x-Category |
||||||
|
</InputLabel> |
||||||
|
<Select |
||||||
|
labelId="Sub-3x-Category" |
||||||
|
input={<OutlinedInput label="Sub-sx-Category" />} |
||||||
|
value={selectedSubCategoryVideos3?.id || ""} |
||||||
|
onChange={(e) => |
||||||
|
handleOptionSubCategoryChangeVideos3( |
||||||
|
e, |
||||||
|
subCategories3[selectedSubCategoryVideos2?.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
|
||||||
|
}, |
||||||
|
}, |
||||||
|
}} |
||||||
|
> |
||||||
|
{subCategories3[selectedSubCategoryVideos2.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", |
||||||
|
}} |
||||||
|
> |
||||||
|
|
||||||
|
</SubtitleContainer> |
||||||
|
|
||||||
|
<VideoContainer> |
||||||
|
{videos.map((video: any, index: number) => { |
||||||
|
const existingVideo = hashMapVideos[video?.id]; |
||||||
|
let hasHash = false; |
||||||
|
let videoObj = video; |
||||||
|
if (existingVideo) { |
||||||
|
videoObj = existingVideo; |
||||||
|
hasHash = true; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return ( |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: "flex", |
||||||
|
alignItems: "center", |
||||||
|
width: "100%", |
||||||
|
height: "75px", |
||||||
|
position:"relative" |
||||||
|
|
||||||
|
}} |
||||||
|
key={videoObj.id} |
||||||
|
onMouseEnter={() => setShowIcons(videoObj.id)} |
||||||
|
onMouseLeave={() => setShowIcons(null)} |
||||||
|
> |
||||||
|
{hasHash ? ( |
||||||
|
<> |
||||||
|
<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(`/share/${videoObj?.user}/${videoObj?.id}`); |
||||||
|
}} |
||||||
|
sx={{ |
||||||
|
height: '100%', |
||||||
|
width: '100%', |
||||||
|
display: 'flex', |
||||||
|
gap: '25px', |
||||||
|
flexDirection: 'row', |
||||||
|
justifyContent: 'space-between' |
||||||
|
}} |
||||||
|
> |
||||||
|
|
||||||
|
<Box sx={{ |
||||||
|
|
||||||
|
display: 'flex', |
||||||
|
gap: '25px', |
||||||
|
alignItems: 'center' |
||||||
|
}}> |
||||||
|
<AttachFileIcon /> |
||||||
|
<VideoCardTitle sx={{ |
||||||
|
width: '100px' |
||||||
|
}}> |
||||||
|
{formatBytes(videoObj?.files.reduce((acc, cur) => acc + (cur?.size || 0), 0))} |
||||||
|
</VideoCardTitle> |
||||||
|
<VideoCardTitle>{videoObj.title}</VideoCardTitle> |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</Box> |
||||||
|
<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> |
||||||
|
</> |
||||||
|
) : ( |
||||||
|
<Skeleton |
||||||
|
variant="rectangular" |
||||||
|
style={{ |
||||||
|
width: "100%", |
||||||
|
height: "100%", |
||||||
|
paddingBottom: "10px", |
||||||
|
objectFit: "contain", |
||||||
|
visibility: "visible", |
||||||
|
borderRadius: "8px", |
||||||
|
}} |
||||||
|
/> |
||||||
|
)} |
||||||
|
|
||||||
|
</Box> |
||||||
|
); |
||||||
|
})} |
||||||
|
</VideoContainer> |
||||||
|
|
||||||
|
<LazyLoad |
||||||
|
onLoadMore={getVideosHandler} |
||||||
|
isLoading={isLoading} |
||||||
|
></LazyLoad> |
||||||
|
</Box> |
||||||
|
</Grid> |
||||||
|
</Grid> |
||||||
|
); |
||||||
|
}; |
@ -0,0 +1,261 @@ |
|||||||
|
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 AttachFileIcon from '@mui/icons-material/AttachFile'; |
||||||
|
|
||||||
|
import { |
||||||
|
Avatar, |
||||||
|
Box, |
||||||
|
Button, |
||||||
|
Skeleton, |
||||||
|
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' |
||||||
|
import { formatBytes } from '../VideoContent/VideoContent' |
||||||
|
|
||||||
|
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; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return ( |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: "flex", |
||||||
|
alignItems: "center", |
||||||
|
width: "100%", |
||||||
|
height: "75px", |
||||||
|
position:"relative" |
||||||
|
|
||||||
|
}} |
||||||
|
key={videoObj.id} |
||||||
|
|
||||||
|
> |
||||||
|
{hasHash ? ( |
||||||
|
<> |
||||||
|
<VideoCard |
||||||
|
onClick={() => { |
||||||
|
navigate(`/share/${videoObj?.user}/${videoObj?.id}`); |
||||||
|
}} |
||||||
|
sx={{ |
||||||
|
height: '100%', |
||||||
|
width: '100%', |
||||||
|
display: 'flex', |
||||||
|
gap: '25px', |
||||||
|
flexDirection: 'row', |
||||||
|
justifyContent: 'space-between' |
||||||
|
}} |
||||||
|
> |
||||||
|
|
||||||
|
<Box sx={{ |
||||||
|
|
||||||
|
display: 'flex', |
||||||
|
gap: '25px', |
||||||
|
alignItems: 'center' |
||||||
|
}}> |
||||||
|
<AttachFileIcon /> |
||||||
|
<VideoCardTitle sx={{ |
||||||
|
width: '100px' |
||||||
|
}}> |
||||||
|
{formatBytes(videoObj?.files.reduce((acc, cur) => acc + (cur?.size || 0), 0))} |
||||||
|
</VideoCardTitle> |
||||||
|
<VideoCardTitle>{videoObj.title}</VideoCardTitle> |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</Box> |
||||||
|
<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> |
||||||
|
</> |
||||||
|
) : ( |
||||||
|
<Skeleton |
||||||
|
variant="rectangular" |
||||||
|
style={{ |
||||||
|
width: "100%", |
||||||
|
height: "100%", |
||||||
|
paddingBottom: "10px", |
||||||
|
objectFit: "contain", |
||||||
|
visibility: "visible", |
||||||
|
borderRadius: "8px", |
||||||
|
}} |
||||||
|
/> |
||||||
|
)} |
||||||
|
|
||||||
|
</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,486 @@ |
|||||||
|
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 function formatBytes(bytes, decimals = 2) { |
||||||
|
if (bytes === 0) return '0 Bytes'; |
||||||
|
|
||||||
|
const k = 1024; |
||||||
|
const dm = decimals < 0 ? 0 : decimals; |
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; |
||||||
|
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k)); |
||||||
|
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const 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; |
||||||
|
if (height > 100) { // Assuming 100px is your threshold
|
||||||
|
setDescriptionHeight(100) |
||||||
|
} |
||||||
|
} |
||||||
|
}, [videoData]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return ( |
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: "flex", |
||||||
|
alignItems: "center", |
||||||
|
flexDirection: "column", |
||||||
|
padding: "20px 10px", |
||||||
|
}} |
||||||
|
> |
||||||
|
<VideoPlayerContainer |
||||||
|
sx={{ |
||||||
|
marginBottom: "30px", |
||||||
|
}} |
||||||
|
> |
||||||
|
|
||||||
|
<Spacer height="15px" /> |
||||||
|
|
||||||
|
<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> |
||||||
|
<Box sx={{ |
||||||
|
width: '100%', |
||||||
|
display: 'flex', |
||||||
|
alignItems: 'flex-start', |
||||||
|
flexDirection: 'column', |
||||||
|
gap: '25px', |
||||||
|
marginTop: '25px' |
||||||
|
}}> |
||||||
|
{videoData?.files?.map((file)=> { |
||||||
|
return ( |
||||||
|
<FileAttachmentContainer sx={{ |
||||||
|
width: '100%', |
||||||
|
display: 'flex', |
||||||
|
justifyContent: 'space-between' |
||||||
|
}}> |
||||||
|
|
||||||
|
<FileAttachmentFont> |
||||||
|
{file.filename} |
||||||
|
</FileAttachmentFont> |
||||||
|
<Box sx={{ |
||||||
|
|
||||||
|
display: 'flex', |
||||||
|
gap: '25px', |
||||||
|
alignItems: 'center', |
||||||
|
|
||||||
|
}}> |
||||||
|
|
||||||
|
|
||||||
|
<FileAttachmentFont> |
||||||
|
{formatBytes(file?.size || 0)} |
||||||
|
</FileAttachmentFont> |
||||||
|
<FileElement |
||||||
|
fileInfo={{...file, |
||||||
|
filename: file?.filename, |
||||||
|
mimeType: file?.mimetype |
||||||
|
|
||||||
|
}} |
||||||
|
jsonId={id} |
||||||
|
title={file?.filename} |
||||||
|
customStyles={{ |
||||||
|
display: "flex", |
||||||
|
alignItems: "center", |
||||||
|
justifyContent: "flex-end", |
||||||
|
}} |
||||||
|
> |
||||||
|
<DownloadIcon /> |
||||||
|
</FileElement> |
||||||
|
</Box> |
||||||
|
</FileAttachmentContainer> |
||||||
|
) |
||||||
|
})} |
||||||
|
|
||||||
|
</Box> |
||||||
|
</VideoPlayerContainer> |
||||||
|
|
||||||
|
<Box |
||||||
|
sx={{ |
||||||
|
display: "flex", |
||||||
|
gap: "20px", |
||||||
|
width: "100%", |
||||||
|
maxWidth: "1200px", |
||||||
|
}} |
||||||
|
> |
||||||
|
<CommentSection postId={id || ""} postName={name || ""} /> |
||||||
|
</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,216 @@ |
|||||||
|
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 |
||||||
|
selectedSubCategoryVideos2: any |
||||||
|
selectedSubCategoryVideos3: any |
||||||
|
editVideoProperties: any |
||||||
|
editPlaylistProperties: any |
||||||
|
} |
||||||
|
const initialState: GlobalState = { |
||||||
|
videos: [], |
||||||
|
filteredVideos: [], |
||||||
|
hashMapVideos: {}, |
||||||
|
countNewVideos: 0, |
||||||
|
isFiltering: false, |
||||||
|
filterValue: '', |
||||||
|
filterType: 'videos', |
||||||
|
filterSearch: '', |
||||||
|
filterName: '', |
||||||
|
selectedCategoryVideos: null, |
||||||
|
selectedSubCategoryVideos: null, |
||||||
|
selectedSubCategoryVideos2: null, |
||||||
|
selectedSubCategoryVideos3: 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 |
||||||
|
}, |
||||||
|
changeSelectedSubCategoryVideos2: (state, action) => { |
||||||
|
state.selectedSubCategoryVideos2 = action.payload |
||||||
|
}, |
||||||
|
changeSelectedSubCategoryVideos3: (state, action) => { |
||||||
|
state.selectedSubCategoryVideos3 = 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, |
||||||
|
changeSelectedSubCategoryVideos2, |
||||||
|
changeSelectedSubCategoryVideos3, |
||||||
|
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 |
@ -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: "#FF1493", // Neon pink
|
||||||
|
dark: "#C6127A", // Darker shade of neon pink
|
||||||
|
light: "#FF5EC4" // Lighter shade of neon pink
|
||||||
|
}, |
||||||
|
secondary: { |
||||||
|
main: "#007FFF", // Electric blue
|
||||||
|
dark: "#0059B2", // Darker shade of electric blue
|
||||||
|
light: "#3399FF" // Lighter shade of electric blue
|
||||||
|
}, |
||||||
|
background: { |
||||||
|
default: "#1C1C1C", // Deep space black
|
||||||
|
paper: "#342F41" // Dark cyberpunk-style purple
|
||||||
|
}, |
||||||
|
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"] |
||||||
|
} |