3
0
mirror of https://github.com/Qortal/q-support.git synced 2025-02-11 17:55:50 +00:00

Merge pull request #7 from QuickMythril/qshare-20240520

Q-Support 1.0
This commit is contained in:
Qortal Dev 2024-06-14 14:20:18 -06:00 committed by GitHub
commit 6bbe68a3fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
99 changed files with 13780 additions and 12429 deletions

View File

@ -1,16 +1,16 @@
module.exports = { module.exports = {
env: { browser: true, es2020: true }, env: { browser: true, es2020: true },
extends: [ extends: [
'eslint:recommended', 'eslint:recommended',
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended', 'plugin:react-hooks/recommended',
], ],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
plugins: ['react-refresh'], plugins: ['react-refresh'],
rules: { rules: {
'react-refresh/only-export-components': 'warn', 'react-refresh/only-export-components': 'warn',
'@typescript-eslint/no-explicit-any': "off" '@typescript-eslint/no-explicit-any': "off"
}, },
} }

49
.gitignore vendored
View File

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

View File

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

View File

@ -1,2 +1,2 @@
# q-support # q-support
This Q-App lets users submit issues involving the Qortal Core, UI, and Q-Apps to the Qortal Dev team. This Q-App lets users submit issues involving the Qortal Core, UI, and Q-Apps to the Qortal Dev team.

View File

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

15060
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,48 +1,48 @@
{ {
"name": "qsupport", "name": "qsupport",
"private": true, "private": true,
"version": "1.0.0", "version": "1.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.10.6", "@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6", "@emotion/styled": "^11.10.6",
"@mui/icons-material": "^5.11.11", "@mui/icons-material": "^5.11.11",
"@mui/material": "^5.11.13", "@mui/material": "^5.11.13",
"@reduxjs/toolkit": "^1.9.3", "@reduxjs/toolkit": "^1.9.3",
"compressorjs": "^1.2.1", "compressorjs": "^1.2.1",
"dompurify": "^3.0.6", "dompurify": "^3.0.6",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"moment": "^2.29.4", "moment": "^2.29.4",
"prettier": "^3.2.4", "prettier": "^3.2.4",
"quill-image-resize-module-react": "^3.0.0", "quill-image-resize-module-react": "^3.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",
"react-intersection-observer": "^9.4.3", "react-intersection-observer": "^9.4.3",
"react-quill": "^2.0.0", "react-quill": "^2.0.0",
"react-redux": "^8.0.5", "react-redux": "^8.0.5",
"react-rnd": "^10.4.1", "react-rnd": "^10.4.1",
"react-router-dom": "^6.9.0", "react-router-dom": "^6.9.0",
"react-toastify": "^9.1.2", "react-toastify": "^9.1.2",
"short-unique-id": "^4.4.4", "short-unique-id": "^4.4.4",
"ts-key-enum": "^2.0.12" "ts-key-enum": "^2.0.12"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.0.28", "@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^5.57.1", "@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1", "@typescript-eslint/parser": "^5.57.1",
"@vitejs/plugin-react": "^4.0.0", "@vitejs/plugin-react": "^4.0.0",
"eslint": "^8.38.0", "eslint": "^8.38.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.3.4", "eslint-plugin-react-refresh": "^0.3.4",
"typescript": "^5.0.2", "typescript": "^5.0.2",
"vite": "6.0.0-alpha.1" "vite": "6.0.0-alpha.1"
} }
} }

View File

