mirror of
https://github.com/Qortal/q-support.git
synced 2025-02-11 17:55:50 +00:00
Added features from Q-Support 1.1.0 posted on Q-Share:
Bounties can be added to an Issue. They can be either a direct payment from the publisher in ANY supported coin, or they can be a link to a Q-Fund. Q-Funds that are still in progress have a donate button so users can support it without having to leave Q-Support and open the Q-Fund. Any category can be searched for individually using the "Single Category" Combobox. user can add source code to their Issue, so it is easier to see. IssueIcons have a tooltip that displays the name of its category. If the category is Q-Apps/Websites, then the icon of its owner will also be displayed.
This commit is contained in:
parent
87c990c164
commit
64b4cc0304
2193
package-lock.json
generated
2193
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "qsupport",
|
"name": "qsupport",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@ -17,11 +17,12 @@
|
|||||||
"@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",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.2.5",
|
||||||
"quill-image-resize-module-react": "^3.0.0",
|
"quill-image-resize-module-react": "^3.0.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.3.1",
|
||||||
"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",
|
||||||
@ -36,13 +37,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.62.0",
|
||||||
"@typescript-eslint/parser": "^5.57.1",
|
"@typescript-eslint/parser": "^5.62.0",
|
||||||
"@vitejs/plugin-react": "^4.0.0",
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
"eslint": "^8.38.0",
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-refresh": "^0.4.7",
|
||||||
"eslint-plugin-react-refresh": "^0.3.4",
|
|
||||||
"typescript": "^5.0.2",
|
"typescript": "^5.0.2",
|
||||||
"vite": "6.0.0-alpha.1"
|
"vite": "5.2.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
46
src/assets/svgs/QortalSVG.tsx
Normal file
46
src/assets/svgs/QortalSVG.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { IconTypes } from "./IconTypes";
|
||||||
|
|
||||||
|
export const QortalSVG: React.FC<IconTypes> = ({
|
||||||
|
color,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
className
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={className}
|
||||||
|
fill={color}
|
||||||
|
version="1.0"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
viewBox="0 0 695.000000 754.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
>
|
||||||
|
<g
|
||||||
|
transform="translate(0.000000,754.000000) scale(0.100000,-0.100000)"
|
||||||
|
stroke="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3035 7289 c-374 -216 -536 -309 -1090 -629 -409 -236 -1129 -652
|
||||||
|
-1280 -739 -82 -48 -228 -132 -322 -186 l-173 -100 0 -1882 0 -1883 38 -24
|
||||||
|
c20 -13 228 -134 462 -269 389 -223 1779 -1026 2335 -1347 127 -73 268 -155
|
||||||
|
314 -182 56 -32 95 -48 118 -48 33 0 207 97 991 552 l102 60 0 779 c0 428 -2
|
||||||
|
779 -4 779 -3 0 -247 -140 -543 -311 -296 -170 -544 -308 -553 -306 -8 2 -188
|
||||||
|
104 -400 226 -212 123 -636 368 -942 544 l-558 322 0 1105 c0 1042 1 1106 18
|
||||||
|
1116 9 6 107 63 217 126 110 64 421 243 690 398 270 156 601 347 736 425 l247
|
||||||
|
142 363 -210 c200 -115 551 -317 779 -449 228 -132 495 -286 594 -341 l178
|
||||||
|
-102 -6 -1889 -6 -1888 23 14 c12 8 318 185 680 393 l657 379 0 1887 0 1886
|
||||||
|
-77 46 c-43 25 -458 264 -923 532 -465 268 -1047 605 -1295 748 -646 373 -965
|
||||||
|
557 -968 557 -1 0 -182 -104 -402 -231z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3010 4769 c-228 -133 -471 -274 -540 -313 l-125 -72 0 -633 0 -632
|
||||||
|
295 -171 c162 -94 407 -235 544 -315 137 -79 255 -142 261 -139 6 2 200 113
|
||||||
|
431 247 230 133 471 272 534 308 l115 66 2 635 3 635 -536 309 c-294 169 -543
|
||||||
|
310 -552 312 -9 2 -204 -105 -432 -237z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
@ -1,32 +1,43 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
|
||||||
import {
|
|
||||||
ActionButton,
|
|
||||||
CrowdfundActionButtonRow,
|
|
||||||
CustomInputField,
|
|
||||||
ModalBody,
|
|
||||||
NewCrowdfundTitle,
|
|
||||||
} from "./EditIssue-styles.tsx";
|
|
||||||
import { Box, Modal, Typography, useTheme } from "@mui/material";
|
|
||||||
import RemoveIcon from "@mui/icons-material/Remove";
|
import RemoveIcon from "@mui/icons-material/Remove";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
MenuItem,
|
||||||
|
Modal,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
useTheme,
|
||||||
|
} from "@mui/material";
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import { useDropzone } from "react-dropzone";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
|
||||||
import ShortUniqueId from "short-unique-id";
|
import ShortUniqueId from "short-unique-id";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { allCategoryData } from "../../constants/Categories/Categories.ts";
|
||||||
import { useDropzone } from "react-dropzone";
|
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
|
||||||
|
import { log, titleFormatter } from "../../constants/Misc.ts";
|
||||||
import { setNotification } from "../../state/features/notificationsSlice";
|
import {
|
||||||
import { objectToBase64 } from "../../utils/toBase64";
|
feeAmountBase,
|
||||||
import { RootState } from "../../state/store";
|
supportedCoins,
|
||||||
|
} from "../../constants/PublishFees/FeeData.tsx";
|
||||||
|
import { CoinType } from "../../constants/PublishFees/FeePricePublish/FeePricePublish.ts";
|
||||||
|
import { payPublishFeeQORT } from "../../constants/PublishFees/SendFeeFunctions.ts";
|
||||||
|
import { verifyPayment } from "../../constants/PublishFees/VerifyPayment.ts";
|
||||||
|
import { ThemeButtonBright } from "../../pages/Home/Home-styles.tsx";
|
||||||
import {
|
import {
|
||||||
setEditFile,
|
setEditFile,
|
||||||
updateFile,
|
updateFile,
|
||||||
updateInHashMap,
|
updateInHashMap,
|
||||||
} from "../../state/features/fileSlice.ts";
|
} from "../../state/features/fileSlice.ts";
|
||||||
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
|
|
||||||
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll";
|
import { setNotification } from "../../state/features/notificationsSlice.ts";
|
||||||
import { TextEditor } from "../common/TextEditor/TextEditor";
|
import { RootState } from "../../state/store.ts";
|
||||||
import { extractTextFromHTML } from "../common/TextEditor/utils";
|
import { BountyData, validateBountyInput } from "../../utils/qortalRequests.ts";
|
||||||
import { allCategoryData } from "../../constants/Categories/Categories.ts";
|
import { objectToBase64 } from "../../utils/toBase64.js";
|
||||||
import { log, titleFormatter } from "../../constants/Misc.ts";
|
import { isNumber } from "../../utils/utilFunctions.ts";
|
||||||
|
import {
|
||||||
|
AutocompleteQappNames,
|
||||||
|
QappNamesRef,
|
||||||
|
} from "../common/AutocompleteQappNames.tsx";
|
||||||
import {
|
import {
|
||||||
CategoryList,
|
CategoryList,
|
||||||
CategoryListRef,
|
CategoryListRef,
|
||||||
@ -36,14 +47,16 @@ import {
|
|||||||
ImagePublisher,
|
ImagePublisher,
|
||||||
ImagePublisherRef,
|
ImagePublisherRef,
|
||||||
} from "../common/ImagePublisher/ImagePublisher.tsx";
|
} from "../common/ImagePublisher/ImagePublisher.tsx";
|
||||||
import { ThemeButtonBright } from "../../pages/Home/Home-styles.tsx";
|
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll.js";
|
||||||
|
import { TextEditor } from "../common/TextEditor/TextEditor.js";
|
||||||
|
import { extractTextFromHTML } from "../common/TextEditor/utils.js";
|
||||||
import {
|
import {
|
||||||
AutocompleteQappNames,
|
ActionButton,
|
||||||
QappNamesRef,
|
CrowdfundActionButtonRow,
|
||||||
} from "../common/AutocompleteQappNames.tsx";
|
CustomInputField,
|
||||||
import { payPublishFeeQORT } from "../../constants/PublishFees/SendFeeFunctions.ts";
|
ModalBody,
|
||||||
import { feeAmountBase } from "../../constants/PublishFees/FeeData.tsx";
|
NewCrowdfundTitle,
|
||||||
import { verifyPayment } from "../../constants/PublishFees/VerifyPayment.ts";
|
} from "./EditIssue-styles.tsx";
|
||||||
|
|
||||||
const uid = new ShortUniqueId();
|
const uid = new ShortUniqueId();
|
||||||
const shortuid = new ShortUniqueId({ length: 5 });
|
const shortuid = new ShortUniqueId({ length: 5 });
|
||||||
@ -81,6 +94,9 @@ export const EditIssue = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [publishes, setPublishes] = useState<any>(null);
|
const [publishes, setPublishes] = useState<any>(null);
|
||||||
|
const bountyData = editIssueProperties?.bountyData;
|
||||||
|
const [bounty, setBounty] = useState<string>("");
|
||||||
|
const [sourceCode, setSourceCode] = useState<string>("");
|
||||||
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false);
|
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false);
|
||||||
const [videoPropertiesToSetToRedux, setVideoPropertiesToSetToRedux] =
|
const [videoPropertiesToSetToRedux, setVideoPropertiesToSetToRedux] =
|
||||||
useState(null);
|
useState(null);
|
||||||
@ -92,6 +108,11 @@ export const EditIssue = () => {
|
|||||||
const [files, setFiles] = useState<VideoFile[]>([]);
|
const [files, setFiles] = useState<VideoFile[]>([]);
|
||||||
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
||||||
const [isIssuePaid, setIsIssuePaid] = useState<boolean>(true);
|
const [isIssuePaid, setIsIssuePaid] = useState<boolean>(true);
|
||||||
|
|
||||||
|
const [coin, setCoin] = useState<CoinType>("QORT");
|
||||||
|
|
||||||
|
const [showCoins, setShowCoins] = useState<boolean>(false);
|
||||||
|
|
||||||
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 autocompleteRef = useRef<QappNamesRef>(null);
|
||||||
@ -148,6 +169,14 @@ export const EditIssue = () => {
|
|||||||
const categoriesFromEditFile =
|
const categoriesFromEditFile =
|
||||||
getCategoriesFromObject(editIssueProperties);
|
getCategoriesFromObject(editIssueProperties);
|
||||||
setSelectedCategories(categoriesFromEditFile);
|
setSelectedCategories(categoriesFromEditFile);
|
||||||
|
setBounty(bountyData?.crowdfundLink || bountyData?.amount || "");
|
||||||
|
setShowCoins(
|
||||||
|
isNumber(editIssueProperties?.bountyData?.amount || undefined)
|
||||||
|
);
|
||||||
|
if (editIssueProperties?.bountyData?.coinType)
|
||||||
|
setCoin(editIssueProperties?.bountyData?.coinType);
|
||||||
|
if (editIssueProperties?.bountyData?.sourceCodeLink)
|
||||||
|
setCoin(editIssueProperties?.bountyData?.sourceCodeLink);
|
||||||
}
|
}
|
||||||
}, [editIssueProperties]);
|
}, [editIssueProperties]);
|
||||||
|
|
||||||
@ -162,6 +191,11 @@ export const EditIssue = () => {
|
|||||||
|
|
||||||
async function publishQDNResource(payFee: boolean) {
|
async function publishQDNResource(payFee: boolean) {
|
||||||
try {
|
try {
|
||||||
|
if (bounty) {
|
||||||
|
const isValidated = await validateBountyInput(bounty);
|
||||||
|
if (!isValidated) throw new Error("Bounty is not valid");
|
||||||
|
}
|
||||||
|
|
||||||
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");
|
||||||
@ -169,7 +203,7 @@ export const EditIssue = () => {
|
|||||||
if (!allCategoriesSelected)
|
if (!allCategoriesSelected)
|
||||||
throw new Error("All Categories must be selected");
|
throw new Error("All Categories must be selected");
|
||||||
|
|
||||||
console.log("categories", selectedCategories);
|
if (log) console.log("categories", selectedCategories);
|
||||||
const QappsCategoryID = "3";
|
const QappsCategoryID = "3";
|
||||||
if (
|
if (
|
||||||
selectedCategories[0] === QappsCategoryID &&
|
selectedCategories[0] === QappsCategoryID &&
|
||||||
@ -208,9 +242,9 @@ export const EditIssue = () => {
|
|||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
|
|
||||||
if (!sanitizeTitle) throw new Error("Please enter a title");
|
if (!sanitizeTitle) throw new Error("Please enter a title");
|
||||||
let fileReferences = [];
|
const fileReferences = [];
|
||||||
|
|
||||||
let listOfPublishes = [];
|
const listOfPublishes = [];
|
||||||
const fullDescription = extractTextFromHTML(description);
|
const fullDescription = extractTextFromHTML(description);
|
||||||
|
|
||||||
for (const publish of files) {
|
for (const publish of files) {
|
||||||
@ -228,17 +262,17 @@ export const EditIssue = () => {
|
|||||||
if (fileExtensionSplit?.length > 1) {
|
if (fileExtensionSplit?.length > 1) {
|
||||||
fileExtension = fileExtensionSplit?.pop() || "";
|
fileExtension = fileExtensionSplit?.pop() || "";
|
||||||
}
|
}
|
||||||
let firstPartName = fileExtensionSplit[0];
|
const firstPartName = fileExtensionSplit[0];
|
||||||
|
|
||||||
let filename = firstPartName.slice(0, 15);
|
let filename = firstPartName.slice(0, 15);
|
||||||
|
|
||||||
// Step 1: Replace all white spaces with underscores
|
// Step 1: Replace all white spaces with underscores
|
||||||
|
|
||||||
// Replace all forms of whitespace (including non-standard ones) with underscores
|
// Replace all forms of whitespace (including non-standard ones) with underscores
|
||||||
let stringWithUnderscores = filename.replace(/[\s\uFEFF\xA0]+/g, "_");
|
const stringWithUnderscores = filename.replace(/[\s\uFEFF\xA0]+/g, "_");
|
||||||
|
|
||||||
// Remove all non-alphanumeric characters (except underscores)
|
// Remove all non-alphanumeric characters (except underscores)
|
||||||
let alphanumericString = stringWithUnderscores.replace(
|
const alphanumericString = stringWithUnderscores.replace(
|
||||||
/[^a-zA-Z0-9_]/g,
|
/[^a-zA-Z0-9_]/g,
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
@ -250,7 +284,7 @@ export const EditIssue = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const categoryString = `**${categoryListRef.current?.getSelectedCategories()}**`;
|
const categoryString = `**${categoryListRef.current?.getSelectedCategories()}**`;
|
||||||
let metadescription = categoryString + fullDescription.slice(0, 150);
|
const metadescription = categoryString + fullDescription.slice(0, 150);
|
||||||
|
|
||||||
const requestBodyVideo: any = {
|
const requestBodyVideo: any = {
|
||||||
action: "PUBLISH_QDN_RESOURCE",
|
action: "PUBLISH_QDN_RESOURCE",
|
||||||
@ -275,6 +309,14 @@ export const EditIssue = () => {
|
|||||||
}
|
}
|
||||||
const selectedQappName = autocompleteRef?.current?.getSelectedValue();
|
const selectedQappName = autocompleteRef?.current?.getSelectedValue();
|
||||||
|
|
||||||
|
const isBountyNumber = isNumber(bounty);
|
||||||
|
const bountyData: BountyData = {
|
||||||
|
amount: isBountyNumber ? Number(bounty) : undefined,
|
||||||
|
crowdfundLink: isBountyNumber ? undefined : bounty,
|
||||||
|
coinType: coin,
|
||||||
|
sourceCodeLink: sourceCode,
|
||||||
|
};
|
||||||
|
|
||||||
const issueObject: any = {
|
const issueObject: any = {
|
||||||
title,
|
title,
|
||||||
version: editIssueProperties.version,
|
version: editIssueProperties.version,
|
||||||
@ -286,6 +328,7 @@ export const EditIssue = () => {
|
|||||||
images: imagePublisherRef?.current?.getImageArray(),
|
images: imagePublisherRef?.current?.getImageArray(),
|
||||||
QappName: selectedQappName,
|
QappName: selectedQappName,
|
||||||
feeData: editIssueProperties?.feeData,
|
feeData: editIssueProperties?.feeData,
|
||||||
|
bountyData,
|
||||||
};
|
};
|
||||||
if (payFee) {
|
if (payFee) {
|
||||||
const publishFeeResponse = await payPublishFeeQORT(feeAmountBase);
|
const publishFeeResponse = await payPublishFeeQORT(feeAmountBase);
|
||||||
@ -309,7 +352,7 @@ export const EditIssue = () => {
|
|||||||
categoryListRef.current?.getCategoriesFetchString(selectedCategories);
|
categoryListRef.current?.getCategoriesFetchString(selectedCategories);
|
||||||
const metaDataString = `**${categoryString + QappNameString}**`;
|
const metaDataString = `**${categoryString + QappNameString}**`;
|
||||||
|
|
||||||
let metadescription = metaDataString + fullDescription.slice(0, 150);
|
const metadescription = metaDataString + fullDescription.slice(0, 150);
|
||||||
if (log) console.log("description is: ", metadescription);
|
if (log) console.log("description is: ", metadescription);
|
||||||
if (log) console.log("description length is: ", metadescription.length);
|
if (log) console.log("description length is: ", metadescription.length);
|
||||||
if (log) console.log("characters left:", 240 - metadescription.length);
|
if (log) console.log("characters left:", 240 - metadescription.length);
|
||||||
@ -318,7 +361,6 @@ export const EditIssue = () => {
|
|||||||
|
|
||||||
const fileObjectToBase64 = await objectToBase64(issueObject);
|
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",
|
||||||
name: name,
|
name: name,
|
||||||
@ -465,12 +507,53 @@ export const EditIssue = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{isShowQappNameTextField() && (
|
{isShowQappNameTextField() && (
|
||||||
|
<>
|
||||||
<AutocompleteQappNames
|
<AutocompleteQappNames
|
||||||
ref={autocompleteRef}
|
ref={autocompleteRef}
|
||||||
namesList={QappNames}
|
namesList={QappNames}
|
||||||
initialSelection={editIssueProperties?.QappName}
|
initialSelection={editIssueProperties?.QappName}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
<CustomInputField
|
||||||
|
name="q-app-source-code"
|
||||||
|
label="Link to Source Code"
|
||||||
|
variant="filled"
|
||||||
|
value={sourceCode}
|
||||||
|
onChange={e => setSourceCode(e.target.value.trim())}
|
||||||
|
inputProps={{ maxLength: 200 }}
|
||||||
|
/>
|
||||||
|
<CustomInputField
|
||||||
|
name="q-fund-link"
|
||||||
|
label="Bounty Amount or Q-Fund Link"
|
||||||
|
variant="filled"
|
||||||
|
value={bounty}
|
||||||
|
onChange={e => {
|
||||||
|
const bountyValue = e.target.value.trim();
|
||||||
|
setBounty(bountyValue);
|
||||||
|
const bountyIsNumber = isNumber(bountyValue);
|
||||||
|
setShowCoins(bountyIsNumber);
|
||||||
|
if (!bountyIsNumber) setCoin("QORT");
|
||||||
|
}}
|
||||||
|
inputProps={{ maxLength: 200 }}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label={"Select Coin"}
|
||||||
|
select
|
||||||
|
fullWidth
|
||||||
|
value={coin}
|
||||||
|
onChange={e => setCoin(e.target.value as CoinType)}
|
||||||
|
sx={{
|
||||||
|
display: showCoins ? "block" : "none",
|
||||||
|
width: "20%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{supportedCoins.map((coin, index) => (
|
||||||
|
<MenuItem value={coin} key={coin + index}>
|
||||||
|
{coin}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
<ImagePublisher
|
<ImagePublisher
|
||||||
ref={imagePublisherRef}
|
ref={imagePublisherRef}
|
||||||
initialImages={editIssueProperties?.images}
|
initialImages={editIssueProperties?.images}
|
||||||
|
@ -1,33 +1,46 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
|
||||||
import {
|
|
||||||
ActionButton,
|
|
||||||
ActionButtonRow,
|
|
||||||
CustomInputField,
|
|
||||||
ModalBody,
|
|
||||||
NewCrowdfundTitle,
|
|
||||||
StyledButton,
|
|
||||||
} from "./PublishIssue-styles.tsx";
|
|
||||||
import { Box, Modal, Typography, useTheme } from "@mui/material";
|
|
||||||
import RemoveIcon from "@mui/icons-material/Remove";
|
|
||||||
import ShortUniqueId from "short-unique-id";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import AddBoxIcon from "@mui/icons-material/AddBox";
|
import AddBoxIcon from "@mui/icons-material/AddBox";
|
||||||
|
import RemoveIcon from "@mui/icons-material/Remove";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
MenuItem,
|
||||||
|
Modal,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
useTheme,
|
||||||
|
} from "@mui/material";
|
||||||
|
import React, { useRef, useState } from "react";
|
||||||
import { useDropzone } from "react-dropzone";
|
import { useDropzone } from "react-dropzone";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { setNotification } from "../../state/features/notificationsSlice";
|
import ShortUniqueId from "short-unique-id";
|
||||||
import { objectToBase64 } from "../../utils/toBase64";
|
|
||||||
import { RootState } from "../../state/store";
|
|
||||||
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
|
|
||||||
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll";
|
|
||||||
import { TextEditor } from "../common/TextEditor/TextEditor";
|
|
||||||
import { extractTextFromHTML } from "../common/TextEditor/utils";
|
|
||||||
import { allCategoryData } from "../../constants/Categories/Categories.ts";
|
import { allCategoryData } from "../../constants/Categories/Categories.ts";
|
||||||
|
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
|
||||||
import {
|
import {
|
||||||
fontSizeLarge,
|
fontSizeLarge,
|
||||||
fontSizeSmall,
|
fontSizeSmall,
|
||||||
log,
|
log,
|
||||||
titleFormatter,
|
titleFormatter,
|
||||||
} from "../../constants/Misc.ts";
|
} from "../../constants/Misc.ts";
|
||||||
|
import {
|
||||||
|
feeAmountBase,
|
||||||
|
feeDisclaimer,
|
||||||
|
supportedCoins,
|
||||||
|
} from "../../constants/PublishFees/FeeData.tsx";
|
||||||
|
import { CoinType } from "../../constants/PublishFees/FeePricePublish/FeePricePublish.ts";
|
||||||
|
import {
|
||||||
|
payPublishFeeQORT,
|
||||||
|
PublishFeeData,
|
||||||
|
} from "../../constants/PublishFees/SendFeeFunctions.ts";
|
||||||
|
import { ThemeButtonBright } from "../../pages/Home/Home-styles.tsx";
|
||||||
|
|
||||||
|
import { setNotification } from "../../state/features/notificationsSlice";
|
||||||
|
import { RootState } from "../../state/store";
|
||||||
|
import { BountyData, validateBountyInput } from "../../utils/qortalRequests.ts";
|
||||||
|
import { objectToBase64 } from "../../utils/toBase64";
|
||||||
|
import { isNumber } from "../../utils/utilFunctions.ts";
|
||||||
|
import {
|
||||||
|
AutocompleteQappNames,
|
||||||
|
QappNamesRef,
|
||||||
|
} from "../common/AutocompleteQappNames.tsx";
|
||||||
import {
|
import {
|
||||||
CategoryList,
|
CategoryList,
|
||||||
CategoryListRef,
|
CategoryListRef,
|
||||||
@ -36,19 +49,17 @@ import {
|
|||||||
ImagePublisher,
|
ImagePublisher,
|
||||||
ImagePublisherRef,
|
ImagePublisherRef,
|
||||||
} from "../common/ImagePublisher/ImagePublisher.tsx";
|
} from "../common/ImagePublisher/ImagePublisher.tsx";
|
||||||
import { ThemeButtonBright } from "../../pages/Home/Home-styles.tsx";
|
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll";
|
||||||
|
import { TextEditor } from "../common/TextEditor/TextEditor";
|
||||||
|
import { extractTextFromHTML } from "../common/TextEditor/utils";
|
||||||
import {
|
import {
|
||||||
AutocompleteQappNames,
|
ActionButton,
|
||||||
QappNamesRef,
|
ActionButtonRow,
|
||||||
} from "../common/AutocompleteQappNames.tsx";
|
CustomInputField,
|
||||||
import {
|
ModalBody,
|
||||||
feeAmountBase,
|
NewCrowdfundTitle,
|
||||||
feeDisclaimer,
|
StyledButton,
|
||||||
} from "../../constants/PublishFees/FeeData.tsx";
|
} from "./PublishIssue-styles.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 });
|
||||||
@ -72,10 +83,6 @@ interface VideoFile {
|
|||||||
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 [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
|
||||||
@ -83,6 +90,12 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
|||||||
const QappNames = useSelector(
|
const QappNames = useSelector(
|
||||||
(state: RootState) => state.file.publishedQappNames
|
(state: RootState) => state.file.publishedQappNames
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false);
|
||||||
|
const [QappName, setQappName] = useState<string>("");
|
||||||
|
|
||||||
|
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
||||||
|
|
||||||
const [files, setFiles] = useState<VideoFile[]>([]);
|
const [files, setFiles] = useState<VideoFile[]>([]);
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
@ -99,6 +112,10 @@ 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 [bounty, setBounty] = useState<string>("");
|
||||||
|
const [sourceCode, setSourceCode] = useState<string>("");
|
||||||
|
const [coin, setCoin] = useState<CoinType>("QORT");
|
||||||
|
const [showCoins, setShowCoins] = useState<boolean>(false);
|
||||||
|
|
||||||
const categoryListRef = useRef<CategoryListRef>(null);
|
const categoryListRef = useRef<CategoryListRef>(null);
|
||||||
const imagePublisherRef = useRef<ImagePublisherRef>(null);
|
const imagePublisherRef = useRef<ImagePublisherRef>(null);
|
||||||
@ -139,17 +156,16 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (editContent) {
|
|
||||||
}
|
|
||||||
}, [editContent]);
|
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
async function publishQDNResource() {
|
const publishQDNResource = async () => {
|
||||||
try {
|
try {
|
||||||
|
if (bounty) {
|
||||||
|
const isValidated = await validateBountyInput(bounty);
|
||||||
|
if (!isValidated) throw new Error("Bounty is not valid");
|
||||||
|
}
|
||||||
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");
|
||||||
@ -197,9 +213,9 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
|||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
if (!sanitizeTitle) throw new Error("Please enter a title");
|
if (!sanitizeTitle) throw new Error("Please enter a title");
|
||||||
|
|
||||||
let fileReferences = [];
|
const fileReferences = [];
|
||||||
|
|
||||||
let listOfPublishes = [];
|
const listOfPublishes = [];
|
||||||
|
|
||||||
const fullDescription = extractTextFromHTML(description);
|
const fullDescription = extractTextFromHTML(description);
|
||||||
|
|
||||||
@ -214,17 +230,17 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
|||||||
if (fileExtensionSplit?.length > 1) {
|
if (fileExtensionSplit?.length > 1) {
|
||||||
fileExtension = fileExtensionSplit?.pop() || "";
|
fileExtension = fileExtensionSplit?.pop() || "";
|
||||||
}
|
}
|
||||||
let firstPartName = fileExtensionSplit[0];
|
const firstPartName = fileExtensionSplit[0];
|
||||||
|
|
||||||
let filename = firstPartName.slice(0, 15);
|
let filename = firstPartName.slice(0, 15);
|
||||||
|
|
||||||
// Step 1: Replace all white spaces with underscores
|
// Step 1: Replace all white spaces with underscores
|
||||||
|
|
||||||
// Replace all forms of whitespace (including non-standard ones) with underscores
|
// Replace all forms of whitespace (including non-standard ones) with underscores
|
||||||
let stringWithUnderscores = filename.replace(/[\s\uFEFF\xA0]+/g, "_");
|
const stringWithUnderscores = filename.replace(/[\s\uFEFF\xA0]+/g, "_");
|
||||||
|
|
||||||
// Remove all non-alphanumeric characters (except underscores)
|
// Remove all non-alphanumeric characters (except underscores)
|
||||||
let alphanumericString = stringWithUnderscores.replace(
|
const alphanumericString = stringWithUnderscores.replace(
|
||||||
/[^a-zA-Z0-9_]/g,
|
/[^a-zA-Z0-9_]/g,
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
@ -236,7 +252,7 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const categoryString = `**${categoryListRef.current?.getSelectedCategories()}**`;
|
const categoryString = `**${categoryListRef.current?.getSelectedCategories()}**`;
|
||||||
let metadescription = categoryString + fullDescription.slice(0, 150);
|
const metadescription = categoryString + fullDescription.slice(0, 150);
|
||||||
|
|
||||||
const requestBodyFile: any = {
|
const requestBodyFile: any = {
|
||||||
action: "PUBLISH_QDN_RESOURCE",
|
action: "PUBLISH_QDN_RESOURCE",
|
||||||
@ -275,6 +291,13 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
|||||||
senderName: "",
|
senderName: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isBountyNumber = isNumber(bounty);
|
||||||
|
const bountyData: BountyData = {
|
||||||
|
amount: isBountyNumber ? Number(bounty) : undefined,
|
||||||
|
crowdfundLink: isBountyNumber ? undefined : bounty,
|
||||||
|
coinType: coin,
|
||||||
|
sourceCodeLink: sourceCode,
|
||||||
|
};
|
||||||
const issueObject: any = {
|
const issueObject: any = {
|
||||||
title,
|
title,
|
||||||
version: 1,
|
version: 1,
|
||||||
@ -286,6 +309,7 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
|||||||
images: imagePublisherRef?.current?.getImageArray(),
|
images: imagePublisherRef?.current?.getImageArray(),
|
||||||
QappName: selectedQappName,
|
QappName: selectedQappName,
|
||||||
feeData,
|
feeData,
|
||||||
|
bountyData,
|
||||||
};
|
};
|
||||||
|
|
||||||
const QappNameString = autocompleteRef?.current?.getQappNameFetchString();
|
const QappNameString = autocompleteRef?.current?.getQappNameFetchString();
|
||||||
@ -293,7 +317,7 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
|||||||
categoryListRef.current?.getCategoriesFetchString(categoryList);
|
categoryListRef.current?.getCategoriesFetchString(categoryList);
|
||||||
const metaDataString = `**${categoryString + QappNameString}**`;
|
const metaDataString = `**${categoryString + QappNameString}**`;
|
||||||
|
|
||||||
let metadescription = metaDataString + fullDescription.slice(0, 150);
|
const metadescription = metaDataString + fullDescription.slice(0, 150);
|
||||||
|
|
||||||
if (log) console.log("description is: ", metadescription);
|
if (log) console.log("description is: ", metadescription);
|
||||||
if (log) console.log("description length is: ", metadescription.length);
|
if (log) console.log("description length is: ", metadescription.length);
|
||||||
@ -343,7 +367,7 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
|||||||
if (!notificationObj) return;
|
if (!notificationObj) return;
|
||||||
dispatch(setNotification(notificationObj));
|
dispatch(setNotification(notificationObj));
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const isShowQappNameTextField = () => {
|
const isShowQappNameTextField = () => {
|
||||||
const QappID = "3";
|
const QappID = "3";
|
||||||
@ -460,11 +484,52 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
{isShowQappNameTextField() && (
|
{isShowQappNameTextField() && (
|
||||||
|
<>
|
||||||
<AutocompleteQappNames
|
<AutocompleteQappNames
|
||||||
ref={autocompleteRef}
|
ref={autocompleteRef}
|
||||||
namesList={QappNames}
|
namesList={QappNames}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
<CustomInputField
|
||||||
|
name="q-app-source-code"
|
||||||
|
label="Link to Source Code"
|
||||||
|
variant="filled"
|
||||||
|
value={sourceCode}
|
||||||
|
onChange={e => setSourceCode(e.target.value.trim())}
|
||||||
|
inputProps={{ maxLength: 200 }}
|
||||||
|
/>
|
||||||
|
<CustomInputField
|
||||||
|
name="q-fund-link"
|
||||||
|
label="Bounty Amount or Q-Fund Link"
|
||||||
|
variant="filled"
|
||||||
|
value={bounty}
|
||||||
|
onChange={e => {
|
||||||
|
const bountyValue = e.target.value.trim();
|
||||||
|
setBounty(bountyValue);
|
||||||
|
const bountyIsNumber = isNumber(bountyValue);
|
||||||
|
setShowCoins(bountyIsNumber);
|
||||||
|
if (!bountyIsNumber) setCoin("QORT");
|
||||||
|
}}
|
||||||
|
inputProps={{ maxLength: 200 }}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label={"Select Coin"}
|
||||||
|
select
|
||||||
|
fullWidth
|
||||||
|
value={coin}
|
||||||
|
onChange={e => setCoin(e.target.value as CoinType)}
|
||||||
|
sx={{
|
||||||
|
display: showCoins ? "block" : "none",
|
||||||
|
width: "20%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{supportedCoins.map((coin, index) => (
|
||||||
|
<MenuItem value={coin} key={coin + index}>
|
||||||
|
{coin}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</TextField>
|
||||||
<ImagePublisher ref={imagePublisherRef} />
|
<ImagePublisher ref={imagePublisherRef} />
|
||||||
<CustomInputField
|
<CustomInputField
|
||||||
name="title"
|
name="title"
|
||||||
|
129
src/components/common/BountyDisplay.tsx
Normal file
129
src/components/common/BountyDisplay.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import CSS from "csstype";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
BountyData,
|
||||||
|
DayTime,
|
||||||
|
getATaddress,
|
||||||
|
getATAmount,
|
||||||
|
getCrowdfundEndDate,
|
||||||
|
getDaySummary,
|
||||||
|
getDurationFromBlocks,
|
||||||
|
getHasQFundEnded,
|
||||||
|
} from "../../utils/qortalRequests.js";
|
||||||
|
import { CoinIcon } from "./CoinIcon.js";
|
||||||
|
|
||||||
|
type TimeDisplay = "TIME" | "AMOUNT" | "BOTH";
|
||||||
|
interface BountyDataProps {
|
||||||
|
bountyData?: BountyData;
|
||||||
|
timeDisplay?: TimeDisplay;
|
||||||
|
fontStyle?: CSS.Properties;
|
||||||
|
divStyle?: CSS.Properties;
|
||||||
|
}
|
||||||
|
export const BountyDisplay = ({
|
||||||
|
bountyData,
|
||||||
|
timeDisplay = "TIME",
|
||||||
|
divStyle,
|
||||||
|
fontStyle,
|
||||||
|
}: BountyDataProps) => {
|
||||||
|
const emptyTime: DayTime = { days: 0, hours: 0, minutes: 0 };
|
||||||
|
const [timeRemaining, setTimeRemaining] = useState<DayTime>(emptyTime);
|
||||||
|
const [hasQfundEnded, setHasQfundEnded] = useState<boolean>(undefined);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
const [amount, setAmount] = useState<number>(bountyData?.amount);
|
||||||
|
const setTimeRemainingState = () => {
|
||||||
|
getCrowdfundEndDate(crowdfundLink).then(endDate => {
|
||||||
|
if (!endDate) return;
|
||||||
|
const blocksRemaining = moment
|
||||||
|
.duration(endDate.diff(moment()))
|
||||||
|
.asMinutes();
|
||||||
|
getDaySummary().then(response => {
|
||||||
|
const blockCount = response.blockCount;
|
||||||
|
const timeCount = getDurationFromBlocks(blocksRemaining, blockCount);
|
||||||
|
setTimeRemaining(timeCount);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const initializeBountyDisplay = async () => {
|
||||||
|
if (!crowdfundLink) {
|
||||||
|
setIsLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeRemainingState();
|
||||||
|
const address = await getATaddress(crowdfundLink);
|
||||||
|
const QfundState = await getHasQFundEnded(address);
|
||||||
|
setAmount(await getATAmount(crowdfundLink));
|
||||||
|
setHasQfundEnded(QfundState);
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
initializeBountyDisplay();
|
||||||
|
setInterval(initializeBountyDisplay, 60_001);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const padDigits = (num: number) => {
|
||||||
|
let output = "";
|
||||||
|
if (num < 10) output += "0";
|
||||||
|
return output + num.toString();
|
||||||
|
};
|
||||||
|
if (!bountyData) return <></>;
|
||||||
|
|
||||||
|
const { coinType, crowdfundLink } = bountyData;
|
||||||
|
const hasCrowdfund = !!crowdfundLink;
|
||||||
|
const { days, hours, minutes } = timeRemaining;
|
||||||
|
|
||||||
|
const defaultDivStyle = {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
};
|
||||||
|
|
||||||
|
const timeIsEmpty = days === 0 && hours === 0 && minutes === 0;
|
||||||
|
const shortTimeDisplay = `${padDigits(days)}:${padDigits(hours)}:${padDigits(minutes)}`;
|
||||||
|
const longTimeDisplay = `${days} Days ${hours} Hours ${minutes} Minutes left\n`;
|
||||||
|
const timeJSX = timeIsEmpty ? (
|
||||||
|
<></>
|
||||||
|
) : (
|
||||||
|
<span style={{ ...divStyle, ...fontStyle }}>
|
||||||
|
{timeDisplay === "BOTH" ? longTimeDisplay : shortTimeDisplay}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
const amountJSX = (
|
||||||
|
<div style={{ ...defaultDivStyle, ...divStyle }}>
|
||||||
|
{(amount > 0 || hasCrowdfund) && !isLoading && (
|
||||||
|
<>
|
||||||
|
<CoinIcon
|
||||||
|
coinType={coinType}
|
||||||
|
style={{
|
||||||
|
marginRight: "10px",
|
||||||
|
width: "40px",
|
||||||
|
height: "40px",
|
||||||
|
}}
|
||||||
|
isQfund={hasCrowdfund}
|
||||||
|
/>
|
||||||
|
<span style={fontStyle}>{Math.round(amount)}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (timeDisplay) {
|
||||||
|
case "AMOUNT":
|
||||||
|
return amountJSX;
|
||||||
|
case "TIME":
|
||||||
|
return hasQfundEnded ?? hasQfundEnded === undefined ? amountJSX : timeJSX;
|
||||||
|
case "BOTH":
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div style={{ display: "flex" }}>
|
||||||
|
{amountJSX}
|
||||||
|
<div style={{ width: "10px", marginLeft: "10%" }} />
|
||||||
|
{timeJSX}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
52
src/components/common/CoinIcon.tsx
Normal file
52
src/components/common/CoinIcon.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import React, { CSSProperties } from "react";
|
||||||
|
import ARRRicon from "../../../src/assets/icons/CoinIcons/arrr.png";
|
||||||
|
import BTCicon from "../../../src/assets/icons/CoinIcons/btc.png";
|
||||||
|
import DGBicon from "../../../src/assets/icons/CoinIcons/dgb.png";
|
||||||
|
import DOGEicon from "../../../src/assets/icons/CoinIcons/doge.png";
|
||||||
|
import LTCicon from "../../../src/assets/icons/CoinIcons/ltc.png";
|
||||||
|
import QfundIcon from "../../../src/assets/icons/CoinIcons/Q-FundIcon.png";
|
||||||
|
import QORTicon from "../../../src/assets/icons/CoinIcons/qort.png";
|
||||||
|
import RVNicon from "../../../src/assets/icons/CoinIcons/rvn.png";
|
||||||
|
import { CoinType } from "../../constants/PublishFees/FeePricePublish/FeePricePublish.ts";
|
||||||
|
import { IssueIcon } from "./IssueIcon.tsx";
|
||||||
|
|
||||||
|
interface CoinIconProps {
|
||||||
|
coinType: CoinType;
|
||||||
|
showBackupIcon?: boolean;
|
||||||
|
style?: CSSProperties;
|
||||||
|
isQfund: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CoinIcon = ({
|
||||||
|
coinType = "QORT",
|
||||||
|
showBackupIcon,
|
||||||
|
style,
|
||||||
|
isQfund,
|
||||||
|
}: CoinIconProps) => {
|
||||||
|
const getIcon = () => {
|
||||||
|
if (isQfund) return QfundIcon;
|
||||||
|
switch (coinType) {
|
||||||
|
case "QORT":
|
||||||
|
return QORTicon;
|
||||||
|
case "LTC":
|
||||||
|
return LTCicon;
|
||||||
|
case "BTC":
|
||||||
|
return BTCicon;
|
||||||
|
case "DOGE":
|
||||||
|
return DOGEicon;
|
||||||
|
case "DGB":
|
||||||
|
return DGBicon;
|
||||||
|
case "RVN":
|
||||||
|
return RVNicon;
|
||||||
|
case "ARRR":
|
||||||
|
return ARRRicon;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<IssueIcon
|
||||||
|
iconSrc={getIcon()}
|
||||||
|
style={style}
|
||||||
|
showBackupIcon={showBackupIcon}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -9,7 +9,10 @@ import {
|
|||||||
CommentInputContainer,
|
CommentInputContainer,
|
||||||
SubmitCommentButton,
|
SubmitCommentButton,
|
||||||
} from "./Comments-styles";
|
} from "./Comments-styles";
|
||||||
import { QSUPPORT_COMMENT_BASE } from "../../../constants/Identifiers.ts";
|
import {
|
||||||
|
QSUPPORT_COMMENT_BASE,
|
||||||
|
useTestIdentifiers,
|
||||||
|
} from "../../../constants/Identifiers.ts";
|
||||||
import { sendQchatDM } from "../../../utils/qortalRequests.ts";
|
import { sendQchatDM } from "../../../utils/qortalRequests.ts";
|
||||||
import { maxCommentLength } from "../../../constants/Misc.ts";
|
import { maxCommentLength } from "../../../constants/Misc.ts";
|
||||||
|
|
||||||
@ -127,6 +130,10 @@ export const CommentEditor = ({
|
|||||||
address = user?.address;
|
address = user?.address;
|
||||||
name = user?.name || "";
|
name = user?.name || "";
|
||||||
|
|
||||||
|
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}`;
|
||||||
|
|
||||||
|
if (useTestIdentifiers) await sendQchatDM(postName, notificationMessage);
|
||||||
if (!address) {
|
if (!address) {
|
||||||
errorMsg = "Cannot post: your address isn't available";
|
errorMsg = "Cannot post: your address isn't available";
|
||||||
}
|
}
|
||||||
@ -180,9 +187,6 @@ export const CommentEditor = ({
|
|||||||
//
|
//
|
||||||
// ${value.substring(0, maxNotificationLength)}`;
|
// ${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);
|
await sendQchatDM(postName, notificationMessage);
|
||||||
}
|
}
|
||||||
return resourceResponse;
|
return resourceResponse;
|
||||||
|
35
src/components/common/Donate/Donate-styles.tsx
Normal file
35
src/components/common/Donate/Donate-styles.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { Box, Button, InputLabel } from "@mui/material";
|
||||||
|
import { styled } from "@mui/material/styles";
|
||||||
|
|
||||||
|
const ButtonStyle = styled(Button)({
|
||||||
|
fontFamily: "Mulish",
|
||||||
|
fontWeight: "800",
|
||||||
|
fontSize: "21px",
|
||||||
|
lineHeight: "1.75",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
minWidth: "64px",
|
||||||
|
padding: "15px 25px",
|
||||||
|
color: "#ffffff",
|
||||||
|
"&:disabled": {
|
||||||
|
filter: "brightness(0.8)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DonateModalCol = styled(Box)({
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
width: "400px",
|
||||||
|
justifyContent: "center",
|
||||||
|
gap: "20px",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const DonorDetailsButton = styled(ButtonStyle)(({ theme }) => ({}));
|
||||||
|
|
||||||
|
export const DonateModalLabel = styled(InputLabel)(({ theme }) => ({
|
||||||
|
fontFamily: "Copse",
|
||||||
|
fontSize: "27px",
|
||||||
|
letterSpacing: "1px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
fontWeight: 400,
|
||||||
|
}));
|
275
src/components/common/Donate/Donate.tsx
Normal file
275
src/components/common/Donate/Donate.tsx
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
InputAdornment,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
useTheme,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { QortalSVG } from "../../../assets/svgs/QortalSVG";
|
||||||
|
import { ThemeButton } from "../../../pages/Home/Home-styles.tsx";
|
||||||
|
import { setNotification } from "../../../state/features/notificationsSlice";
|
||||||
|
import BoundedNumericTextField from "../../../utils/BoundedNumericTextField";
|
||||||
|
import {
|
||||||
|
getATaddress,
|
||||||
|
getATInfo,
|
||||||
|
getUserBalance,
|
||||||
|
} from "../../../utils/qortalRequests.ts";
|
||||||
|
import { truncateNumber } from "../../../utils/utilFunctions.ts";
|
||||||
|
import Portal from "../Portal";
|
||||||
|
import { DonateModalCol, DonateModalLabel } from "./Donate-styles";
|
||||||
|
|
||||||
|
interface DonateProps {
|
||||||
|
crowdfundLink: string;
|
||||||
|
onSubmit?: () => void;
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Donate = ({ crowdfundLink, onSubmit, onClose }: DonateProps) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
|
const [amount, setAmount] = useState<number>(0);
|
||||||
|
const [currentBalance, setCurrentBalance] = useState<string>("");
|
||||||
|
const [disableDonation, setDisableDonation] = useState<boolean>(true);
|
||||||
|
const [ATaddress, setATaddress] = useState<string>("");
|
||||||
|
const [ATDonationPossible, setATDonationPossible] = useState<boolean>(false);
|
||||||
|
const emptyDonationHelperText = "Donation amount must not be empty";
|
||||||
|
|
||||||
|
const [helperText, setHelperText] = useState<string>(emptyDonationHelperText);
|
||||||
|
const resetValues = () => {
|
||||||
|
setAmount(0);
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendCoin = async () => {
|
||||||
|
try {
|
||||||
|
if (!ATaddress) return;
|
||||||
|
if (isNaN(amount)) return;
|
||||||
|
|
||||||
|
// Check one last time if the AT has finished and if so, don't send the coin
|
||||||
|
const url = `/at/${ATaddress}`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const responseDataSearch = await response.json();
|
||||||
|
if (response.status !== 200 || responseDataSearch?.isFinished) {
|
||||||
|
dispatch(
|
||||||
|
setNotification({
|
||||||
|
msg: "This crowdfund has ended",
|
||||||
|
alertType: "error",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
resetValues();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Prevent them from sending a coin if there's 4 blocks left or less to avoid timing issues
|
||||||
|
const url2 = `/blocks/height`;
|
||||||
|
const blockHeightResponse = await fetch(url2, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const blockHeight = await blockHeightResponse.json();
|
||||||
|
const diff = +responseDataSearch?.sleepUntilHeight - +blockHeight;
|
||||||
|
if (diff <= 4) {
|
||||||
|
dispatch(
|
||||||
|
setNotification({
|
||||||
|
msg: "This crowdfund has ended",
|
||||||
|
alertType: "error",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
resetValues();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await qortalRequest({
|
||||||
|
action: "SEND_COIN",
|
||||||
|
coin: "QORT",
|
||||||
|
destinationAddress: ATaddress,
|
||||||
|
amount: amount,
|
||||||
|
});
|
||||||
|
dispatch(
|
||||||
|
setNotification({
|
||||||
|
msg: "Donation successfully sent",
|
||||||
|
alertType: "success",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
resetValues();
|
||||||
|
if (onSubmit) onSubmit();
|
||||||
|
} catch (error: any) {
|
||||||
|
let notificationObj: any = null;
|
||||||
|
if (typeof error === "string") {
|
||||||
|
notificationObj = {
|
||||||
|
msg: error || "Failed to send coin",
|
||||||
|
alertType: "error",
|
||||||
|
};
|
||||||
|
} else if (typeof error?.error === "string") {
|
||||||
|
notificationObj = {
|
||||||
|
msg: error?.error || "Failed to send coin",
|
||||||
|
alertType: "error",
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
notificationObj = {
|
||||||
|
msg: error?.message || "Failed to send coin",
|
||||||
|
alertType: "error",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!notificationObj) return;
|
||||||
|
dispatch(setNotification(notificationObj));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const allowDonationIfSafe = async (value: number) => {
|
||||||
|
if (isNaN(value) || value === 0) {
|
||||||
|
setDisableDonation(true);
|
||||||
|
setHelperText(emptyDonationHelperText);
|
||||||
|
} else {
|
||||||
|
setDisableDonation(false);
|
||||||
|
setHelperText("");
|
||||||
|
}
|
||||||
|
setAmount(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const initializeState = async () => {
|
||||||
|
const [userBalance, ATaddress] = await Promise.all([
|
||||||
|
getUserBalance(),
|
||||||
|
getATaddress(crowdfundLink),
|
||||||
|
]);
|
||||||
|
|
||||||
|
setCurrentBalance(truncateNumber(userBalance, 2));
|
||||||
|
setATaddress(ATaddress);
|
||||||
|
const ATinfo = await getATInfo(ATaddress);
|
||||||
|
let isDeployed = false;
|
||||||
|
if (ATinfo) {
|
||||||
|
const ATdeployed1 = ATinfo?.sleepUntilHeight && !ATinfo?.isFinished;
|
||||||
|
const ATdeployed2 = !ATinfo.sleepUntilHeight && ATinfo.isFinished;
|
||||||
|
if (ATdeployed1 || ATdeployed2) isDeployed = true;
|
||||||
|
const ATended = Object.keys(ATinfo).length > 0 && ATinfo?.isFinished;
|
||||||
|
setATDonationPossible(isDeployed && !ATended);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
if (!crowdfundLink) return;
|
||||||
|
initializeState();
|
||||||
|
}, []);
|
||||||
|
if (!crowdfundLink) return <></>;
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
display: ATDonationPossible ? "flex" : "none",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
title={<Typography fontSize={16}>Support This Crowdfund</Typography>}
|
||||||
|
arrow
|
||||||
|
disableHoverListener={!ATDonationPossible}
|
||||||
|
placement={"right-end"}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "relative",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 1,
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ThemeButton
|
||||||
|
onClick={() => setIsOpen(prev => !prev)}
|
||||||
|
disabled={!ATDonationPossible}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
Donate to Crowdfund
|
||||||
|
</ThemeButton>
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
{isOpen && (
|
||||||
|
<Portal>
|
||||||
|
<Dialog
|
||||||
|
open={isOpen}
|
||||||
|
onClose={() => setIsOpen(false)}
|
||||||
|
aria-labelledby="alert-dialog-title"
|
||||||
|
aria-describedby="alert-dialog-description"
|
||||||
|
>
|
||||||
|
<DialogTitle id="alert-dialog-title"></DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DonateModalCol>
|
||||||
|
<DonateModalLabel htmlFor="standard-adornment-amount">
|
||||||
|
Amount
|
||||||
|
</DonateModalLabel>
|
||||||
|
<BoundedNumericTextField
|
||||||
|
style={{ fontFamily: "Mulish" }}
|
||||||
|
minValue={1}
|
||||||
|
maxValue={+truncateNumber(currentBalance, 0)}
|
||||||
|
id="standard-adornment-amount"
|
||||||
|
value={amount}
|
||||||
|
onChange={value => allowDonationIfSafe(+value)}
|
||||||
|
variant={"standard"}
|
||||||
|
allowDecimals={false}
|
||||||
|
allowNegatives={false}
|
||||||
|
addIconButtons={true}
|
||||||
|
InputProps={{
|
||||||
|
startAdornment: (
|
||||||
|
<InputAdornment position="start">
|
||||||
|
<QortalSVG
|
||||||
|
height="20px"
|
||||||
|
width="20px"
|
||||||
|
color={theme.palette.text.primary}
|
||||||
|
/>
|
||||||
|
</InputAdornment>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
error={disableDonation}
|
||||||
|
helperText={helperText}
|
||||||
|
FormHelperTextProps={{ sx: { fontSize: 20 } }}
|
||||||
|
/>
|
||||||
|
</DonateModalCol>
|
||||||
|
{currentBalance ? (
|
||||||
|
<div>You have {currentBalance} QORT</div>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="error"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
resetValues();
|
||||||
|
if (onClose) onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={sendCoin}
|
||||||
|
sx={{
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
disabled={disableDonation}
|
||||||
|
>
|
||||||
|
Send Coin
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</Portal>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
@ -1,37 +1,49 @@
|
|||||||
import AttachFileIcon from "@mui/icons-material/AttachFile";
|
import AttachFileIcon from "@mui/icons-material/AttachFile";
|
||||||
import React, { CSSProperties } from "react";
|
import { Tooltip, Typography } from "@mui/material";
|
||||||
|
import React, { CSSProperties, useEffect, useMemo, useState } from "react";
|
||||||
|
import { getIconsAndLabels } from "../../pages/IssueContent/IssueContent-functions.ts";
|
||||||
|
import { Issue } from "../../state/features/fileSlice.ts";
|
||||||
|
|
||||||
interface IssueIconProps {
|
interface IssueIconProps {
|
||||||
iconSrc: string;
|
iconSrc: string;
|
||||||
|
label?: string;
|
||||||
showBackupIcon?: boolean;
|
showBackupIcon?: boolean;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
}
|
}
|
||||||
export const IssueIcon = ({
|
export const IssueIcon = ({
|
||||||
iconSrc,
|
iconSrc,
|
||||||
|
label,
|
||||||
showBackupIcon = true,
|
showBackupIcon = true,
|
||||||
style,
|
style,
|
||||||
}: IssueIconProps) => {
|
}: IssueIconProps) => {
|
||||||
const displayFileIcon = !iconSrc && showBackupIcon;
|
const displayFileIcon = !iconSrc && showBackupIcon;
|
||||||
|
const widthAndHeight = "50px";
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{iconSrc && (
|
{iconSrc && (
|
||||||
|
<Tooltip
|
||||||
|
title={<Typography fontSize={16}>{label}</Typography>}
|
||||||
|
arrow
|
||||||
|
disableHoverListener={!label}
|
||||||
|
placement={"top"}
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src={iconSrc}
|
src={iconSrc}
|
||||||
width="50px"
|
width={style?.width || widthAndHeight}
|
||||||
height="50px"
|
height={style?.width || widthAndHeight}
|
||||||
style={{
|
style={{
|
||||||
borderRadius: "5px",
|
borderRadius: "5px",
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{displayFileIcon && (
|
{displayFileIcon && (
|
||||||
<AttachFileIcon
|
<AttachFileIcon
|
||||||
sx={{
|
sx={{
|
||||||
...style,
|
...style,
|
||||||
width: "40px",
|
width: style?.width || widthAndHeight,
|
||||||
height: "40px",
|
height: style?.width || widthAndHeight,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -40,20 +52,34 @@ export const IssueIcon = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface IssueIconsProps {
|
interface IssueIconsProps {
|
||||||
iconSources: string[];
|
issueData: Issue;
|
||||||
showBackupIcon?: boolean;
|
showBackupIcon?: boolean;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IssueIcons = ({
|
export const IssueIcons = ({
|
||||||
iconSources,
|
issueData,
|
||||||
showBackupIcon = true,
|
showBackupIcon = true,
|
||||||
style,
|
style,
|
||||||
}: IssueIconsProps) => {
|
}: IssueIconsProps) => {
|
||||||
return iconSources.map((icon, index) => (
|
const [icons, setIcons] = useState<string[]>([]);
|
||||||
|
const [iconLabels, setIconLabels] = useState<string[]>([]);
|
||||||
|
|
||||||
|
useMemo(() => {
|
||||||
|
if (issueData) {
|
||||||
|
getIconsAndLabels(issueData).then(data => {
|
||||||
|
const [iconData, labels] = data;
|
||||||
|
if (iconData) setIcons(iconData);
|
||||||
|
if (labels) setIconLabels(labels);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [issueData]);
|
||||||
|
|
||||||
|
return icons.map((icon, index) => (
|
||||||
<IssueIcon
|
<IssueIcon
|
||||||
key={icon + index}
|
key={icon + index}
|
||||||
iconSrc={icon}
|
iconSrc={icon}
|
||||||
|
label={iconLabels ? iconLabels[index] : ""}
|
||||||
style={{ ...style }}
|
style={{ ...style }}
|
||||||
showBackupIcon={showBackupIcon}
|
showBackupIcon={showBackupIcon}
|
||||||
/>
|
/>
|
||||||
|
@ -1,22 +1,21 @@
|
|||||||
|
import BugReportIcon from "../../assets/icons/Bug-Report-Icon.webp";
|
||||||
|
import ClosedIcon from "../../assets/icons/Closed-Icon.webp";
|
||||||
|
import CompleteIcon from "../../assets/icons/Complete-Icon.webp";
|
||||||
|
import FeatureRequestIcon from "../../assets/icons/Feature-Request-Icon.webp";
|
||||||
|
import InProgressIcon from "../../assets/icons/In-Progress-Icon.webp";
|
||||||
|
|
||||||
|
import OpenIcon from "../../assets/icons/Open-Icon.webp";
|
||||||
|
import QappIcon from "../../assets/icons/Q-App-Icon.webp";
|
||||||
|
import CoreIcon from "../../assets/icons/Qortal-Core-Icon.webp";
|
||||||
|
import UIicon from "../../assets/icons/Qortal-UI-Icon.webp";
|
||||||
|
import TechSupportIcon from "../../assets/icons/Tech-Support-Icon.webp";
|
||||||
|
import UnknownIcon from "../../assets/icons/unknown.webp";
|
||||||
import {
|
import {
|
||||||
Categories,
|
Categories,
|
||||||
Category,
|
Category,
|
||||||
CategoryData,
|
CategoryData,
|
||||||
} from "../../components/common/CategoryList/CategoryList.tsx";
|
} from "../../components/common/CategoryList/CategoryList.tsx";
|
||||||
import { getAllCategoriesWithIcons } from "./CategoryFunctions.ts";
|
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";
|
const issueLocationLabel = "Issue Location";
|
||||||
export const issueLocation: Category[] = [
|
export const issueLocation: Category[] = [
|
||||||
@ -48,7 +47,7 @@ export const secondCategories: Categories = {};
|
|||||||
issueLocation.map(c => (secondCategories[c.id] = issueType));
|
issueLocation.map(c => (secondCategories[c.id] = issueType));
|
||||||
|
|
||||||
const issueLabel = "Issue State";
|
const issueLabel = "Issue State";
|
||||||
export const IssueState = [
|
export const issueState = [
|
||||||
{ id: 101, name: "Open", icon: OpenIcon, label: issueLabel },
|
{ id: 101, name: "Open", icon: OpenIcon, label: issueLabel },
|
||||||
{ id: 102, name: "Closed", icon: ClosedIcon, label: issueLabel },
|
{ id: 102, name: "Closed", icon: ClosedIcon, label: issueLabel },
|
||||||
{ id: 103, name: "In Progress", icon: InProgressIcon, label: issueLabel },
|
{ id: 103, name: "In Progress", icon: InProgressIcon, label: issueLabel },
|
||||||
@ -57,7 +56,16 @@ export const IssueState = [
|
|||||||
|
|
||||||
export const thirdCategories: Categories = {};
|
export const thirdCategories: Categories = {};
|
||||||
|
|
||||||
issueType.map(issueType => (thirdCategories[issueType.id] = IssueState));
|
issueType.map(issueType => (thirdCategories[issueType.id] = issueState));
|
||||||
|
|
||||||
|
export const allCategories = [
|
||||||
|
...issueLocation,
|
||||||
|
...issueType,
|
||||||
|
...issueState,
|
||||||
|
].map(category => ({
|
||||||
|
...category,
|
||||||
|
label: "Single Category",
|
||||||
|
}));
|
||||||
|
|
||||||
export const allCategoryData: CategoryData = {
|
export const allCategoryData: CategoryData = {
|
||||||
category: issueLocation,
|
category: issueLocation,
|
||||||
|
@ -43,7 +43,7 @@ export const findAllCategoryData = (
|
|||||||
categories: string[],
|
categories: string[],
|
||||||
direction: Direction = "forward"
|
direction: Direction = "forward"
|
||||||
) => {
|
) => {
|
||||||
let foundIcons: Category[] = [];
|
const foundIcons: Category[] = [];
|
||||||
if (direction === "backward") categories.reverse();
|
if (direction === "backward") categories.reverse();
|
||||||
|
|
||||||
categories.map(category => {
|
categories.map(category => {
|
||||||
@ -79,6 +79,15 @@ export const getAllCategoriesWithIcons = () => {
|
|||||||
return categoriesWithIcons;
|
return categoriesWithIcons;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getnamesFromObject = (fileObj: any) => {
|
||||||
|
const categories = getCategoriesFromObject(fileObj);
|
||||||
|
const names = categories.map(categoryID => {
|
||||||
|
return iconCategories.find(category => category.id === +categoryID)?.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
return names
|
||||||
|
};
|
||||||
|
|
||||||
export const getIconsFromObject = (fileObj: any) => {
|
export const getIconsFromObject = (fileObj: any) => {
|
||||||
const categories = getCategoriesFromObject(fileObj);
|
const categories = getCategoriesFromObject(fileObj);
|
||||||
const icons = categories.map(categoryID => {
|
const icons = categories.map(categoryID => {
|
||||||
|
@ -10,6 +10,17 @@ export const FEE_BASE = useTestIdentifiers
|
|||||||
? "MYTEST_support_fees"
|
? "MYTEST_support_fees"
|
||||||
: "q_support_fees";
|
: "q_support_fees";
|
||||||
|
|
||||||
|
export const supportedCoins = [
|
||||||
|
"QORT",
|
||||||
|
"BTC",
|
||||||
|
"LTC",
|
||||||
|
"DOGE",
|
||||||
|
"DGB",
|
||||||
|
"RVN",
|
||||||
|
"ARRR",
|
||||||
|
].sort((a, b) => {
|
||||||
|
return a.localeCompare(b);
|
||||||
|
});
|
||||||
export const maxFeePublishTimeDiff = 10; // time in minutes before/after publish when fee is considered valid
|
export const maxFeePublishTimeDiff = 10; // time in minutes before/after publish when fee is considered valid
|
||||||
export type FeeType = "default" | "comment" | "like" | "dislike" | "superlike";
|
export type FeeType = "default" | "comment" | "like" | "dislike" | "superlike";
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
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 { setFeeData } from "../../../state/features/globalSlice.ts";
|
||||||
|
import { store } from "../../../state/store.js";
|
||||||
|
import { objectToBase64 } from "../../../utils/toBase64.ts";
|
||||||
import { useTestIdentifiers } from "../../Identifiers.ts";
|
import { useTestIdentifiers } from "../../Identifiers.ts";
|
||||||
|
import { appName, FEE_BASE, feeAmountBase, FeeType } from "../FeeData.tsx";
|
||||||
|
|
||||||
export type CoinType = "QORT" | "BTC" | "LTC" | "DOGE" | "DGB" | "RVN" | "ARRR";
|
export type CoinType = "QORT" | "BTC" | "LTC" | "DOGE" | "DGB" | "RVN" | "ARRR";
|
||||||
|
|
||||||
|
2
src/global.d.ts
vendored
2
src/global.d.ts
vendored
@ -39,6 +39,8 @@ interface QortalRequestOptions {
|
|||||||
excludeBlocked?: boolean;
|
excludeBlocked?: boolean;
|
||||||
exactMatchNames?: boolean;
|
exactMatchNames?: boolean;
|
||||||
message?: string;
|
message?: string;
|
||||||
|
txType?: string[];
|
||||||
|
confirmationStatus?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare function qortalRequest(options: QortalRequestOptions): Promise<any>;
|
declare function qortalRequest(options: QortalRequestOptions): Promise<any>;
|
||||||
|
@ -1,5 +1,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import {
|
||||||
|
QSUPPORT_FILE_BASE,
|
||||||
|
QSUPPORT_PLAYLIST_BASE,
|
||||||
|
} from "../constants/Identifiers.ts";
|
||||||
|
import { log } from "../constants/Misc.ts";
|
||||||
|
import { verifyAllPayments } from "../constants/PublishFees/VerifyPayment.ts";
|
||||||
import {
|
import {
|
||||||
addFiles,
|
addFiles,
|
||||||
addToHashMap,
|
addToHashMap,
|
||||||
@ -19,13 +25,8 @@ import {
|
|||||||
} from "../state/features/globalSlice";
|
} from "../state/features/globalSlice";
|
||||||
import { RootState } from "../state/store";
|
import { RootState } from "../state/store";
|
||||||
import { fetchAndEvaluateIssues } from "../utils/fetchVideos";
|
import { fetchAndEvaluateIssues } from "../utils/fetchVideos";
|
||||||
import {
|
import { getBountyAmounts } from "../utils/qortalRequests.ts";
|
||||||
QSUPPORT_FILE_BASE,
|
|
||||||
QSUPPORT_PLAYLIST_BASE,
|
|
||||||
} from "../constants/Identifiers.ts";
|
|
||||||
import { queue } from "../wrappers/GlobalWrapper";
|
import { queue } from "../wrappers/GlobalWrapper";
|
||||||
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();
|
||||||
@ -70,7 +71,7 @@ export const useFetchIssues = () => {
|
|||||||
|
|
||||||
const getAvatar = React.useCallback(async (author: string) => {
|
const getAvatar = React.useCallback(async (author: string) => {
|
||||||
try {
|
try {
|
||||||
let url = await qortalRequest({
|
const url = await qortalRequest({
|
||||||
action: "GET_QDN_RESOURCE_URL",
|
action: "GET_QDN_RESOURCE_URL",
|
||||||
name: author,
|
name: author,
|
||||||
service: "THUMBNAIL",
|
service: "THUMBNAIL",
|
||||||
@ -83,14 +84,16 @@ export const useFetchIssues = () => {
|
|||||||
url,
|
url,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} catch (error) {}
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const getIssue = async (
|
const getIssue = async (
|
||||||
user: string,
|
user: string,
|
||||||
issueID: string,
|
issueID: string,
|
||||||
content: any,
|
content: any,
|
||||||
retries: number = 0
|
retries = 0
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const res = await fetchAndEvaluateIssues({
|
const res = await fetchAndEvaluateIssues({
|
||||||
@ -182,6 +185,7 @@ export const useFetchIssues = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
} finally {
|
} finally {
|
||||||
dispatch(setIsLoadingGlobal(false));
|
dispatch(setIsLoadingGlobal(false));
|
||||||
}
|
}
|
||||||
@ -277,11 +281,16 @@ export const useFetchIssues = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const issues = await Promise.all(verifiedIssuePromises);
|
const issues = await Promise.all(verifiedIssuePromises);
|
||||||
const verifiedIssues = await verifyAllPayments(issues);
|
const [verifiedIssues, bountyIssues] = await Promise.all([
|
||||||
|
verifyAllPayments(issues),
|
||||||
|
getBountyAmounts(issues),
|
||||||
|
]);
|
||||||
|
|
||||||
structureData = structureData.map((issue, index) => {
|
structureData = structureData.map((issue, index) => {
|
||||||
return {
|
return {
|
||||||
...issue,
|
...issue,
|
||||||
feeData: verifiedIssues[index]?.feeData,
|
feeData: verifiedIssues[index]?.feeData,
|
||||||
|
bountyData: bountyIssues[index]?.bountyData,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -289,7 +298,6 @@ export const useFetchIssues = () => {
|
|||||||
else dispatch(upsertFiles(structureData));
|
else dispatch(upsertFiles(structureData));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log({ error });
|
console.log({ error });
|
||||||
} finally {
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[videos, hashMapFiles]
|
[videos, hashMapFiles]
|
||||||
@ -349,7 +357,7 @@ export const useFetchIssues = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
} finally {
|
console.log(error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[filteredVideos, hashMapFiles]
|
[filteredVideos, hashMapFiles]
|
||||||
@ -389,12 +397,14 @@ export const useFetchIssues = () => {
|
|||||||
const newArray = responseData.slice(0, findVideo);
|
const newArray = responseData.slice(0, findVideo);
|
||||||
dispatch(setCountNewFiles(newArray.length));
|
dispatch(setCountNewFiles(newArray.length));
|
||||||
return;
|
return;
|
||||||
} catch (error) {}
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
}, [videos]);
|
}, [videos]);
|
||||||
|
|
||||||
const getIssuesCount = 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}`;
|
const url = `/arbitrary/resources/search?mode=ALL&includemetadata=false&limit=0&service=DOCUMENT&identifier=${QSUPPORT_FILE_BASE}`;
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@ -416,7 +426,6 @@ export const useFetchIssues = () => {
|
|||||||
dispatch(setFilesPerNamePublished(filesPerNamePublished));
|
dispatch(setFilesPerNamePublished(filesPerNamePublished));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log({ error });
|
console.log({ error });
|
||||||
} finally {
|
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -1,38 +1,38 @@
|
|||||||
|
import { Box, Grid, Input, useTheme } from "@mui/material";
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { RootState } from "../../state/store";
|
import {
|
||||||
import { IssueList } from "./IssueList.tsx";
|
AutocompleteQappNames,
|
||||||
import { Box, Grid, Input, useTheme } from "@mui/material";
|
getPublishedQappNames,
|
||||||
import { useFetchIssues } from "../../hooks/useFetchIssues.tsx";
|
QappNamesRef,
|
||||||
|
} from "../../components/common/AutocompleteQappNames.tsx";
|
||||||
|
import {
|
||||||
|
CategoryList,
|
||||||
|
CategoryListRef,
|
||||||
|
getCategoriesFetchString,
|
||||||
|
} from "../../components/common/CategoryList/CategoryList.tsx";
|
||||||
|
import {
|
||||||
|
CategorySelect,
|
||||||
|
CategorySelectRef,
|
||||||
|
} from "../../components/common/CategoryList/CategorySelect.tsx";
|
||||||
import LazyLoad from "../../components/common/LazyLoad";
|
import LazyLoad from "../../components/common/LazyLoad";
|
||||||
import { FiltersCol, FiltersContainer } from "./IssueList-styles.tsx";
|
import { StatsData } from "../../components/StatsData.tsx";
|
||||||
import { SubtitleContainer, ThemeButton } from "./Home-styles";
|
import {
|
||||||
|
allCategories,
|
||||||
|
allCategoryData,
|
||||||
|
} from "../../constants/Categories/Categories.ts";
|
||||||
|
import { useFetchIssues } from "../../hooks/useFetchIssues.tsx";
|
||||||
import {
|
import {
|
||||||
changefilterName,
|
changefilterName,
|
||||||
changefilterSearch,
|
changefilterSearch,
|
||||||
changeFilterType,
|
changeFilterType,
|
||||||
setQappNames,
|
setQappNames,
|
||||||
} from "../../state/features/fileSlice.ts";
|
} from "../../state/features/fileSlice.ts";
|
||||||
import {
|
import { RootState } from "../../state/store";
|
||||||
allCategoryData,
|
import { SubtitleContainer, ThemeButton } from "./Home-styles";
|
||||||
IssueState,
|
import { FiltersCol, FiltersContainer } from "./IssueList-styles.tsx";
|
||||||
} from "../../constants/Categories/Categories.ts";
|
import { IssueList } from "./IssueList.tsx";
|
||||||
import {
|
|
||||||
CategoryList,
|
|
||||||
CategoryListRef,
|
|
||||||
getCategoriesFetchString,
|
|
||||||
} from "../../components/common/CategoryList/CategoryList.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;
|
||||||
@ -109,11 +109,12 @@ export const Home = ({ mode }: HomeProps) => {
|
|||||||
const selectedCategories =
|
const selectedCategories =
|
||||||
categoryListRef.current?.getSelectedCategories() || [];
|
categoryListRef.current?.getSelectedCategories() || [];
|
||||||
const issueType = categorySelectRef?.current?.getSelectedCategory();
|
const issueType = categorySelectRef?.current?.getSelectedCategory();
|
||||||
if (issueType) selectedCategories[2] = issueType;
|
let categoriesString = getCategoriesFetchString(selectedCategories);
|
||||||
|
if (issueType) categoriesString = ":" + issueType + ";";
|
||||||
await getIssues(
|
await getIssues(
|
||||||
{
|
{
|
||||||
name: filterName,
|
name: filterName,
|
||||||
categories: getCategoriesFetchString(selectedCategories),
|
categories: categoriesString,
|
||||||
QappName: autocompleteRef?.current?.getQappNameFetchString(),
|
QappName: autocompleteRef?.current?.getQappNameFetchString(),
|
||||||
keywords: filterSearch,
|
keywords: filterSearch,
|
||||||
type: filterType,
|
type: filterType,
|
||||||
@ -168,32 +169,6 @@ export const Home = ({ mode }: HomeProps) => {
|
|||||||
isFilterMode.current = false;
|
isFilterMode.current = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// const interval = useRef<any>(null);
|
|
||||||
|
|
||||||
// const checkNewVideosFunc = useCallback(() => {
|
|
||||||
// let isCalling = false;
|
|
||||||
// interval.current = setInterval(async () => {
|
|
||||||
// if (isCalling || !firstFetch.current) return;
|
|
||||||
// isCalling = true;
|
|
||||||
// await checkNewVideos();
|
|
||||||
// isCalling = false;
|
|
||||||
// }, 30000); // 1 second interval
|
|
||||||
// }, [checkNewVideos]);
|
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (isFiltering && interval.current) {
|
|
||||||
// clearInterval(interval.current);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// checkNewVideosFunc();
|
|
||||||
|
|
||||||
// return () => {
|
|
||||||
// if (interval?.current) {
|
|
||||||
// clearInterval(interval.current);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// }, [mode, checkNewVideosFunc, isFiltering]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!firstFetch.current &&
|
!firstFetch.current &&
|
||||||
@ -294,7 +269,7 @@ export const Home = ({ mode }: HomeProps) => {
|
|||||||
)}
|
)}
|
||||||
{showCategorySelect && (
|
{showCategorySelect && (
|
||||||
<CategorySelect
|
<CategorySelect
|
||||||
categoryData={IssueState}
|
categoryData={allCategories}
|
||||||
ref={categorySelectRef}
|
ref={categorySelectRef}
|
||||||
sx={{ marginTop: "20px" }}
|
sx={{ marginTop: "20px" }}
|
||||||
afterChange={value => {
|
afterChange={value => {
|
||||||
|
@ -1,4 +1,26 @@
|
|||||||
import { Avatar, Box, Skeleton } from "@mui/material";
|
import BlockIcon from "@mui/icons-material/Block";
|
||||||
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
|
import { Avatar, Box, Skeleton, useTheme } from "@mui/material";
|
||||||
|
import React, { useMemo, useState } from "react";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import QORTicon from "../../assets/icons/CoinIcons/qort.png";
|
||||||
|
import { BountyDisplay } from "../../components/common/BountyDisplay.tsx";
|
||||||
|
import { IssueIcon, IssueIcons } from "../../components/common/IssueIcon.tsx";
|
||||||
|
import {
|
||||||
|
getIconsFromObject,
|
||||||
|
getnamesFromObject,
|
||||||
|
} from "../../constants/Categories/CategoryFunctions.ts";
|
||||||
|
import { fontSizeExLarge } from "../../constants/Misc.ts";
|
||||||
|
import {
|
||||||
|
blockUser,
|
||||||
|
Issue,
|
||||||
|
setEditFile,
|
||||||
|
} from "../../state/features/fileSlice.ts";
|
||||||
|
import { RootState } from "../../state/store.ts";
|
||||||
|
import { BountyData } from "../../utils/qortalRequests.ts";
|
||||||
|
import { formatDate } from "../../utils/time.ts";
|
||||||
import {
|
import {
|
||||||
BlockIconContainer,
|
BlockIconContainer,
|
||||||
IconsBox,
|
IconsBox,
|
||||||
@ -9,24 +31,6 @@ import {
|
|||||||
VideoCardTitle,
|
VideoCardTitle,
|
||||||
VideoUploadDate,
|
VideoUploadDate,
|
||||||
} from "./IssueList-styles.tsx";
|
} from "./IssueList-styles.tsx";
|
||||||
import EditIcon from "@mui/icons-material/Edit";
|
|
||||||
import {
|
|
||||||
blockUser,
|
|
||||||
Issue,
|
|
||||||
setEditFile,
|
|
||||||
} from "../../state/features/fileSlice.ts";
|
|
||||||
import BlockIcon from "@mui/icons-material/Block";
|
|
||||||
import { formatBytes } from "../IssueContent/IssueContent.tsx";
|
|
||||||
import { formatDate } from "../../utils/time.ts";
|
|
||||||
import React, { useMemo, useState } from "react";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import { RootState } from "../../state/store.ts";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { getIconsFromObject } from "../../constants/Categories/CategoryFunctions.ts";
|
|
||||||
|
|
||||||
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: Issue[];
|
issues: Issue[];
|
||||||
@ -35,7 +39,7 @@ export const IssueList = ({ issues }: FileListProps) => {
|
|||||||
const hashMapIssues = useSelector(
|
const hashMapIssues = useSelector(
|
||||||
(state: RootState) => state.file.hashMapFiles
|
(state: RootState) => state.file.hashMapFiles
|
||||||
);
|
);
|
||||||
|
const theme = useTheme();
|
||||||
const [showIcons, setShowIcons] = useState(null);
|
const [showIcons, setShowIcons] = useState(null);
|
||||||
const username = useSelector((state: RootState) => state.auth?.user?.name);
|
const username = useSelector((state: RootState) => state.auth?.user?.name);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -54,7 +58,9 @@ export const IssueList = ({ issues }: FileListProps) => {
|
|||||||
if (response === true) {
|
if (response === true) {
|
||||||
dispatch(blockUser(user));
|
dispatch(blockUser(user));
|
||||||
}
|
}
|
||||||
} catch (error) {}
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredIssues = useMemo(() => {
|
const filteredIssues = useMemo(() => {
|
||||||
@ -71,12 +77,11 @@ export const IssueList = ({ issues }: FileListProps) => {
|
|||||||
issueObj = existingFile;
|
issueObj = existingFile;
|
||||||
hasHash = true;
|
hasHash = true;
|
||||||
}
|
}
|
||||||
|
const bountyData: BountyData = {
|
||||||
|
...issueObj.bountyData,
|
||||||
|
...issue.bountyData,
|
||||||
|
};
|
||||||
|
|
||||||
const issueIcons = getIconsFromObject(issueObj);
|
|
||||||
const fileBytes = issueObj?.files.reduce(
|
|
||||||
(acc, cur) => acc + (cur?.size || 0),
|
|
||||||
0
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@ -142,24 +147,34 @@ export const IssueList = ({ issues }: FileListProps) => {
|
|||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
width: "200px",
|
width: "280px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IssueIcons
|
<IssueIcons
|
||||||
iconSources={issueIcons}
|
issueData={issueObj}
|
||||||
style={{ marginRight: "20px" }}
|
style={{ marginRight: "20px" }}
|
||||||
showBackupIcon={true}
|
showBackupIcon={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<VideoCardTitle
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
width: "150px",
|
display: "flex",
|
||||||
fontSize: fontSizeMedium,
|
justifyContent: "left",
|
||||||
|
alignItems: "center",
|
||||||
|
width: "250px",
|
||||||
|
fontSize: fontSizeExLarge,
|
||||||
|
fontFamily: "Cairo",
|
||||||
|
letterSpacing: "0.4px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{fileBytes > 0 && formatBytes(fileBytes)}
|
<BountyDisplay
|
||||||
</VideoCardTitle>
|
bountyData={bountyData}
|
||||||
<VideoCardTitle sx={{ fontWeight: "bold", width: "500px" }}>
|
divStyle={{ marginLeft: "20px" }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<VideoCardTitle sx={{ fontWeight: "bold", width: "400px" }}>
|
||||||
{issueObj.title}
|
{issueObj.title}
|
||||||
</VideoCardTitle>
|
</VideoCardTitle>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -1,11 +1,21 @@
|
|||||||
import React, { useEffect, useRef, useState } from "react";
|
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
|
||||||
import { useSelector } from "react-redux";
|
|
||||||
import { RootState } from "../../state/store";
|
|
||||||
|
|
||||||
import { Avatar, Box, Skeleton, useTheme } from "@mui/material";
|
import { Avatar, Box, Skeleton, useTheme } from "@mui/material";
|
||||||
import { useFetchIssues } from "../../hooks/useFetchIssues.tsx";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import QORTicon from "../../assets/icons/CoinIcons/qort.png";
|
||||||
|
import { BountyDisplay } from "../../components/common/BountyDisplay.tsx";
|
||||||
|
import { IssueIcon, IssueIcons } from "../../components/common/IssueIcon.tsx";
|
||||||
import LazyLoad from "../../components/common/LazyLoad";
|
import LazyLoad from "../../components/common/LazyLoad";
|
||||||
|
import { getIconsFromObject } from "../../constants/Categories/CategoryFunctions.ts";
|
||||||
|
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
|
||||||
|
import { fontSizeExLarge } from "../../constants/Misc.ts";
|
||||||
|
import { verifyAllPayments } from "../../constants/PublishFees/VerifyPayment.ts";
|
||||||
|
import { useFetchIssues } from "../../hooks/useFetchIssues.tsx";
|
||||||
|
import { Issue } from "../../state/features/fileSlice.ts";
|
||||||
|
import { RootState } from "../../state/store";
|
||||||
|
import { BountyData, getBountyAmounts } from "../../utils/qortalRequests.ts";
|
||||||
|
import { formatDate } from "../../utils/time";
|
||||||
|
import { queue } from "../../wrappers/GlobalWrapper";
|
||||||
import {
|
import {
|
||||||
BottomParent,
|
BottomParent,
|
||||||
IssueCard,
|
IssueCard,
|
||||||
@ -15,15 +25,6 @@ import {
|
|||||||
VideoCardTitle,
|
VideoCardTitle,
|
||||||
VideoUploadDate,
|
VideoUploadDate,
|
||||||
} from "./IssueList-styles.tsx";
|
} from "./IssueList-styles.tsx";
|
||||||
import { formatDate } from "../../utils/time";
|
|
||||||
import { Issue } from "../../state/features/fileSlice.ts";
|
|
||||||
import { queue } from "../../wrappers/GlobalWrapper";
|
|
||||||
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
|
|
||||||
import { formatBytes } from "../IssueContent/IssueContent.tsx";
|
|
||||||
import { getIconsFromObject } from "../../constants/Categories/CategoryFunctions.ts";
|
|
||||||
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;
|
||||||
@ -97,9 +98,11 @@ export const IssueListComponentLevel = ({ mode }: VideoListProps) => {
|
|||||||
|
|
||||||
const issueData = await Promise.all(verifiedIssuePromises);
|
const issueData = await Promise.all(verifiedIssuePromises);
|
||||||
const verifiedIssues = await verifyAllPayments(issueData);
|
const verifiedIssues = await verifyAllPayments(issueData);
|
||||||
setIssues(verifiedIssues);
|
const bountyIssues = await getBountyAmounts(verifiedIssues);
|
||||||
|
console.log("bountyIssues: ", bountyIssues);
|
||||||
|
setIssues(bountyIssues);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
} finally {
|
console.log(error);
|
||||||
}
|
}
|
||||||
}, [issues, hashMapVideos]);
|
}, [issues, hashMapVideos]);
|
||||||
|
|
||||||
@ -140,12 +143,10 @@ export const IssueListComponentLevel = ({ mode }: VideoListProps) => {
|
|||||||
issueObj = existingFile;
|
issueObj = existingFile;
|
||||||
hasHash = true;
|
hasHash = true;
|
||||||
}
|
}
|
||||||
|
const bountyData: BountyData = {
|
||||||
const issueIcons = getIconsFromObject(issueObj);
|
...issueObj.bountyData,
|
||||||
const fileBytes = issueObj?.files.reduce(
|
...issue.bountyData,
|
||||||
(acc, cur) => acc + (cur?.size || 0),
|
};
|
||||||
0
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@ -183,22 +184,32 @@ export const IssueListComponentLevel = ({ mode }: VideoListProps) => {
|
|||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
width: "200px",
|
width: "280px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<IssueIcons
|
<IssueIcons
|
||||||
iconSources={issueIcons}
|
issueData={issueObj}
|
||||||
style={{ marginRight: "20px" }}
|
style={{ marginRight: "20px" }}
|
||||||
showBackupIcon={true}
|
showBackupIcon={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<VideoCardTitle
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
width: "100px",
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
width: "250px",
|
||||||
|
fontSize: fontSizeExLarge,
|
||||||
|
fontFamily: "Cairo",
|
||||||
|
letterSpacing: "0.4px",
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
userSelect: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{fileBytes > 0 && formatBytes(fileBytes)}
|
<BountyDisplay
|
||||||
</VideoCardTitle>
|
bountyData={bountyData}
|
||||||
|
divStyle={{ marginLeft: "20px" }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
<VideoCardTitle
|
<VideoCardTitle
|
||||||
sx={{ fontWeight: "bold", width: "500px" }}
|
sx={{ fontWeight: "bold", width: "500px" }}
|
||||||
>
|
>
|
||||||
@ -213,13 +224,20 @@ export const IssueListComponentLevel = ({ mode }: VideoListProps) => {
|
|||||||
)}
|
)}
|
||||||
<BottomParent>
|
<BottomParent>
|
||||||
<NameAndDateContainer
|
<NameAndDateContainer
|
||||||
|
sx={{ width: "200px", height: "100%" }}
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
navigate(`/channel/${issueObj?.user}`);
|
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/${issueObj?.user}/qortal_avatar`}
|
src={`/arbitrary/THUMBNAIL/${issueObj?.user}/qortal_avatar`}
|
||||||
alt={`${issueObj?.user}'s avatar`}
|
alt={`${issueObj?.user}'s avatar`}
|
||||||
/>
|
/>
|
||||||
@ -232,13 +250,14 @@ export const IssueListComponentLevel = ({ mode }: VideoListProps) => {
|
|||||||
>
|
>
|
||||||
{issueObj?.user}
|
{issueObj?.user}
|
||||||
</VideoCardName>
|
</VideoCardName>
|
||||||
</NameAndDateContainer>
|
</div>
|
||||||
|
|
||||||
{issueObj?.created && (
|
{issueObj?.created && (
|
||||||
<VideoUploadDate>
|
<VideoUploadDate>
|
||||||
{formatDate(issueObj.created)}
|
{formatDate(issueObj.created)}
|
||||||
</VideoUploadDate>
|
</VideoUploadDate>
|
||||||
)}
|
)}
|
||||||
|
</NameAndDateContainer>
|
||||||
</BottomParent>
|
</BottomParent>
|
||||||
</IssueCard>
|
</IssueCard>
|
||||||
</>
|
</>
|
||||||
|
65
src/pages/IssueContent/IssueContent-functions.ts
Normal file
65
src/pages/IssueContent/IssueContent-functions.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import {
|
||||||
|
Category,
|
||||||
|
getCategoriesFromObject,
|
||||||
|
} from "../../components/common/CategoryList/CategoryList.tsx";
|
||||||
|
import { allCategoryData } from "../../constants/Categories/Categories.ts";
|
||||||
|
import {
|
||||||
|
getIconsFromObject,
|
||||||
|
getnamesFromObject,
|
||||||
|
} from "../../constants/Categories/CategoryFunctions.ts";
|
||||||
|
import { getAvatarFromName } from "../../utils/qortalRequests.ts";
|
||||||
|
export const getIconsAndLabels = async issueData => {
|
||||||
|
if (issueData) {
|
||||||
|
const tempIcons = getIconsFromObject(issueData);
|
||||||
|
const tempIconLabels = getnamesFromObject(issueData);
|
||||||
|
|
||||||
|
const QappName = issueData?.QappName;
|
||||||
|
if (QappName) {
|
||||||
|
const QappIcon = await getAvatarFromName(QappName);
|
||||||
|
tempIcons.push(QappIcon);
|
||||||
|
tempIconLabels.push(QappName);
|
||||||
|
}
|
||||||
|
return [tempIcons, tempIconLabels];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const categoryNamesToString = (
|
||||||
|
categoryNames: string[],
|
||||||
|
QappName: string
|
||||||
|
) => {
|
||||||
|
const filteredCategoryNames = categoryNames.filter(name => name);
|
||||||
|
let categoryDisplay = "";
|
||||||
|
const separator = " > ";
|
||||||
|
|
||||||
|
filteredCategoryNames.map((name, index) => {
|
||||||
|
if (QappName && index === 1) {
|
||||||
|
categoryDisplay += QappName + separator;
|
||||||
|
}
|
||||||
|
categoryDisplay += name;
|
||||||
|
|
||||||
|
if (index !== filteredCategoryNames.length - 1)
|
||||||
|
categoryDisplay += separator;
|
||||||
|
});
|
||||||
|
return categoryDisplay;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCategoryNames = issueData => {
|
||||||
|
const categoryList = getCategoriesFromObject(issueData);
|
||||||
|
return categoryList.map((categoryID, index) => {
|
||||||
|
let categoryName: Category;
|
||||||
|
if (index === 0) {
|
||||||
|
categoryName = allCategoryData.category.find(
|
||||||
|
item => item?.id === +categoryList[0]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const subCategories = allCategoryData.subCategories[index - 1];
|
||||||
|
const selectedSubCategory = subCategories[categoryList[index - 1]];
|
||||||
|
if (selectedSubCategory) {
|
||||||
|
categoryName = selectedSubCategory.find(
|
||||||
|
item => item?.id === +categoryList[index]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return categoryName?.name;
|
||||||
|
});
|
||||||
|
};
|
@ -1,9 +1,9 @@
|
|||||||
import { styled } from "@mui/system";
|
|
||||||
import { Box, Typography } from "@mui/material";
|
import { Box, Typography } from "@mui/material";
|
||||||
|
import { styled } from "@mui/system";
|
||||||
|
|
||||||
export const FilePlayerContainer = styled(Box)(({ theme }) => ({
|
export const FilePlayerContainer = styled(Box)(({ theme }) => ({
|
||||||
maxWidth: "95%",
|
maxWidth: "95%",
|
||||||
width: "1000px",
|
width: "90vw",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
alignItems: "flex-start",
|
alignItems: "flex-start",
|
||||||
@ -11,7 +11,6 @@ export const FilePlayerContainer = styled(Box)(({ theme }) => ({
|
|||||||
|
|
||||||
export const FileTitle = styled(Typography)(({ theme }) => ({
|
export const FileTitle = styled(Typography)(({ theme }) => ({
|
||||||
fontFamily: "Raleway",
|
fontFamily: "Raleway",
|
||||||
fontSize: "20px",
|
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
|
@ -1,11 +1,40 @@
|
|||||||
|
import DownloadIcon from "@mui/icons-material/Download";
|
||||||
|
import { Avatar, Box, Typography, useTheme } from "@mui/material";
|
||||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { setIsLoadingGlobal } from "../../state/features/globalSlice";
|
import QORTicon from "../../assets/icons/CoinIcons/qort.png";
|
||||||
import { Avatar, Box, Typography, useTheme } from "@mui/material";
|
import { BountyDisplay } from "../../components/common/BountyDisplay.tsx";
|
||||||
import { RootState } from "../../state/store";
|
import {
|
||||||
|
Category,
|
||||||
|
getCategoriesFromObject,
|
||||||
|
} from "../../components/common/CategoryList/CategoryList.tsx";
|
||||||
|
import { CommentSection } from "../../components/common/Comments/CommentSection";
|
||||||
|
import { Donate } from "../../components/common/Donate/Donate.tsx";
|
||||||
|
import FileElement from "../../components/common/FileElement";
|
||||||
|
import { IssueIcon, IssueIcons } from "../../components/common/IssueIcon.tsx";
|
||||||
|
import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml";
|
||||||
|
import { allCategoryData } from "../../constants/Categories/Categories.ts";
|
||||||
|
import {
|
||||||
|
getIconsFromObject,
|
||||||
|
getnamesFromObject,
|
||||||
|
} from "../../constants/Categories/CategoryFunctions.ts";
|
||||||
|
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
|
||||||
|
import { fontSizeExLarge } from "../../constants/Misc.ts";
|
||||||
|
import {
|
||||||
|
appendIsPaidToFeeData,
|
||||||
|
verifyPayment,
|
||||||
|
} from "../../constants/PublishFees/VerifyPayment.ts";
|
||||||
import { addToHashMap } from "../../state/features/fileSlice.ts";
|
import { addToHashMap } from "../../state/features/fileSlice.ts";
|
||||||
import DownloadIcon from "@mui/icons-material/Download";
|
import { setIsLoadingGlobal } from "../../state/features/globalSlice";
|
||||||
|
import { RootState } from "../../state/store";
|
||||||
|
import { getAvatarFromName } from "../../utils/qortalRequests.ts";
|
||||||
|
import { formatDate } from "../../utils/time";
|
||||||
|
import {
|
||||||
|
categoryNamesToString,
|
||||||
|
getCategoryNames,
|
||||||
|
getIconsAndLabels,
|
||||||
|
} from "./IssueContent-functions.ts";
|
||||||
import {
|
import {
|
||||||
AuthorTextComment,
|
AuthorTextComment,
|
||||||
FileAttachmentContainer,
|
FileAttachmentContainer,
|
||||||
@ -18,23 +47,6 @@ import {
|
|||||||
StyledCardColComment,
|
StyledCardColComment,
|
||||||
StyledCardHeaderComment,
|
StyledCardHeaderComment,
|
||||||
} from "./IssueContent-styles.tsx";
|
} from "./IssueContent-styles.tsx";
|
||||||
import { formatDate } from "../../utils/time";
|
|
||||||
import { CommentSection } from "../../components/common/Comments/CommentSection";
|
|
||||||
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
|
|
||||||
import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml";
|
|
||||||
import FileElement from "../../components/common/FileElement";
|
|
||||||
import { allCategoryData } from "../../constants/Categories/Categories.ts";
|
|
||||||
import {
|
|
||||||
Category,
|
|
||||||
getCategoriesFromObject,
|
|
||||||
} from "../../components/common/CategoryList/CategoryList.tsx";
|
|
||||||
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: number, decimals = 2) {
|
export function formatBytes(bytes: number, decimals = 2) {
|
||||||
if (bytes === 0) return "0 Bytes";
|
if (bytes === 0) return "0 Bytes";
|
||||||
@ -55,7 +67,6 @@ export const IssueContent = () => {
|
|||||||
const [descriptionHeight, setDescriptionHeight] = useState<null | number>(
|
const [descriptionHeight, setDescriptionHeight] = useState<null | number>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
const [issueIcons, setIssueIcons] = useState<string[]>([]);
|
|
||||||
const userAvatarHash = useSelector(
|
const userAvatarHash = useSelector(
|
||||||
(state: RootState) => state.global.userAvatarHash
|
(state: RootState) => state.global.userAvatarHash
|
||||||
);
|
);
|
||||||
@ -99,7 +110,7 @@ export const IssueContent = () => {
|
|||||||
}, [issueData]);
|
}, [issueData]);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const getVideoData = React.useCallback(async (name: string, id: string) => {
|
const getIssueData = React.useCallback(async (name: string, id: string) => {
|
||||||
try {
|
try {
|
||||||
if (!name || !id) return;
|
if (!name || !id) return;
|
||||||
dispatch(setIsLoadingGlobal(true));
|
dispatch(setIsLoadingGlobal(true));
|
||||||
@ -142,17 +153,16 @@ export const IssueContent = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
verifyPayment(combinedData).then(feeData => {
|
verifyPayment(combinedData).then(feeData => {
|
||||||
console.log(
|
const dataWithFees = appendIsPaidToFeeData(combinedData, feeData);
|
||||||
"async data: ",
|
console.log("dataWithFees: ", dataWithFees);
|
||||||
appendIsPaidToFeeData(combinedData, feeData)
|
setIssueData(dataWithFees);
|
||||||
);
|
|
||||||
setIssueData(appendIsPaidToFeeData(combinedData, feeData));
|
|
||||||
dispatch(addToHashMap(combinedData));
|
dispatch(addToHashMap(combinedData));
|
||||||
checkforPlaylist(name, id, combinedData?.code);
|
checkforPlaylist(name, id, combinedData?.code);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
} finally {
|
} finally {
|
||||||
dispatch(setIsLoadingGlobal(false));
|
dispatch(setIsLoadingGlobal(false));
|
||||||
}
|
}
|
||||||
@ -212,7 +222,7 @@ export const IssueContent = () => {
|
|||||||
const responseDataSearchVid = await response.json();
|
const responseDataSearchVid = await response.json();
|
||||||
|
|
||||||
if (responseDataSearchVid?.length > 0) {
|
if (responseDataSearchVid?.length > 0) {
|
||||||
let resourceData2 = responseDataSearchVid[0];
|
const resourceData2 = responseDataSearchVid[0];
|
||||||
videos.push(resourceData2);
|
videos.push(resourceData2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -221,10 +231,12 @@ export const IssueContent = () => {
|
|||||||
setPlaylistData(combinedData);
|
setPlaylistData(combinedData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {}
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
if (name && id) {
|
if (name && id) {
|
||||||
const existingVideo = hashMapVideos[id];
|
const existingVideo = hashMapVideos[id];
|
||||||
|
|
||||||
@ -234,42 +246,11 @@ export const IssueContent = () => {
|
|||||||
checkforPlaylist(name, id, existingVideo?.code);
|
checkforPlaylist(name, id, existingVideo?.code);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
getVideoData(name, id);
|
getIssueData(name, id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [id, name]);
|
}, [id, name]);
|
||||||
|
|
||||||
// const getAvatar = React.useCallback(async (author: string) => {
|
|
||||||
// try {
|
|
||||||
// let url = await qortalRequest({
|
|
||||||
// action: 'GET_QDN_RESOURCE_URL',
|
|
||||||
// name: author,
|
|
||||||
// service: 'THUMBNAIL',
|
|
||||||
// identifier: 'qortal_avatar'
|
|
||||||
// })
|
|
||||||
|
|
||||||
// setAvatarUrl(url)
|
|
||||||
// dispatch(setUserAvatarHash({
|
|
||||||
// name: author,
|
|
||||||
// url
|
|
||||||
// }))
|
|
||||||
// } catch (error) { }
|
|
||||||
// }, [])
|
|
||||||
|
|
||||||
// React.useEffect(() => {
|
|
||||||
// if (name && !avatarUrl) {
|
|
||||||
// const existingAvatar = userAvatarHash[name]
|
|
||||||
|
|
||||||
// if (existingAvatar) {
|
|
||||||
// setAvatarUrl(existingAvatar)
|
|
||||||
// } else {
|
|
||||||
// getAvatar(name)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// }
|
|
||||||
|
|
||||||
// }, [name, userAvatarHash])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (contentRef.current) {
|
if (contentRef.current) {
|
||||||
const height = contentRef.current.offsetHeight;
|
const height = contentRef.current.offsetHeight;
|
||||||
@ -279,49 +260,14 @@ export const IssueContent = () => {
|
|||||||
setDescriptionHeight(maxDescriptionHeight);
|
setDescriptionHeight(maxDescriptionHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (issueData) {
|
|
||||||
const icons = getIconsFromObject(issueData);
|
|
||||||
setIssueIcons(icons);
|
|
||||||
}
|
|
||||||
}, [issueData]);
|
}, [issueData]);
|
||||||
|
|
||||||
const categoriesDisplay = useMemo(() => {
|
const categoriesDisplay = useMemo(() => {
|
||||||
if (issueData) {
|
if (issueData) {
|
||||||
const categoryList = getCategoriesFromObject(issueData);
|
const categoryNames = getCategoryNames(issueData);
|
||||||
const categoryNames = categoryList.map((categoryID, index) => {
|
return categoryNamesToString(categoryNames, issueData?.QappName);
|
||||||
let categoryName: Category;
|
|
||||||
if (index === 0) {
|
|
||||||
categoryName = allCategoryData.category.find(
|
|
||||||
item => item?.id === +categoryList[0]
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const subCategories = allCategoryData.subCategories[index - 1];
|
|
||||||
const selectedSubCategory = subCategories[categoryList[index - 1]];
|
|
||||||
if (selectedSubCategory) {
|
|
||||||
categoryName = selectedSubCategory.find(
|
|
||||||
item => item?.id === +categoryList[index]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
return "No Issue Data";
|
||||||
return categoryName?.name;
|
|
||||||
});
|
|
||||||
const filteredCategoryNames = categoryNames.filter(name => name);
|
|
||||||
let categoryDisplay = "";
|
|
||||||
const separator = " > ";
|
|
||||||
const QappName = issueData?.QappName || "";
|
|
||||||
|
|
||||||
filteredCategoryNames.map((name, index) => {
|
|
||||||
if (QappName && index === 1) {
|
|
||||||
categoryDisplay += QappName + separator;
|
|
||||||
}
|
|
||||||
categoryDisplay += name;
|
|
||||||
|
|
||||||
if (index !== filteredCategoryNames.length - 1)
|
|
||||||
categoryDisplay += separator;
|
|
||||||
});
|
|
||||||
return categoryDisplay;
|
|
||||||
}
|
|
||||||
return "no videodata";
|
|
||||||
}, [issueData]);
|
}, [issueData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -347,10 +293,12 @@ export const IssueContent = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ display: "flex", alignItems: "center" }}>
|
<div style={{ display: "flex", alignItems: "center" }}>
|
||||||
|
{issueData && (
|
||||||
<IssueIcons
|
<IssueIcons
|
||||||
iconSources={issueIcons}
|
issueData={issueData}
|
||||||
style={{ marginRight: "20px" }}
|
style={{ marginRight: "20px" }}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<FileTitle
|
<FileTitle
|
||||||
variant="h1"
|
variant="h1"
|
||||||
@ -367,9 +315,9 @@ export const IssueContent = () => {
|
|||||||
</div>
|
</div>
|
||||||
{issueData?.created && (
|
{issueData?.created && (
|
||||||
<Typography
|
<Typography
|
||||||
variant="h6"
|
variant="h4"
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: "12px",
|
fontSize: "18px",
|
||||||
}}
|
}}
|
||||||
color={theme.palette.text.primary}
|
color={theme.palette.text.primary}
|
||||||
>
|
>
|
||||||
@ -413,11 +361,33 @@ export const IssueContent = () => {
|
|||||||
</StyledCardHeaderComment>
|
</StyledCardHeaderComment>
|
||||||
</Box>
|
</Box>
|
||||||
<Spacer height="15px" />
|
<Spacer height="15px" />
|
||||||
|
<Box sx={{ display: "flex", direction: "row", alignItems: "center" }}>
|
||||||
|
{issueData?.bountyData && (
|
||||||
|
<div style={{ fontWeight: "bold" }}>
|
||||||
|
<BountyDisplay
|
||||||
|
bountyData={issueData?.bountyData}
|
||||||
|
timeDisplay={"BOTH"}
|
||||||
|
fontStyle={{ fontSize: fontSizeExLarge, fontWeight: "bold" }}
|
||||||
|
/>
|
||||||
|
{(issueData?.bountyData?.sourceCodeLink ||
|
||||||
|
issueData?.bountyData?.crowdfundLink) && (
|
||||||
|
<>
|
||||||
|
<div>Relevant Links:</div>
|
||||||
|
<div style={{ fontWeight: "bold" }}>
|
||||||
|
<div>{issueData?.bountyData?.crowdfundLink}</div>
|
||||||
|
<div>{issueData?.bountyData?.sourceCodeLink}</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Donate crowdfundLink={issueData?.bountyData?.crowdfundLink} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Spacer height="15px" />
|
||||||
<Box>
|
<Box>
|
||||||
<Typography
|
<Typography
|
||||||
sx={{
|
sx={{
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
fontSize: "16px",
|
|
||||||
userSelect: "none",
|
userSelect: "none",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { createSlice } from "@reduxjs/toolkit";
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
import { PublishFeeData } from "../../constants/PublishFees/SendFeeFunctions.ts";
|
import { PublishFeeData } from "../../constants/PublishFees/SendFeeFunctions.ts";
|
||||||
|
import { BountyData } from "../../utils/qortalRequests.ts";
|
||||||
|
|
||||||
interface GlobalState {
|
interface GlobalState {
|
||||||
files: Issue[];
|
files: Issue[];
|
||||||
@ -47,6 +48,7 @@ export interface Issue {
|
|||||||
isValid?: boolean;
|
isValid?: boolean;
|
||||||
code?: string;
|
code?: string;
|
||||||
feeData?: PublishFeeData;
|
feeData?: PublishFeeData;
|
||||||
|
bountyData?: BountyData;
|
||||||
paymentVerified?: boolean;
|
paymentVerified?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { configureStore } from "@reduxjs/toolkit";
|
import { configureStore } from "@reduxjs/toolkit";
|
||||||
import notificationsReducer from "./features/notificationsSlice";
|
import authReducer from "./features/authSlice.js";
|
||||||
import authReducer from "./features/authSlice";
|
|
||||||
import globalReducer from "./features/globalSlice";
|
|
||||||
import fileReducer from "./features/fileSlice.ts";
|
import fileReducer from "./features/fileSlice.ts";
|
||||||
|
import globalReducer from "./features/globalSlice.js";
|
||||||
|
import notificationsReducer from "./features/notificationsSlice.js";
|
||||||
|
|
||||||
export const store = configureStore({
|
export const store = configureStore({
|
||||||
reducer: {
|
reducer: {
|
||||||
|
165
src/utils/BoundedNumericTextField.tsx
Normal file
165
src/utils/BoundedNumericTextField.tsx
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
import RemoveIcon from "@mui/icons-material/Remove";
|
||||||
|
import {
|
||||||
|
IconButton,
|
||||||
|
InputAdornment,
|
||||||
|
TextField,
|
||||||
|
TextFieldProps,
|
||||||
|
} from "@mui/material";
|
||||||
|
import React, { useRef, useState } from "react";
|
||||||
|
|
||||||
|
type eventType = React.ChangeEvent<HTMLInputElement>;
|
||||||
|
type BoundedNumericTextFieldProps = {
|
||||||
|
minValue: number;
|
||||||
|
maxValue: number;
|
||||||
|
addIconButtons?: boolean;
|
||||||
|
allowDecimals?: boolean;
|
||||||
|
allowNegatives?: boolean;
|
||||||
|
onChange?: (s: string) => void;
|
||||||
|
initialValue?: string;
|
||||||
|
maxSigDigits?: number;
|
||||||
|
} & TextFieldProps;
|
||||||
|
|
||||||
|
export const BoundedNumericTextField = ({
|
||||||
|
minValue,
|
||||||
|
maxValue,
|
||||||
|
addIconButtons = true,
|
||||||
|
allowDecimals = true,
|
||||||
|
allowNegatives = false,
|
||||||
|
initialValue,
|
||||||
|
maxSigDigits = 6,
|
||||||
|
...props
|
||||||
|
}: BoundedNumericTextFieldProps) => {
|
||||||
|
const [textFieldValue, setTextFieldValue] = useState<string>(
|
||||||
|
initialValue || ""
|
||||||
|
);
|
||||||
|
const ref = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
const stringIsEmpty = (value: string) => {
|
||||||
|
return value === "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const isAllZerosNum = /^0*\.?0*$/;
|
||||||
|
const isFloatNum = /^-?[0-9]*\.?[0-9]*$/;
|
||||||
|
const isIntegerNum = /^-?[0-9]+$/;
|
||||||
|
|
||||||
|
const skipMinMaxCheck = (value: string) => {
|
||||||
|
const lastIndexIsDecimal = value.charAt(value.length - 1) === ".";
|
||||||
|
const isEmpty = stringIsEmpty(value);
|
||||||
|
const isAllZeros = isAllZerosNum.test(value);
|
||||||
|
const isInteger = isIntegerNum.test(value);
|
||||||
|
// skipping minMax on all 0s allows values less than 1 to be entered
|
||||||
|
|
||||||
|
return lastIndexIsDecimal || isEmpty || (isAllZeros && !isInteger);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setMinMaxValue = (value: string): string => {
|
||||||
|
if (skipMinMaxCheck(value)) return value;
|
||||||
|
const valueNum = Number(value);
|
||||||
|
|
||||||
|
const boundedNum = setNumberWithinBounds(valueNum, minValue, maxValue);
|
||||||
|
|
||||||
|
const numberInBounds = boundedNum === valueNum;
|
||||||
|
return numberInBounds ? value : boundedNum.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSigDigits = (number: string) => {
|
||||||
|
if (isIntegerNum.test(number)) return 0;
|
||||||
|
const decimalSplit = number.split(".");
|
||||||
|
return decimalSplit[decimalSplit.length - 1].length;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sigDigitsExceeded = (number: string, sigDigits: number) => {
|
||||||
|
return getSigDigits(number) > sigDigits;
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterTypes = (value: string) => {
|
||||||
|
if (allowDecimals === false) value = value.replace(".", "");
|
||||||
|
if (allowNegatives === false) value = value.replace("-", "");
|
||||||
|
if (sigDigitsExceeded(value, maxSigDigits)) {
|
||||||
|
value = value.substring(0, value.length - 1);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
const filterValue = (value: string) => {
|
||||||
|
if (stringIsEmpty(value)) return "";
|
||||||
|
value = filterTypes(value);
|
||||||
|
if (isFloatNum.test(value)) {
|
||||||
|
return setMinMaxValue(value);
|
||||||
|
}
|
||||||
|
return textFieldValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const listeners = (e: eventType) => {
|
||||||
|
// console.log("changeEvent:", e);
|
||||||
|
const newValue = filterValue(e.target.value);
|
||||||
|
setTextFieldValue(newValue);
|
||||||
|
if (props?.onChange) props.onChange(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeValueWithIncDecButton = (e, changeAmount: number) => {
|
||||||
|
const changedValue = (+textFieldValue + changeAmount).toString();
|
||||||
|
const inBoundsValue = setMinMaxValue(changedValue);
|
||||||
|
setTextFieldValue(inBoundsValue);
|
||||||
|
if (props?.onChange) props.onChange(inBoundsValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatValueOnBlur = (e: eventType) => {
|
||||||
|
let value = e.target.value;
|
||||||
|
if (stringIsEmpty(value) || value === ".") {
|
||||||
|
setTextFieldValue("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = setMinMaxValue(value);
|
||||||
|
value = removeTrailingZeros(value);
|
||||||
|
if (isAllZerosNum.test(value)) value = minValue.toString();
|
||||||
|
|
||||||
|
setTextFieldValue(value);
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { onChange, ...noChangeProps } = { ...props };
|
||||||
|
return (
|
||||||
|
<TextField
|
||||||
|
{...noChangeProps}
|
||||||
|
InputProps={{
|
||||||
|
...props?.InputProps,
|
||||||
|
endAdornment: addIconButtons ? (
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton onClick={e => changeValueWithIncDecButton(e, 1)}>
|
||||||
|
<AddIcon />{" "}
|
||||||
|
</IconButton>
|
||||||
|
<IconButton onClick={e => changeValueWithIncDecButton(e, -1)}>
|
||||||
|
<RemoveIcon />{" "}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
onChange={e => listeners(e as eventType)}
|
||||||
|
onBlur={e => {
|
||||||
|
formatValueOnBlur(e as eventType);
|
||||||
|
}}
|
||||||
|
autoComplete="off"
|
||||||
|
value={textFieldValue}
|
||||||
|
inputRef={ref}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeTrailingZeros = (s: string) => {
|
||||||
|
return Number(s).toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const setNumberWithinBounds = (
|
||||||
|
num: number,
|
||||||
|
minValue: number,
|
||||||
|
maxValue: number
|
||||||
|
) => {
|
||||||
|
if (num > maxValue) return maxValue;
|
||||||
|
if (num < minValue) return minValue;
|
||||||
|
return num;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BoundedNumericTextField;
|
@ -1,5 +1,12 @@
|
|||||||
|
import moment from "moment";
|
||||||
|
import { CoinType } from "../constants/PublishFees/FeePricePublish/FeePricePublish.ts";
|
||||||
import { NameData } from "../constants/PublishFees/SendFeeFunctions.ts";
|
import { NameData } from "../constants/PublishFees/SendFeeFunctions.ts";
|
||||||
import { getUserAccountNames } from "../constants/PublishFees/VerifyPayment-Functions.ts";
|
import {
|
||||||
|
getUserAccount,
|
||||||
|
getUserAccountNames,
|
||||||
|
} from "../constants/PublishFees/VerifyPayment-Functions.ts";
|
||||||
|
import { Issue } from "../state/features/fileSlice.ts";
|
||||||
|
import { isNumber } from "./utilFunctions.ts";
|
||||||
|
|
||||||
export const getNameData = async (name: string) => {
|
export const getNameData = async (name: string) => {
|
||||||
return (await qortalRequest({
|
return (await qortalRequest({
|
||||||
@ -31,3 +38,216 @@ export const sendQchatDM = async (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface BountyData {
|
||||||
|
amount: number;
|
||||||
|
coinType: CoinType;
|
||||||
|
crowdfundLink?: string;
|
||||||
|
sourceCodeLink?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getATAmount = async crowdfundLink => {
|
||||||
|
const crowdfund = await getCrowdfund(crowdfundLink);
|
||||||
|
const atAddress = crowdfund?.deployedAT?.aTAddress;
|
||||||
|
if (!atAddress) return 0;
|
||||||
|
try {
|
||||||
|
const res = await qortalRequest({
|
||||||
|
action: "SEARCH_TRANSACTIONS",
|
||||||
|
txType: ["PAYMENT"],
|
||||||
|
confirmationStatus: "CONFIRMED",
|
||||||
|
address: atAddress,
|
||||||
|
limit: 0,
|
||||||
|
reverse: true,
|
||||||
|
});
|
||||||
|
if (res?.length > 0) {
|
||||||
|
const totalAmount: number = res.reduce(
|
||||||
|
(total: number, transaction) => total + parseFloat(transaction.amount),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
return totalAmount;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCrowdfund = async (crowdfundLink: string) => {
|
||||||
|
const splitLink = crowdfundLink.split("/");
|
||||||
|
const name = splitLink[5];
|
||||||
|
const identifier = splitLink[6];
|
||||||
|
console.log("fetching crowdfund");
|
||||||
|
return await qortalRequest({
|
||||||
|
action: "FETCH_QDN_RESOURCE",
|
||||||
|
service: "DOCUMENT",
|
||||||
|
name,
|
||||||
|
identifier,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getATInfo = async (atAddress: string) => {
|
||||||
|
try {
|
||||||
|
const url = `/at/${atAddress}`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (response.status === 200) {
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getNodeInfo = async () => {
|
||||||
|
try {
|
||||||
|
const url = `/blocks/height`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const responseDataSearch = await response.json();
|
||||||
|
return { height: responseDataSearch };
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCrowdfundEndDate = async (crowdfundLink: string) => {
|
||||||
|
const { deployedAT } = await getCrowdfund(crowdfundLink);
|
||||||
|
|
||||||
|
const ATinfo = await getATInfo(deployedAT.aTAddress);
|
||||||
|
const nodeInfo = await getNodeInfo();
|
||||||
|
if (!ATinfo?.sleepUntilHeight || !nodeInfo?.height) return null;
|
||||||
|
|
||||||
|
const blocksRemaining = +ATinfo?.sleepUntilHeight - +nodeInfo.height;
|
||||||
|
return moment().add(blocksRemaining, "minutes");
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DayTime = { days: number; hours: number; minutes: number };
|
||||||
|
export const getDurationFromBlocks = (blocks: number, blockCount: number) => {
|
||||||
|
const minutesPerDay = 60 * 24;
|
||||||
|
const blocksPerMinute = blockCount / minutesPerDay;
|
||||||
|
const duration = blocks / blocksPerMinute;
|
||||||
|
|
||||||
|
const days = Math.floor(duration / minutesPerDay);
|
||||||
|
const hours = Math.floor((duration % minutesPerDay) / 60);
|
||||||
|
const minutes = Math.floor(duration % 60);
|
||||||
|
|
||||||
|
return { days, hours, minutes } as DayTime;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getATaddress = async (crowdfundLink: string) => {
|
||||||
|
const { deployedAT } = await getCrowdfund(crowdfundLink);
|
||||||
|
return deployedAT?.aTAddress;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getHasQFundEnded = async (atAddress: string) => {
|
||||||
|
const url = `/at/${atAddress}`;
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (response.status === 200) {
|
||||||
|
const responseDataSearch = await response.json();
|
||||||
|
return !!(
|
||||||
|
Object.keys(responseDataSearch).length > 0 &&
|
||||||
|
responseDataSearch?.isFinished
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export interface SummaryTransactionCounts {
|
||||||
|
arbitrary: number;
|
||||||
|
AT: number;
|
||||||
|
deployAt: number;
|
||||||
|
groupInvite: number;
|
||||||
|
joinGroup: number;
|
||||||
|
message: number;
|
||||||
|
payment: number;
|
||||||
|
registerName: number;
|
||||||
|
rewardShare: number;
|
||||||
|
updateName: number;
|
||||||
|
voteOnPoll: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DaySummaryResponse {
|
||||||
|
assetsIssued: number;
|
||||||
|
blockCount: number;
|
||||||
|
namesRegistered: number;
|
||||||
|
totalTransactionCount: number;
|
||||||
|
transactionCountByType: SummaryTransactionCounts;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDaySummary = async () => {
|
||||||
|
return (await qortalRequest({
|
||||||
|
action: "GET_DAY_SUMMARY",
|
||||||
|
})) as DaySummaryResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const validateBountyInput = async (input: string) => {
|
||||||
|
if (isNumber(input)) return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const crowdfund = await getCrowdfund(input);
|
||||||
|
const ATaddress = crowdfund.aTAddress;
|
||||||
|
|
||||||
|
return ATaddress !== "";
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appendBountyAmount = (issue: Issue, bountyAmount: number) => {
|
||||||
|
return {
|
||||||
|
...issue,
|
||||||
|
bountyData: {
|
||||||
|
amount: bountyAmount || undefined,
|
||||||
|
coinType: issue?.bountyData?.coinType,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
export const getBountyAmounts = async (issues: Issue[]) => {
|
||||||
|
const issuePromises = issues.map(issue => {
|
||||||
|
if (!issue?.bountyData?.crowdfundLink) {
|
||||||
|
const numberAsPromise = async (num: number) => num;
|
||||||
|
return numberAsPromise(Number(issue?.bountyData?.amount) || undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
return getATAmount(issue?.bountyData?.crowdfundLink);
|
||||||
|
});
|
||||||
|
|
||||||
|
const bountyAmounts = await Promise.all(issuePromises);
|
||||||
|
return issues.map((issue, index) => {
|
||||||
|
return appendBountyAmount(issue, bountyAmounts[index]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBalance = async (address: string) => {
|
||||||
|
return (await qortalRequest({
|
||||||
|
action: "GET_BALANCE",
|
||||||
|
address,
|
||||||
|
})) as number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getUserBalance = async () => {
|
||||||
|
const accountInfo = await getUserAccount();
|
||||||
|
return (await getBalance(accountInfo.address)) as number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getAvatarFromName = async (name: string) => {
|
||||||
|
return await qortalRequest({
|
||||||
|
action: "GET_QDN_RESOURCE_URL",
|
||||||
|
name,
|
||||||
|
service: "THUMBNAIL",
|
||||||
|
identifier: "qortal_avatar",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -18,3 +18,13 @@ export const printVar = (variable: object) => {
|
|||||||
const [key, value] = Object.entries(variable)[0];
|
const [key, value] = Object.entries(variable)[0];
|
||||||
console.log(key, " is: ", value);
|
console.log(key, " is: ", value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isNumber = (input: string) => {
|
||||||
|
if (input === "") return false;
|
||||||
|
const num = Number(input);
|
||||||
|
return !isNaN(num);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const truncateNumber = (value: string | number, sigDigits: number) => {
|
||||||
|
return Number(value).toFixed(sigDigits);
|
||||||
|
};
|
||||||
|
@ -5,9 +5,9 @@
|
|||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "node",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user