@ -1,43 +1,43 @@
#root { #root {
max-width: 1280px; max-width: 1280px;
margin: 0 auto; margin: 0 auto;
padding: 2rem; padding: 2rem;
text-align: center; text-align: center;
} }
.logo { .logo {
height: 6em; height: 6em;
padding: 1.5em; padding: 1.5em;
will-change: filter; will-change: filter;
transition: filter 300ms; transition: filter 300ms;
} }
.logo:hover { .logo:hover {
filter: drop-shadow(0 0 2em #646cffaa); filter: drop-shadow(0 0 2em #646cffaa);
} }
.logo.react:hover { .logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa); filter: drop-shadow(0 0 2em #61dafbaa);
} }
@keyframes logo-spin { @keyframes logo-spin {
from { from {
transform: rotate(0deg); transform: rotate(0deg);
} }
to { to {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
@media (prefers-reduced-motion: no-preference) { @media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo { a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear; animation: logo-spin infinite 20s linear;
} }
} }
.card { .card {
padding: 2em; padding: 2em;
} }
.read-the-docs { .read-the-docs {
color: #888; color: #888;
} }

View File

@ -1,4 +1,4 @@
import { useState } from "react"; import { useEffect, useState } from "react";
import { Route, Routes } from "react-router-dom"; import { Route, Routes } from "react-router-dom";
import { ThemeProvider } from "@mui/material/styles"; import { ThemeProvider } from "@mui/material/styles";
import { CssBaseline } from "@mui/material"; import { CssBaseline } from "@mui/material";
@ -11,12 +11,16 @@ import { Home } from "./pages/Home/Home";
import { IssueContent } from "./pages/IssueContent/IssueContent.tsx"; import { IssueContent } from "./pages/IssueContent/IssueContent.tsx";
import DownloadWrapper from "./wrappers/DownloadWrapper"; import DownloadWrapper from "./wrappers/DownloadWrapper";
import { IndividualProfile } from "./pages/IndividualProfile/IndividualProfile"; import { IndividualProfile } from "./pages/IndividualProfile/IndividualProfile";
import { fetchFeesRedux } from "./constants/PublishFees/FeePricePublish/FeePricePublish.ts";
function App() { function App() {
// const themeColor = window._qdnTheme // const themeColor = window._qdnTheme
const [theme, setTheme] = useState("dark"); const [theme, setTheme] = useState("dark");
useEffect(() => {
fetchFeesRedux();
}, []);
return ( return (
<Provider store={store}> <Provider store={store}>
<ThemeProvider theme={theme === "light" ? lightTheme : darkTheme}> <ThemeProvider theme={theme === "light" ? lightTheme : darkTheme}>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

View File

@ -1,25 +1,25 @@
interface AccountCircleSVGProps { interface AccountCircleSVGProps {
color: string color: string
height: string height: string
width: string width: string
} }
export const AccountCircleSVG: React.FC<AccountCircleSVGProps> = ({ export const AccountCircleSVG: React.FC<AccountCircleSVGProps> = ({
color, color,
height, height,
width width
}) => { }) => {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
height={height} height={height}
viewBox="0 96 960 960" viewBox="0 96 960 960"
width={width} width={width}
> >
<path <path
fill={color} 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" 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> </svg>
) )
} }

View File

@ -1,23 +1,23 @@
import { IconTypes } from "./IconTypes"; import { IconTypes } from "./IconTypes";
export const CircleSVG: React.FC<IconTypes> = ({ export const CircleSVG: React.FC<IconTypes> = ({
color, color,
height, height,
width, width,
className, className,
onClickFunc, onClickFunc,
}) => { }) => {
return ( return (
<svg <svg
onClick={onClickFunc} onClick={onClickFunc}
className={className} className={className}
fill={color} fill={color}
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
height={height} height={height}
viewBox="0 -960 960 960" viewBox="0 -960 960 960"
width={width} 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" /> <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> </svg>
); );
}; };

View File

@ -1,23 +1,23 @@
import { IconTypes } from './IconTypes' import { IconTypes } from './IconTypes'
export const DarkModeSVG: React.FC<IconTypes> = ({ export const DarkModeSVG: React.FC<IconTypes> = ({
color, color,
height, height,
width, width,
className, className,
onClickFunc onClickFunc
}) => { }) => {
return ( return (
<svg <svg
className={className} className={className}
onClick={onClickFunc} onClick={onClickFunc}
fill={color} fill={color}
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
height={height} height={height}
viewBox="0 96 960 960" viewBox="0 96 960 960"
width={width} 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" /> <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> </svg>
) )
} }

View File

@ -1,13 +1,13 @@
import { IconTypes } from './IconTypes' import { IconTypes } from './IconTypes'
export const DownloadedLight: React.FC<IconTypes> = ({ export const DownloadedLight: React.FC<IconTypes> = ({
color, color,
height, height,
width, width,
className, className,
onClickFunc onClickFunc
}) => { }) => {
return ( 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> <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>
) )
} }

View File

@ -1,13 +1,13 @@
import { IconTypes } from './IconTypes' import { IconTypes } from './IconTypes'
export const DownloadingLight: React.FC<IconTypes> = ({ export const DownloadingLight: React.FC<IconTypes> = ({
color, color,
height, height,
width, width,
className, className,
onClickFunc onClickFunc
}) => { }) => {
return ( 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> <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>
) )
} }

View File

@ -1,23 +1,23 @@
import { IconTypes } from "./IconTypes"; import { IconTypes } from "./IconTypes";
export const EmptyCircleSVG: React.FC<IconTypes> = ({ export const EmptyCircleSVG: React.FC<IconTypes> = ({
color, color,
height, height,
width, width,
className, className,
onClickFunc, onClickFunc,
}) => { }) => {
return ( return (
<svg onClick={onClickFunc} <svg onClick={onClickFunc}
className={className} className={className}
fill={color} fill={color}
height={height} 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> 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>
); );
}; };

View File

@ -1,22 +1,22 @@
import { IconTypes } from "./IconTypes"; import { IconTypes } from "./IconTypes";
export const ExpandMoreSVG: React.FC<IconTypes> = ({ export const ExpandMoreSVG: React.FC<IconTypes> = ({
color, color,
height, height,
width, width,
className, className,
onClickFunc onClickFunc
}) => { }) => {
return ( return (
<svg <svg
onClick={onClickFunc} onClick={onClickFunc}
height={height} height={height}
width={width} width={width}
fill={color} fill={color}
className={className} className={className}
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 -960 960 960" viewBox="0 -960 960 960"
> >
<path d="M480-345 240-585l43-43 197 198 197-197 43 43-240 239Z" /> <path d="M480-345 240-585l43-43 197 198 197-197 43 43-240 239Z" />
</svg> </svg>
); );
}; };

View File

@ -1,7 +1,7 @@
export interface IconTypes { export interface IconTypes {
color?: string; color?: string;
height: string; height: string;
width: string; width: string;
className?: string; className?: string;
onClickFunc?: (e?: any) => void; onClickFunc?: (e?: any) => void;
} }

View File

@ -1,23 +1,23 @@
import { IconTypes } from './IconTypes' import { IconTypes } from './IconTypes'
export const LightModeSVG: React.FC<IconTypes> = ({ export const LightModeSVG: React.FC<IconTypes> = ({
color, color,
height, height,
width, width,
className, className,
onClickFunc onClickFunc
}) => { }) => {
return ( return (
<svg <svg
className={className} className={className}
onClick={onClickFunc} onClick={onClickFunc}
fill={color} fill={color}
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
height={height} height={height}
viewBox="0 96 960 960" viewBox="0 96 960 960"
width={width} 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" /> <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> </svg>
) )
} }

View File

@ -1,18 +1,18 @@
import { IconTypes } from "./IconTypes"; import { IconTypes } from "./IconTypes";
export const PlaylistSVG: React.FC<IconTypes> = ({ export const PlaylistSVG: React.FC<IconTypes> = ({
color, color,
height, height,
width, width,
className, className,
onClickFunc onClickFunc
}) => { }) => {
return ( return (
<svg onClick={onClickFunc} <svg onClick={onClickFunc}
className={className} className={className}
fill={color} fill={color}
height={height} 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> 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>
); );
}; };

View File

@ -1,23 +1,23 @@
import { IconTypes } from "./IconTypes"; import { IconTypes } from "./IconTypes";
export const TimesSVG: React.FC<IconTypes> = ({ export const TimesSVG: React.FC<IconTypes> = ({
color, color,
height, height,
width, width,
className, className,
onClickFunc onClickFunc
}) => { }) => {
return ( return (
<svg <svg
onClick={onClickFunc} onClick={onClickFunc}
className={className} className={className}
fill={color} fill={color}
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
height={height} height={height}
viewBox="0 -960 960 960" viewBox="0 -960 960 960"
width={width} 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" /> <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> </svg>
); );
}; };

View File

@ -25,8 +25,8 @@ import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll"; import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll";
import { TextEditor } from "../common/TextEditor/TextEditor"; import { TextEditor } from "../common/TextEditor/TextEditor";
import { extractTextFromHTML } from "../common/TextEditor/utils"; import { extractTextFromHTML } from "../common/TextEditor/utils";
import { allCategoryData } from "../../constants/Categories/1stCategories.ts"; import { allCategoryData } from "../../constants/Categories/Categories.ts";
import { titleFormatter } from "../../constants/Misc.ts"; import { log, titleFormatter } from "../../constants/Misc.ts";
import { import {
CategoryList, CategoryList,
CategoryListRef, CategoryListRef,
@ -37,6 +37,13 @@ import {
ImagePublisherRef, ImagePublisherRef,
} from "../common/ImagePublisher/ImagePublisher.tsx"; } from "../common/ImagePublisher/ImagePublisher.tsx";
import { ThemeButtonBright } from "../../pages/Home/Home-styles.tsx"; import { ThemeButtonBright } from "../../pages/Home/Home-styles.tsx";
import {
AutocompleteQappNames,
QappNamesRef,
} from "../common/AutocompleteQappNames.tsx";
import { payPublishFeeQORT } from "../../constants/PublishFees/SendFeeFunctions.ts";
import { feeAmountBase } from "../../constants/PublishFees/FeeData.tsx";
import { verifyPayment } from "../../constants/PublishFees/VerifyPayment.ts";
const uid = new ShortUniqueId(); const uid = new ShortUniqueId();
const shortuid = new ShortUniqueId({ length: 5 }); const shortuid = new ShortUniqueId({ length: 5 });
@ -58,6 +65,7 @@ interface VideoFile {
identifier?: string; identifier?: string;
filename?: string; filename?: string;
} }
export const EditIssue = () => { export const EditIssue = () => {
const theme = useTheme(); const theme = useTheme();
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -65,9 +73,13 @@ export const EditIssue = () => {
const userAddress = useSelector( const userAddress = useSelector(
(state: RootState) => state.auth?.user?.address (state: RootState) => state.auth?.user?.address
); );
const editFileProperties = useSelector( const editIssueProperties = useSelector(
(state: RootState) => state.file.editFileProperties (state: RootState) => state.file.editFileProperties
); );
const QappNames = useSelector(
(state: RootState) => state.file.publishedQappNames
);
const [publishes, setPublishes] = useState<any>(null); const [publishes, setPublishes] = useState<any>(null);
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false); const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false);
const [videoPropertiesToSetToRedux, setVideoPropertiesToSetToRedux] = const [videoPropertiesToSetToRedux, setVideoPropertiesToSetToRedux] =
@ -78,9 +90,11 @@ export const EditIssue = () => {
const [coverImage, setCoverImage] = useState<string>(""); const [coverImage, setCoverImage] = useState<string>("");
const [file, setFile] = useState(null); const [file, setFile] = useState(null);
const [files, setFiles] = useState<VideoFile[]>([]); const [files, setFiles] = useState<VideoFile[]>([]);
const [editCategories, setEditCategories] = useState<string[]>([]); const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [isIssuePaid, setIsIssuePaid] = useState<boolean>(true);
const categoryListRef = useRef<CategoryListRef>(null); const categoryListRef = useRef<CategoryListRef>(null);
const imagePublisherRef = useRef<ImagePublisherRef>(null); const imagePublisherRef = useRef<ImagePublisherRef>(null);
const autocompleteRef = useRef<QappNamesRef>(null);
const { getRootProps, getInputProps } = useDropzone({ const { getRootProps, getInputProps } = useDropzone({
maxFiles: 10, maxFiles: 10,
@ -118,21 +132,25 @@ export const EditIssue = () => {
}); });
useEffect(() => { useEffect(() => {
if (editFileProperties) { if (editIssueProperties) {
setTitle(editFileProperties?.title || ""); setTitle(editIssueProperties?.title || "");
setFiles(editFileProperties?.files || []); setFiles(editIssueProperties?.files || []);
if (editFileProperties?.htmlDescription) { if (editIssueProperties?.htmlDescription) {
setDescription(editFileProperties?.htmlDescription); setDescription(editIssueProperties?.htmlDescription);
} else if (editFileProperties?.fullDescription) { } else if (editIssueProperties?.fullDescription) {
const paragraph = `<p>${editFileProperties?.fullDescription}</p>`; const paragraph = `<p>${editIssueProperties?.fullDescription}</p>`;
setDescription(paragraph); setDescription(paragraph);
} }
verifyPayment(editIssueProperties).then(isIssuePaid => {
setIsIssuePaid(isIssuePaid);
});
const categoriesFromEditFile = const categoriesFromEditFile =
getCategoriesFromObject(editFileProperties); getCategoriesFromObject(editIssueProperties);
setEditCategories(categoriesFromEditFile); setSelectedCategories(categoriesFromEditFile);
} }
}, [editFileProperties]); }, [editIssueProperties]);
const onClose = () => { const onClose = () => {
dispatch(setEditFile(null)); dispatch(setEditFile(null));
setVideoPropertiesToSetToRedux(null); setVideoPropertiesToSetToRedux(null);
@ -142,14 +160,22 @@ export const EditIssue = () => {
setCoverImage(""); setCoverImage("");
}; };
async function publishQDNResource() { async function publishQDNResource(payFee: boolean) {
try { try {
const categoryList = categoryListRef.current?.getSelectedCategories(); if (!categoryListRef.current) throw new Error("No CategoryListRef found");
if (!description) throw new Error("Please enter a description");
if (!categoryList[0]) throw new Error("Please select a category");
if (!editFileProperties) return;
if (!userAddress) throw new Error("Unable to locate user address"); if (!userAddress) throw new Error("Unable to locate user address");
if (!description) throw new Error("Please enter a description");
const allCategoriesSelected = !selectedCategories.includes("");
if (!allCategoriesSelected)
throw new Error("All Categories must be selected");
console.log("categories", selectedCategories);
const QappsCategoryID = "3";
if (
selectedCategories[0] === QappsCategoryID &&
!autocompleteRef?.current?.getSelectedValue()
)
throw new Error("Select a published Q-App");
let errorMsg = ""; let errorMsg = "";
let name = ""; let name = "";
if (username) { if (username) {
@ -160,7 +186,7 @@ export const EditIssue = () => {
"Cannot publish without access to your name. Please authenticate."; "Cannot publish without access to your name. Please authenticate.";
} }
if (editFileProperties?.user !== username) { if (editIssueProperties?.user !== username) {
errorMsg = "Cannot publish another user's resource"; errorMsg = "Cannot publish another user's resource";
} }
@ -223,9 +249,8 @@ export const EditIssue = () => {
filename = alphanumericString; filename = alphanumericString;
} }
let metadescription = const categoryString = `**${categoryListRef.current?.getSelectedCategories()}**`;
`**${categoryListRef.current?.getCategoriesFetchString()}**` + let metadescription = categoryString + fullDescription.slice(0, 150);
fullDescription.slice(0, 150);
const requestBodyVideo: any = { const requestBodyVideo: any = {
action: "PUBLISH_QDN_RESOURCE", action: "PUBLISH_QDN_RESOURCE",
@ -248,23 +273,50 @@ export const EditIssue = () => {
size: file.size, size: file.size,
}); });
} }
const selectedQappName = autocompleteRef?.current?.getSelectedValue();
const fileObject: any = { const issueObject: any = {
title, title,
version: editFileProperties.version, version: editIssueProperties.version,
fullDescription, fullDescription,
htmlDescription: description, htmlDescription: description,
commentsId: editFileProperties.commentsId, commentsId: editIssueProperties.commentsId,
...categoryListRef.current?.categoriesToObject(), ...categoryListRef.current?.categoriesToObject(),
files: fileReferences, files: fileReferences,
images: imagePublisherRef?.current?.getImageArray(), images: imagePublisherRef?.current?.getImageArray(),
QappName: selectedQappName,
feeData: editIssueProperties?.feeData,
}; };
if (payFee) {
const publishFeeResponse = await payPublishFeeQORT(feeAmountBase);
if (!publishFeeResponse) {
dispatch(
setNotification({
msg: "Fee publish rejected by user.",
alertType: "error",
})
);
return;
}
if (log) console.log("feeResponse: ", publishFeeResponse);
let metadescription = issueObject.feeData = { signature: publishFeeResponse };
`**${categoryListRef.current?.getCategoriesFetchString()}**` + dispatch(updateInHashMap(issueObject)); // shows issue as paid right away?
fullDescription.slice(0, 150); }
const fileObjectToBase64 = await objectToBase64(fileObject); const QappNameString = autocompleteRef?.current?.getQappNameFetchString();
const categoryString =
categoryListRef.current?.getCategoriesFetchString(selectedCategories);
const metaDataString = `**${categoryString + QappNameString}**`;
let metadescription = metaDataString + fullDescription.slice(0, 150);
if (log) console.log("description is: ", metadescription);
if (log) console.log("description length is: ", metadescription.length);
if (log) console.log("characters left:", 240 - metadescription.length);
if (log)
console.log("% of characters used:", metadescription.length / 240);
const fileObjectToBase64 = await objectToBase64(issueObject);
// Description is obtained from raw data // Description is obtained from raw data
const requestBodyJson: any = { const requestBodyJson: any = {
@ -274,7 +326,7 @@ export const EditIssue = () => {
data64: fileObjectToBase64, data64: fileObjectToBase64,
title: title.slice(0, 50), title: title.slice(0, 50),
description: metadescription, description: metadescription,
identifier: editFileProperties.id, identifier: editIssueProperties.id,
tag1: QSUPPORT_FILE_BASE, tag1: QSUPPORT_FILE_BASE,
filename: `video_metadata.json`, filename: `video_metadata.json`,
}; };
@ -287,10 +339,13 @@ export const EditIssue = () => {
setPublishes(multiplePublish); setPublishes(multiplePublish);
setIsOpenMultiplePublish(true); setIsOpenMultiplePublish(true);
setVideoPropertiesToSetToRedux({ setVideoPropertiesToSetToRedux({
...editFileProperties, ...editIssueProperties,
...fileObject, ...issueObject,
}); });
} catch (error: any) { } catch (error: any) {
console.log("error is: ", error);
if (error === "User declined request") return;
let notificationObj: any = null; let notificationObj: any = null;
if (typeof error === "string") { if (typeof error === "string") {
notificationObj = { notificationObj = {
@ -315,26 +370,15 @@ export const EditIssue = () => {
} }
} }
const handleOnchange = (index: number, type: string, value: string) => { const isShowQappNameTextField = () => {
// setFiles((prev) => { const QappID = "3";
// let formattedValue = value return selectedCategories[0] === QappID;
// console.log({type})
// if(type === 'title'){
// formattedValue = value.replace(/[^a-zA-Z0-9\s]/g, "")
// }
// const copyFiles = [...prev];
// copyFiles[index] = {
// ...copyFiles[index],
// [type]: formattedValue,
// };
// return copyFiles;
// });
}; };
return ( return (
<> <>
<Modal <Modal
open={!!editFileProperties} open={!!editIssueProperties}
aria-labelledby="modal-title" aria-labelledby="modal-title"
aria-describedby="modal-description" aria-describedby="modal-description"
> >
@ -410,15 +454,26 @@ export const EditIssue = () => {
> >
<CategoryList <CategoryList
categoryData={allCategoryData} categoryData={allCategoryData}
initialCategories={editCategories} initialCategories={selectedCategories}
columns={3} columns={3}
ref={categoryListRef} ref={categoryListRef}
showEmptyItem={false}
afterChange={newSelectedCategories => {
setSelectedCategories(newSelectedCategories);
}}
/> />
</Box> </Box>
</Box> </Box>
{isShowQappNameTextField() && (
<AutocompleteQappNames
ref={autocompleteRef}
namesList={QappNames}
initialSelection={editIssueProperties?.QappName}
/>
)}
<ImagePublisher <ImagePublisher
ref={imagePublisherRef} ref={imagePublisherRef}
initialImages={editFileProperties?.images} initialImages={editIssueProperties?.images}
/> />
<CustomInputField <CustomInputField
name="title" name="title"
@ -466,10 +521,27 @@ export const EditIssue = () => {
alignItems: "center", alignItems: "center",
}} }}
> >
{isIssuePaid === false && (
<ThemeButtonBright
variant="contained"
onClick={() => {
publishQDNResource(true);
}}
sx={{
fontFamily: "Montserrat",
fontSize: "16px",
fontWeight: 400,
letterSpacing: "0.2px",
}}
>
Publish Edit with Fee
</ThemeButtonBright>
)}
<ThemeButtonBright <ThemeButtonBright
variant="contained" variant="contained"
onClick={() => { onClick={() => {
publishQDNResource(); publishQDNResource(false);
}} }}
sx={{ sx={{
fontFamily: "Montserrat", fontFamily: "Montserrat",
@ -478,7 +550,7 @@ export const EditIssue = () => {
letterSpacing: "0.2px", letterSpacing: "0.2px",
}} }}
> >
Publish Publish Edit
</ThemeButtonBright> </ThemeButtonBright>
</Box> </Box>
</CrowdfundActionButtonRow> </CrowdfundActionButtonRow>
@ -506,7 +578,7 @@ export const EditIssue = () => {
dispatch(updateInHashMap(clonedCopy)); dispatch(updateInHashMap(clonedCopy));
dispatch( dispatch(
setNotification({ setNotification({
msg: "File updated", msg: "Issue updated",
alertType: "success", alertType: "success",
}) })
); );

View File

@ -43,9 +43,9 @@ import { PlaylistListEdit } from "../PlaylistListEdit/PlaylistListEdit";
import { TextEditor } from "../common/TextEditor/TextEditor"; import { TextEditor } from "../common/TextEditor/TextEditor";
import { extractTextFromHTML } from "../common/TextEditor/utils"; import { extractTextFromHTML } from "../common/TextEditor/utils";
import { import {
firstCategories, issueLocation,
secondCategories, thirdCategories,
} from "../../constants/Categories/1stCategories.ts"; } from "../../constants/Categories/Categories.ts";
const uid = new ShortUniqueId(); const uid = new ShortUniqueId();
const shortuid = new ShortUniqueId({ length: 5 }); const shortuid = new ShortUniqueId({ length: 5 });
@ -183,7 +183,7 @@ export const EditPlaylist = () => {
setVideos(editVideoProperties?.videos || []); setVideos(editVideoProperties?.videos || []);
if (editVideoProperties?.category) { if (editVideoProperties?.category) {
const selectedOption = firstCategories.find( const selectedOption = issueLocation.find(
option => option.id === +editVideoProperties.category option => option.id === +editVideoProperties.category
); );
setSelectedCategoryVideos(selectedOption || null); setSelectedCategoryVideos(selectedOption || null);
@ -192,9 +192,9 @@ export const EditPlaylist = () => {
if ( if (
editVideoProperties?.category && editVideoProperties?.category &&
editVideoProperties?.subcategory && editVideoProperties?.subcategory &&
secondCategories[+editVideoProperties?.category] thirdCategories[+editVideoProperties?.category]
) { ) {
const selectedOption = secondCategories[ const selectedOption = thirdCategories[
+editVideoProperties?.category +editVideoProperties?.category
]?.find(option => option.id === +editVideoProperties.subcategory); ]?.find(option => option.id === +editVideoProperties.subcategory);
setSelectedSubCategoryVideos(selectedOption || null); setSelectedSubCategoryVideos(selectedOption || null);
@ -405,7 +405,7 @@ export const EditPlaylist = () => {
event: SelectChangeEvent<string> event: SelectChangeEvent<string>
) => { ) => {
const optionId = event.target.value; const optionId = event.target.value;
const selectedOption = firstCategories.find( const selectedOption = issueLocation.find(
option => option.id === +optionId option => option.id === +optionId
); );
setSelectedCategoryVideos(selectedOption || null); setSelectedCategoryVideos(selectedOption || null);
@ -479,7 +479,7 @@ export const EditPlaylist = () => {
value={selectedCategoryVideos?.id || ""} value={selectedCategoryVideos?.id || ""}
onChange={handleOptionCategoryChangeVideos} onChange={handleOptionCategoryChangeVideos}
> >
{firstCategories.map(option => ( {issueLocation.map(option => (
<MenuItem key={option.id} value={option.id}> <MenuItem key={option.id} value={option.id}>
{option.name} {option.name}
</MenuItem> </MenuItem>
@ -487,7 +487,7 @@ export const EditPlaylist = () => {
</Select> </Select>
</FormControl> </FormControl>
{selectedCategoryVideos && {selectedCategoryVideos &&
secondCategories[selectedCategoryVideos?.id] && ( thirdCategories[selectedCategoryVideos?.id] && (
<FormControl fullWidth sx={{ marginBottom: 2 }}> <FormControl fullWidth sx={{ marginBottom: 2 }}>
<InputLabel id="Category">Select a Sub-Category</InputLabel> <InputLabel id="Category">Select a Sub-Category</InputLabel>
<Select <Select
@ -497,11 +497,11 @@ export const EditPlaylist = () => {
onChange={e => onChange={e =>
handleOptionSubCategoryChangeVideos( handleOptionSubCategoryChangeVideos(
e, e,
secondCategories[selectedCategoryVideos?.id] thirdCategories[selectedCategoryVideos?.id]
) )
} }
> >
{secondCategories[selectedCategoryVideos.id].map( {thirdCategories[selectedCategoryVideos.id].map(
option => ( option => (
<MenuItem key={option.id} value={option.id}> <MenuItem key={option.id} value={option.id}>
{option.name} {option.name}

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@ import {
Accordion, Accordion,
AccordionDetails, AccordionDetails,
AccordionSummary, AccordionSummary,
Autocomplete,
Box, Box,
Button, Button,
Grid, Grid,
@ -11,8 +12,10 @@ import {
TextField, TextField,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternate"; import AddPhotoAlternateIcon from "@mui/icons-material/AddPhotoAlternate";
import { TimesSVG } from "../../assets/svgs/TimesSVG"; import { TimesSVG } from "../../assets/svgs/TimesSVG";
import { fontSizeMedium } from "../../constants/Misc.ts";
export const DoubleLine = styled(Typography)` export const DoubleLine = styled(Typography)`
display: -webkit-box; display: -webkit-box;
@ -59,7 +62,7 @@ export const ModalBody = styled(Box)(({ theme }) => ({
left: "50%", left: "50%",
transform: "translate(-50%, -50%)", transform: "translate(-50%, -50%)",
width: "75%", width: "75%",
maxWidth: "900px", maxWidth: "1000px",
padding: "15px 35px", padding: "15px 35px",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
@ -113,50 +116,59 @@ export const NewCrowdfundTimeDescription = styled(Typography)(({ theme }) => ({
textDecoration: "underline", textDecoration: "underline",
})); }));
const getInputFieldStyles = (theme: any) => {
return {
fontFamily: "Mulish",
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: fontSizeMedium,
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: "25px",
letterSpacing: "0px",
fontWeight: 400,
},
"& [class$='-MuiFilledInput-root']": {
padding: "30px 12px 8px",
},
"& .MuiFilledInput-root:after": {
borderBottomColor: theme.palette.secondary.main,
},
};
};
export const CustomInputField = styled(TextField)(({ theme }) => ({ export const CustomInputField = styled(TextField)(({ theme }) => ({
fontFamily: "Mulish", ...getInputFieldStyles(theme),
fontSize: "19px", }));
letterSpacing: "0px",
fontWeight: 400, export const CustomAutoCompleteField = styled(Autocomplete)(({ theme }) => ({
color: theme.palette.text.primary, ...getInputFieldStyles(theme),
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 }) => ({ export const CrowdfundTitle = styled(Typography)(({ theme }) => ({

View File

@ -21,19 +21,34 @@ import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll"; import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll";
import { TextEditor } from "../common/TextEditor/TextEditor"; import { TextEditor } from "../common/TextEditor/TextEditor";
import { extractTextFromHTML } from "../common/TextEditor/utils"; import { extractTextFromHTML } from "../common/TextEditor/utils";
import { allCategoryData } from "../../constants/Categories/1stCategories.ts"; import { allCategoryData } from "../../constants/Categories/Categories.ts";
import { titleFormatter } from "../../constants/Misc.ts"; import {
fontSizeLarge,
fontSizeSmall,
log,
titleFormatter,
} from "../../constants/Misc.ts";
import { import {
appendCategoryToList,
CategoryList, CategoryList,
CategoryListRef, CategoryListRef,
} from "../common/CategoryList/CategoryList.tsx"; } from "../common/CategoryList/CategoryList.tsx";
import { SupportState } from "../../constants/Categories/2ndCategories.ts";
import { import {
ImagePublisher, ImagePublisher,
ImagePublisherRef, ImagePublisherRef,
} from "../common/ImagePublisher/ImagePublisher.tsx"; } from "../common/ImagePublisher/ImagePublisher.tsx";
import { ThemeButtonBright } from "../../pages/Home/Home-styles.tsx"; import { ThemeButtonBright } from "../../pages/Home/Home-styles.tsx";
import {
AutocompleteQappNames,
QappNamesRef,
} from "../common/AutocompleteQappNames.tsx";
import {
feeAmountBase,
feeDisclaimer,
} from "../../constants/PublishFees/FeeData.tsx";
import {
payPublishFeeQORT,
PublishFeeData,
} from "../../constants/PublishFees/SendFeeFunctions.ts";
const uid = new ShortUniqueId(); const uid = new ShortUniqueId();
const shortuid = new ShortUniqueId({ length: 5 }); const shortuid = new ShortUniqueId({ length: 5 });
@ -53,14 +68,21 @@ interface VideoFile {
description: string; description: string;
coverImage?: string; coverImage?: string;
} }
export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => { export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
const theme = useTheme(); const theme = useTheme();
const dispatch = useDispatch(); const dispatch = useDispatch();
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false); const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false);
const [QappName, setQappName] = useState<string>("");
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const username = useSelector((state: RootState) => state.auth?.user?.name); const username = useSelector((state: RootState) => state.auth?.user?.name);
const userAddress = useSelector( const userAddress = useSelector(
(state: RootState) => state.auth?.user?.address (state: RootState) => state.auth?.user?.address
); );
const QappNames = useSelector(
(state: RootState) => state.file.publishedQappNames
);
const [files, setFiles] = useState<VideoFile[]>([]); const [files, setFiles] = useState<VideoFile[]>([]);
const [isOpen, setIsOpen] = useState<boolean>(false); const [isOpen, setIsOpen] = useState<boolean>(false);
@ -77,8 +99,11 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
const [playlistSetting, setPlaylistSetting] = useState<null | string>(null); const [playlistSetting, setPlaylistSetting] = useState<null | string>(null);
const [publishes, setPublishes] = useState<any>(null); const [publishes, setPublishes] = useState<any>(null);
const categoryListRef = useRef<CategoryListRef>(null); const categoryListRef = useRef<CategoryListRef>(null);
const imagePublisherRef = useRef<ImagePublisherRef>(null); const imagePublisherRef = useRef<ImagePublisherRef>(null);
const autocompleteRef = useRef<QappNamesRef>(null);
const { getRootProps, getInputProps } = useDropzone({ const { getRootProps, getInputProps } = useDropzone({
maxFiles: 10, maxFiles: 10,
maxSize: 419430400, // 400 MB in bytes maxSize: 419430400, // 400 MB in bytes
@ -128,8 +153,18 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
if (!categoryListRef.current) throw new Error("No CategoryListRef found"); if (!categoryListRef.current) throw new Error("No CategoryListRef found");
if (!userAddress) throw new Error("Unable to locate user address"); if (!userAddress) throw new Error("Unable to locate user address");
if (!description) throw new Error("Please enter a description"); if (!description) throw new Error("Please enter a description");
if (!categoryListRef.current?.getSelectedCategories()[0])
throw new Error("Please select a category"); const allCategoriesSelected =
selectedCategories && selectedCategories[0] && selectedCategories[1];
if (!allCategoriesSelected)
throw new Error("All Categories must be selected");
const QappsCategoryID = "3";
if (
selectedCategories[0] === QappsCategoryID &&
!autocompleteRef?.current?.getSelectedValue()
)
throw new Error("Select a published Q-App");
let errorMsg = ""; let errorMsg = "";
let name = ""; let name = "";
if (username) { if (username) {
@ -200,14 +235,10 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
filename = alphanumericString; filename = alphanumericString;
} }
const categoryList = appendCategoryToList( const categoryString = `**${categoryListRef.current?.getSelectedCategories()}**`;
categoryListRef.current?.getSelectedCategories(),
"101"
);
const categoryString = `**${categoryListRef.current?.getCategoriesFetchString(categoryList)}**`;
let metadescription = categoryString + fullDescription.slice(0, 150); let metadescription = categoryString + fullDescription.slice(0, 150);
const requestBodyVideo: any = { const requestBodyFile: any = {
action: "PUBLISH_QDN_RESOURCE", action: "PUBLISH_QDN_RESOURCE",
name: name, name: name,
service: "FILE", service: "FILE",
@ -218,7 +249,7 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
filename, filename,
tag1: QSUPPORT_FILE_BASE, tag1: QSUPPORT_FILE_BASE,
}; };
listOfPublishes.push(requestBodyVideo); listOfPublishes.push(requestBodyFile);
fileReferences.push({ fileReferences.push({
filename: file.name, filename: file.name,
identifier, identifier,
@ -232,12 +263,19 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
const idMeta = uid(); const idMeta = uid();
const identifier = `${QSUPPORT_FILE_BASE}${sanitizeTitle.slice(0, 30)}_${idMeta}`; const identifier = `${QSUPPORT_FILE_BASE}${sanitizeTitle.slice(0, 30)}_${idMeta}`;
const categoryList = appendCategoryToList( const categoryList = categoryListRef.current?.getSelectedCategories();
categoryListRef.current?.getSelectedCategories(),
"101"
);
const fileObject: any = { const selectedQappName = autocompleteRef?.current?.getSelectedValue();
const publishFeeResponse = await payPublishFeeQORT(feeAmountBase);
if (log) console.log("feeResponse: ", publishFeeResponse);
const feeData: PublishFeeData = {
signature: publishFeeResponse,
senderName: "",
};
const issueObject: any = {
title, title,
version: 1, version: 1,
fullDescription, fullDescription,
@ -246,12 +284,24 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
...categoryListRef.current?.categoriesToObject(categoryList), ...categoryListRef.current?.categoriesToObject(categoryList),
files: fileReferences, files: fileReferences,
images: imagePublisherRef?.current?.getImageArray(), images: imagePublisherRef?.current?.getImageArray(),
QappName: selectedQappName,
feeData,
}; };
const categoryString = `**${categoryListRef.current?.getCategoriesFetchString(categoryList)}**`; const QappNameString = autocompleteRef?.current?.getQappNameFetchString();
let metadescription = categoryString + fullDescription.slice(0, 150); const categoryString =
categoryListRef.current?.getCategoriesFetchString(categoryList);
const metaDataString = `**${categoryString + QappNameString}**`;
const fileObjectToBase64 = await objectToBase64(fileObject); let metadescription = metaDataString + fullDescription.slice(0, 150);
if (log) console.log("description is: ", metadescription);
if (log) console.log("description length is: ", metadescription.length);
if (log) console.log("characters left:", 240 - metadescription.length);
if (log)
console.log("% of characters used:", metadescription.length / 240);
const fileObjectToBase64 = await objectToBase64(issueObject);
// Description is obtained from raw data // Description is obtained from raw data
const requestBodyJson: any = { const requestBodyJson: any = {
action: "PUBLISH_QDN_RESOURCE", action: "PUBLISH_QDN_RESOURCE",
@ -295,6 +345,11 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
} }
} }
const isShowQappNameTextField = () => {
const QappID = "3";
return selectedCategories[0] === QappID;
};
return ( return (
<> <>
{username && ( {username && (
@ -386,13 +441,29 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
categoryData={allCategoryData} categoryData={allCategoryData}
ref={categoryListRef} ref={categoryListRef}
columns={3} columns={3}
excludeCategories={SupportState} afterChange={newSelectedCategories => {
if (
newSelectedCategories[0] &&
newSelectedCategories[1] &&
!newSelectedCategories[2]
) {
newSelectedCategories[2] = "101";
}
setSelectedCategories(newSelectedCategories);
}}
showEmptyItem={false}
/> />
</Box> </Box>
{isShowQappNameTextField() && (
<AutocompleteQappNames
ref={autocompleteRef}
namesList={QappNames}
/>
)}
<ImagePublisher ref={imagePublisherRef} /> <ImagePublisher ref={imagePublisherRef} />
<CustomInputField <CustomInputField
name="title" name="title"
label="Title of Issue" label="Title"
variant="filled" variant="filled"
value={title} value={title}
onChange={e => { onChange={e => {
@ -400,15 +471,15 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
const formattedValue = value.replace(titleFormatter, ""); const formattedValue = value.replace(titleFormatter, "");
setTitle(formattedValue); setTitle(formattedValue);
}} }}
inputProps={{ maxLength: 180 }} inputProps={{ maxLength: 60 }}
required required
/> />
<Typography <Typography
sx={{ sx={{
fontSize: "18px", fontSize: fontSizeLarge,
}} }}
> >
Description of Issue Description
</Typography> </Typography>
<TextEditor <TextEditor
inlineContent={description} inlineContent={description}
@ -426,7 +497,10 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
}} }}
variant="contained" variant="contained"
color="error" color="error"
sx={{ color: theme.palette.text.primary }} sx={{
color: theme.palette.text.primary,
fontSize: fontSizeSmall,
}}
> >
Cancel Cancel
</ActionButton> </ActionButton>
@ -439,13 +513,10 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
> >
<ThemeButtonBright <ThemeButtonBright
variant="contained" variant="contained"
onClick={() => { onClick={publishQDNResource}
publishQDNResource();
}}
sx={{ sx={{
fontFamily: "Montserrat", fontFamily: "Montserrat",
fontSize: "16px", fontWeight: "400",
fontWeight: 400,
letterSpacing: "0.2px", letterSpacing: "0.2px",
}} }}
> >
@ -453,6 +524,7 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
</ThemeButtonBright> </ThemeButtonBright>
</Box> </Box>
</ActionButtonRow> </ActionButtonRow>
{feeDisclaimer}
</ModalBody> </ModalBody>
</Modal> </Modal>

View File

@ -1,109 +1,109 @@
import React, { useState, useEffect, CSSProperties } from 'react' import React, { useState, useEffect, CSSProperties } from 'react'
import Skeleton from '@mui/material/Skeleton' import Skeleton from '@mui/material/Skeleton'
import { Box } from '@mui/material' import { Box } from '@mui/material'
interface ResponsiveImageProps { interface ResponsiveImageProps {
src: string src: string
width: number width: number
height: number height: number
alt?: string alt?: string
className?: string className?: string
style?: CSSProperties style?: CSSProperties
} }
const ResponsiveImage: React.FC<ResponsiveImageProps> = ({ const ResponsiveImage: React.FC<ResponsiveImageProps> = ({
src, src,
width, width,
height, height,
alt, alt,
className, className,
style style
}) => { }) => {
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const aspectRatio = (height / width) * 100 const aspectRatio = (height / width) * 100
const imageStyle: CSSProperties = { const imageStyle: CSSProperties = {
width: '100%', width: '100%',
height: '100%', height: '100%',
objectFit: 'cover' objectFit: 'cover'
} }
const wrapperStyle: CSSProperties = { const wrapperStyle: CSSProperties = {
position: 'relative', position: 'relative',
paddingBottom: `${aspectRatio}%`, paddingBottom: `${aspectRatio}%`,
overflow: 'hidden', overflow: 'hidden',
...style ...style
} }
return ( return (
<Box <Box
sx={{ sx={{
padding: '2px', padding: '2px',
maxHeight: '50%' maxHeight: '50%'
}} }}
> >
{loading && ( {loading && (
<Skeleton <Skeleton
variant="rectangular" variant="rectangular"
style={{ style={{
width: '100%', width: '100%',
height: 0, height: 0,
paddingBottom: `${(height / width) * 100}%`, paddingBottom: `${(height / width) * 100}%`,
objectFit: 'contain', objectFit: 'contain',
visibility: loading ? 'visible' : 'hidden', visibility: loading ? 'visible' : 'hidden',
borderRadius: '8px' borderRadius: '8px'
}} }}
/> />
)} )}
<img <img
onLoad={() => setLoading(false)} onLoad={() => setLoading(false)}
src={src} src={src}
style={{ style={{
width: '100%', width: '100%',
height: '100%', height: '100%',
borderRadius: '8px', borderRadius: '8px',
visibility: loading ? 'hidden' : 'visible', visibility: loading ? 'hidden' : 'visible',
position: loading ? 'absolute' : 'unset', position: loading ? 'absolute' : 'unset',
objectFit: 'contain' objectFit: 'contain'
}} }}
/> />
</Box> </Box>
) )
return ( return (
<div style={wrapperStyle} className={className}> <div style={wrapperStyle} className={className}>
{loading ? ( {loading ? (
<Skeleton <Skeleton
variant="rectangular" variant="rectangular"
sx={{ sx={{
position: 'absolute', position: 'absolute',
top: 0, top: 0,
left: 0, left: 0,
right: 0, right: 0,
bottom: 0 bottom: 0
}} }}
/> />
) : ( ) : (
<img <img
src={src} src={src}
alt={alt} alt={alt}
style={{ style={{
...imageStyle, ...imageStyle,
position: 'absolute', position: 'absolute',
top: 0, top: 0,
left: 0 left: 0
}} }}
/> />
)} )}
</div> </div>
) )
} }
export default ResponsiveImage export default ResponsiveImage

View File

@ -15,14 +15,14 @@ export const StatsData = () => {
})); }));
const { const {
getFiles, getIssues,
checkAndUpdateFile, checkAndUpdateIssue,
getFile, getIssue,
hashMapFiles, hashMapFiles,
getNewFiles, getNewIssues,
checkNewFiles, checkNewIssues,
getFilesFiltered, getIssuesFiltered,
getFilesCount, getIssuesCount,
} = useFetchIssues(); } = useFetchIssues();
const totalIssuesPublished = useSelector( const totalIssuesPublished = useSelector(
@ -36,8 +36,8 @@ export const StatsData = () => {
); );
useEffect(() => { useEffect(() => {
getFilesCount(); getIssuesCount();
}, [getFilesCount]); }, [getIssuesCount]);
return ( return (
totalIssuesPublished > 0 && ( totalIssuesPublished > 0 && (

View File

@ -0,0 +1,135 @@
import React, { useEffect, useImperativeHandle, useState } from "react";
import { Autocomplete, SxProps, Theme } from "@mui/material";
import { CustomInputField } from "../PublishIssue/PublishIssue-styles.tsx";
import { log } from "../../constants/Misc.ts";
interface AutoCompleteQappNamesProps {
namesList?: string[];
afterChange?: (selectedName: string) => void;
sx?: SxProps<Theme>;
required?: boolean;
initialSelection?: string;
}
export type QappNamesRef = {
getSelectedValue: () => string;
setSelectedValue: (selectedValue: string) => void;
getQappNameFetchString: () => string;
};
export const AutocompleteQappNames = React.forwardRef<
QappNamesRef,
AutoCompleteQappNamesProps
>(
(
{
namesList,
afterChange,
sx,
required = true,
initialSelection = null,
}: AutoCompleteQappNamesProps,
ref
) => {
const [QappNamesList, setQappNamesList] = useState<string[]>([]);
const [selectedQappName, setSelectedQappName] = useState<string>(
initialSelection || null
);
if (log) console.log("initial selection: ", initialSelection);
useEffect(() => {
if (namesList) {
if (log) console.log("prop namesList: ", namesList);
setQappNamesList(namesList);
return;
}
getPublishedQappNames().then((names: string[]) => {
setQappNamesList(names);
if (log) console.log("QappNames set manually");
});
}, []);
useEffect(() => {
setSelectedQappName(initialSelection || null);
}, [initialSelection]);
useImperativeHandle(ref, () => ({
getSelectedValue: () => {
return selectedQappName;
},
setSelectedValue: (selectedValue: string) => {
setSelectedQappName(selectedValue);
},
getQappNameFetchString: () => {
return getQappNameFetchString(selectedQappName);
},
}));
return (
<Autocomplete
options={QappNamesList}
value={selectedQappName}
onChange={(e, newValue) => {
setSelectedQappName(newValue);
if (afterChange) afterChange(newValue || null);
}}
sx={{ height: "100px", ...sx }}
renderInput={params => (
<CustomInputField
{...params}
label={"Q-App/Website Name"}
value={selectedQappName}
variant={"filled"}
required={required}
/>
)}
/>
);
}
);
export interface MetaData {
title: string;
description: string;
tags: string[];
mimeType: string;
}
export interface SearchResourcesResponse {
name: string;
service: string;
identifier: string;
metadata?: MetaData;
size: number;
created: number;
updated: number;
}
const searchService = (service: string) => {
return qortalRequest({
action: "SEARCH_QDN_RESOURCES",
service: service,
limit: 0,
});
};
export const getPublishedQappNames = async () => {
const QappList: Promise<SearchResourcesResponse[]> = searchService("APP");
const siteList: Promise<SearchResourcesResponse[]> = searchService("WEBSITE");
const responses = await Promise.all([QappList, siteList]);
const processedQappList = responses[0].map(value => value.name);
const processedWebsiteList = responses[1].map(value => value.name);
const removedDuplicates = Array.from(
new Set<string>([...processedQappList, ...processedWebsiteList])
);
return removedDuplicates.sort((a, b) => {
return a.localeCompare(b);
});
};
export const getQappNameFetchString = (selectedQappName: string) => {
return selectedQappName ? `Qapp:${selectedQappName};` : "";
};

View File

@ -12,13 +12,15 @@ import {
import React, { useEffect, useImperativeHandle, useState } from "react"; import React, { useEffect, useImperativeHandle, useState } from "react";
import { CategoryContainer } from "./CategoryList-styles.tsx"; import { CategoryContainer } from "./CategoryList-styles.tsx";
import { allCategoryData } from "../../../constants/Categories/1stCategories.ts"; import { allCategoryData } from "../../../constants/Categories/Categories.ts";
import { log } from "../../../constants/Misc.ts"; import { log } from "../../../constants/Misc.ts";
import { findCategoryData } from "../../../constants/Categories/CategoryFunctions.ts";
export interface Category { export interface Category {
id: number; id: number;
name: string; name: string;
icon?: string; icon?: string;
label?: string;
} }
export interface Categories { export interface Categories {
@ -29,8 +31,6 @@ export interface CategoryData {
subCategories: Categories[]; subCategories: Categories[];
} }
type ListDirection = "column" | "row";
interface CategoryListProps { interface CategoryListProps {
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
categoryData: CategoryData; categoryData: CategoryData;
@ -38,6 +38,7 @@ interface CategoryListProps {
columns?: number; columns?: number;
afterChange?: (categories: string[]) => void; afterChange?: (categories: string[]) => void;
excludeCategories?: Category[]; excludeCategories?: Category[];
showEmptyItem?: boolean;
} }
export type CategoryListRef = { export type CategoryListRef = {
@ -60,6 +61,7 @@ export const CategoryList = React.forwardRef<
columns = 1, columns = 1,
afterChange, afterChange,
excludeCategories, excludeCategories,
showEmptyItem = true,
}: CategoryListProps, }: CategoryListProps,
ref ref
) => { ) => {
@ -127,7 +129,8 @@ export const CategoryList = React.forwardRef<
const newSelectedCategories: string[] = selectedCategories.map( const newSelectedCategories: string[] = selectedCategories.map(
(category, categoryIndex) => { (category, categoryIndex) => {
if (index > categoryIndex) return category; if (index > categoryIndex) return category;
else if (index === categoryIndex) return selectedOption.id.toString(); else if (index === categoryIndex)
return selectedOption?.id?.toString();
else return ""; else return "";
} }
); );
@ -140,23 +143,31 @@ export const CategoryList = React.forwardRef<
}; };
const categorySelectSX = { const categorySelectSX = {
// Target the input field // // Target the input field
".MuiSelect-select": { // ".MuiSelect-select": {
fontSize: "16px", // Change font size for the selected value // padding: "10px 5px 15px 15px;",
padding: "10px 5px 15px 15px;", // },
}, // // Target the dropdown icon
// Target the dropdown icon // ".MuiSelect-icon": {
".MuiSelect-icon": { // fontSize: "20px", // Adjust if needed
fontSize: "20px", // Adjust if needed // },
}, // // Target the dropdown menu
// Target the dropdown menu // "& .MuiMenu-paper": {
"& .MuiMenu-paper": { // ".MuiMenuItem-root": {
".MuiMenuItem-root": { // fontSize: "14px", // Change font size for the menu items
fontSize: "14px", // Change font size for the menu items // },
}, // },
},
}; };
const emptyMenuItem = (
<MenuItem
key={""}
value={""}
sx={{
"@media (min-width: 600px)": { minHeight: "46.5px" },
}}
/>
);
const fillMenu = (category: Categories, index: number) => { const fillMenu = (category: Categories, index: number) => {
const subCategoryIndex = selectedCategories[index]; const subCategoryIndex = selectedCategories[index];
if (log) console.log("selected categories: ", selectedCategories); if (log) console.log("selected categories: ", selectedCategories);
@ -171,12 +182,23 @@ export const CategoryList = React.forwardRef<
if (log) console.log("categoryData: ", categoryData); if (log) console.log("categoryData: ", categoryData);
const menuToFill = category[subCategoryIndex]; const menuToFill = category[subCategoryIndex];
if (menuToFill) if (menuToFill) {
return menuToFill.map(option => ( const menuItems = [];
<MenuItem key={option.id} value={option.id}>
{option.name} if (showEmptyItem) menuItems.push(emptyMenuItem);
</MenuItem>
)); menuToFill.map(option =>
menuItems.push(
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
)
);
if (log) console.log(" returning menuItems: ", menuItems);
return menuItems;
}
if (log) console.log("not returning menuItems");
}; };
const hasSubCategory = (category: Categories, index: number) => { const hasSubCategory = (category: Categories, index: number) => {
@ -202,11 +224,11 @@ export const CategoryList = React.forwardRef<
<FormControl fullWidth sx={{ marginBottom: 1 }}> <FormControl fullWidth sx={{ marginBottom: 1 }}>
<InputLabel <InputLabel
sx={{ sx={{
fontSize: "16px", fontSize: "24px",
}} }}
id="Category-1" id="Category-1"
> >
Category {categoryData.category[0]?.label || "Category"}
</InputLabel> </InputLabel>
<Select <Select
labelId="Category 1" labelId="Category 1"
@ -217,6 +239,7 @@ export const CategoryList = React.forwardRef<
}} }}
sx={categorySelectSX} sx={categorySelectSX}
> >
{showEmptyItem && emptyMenuItem}
{categoryData.category.map(option => ( {categoryData.category.map(option => (
<MenuItem key={option.id} value={option.id}> <MenuItem key={option.id} value={option.id}>
{option.name} {option.name}
@ -237,11 +260,14 @@ export const CategoryList = React.forwardRef<
> >
<InputLabel <InputLabel
sx={{ sx={{
fontSize: "16px", fontSize: "24px",
}} }}
id={`Category-${index + 2}`} id={`Category-${index + 2}`}
> >
{`Category-${index + 2}`} {findCategoryData(+selectedCategories[index + 1])
?.label ||
category[selectedCategories[index]][0]?.label ||
`Category-${index + 2}`}
</InputLabel> </InputLabel>
<Select <Select
labelId={`Category ${index + 2}`} labelId={`Category ${index + 2}`}
@ -250,24 +276,6 @@ export const CategoryList = React.forwardRef<
onChange={e => { onChange={e => {
selectCategoryEvent(e, index + 1); selectCategoryEvent(e, index + 1);
}} }}
sx={{
width: "100%",
// Target the input field
".MuiSelect-select": {
fontSize: "16px", // Change font size for the selected value
padding: "10px 5px 15px 15px;",
},
// Target the dropdown icon
".MuiSelect-icon": {
fontSize: "20px", // Adjust if needed
},
// Target the dropdown menu
"& .MuiMenu-paper": {
".MuiMenuItem-root": {
fontSize: "14px", // Change font size for the menu items
},
},
}}
> >
{fillMenu(category, index)} {fillMenu(category, index)}
</Select> </Select>
@ -285,9 +293,9 @@ export const getCategoriesFetchString = (categories: string[]) => {
let fetchString = ""; let fetchString = "";
categories.map((category, index) => { categories.map((category, index) => {
if (category) { if (category) {
if (index === 0) fetchString += `cat:${category}`; if (index === 0 && category) fetchString += `cat:${category};`;
else if (index === 1) fetchString += `;sub:${category}`; else if (index === 1 && category) fetchString += `sub:${category};`;
else fetchString += `;sub${index}:${category}`; else if (category) fetchString += `sub${index}:${category};`;
} }
}); });
if (log) console.log("categoriesAsDescription: ", fetchString); if (log) console.log("categoriesAsDescription: ", fetchString);
@ -318,3 +326,16 @@ export const getCategoriesFromObject = (editFileProperties: any) => {
} }
return categoryList; return categoryList;
}; };
export const getCategoriesLength = categoryList => {
return categoryList.filter(category => category !== "").length;
};
export const hasCategories = (categories: string[]) => {
return categories.findIndex(category => category !== "") >= 0;
};
export const appendCategory = (categoryList: string[], category: string) => {
const nextIndex = categoryList.findIndex(category => category === "");
categoryList[nextIndex] = category;
};

View File

@ -0,0 +1,178 @@
import {
FormControl,
InputLabel,
MenuItem,
OutlinedInput,
Select,
SxProps,
Theme,
} from "@mui/material";
import React, { useEffect, useImperativeHandle, useState } from "react";
import { CategoryContainer } from "./CategoryList-styles.tsx";
import { log } from "../../../constants/Misc.ts";
export interface Category {
id: number;
name: string;
icon?: string;
label?: string;
}
export interface Categories {
[key: number]: Category[];
}
export interface CategoryData {
category: Category[];
subCategories: Categories[];
}
interface CategoryListProps {
sx?: SxProps<Theme>;
categoryData: Category[];
initialCategory?: string;
afterChange?: (category: string) => void;
showEmptyItem?: boolean;
}
export type CategorySelectRef = {
getSelectedCategory: () => string;
setSelectedCategory: (arr: string) => void;
clearCategory: () => void;
getCategoryFetchString: (categories?: string) => string;
};
export const CategorySelect = React.forwardRef<
CategorySelectRef,
CategoryListProps
>(
(
{
sx,
categoryData,
initialCategory,
afterChange,
showEmptyItem = true,
}: CategoryListProps,
ref
) => {
const [selectedCategory, setSelectedCategory] = useState<string>(
initialCategory || ""
);
useEffect(() => {
if (initialCategory) setSelectedCategory(initialCategory);
}, [initialCategory]);
const updateCategory = (category: string) => {
if (log) console.log("updateCategory ID: ", category);
setSelectedCategory(category);
if (afterChange) afterChange(category);
};
const categoryToObject = (category: string) => {
let categoryObject = {};
categoryObject["category"] = category;
if (log) console.log("categoryObject is: ", categoryObject);
return categoryObject;
};
const clearCategory = () => {
updateCategory("");
};
useImperativeHandle(ref, () => ({
getSelectedCategory: () => {
return selectedCategory;
},
setSelectedCategory: category => {
if (log) console.log("setSelectedCategory: ", category);
updateCategory(category);
},
clearCategory,
getCategoryFetchString: (category?: string) =>
getCategoryFetchString(category || selectedCategory),
categoriesToObject: (category?: string) =>
categoryToObject(category || selectedCategory),
}));
const categorySelectSX = {
// // Target the input field
// ".MuiSelect-select": {
// fontSize: "16px", // Change font size for the selected value
// padding: "10px 5px 15px 15px;",
// },
// // Target the dropdown icon
// ".MuiSelect-icon": {
// fontSize: "20px", // Adjust if needed
// },
// // Target the dropdown menu
// "& .MuiMenu-paper": {
// ".MuiMenuItem-root": {
// fontSize: "14px", // Change font size for the menu items
// },
// },
};
const emptyMenuItem = (
<MenuItem
key={""}
value={""}
// sx={{
// "& .MuiButtonBase-root-MuiMenuItem-root": {
// minHeight: "50px",
// },
sx={{
"@media (min-width: 600px)": { minHeight: "46.5px" },
}}
/>
);
const fillMenu = () => {
const menuItems = [];
if (showEmptyItem) menuItems.push(emptyMenuItem);
categoryData.map(option =>
menuItems.push(
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
)
);
return menuItems;
};
return (
<CategoryContainer sx={{ width: "100%", ...sx }}>
<FormControl fullWidth sx={{ marginBottom: 1 }}>
<InputLabel
sx={{
fontSize: "24px",
}}
id="Category-1"
>
{categoryData[0]?.label || "Category"}
</InputLabel>
<Select
labelId="Category 1"
input={<OutlinedInput label="Category 1" />}
value={selectedCategory || ""}
onChange={e => {
updateCategory(e.target.value);
}}
>
{fillMenu()}
</Select>
</FormControl>
</CategoryContainer>
);
}
);
export const getCategoryFetchString = (category: string) => {
return `cat:${category}`;
};
export const getCategoryFromObject = (editFileProperties: any) => {
const categoryList: string[] = [];
if (editFileProperties.category)
categoryList.push(editFileProperties.category);
return categoryList;
};

View File

@ -1,7 +1,6 @@
import { import {
Avatar, Avatar,
Box, Box,
Button,
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
@ -9,26 +8,26 @@ import {
Typography, Typography,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import React, { useCallback, useState, useEffect } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { CommentEditor } from "./CommentEditor"; import { CommentEditor } from "./CommentEditor";
import { import {
AuthorTextComment,
CardContentContainerComment, CardContentContainerComment,
CommentActionButtonRow, CommentActionButtonRow,
CommentDateText, CommentDateText,
EditReplyButton, 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, LoadMoreCommentsButton as CommentActionButton,
StyledCardColComment,
StyledCardComment,
StyledCardContentComment,
StyledCardHeaderComment,
} from "./Comments-styles"; } from "./Comments-styles";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { RootState } from "../../../state/store"; import { RootState } from "../../../state/store";
import Portal from "../Portal"; import Portal from "../Portal";
import { formatDate } from "../../../utils/time"; import { formatDate } from "../../../utils/time";
import { ThemeButton } from "../../../pages/Home/Home-styles.tsx";
interface CommentProps { interface CommentProps {
comment: any; comment: any;
postId: string; postId: string;
@ -69,12 +68,15 @@ export const Comment = ({
onClose={() => setCurrentEdit(null)} onClose={() => setCurrentEdit(null)}
aria-labelledby="alert-dialog-title" aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description" aria-describedby="alert-dialog-description"
maxWidth={false}
> >
<DialogTitle id="alert-dialog-title"></DialogTitle> <DialogTitle id="alert-dialog-title" sx={{ fontSize: "30px" }}>
Edit Comment
</DialogTitle>
<DialogContent> <DialogContent>
<Box <Box
sx={{ sx={{
width: "300px", width: "1000px",
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
}} }}
@ -90,9 +92,12 @@ export const Comment = ({
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button variant="contained" onClick={() => setCurrentEdit(null)}> <ThemeButton
variant="contained"
onClick={() => setCurrentEdit(null)}
>
Close Close
</Button> </ThemeButton>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
</Portal> </Portal>
@ -125,13 +130,15 @@ export const Comment = ({
</Typography> </Typography>
)} )}
<CommentActionButtonRow> <CommentActionButtonRow>
<CommentActionButton {user?.name !== comment?.name && (
size="small" <CommentActionButton
variant="contained" size="small"
onClick={() => setIsReplying(true)} variant="contained"
> onClick={() => setIsReplying(true)}
reply >
</CommentActionButton> reply
</CommentActionButton>
)}
{user?.name === comment?.name && ( {user?.name === comment?.name && (
<CommentActionButton <CommentActionButton
size="small" size="small"

View File

@ -1,10 +1,8 @@
import { Box, Button, TextField } from "@mui/material";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../../state/store"; import { RootState } from "../../../state/store";
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
import { setNotification } from "../../../state/features/notificationsSlice"; import { setNotification } from "../../../state/features/notificationsSlice";
import { toBase64 } from "../../../utils/toBase64";
import localforage from "localforage"; import localforage from "localforage";
import { import {
CommentInput, CommentInput,
@ -12,6 +10,9 @@ import {
SubmitCommentButton, SubmitCommentButton,
} from "./Comments-styles"; } from "./Comments-styles";
import { QSUPPORT_COMMENT_BASE } from "../../../constants/Identifiers.ts"; import { QSUPPORT_COMMENT_BASE } from "../../../constants/Identifiers.ts";
import { sendQchatDM } from "../../../utils/qortalRequests.ts";
import { maxCommentLength } from "../../../constants/Misc.ts";
const uid = new ShortUniqueId(); const uid = new ShortUniqueId();
const notification = localforage.createInstance({ const notification = localforage.createInstance({
@ -123,7 +124,6 @@ export const CommentEditor = ({
let address; let address;
let name; let name;
let errorMsg = ""; let errorMsg = "";
address = user?.address; address = user?.address;
name = user?.name || ""; name = user?.name || "";
@ -134,8 +134,8 @@ export const CommentEditor = ({
errorMsg = "Cannot post without a name"; errorMsg = "Cannot post without a name";
} }
if (value.length > 200) { if (value.length > maxCommentLength) {
errorMsg = "Comment needs to be under 200 characters"; errorMsg = `Comment needs to be under ${maxCommentLength} characters`;
} }
if (errorMsg) { if (errorMsg) {
@ -157,6 +157,7 @@ export const CommentEditor = ({
data64: base64, data64: base64,
identifier: identifier, identifier: identifier,
}); });
dispatch( dispatch(
setNotification({ setNotification({
msg: "Comment successfully published", msg: "Comment successfully published",
@ -171,7 +172,19 @@ export const CommentEditor = ({
postName: postName, postName: postName,
}); });
} }
if (!isReply && !isEdit) {
// const notificationMessage = `This is an automated Q-Support notification indicating that someone has commented on your issue here:
// qortal://APP/Q-Support/issue/${postName}/${postId}
//
// Here are the first ${maxNotificationLength} characters of the comment:
//
// ${value.substring(0, maxNotificationLength)}`;
const notificationMessage = `This is an automated Q-Support notification indicating that someone has commented on your issue here:
qortal://APP/Q-Support/issue/${postName}/${postId}`;
await sendQchatDM(postName, notificationMessage);
}
return resourceResponse; return resourceResponse;
} catch (error: any) { } catch (error: any) {
let notificationObj: any = null; let notificationObj: any = null;
@ -236,11 +249,11 @@ export const CommentEditor = ({
id="standard-multiline-flexible" id="standard-multiline-flexible"
label="Your comment" label="Your comment"
multiline multiline
maxRows={4} maxRows={10}
variant="filled" variant="filled"
value={value} value={value}
inputProps={{ inputProps={{
maxLength: 200, maxLength: maxCommentLength,
}} }}
InputLabelProps={{ style: { fontSize: "18px" } }} InputLabelProps={{ style: { fontSize: "18px" } }}
onChange={e => setValue(e.target.value)} onChange={e => setValue(e.target.value)}
@ -252,3 +265,5 @@ export const CommentEditor = ({
</CommentInputContainer> </CommentInputContainer>
); );
}; };
const sendDMwithComment = () => {};

View File

@ -1,5 +1,6 @@
import { styled } from "@mui/system"; import { styled } from "@mui/system";
import { Card, Box, Typography, Button, TextField } from "@mui/material"; import { Box, Button, Card, TextField, Typography } from "@mui/material";
import { ThemeButton } from "../../../pages/Home/Home-styles.tsx";
export const StyledCard = styled(Card)(({ theme }) => ({ export const StyledCard = styled(Card)(({ theme }) => ({
backgroundColor: backgroundColor:
@ -93,7 +94,7 @@ export const StyledCardComment = styled(Typography)(({ theme }) => ({
fontWeight: 400, fontWeight: 400,
color: theme.palette.text.primary, color: theme.palette.text.primary,
fontSize: "19px", fontSize: "19px",
wordBreak: "break-word" wordBreak: "break-word",
})); }));
export const TitleText = styled(Typography)({ export const TitleText = styled(Typography)({
@ -200,13 +201,10 @@ export const EditReplyButton = styled(Button)(({ theme }) => ({
color: "#ffffff", color: "#ffffff",
})); }));
export const LoadMoreCommentsButton = styled(Button)(({ theme }) => ({ export const LoadMoreCommentsButton = styled(ThemeButton)(({ theme }) => ({
fontFamily: "Montserrat", fontFamily: "Montserrat",
fontWeight: 400, fontWeight: 400,
letterSpacing: "0.2px", letterSpacing: "0.2px",
fontSize: "15px",
backgroundColor: theme.palette.primary.main,
color: "#ffffff",
})); }));
export const CommentActionButtonRow = styled(Box)({ export const CommentActionButtonRow = styled(Box)({
@ -234,8 +232,7 @@ export const CommentInputContainer = styled(Box)({
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
marginTop: "15px", marginTop: "15px",
width: "90%", width: "100%",
maxWidth: "1000px",
borderRadius: "8px", borderRadius: "8px",
gap: "10px", gap: "10px",
alignItems: "center", alignItems: "center",
@ -270,12 +267,9 @@ export const CommentInput = styled(TextField)(({ theme }) => ({
}, },
})); }));
export const SubmitCommentButton = styled(Button)(({ theme }) => ({ export const SubmitCommentButton = styled(ThemeButton)(({ theme }) => ({
fontFamily: "Montserrat", fontFamily: "Montserrat",
fontWeight: 400, fontWeight: 400,
letterSpacing: "0.2px", letterSpacing: "0.2px",
fontSize: "15px",
backgroundColor: theme.palette.primary.main,
color: "#ffffff",
width: "75%", width: "75%",
})); }));

View File

@ -1,446 +1,446 @@
import * as React from "react"; import * as React from "react";
import { styled, useTheme } from "@mui/material/styles"; import { styled, useTheme } from "@mui/material/styles";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { CircularProgress } from "@mui/material"; import { CircularProgress } from "@mui/material";
import AttachFileIcon from "@mui/icons-material/AttachFile"; import AttachFileIcon from "@mui/icons-material/AttachFile";
import { MyContext } from "../../wrappers/DownloadWrapper"; import { MyContext } from "../../wrappers/DownloadWrapper";
import { RootState } from "../../state/store"; import { RootState } from "../../state/store";
import { setNotification } from "../../state/features/notificationsSlice"; import { setNotification } from "../../state/features/notificationsSlice";
const Widget = styled("div")(({ theme }) => ({ const Widget = styled("div")(({ theme }) => ({
padding: 8, padding: 8,
borderRadius: 10, borderRadius: 10,
maxWidth: 350, maxWidth: 350,
position: "relative", position: "relative",
zIndex: 1, zIndex: 1,
backdropFilter: "blur(40px)", backdropFilter: "blur(40px)",
background: "skyblue", background: "skyblue",
transition: "0.2s all", transition: "0.2s all",
"&:hover": { "&:hover": {
opacity: 0.75, opacity: 0.75,
}, },
})); }));
const CoverImage = styled("div")({ const CoverImage = styled("div")({
width: 40, width: 40,
height: 40, height: 40,
objectFit: "cover", objectFit: "cover",
overflow: "hidden", overflow: "hidden",
flexShrink: 0, flexShrink: 0,
borderRadius: 8, borderRadius: 8,
backgroundColor: "rgba(0,0,0,0.08)", backgroundColor: "rgba(0,0,0,0.08)",
"& > img": { "& > img": {
width: "100%", width: "100%",
}, },
}); });
interface IAudioElement { interface IAudioElement {
title: string; title: string;
description?: string; description?: string;
author?: string; author?: string;
fileInfo?: any; fileInfo?: any;
postId?: string; postId?: string;
user?: string; user?: string;
children?: React.ReactNode; children?: React.ReactNode;
mimeType?: string; mimeType?: string;
disable?: boolean; disable?: boolean;
mode?: string; mode?: string;
otherUser?: string; otherUser?: string;
customStyles?: any; customStyles?: any;
jsonId:string; jsonId:string;
} }
interface CustomWindow extends Window { interface CustomWindow extends Window {
showSaveFilePicker: any; // Replace 'any' with the appropriate type if you know it showSaveFilePicker: any; // Replace 'any' with the appropriate type if you know it
} }
const customWindow = window as unknown as CustomWindow; const customWindow = window as unknown as CustomWindow;
export default function FileElement({ export default function FileElement({
title, title,
description, description,
author, author,
fileInfo, fileInfo,
children, children,
mimeType, mimeType,
disable, disable,
customStyles, customStyles,
jsonId jsonId
}: IAudioElement) { }: IAudioElement) {
const { downloadVideo } = React.useContext(MyContext); const { downloadVideo } = React.useContext(MyContext);
const [startedDownload, setStartedDownload] = React.useState<boolean>(false) const [startedDownload, setStartedDownload] = React.useState<boolean>(false)
const [isLoading, setIsLoading] = React.useState<boolean>(false); const [isLoading, setIsLoading] = React.useState<boolean>(false);
const [fileProperties, setFileProperties] = React.useState<any>(null); const [fileProperties, setFileProperties] = React.useState<any>(null);
const [downloadLoader, setDownloadLoader] = React.useState<any>(false); const [downloadLoader, setDownloadLoader] = React.useState<any>(false);
const downloads = useSelector((state: RootState) => state.global?.downloads); const downloads = useSelector((state: RootState) => state.global?.downloads);
const status = React.useRef<null | string>(null) const status = React.useRef<null | string>(null)
const hasCommencedDownload = React.useRef(false); const hasCommencedDownload = React.useRef(false);
const dispatch = useDispatch(); const dispatch = useDispatch();
const reDownload = React.useRef<boolean>(false) const reDownload = React.useRef<boolean>(false)
const isFetchingProperties = React.useRef<boolean>(false) const isFetchingProperties = React.useRef<boolean>(false)
const download = React.useMemo(() => { const download = React.useMemo(() => {
if (!downloads || !fileInfo?.identifier) return {}; if (!downloads || !fileInfo?.identifier) return {};
const findDownload = downloads[fileInfo?.identifier]; const findDownload = downloads[fileInfo?.identifier];
if (!findDownload) return {}; if (!findDownload) return {};
return findDownload; return findDownload;
}, [downloads, fileInfo]); }, [downloads, fileInfo]);
const resourceStatus = React.useMemo(() => { const resourceStatus = React.useMemo(() => {
return download?.status || {}; return download?.status || {};
}, [download]); }, [download]);
const retryDownload = React.useRef(0); const retryDownload = React.useRef(0);
const handlePlay = async () => { const handlePlay = async () => {
if (disable) return; if (disable) return;
hasCommencedDownload.current = true; hasCommencedDownload.current = true;
setStartedDownload(true) setStartedDownload(true)
if ( if (
resourceStatus?.status === "READY" resourceStatus?.status === "READY"
) { ) {
if (downloadLoader) return; if (downloadLoader) return;
setDownloadLoader(true); setDownloadLoader(true);
let filename = download?.properties?.filename let filename = download?.properties?.filename
let mimeType = download?.properties?.type let mimeType = download?.properties?.type
try { try {
const { name, service, identifier } = fileInfo; const { name, service, identifier } = fileInfo;
const res = await qortalRequest({ const res = await qortalRequest({
action: "GET_QDN_RESOURCE_PROPERTIES", action: "GET_QDN_RESOURCE_PROPERTIES",
name: name, name: name,
service: service, service: service,
identifier: identifier, identifier: identifier,
}); });
filename = res?.filename || filename; filename = res?.filename || filename;
mimeType = res?.mimeType || mimeType; mimeType = res?.mimeType || mimeType;
} catch (error) { } catch (error) {
} }
try { try {
const { name, service, identifier } = fileInfo; const { name, service, identifier } = fileInfo;
const url = `/arbitrary/${service}/${name}/${identifier}`; const url = `/arbitrary/${service}/${name}/${identifier}`;
fetch(url) fetch(url)
.then(response => response.blob()) .then(response => response.blob())
.then(async blob => { .then(async blob => {
await qortalRequest({ await qortalRequest({
action: "SAVE_FILE", action: "SAVE_FILE",
blob, blob,
filename: filename, filename: filename,
mimeType, mimeType,
}); });
}) })
.catch(error => { .catch(error => {
console.error("Error fetching the video:", error); console.error("Error fetching the video:", error);
}); });
} catch (error: any) { } catch (error: any) {
let notificationObj: any = null; let notificationObj: any = null;
if (typeof error === "string") { if (typeof error === "string") {
notificationObj = { notificationObj = {
msg: error || "Failed to send message", msg: error || "Failed to send message",
alertType: "error", alertType: "error",
}; };
} else if (typeof error?.error === "string") { } else if (typeof error?.error === "string") {
notificationObj = { notificationObj = {
msg: error?.error || "Failed to send message", msg: error?.error || "Failed to send message",
alertType: "error", alertType: "error",
}; };
} else { } else {
notificationObj = { notificationObj = {
msg: error?.message || "Failed to send message", msg: error?.message || "Failed to send message",
alertType: "error", alertType: "error",
}; };
} }
if (!notificationObj) return; if (!notificationObj) return;
dispatch(setNotification(notificationObj)); dispatch(setNotification(notificationObj));
} finally { } finally {
setDownloadLoader(false); setDownloadLoader(false);
} }
return; return;
} }
const { name, service, identifier } = fileInfo; const { name, service, identifier } = fileInfo;
setIsLoading(true); setIsLoading(true);
downloadVideo({ downloadVideo({
name, name,
service, service,
identifier, identifier,
properties: { properties: {
...fileInfo, ...fileInfo,
jsonId jsonId
}, },
}); });
}; };
const refetch = React.useCallback(async () => { const refetch = React.useCallback(async () => {
if (!fileInfo) return if (!fileInfo) return
try { try {
const { name, service, identifier } = fileInfo; const { name, service, identifier } = fileInfo;
isFetchingProperties.current = true isFetchingProperties.current = true
await qortalRequest({ await qortalRequest({
action: 'GET_QDN_RESOURCE_PROPERTIES', action: 'GET_QDN_RESOURCE_PROPERTIES',
name, name,
service, service,
identifier identifier
}) })
} catch (error) { } catch (error) {
} finally { } finally {
isFetchingProperties.current = false isFetchingProperties.current = false
} }
}, [fileInfo]) }, [fileInfo])
const refetchInInterval = ()=> { const refetchInInterval = ()=> {
try { try {
const interval = setInterval(()=> { const interval = setInterval(()=> {
if(status?.current === 'DOWNLOADED'){ if(status?.current === 'DOWNLOADED'){
refetch() refetch()
} }
if(status?.current === 'READY'){ if(status?.current === 'READY'){
clearInterval(interval); clearInterval(interval);
} }
}, 7500) }, 7500)
} catch (error) { } catch (error) {
} }
} }
React.useEffect(() => { React.useEffect(() => {
if(resourceStatus?.status){ if(resourceStatus?.status){
status.current = resourceStatus?.status status.current = resourceStatus?.status
} }
if ( if (
resourceStatus?.status === 'DOWNLOADED' && resourceStatus?.status === 'DOWNLOADED' &&
reDownload?.current === false reDownload?.current === false
) { ) {
refetchInInterval() refetchInInterval()
reDownload.current = true reDownload.current = true
} }
}, [resourceStatus]) }, [resourceStatus])
React.useEffect(() => { React.useEffect(() => {
if ( if (
resourceStatus?.status === "READY" && resourceStatus?.status === "READY" &&
download?.url && download?.url &&
download?.properties?.filename && download?.properties?.filename &&
hasCommencedDownload.current hasCommencedDownload.current
) { ) {
setIsLoading(false); setIsLoading(false);
dispatch( dispatch(
setNotification({ setNotification({
msg: "Download completed. Click to save file", msg: "Download completed. Click to save file",
alertType: "info", alertType: "info",
}) })
); );
} }
}, [resourceStatus, download]); }, [resourceStatus, download]);
return ( return (
<Box <Box
onClick={handlePlay} onClick={handlePlay}
sx={{ sx={{
width: "100%", width: "100%",
overflow: "hidden", overflow: "hidden",
position: "relative", position: "relative",
cursor: "pointer", cursor: "pointer",
...(customStyles || {}), ...(customStyles || {}),
}} }}
> >
{children && ( {children && (
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
position: "relative", position: "relative",
gap: "7px", gap: "7px",
}} }}
> >
{children}{" "} {children}{" "}
{((resourceStatus.status && resourceStatus?.status !== "READY") || {((resourceStatus.status && resourceStatus?.status !== "READY") ||
isLoading) && startedDownload ? ( isLoading) && startedDownload ? (
<> <>
<CircularProgress color="secondary" size={14} /> <CircularProgress color="secondary" size={14} />
<Typography variant="body2">{`${Math.round( <Typography variant="body2">{`${Math.round(
resourceStatus?.percentLoaded || 0 resourceStatus?.percentLoaded || 0
).toFixed(0)}% loaded`}</Typography> ).toFixed(0)}% loaded`}</Typography>
</> </>
) : resourceStatus?.status === "READY" ? ( ) : resourceStatus?.status === "READY" ? (
<> <>
<Typography <Typography
sx={{ sx={{
fontSize: "14px", fontSize: "14px",
}} }}
> >
Ready to save: click here Ready to save: click here
</Typography> </Typography>
{downloadLoader && ( {downloadLoader && (
<CircularProgress color="secondary" size={14} /> <CircularProgress color="secondary" size={14} />
)} )}
</> </>
) : null} ) : null}
</Box> </Box>
)} )}
{!children && ( {!children && (
<Widget> <Widget>
<Box sx={{ display: "flex", alignItems: "center" }}> <Box sx={{ display: "flex", alignItems: "center" }}>
<CoverImage> <CoverImage>
<AttachFileIcon <AttachFileIcon
sx={{ sx={{
width: "90%", width: "90%",
height: "auto", height: "auto",
}} }}
/> />
</CoverImage> </CoverImage>
<Box sx={{ ml: 1.5, minWidth: 0 }}> <Box sx={{ ml: 1.5, minWidth: 0 }}>
<Typography <Typography
variant="caption" variant="caption"
color="text.secondary" color="text.secondary"
fontWeight={500} fontWeight={500}
> >
{author} {author}
</Typography> </Typography>
<Typography <Typography
noWrap noWrap
sx={{ sx={{
fontSize: "16px", fontSize: "16px",
}} }}
> >
<b>{title}</b> <b>{title}</b>
</Typography> </Typography>
<Typography <Typography
noWrap noWrap
letterSpacing={-0.25} letterSpacing={-0.25}
sx={{ sx={{
fontSize: "14px", fontSize: "14px",
}} }}
> >
{description} {description}
</Typography> </Typography>
{mimeType && ( {mimeType && (
<Typography <Typography
noWrap noWrap
letterSpacing={-0.25} letterSpacing={-0.25}
sx={{ sx={{
fontSize: "12px", fontSize: "12px",
}} }}
> >
{mimeType} {mimeType}
</Typography> </Typography>
)} )}
</Box> </Box>
</Box> </Box>
{((resourceStatus.status && resourceStatus?.status !== "READY") || {((resourceStatus.status && resourceStatus?.status !== "READY") ||
isLoading) && startedDownload && ( isLoading) && startedDownload && (
<Box <Box
position="absolute" position="absolute"
top={0} top={0}
left={0} left={0}
right={0} right={0}
bottom={0} bottom={0}
display="flex" display="flex"
justifyContent="center" justifyContent="center"
alignItems="center" alignItems="center"
zIndex={4999} zIndex={4999}
bgcolor="rgba(0, 0, 0, 0.6)" bgcolor="rgba(0, 0, 0, 0.6)"
sx={{ sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: "10px", gap: "10px",
padding: "8px", padding: "8px",
borderRadius: "10px", borderRadius: "10px",
}} }}
> >
<CircularProgress color="secondary" /> <CircularProgress color="secondary" />
{resourceStatus && ( {resourceStatus && (
<Typography <Typography
variant="subtitle2" variant="subtitle2"
component="div" component="div"
sx={{ sx={{
color: "white", color: "white",
fontSize: "14px", fontSize: "14px",
}} }}
> >
{resourceStatus?.status === "REFETCHING" ? ( {resourceStatus?.status === "REFETCHING" ? (
<> <>
<> <>
{( {(
(resourceStatus?.localChunkCount / (resourceStatus?.localChunkCount /
resourceStatus?.totalChunkCount) * resourceStatus?.totalChunkCount) *
100 100
)?.toFixed(0)} )?.toFixed(0)}
% %
</> </>
<> Refetching in 2 minutes</> <> Refetching in 2 minutes</>
</> </>
) : resourceStatus?.status === "DOWNLOADED" ? ( ) : resourceStatus?.status === "DOWNLOADED" ? (
<>Download Completed: building file...</> <>Download Completed: building file...</>
) : resourceStatus?.status !== "READY" ? ( ) : resourceStatus?.status !== "READY" ? (
<> <>
{( {(
(resourceStatus?.localChunkCount / (resourceStatus?.localChunkCount /
resourceStatus?.totalChunkCount) * resourceStatus?.totalChunkCount) *
100 100
)?.toFixed(0)} )?.toFixed(0)}
% %
</> </>
) : ( ) : (
<>Download Completed: fetching file...</> <>Download Completed: fetching file...</>
)} )}
</Typography> </Typography>
)} )}
</Box> </Box>
)} )}
{resourceStatus?.status === "READY" && {resourceStatus?.status === "READY" &&
download?.url && download?.url &&
download?.properties?.filename && ( download?.properties?.filename && (
<Box <Box
position="absolute" position="absolute"
top={0} top={0}
left={0} left={0}
right={0} right={0}
bottom={0} bottom={0}
display="flex" display="flex"
justifyContent="center" justifyContent="center"
alignItems="center" alignItems="center"
zIndex={4999} zIndex={4999}
bgcolor="rgba(0, 0, 0, 0.6)" bgcolor="rgba(0, 0, 0, 0.6)"
sx={{ sx={{
display: "flex", display: "flex",
flexDirection: "row", flexDirection: "row",
gap: "10px", gap: "10px",
padding: "8px", padding: "8px",
borderRadius: "10px", borderRadius: "10px",
}} }}
> >
<Typography <Typography
variant="subtitle2" variant="subtitle2"
component="div" component="div"
sx={{ sx={{
color: "white", color: "white",
fontSize: "14px", fontSize: "14px",
}} }}
> >
Ready to save: click here Ready to save: click here
</Typography> </Typography>
{downloadLoader && ( {downloadLoader && (
<CircularProgress color="secondary" size={14} /> <CircularProgress color="secondary" size={14} />
)} )}
</Box> </Box>
)} )}
</Widget> </Widget>
)} )}
</Box> </Box>
); );
} }

View File

@ -11,6 +11,7 @@ export const AddCoverImageButton = styled(Button)(({ theme }) => ({
fontWeight: 400, fontWeight: 400,
letterSpacing: "0.2px", letterSpacing: "0.2px",
color: theme.palette.text.primary, color: theme.palette.text.primary,
width: "170px",
backgroundColor: "#44c4ff", backgroundColor: "#44c4ff",
"&:hover": { backgroundColor: "#01a9e9" }, "&:hover": { backgroundColor: "#01a9e9" },
gap: "5px", gap: "5px",

View File

@ -98,6 +98,7 @@ export const ImageUploader: React.FC<ImageUploaderProps> = ({
{...getRootProps()} {...getRootProps()}
sx={{ sx={{
display: "flex", display: "flex",
width: "170px",
}} }}
> >
<input {...getInputProps()} /> <input {...getInputProps()} />

View File

@ -0,0 +1,61 @@
import AttachFileIcon from "@mui/icons-material/AttachFile";
import React, { CSSProperties } from "react";
interface IssueIconProps {
iconSrc: string;
showBackupIcon?: boolean;
style?: CSSProperties;
}
export const IssueIcon = ({
iconSrc,
showBackupIcon = true,
style,
}: IssueIconProps) => {
const displayFileIcon = !iconSrc && showBackupIcon;
return (
<>
{iconSrc && (
<img
src={iconSrc}
width="50px"
height="50px"
style={{
borderRadius: "5px",
...style,
}}
/>
)}
{displayFileIcon && (
<AttachFileIcon
sx={{
...style,
width: "40px",
height: "40px",
}}
/>
)}
</>
);
};
interface IssueIconsProps {
iconSources: string[];
showBackupIcon?: boolean;
style?: CSSProperties;
}
export const IssueIcons = ({
iconSources,
showBackupIcon = true,
style,
}: IssueIconsProps) => {
return iconSources.map((icon, index) => (
<IssueIcon
key={icon + index}
iconSrc={icon}
style={{ ...style }}
showBackupIcon={showBackupIcon}
/>
));
};

View File

@ -1,48 +1,48 @@
import React, { useState, useEffect, useRef } from 'react' import React, { useState, useEffect, useRef } from 'react'
import { useInView } from 'react-intersection-observer' import { useInView } from 'react-intersection-observer'
import CircularProgress from '@mui/material/CircularProgress' import CircularProgress from '@mui/material/CircularProgress'
interface Props { interface Props {
onLoadMore: () => Promise<void> onLoadMore: () => Promise<void>
isLoading?: boolean isLoading?: boolean
} }
const LazyLoad: React.FC<Props> = ({ onLoadMore, isLoading }) => { const LazyLoad: React.FC<Props> = ({ onLoadMore, isLoading }) => {
const [isFetching, setIsFetching] = useState<boolean>(false) const [isFetching, setIsFetching] = useState<boolean>(false)
const firstLoad = useRef(false) const firstLoad = useRef(false)
const [ref, inView] = useInView({ const [ref, inView] = useInView({
threshold: 0.7 threshold: 0.7
}) })
useEffect(() => { useEffect(() => {
if (inView) { if (inView) {
setIsFetching(true) setIsFetching(true)
onLoadMore().finally(() => { onLoadMore().finally(() => {
setIsFetching(false) setIsFetching(false)
firstLoad.current = true firstLoad.current = true
}) })
} }
}, [inView]) }, [inView])
return ( return (
<div <div
ref={ref} ref={ref}
style={{ style={{
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
minHeight: '25px' minHeight: '25px'
}} }}
> >
<div <div
style={{ style={{
visibility: (isFetching || isLoading) ? 'visible' : 'hidden' visibility: (isFetching || isLoading) ? 'visible' : 'hidden'
}} }}
> >
<CircularProgress /> <CircularProgress />
</div> </div>
</div> </div>
) )
} }
export default LazyLoad export default LazyLoad

View File

@ -1,86 +1,86 @@
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { toast, ToastContainer, Zoom, Slide } from 'react-toastify' import { toast, ToastContainer, Zoom, Slide } from 'react-toastify'
import { removeNotification } from '../../../state/features/notificationsSlice' import { removeNotification } from '../../../state/features/notificationsSlice'
import 'react-toastify/dist/ReactToastify.css' import 'react-toastify/dist/ReactToastify.css'
import { RootState } from '../../../state/store' import { RootState } from '../../../state/store'
const Notification = () => { const Notification = () => {
const dispatch = useDispatch() const dispatch = useDispatch()
const { alertTypes } = useSelector((state: RootState) => state.notifications) const { alertTypes } = useSelector((state: RootState) => state.notifications)
if (alertTypes.alertError) { if (alertTypes.alertError) {
toast.error(`${alertTypes?.alertError}`, { toast.error(`${alertTypes?.alertError}`, {
position: 'bottom-right', position: 'bottom-right',
autoClose: 4000, autoClose: 4000,
hideProgressBar: false, hideProgressBar: false,
closeOnClick: true, closeOnClick: true,
pauseOnHover: true, pauseOnHover: true,
draggable: true, draggable: true,
progress: undefined, progress: undefined,
icon: false icon: false
}) })
dispatch(removeNotification()) dispatch(removeNotification())
} }
if (alertTypes.alertSuccess) { if (alertTypes.alertSuccess) {
toast.success(`✔️ ${alertTypes?.alertSuccess}`, { toast.success(`✔️ ${alertTypes?.alertSuccess}`, {
position: 'bottom-right', position: 'bottom-right',
autoClose: 4000, autoClose: 4000,
hideProgressBar: false, hideProgressBar: false,
closeOnClick: true, closeOnClick: true,
pauseOnHover: true, pauseOnHover: true,
draggable: true, draggable: true,
progress: undefined, progress: undefined,
icon: false icon: false
}) })
dispatch(removeNotification()) dispatch(removeNotification())
} }
if (alertTypes.alertInfo) { if (alertTypes.alertInfo) {
toast.info(`${alertTypes?.alertInfo}`, { toast.info(`${alertTypes?.alertInfo}`, {
position: 'top-right', position: 'top-right',
autoClose: 1300, autoClose: 1300,
hideProgressBar: false, hideProgressBar: false,
closeOnClick: true, closeOnClick: true,
pauseOnHover: true, pauseOnHover: true,
draggable: true, draggable: true,
progress: undefined, progress: undefined,
theme: 'light' theme: 'light'
}) })
dispatch(removeNotification()) dispatch(removeNotification())
} }
if (alertTypes.alertInfo) { if (alertTypes.alertInfo) {
return ( return (
<ToastContainer <ToastContainer
position="top-right" position="top-right"
autoClose={2000} autoClose={2000}
hideProgressBar={false} hideProgressBar={false}
newestOnTop={false} newestOnTop={false}
closeOnClick closeOnClick
rtl={false} rtl={false}
pauseOnFocusLoss pauseOnFocusLoss
draggable draggable
pauseOnHover pauseOnHover
theme="light" theme="light"
toastStyle={{ fontSize: '16px' }} toastStyle={{ fontSize: '16px' }}
transition={Slide} transition={Slide}
/> />
) )
} }
return ( return (
<ToastContainer <ToastContainer
transition={Zoom} transition={Zoom}
position="bottom-right" position="bottom-right"
autoClose={false} autoClose={false}
hideProgressBar={false} hideProgressBar={false}
newestOnTop={false} newestOnTop={false}
closeOnClick closeOnClick
rtl={false} rtl={false}
draggable draggable
pauseOnHover pauseOnHover
/> />
) )
} }
export default Notification export default Notification

View File

@ -1,43 +1,43 @@
import React from 'react'; import React from 'react';
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import Box from '@mui/system/Box'; import Box from '@mui/system/Box';
import { useTheme } from '@mui/material' import { useTheme } from '@mui/material'
interface PageLoaderProps { interface PageLoaderProps {
size?: number size?: number
thickness?: number thickness?: number
} }
const PageLoader: React.FC<PageLoaderProps> = ({ const PageLoader: React.FC<PageLoaderProps> = ({
size = 40, size = 40,
thickness = 5 thickness = 5
}) => { }) => {
const theme = useTheme() const theme = useTheme()
return ( return (
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
height: '100vh', height: '100vh',
width: '100%', width: '100%',
position: 'fixed', position: 'fixed',
top: 0, top: 0,
left: 0, left: 0,
backgroundColor: 'rgba(255, 255, 255, 0.25)', backgroundColor: 'rgba(255, 255, 255, 0.25)',
zIndex: 1000 zIndex: 1000
}} }}
> >
<CircularProgress <CircularProgress
size={size} size={size}
thickness={thickness} thickness={thickness}
sx={{ sx={{
color: theme.palette.secondary.main color: theme.palette.secondary.main
}} }}
/> />
</Box> </Box>
) )
} }
export default PageLoader; export default PageLoader;

View File

@ -1,25 +1,25 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
interface PortalProps { interface PortalProps {
children: React.ReactNode children: React.ReactNode
} }
const Portal: React.FC<PortalProps> = ({ children }) => { const Portal: React.FC<PortalProps> = ({ children }) => {
const [mounted, setMounted] = useState(false) const [mounted, setMounted] = useState(false)
useEffect(() => { useEffect(() => {
setMounted(true) setMounted(true)
return () => setMounted(false) return () => setMounted(false)
}, []) }, [])
return mounted return mounted
? createPortal( ? createPortal(
children, children,
document.querySelector('#modal-root') as HTMLElement document.querySelector('#modal-root') as HTMLElement
) )
: null : null
} }
export default Portal export default Portal

View File

@ -1,40 +1,40 @@
import { useMemo } from "react"; import { useMemo } from "react";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
import "react-quill/dist/quill.snow.css"; import "react-quill/dist/quill.snow.css";
import "react-quill/dist/quill.core.css"; import "react-quill/dist/quill.core.css";
import "react-quill/dist/quill.bubble.css"; import "react-quill/dist/quill.bubble.css";
import { convertQortalLinks } from "./utils"; import { convertQortalLinks } from "./utils";
import { Box, styled } from "@mui/material"; import { Box, styled } from "@mui/material";
const CrowdfundInlineContent = styled(Box)(({ theme }) => ({ const CrowdfundInlineContent = styled(Box)(({ theme }) => ({
display: "flex", display: "flex",
fontFamily: "Mulish", fontFamily: "Mulish",
fontSize: "19px", fontSize: "19px",
fontWeight: 400, fontWeight: 400,
letterSpacing: 0, letterSpacing: 0,
color: theme.palette.text.primary, color: theme.palette.text.primary,
width: '100%' width: '100%'
})); }));
export const DisplayHtml = ({ html }) => { export const DisplayHtml = ({ html }) => {
const cleanContent = useMemo(() => { const cleanContent = useMemo(() => {
if (!html) return null; if (!html) return null;
const sanitize: string = DOMPurify.sanitize(html, { const sanitize: string = DOMPurify.sanitize(html, {
USE_PROFILES: { html: true }, USE_PROFILES: { html: true },
}); });
const anchorQortal = convertQortalLinks(sanitize); const anchorQortal = convertQortalLinks(sanitize);
return anchorQortal; return anchorQortal;
}, [html]); }, [html]);
if (!cleanContent) return null; if (!cleanContent) return null;
return ( return (
<CrowdfundInlineContent> <CrowdfundInlineContent>
<div <div
className="ql-editor" className="ql-editor"
dangerouslySetInnerHTML={{ __html: cleanContent }} dangerouslySetInnerHTML={{ __html: cleanContent }}
/> />
</CrowdfundInlineContent> </CrowdfundInlineContent>
); );
}; };

View File

@ -1,26 +1,26 @@
export function convertQortalLinks(inputHtml) { export function convertQortalLinks(inputHtml) {
// Regular expression to match 'qortal://...' URLs. // Regular expression to match 'qortal://...' URLs.
// This will stop at the first whitespace, comma, or HTML tag // This will stop at the first whitespace, comma, or HTML tag
var regex = /(qortal:\/\/[^\s,<]+)/g; var regex = /(qortal:\/\/[^\s,<]+)/g;
// Replace matches in inputHtml with formatted anchor tag // Replace matches in inputHtml with formatted anchor tag
var outputHtml = inputHtml.replace(regex, function (match) { var outputHtml = inputHtml.replace(regex, function (match) {
return `<a href="${match}" className="qortal-link">${match}</a>`; return `<a href="${match}" className="qortal-link">${match}</a>`;
}); });
return outputHtml; return outputHtml;
} }
export function extractTextFromHTML(htmlString: any, length = 150) { export function extractTextFromHTML(htmlString: any, length = 150) {
// Create a temporary DOM element // Create a temporary DOM element
const tempDiv = document.createElement("div"); const tempDiv = document.createElement("div");
// Replace br tags and block-level tags with a space before setting the HTML content // 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, ' '); const htmlWithSpaces = htmlString.replace(/<\/?(br|p|div|h[1-6]|ul|ol|li|blockquote)[^>]*>/gi, ' ');
tempDiv.innerHTML = htmlWithSpaces; tempDiv.innerHTML = htmlWithSpaces;
// Extract the text content // Extract the text content
let text = tempDiv.textContent || tempDiv.innerText || ""; let text = tempDiv.textContent || tempDiv.innerText || "";
// Replace multiple spaces with a single space and trim // Replace multiple spaces with a single space and trim
text = text.replace(/\s+/g, ' ').trim(); text = text.replace(/\s+/g, ' ').trim();
// Slice the text to the desired length // Slice the text to the desired length
return text.slice(0, length); return text.slice(0, length);
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -22,7 +22,7 @@ import { useNavigate } from "react-router-dom";
import SearchIcon from "@mui/icons-material/Search"; import SearchIcon from "@mui/icons-material/Search";
import { DownloadTaskManager } from "../../common/DownloadTaskManager"; import { DownloadTaskManager } from "../../common/DownloadTaskManager";
import QSupportLogo from "../../../assets/img/Q-SupportIcon.webp"; import QSupportIcon from "../../../assets/img/Q-SupportIcon(AlphaX).webp";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { import {
addFilteredFiles, addFilteredFiles,
@ -32,6 +32,7 @@ import {
import { RootState } from "../../../state/store"; import { RootState } from "../../../state/store";
import { useWindowSize } from "../../../hooks/useWindowSize"; import { useWindowSize } from "../../../hooks/useWindowSize";
import { PublishIssue } from "../../PublishIssue/PublishIssue.tsx"; import { PublishIssue } from "../../PublishIssue/PublishIssue.tsx";
import { FeeHistoryModal } from "../../../constants/PublishFees/FeePricePublish/FeeHistoryModal.tsx";
interface Props { interface Props {
isAuthenticated: boolean; isAuthenticated: boolean;
@ -114,7 +115,7 @@ const NavBar: React.FC<Props> = ({
}} }}
> >
<img <img
src={QSupportLogo} src={QSupportIcon}
style={{ style={{
width: "auto", width: "auto",
height: "100px", height: "100px",
@ -238,6 +239,7 @@ const NavBar: React.FC<Props> = ({
</Popover> </Popover>
<DownloadTaskManager /> <DownloadTaskManager />
<FeeHistoryModal />
{theme.palette.mode === "dark" ? ( {theme.palette.mode === "dark" ? (
<LightModeIcon <LightModeIcon
onClickFunc={() => setTheme("light")} onClickFunc={() => setTheme("light")}

View File

@ -1,48 +0,0 @@
import audioIcon from "../../assets/icons/audio.webp";
import bookIcon from "../../assets/icons/book.webp";
import documentIcon from "../../assets/icons/document.webp";
import gamingIcon from "../../assets/icons/gaming.webp";
import imageIcon from "../../assets/icons/image.webp";
import softwareIcon from "../../assets/icons/software.webp";
import unknownIcon from "../../assets/icons/unknown.webp";
import videoIcon from "../../assets/icons/video.webp";
import {
Categories,
Category,
CategoryData,
} from "../../components/common/CategoryList/CategoryList.tsx";
import {
getAllCategoriesWithIcons,
sortCategory,
} from "./CategoryFunctions.ts";
import { QappCategories, SupportState } from "./2ndCategories.ts";
export const firstCategories: Category[] = [
{ id: 1, name: "Core" },
{ id: 2, name: "UI" },
{ id: 3, name: "Q-Apps" },
{ id: 4, name: "Website" },
{ id: 5, name: "Marketing" },
{ id: 99, name: "Other" },
];
export const secondCategories: Categories = {
1: SupportState,
2: SupportState,
3: QappCategories,
4: SupportState,
5: SupportState,
99: SupportState,
};
export let thirdCategories: Categories = {};
QappCategories.map(
supportStateCategory =>
(thirdCategories[supportStateCategory.id] = SupportState)
);
export const allCategoryData: CategoryData = {
category: firstCategories,
subCategories: [secondCategories, thirdCategories],
};
export const iconCategories = getAllCategoriesWithIcons();

View File

@ -1,23 +0,0 @@
import OpenIcon from "../../assets/icons/OpenIcon.png";
import ClosedIcon from "../../assets/icons/ClosedIcon.png";
import InProgressIcon from "../../assets/icons/InProgressIcon.png";
import CompleteIcon from "../../assets/icons/CompleteIcon.png";
export const SupportState = [
{ id: 101, name: "Open", icon: OpenIcon },
{ id: 102, name: "Closed", icon: ClosedIcon },
{ id: 103, name: "In Progress", icon: InProgressIcon },
{ id: 104, name: "Complete", icon: CompleteIcon },
];
export const QappCategories = [
{ id: 301, name: "Q-Blog" },
{ id: 302, name: "Q-Mail" },
{ id: 303, name: "Q-Shop" },
{ id: 304, name: "Q-Fund" },
{ id: 305, name: "Ear-Bump" },
{ id: 306, name: "Q-Tube" },
{ id: 307, name: "Q-Share" },
{ id: 308, name: "Q-Support" },
{ id: 399, name: "Other" },
];

View File

@ -0,0 +1,67 @@
import {
Categories,
Category,
CategoryData,
} from "../../components/common/CategoryList/CategoryList.tsx";
import { getAllCategoriesWithIcons } from "./CategoryFunctions.ts";
import CoreIcon from "../../assets/icons/Qortal-Core-Icon.webp";
import UIicon from "../../assets/icons/Qortal-UI-Icon.webp";
import QappIcon from "../../assets/icons/Q-App-Icon.webp";
import UnknownIcon from "../../assets/icons/unknown.webp";
import BugReportIcon from "../../assets/icons/Bug-Report-Icon.webp";
import FeatureRequestIcon from "../../assets/icons/Feature-Request-Icon.webp";
import TechSupportIcon from "../../assets/icons/Tech-Support-Icon.webp";
import OpenIcon from "../../assets/icons/Open-Icon.webp";
import ClosedIcon from "../../assets/icons/Closed-Icon.webp";
import InProgressIcon from "../../assets/icons/In-Progress-Icon.webp";
import CompleteIcon from "../../assets/icons/Complete-Icon.webp";
const issueLocationLabel = "Issue Location";
export const issueLocation: Category[] = [
{ id: 1, name: "Core", icon: CoreIcon, label: issueLocationLabel },
{ id: 2, name: "UI", icon: UIicon, label: issueLocationLabel },
{ id: 3, name: "Q-Apps/Websites", icon: QappIcon, label: issueLocationLabel },
{ id: 99, name: "Other", icon: UnknownIcon, label: issueLocationLabel },
];
const issueTypeLabel = "Issue Type";
export const issueType = [
{ id: 11, name: "Bug Report", icon: BugReportIcon, label: issueTypeLabel },
{
id: 12,
name: "Feature Request",
icon: FeatureRequestIcon,
label: issueTypeLabel,
},
{
id: 13,
name: "Tech Support",
icon: TechSupportIcon,
label: issueTypeLabel,
},
{ id: 19, name: "Other", icon: UnknownIcon, label: issueTypeLabel },
];
export const secondCategories: Categories = {};
issueLocation.map(c => (secondCategories[c.id] = issueType));
const issueLabel = "Issue State";
export const IssueState = [
{ id: 101, name: "Open", icon: OpenIcon, label: issueLabel },
{ id: 102, name: "Closed", icon: ClosedIcon, label: issueLabel },
{ id: 103, name: "In Progress", icon: InProgressIcon, label: issueLabel },
{ id: 104, name: "Complete", icon: CompleteIcon, label: issueLabel },
];
export const thirdCategories: Categories = {};
issueType.map(issueType => (thirdCategories[issueType.id] = IssueState));
export const allCategoryData: CategoryData = {
category: issueLocation,
subCategories: [secondCategories, thirdCategories],
};
export const iconCategories = getAllCategoriesWithIcons();

View File

@ -2,7 +2,7 @@ import {
Category, Category,
getCategoriesFromObject, getCategoriesFromObject,
} from "../../components/common/CategoryList/CategoryList.tsx"; } from "../../components/common/CategoryList/CategoryList.tsx";
import { allCategoryData, iconCategories } from "./1stCategories.ts"; import { allCategoryData, iconCategories } from "./Categories.ts";
export const sortCategory = (a: Category, b: Category) => { export const sortCategory = (a: Category, b: Category) => {
if (a.name === "Other") return 1; if (a.name === "Other") return 1;
@ -81,11 +81,9 @@ export const getAllCategoriesWithIcons = () => {
export const getIconsFromObject = (fileObj: any) => { export const getIconsFromObject = (fileObj: any) => {
const categories = getCategoriesFromObject(fileObj); const categories = getCategoriesFromObject(fileObj);
const icons = categories const icons = categories.map(categoryID => {
.map(categoryID => { return iconCategories.find(category => category.id === +categoryID)?.icon;
return iconCategories.find(category => category.id === +categoryID)?.icon; });
})
.reverse();
return icons.find(icon => icon !== undefined); return icons.filter(icon => icon !== undefined);
}; };

View File

@ -1,4 +1,4 @@
const useTestIdentifiers = true; export const useTestIdentifiers = false;
export const QSUPPORT_FILE_BASE = useTestIdentifiers export const QSUPPORT_FILE_BASE = useTestIdentifiers
? "MYTEST_support_issue_" ? "MYTEST_support_issue_"

View File

@ -3,3 +3,10 @@ export const titleFormatter = /[^a-zA-Z0-9\s-_!?()&'",.;:|—~@#$%^*+=<>]/g;
export const titleFormatterOnSave = /[^a-zA-Z0-9\s-_!()&',.;—~@#$%^+=]/g; export const titleFormatterOnSave = /[^a-zA-Z0-9\s-_!()&',.;—~@#$%^+=]/g;
export const log = false; export const log = false;
export const fontSizeSmall = "80%";
export const fontSizeMedium = "100%";
export const fontSizeLarge = "120%";
export const fontSizeExLarge = "150%";
export const maxCommentLength = 10_000;
export const maxNotificationLength = 2000;

View File

@ -0,0 +1,29 @@
import { Box } from "@mui/material";
import React from "react";
import { useTestIdentifiers } from "../Identifiers.ts";
export const appName = "Q-Support";
export const feeDestinationName = "Q-Support";
export const feeAmountBase = useTestIdentifiers ? 0.000001 : 0.25;
export const FEE_BASE = useTestIdentifiers
? "MYTEST_support_fees"
: "q_support_fees";
export const maxFeePublishTimeDiff = 10; // time in minutes before/after publish when fee is considered valid
export type FeeType = "default" | "comment" | "like" | "dislike" | "superlike";
export const feeDisclaimerString = `When Publishing (but not editing) Issues ${feeAmountBase} \n
QORT is requested to fund continued development of Q-Support.`;
export const feeDisclaimer = (
<Box
sx={{
fontSize: "28px",
color: "#f44336",
fontWeight: 600,
}}
>
{feeDisclaimerString}
</Box>
);

View File

@ -0,0 +1,60 @@
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from "@mui/material";
import React from "react";
import { SxProps } from "@mui/material/styles";
export interface DataTableProps {
columnNames: string[];
data: string[][];
sx?: SxProps;
}
export const DataTable = ({ columnNames, data, sx }: DataTableProps) => {
return (
<TableContainer sx={{ ...sx }}>
<Table align="center" stickyHeader>
<TableHead>
<TableRow>
{columnNames.map((columnName, index) => (
<TableCell
sx={{
fontSize: "30px",
textAlign: "center",
fontWeight: "bold",
}}
key={columnName + index}
>
{columnName}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{data.map((tableRow, index) => {
return (
<TableRow key={tableRow.toString() + index}>
{tableRow.map((tableCell, index) => (
<TableCell
sx={{
fontSize: index === 0 ? "30px" : "25px",
fontWeight: index === 0 ? "bold" : "normal",
textAlign: "center",
}}
key={tableCell + index}
>
{tableCell}
</TableCell>
))}
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
);
};

View File

@ -0,0 +1,48 @@
import { Button, Modal, useTheme } from "@mui/material";
import { ThemeButton } from "../../../pages/Home/Home-styles.tsx";
import { appName } from "../FeeData.tsx";
import { ModalBody } from "./FeePricePublish-styles.tsx";
import { useEffect, useState } from "react";
import { userHasName } from "../VerifyPayment-Functions.ts";
import { FeeHistoryTable } from "./FeeHistoryTable.tsx";
export const FeeHistoryModal = () => {
const [open, setOpen] = useState<boolean>(false);
const [userOwnsApp, setUserOwnsApp] = useState<boolean>(false);
const theme = useTheme();
useEffect(() => {
userHasName(appName).then(userHasName => setUserOwnsApp(userHasName));
}, []);
const buttonSX = {
fontSize: "20px",
color: theme.palette.secondary.main,
fontWeight: "bold",
};
if (theme.palette.mode === "light")
buttonSX["&:hover"] = { backgroundColor: theme.palette.primary.dark };
return (
<>
<ThemeButton
sx={{ height: "40px", marginRight: "5px" }}
onClick={() => setOpen(true)}
>
{appName} Fees
</ThemeButton>
<Modal
open={open}
aria-labelledby="modal-title"
aria-describedby="modal-description"
onClose={() => setOpen(false)}
>
<ModalBody sx={{ width: "75vw", maxWidth: "75vw" }}>
<FeeHistoryTable />
<Button sx={buttonSX} onClick={() => setOpen(false)}>
Close
</Button>
</ModalBody>
</Modal>
</>
);
};

View File

@ -0,0 +1,49 @@
import { DataTable } from "./DataTable.tsx";
import { FeePrice, fetchFees } from "./FeePricePublish.ts";
import React, { useEffect, useState } from "react";
export interface FeeHistoryProps {
showFeeType?: boolean;
showCoinType?: boolean;
filterData?: () => string[][];
}
export const FeeHistoryTable = ({
showFeeType = true,
showCoinType = true,
filterData,
}: FeeHistoryProps) => {
const [feeData, setFeeData] = useState<FeePrice[]>([]);
const fetchFeesOnStartup = () => {
fetchFees().then(feeResponse => {
setFeeData(filterData ? feeData.filter(filterData) : feeResponse);
});
};
useEffect(fetchFeesOnStartup, []);
const columnNames = ["ID", "Date", "Fee Amount"];
if (showFeeType) columnNames.push("Fee Type");
if (showCoinType) columnNames.push("Coin Type");
const data: string[][] = [];
const getRowData = (row: FeePrice, index: number) => {
const rowData: string[] = [];
rowData.push(
index.toString(),
new Date(row.time).toDateString(),
row.feeAmount.toString()
);
if (showFeeType) rowData.push(row.feeType);
if (showCoinType) rowData.push(row.coinType);
return rowData;
};
feeData.map((row, index) => {
data.push(getRowData(row, index + 1));
});
return <DataTable columnNames={columnNames} data={data} />;
};

View File

@ -0,0 +1,43 @@
import { Box } from "@mui/material";
import { styled } from "@mui/system";
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",
},
}));

View File

@ -0,0 +1,90 @@
import { appName, FEE_BASE, feeAmountBase, FeeType } from "../FeeData.tsx";
import { objectToBase64 } from "../../../utils/toBase64.ts";
import { store } from "../../../state/store.ts";
import { setFeeData } from "../../../state/features/globalSlice.ts";
import { useTestIdentifiers } from "../../Identifiers.ts";
export type CoinType = "QORT" | "BTC" | "LTC" | "DOGE" | "DGB" | "RVN" | "ARRR";
export interface FeePrice {
time: number;
feeAmount: number;
feeType: FeeType; // used to differentiate different types of fees such as comments, likes, data, etc.
coinType: CoinType;
}
const feesPublishService = "DOCUMENT";
export const fetchFees = async () => {
const feeData = store.getState().global.feeData;
if (feeData.length > 0) {
return feeData;
}
try {
const response = await qortalRequest({
action: "FETCH_QDN_RESOURCE",
identifier: FEE_BASE,
name: "Q-Support",
service: feesPublishService,
});
return (await response) as FeePrice[];
} catch (e) {
console.log("fetch current fees error: ", e);
return [] as FeePrice[];
}
};
export const fetchFeesRedux = () => {
const feeData = store.getState().global.feeData;
if (feeData.length > 0) {
return feeData;
}
fetchFees().then(feeData => store.dispatch(setFeeData(feeData)));
};
export const addFeePrice = async (
feeAmount = feeAmountBase,
feeType: FeeType = "default",
coinType: CoinType = "QORT"
) => {
let fees = await fetchFees();
fees.push({
time: Date.now(),
feeAmount,
feeType,
coinType,
});
const feesBase64 = await objectToBase64(fees);
console.log("fees are: ", fees);
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: appName,
identifier: FEE_BASE,
service: feesPublishService,
data64: feesBase64,
});
};
const feeFilter = (fee: FeePrice, feeToVerify: FeePrice) => {
const nameCheck = fee.feeType === feeToVerify.feeType;
const coinTypeCheck = fee.coinType === feeToVerify.coinType;
const timeCheck = feeToVerify.time <= feeToVerify.time;
return nameCheck && coinTypeCheck && timeCheck;
};
export const verifyFeeAmount = async (feeToVerify: FeePrice) => {
if (useTestIdentifiers) return true;
const fees = await fetchFees();
const filteredFees = fees.filter(fee => feeFilter(fee, feeToVerify));
if (filteredFees.length === 0) return false;
const feeToCheck = filteredFees[filteredFees.length - 1]; // gets fee that applies at the time of feeToVerify
return feeToVerify.feeAmount >= feeToCheck.feeAmount;
};

View File

@ -0,0 +1,79 @@
import { feeDestinationName, FeeType } from "./FeeData.tsx";
import { CoinType } from "./FeePricePublish/FeePricePublish.ts";
export interface NameData {
name: string;
reducedName: string;
owner: string;
data: string;
registered: number;
isForSale: boolean;
}
export const getNameData = async (name: string) => {
return qortalRequest({
action: "GET_NAME_DATA",
name: name,
}) as Promise<NameData>;
};
export interface SendCoinResponse {
amount: number;
approvalStatus: string;
fee: string;
recipient: string;
reference: string;
senderPublicKey: string;
signature: string;
timestamp: number;
txGroupId: number;
type: string;
}
export const sendCoin = async (
address: string,
amount: number,
coin: CoinType
) => {
try {
return (await qortalRequest({
action: "SEND_COIN",
coin,
destinationAddress: address,
amount,
})) as SendCoinResponse;
} catch (e) {
console.log("sendCoin refused", e);
}
};
export const sendQORT = async (address: string, amount: number) => {
return await sendCoin(address, amount, "QORT");
};
export const sendQORTtoName = async (name: string, amount: number) => {
const address = await getNameData(name);
if (address) return await sendQORT(address.owner, amount);
else throw Error("Name Not Found");
};
export interface PublishFeeData {
signature: string;
senderName: string;
createdTimestamp?: number; //timestamp of the metadata publish, NOT the send feeAmount publish, added after publish is fetched
updatedTimestamp?: number;
feeType?: FeeType;
coinType?: CoinType;
isPaid?: boolean;
}
export type CommentType = "reply" | "edit" | "comment";
export interface CommentObject {
text: string;
feeData: PublishFeeData;
}
export const payPublishFeeQORT = async (feeAmount: number) => {
const publish = await sendQORTtoName(feeDestinationName, feeAmount);
return publish?.signature;
};

View File

@ -0,0 +1,85 @@
import { Issue } from "../../state/features/fileSlice.ts";
import { PublishFeeData } from "./SendFeeFunctions.ts";
export type AccountName = { name: string; owner: string };
export interface GetRequestData {
limit?: number;
offset?: number;
reverse?: boolean;
}
export interface getTransactionBySignatureResponse {
type: "string";
timestamp: number;
reference: string;
fee: number;
signature: string;
txGroupId: number;
recipient: string;
blockHeight: number;
approvalStatus: string;
creatorAddress: string;
senderPublicKey: string;
amount: string;
}
export const stringIsEmpty = (value: string) => {
return value === "";
};
export const getAccountNames = async (
address: string,
params?: GetRequestData
) => {
const names = (await qortalRequest({
action: "GET_ACCOUNT_NAMES",
address: address,
...params,
})) as AccountName[];
const namelessAddress = { name: "", owner: address };
const emptyNamesFilled = names.map(({ name, owner }) => {
return stringIsEmpty(name) ? namelessAddress : { name, owner };
});
const returnValue =
emptyNamesFilled.length > 0 ? emptyNamesFilled : [namelessAddress];
return returnValue as AccountName[];
};
export const getUserAccountNames = async () => {
const account = await getUserAccount();
return await getAccountNames(account.address);
};
export const userHasName = async (name: string) => {
const userAccountNames = await getUserAccountNames();
const userNames = userAccountNames.map(userName => userName.name);
return userNames.includes(name);
};
export const objectToPublishFeeData = (object: Issue) => {
const createdTimestamp = +object?.created || 0;
const updatedTimestamp = +object?.updated || 0;
return {
signature: object?.feeData?.signature,
createdTimestamp,
updatedTimestamp,
feeType: object?.feeData?.feeType || "default",
coinType: object?.feeData?.coinType || "QORT",
senderName: object?.user,
isPaid: object?.feeData?.isPaid || false,
} as PublishFeeData;
};
export const objectHasNullValues = (object: object) => {
const objectAsArray = Object.values(object);
return objectAsArray.some(value => value == null);
};
export type AccountInfo = { address: string; publicKey: string };
export const getUserAccount = async () => {
return (await qortalRequest({
action: "GET_USER_ACCOUNT",
})) as AccountInfo;
};

View File

@ -0,0 +1,120 @@
import { feeDestinationName, maxFeePublishTimeDiff } from "./FeeData.tsx";
import {
getAccountNames,
getTransactionBySignatureResponse,
objectHasNullValues,
objectToPublishFeeData,
} from "./VerifyPayment-Functions.ts";
import { verifyFeeAmount } from "./FeePricePublish/FeePricePublish.ts";
import { getNameData, PublishFeeData } from "./SendFeeFunctions.ts";
import { Issue } from "../../state/features/fileSlice.ts";
const getSignature = async (signature: string) => {
const url = "/transactions/signature/" + signature;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
return (await response.json()) as getTransactionBySignatureResponse;
};
const verifySignature = async (feeData: PublishFeeData) => {
const {
signature,
createdTimestamp,
updatedTimestamp,
feeType,
coinType,
senderName,
} = feeData;
const [signatureData, accountData] = await Promise.all([
getSignature(signature),
getNameData(senderName),
]);
const namesofFeeRecipient = await getAccountNames(signatureData.recipient);
const doesFeeAmountMatch = await verifyFeeAmount({
time: signatureData.timestamp,
feeAmount: +signatureData.amount,
feeType,
coinType,
});
const signatureTime = signatureData.timestamp;
let doesTimeMatch: boolean = false;
if (!updatedTimestamp) {
const timeDiff = createdTimestamp - signatureTime;
const timeDiffMinutes = Math.abs(timeDiff) / 1000 / 60;
doesTimeMatch = timeDiffMinutes <= maxFeePublishTimeDiff;
} else {
const minutesPublishDiff = 1000 * 60 * maxFeePublishTimeDiff;
const startTime = createdTimestamp - minutesPublishDiff;
const endTime = updatedTimestamp;
const sigTimeAfterStartTime = signatureTime > startTime;
const sigTimeBeforeEndTime = signatureTime < endTime;
doesTimeMatch = sigTimeAfterStartTime && sigTimeBeforeEndTime;
}
const doesSignatureMatch = signature === signatureData?.signature;
const doesSenderMatch = signatureData.creatorAddress === accountData.owner;
const doesFeeRecipientNameMatch =
namesofFeeRecipient.findIndex(
nameData => nameData?.name === feeDestinationName
) >= 0;
if (!doesTimeMatch) console.log("Time does not match");
if (!doesSignatureMatch) console.log("Signature does not match");
if (!doesSenderMatch) console.log("Sender does not match");
if (!doesFeeRecipientNameMatch) console.log("Recipient does not match");
if (!doesFeeAmountMatch) console.log("FeeAmount does not match");
return (
doesTimeMatch &&
doesSignatureMatch &&
doesSenderMatch &&
doesFeeRecipientNameMatch &&
doesFeeAmountMatch
);
};
export const verifyPayment = async (publishToVerify: Issue) => {
if (!publishToVerify) return false;
const publishFeeData = objectToPublishFeeData(publishToVerify);
if (objectHasNullValues(publishFeeData)) return false;
const verifyFunctionsList: Promise<boolean>[] = [];
verifyFunctionsList.push(verifySignature(publishFeeData));
const paymentChecks = await Promise.all(verifyFunctionsList);
return paymentChecks.every(check => check === true);
};
export const appendIsPaidToFeeData = (issue: Issue, isPaid: boolean): Issue => {
return {
...issue,
feeData: {
...(issue?.feeData || { signature: undefined, senderName: "" }),
isPaid,
},
};
};
export const verifyAllPayments = async (issues: Issue[]) => {
const verifiedPayments = await Promise.all(
issues.map(issue => verifyPayment(issue))
);
return issues.map((issue, index) => {
return appendIsPaidToFeeData(issue, verifiedPayments[index]);
});
};

87
src/global.d.ts vendored
View File

@ -1,55 +1,56 @@
// src/global.d.ts // src/global.d.ts
interface QortalRequestOptions { interface QortalRequestOptions {
action: string action: string;
name?: string name?: string;
service?: string service?: string;
data64?: string data64?: string;
title?: string title?: string;
description?: string description?: string;
category?: string category?: string;
tags?: string[] tags?: string[];
identifier?: string identifier?: string;
address?: string address?: string;
metaData?: string metaData?: string;
encoding?: string encoding?: string;
includeMetadata?: boolean includeMetadata?: boolean;
limit?: numebr limit?: numebr;
offset?: number offset?: number;
reverse?: boolean reverse?: boolean;
resources?: any[] resources?: any[];
filename?: string filename?: string;
list_name?: string list_name?: string;
item?: string item?: string;
items?: strings[] items?: strings[];
tag1?: string tag1?: string;
tag2?: string tag2?: string;
tag3?: string tag3?: string;
tag4?: string tag4?: string;
tag5?: string tag5?: string;
coin?: string coin?: string;
destinationAddress?: string destinationAddress?: string;
amount?: number amount?: number;
blob?: Blob blob?: Blob;
mimeType?: string mimeType?: string;
file?: File file?: File;
encryptedData?: string encryptedData?: string;
name?: string name?: string;
mode?: string mode?: string;
query?: string query?: string;
excludeBlocked?: boolean excludeBlocked?: boolean;
exactMatchNames?: boolean exactMatchNames?: boolean;
message?: string;
} }
declare function qortalRequest(options: QortalRequestOptions): Promise<any> declare function qortalRequest(options: QortalRequestOptions): Promise<any>;
declare function qortalRequestWithTimeout( declare function qortalRequestWithTimeout(
options: QortalRequestOptions, options: QortalRequestOptions,
time: number time: number
): Promise<any> ): Promise<any>;
declare global { declare global {
interface Window { interface Window {
_qdnBase: any // Replace 'any' with the appropriate type if you know it _qdnBase: any; // Replace 'any' with the appropriate type if you know it
_qdnTheme: string _qdnTheme: string;
} }
} }
@ -57,6 +58,6 @@ declare global {
interface Window { interface Window {
showSaveFilePicker: ( showSaveFilePicker: (
options?: SaveFilePickerOptions options?: SaveFilePickerOptions
) => Promise<FileSystemFileHandle> ) => Promise<FileSystemFileHandle>;
} }
} }

View File

@ -3,12 +3,12 @@ import { useDispatch, useSelector } from "react-redux";
import { import {
addFiles, addFiles,
addToHashMap, addToHashMap,
Issue,
removeFromHashMap, removeFromHashMap,
setCountNewFiles, setCountNewFiles,
upsertFiles, upsertFiles,
upsertFilesBeginning, upsertFilesBeginning,
upsertFilteredFiles, upsertFilteredFiles,
Video,
} from "../state/features/fileSlice.ts"; } from "../state/features/fileSlice.ts";
import { import {
setFilesPerNamePublished, setFilesPerNamePublished,
@ -24,7 +24,8 @@ import {
QSUPPORT_PLAYLIST_BASE, QSUPPORT_PLAYLIST_BASE,
} from "../constants/Identifiers.ts"; } from "../constants/Identifiers.ts";
import { queue } from "../wrappers/GlobalWrapper"; import { queue } from "../wrappers/GlobalWrapper";
import { getCategoriesFetchString } from "../components/common/CategoryList/CategoryList.tsx"; import { log } from "../constants/Misc.ts";
import { verifyAllPayments } from "../constants/PublishFees/VerifyPayment.ts";
export const useFetchIssues = () => { export const useFetchIssues = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -50,7 +51,7 @@ export const useFetchIssues = () => {
); );
const checkAndUpdateIssue = React.useCallback( const checkAndUpdateIssue = React.useCallback(
(video: Video) => { (video: Issue) => {
const existingVideo = hashMapFiles[video.id]; const existingVideo = hashMapFiles[video.id];
if (!existingVideo) { if (!existingVideo) {
return true; return true;
@ -97,10 +98,10 @@ export const useFetchIssues = () => {
videoId: issueID, videoId: issueID,
content, content,
}); });
console.log("response is: ", res);
res?.isValid res?.isValid
? dispatch(addToHashMap(res)) ? dispatch(addToHashMap(res))
: dispatch(removeFromHashMap(issueID)); : dispatch(removeFromHashMap(issueID));
return res;
} catch (error) { } catch (error) {
retries = retries + 1; retries = retries + 1;
if (retries < 2) { if (retries < 2) {
@ -112,7 +113,7 @@ export const useFetchIssues = () => {
} }
}; };
const getNewFiles = React.useCallback(async () => { const getNewIssues = React.useCallback(async () => {
try { try {
dispatch(setIsLoadingGlobal(true)); dispatch(setIsLoadingGlobal(true));
@ -149,7 +150,7 @@ export const useFetchIssues = () => {
fetchAll = responseData.slice(0, findVideo); fetchAll = responseData.slice(0, findVideo);
} }
const structureData = fetchAll.map((video: any): Video => { const structureData = fetchAll.map((video: any): Issue => {
return { return {
title: video?.metadata?.title, title: video?.metadata?.title,
category: video?.metadata?.category, category: video?.metadata?.category,
@ -186,20 +187,21 @@ export const useFetchIssues = () => {
} }
}, [videos, hashMapFiles]); }, [videos, hashMapFiles]);
const getFiles = React.useCallback( const getIssues = React.useCallback(
async ( async (
filters = {}, filters = {},
reset?: boolean, reset?: boolean,
resetFilers?: boolean, resetFilters?: boolean,
limit?: number limit?: number
) => { ) => {
try { try {
const { const {
name = "", name = "",
categories = [], categories = "",
QappName = "",
keywords = "", keywords = "",
type = "", type = "",
}: any = resetFilers ? {} : filters; }: any = resetFilters ? {} : filters;
let offset = videos.length; let offset = videos.length;
if (reset) { if (reset) {
offset = 0; offset = 0;
@ -211,10 +213,17 @@ export const useFetchIssues = () => {
defaultUrl += `&name=${name}`; defaultUrl += `&name=${name}`;
} }
if (categories.length > 0) { if (categories) {
defaultUrl += "&description=" + getCategoriesFetchString(categories); defaultUrl += "&description=";
} if (log) console.log("categories: ", categories);
if (categories) defaultUrl += categories;
if (log) console.log("description: ", defaultUrl);
}
if (QappName) {
defaultUrl += `&query=${QappName}`;
}
if (log) console.log("defaultURL: ", defaultUrl);
if (keywords) { if (keywords) {
defaultUrl = defaultUrl + `&query=${keywords}`; defaultUrl = defaultUrl + `&query=${keywords}`;
} }
@ -236,47 +245,48 @@ export const useFetchIssues = () => {
}); });
const responseData = await response.json(); const responseData = await response.json();
// const responseData = await qortalRequest({ let structureData = responseData.map((issue: any): Issue => {
// 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 { return {
title: video?.metadata?.title, title: issue?.metadata?.title,
service: video?.service, service: issue?.service,
category: video?.metadata?.category, category: issue?.metadata?.category,
categoryName: video?.metadata?.categoryName, categoryName: issue?.metadata?.categoryName,
tags: video?.metadata?.tags || [], tags: issue?.metadata?.tags || [],
description: video?.metadata?.description, description: issue?.metadata?.description,
created: video?.created, created: issue?.created,
updated: video?.updated, updated: issue?.updated,
user: video.name, user: issue.name,
videoImage: "", videoImage: "",
id: video.identifier, id: issue.identifier,
}; };
}); });
if (reset) { const verifiedIssuePromises: Promise<Issue>[] = [];
dispatch(addFiles(structureData));
} else {
dispatch(upsertFiles(structureData));
}
for (const content of structureData) { for (const content of structureData) {
if (content.user && content.id) { if (content.user && content.id) {
const res = checkAndUpdateIssue(content); const res = checkAndUpdateIssue(content);
const issue: Promise<Issue> = getIssue(
content.user,
content.id,
content
);
verifiedIssuePromises.push(issue);
if (res) { if (res) {
queue.push(() => getIssue(content.user, content.id, content)); queue.push(() => issue);
} }
} }
} }
const issues = await Promise.all(verifiedIssuePromises);
const verifiedIssues = await verifyAllPayments(issues);
structureData = structureData.map((issue, index) => {
return {
...issue,
feeData: verifiedIssues[index]?.feeData,
};
});
if (reset) dispatch(addFiles(structureData));
else dispatch(upsertFiles(structureData));
} catch (error) { } catch (error) {
console.log({ error }); console.log({ error });
} finally { } finally {
@ -285,7 +295,7 @@ export const useFetchIssues = () => {
[videos, hashMapFiles] [videos, hashMapFiles]
); );
const getFilesFiltered = React.useCallback( const getIssuesFiltered = React.useCallback(
async (filterValue: string) => { async (filterValue: string) => {
try { try {
const offset = filteredVideos.length; const offset = filteredVideos.length;
@ -314,7 +324,7 @@ export const useFetchIssues = () => {
// exactMatchNames: true, // exactMatchNames: true,
// name: names // name: names
// }) // })
const structureData = responseData.map((video: any): Video => { const structureData = responseData.map((video: any): Issue => {
return { return {
title: video?.metadata?.title, title: video?.metadata?.title,
category: video?.metadata?.category, category: video?.metadata?.category,
@ -345,7 +355,7 @@ export const useFetchIssues = () => {
[filteredVideos, hashMapFiles] [filteredVideos, hashMapFiles]
); );
const checkNewFiles = React.useCallback(async () => { const checkNewIssues = React.useCallback(async () => {
try { try {
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QSUPPORT_FILE_BASE}&limit=20&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true`; const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QSUPPORT_FILE_BASE}&limit=20&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true`;
const response = await fetch(url, { const response = await fetch(url, {
@ -382,7 +392,7 @@ export const useFetchIssues = () => {
} catch (error) {} } catch (error) {}
}, [videos]); }, [videos]);
const getFilesCount = React.useCallback(async () => { const getIssuesCount = React.useCallback(async () => {
try { try {
let url = `/arbitrary/resources/search?mode=ALL&includemetadata=false&limit=0&service=DOCUMENT&identifier=${QSUPPORT_FILE_BASE}`; let url = `/arbitrary/resources/search?mode=ALL&includemetadata=false&limit=0&service=DOCUMENT&identifier=${QSUPPORT_FILE_BASE}`;
@ -411,13 +421,13 @@ export const useFetchIssues = () => {
}, []); }, []);
return { return {
getFiles, getIssues,
checkAndUpdateFile: checkAndUpdateIssue, checkAndUpdateIssue,
getFile: getIssue, getIssue,
hashMapFiles, hashMapFiles,
getNewFiles, getNewIssues,
checkNewFiles, checkNewIssues,
getFilesFiltered, getIssuesFiltered,
getFilesCount, getIssuesCount,
}; };
}; };

View File

@ -1,25 +1,25 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
export function useWindowSize() { export function useWindowSize() {
const [windowSize, setWindowSize] = useState<any>({ const [windowSize, setWindowSize] = useState<any>({
width: undefined, width: undefined,
}); });
useEffect(() => { useEffect(() => {
function handleResize() { function handleResize() {
setWindowSize({ setWindowSize({
width: window.innerWidth, width: window.innerWidth,
}); });
} }
window.addEventListener("resize", handleResize); window.addEventListener("resize", handleResize);
// Call handler right away so state gets updated with initial window size // Call handler right away so state gets updated with initial window size
handleResize(); handleResize();
// Remove event listener on cleanup // Remove event listener on cleanup
return () => window.removeEventListener("resize", handleResize); 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. }, []); // 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; return windowSize;
} }

View File

@ -1,229 +1,229 @@
@font-face { @font-face {
font-family: 'Cambon Light'; font-family: 'Cambon Light';
src: url("./styles/fonts/Cambon-Light.ttf") format("truetype"); src: url("./styles/fonts/Cambon-Light.ttf") format("truetype");
} }
@font-face { @font-face {
font-family: 'Merriweather Sans'; font-family: 'Merriweather Sans';
src: url("./styles/fonts/Merriweather Sans.ttf") format("truetype"); src: url("./styles/fonts/Merriweather Sans.ttf") format("truetype");
} }
@font-face { @font-face {
font-family: 'Karla'; font-family: 'Karla';
src: url("./styles/fonts/Karla.ttf") format("truetype"); src: url("./styles/fonts/Karla.ttf") format("truetype");
} }
@font-face { @font-face {
font-family: 'Proxima Nova'; font-family: 'Proxima Nova';
src: url("./styles/fonts/ProximaNova.otf") format("opentype"); src: url("./styles/fonts/ProximaNova.otf") format("opentype");
} }
@font-face { @font-face {
font-family: 'Raleway'; font-family: 'Raleway';
src: url("./styles/fonts/Raleway.ttf") format("truetype"); src: url("./styles/fonts/Raleway.ttf") format("truetype");
} }
@font-face { @font-face {
font-family: 'Catamaran'; font-family: 'Catamaran';
src: url("./styles/fonts/Catamaran.ttf") format("truetype"); src: url("./styles/fonts/Catamaran.ttf") format("truetype");
} }
@font-face { @font-face {
font-family: 'Oxygen'; font-family: 'Oxygen';
src: url("./styles/fonts/Oxygen.ttf") format("truetype"); src: url("./styles/fonts/Oxygen.ttf") format("truetype");
} }
@font-face { @font-face {
font-family: 'Cairo'; font-family: 'Cairo';
src: url("./styles/fonts/Cairo.ttf") format("truetype"); src: url("./styles/fonts/Cairo.ttf") format("truetype");
} }
:root { :root {
padding: 0px; padding: 0px;
margin: 0px; margin: 0px;
box-sizing: border-box; box-sizing: border-box;
} }
.line-clamp { .line-clamp {
height: 100px; height: 100px;
overflow: hidden; overflow: hidden;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 5; /* number of lines to show */ -webkit-line-clamp: 5; /* number of lines to show */
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.edit-btn:hover { .edit-btn:hover {
opacity: .75; opacity: .75;
transition: .2s all; transition: .2s all;
} }
.post-image { .post-image {
max-width: 100%; max-width: 100%;
border-radius: 5px; border-radius: 5px;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.test-grid { .test-grid {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
min-height: 25px; min-height: 25px;
} }
.test-grid-item { .test-grid-item {
border: 1px solid powderblue; border: 1px solid powderblue;
} }
body::-webkit-scrollbar-track { body::-webkit-scrollbar-track {
background-color: transparent; background-color: transparent;
} }
body::-webkit-scrollbar-track:hover { body::-webkit-scrollbar-track:hover {
background-color: transparent; background-color: transparent;
} }
body::-webkit-scrollbar { body::-webkit-scrollbar {
width: 16px; width: 16px;
height: 10px; height: 10px;
background-color: white; background-color: white;
} }
body::-webkit-scrollbar-thumb { body::-webkit-scrollbar-thumb {
background-color: #838eee; background-color: #838eee;
border-radius: 8px; border-radius: 8px;
background-clip: content-box; background-clip: content-box;
border: 4px solid transparent; border: 4px solid transparent;
} }
body::-webkit-scrollbar-thumb:hover { body::-webkit-scrollbar-thumb:hover {
background-color: #6270f0; background-color: #6270f0;
} }
.MuiList-root::-webkit-scrollbar-track { .MuiList-root::-webkit-scrollbar-track {
background-color: transparent; background-color: transparent;
} }
.MuiList-root::-webkit-scrollbar-track:hover { .MuiList-root::-webkit-scrollbar-track:hover {
background-color: transparent; background-color: transparent;
} }
.MuiList-root::-webkit-scrollbar { .MuiList-root::-webkit-scrollbar {
width: 14px; width: 14px;
height: 10px; height: 10px;
background-color: white; background-color: white;
} }
.MuiList-root::-webkit-scrollbar-thumb { .MuiList-root::-webkit-scrollbar-thumb {
background-color: lightgray; background-color: lightgray;
border-radius: 8px; border-radius: 8px;
background-clip: content-box; background-clip: content-box;
border: 4px solid transparent; border: 4px solid transparent;
} }
.MuiList-root::-webkit-scrollbar-thumb:hover { .MuiList-root::-webkit-scrollbar-thumb:hover {
background-color: lightslategray; background-color: lightslategray;
} }
.my-masonry-grid { .my-masonry-grid {
display: -webkit-box; /* Not needed if autoprefixing */ display: -webkit-box; /* Not needed if autoprefixing */
display: -ms-flexbox; /* Not needed if autoprefixing */ display: -ms-flexbox; /* Not needed if autoprefixing */
display: flex; display: flex;
margin-left: -20px; /* gutter size offset */ margin-left: -20px; /* gutter size offset */
width: auto; width: auto;
padding: 15px 20px; padding: 15px 20px;
} }
.my-masonry-grid_column { .my-masonry-grid_column {
padding-left: 20px; /* gutter size */ padding-left: 20px; /* gutter size */
background-clip: padding-box; background-clip: padding-box;
} }
/* Style your items */ /* Style your items */
.my-masonry-grid_column > li { /* change div to reference your elements you put in <Masonry> */ .my-masonry-grid_column > li { /* change div to reference your elements you put in <Masonry> */
margin-bottom: 30px; margin-bottom: 30px;
} }
.my-svg path { .my-svg path {
fill: red fill: red
} }
.qortal-link { .qortal-link {
text-decoration: none; /* Removes the underline */ text-decoration: none; /* Removes the underline */
color: inherit; /* Inherits the color of the parent element */ color: inherit; /* Inherits the color of the parent element */
} }
.qortal-link:hover, a:focus { .qortal-link:hover, a:focus {
text-decoration: underline; /* Adds underline on hover and focus for accessibility */ text-decoration: underline; /* Adds underline on hover and focus for accessibility */
} }
.download-icon { .download-icon {
transition: all 0.5s ease-in-out; transition: all 0.5s ease-in-out;
animation: downloadIconAnimation 2s infinite; animation: downloadIconAnimation 2s infinite;
} }
@keyframes downloadIconAnimation { @keyframes downloadIconAnimation {
0% { transform: scale(1); fill: #fff; } 0% { transform: scale(1); fill: #fff; }
50% { transform: scale(1.2); fill: #3498db; } 50% { transform: scale(1.2); fill: #3498db; }
100% { transform: scale(1); fill: #fff; } 100% { transform: scale(1); fill: #fff; }
} }
.closePlayer { .closePlayer {
position: absolute; position: absolute;
top: 0px; top: 0px;
width: 100%; width: 100%;
transition: all 0.3s; transition: all 0.3s;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
z-index: 8000; z-index: 8000;
} }
/* When the screen is 600px or less, display .myClassUnder600 and hide .myClassOver600 */ /* When the screen is 600px or less, display .myClassUnder600 and hide .myClassOver600 */
@media screen and (max-width: 600px) { @media screen and (max-width: 600px) {
.myClassUnder600 { .myClassUnder600 {
display: none !important; display: none !important;
} }
} }
@media screen and (min-width: 601px) { @media screen and (min-width: 601px) {
.myClassOver600 { .myClassOver600 {
display: none !important; display: none !important;
} }
} }
.ql-editor { .ql-editor {
min-height: 100px; min-height: 100px;
width: 100% width: 100%
} }
.ql-editor img { .ql-editor img {
cursor: default; cursor: default;
} }
.ql-container { .ql-container {
font-size: 16px font-size: 16px
} }
.hover-click { .hover-click {
transition: opacity 0.2s; transition: opacity 0.2s;
} }
.hover-click:hover { .hover-click:hover {
opacity: 0.7; opacity: 0.7;
} }

View File

@ -1,17 +1,17 @@
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import App from './App' import App from './App'
import './index.css' import './index.css'
import { BrowserRouter } from 'react-router-dom' import { BrowserRouter } from 'react-router-dom'
interface CustomWindow extends Window { interface CustomWindow extends Window {
_qdnBase: string _qdnBase: string
} }
const customWindow = window as unknown as CustomWindow const customWindow = window as unknown as CustomWindow
const baseUrl = customWindow?._qdnBase || '' const baseUrl = customWindow?._qdnBase || ''
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<BrowserRouter basename={baseUrl}> <BrowserRouter basename={baseUrl}>
<App /> <App />
<div id="modal-root" /> <div id="modal-root" />
</BrowserRouter> </BrowserRouter>
) )

View File

@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { RootState } from "../../state/store"; import { RootState } from "../../state/store";
import { Box, useTheme } from "@mui/material"; import { Box, useTheme } from "@mui/material";
import { FileContainer } from "./IssueList-styles.tsx"; import { IssueContainer } from "./IssueList-styles.tsx";
import ResponsiveImage from "../../components/ResponsiveImage"; import ResponsiveImage from "../../components/ResponsiveImage";
import { ChannelCard, ChannelTitle } from "./Home-styles"; import { ChannelCard, ChannelTitle } from "./Home-styles";
@ -30,7 +30,7 @@ export const Channels = ({ mode }: VideoListProps) => {
minHeight: "50vh", minHeight: "50vh",
}} }}
> >
<FileContainer> <IssueContainer>
{publishNames && {publishNames &&
publishNames?.slice(0, 10).map(name => { publishNames?.slice(0, 10).map(name => {
let avatarUrl = ""; let avatarUrl = "";
@ -62,7 +62,7 @@ export const Channels = ({ mode }: VideoListProps) => {
</Box> </Box>
); );
})} })}
</FileContainer> </IssueContainer>
</Box> </Box>
); );
}; };

View File

@ -1,5 +1,6 @@
import { styled } from "@mui/system"; import { styled } from "@mui/system";
import { Box, Button, Grid, Typography } from "@mui/material"; import { Box, Button, Grid, Typography } from "@mui/material";
import { fontSizeSmall } from "../../constants/Misc.ts";
export const SubtitleContainer = styled(Box)(({ theme }) => ({ export const SubtitleContainer = styled(Box)(({ theme }) => ({
display: "flex", display: "flex",
@ -88,12 +89,12 @@ export const ThemeButton = styled(Button)(({ theme }) => ({
color: theme.palette.text.primary, color: theme.palette.text.primary,
backgroundColor: "#01a9e9", backgroundColor: "#01a9e9",
fontSize: "18px", fontSize: "18px",
"&:hover": { backgroundColor: "#3e74c1" }, "&:hover": { backgroundColor: "#008fcd" },
})); }));
export const ThemeButtonBright = styled(Button)(({ theme }) => ({ export const ThemeButtonBright = styled(Button)(({ theme }) => ({
color: theme.palette.text.primary, color: theme.palette.text.primary,
backgroundColor: "#44c4ff", backgroundColor: "#44c4ff",
fontSize: "18px", fontSize: fontSizeSmall,
"&:hover": { backgroundColor: "#01a9e9" }, "&:hover": { backgroundColor: "#01a9e9" },
})); }));

View File

@ -12,40 +12,48 @@ import {
changefilterName, changefilterName,
changefilterSearch, changefilterSearch,
changeFilterType, changeFilterType,
setQappNames,
} from "../../state/features/fileSlice.ts"; } from "../../state/features/fileSlice.ts";
import { allCategoryData } from "../../constants/Categories/1stCategories.ts"; import {
allCategoryData,
IssueState,
} from "../../constants/Categories/Categories.ts";
import { import {
CategoryList, CategoryList,
CategoryListRef, CategoryListRef,
getCategoriesFetchString,
} from "../../components/common/CategoryList/CategoryList.tsx"; } from "../../components/common/CategoryList/CategoryList.tsx";
import { StatsData } from "../../components/StatsData.tsx"; import { StatsData } from "../../components/StatsData.tsx";
import {
CategorySelect,
CategorySelectRef,
} from "../../components/common/CategoryList/CategorySelect.tsx";
import {
AutocompleteQappNames,
getPublishedQappNames,
QappNamesRef,
} from "../../components/common/AutocompleteQappNames.tsx";
interface HomeProps { interface HomeProps {
mode?: string; mode?: string;
} }
export const Home = ({ mode }: HomeProps) => { export const Home = ({ mode }: HomeProps) => {
const theme = useTheme(); const theme = useTheme();
const prevVal = useRef("");
const categoryListRef = useRef<CategoryListRef>(null);
const isFiltering = useSelector((state: RootState) => state.file.isFiltering); const isFiltering = useSelector((state: RootState) => state.file.isFiltering);
const filterValue = useSelector((state: RootState) => state.file.filterValue); const filterValue = useSelector((state: RootState) => state.file.filterValue);
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const filterType = useSelector((state: RootState) => state.file.filterType); const filterType = useSelector((state: RootState) => state.file.filterType);
const totalFilesPublished = useSelector(
(state: RootState) => state.global.totalFilesPublished
);
const totalNamesPublished = useSelector(
(state: RootState) => state.global.totalNamesPublished
);
const filesPerNamePublished = useSelector(
(state: RootState) => state.global.filesPerNamePublished
);
const setFilterType = payload => { const setFilterType = payload => {
dispatch(changeFilterType(payload)); dispatch(changeFilterType(payload));
}; };
const filterSearch = useSelector( const filterSearch = useSelector(
(state: RootState) => state.file.filterSearch (state: RootState) => state.file.filterSearch
); );
const QappNames = useSelector(
(state: RootState) => state.file.publishedQappNames
);
const autocompleteRef = useRef<QappNamesRef>(null);
const setFilterSearch = payload => { const setFilterSearch = payload => {
dispatch(changefilterSearch(payload)); dispatch(changefilterSearch(payload));
@ -59,60 +67,66 @@ export const Home = ({ mode }: HomeProps) => {
const isFilterMode = useRef(false); const isFilterMode = useRef(false);
const firstFetch = useRef(false); const firstFetch = useRef(false);
const afterFetch = useRef(false); const afterFetch = useRef(false);
const isFetchingFiltered = useRef(false);
const isFetching = useRef(false); const isFetching = useRef(false);
const prevVal = useRef("");
const categoryListRef = useRef<CategoryListRef>(null);
const categorySelectRef = useRef<CategorySelectRef>(null);
const countNewFiles = useSelector( const [showCategoryList, setShowCategoryList] = useState<boolean>(true);
(state: RootState) => state.file.countNewFiles const [showCategorySelect, setShowCategorySelect] = useState<boolean>(true);
);
const userAvatarHash = useSelector(
(state: RootState) => state.global.userAvatarHash
);
const { files: globalVideos } = useSelector((state: RootState) => state.file); const { files: globalVideos } = useSelector((state: RootState) => state.file);
const setSelectedCategoryFiles = payload => {};
const dispatch = useDispatch(); const dispatch = useDispatch();
const filteredFiles = useSelector( const filteredFiles = useSelector(
(state: RootState) => state.file.filteredFiles (state: RootState) => state.file.filteredFiles
); );
const [QappNamesParam, setQappNamesParam] = useState<string[]>([]);
useEffect(() => {
getPublishedQappNames().then(QappNamesResult => {
dispatch(setQappNames(QappNamesResult));
setQappNamesParam(QappNamesResult);
});
}, []);
const { const {
getFiles, getIssues,
checkAndUpdateFile, checkAndUpdateIssue,
getFile, getIssue,
hashMapFiles, hashMapFiles,
getNewFiles, getNewIssues,
checkNewFiles, checkNewIssues,
getFilesFiltered, getIssuesFiltered,
getFilesCount, getIssuesCount,
} = useFetchIssues(); } = useFetchIssues();
const getFilesHandler = React.useCallback( const getIssuesHandler = React.useCallback(
async (reset?: boolean, resetFilers?: boolean) => { async (reset?: boolean, resetFilters?: boolean) => {
if (!firstFetch.current || !afterFetch.current) return; if (!firstFetch.current || !afterFetch.current) return;
if (isFetching.current) return; if (isFetching.current) return;
isFetching.current = true; isFetching.current = true;
const selectedCategories = const selectedCategories =
categoryListRef.current.getSelectedCategories() || []; categoryListRef.current?.getSelectedCategories() || [];
const issueType = categorySelectRef?.current?.getSelectedCategory();
await getFiles( if (issueType) selectedCategories[2] = issueType;
await getIssues(
{ {
name: filterName, name: filterName,
categories: selectedCategories, categories: getCategoriesFetchString(selectedCategories),
QappName: autocompleteRef?.current?.getQappNameFetchString(),
keywords: filterSearch, keywords: filterSearch,
type: filterType, type: filterType,
}, },
reset, reset,
resetFilers resetFilters
); );
isFetching.current = false; isFetching.current = false;
}, },
[ [
getFiles, getIssues,
filterValue, filterValue,
getFilesFiltered, getIssuesFiltered,
isFiltering, isFiltering,
filterName, filterName,
filterSearch, filterSearch,
@ -122,33 +136,33 @@ export const Home = ({ mode }: HomeProps) => {
const searchOnEnter = e => { const searchOnEnter = e => {
if (e.keyCode == 13) { if (e.keyCode == 13) {
getFilesHandler(true); getIssuesHandler(true);
} }
}; };
useEffect(() => { useEffect(() => {
if (isFiltering && filterValue !== prevVal?.current) { if (isFiltering && filterValue !== prevVal?.current) {
prevVal.current = filterValue; prevVal.current = filterValue;
getFilesHandler(); getIssuesHandler();
} }
}, [filterValue, isFiltering, filteredFiles, getFilesCount]); }, [filterValue, isFiltering, filteredFiles, getIssuesCount]);
const getFilesHandlerMount = React.useCallback(async () => { const getFilesHandlerMount = React.useCallback(async () => {
if (firstFetch.current) return; if (firstFetch.current) return;
firstFetch.current = true; firstFetch.current = true;
setIsLoading(true); setIsLoading(true);
await getFiles(); await getIssues();
afterFetch.current = true; afterFetch.current = true;
isFetching.current = false; isFetching.current = false;
setIsLoading(false); setIsLoading(false);
}, [getFiles]); }, [getIssues]);
let videos = globalVideos; let issues = globalVideos;
if (isFiltering) { if (isFiltering) {
videos = filteredFiles; issues = filteredFiles;
isFilterMode.current = true; isFilterMode.current = true;
} else { } else {
isFilterMode.current = false; isFilterMode.current = false;
@ -199,9 +213,10 @@ export const Home = ({ mode }: HomeProps) => {
setFilterSearch(""); setFilterSearch("");
setFilterName(""); setFilterName("");
categoryListRef.current?.clearCategories(); categoryListRef.current?.clearCategories();
categorySelectRef.current?.clearCategory();
autocompleteRef.current?.setSelectedValue(null);
ReactDOM.flushSync(() => { ReactDOM.flushSync(() => {
getFilesHandler(true, true); getIssuesHandler(true, true);
}); });
}; };
@ -268,7 +283,43 @@ export const Home = ({ mode }: HomeProps) => {
fontSize: "20px", fontSize: "20px",
}} }}
/> />
<CategoryList categoryData={allCategoryData} ref={categoryListRef} /> {showCategoryList && (
<CategoryList
categoryData={allCategoryData}
ref={categoryListRef}
afterChange={value => {
setShowCategorySelect(!value[0]);
}}
/>
)}
{showCategorySelect && (
<CategorySelect
categoryData={IssueState}
ref={categorySelectRef}
sx={{ marginTop: "20px" }}
afterChange={value => {
setShowCategoryList(!value);
}}
/>
)}
{QappNamesParam.length > 0 && (
<AutocompleteQappNames
ref={autocompleteRef}
namesList={QappNamesParam}
sx={{ marginTop: "20px" }}
required={false}
afterChange={() => {
const currentSelectedCategories =
categoryListRef?.current?.getSelectedCategories();
categoryListRef?.current?.setSelectedCategories([
"3",
currentSelectedCategories[1],
currentSelectedCategories[2],
]);
}}
/>
)}
<ThemeButton <ThemeButton
onClick={() => { onClick={() => {
@ -284,7 +335,7 @@ export const Home = ({ mode }: HomeProps) => {
</ThemeButton> </ThemeButton>
<ThemeButton <ThemeButton
onClick={() => { onClick={() => {
getFilesHandler(true); getIssuesHandler(true);
}} }}
sx={{ sx={{
marginTop: "20px", marginTop: "20px",
@ -314,9 +365,9 @@ export const Home = ({ mode }: HomeProps) => {
maxWidth: "1400px", maxWidth: "1400px",
}} }}
></SubtitleContainer> ></SubtitleContainer>
<IssueList issues={videos} /> <IssueList issues={issues} />
<LazyLoad <LazyLoad
onLoadMore={getFilesHandler} onLoadMore={getIssuesHandler}
isLoading={isLoading} isLoading={isLoading}
></LazyLoad> ></LazyLoad>
</Box> </Box>

View File

@ -8,8 +8,9 @@ import {
TextField, TextField,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import { fontSizeMedium, fontSizeSmall } from "../../constants/Misc.ts";
export const FileContainer = styled(Box)(({ theme }) => ({ export const IssueContainer = styled(Box)(({ theme }) => ({
position: "relative", position: "relative",
display: "flex", display: "flex",
padding: "15px", padding: "15px",
@ -33,7 +34,7 @@ export const StoresRow = styled(Grid)(({ theme }) => ({
}, },
})); }));
export const VideoCard = styled(Grid)(({ theme }) => ({ export const IssueCard = styled(Grid)(({ theme }) => ({
position: "relative", position: "relative",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
@ -89,14 +90,14 @@ const DoubleLine = styled(Typography)`
export const VideoCardTitle = styled(DoubleLine)(({ theme }) => ({ export const VideoCardTitle = styled(DoubleLine)(({ theme }) => ({
fontFamily: "Cairo", fontFamily: "Cairo",
fontSize: "16px", fontSize: fontSizeMedium,
letterSpacing: "0.4px", letterSpacing: "0.4px",
color: theme.palette.text.primary, color: theme.palette.text.primary,
userSelect: "none", userSelect: "none",
})); }));
export const VideoCardName = styled(Typography)(({ theme }) => ({ export const VideoCardName = styled(Typography)(({ theme }) => ({
fontFamily: "Cairo", fontFamily: "Cairo",
fontSize: "14px", fontSize: fontSizeSmall,
letterSpacing: "0.4px", letterSpacing: "0.4px",
color: theme.palette.text.primary, color: theme.palette.text.primary,
userSelect: "none", userSelect: "none",
@ -107,14 +108,16 @@ export const VideoCardName = styled(Typography)(({ theme }) => ({
})); }));
export const VideoUploadDate = styled(Typography)(({ theme }) => ({ export const VideoUploadDate = styled(Typography)(({ theme }) => ({
fontFamily: "Cairo", fontFamily: "Cairo",
fontSize: "12px", display: "span",
fontSize: fontSizeSmall,
letterSpacing: "0.4px", letterSpacing: "0.4px",
color: theme.palette.text.primary, color: theme.palette.text.primary,
userSelect: "none", userSelect: "none",
})); }));
export const BottomParent = styled(Box)(({ theme }) => ({ export const BottomParent = styled(Box)(({ theme }) => ({
display: "flex", display: "flex",
alignItems: "flex-start", justifyItems: "flex-end",
alignItems: "flex-end",
flexDirection: "column", flexDirection: "column",
})); }));
export const VideoCardDescription = styled(Typography)(({ theme }) => ({ export const VideoCardDescription = styled(Typography)(({ theme }) => ({
@ -155,11 +158,11 @@ export const MyStoresRow = styled(Grid)(({ theme }) => ({
width: "100%", width: "100%",
})); }));
export const NameContainer = styled(Box)(({ theme }) => ({ export const NameAndDateContainer = styled(Box)(({ theme }) => ({
display: "flex", display: "grid",
flexDirection: "row", gridTemplateRows: "1fr 1fr",
justifyContent: "flex-start", justifyContent: "end",
alignItems: "center", alignContent: "center",
gap: "10px", gap: "10px",
marginBottom: "10px", marginBottom: "10px",
})); }));

View File

@ -1,11 +1,10 @@
import { Avatar, Box, Skeleton } from "@mui/material"; import { Avatar, Box, Skeleton } from "@mui/material";
import { import {
BlockIconContainer, BlockIconContainer,
BottomParent,
FileContainer,
IconsBox, IconsBox,
NameContainer, IssueCard,
VideoCard, IssueContainer,
NameAndDateContainer,
VideoCardName, VideoCardName,
VideoCardTitle, VideoCardTitle,
VideoUploadDate, VideoUploadDate,
@ -13,11 +12,10 @@ import {
import EditIcon from "@mui/icons-material/Edit"; import EditIcon from "@mui/icons-material/Edit";
import { import {
blockUser, blockUser,
Issue,
setEditFile, setEditFile,
Video,
} from "../../state/features/fileSlice.ts"; } from "../../state/features/fileSlice.ts";
import BlockIcon from "@mui/icons-material/Block"; import BlockIcon from "@mui/icons-material/Block";
import AttachFileIcon from "@mui/icons-material/AttachFile";
import { formatBytes } from "../IssueContent/IssueContent.tsx"; import { formatBytes } from "../IssueContent/IssueContent.tsx";
import { formatDate } from "../../utils/time.ts"; import { formatDate } from "../../utils/time.ts";
import React, { useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
@ -26,8 +24,12 @@ import { RootState } from "../../state/store.ts";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { getIconsFromObject } from "../../constants/Categories/CategoryFunctions.ts"; import { getIconsFromObject } from "../../constants/Categories/CategoryFunctions.ts";
import { IssueIcon, IssueIcons } from "../../components/common/IssueIcon.tsx";
import QORTicon from "../../assets/icons/qort.png";
import { fontSizeMedium } from "../../constants/Misc.ts";
interface FileListProps { interface FileListProps {
issues: Video[]; issues: Issue[];
} }
export const IssueList = ({ issues }: FileListProps) => { export const IssueList = ({ issues }: FileListProps) => {
const hashMapIssues = useSelector( const hashMapIssues = useSelector(
@ -60,16 +62,21 @@ export const IssueList = ({ issues }: FileListProps) => {
}, [issues, hashMapIssues]); }, [issues, hashMapIssues]);
return ( return (
<FileContainer> <IssueContainer>
{filteredIssues.map((file: any, index: number) => { {filteredIssues.map((issue: any, index: number) => {
const existingFile = hashMapIssues[file?.id]; const existingFile = hashMapIssues[issue?.id];
let hasHash = false; let hasHash = false;
let fileObj = file; let issueObj = issue;
if (existingFile) { if (existingFile) {
fileObj = existingFile; issueObj = existingFile;
hasHash = true; hasHash = true;
} }
const icon = getIconsFromObject(fileObj);
const issueIcons = getIconsFromObject(issueObj);
const fileBytes = issueObj?.files.reduce(
(acc, cur) => acc + (cur?.size || 0),
0
);
return ( return (
<Box <Box
sx={{ sx={{
@ -79,22 +86,22 @@ export const IssueList = ({ issues }: FileListProps) => {
height: "75px", height: "75px",
position: "relative", position: "relative",
}} }}
key={fileObj.id} key={issueObj.id}
onMouseEnter={() => setShowIcons(fileObj.id)} onMouseEnter={() => setShowIcons(issueObj.id)}
onMouseLeave={() => setShowIcons(null)} onMouseLeave={() => setShowIcons(null)}
> >
{hasHash ? ( {hasHash ? (
<> <>
<IconsBox <IconsBox
sx={{ sx={{
opacity: showIcons === fileObj.id ? 1 : 0, opacity: showIcons === issueObj.id ? 1 : 0,
zIndex: 2, zIndex: 2,
}} }}
> >
{fileObj?.user === username && ( {issueObj?.user === username && (
<BlockIconContainer <BlockIconContainer
onClick={() => { onClick={() => {
dispatch(setEditFile(fileObj)); dispatch(setEditFile(issueObj));
}} }}
> >
<EditIcon /> <EditIcon />
@ -102,10 +109,10 @@ export const IssueList = ({ issues }: FileListProps) => {
</BlockIconContainer> </BlockIconContainer>
)} )}
{fileObj?.user !== username && ( {issueObj?.user !== username && (
<BlockIconContainer <BlockIconContainer
onClick={() => { onClick={() => {
blockUserFunc(fileObj?.user); blockUserFunc(issueObj?.user);
}} }}
> >
<BlockIcon /> <BlockIcon />
@ -113,15 +120,14 @@ export const IssueList = ({ issues }: FileListProps) => {
</BlockIconContainer> </BlockIconContainer>
)} )}
</IconsBox> </IconsBox>
<VideoCard <IssueCard
onClick={() => { onClick={() => {
navigate(`/issue/${fileObj?.user}/${fileObj?.id}`); navigate(`/issue/${issueObj?.user}/${issueObj?.id}`);
}} }}
sx={{ sx={{
height: "100%", height: "100%",
width: "100%", width: "100%",
display: "flex", display: "flex",
gap: "25px",
flexDirection: "row", flexDirection: "row",
justifyContent: "space-between", justifyContent: "space-between",
}} }}
@ -129,47 +135,59 @@ export const IssueList = ({ issues }: FileListProps) => {
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
gap: "25px",
alignItems: "center", alignItems: "center",
}} }}
> >
{icon ? ( <div
<img style={{
src={icon} display: "flex",
width="50px" alignItems: "center",
style={{ width: "200px",
borderRadius: "5px",
}}
/>
) : (
<AttachFileIcon />
)}
<VideoCardTitle
sx={{
width: "100px",
}} }}
> >
{formatBytes( <IssueIcons
fileObj?.files.reduce( iconSources={issueIcons}
(acc, cur) => acc + (cur?.size || 0), style={{ marginRight: "20px" }}
0 showBackupIcon={true}
) />
)} </div>
<VideoCardTitle
sx={{
width: "150px",
fontSize: fontSizeMedium,
}}
>
{fileBytes > 0 && formatBytes(fileBytes)}
</VideoCardTitle>
<VideoCardTitle sx={{ fontWeight: "bold", width: "500px" }}>
{issueObj.title}
</VideoCardTitle> </VideoCardTitle>
<VideoCardTitle>{fileObj.title}</VideoCardTitle>
</Box> </Box>
<BottomParent>
<NameContainer {issue?.feeData?.isPaid && (
onClick={e => { <IssueIcon
e.stopPropagation(); iconSrc={QORTicon}
navigate(`/channel/${fileObj?.user}`); style={{ marginRight: "20px" }}
/>
)}
<NameAndDateContainer
sx={{ width: "200px", height: "100%" }}
onClick={e => {
e.stopPropagation();
navigate(`/channel/${issueObj?.user}`);
}}
>
<div
style={{
display: "flex",
width: "200px",
}} }}
> >
<Avatar <Avatar
sx={{ height: 24, width: 24 }} sx={{ height: 24, width: 24, marginRight: "10px" }}
src={`/arbitrary/THUMBNAIL/${fileObj?.user}/qortal_avatar`} src={`/arbitrary/THUMBNAIL/${issueObj?.user}/qortal_avatar`}
alt={`${fileObj?.user}'s avatar`} alt={`${issueObj?.user}'s avatar`}
/> />
<VideoCardName <VideoCardName
sx={{ sx={{
@ -178,17 +196,17 @@ export const IssueList = ({ issues }: FileListProps) => {
}, },
}} }}
> >
{fileObj?.user} {issueObj?.user}
</VideoCardName> </VideoCardName>
</NameContainer> </div>
{fileObj?.created && ( {issueObj?.created && (
<VideoUploadDate> <VideoUploadDate>
{formatDate(fileObj.created)} {formatDate(issueObj.created)}
</VideoUploadDate> </VideoUploadDate>
)} )}
</BottomParent> </NameAndDateContainer>
</VideoCard> </IssueCard>
</> </>
) : ( ) : (
<Skeleton <Skeleton
@ -206,6 +224,6 @@ export const IssueList = ({ issues }: FileListProps) => {
</Box> </Box>
); );
})} })}
</FileContainer> </IssueContainer>
); );
}; };

View File

@ -2,31 +2,33 @@ import React, { useEffect, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { RootState } from "../../state/store"; import { RootState } from "../../state/store";
import AttachFileIcon from "@mui/icons-material/AttachFile";
import { Avatar, Box, Skeleton, useTheme } from "@mui/material"; import { Avatar, Box, Skeleton, useTheme } from "@mui/material";
import { useFetchIssues } from "../../hooks/useFetchIssues.tsx"; import { useFetchIssues } from "../../hooks/useFetchIssues.tsx";
import LazyLoad from "../../components/common/LazyLoad"; import LazyLoad from "../../components/common/LazyLoad";
import { import {
BottomParent, BottomParent,
FileContainer, IssueCard,
NameContainer, IssueContainer,
VideoCard, NameAndDateContainer,
VideoCardName, VideoCardName,
VideoCardTitle, VideoCardTitle,
VideoUploadDate, VideoUploadDate,
} from "./IssueList-styles.tsx"; } from "./IssueList-styles.tsx";
import { formatDate } from "../../utils/time"; import { formatDate } from "../../utils/time";
import { Video } from "../../state/features/fileSlice.ts"; import { Issue } from "../../state/features/fileSlice.ts";
import { queue } from "../../wrappers/GlobalWrapper"; import { queue } from "../../wrappers/GlobalWrapper";
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts"; import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
import { formatBytes } from "../IssueContent/IssueContent.tsx"; import { formatBytes } from "../IssueContent/IssueContent.tsx";
import { getIconsFromObject } from "../../constants/Categories/CategoryFunctions.ts"; import { getIconsFromObject } from "../../constants/Categories/CategoryFunctions.ts";
import { IssueIcon, IssueIcons } from "../../components/common/IssueIcon.tsx";
import QORTicon from "../../assets/icons/qort.png";
import { verifyAllPayments } from "../../constants/PublishFees/VerifyPayment.ts";
interface VideoListProps { interface VideoListProps {
mode?: string; mode?: string;
} }
export const FileListComponentLevel = ({ mode }: VideoListProps) => { export const IssueListComponentLevel = ({ mode }: VideoListProps) => {
const { name: paramName } = useParams(); const { name: paramName } = useParams();
const theme = useTheme(); const theme = useTheme();
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
@ -37,16 +39,17 @@ export const FileListComponentLevel = ({ mode }: VideoListProps) => {
(state: RootState) => state.file.hashMapFiles (state: RootState) => state.file.hashMapFiles
); );
const [videos, setVideos] = React.useState<Video[]>([]); const [issues, setIssues] = React.useState<Issue[]>([]);
const navigate = useNavigate(); const navigate = useNavigate();
const { getFile, getNewFiles, checkNewFiles, checkAndUpdateFile } = const { getIssue, getNewIssues, checkNewIssues, checkAndUpdateIssue } =
useFetchIssues(); useFetchIssues();
const getVideos = React.useCallback(async () => { const getIssues = React.useCallback(async () => {
try { try {
const offset = videos.length; const offset = issues.length;
const url = `/arbitrary/resources/search?mode=ALL&service=DOCUMENT&query=${QSUPPORT_FILE_BASE}_&limit=50&includemetadata=false&reverse=true&excludeblocked=true&name=${paramName}&exactmatchnames=true&offset=${offset}`; // `/arbitrary/resources/search?mode=ALL&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}&limit=${videoLimit}`;
const url = `/arbitrary/resources/search?mode=ALL&includemetadata=false&reverse=true&excludeblocked=true&exactmatchnames=true&offset=${offset}&limit=50&service=DOCUMENT&query=${QSUPPORT_FILE_BASE}_&name=${paramName}`;
const response = await fetch(url, { const response = await fetch(url, {
method: "GET", method: "GET",
headers: { headers: {
@ -55,63 +58,69 @@ export const FileListComponentLevel = ({ mode }: VideoListProps) => {
}); });
const responseData = await response.json(); const responseData = await response.json();
const structureData = responseData.map((video: any): Video => { const structureData = responseData.map((issue: any): Issue => {
return { return {
title: video?.metadata?.title, title: issue?.metadata?.title,
category: video?.metadata?.category, category: issue?.metadata?.category,
categoryName: video?.metadata?.categoryName, categoryName: issue?.metadata?.categoryName,
tags: video?.metadata?.tags || [], tags: issue?.metadata?.tags || [],
description: video?.metadata?.description, description: issue?.metadata?.description,
created: video?.created, created: issue?.created,
updated: video?.updated, updated: issue?.updated,
user: video.name, user: issue.name,
videoImage: "", videoImage: "",
id: video.identifier, id: issue.identifier,
}; };
}); });
const copiedVideos: Video[] = [...videos]; const copiedIssues: Issue[] = [...issues];
structureData.forEach((video: Video) => { structureData.forEach((issue: Issue) => {
const index = videos.findIndex(p => p.id === video.id); const index = issues.findIndex(p => p.id === issue.id);
if (index !== -1) { if (index !== -1) {
copiedVideos[index] = video; copiedIssues[index] = issue;
} else { } else {
copiedVideos.push(video); copiedIssues.push(issue);
} }
}); });
setVideos(copiedVideos);
for (const content of structureData) { const verifiedIssuePromises: Promise<Issue>[] = [];
for (const content of copiedIssues) {
if (content.user && content.id) { if (content.user && content.id) {
const res = checkAndUpdateFile(content); const res = checkAndUpdateIssue(content);
if (res) { const getIssueData = getIssue(content.user, content.id, content);
queue.push(() => getFile(content.user, content.id, content)); if (res) queue.push(() => getIssueData);
}
verifiedIssuePromises.push(getIssueData);
} }
} }
const issueData = await Promise.all(verifiedIssuePromises);
const verifiedIssues = await verifyAllPayments(issueData);
setIssues(verifiedIssues);
} catch (error) { } catch (error) {
} finally { } finally {
} }
}, [videos, hashMapVideos]); }, [issues, hashMapVideos]);
const getVideosHandler = React.useCallback(async () => { const getIssuesHandler = React.useCallback(async () => {
if (!firstFetch.current || !afterFetch.current) return; if (!firstFetch.current || !afterFetch.current) return;
await getVideos(); await getIssues();
}, [getVideos]); }, [getIssues]);
const getVideosHandlerMount = React.useCallback(async () => { const getIssuesHandlerMount = React.useCallback(async () => {
if (firstFetch.current) return; if (firstFetch.current) return;
firstFetch.current = true; firstFetch.current = true;
await getVideos(); await getIssues();
afterFetch.current = true; afterFetch.current = true;
setIsLoading(false); setIsLoading(false);
}, [getVideos]); }, [getIssues]);
useEffect(() => { useEffect(() => {
if (!firstFetch.current) { if (!firstFetch.current) {
getVideosHandlerMount(); getIssuesHandlerMount();
} }
}, [getVideosHandlerMount]); }, [getIssuesHandlerMount]);
return ( return (
<Box <Box
@ -122,18 +131,21 @@ export const FileListComponentLevel = ({ mode }: VideoListProps) => {
alignItems: "center", alignItems: "center",
}} }}
> >
<FileContainer> <IssueContainer>
{videos.map((file: any, index: number) => { {issues.map((issue: any, index: number) => {
const existingFile = hashMapVideos[file?.id]; const existingFile = hashMapVideos[issue?.id];
let hasHash = false; let hasHash = false;
let fileObj = file; let issueObj = issue;
if (existingFile) { if (existingFile) {
fileObj = existingFile; issueObj = existingFile;
hasHash = true; hasHash = true;
} }
const icon = getIconsFromObject(fileObj); const issueIcons = getIconsFromObject(issueObj);
const fileBytes = issueObj?.files.reduce(
(acc, cur) => acc + (cur?.size || 0),
0
);
return ( return (
<Box <Box
sx={{ sx={{
@ -143,13 +155,13 @@ export const FileListComponentLevel = ({ mode }: VideoListProps) => {
height: "75px", height: "75px",
position: "relative", position: "relative",
}} }}
key={fileObj.id} key={issueObj.id}
> >
{hasHash ? ( {hasHash ? (
<> <>
<VideoCard <IssueCard
onClick={() => { onClick={() => {
navigate(`/issue/${fileObj?.user}/${fileObj?.id}`); navigate(`/issue/${issueObj?.user}/${issueObj?.id}`);
}} }}
sx={{ sx={{
height: "100%", height: "100%",
@ -167,42 +179,49 @@ export const FileListComponentLevel = ({ mode }: VideoListProps) => {
alignItems: "center", alignItems: "center",
}} }}
> >
{icon ? ( <div
<img style={{
src={icon} display: "flex",
width="50px" alignItems: "center",
style={{ width: "200px",
borderRadius: "5px", }}
}} >
<IssueIcons
iconSources={issueIcons}
style={{ marginRight: "20px" }}
showBackupIcon={true}
/> />
) : ( </div>
<AttachFileIcon />
)}
<VideoCardTitle <VideoCardTitle
sx={{ sx={{
width: "100px", width: "100px",
}} }}
> >
{formatBytes( {fileBytes > 0 && formatBytes(fileBytes)}
fileObj?.files.reduce( </VideoCardTitle>
(acc, cur) => acc + (cur?.size || 0), <VideoCardTitle
0 sx={{ fontWeight: "bold", width: "500px" }}
) >
)} {issueObj.title}
</VideoCardTitle> </VideoCardTitle>
<VideoCardTitle>{fileObj.title}</VideoCardTitle>
</Box> </Box>
{issue?.feeData?.isPaid && (
<IssueIcon
iconSrc={QORTicon}
style={{ marginRight: "20px" }}
/>
)}
<BottomParent> <BottomParent>
<NameContainer <NameAndDateContainer
onClick={e => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
navigate(`/channel/${fileObj?.user}`); navigate(`/channel/${issueObj?.user}`);
}} }}
> >
<Avatar <Avatar
sx={{ height: 24, width: 24 }} sx={{ height: 24, width: 24 }}
src={`/arbitrary/THUMBNAIL/${fileObj?.user}/qortal_avatar`} src={`/arbitrary/THUMBNAIL/${issueObj?.user}/qortal_avatar`}
alt={`${fileObj?.user}'s avatar`} alt={`${issueObj?.user}'s avatar`}
/> />
<VideoCardName <VideoCardName
sx={{ sx={{
@ -211,17 +230,17 @@ export const FileListComponentLevel = ({ mode }: VideoListProps) => {
}, },
}} }}
> >
{fileObj?.user} {issueObj?.user}
</VideoCardName> </VideoCardName>
</NameContainer> </NameAndDateContainer>
{fileObj?.created && ( {issueObj?.created && (
<VideoUploadDate> <VideoUploadDate>
{formatDate(fileObj.created)} {formatDate(issueObj.created)}
</VideoUploadDate> </VideoUploadDate>
)} )}
</BottomParent> </BottomParent>
</VideoCard> </IssueCard>
</> </>
) : ( ) : (
<Skeleton <Skeleton
@ -239,8 +258,8 @@ export const FileListComponentLevel = ({ mode }: VideoListProps) => {
</Box> </Box>
); );
})} })}
</FileContainer> </IssueContainer>
<LazyLoad onLoadMore={getVideosHandler} isLoading={isLoading}></LazyLoad> <LazyLoad onLoadMore={getIssuesHandler} isLoading={isLoading}></LazyLoad>
</Box> </Box>
); );
}; };

View File

@ -1,5 +1,5 @@
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import { FileListComponentLevel } from "../Home/FileListComponentLevel.tsx"; import { IssueListComponentLevel } from "../Home/IssueListComponentLevel.tsx";
import { HeaderContainer, ProfileContainer } from "./Profile-styles"; import { HeaderContainer, ProfileContainer } from "./Profile-styles";
import { import {
AuthorTextComment, AuthorTextComment,
@ -62,7 +62,7 @@ export const IndividualProfile = () => {
</StyledCardHeaderComment> </StyledCardHeaderComment>
</Box> </Box>
</HeaderContainer> </HeaderContainer>
<FileListComponentLevel /> <IssueListComponentLevel />
</ProfileContainer> </ProfileContainer>
); );
}; };

View File

@ -1,16 +1,16 @@
import { styled } from "@mui/system"; import { styled } from "@mui/system";
import { Box, Grid, Typography, Checkbox } from "@mui/material"; import { Box, Grid, Typography, Checkbox } from "@mui/material";
export const ProfileContainer = styled(Box)(({ theme }) => ({ export const ProfileContainer = styled(Box)(({ theme }) => ({
position: "relative", position: "relative",
display: "flex", display: "flex",
width: "100%", width: "100%",
flexDirection: "column" flexDirection: "column"
})); }));
export const HeaderContainer = styled(Box)(({ theme }) => ({ export const HeaderContainer = styled(Box)(({ theme }) => ({
position: "relative", position: "relative",
display: "flex", display: "flex",
width: "100%", width: "100%",
justifyContent: "center" justifyContent: "center"
})); }));

View File

@ -5,7 +5,6 @@ import { setIsLoadingGlobal } from "../../state/features/globalSlice";
import { Avatar, Box, Typography, useTheme } from "@mui/material"; import { Avatar, Box, Typography, useTheme } from "@mui/material";
import { RootState } from "../../state/store"; import { RootState } from "../../state/store";
import { addToHashMap } from "../../state/features/fileSlice.ts"; import { addToHashMap } from "../../state/features/fileSlice.ts";
import AttachFileIcon from "@mui/icons-material/AttachFile";
import DownloadIcon from "@mui/icons-material/Download"; import DownloadIcon from "@mui/icons-material/Download";
import { import {
AuthorTextComment, AuthorTextComment,
@ -24,14 +23,20 @@ import { CommentSection } from "../../components/common/Comments/CommentSection"
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts"; import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml"; import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml";
import FileElement from "../../components/common/FileElement"; import FileElement from "../../components/common/FileElement";
import { allCategoryData } from "../../constants/Categories/1stCategories.ts"; import { allCategoryData } from "../../constants/Categories/Categories.ts";
import { import {
Category, Category,
getCategoriesFromObject, getCategoriesFromObject,
} from "../../components/common/CategoryList/CategoryList.tsx"; } from "../../components/common/CategoryList/CategoryList.tsx";
import { getIconsFromObject } from "../../constants/Categories/CategoryFunctions.ts"; import { getIconsFromObject } from "../../constants/Categories/CategoryFunctions.ts";
import { IssueIcon, IssueIcons } from "../../components/common/IssueIcon.tsx";
import QORTicon from "../../assets/icons/qort.png";
import {
appendIsPaidToFeeData,
verifyPayment,
} from "../../constants/PublishFees/VerifyPayment.ts";
export function formatBytes(bytes, decimals = 2) { export function formatBytes(bytes: number, decimals = 2) {
if (bytes === 0) return "0 Bytes"; if (bytes === 0) return "0 Bytes";
const k = 1024; const k = 1024;
@ -50,7 +55,7 @@ export const IssueContent = () => {
const [descriptionHeight, setDescriptionHeight] = useState<null | number>( const [descriptionHeight, setDescriptionHeight] = useState<null | number>(
null null
); );
const [icon, setIcon] = useState<string>(""); const [issueIcons, setIssueIcons] = useState<string[]>([]);
const userAvatarHash = useSelector( const userAvatarHash = useSelector(
(state: RootState) => state.global.userAvatarHash (state: RootState) => state.global.userAvatarHash
); );
@ -67,15 +72,15 @@ export const IssueContent = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const theme = useTheme(); const theme = useTheme();
const [fileData, setFileData] = useState<any>(null); const [issueData, setIssueData] = useState<any>(null);
const [playlistData, setPlaylistData] = useState<any>(null); const [playlistData, setPlaylistData] = useState<any>(null);
const hashMapVideos = useSelector( const hashMapVideos = useSelector(
(state: RootState) => state.file.hashMapFiles (state: RootState) => state.file.hashMapFiles
); );
const videoReference = useMemo(() => { const videoReference = useMemo(() => {
if (!fileData) return null; if (!issueData) return null;
const { videoReference } = fileData; const { videoReference } = issueData;
if ( if (
videoReference?.identifier && videoReference?.identifier &&
videoReference?.name && videoReference?.name &&
@ -85,13 +90,13 @@ export const IssueContent = () => {
} else { } else {
return null; return null;
} }
}, [fileData]); }, [issueData]);
const videoCover = useMemo(() => { const videoCover = useMemo(() => {
if (!fileData) return null; if (!issueData) return null;
const { videoImage } = fileData; const { videoImage } = issueData;
return videoImage || null; return videoImage || null;
}, [fileData]); }, [issueData]);
const dispatch = useDispatch(); const dispatch = useDispatch();
const getVideoData = React.useCallback(async (name: string, id: string) => { const getVideoData = React.useCallback(async (name: string, id: string) => {
@ -135,9 +140,16 @@ export const IssueContent = () => {
...resourceData, ...resourceData,
...responseData, ...responseData,
}; };
setFileData(combinedData);
dispatch(addToHashMap(combinedData)); verifyPayment(combinedData).then(feeData => {
checkforPlaylist(name, id, combinedData?.code); console.log(
"async data: ",
appendIsPaidToFeeData(combinedData, feeData)
);
setIssueData(appendIsPaidToFeeData(combinedData, feeData));
dispatch(addToHashMap(combinedData));
checkforPlaylist(name, id, combinedData?.code);
});
} }
} }
} catch (error) { } catch (error) {
@ -217,8 +229,10 @@ export const IssueContent = () => {
const existingVideo = hashMapVideos[id]; const existingVideo = hashMapVideos[id];
if (existingVideo) { if (existingVideo) {
setFileData(existingVideo); verifyPayment(existingVideo).then(feeData => {
checkforPlaylist(name, id, existingVideo?.code); setIssueData(appendIsPaidToFeeData(existingVideo, feeData));
checkforPlaylist(name, id, existingVideo?.code);
});
} else { } else {
getVideoData(name, id); getVideoData(name, id);
} }
@ -259,21 +273,21 @@ export const IssueContent = () => {
useEffect(() => { useEffect(() => {
if (contentRef.current) { if (contentRef.current) {
const height = contentRef.current.offsetHeight; const height = contentRef.current.offsetHeight;
if (height > 100) { const maxDescriptionHeight = 200;
if (height > maxDescriptionHeight) {
// Assuming 100px is your threshold // Assuming 100px is your threshold
setDescriptionHeight(100); setDescriptionHeight(maxDescriptionHeight);
} }
} }
if (fileData) { if (issueData) {
const icon = getIconsFromObject(fileData); const icons = getIconsFromObject(issueData);
setIcon(icon); setIssueIcons(icons);
} }
}, [fileData]); }, [issueData]);
const categoriesDisplay = useMemo(() => { const categoriesDisplay = useMemo(() => {
if (fileData) { if (issueData) {
const categoryList = getCategoriesFromObject(fileData); const categoryList = getCategoriesFromObject(issueData);
const categoryNames = categoryList.map((categoryID, index) => { const categoryNames = categoryList.map((categoryID, index) => {
let categoryName: Category; let categoryName: Category;
if (index === 0) { if (index === 0) {
@ -294,14 +308,21 @@ export const IssueContent = () => {
const filteredCategoryNames = categoryNames.filter(name => name); const filteredCategoryNames = categoryNames.filter(name => name);
let categoryDisplay = ""; let categoryDisplay = "";
const separator = " > "; const separator = " > ";
const QappName = issueData?.QappName || "";
filteredCategoryNames.map((name, index) => { filteredCategoryNames.map((name, index) => {
categoryDisplay += if (QappName && index === 1) {
index !== filteredCategoryNames.length - 1 ? name + separator : name; categoryDisplay += QappName + separator;
}
categoryDisplay += name;
if (index !== filteredCategoryNames.length - 1)
categoryDisplay += separator;
}); });
return categoryDisplay; return categoryDisplay;
} }
return "no videodata"; return "no videodata";
}, [fileData]); }, [issueData]);
return ( return (
<Box <Box
@ -325,18 +346,12 @@ export const IssueContent = () => {
alignItems: "center", alignItems: "center",
}} }}
> >
{icon ? ( <div style={{ display: "flex", alignItems: "center" }}>
<img <IssueIcons
src={icon} iconSources={issueIcons}
width="50px" style={{ marginRight: "20px" }}
style={{
borderRadius: "5px",
marginRight: "10px",
}}
/> />
) : ( </div>
<AttachFileIcon />
)}
<FileTitle <FileTitle
variant="h1" variant="h1"
color="textPrimary" color="textPrimary"
@ -344,10 +359,13 @@ export const IssueContent = () => {
textAlign: "center", textAlign: "center",
}} }}
> >
{fileData?.title} {issueData?.title}
</FileTitle> </FileTitle>
{issueData?.feeData?.isPaid && (
<IssueIcon iconSrc={QORTicon} style={{ marginLeft: "10px" }} />
)}
</div> </div>
{fileData?.created && ( {issueData?.created && (
<Typography <Typography
variant="h6" variant="h6"
sx={{ sx={{
@ -355,7 +373,7 @@ export const IssueContent = () => {
}} }}
color={theme.palette.text.primary} color={theme.palette.text.primary}
> >
{formatDate(fileData.created)} {formatDate(issueData.created)}
</Typography> </Typography>
)} )}
@ -407,12 +425,13 @@ export const IssueContent = () => {
</Typography> </Typography>
</Box> </Box>
<ImageContainer> <ImageContainer>
{fileData?.images && {issueData?.images &&
fileData.images.map(image => { issueData.images.map(image => {
return ( return (
<img <img
key={image}
src={image} src={image}
width={`${1080 / fileData.images.length}px`} width={`${1080 / issueData.images.length}px`}
style={{ style={{
marginRight: "10px", marginRight: "10px",
marginBottom: "10px", marginBottom: "10px",
@ -464,12 +483,12 @@ export const IssueContent = () => {
? "auto" ? "auto"
: isExpandedDescription : isExpandedDescription
? "auto" ? "auto"
: "100px", : "30vh",
overflow: "hidden", overflow: "hidden",
}} }}
> >
{fileData?.htmlDescription ? ( {issueData?.htmlDescription ? (
<DisplayHtml html={fileData?.htmlDescription} /> <DisplayHtml html={issueData?.htmlDescription} />
) : ( ) : (
<FileDescription <FileDescription
variant="body1" variant="body1"
@ -478,7 +497,7 @@ export const IssueContent = () => {
cursor: "default", cursor: "default",
}} }}
> >
{fileData?.fullDescription} {issueData?.fullDescription}
</FileDescription> </FileDescription>
)} )}
</Box> </Box>
@ -509,7 +528,7 @@ export const IssueContent = () => {
marginTop: "25px", marginTop: "25px",
}} }}
> >
{fileData?.files?.map((file, index) => { {issueData?.files?.map((file, index) => {
return ( return (
<FileAttachmentContainer <FileAttachmentContainer
sx={{ sx={{

View File

@ -1,27 +1,27 @@
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
interface AuthState { interface AuthState {
user: { user: {
address: string; address: string;
publicKey: string; publicKey: string;
name?: string; name?: string;
} | null; } | null;
} }
const initialState: AuthState = { const initialState: AuthState = {
user: null user: null
}; };
export const authSlice = createSlice({ export const authSlice = createSlice({
name: 'auth', name: 'auth',
initialState, initialState,
reducers: { reducers: {
addUser: (state, action) => { addUser: (state, action) => {
state.user = action.payload; state.user = action.payload;
}, },
}, },
}); });
export const { addUser } = authSlice.actions; export const { addUser } = authSlice.actions;
export default authSlice.reducer; export default authSlice.reducer;

View File

@ -1,10 +1,10 @@
import { createSlice } from "@reduxjs/toolkit"; import { createSlice } from "@reduxjs/toolkit";
import { RootState } from "../store"; import { PublishFeeData } from "../../constants/PublishFees/SendFeeFunctions.ts";
interface GlobalState { interface GlobalState {
files: Video[]; files: Issue[];
filteredFiles: Video[]; filteredFiles: Issue[];
hashMapFiles: Record<string, Video>; hashMapFiles: Record<string, Issue>;
countNewFiles: number; countNewFiles: number;
isFiltering: boolean; isFiltering: boolean;
filterValue: string; filterValue: string;
@ -14,6 +14,7 @@ interface GlobalState {
selectedCategoryFiles: any[]; selectedCategoryFiles: any[];
editFileProperties: any; editFileProperties: any;
editPlaylistProperties: any; editPlaylistProperties: any;
publishedQappNames: string[];
} }
const initialState: GlobalState = { const initialState: GlobalState = {
files: [], files: [],
@ -28,9 +29,10 @@ const initialState: GlobalState = {
selectedCategoryFiles: [null, null, null, null], selectedCategoryFiles: [null, null, null, null],
editFileProperties: null, editFileProperties: null,
editPlaylistProperties: null, editPlaylistProperties: null,
publishedQappNames: [],
}; };
export interface Video { export interface Issue {
title: string; title: string;
description: string; description: string;
created: number | string; created: number | string;
@ -44,6 +46,8 @@ export interface Video {
updated?: number | string; updated?: number | string;
isValid?: boolean; isValid?: boolean;
code?: string; code?: string;
feeData?: PublishFeeData;
paymentVerified?: boolean;
} }
export const fileSlice = createSlice({ export const fileSlice = createSlice({
@ -113,12 +117,12 @@ export const fileSlice = createSlice({
}, },
addArrayToHashMap: (state, action) => { addArrayToHashMap: (state, action) => {
const videos = action.payload; const videos = action.payload;
videos.forEach((video: Video) => { videos.forEach((video: Issue) => {
state.hashMapFiles[video.id] = video; state.hashMapFiles[video.id] = video;
}); });
}, },
upsertFiles: (state, action) => { upsertFiles: (state, action) => {
action.payload.forEach((video: Video) => { action.payload.forEach((video: Issue) => {
const index = state.files.findIndex(p => p.id === video.id); const index = state.files.findIndex(p => p.id === video.id);
if (index !== -1) { if (index !== -1) {
state.files[index] = video; state.files[index] = video;
@ -128,7 +132,7 @@ export const fileSlice = createSlice({
}); });
}, },
upsertFilteredFiles: (state, action) => { upsertFilteredFiles: (state, action) => {
action.payload.forEach((video: Video) => { action.payload.forEach((video: Issue) => {
const index = state.filteredFiles.findIndex(p => p.id === video.id); const index = state.filteredFiles.findIndex(p => p.id === video.id);
if (index !== -1) { if (index !== -1) {
state.filteredFiles[index] = video; state.filteredFiles[index] = video;
@ -138,7 +142,7 @@ export const fileSlice = createSlice({
}); });
}, },
upsertFilesBeginning: (state, action) => { upsertFilesBeginning: (state, action) => {
action.payload.reverse().forEach((video: Video) => { action.payload.reverse().forEach((video: Issue) => {
const index = state.files.findIndex(p => p.id === video.id); const index = state.files.findIndex(p => p.id === video.id);
if (index !== -1) { if (index !== -1) {
state.files[index] = video; state.files[index] = video;
@ -157,6 +161,9 @@ export const fileSlice = createSlice({
const username = action.payload; const username = action.payload;
state.files = state.files.filter(item => item.user !== username); state.files = state.files.filter(item => item.user !== username);
}, },
setQappNames: (state, action) => {
state.publishedQappNames = action.payload;
},
}, },
}); });
@ -183,6 +190,7 @@ export const {
blockUser, blockUser,
setEditFile, setEditFile,
setEditPlaylist, setEditPlaylist,
setQappNames,
} = fileSlice.actions; } = fileSlice.actions;
export default fileSlice.reducer; export default fileSlice.reducer;

View File

@ -1,4 +1,5 @@
import { createSlice } from "@reduxjs/toolkit"; import { createSlice } from "@reduxjs/toolkit";
import { FeePrice } from "../../constants/PublishFees/FeePricePublish/FeePricePublish.ts";
interface GlobalState { interface GlobalState {
isLoadingGlobal: boolean; isLoadingGlobal: boolean;
@ -9,6 +10,7 @@ interface GlobalState {
totalFilesPublished: number; totalFilesPublished: number;
totalNamesPublished: number; totalNamesPublished: number;
filesPerNamePublished: number; filesPerNamePublished: number;
feeData: FeePrice[];
} }
const initialState: GlobalState = { const initialState: GlobalState = {
isLoadingGlobal: false, isLoadingGlobal: false,
@ -19,6 +21,7 @@ const initialState: GlobalState = {
totalFilesPublished: null, totalFilesPublished: null,
totalNamesPublished: null, totalNamesPublished: null,
filesPerNamePublished: null, filesPerNamePublished: null,
feeData: [],
}; };
export const globalSlice = createSlice({ export const globalSlice = createSlice({
@ -61,6 +64,9 @@ export const globalSlice = createSlice({
setFilesPerNamePublished: (state, action) => { setFilesPerNamePublished: (state, action) => {
state.filesPerNamePublished = action.payload; state.filesPerNamePublished = action.payload;
}, },
setFeeData: (state, action) => {
state.feeData = action.payload;
},
}, },
}); });
@ -74,6 +80,7 @@ export const {
setTotalFilesPublished, setTotalFilesPublished,
setTotalNamesPublished, setTotalNamesPublished,
setFilesPerNamePublished, setFilesPerNamePublished,
setFeeData,
} = globalSlice.actions; } = globalSlice.actions;
export default globalSlice.reducer; export default globalSlice.reducer;

View File

@ -1,73 +1,73 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface AlertTypes { interface AlertTypes {
alertSuccess: string alertSuccess: string
alertError: string alertError: string
alertInfo: string alertInfo: string
} }
interface InitialState { interface InitialState {
alertTypes: AlertTypes alertTypes: AlertTypes
} }
const initialState: InitialState = { const initialState: InitialState = {
alertTypes: { alertTypes: {
alertSuccess: '', alertSuccess: '',
alertError: '', alertError: '',
alertInfo: '' alertInfo: ''
} }
} }
export const notificationsSlice = createSlice({ export const notificationsSlice = createSlice({
name: "notifications", name: "notifications",
initialState, initialState,
reducers: { reducers: {
setNotification: ( setNotification: (
state: InitialState, state: InitialState,
action: PayloadAction<{ alertType: string; msg: string }> action: PayloadAction<{ alertType: string; msg: string }>
) => { ) => {
if (action.payload.alertType === "success") { if (action.payload.alertType === "success") {
return { return {
...state, ...state,
alertTypes: { alertTypes: {
...state.alertTypes, ...state.alertTypes,
alertSuccess: action.payload.msg, alertSuccess: action.payload.msg,
}, },
}; };
} else if (action.payload.alertType === "error") { } else if (action.payload.alertType === "error") {
return { return {
...state, ...state,
alertTypes: { alertTypes: {
...state.alertTypes, ...state.alertTypes,
alertError: action.payload.msg, alertError: action.payload.msg,
}, },
}; };
} else if (action.payload.alertType === "info") { } else if (action.payload.alertType === "info") {
return { return {
...state, ...state,
alertTypes: { alertTypes: {
...state.alertTypes, ...state.alertTypes,
alertInfo: action.payload.msg, alertInfo: action.payload.msg,
}, },
}; };
} }
return state; return state;
}, },
removeNotification: (state: InitialState) => { removeNotification: (state: InitialState) => {
return { return {
...state, ...state,
alertTypes: { alertTypes: {
...state.alertTypes, ...state.alertTypes,
alertSuccess: '', alertSuccess: '',
alertError: '', alertError: '',
alertInfo: '' alertInfo: ''
} }
} }
}, },
}, },
}); });
export const { setNotification, removeNotification } = export const { setNotification, removeNotification } =
notificationsSlice.actions; notificationsSlice.actions;
export default notificationsSlice.reducer; export default notificationsSlice.reducer;

View File

@ -91,7 +91,7 @@ const lightTheme = createTheme({
mode: "light", mode: "light",
primary: { primary: {
main: "#FCFCFC", main: "#FCFCFC",
dark: "#F5F5F5", dark: "#E0E0E0",
light: "#FFFFFF", light: "#FFFFFF",
}, },
secondary: { secondary: {
@ -138,14 +138,14 @@ const darkTheme = createTheme({
palette: { palette: {
mode: "dark", mode: "dark",
primary: { primary: {
main: "#01a9e9", // main: "#01a9e9", // Qortal Blue
dark: "#008fcd", // dark: "#008fcd",
light: "#44c4ff", // light: "#44c4ff",
}, },
secondary: { secondary: {
main: "#007FFF", // Electric blue main: "#007FFF", // Electric blue
dark: "#0059B2", // Darker shade of electric blue dark: "#0059B2",
light: "#3399FF", // Lighter shade of electric blue light: "#3399FF",
}, },
background: { background: {
default: "#1C1C1C", // Deep space black default: "#1C1C1C", // Deep space black

View File

@ -1,7 +1,7 @@
export const checkStructure = (content: any) => { export const checkStructure = (content: any) => {
let isValid = true let isValid = true
return isValid return isValid
} }

View File

@ -1,14 +1,14 @@
export function extractTextFromSlate(nodes: any) { export function extractTextFromSlate(nodes: any) {
if(!Array.isArray(nodes)) return "" if(!Array.isArray(nodes)) return ""
let text = ""; let text = "";
for (const node of nodes) { for (const node of nodes) {
if (node.text) { if (node.text) {
text += node.text; text += node.text;
} else if (node.children) { } else if (node.children) {
text += extractTextFromSlate(node.children); text += extractTextFromSlate(node.children);
} }
} }
return text; return text;
} }

View File

@ -17,6 +17,7 @@ export const fetchAndEvaluateIssues = async (data: any) => {
service: content?.service || "DOCUMENT", service: content?.service || "DOCUMENT",
identifier: videoId, identifier: videoId,
}); });
if (checkStructure(responseData)) { if (checkStructure(responseData)) {
obj = { obj = {
...content, ...content,

View File

@ -0,0 +1,33 @@
import { NameData } from "../constants/PublishFees/SendFeeFunctions.ts";
import { getUserAccountNames } from "../constants/PublishFees/VerifyPayment-Functions.ts";
export const getNameData = async (name: string) => {
return (await qortalRequest({
action: "GET_NAME_DATA",
name: name,
})) as NameData;
};
export const sendQchatDM = async (
recipientName: string,
message: string,
allowSelfAsRecipient = false
) => {
if (!allowSelfAsRecipient) {
const userAccountNames = await getUserAccountNames();
const userNames = userAccountNames.map(name => name.name);
if (userNames.includes(recipientName)) return;
}
const address = await getNameData(recipientName);
try {
return await qortalRequest({
action: "SEND_CHAT_MESSAGE",
destinationAddress: address.owner,
message,
});
} catch (e) {
console.log(e);
return false;
}
};

View File

@ -1,43 +1,43 @@
type QueueItem = { type QueueItem = {
request: () => Promise<any>; request: () => Promise<any>;
resolve: (value: any | PromiseLike<any>) => void; resolve: (value: any | PromiseLike<any>) => void;
reject: (reason?: any) => void; reject: (reason?: any) => void;
}; };
export class RequestQueue { export class RequestQueue {
private queue: QueueItem[]; private queue: QueueItem[];
private maxConcurrent: number; private maxConcurrent: number;
private currentConcurrent: number; private currentConcurrent: number;
constructor(maxConcurrent = 5) { constructor(maxConcurrent = 5) {
this.queue = []; this.queue = [];
this.maxConcurrent = maxConcurrent; this.maxConcurrent = maxConcurrent;
this.currentConcurrent = 0; this.currentConcurrent = 0;
} }
async push(request: () => Promise<any>): Promise<any> { async push(request: () => Promise<any>): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this.queue.push({ this.queue.push({
request, request,
resolve, resolve,
reject, reject,
}); });
this.checkQueue(); this.checkQueue();
}); });
} }
private checkQueue(): void { private checkQueue(): void {
if (this.queue.length === 0 || this.currentConcurrent >= this.maxConcurrent) return; if (this.queue.length === 0 || this.currentConcurrent >= this.maxConcurrent) return;
const { request, resolve, reject } = this.queue.shift() as QueueItem; const { request, resolve, reject } = this.queue.shift() as QueueItem;
this.currentConcurrent++; this.currentConcurrent++;
request() request()
.then(resolve) .then(resolve)
.catch(reject) .catch(reject)
.finally(() => { .finally(() => {
this.currentConcurrent--; this.currentConcurrent--;
this.checkQueue(); this.checkQueue();
}); });
} }
} }

View File

@ -1,46 +1,46 @@
import moment from 'moment' import moment from 'moment'
export function formatTimestamp(timestamp: number): string { export function formatTimestamp(timestamp: number): string {
const now = moment() const now = moment()
const timestampMoment = moment(timestamp) const timestampMoment = moment(timestamp)
const elapsedTime = now.diff(timestampMoment, 'minutes') const elapsedTime = now.diff(timestampMoment, 'minutes')
if (elapsedTime < 1) { if (elapsedTime < 1) {
return 'Just now' return 'Just now'
} else if (elapsedTime < 60) { } else if (elapsedTime < 60) {
return `${elapsedTime}m` return `${elapsedTime}m`
} else if (elapsedTime < 1440) { } else if (elapsedTime < 1440) {
return `${Math.floor(elapsedTime / 60)}h` return `${Math.floor(elapsedTime / 60)}h`
} else { } else {
return timestampMoment.format('MMM D') return timestampMoment.format('MMM D')
} }
} }
export function formatTimestampSeconds(timestamp: number): string { export function formatTimestampSeconds(timestamp: number): string {
const now = moment() const now = moment()
const timestampMoment = moment.unix(timestamp) const timestampMoment = moment.unix(timestamp)
const elapsedTime = now.diff(timestampMoment, 'minutes') const elapsedTime = now.diff(timestampMoment, 'minutes')
if (elapsedTime < 1) { if (elapsedTime < 1) {
return 'Just now' return 'Just now'
} else if (elapsedTime < 60) { } else if (elapsedTime < 60) {
return `${elapsedTime}m` return `${elapsedTime}m`
} else if (elapsedTime < 1440) { } else if (elapsedTime < 1440) {
return `${Math.floor(elapsedTime / 60)}h` return `${Math.floor(elapsedTime / 60)}h`
} else { } else {
return timestampMoment.format('MMM D') return timestampMoment.format('MMM D')
} }
} }
export const formatDate = (unixTimestamp: number): string => { export const formatDate = (unixTimestamp: number): string => {
const date = moment(unixTimestamp, 'x').fromNow() const date = moment(unixTimestamp, 'x').fromNow()
return date return date
} }
export const formatDateSeconds = (unixTimestamp: number): string => { export const formatDateSeconds = (unixTimestamp: number): string => {
const date = moment.unix(unixTimestamp).fromNow(); const date = moment.unix(unixTimestamp).fromNow();
return date return date
} }

View File

@ -1,174 +1,174 @@
export const toBase64 = (file: File): Promise<string | ArrayBuffer | null> => export const toBase64 = (file: File): Promise<string | ArrayBuffer | null> =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const reader = new FileReader() const reader = new FileReader()
reader.readAsDataURL(file) reader.readAsDataURL(file)
reader.onload = () => { reader.onload = () => {
const result = reader.result const result = reader.result
reader.onload = null // remove onload handler reader.onload = null // remove onload handler
reader.onerror = null // remove onerror handler reader.onerror = null // remove onerror handler
resolve(result) resolve(result)
} }
reader.onerror = (error) => { reader.onerror = (error) => {
reader.onload = null // remove onload handler reader.onload = null // remove onload handler
reader.onerror = null // remove onerror handler reader.onerror = null // remove onerror handler
reject(error) reject(error)
} }
}) })
export function objectToBase64(obj: any) { export function objectToBase64(obj: any) {
// Step 1: Convert the object to a JSON string // Step 1: Convert the object to a JSON string
const jsonString = JSON.stringify(obj) const jsonString = JSON.stringify(obj)
// Step 2: Create a Blob from the JSON string // Step 2: Create a Blob from the JSON string
const blob = new Blob([jsonString], { type: 'application/json' }) const blob = new Blob([jsonString], { type: 'application/json' })
// Step 3: Create a FileReader to read the Blob as a base64-encoded string // Step 3: Create a FileReader to read the Blob as a base64-encoded string
return new Promise<string>((resolve, reject) => { return new Promise<string>((resolve, reject) => {
const reader = new FileReader() const reader = new FileReader()
reader.onloadend = () => { reader.onloadend = () => {
if (typeof reader.result === 'string') { if (typeof reader.result === 'string') {
// Remove 'data:application/json;base64,' prefix // Remove 'data:application/json;base64,' prefix
const base64 = reader.result.replace( const base64 = reader.result.replace(
'data:application/json;base64,', 'data:application/json;base64,',
'' ''
) )
resolve(base64) resolve(base64)
} else { } else {
reject(new Error('Failed to read the Blob as a base64-encoded string')) reject(new Error('Failed to read the Blob as a base64-encoded string'))
} }
} }
reader.onerror = () => { reader.onerror = () => {
reject(reader.error) reject(reader.error)
} }
reader.readAsDataURL(blob) reader.readAsDataURL(blob)
}) })
} }
export function objectToUint8Array(obj: any) { export function objectToUint8Array(obj: any) {
// Convert the object to a JSON string // Convert the object to a JSON string
const jsonString = JSON.stringify(obj) const jsonString = JSON.stringify(obj)
// Encode the JSON string as a byte array using TextEncoder // Encode the JSON string as a byte array using TextEncoder
const encoder = new TextEncoder() const encoder = new TextEncoder()
const byteArray = encoder.encode(jsonString) const byteArray = encoder.encode(jsonString)
// Create a new Uint8Array and set its content to the encoded byte array // Create a new Uint8Array and set its content to the encoded byte array
const uint8Array = new Uint8Array(byteArray) const uint8Array = new Uint8Array(byteArray)
return uint8Array return uint8Array
} }
export function uint8ArrayToBase64(uint8Array: Uint8Array): string { export function uint8ArrayToBase64(uint8Array: Uint8Array): string {
const length = uint8Array.length const length = uint8Array.length
let binaryString = '' let binaryString = ''
const chunkSize = 1024 * 1024 // Process 1MB at a time const chunkSize = 1024 * 1024 // Process 1MB at a time
for (let i = 0; i < length; i += chunkSize) { for (let i = 0; i < length; i += chunkSize) {
const chunkEnd = Math.min(i + chunkSize, length) const chunkEnd = Math.min(i + chunkSize, length)
const chunk = uint8Array.subarray(i, chunkEnd) const chunk = uint8Array.subarray(i, chunkEnd)
binaryString += Array.from(chunk, (byte) => String.fromCharCode(byte)).join( binaryString += Array.from(chunk, (byte) => String.fromCharCode(byte)).join(
'' ''
) )
} }
return btoa(binaryString) return btoa(binaryString)
} }
export function objectToUint8ArrayFromResponse(obj: any) { export function objectToUint8ArrayFromResponse(obj: any) {
const len = Object.keys(obj).length const len = Object.keys(obj).length
const result = new Uint8Array(len) const result = new Uint8Array(len)
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
result[i] = obj[i] result[i] = obj[i]
} }
return result return result
} }
// export function uint8ArrayToBase64(arrayBuffer: Uint8Array): string { // export function uint8ArrayToBase64(arrayBuffer: Uint8Array): string {
// let binary = '' // let binary = ''
// const bytes = new Uint8Array(arrayBuffer) // const bytes = new Uint8Array(arrayBuffer)
// const len = bytes.length // const len = bytes.length
// for (let i = 0; i < len; i++) { // for (let i = 0; i < len; i++) {
// binary += String.fromCharCode(bytes[i]) // binary += String.fromCharCode(bytes[i])
// } // }
// return btoa(binary) // return btoa(binary)
// } // }
export function base64ToUint8Array(base64: string) { export function base64ToUint8Array(base64: string) {
const binaryString = atob(base64) const binaryString = atob(base64)
const len = binaryString.length const len = binaryString.length
const bytes = new Uint8Array(len) const bytes = new Uint8Array(len)
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i) bytes[i] = binaryString.charCodeAt(i)
} }
return bytes return bytes
} }
export function uint8ArrayToObject(uint8Array: Uint8Array) { export function uint8ArrayToObject(uint8Array: Uint8Array) {
// Decode the byte array using TextDecoder // Decode the byte array using TextDecoder
const decoder = new TextDecoder() const decoder = new TextDecoder()
const jsonString = decoder.decode(uint8Array) const jsonString = decoder.decode(uint8Array)
// Convert the JSON string back into an object // Convert the JSON string back into an object
const obj = JSON.parse(jsonString) const obj = JSON.parse(jsonString)
return obj return obj
} }
export function processFileInChunks(file: File): Promise<Uint8Array> { export function processFileInChunks(file: File): Promise<Uint8Array> {
return new Promise( return new Promise(
(resolve: (value: Uint8Array) => void, reject: (reason?: any) => void) => { (resolve: (value: Uint8Array) => void, reject: (reason?: any) => void) => {
const reader = new FileReader() const reader = new FileReader()
reader.onload = function (event: ProgressEvent<FileReader>) { reader.onload = function (event: ProgressEvent<FileReader>) {
const arrayBuffer = event.target?.result as ArrayBuffer const arrayBuffer = event.target?.result as ArrayBuffer
const uint8Array = new Uint8Array(arrayBuffer) const uint8Array = new Uint8Array(arrayBuffer)
resolve(uint8Array) resolve(uint8Array)
} }
reader.onerror = function (error: ProgressEvent<FileReader>) { reader.onerror = function (error: ProgressEvent<FileReader>) {
reject(error) reject(error)
} }
reader.readAsArrayBuffer(file) reader.readAsArrayBuffer(file)
} }
) )
} }
// export async function processFileInChunks(file: File, chunkSize = 1024 * 1024): Promise<Uint8Array> { // export async function processFileInChunks(file: File, chunkSize = 1024 * 1024): Promise<Uint8Array> {
// const fileStream = file.stream(); // const fileStream = file.stream();
// const reader = fileStream.getReader(); // const reader = fileStream.getReader();
// const totalLength = file.size; // const totalLength = file.size;
// if (totalLength <= 0 || isNaN(totalLength)) { // if (totalLength <= 0 || isNaN(totalLength)) {
// throw new Error('Invalid file size'); // throw new Error('Invalid file size');
// } // }
// const combinedArray = new Uint8Array(totalLength); // const combinedArray = new Uint8Array(totalLength);
// let offset = 0; // let offset = 0;
// while (offset < totalLength) { // while (offset < totalLength) {
// const { value, done } = await reader.read(); // const { value, done } = await reader.read();
// if (done) { // if (done) {
// break; // break;
// } // }
// const chunk = new Uint8Array(value.buffer, value.byteOffset, value.byteLength); // const chunk = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
// // Set elements one by one instead of using combinedArray.set(chunk, offset) // // Set elements one by one instead of using combinedArray.set(chunk, offset)
// for (let i = 0; i < chunk.length; i++) { // for (let i = 0; i < chunk.length; i++) {
// combinedArray[offset + i] = chunk[i]; // combinedArray[offset + i] = chunk[i];
// } // }
// offset += chunk.length; // offset += chunk.length;
// } // }
// return combinedArray; // return combinedArray;
// } // }

2
src/vite-env.d.ts vendored
View File

@ -1 +1 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />

View File

@ -1,213 +1,213 @@
import React from 'react' import React from 'react'
import { useDispatch, useSelector } from 'react-redux' import { useDispatch, useSelector } from 'react-redux'
import { import {
setAddToDownloads, setAddToDownloads,
updateDownloads updateDownloads
} from '../state/features/globalSlice' } from '../state/features/globalSlice'
import { DownloadTaskManager } from '../components/common/DownloadTaskManager' import { DownloadTaskManager } from '../components/common/DownloadTaskManager'
import { RootState } from '../state/store' import { RootState } from '../state/store'
interface Props { interface Props {
children: React.ReactNode children: React.ReactNode
} }
const defaultValues: MyContextInterface = { const defaultValues: MyContextInterface = {
downloadVideo: () => {} downloadVideo: () => {}
} }
interface IDownloadVideoParams { interface IDownloadVideoParams {
name: string name: string
service: string service: string
identifier: string identifier: string
properties: any properties: any
} }
interface MyContextInterface { interface MyContextInterface {
downloadVideo: ({ downloadVideo: ({
name, name,
service, service,
identifier, identifier,
properties properties
}: IDownloadVideoParams) => void }: IDownloadVideoParams) => void
} }
export const MyContext = React.createContext<MyContextInterface>(defaultValues) export const MyContext = React.createContext<MyContextInterface>(defaultValues)
const DownloadWrapper: React.FC<Props> = ({ children }) => { const DownloadWrapper: React.FC<Props> = ({ children }) => {
const dispatch = useDispatch() const dispatch = useDispatch()
const downloads = useSelector((state: RootState) => state.global?.downloads); const downloads = useSelector((state: RootState) => state.global?.downloads);
const fetchResource = async ({ name, service, identifier }: any) => { const fetchResource = async ({ name, service, identifier }: any) => {
try { try {
await qortalRequest({ await qortalRequest({
action: 'GET_QDN_RESOURCE_PROPERTIES', action: 'GET_QDN_RESOURCE_PROPERTIES',
name, name,
service, service,
identifier identifier
}) })
} catch (error) {} } catch (error) {}
} }
const fetchVideoUrl = async ({ name, service, identifier }: any) => { const fetchVideoUrl = async ({ name, service, identifier }: any) => {
try { try {
fetchResource({ name, service, identifier }) fetchResource({ name, service, identifier })
let url = await qortalRequest({ let url = await qortalRequest({
action: 'GET_QDN_RESOURCE_URL', action: 'GET_QDN_RESOURCE_URL',
service: service, service: service,
name: name, name: name,
identifier: identifier identifier: identifier
}) })
if (url) { if (url) {
dispatch( dispatch(
updateDownloads({ updateDownloads({
name, name,
service, service,
identifier, identifier,
url url
}) })
) )
} }
} catch (error) {} } catch (error) {}
} }
const performDownload = ({ const performDownload = ({
name, name,
service, service,
identifier, identifier,
properties properties
}: IDownloadVideoParams) => { }: IDownloadVideoParams) => {
if(downloads[identifier]) return if(downloads[identifier]) return
dispatch( dispatch(
setAddToDownloads({ setAddToDownloads({
name, name,
service, service,
identifier, identifier,
properties properties
}) })
) )
let isCalling = false let isCalling = false
let percentLoaded = 0 let percentLoaded = 0
let timer = 24 let timer = 24
const intervalId = setInterval(async () => { const intervalId = setInterval(async () => {
if (isCalling) return if (isCalling) return
isCalling = true isCalling = true
const res = await qortalRequest({ const res = await qortalRequest({
action: 'GET_QDN_RESOURCE_STATUS', action: 'GET_QDN_RESOURCE_STATUS',
name: name, name: name,
service: service, service: service,
identifier: identifier identifier: identifier
}) })
if(res?.status === 'NOT_PUBLISHED'){ if(res?.status === 'NOT_PUBLISHED'){
dispatch( dispatch(
updateDownloads({ updateDownloads({
name, name,
service, service,
identifier, identifier,
status: res status: res
}) })
) )
clearInterval(intervalId) clearInterval(intervalId)
} }
isCalling = false isCalling = false
if (res.localChunkCount) { if (res.localChunkCount) {
if (res.percentLoaded) { if (res.percentLoaded) {
if ( if (
res.percentLoaded === percentLoaded && res.percentLoaded === percentLoaded &&
res.percentLoaded !== 100 res.percentLoaded !== 100
) { ) {
timer = timer - 5 timer = timer - 5
} else { } else {
timer = 24 timer = 24
} }
if (timer < 0) { if (timer < 0) {
timer = 24 timer = 24
isCalling = true isCalling = true
dispatch( dispatch(
updateDownloads({ updateDownloads({
name, name,
service, service,
identifier, identifier,
status: { status: {
...res, ...res,
status: 'REFETCHING' status: 'REFETCHING'
} }
}) })
) )
setTimeout(() => { setTimeout(() => {
isCalling = false isCalling = false
fetchResource({ fetchResource({
name, name,
service, service,
identifier identifier
}) })
}, 25000) }, 25000)
return return
} }
percentLoaded = res.percentLoaded percentLoaded = res.percentLoaded
} }
dispatch( dispatch(
updateDownloads({ updateDownloads({
name, name,
service, service,
identifier, identifier,
status: res status: res
}) })
) )
} }
// check if progress is 100% and clear interval if true // check if progress is 100% and clear interval if true
if (res?.status === 'READY') { if (res?.status === 'READY') {
clearInterval(intervalId) clearInterval(intervalId)
dispatch( dispatch(
updateDownloads({ updateDownloads({
name, name,
service, service,
identifier, identifier,
status: res status: res
}) })
) )
} }
}, 5000) // 1 second interval }, 5000) // 1 second interval
fetchVideoUrl({ fetchVideoUrl({
name, name,
service, service,
identifier identifier
}) })
} }
const downloadVideo = async ({ const downloadVideo = async ({
name, name,
service, service,
identifier, identifier,
properties properties
}: IDownloadVideoParams) => { }: IDownloadVideoParams) => {
try { try {
performDownload({ performDownload({
name, name,
service, service,
identifier, identifier,
properties properties
}) })
return 'addedToList' return 'addedToList'
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }
} }
return ( return (
<> <>
<MyContext.Provider value={{ downloadVideo }}> <MyContext.Provider value={{ downloadVideo }}>
{/* <DownloadTaskManager /> */} {/* <DownloadTaskManager /> */}
{children} {children}
</MyContext.Provider> </MyContext.Provider>
</> </>
) )
} }
export default DownloadWrapper export default DownloadWrapper

View File

@ -1,26 +1,26 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext"], "lib": ["DOM", "DOM.Iterable", "ESNext"],
"module": "ESNext", "module": "ESNext",
"skipLibCheck": true, "skipLibCheck": true,
"noImplicitAny": false, "noImplicitAny": false,
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
/* Linting */ /* Linting */
"strict": false, "strict": false,
"noUnusedLocals": false, "noUnusedLocals": false,
"noUnusedParameters": false, "noUnusedParameters": false,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"strictNullChecks": false, "strictNullChecks": false,
}, },
"include": ["src"], "include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]
} }

View File

@ -1,10 +1,10 @@
{ {
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,
"skipLibCheck": true, "skipLibCheck": true,
"module": "ESNext", "module": "ESNext",
"moduleResolution": "bundler", "moduleResolution": "bundler",
"allowSyntheticDefaultImports": true "allowSyntheticDefaultImports": true
}, },
"include": ["vite.config.ts"] "include": ["vite.config.ts"]
} }

View File

@ -1,8 +1,8 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
base: "" base: ""
}) })