mirror of
https://github.com/Qortal/q-support.git
synced 2025-02-11 09:45: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
14997
package-lock.json
generated
14997
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
96
package.json
96
package.json
@ -1,48 +1,48 @@
|
||||
{
|
||||
"name": "qsupport",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@mui/icons-material": "^5.11.11",
|
||||
"@mui/material": "^5.11.13",
|
||||
"@reduxjs/toolkit": "^1.9.3",
|
||||
"compressorjs": "^1.2.1",
|
||||
"dompurify": "^3.0.6",
|
||||
"localforage": "^1.10.0",
|
||||
"moment": "^2.29.4",
|
||||
"prettier": "^3.2.4",
|
||||
"quill-image-resize-module-react": "^3.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-intersection-observer": "^9.4.3",
|
||||
"react-quill": "^2.0.0",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-rnd": "^10.4.1",
|
||||
"react-router-dom": "^6.9.0",
|
||||
"react-toastify": "^9.1.2",
|
||||
"short-unique-id": "^4.4.4",
|
||||
"ts-key-enum": "^2.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "^5.57.1",
|
||||
"@typescript-eslint/parser": "^5.57.1",
|
||||
"@vitejs/plugin-react": "^4.0.0",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.3.4",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "6.0.0-alpha.1"
|
||||
}
|
||||
}
|
||||
{
|
||||
"name": "qsupport",
|
||||
"private": true,
|
||||
"version": "1.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@mui/icons-material": "^5.11.11",
|
||||
"@mui/material": "^5.11.13",
|
||||
"@reduxjs/toolkit": "^1.9.3",
|
||||
"compressorjs": "^1.2.1",
|
||||
"dompurify": "^3.0.6",
|
||||
"eslint": "^8.57.0",
|
||||
"localforage": "^1.10.0",
|
||||
"moment": "^2.29.4",
|
||||
"prettier": "^3.2.5",
|
||||
"quill-image-resize-module-react": "^3.0.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-intersection-observer": "^9.4.3",
|
||||
"react-quill": "^2.0.0",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-rnd": "^10.4.1",
|
||||
"react-router-dom": "^6.9.0",
|
||||
"react-toastify": "^9.1.2",
|
||||
"short-unique-id": "^4.4.4",
|
||||
"ts-key-enum": "^2.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||
"@typescript-eslint/parser": "^5.62.0",
|
||||
"@vitejs/plugin-react": "^4.3.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.7",
|
||||
"typescript": "^5.0.2",
|
||||
"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 {
|
||||
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 { useDispatch, useSelector } from "react-redux";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
|
||||
import { setNotification } from "../../state/features/notificationsSlice";
|
||||
import { objectToBase64 } from "../../utils/toBase64";
|
||||
import { RootState } from "../../state/store";
|
||||
import { allCategoryData } from "../../constants/Categories/Categories.ts";
|
||||
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
|
||||
import { log, titleFormatter } from "../../constants/Misc.ts";
|
||||
import {
|
||||
feeAmountBase,
|
||||
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 {
|
||||
setEditFile,
|
||||
updateFile,
|
||||
updateInHashMap,
|
||||
} from "../../state/features/fileSlice.ts";
|
||||
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
|
||||
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll";
|
||||
import { TextEditor } from "../common/TextEditor/TextEditor";
|
||||
import { extractTextFromHTML } from "../common/TextEditor/utils";
|
||||
import { allCategoryData } from "../../constants/Categories/Categories.ts";
|
||||
import { log, titleFormatter } from "../../constants/Misc.ts";
|
||||
|
||||
import { setNotification } from "../../state/features/notificationsSlice.ts";
|
||||
import { RootState } from "../../state/store.ts";
|
||||
import { BountyData, validateBountyInput } from "../../utils/qortalRequests.ts";
|
||||
import { objectToBase64 } from "../../utils/toBase64.js";
|
||||
import { isNumber } from "../../utils/utilFunctions.ts";
|
||||
import {
|
||||
AutocompleteQappNames,
|
||||
QappNamesRef,
|
||||
} from "../common/AutocompleteQappNames.tsx";
|
||||
import {
|
||||
CategoryList,
|
||||
CategoryListRef,
|
||||
@ -36,14 +47,16 @@ import {
|
||||
ImagePublisher,
|
||||
ImagePublisherRef,
|
||||
} 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 {
|
||||
AutocompleteQappNames,
|
||||
QappNamesRef,
|
||||
} from "../common/AutocompleteQappNames.tsx";
|
||||
import { payPublishFeeQORT } from "../../constants/PublishFees/SendFeeFunctions.ts";
|
||||
import { feeAmountBase } from "../../constants/PublishFees/FeeData.tsx";
|
||||
import { verifyPayment } from "../../constants/PublishFees/VerifyPayment.ts";
|
||||
ActionButton,
|
||||
CrowdfundActionButtonRow,
|
||||
CustomInputField,
|
||||
ModalBody,
|
||||
NewCrowdfundTitle,
|
||||
} from "./EditIssue-styles.tsx";
|
||||
|
||||
const uid = new ShortUniqueId();
|
||||
const shortuid = new ShortUniqueId({ length: 5 });
|
||||
@ -81,6 +94,9 @@ export const EditIssue = () => {
|
||||
);
|
||||
|
||||
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 [videoPropertiesToSetToRedux, setVideoPropertiesToSetToRedux] =
|
||||
useState(null);
|
||||
@ -92,6 +108,11 @@ export const EditIssue = () => {
|
||||
const [files, setFiles] = useState<VideoFile[]>([]);
|
||||
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
|
||||
const [isIssuePaid, setIsIssuePaid] = useState<boolean>(true);
|
||||
|
||||
const [coin, setCoin] = useState<CoinType>("QORT");
|
||||
|
||||
const [showCoins, setShowCoins] = useState<boolean>(false);
|
||||
|
||||
const categoryListRef = useRef<CategoryListRef>(null);
|
||||
const imagePublisherRef = useRef<ImagePublisherRef>(null);
|
||||
const autocompleteRef = useRef<QappNamesRef>(null);
|
||||
@ -148,6 +169,14 @@ export const EditIssue = () => {
|
||||
const categoriesFromEditFile =
|
||||
getCategoriesFromObject(editIssueProperties);
|
||||
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]);
|
||||
|
||||
@ -162,6 +191,11 @@ export const EditIssue = () => {
|
||||
|
||||
async function publishQDNResource(payFee: boolean) {
|
||||
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 (!userAddress) throw new Error("Unable to locate user address");
|
||||
if (!description) throw new Error("Please enter a description");
|
||||
@ -169,7 +203,7 @@ export const EditIssue = () => {
|
||||
if (!allCategoriesSelected)
|
||||
throw new Error("All Categories must be selected");
|
||||
|
||||
console.log("categories", selectedCategories);
|
||||
if (log) console.log("categories", selectedCategories);
|
||||
const QappsCategoryID = "3";
|
||||
if (
|
||||
selectedCategories[0] === QappsCategoryID &&
|
||||
@ -208,9 +242,9 @@ export const EditIssue = () => {
|
||||
.toLowerCase();
|
||||
|
||||
if (!sanitizeTitle) throw new Error("Please enter a title");
|
||||
let fileReferences = [];
|
||||
const fileReferences = [];
|
||||
|
||||
let listOfPublishes = [];
|
||||
const listOfPublishes = [];
|
||||
const fullDescription = extractTextFromHTML(description);
|
||||
|
||||
for (const publish of files) {
|
||||
@ -228,17 +262,17 @@ export const EditIssue = () => {
|
||||
if (fileExtensionSplit?.length > 1) {
|
||||
fileExtension = fileExtensionSplit?.pop() || "";
|
||||
}
|
||||
let firstPartName = fileExtensionSplit[0];
|
||||
const firstPartName = fileExtensionSplit[0];
|
||||
|
||||
let filename = firstPartName.slice(0, 15);
|
||||
|
||||
// Step 1: Replace all white spaces with underscores
|
||||
|
||||
// Replace all forms of whitespace (including non-standard ones) with underscores
|
||||
let stringWithUnderscores = filename.replace(/[\s\uFEFF\xA0]+/g, "_");
|
||||
const stringWithUnderscores = filename.replace(/[\s\uFEFF\xA0]+/g, "_");
|
||||
|
||||
// Remove all non-alphanumeric characters (except underscores)
|
||||
let alphanumericString = stringWithUnderscores.replace(
|
||||
const alphanumericString = stringWithUnderscores.replace(
|
||||
/[^a-zA-Z0-9_]/g,
|
||||
""
|
||||
);
|
||||
@ -250,7 +284,7 @@ export const EditIssue = () => {
|
||||
}
|
||||
|
||||
const categoryString = `**${categoryListRef.current?.getSelectedCategories()}**`;
|
||||
let metadescription = categoryString + fullDescription.slice(0, 150);
|
||||
const metadescription = categoryString + fullDescription.slice(0, 150);
|
||||
|
||||
const requestBodyVideo: any = {
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
@ -275,6 +309,14 @@ export const EditIssue = () => {
|
||||
}
|
||||
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 = {
|
||||
title,
|
||||
version: editIssueProperties.version,
|
||||
@ -286,6 +328,7 @@ export const EditIssue = () => {
|
||||
images: imagePublisherRef?.current?.getImageArray(),
|
||||
QappName: selectedQappName,
|
||||
feeData: editIssueProperties?.feeData,
|
||||
bountyData,
|
||||
};
|
||||
if (payFee) {
|
||||
const publishFeeResponse = await payPublishFeeQORT(feeAmountBase);
|
||||
@ -309,7 +352,7 @@ export const EditIssue = () => {
|
||||
categoryListRef.current?.getCategoriesFetchString(selectedCategories);
|
||||
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 length is: ", metadescription.length);
|
||||
if (log) console.log("characters left:", 240 - metadescription.length);
|
||||
@ -318,7 +361,6 @@ export const EditIssue = () => {
|
||||
|
||||
const fileObjectToBase64 = await objectToBase64(issueObject);
|
||||
// Description is obtained from raw data
|
||||
|
||||
const requestBodyJson: any = {
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
name: name,
|
||||
@ -465,12 +507,53 @@ export const EditIssue = () => {
|
||||
</Box>
|
||||
</Box>
|
||||
{isShowQappNameTextField() && (
|
||||
<AutocompleteQappNames
|
||||
ref={autocompleteRef}
|
||||
namesList={QappNames}
|
||||
initialSelection={editIssueProperties?.QappName}
|
||||
/>
|
||||
<>
|
||||
<AutocompleteQappNames
|
||||
ref={autocompleteRef}
|
||||
namesList={QappNames}
|
||||
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
|
||||
ref={imagePublisherRef}
|
||||
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 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 { setNotification } from "../../state/features/notificationsSlice";
|
||||
import { objectToBase64 } from "../../utils/toBase64";
|
||||
import { RootState } from "../../state/store";
|
||||
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
|
||||
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll";
|
||||
import { TextEditor } from "../common/TextEditor/TextEditor";
|
||||
import { extractTextFromHTML } from "../common/TextEditor/utils";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import ShortUniqueId from "short-unique-id";
|
||||
import { allCategoryData } from "../../constants/Categories/Categories.ts";
|
||||
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
|
||||
import {
|
||||
fontSizeLarge,
|
||||
fontSizeSmall,
|
||||
log,
|
||||
titleFormatter,
|
||||
} 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 {
|
||||
CategoryList,
|
||||
CategoryListRef,
|
||||
@ -36,19 +49,17 @@ import {
|
||||
ImagePublisher,
|
||||
ImagePublisherRef,
|
||||
} 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 {
|
||||
AutocompleteQappNames,
|
||||
QappNamesRef,
|
||||
} from "../common/AutocompleteQappNames.tsx";
|
||||
import {
|
||||
feeAmountBase,
|
||||
feeDisclaimer,
|
||||
} from "../../constants/PublishFees/FeeData.tsx";
|
||||
import {
|
||||
payPublishFeeQORT,
|
||||
PublishFeeData,
|
||||
} from "../../constants/PublishFees/SendFeeFunctions.ts";
|
||||
ActionButton,
|
||||
ActionButtonRow,
|
||||
CustomInputField,
|
||||
ModalBody,
|
||||
NewCrowdfundTitle,
|
||||
StyledButton,
|
||||
} from "./PublishIssue-styles.tsx";
|
||||
|
||||
const uid = new ShortUniqueId();
|
||||
const shortuid = new ShortUniqueId({ length: 5 });
|
||||
@ -72,10 +83,6 @@ interface VideoFile {
|
||||
export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
const theme = useTheme();
|
||||
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 userAddress = useSelector(
|
||||
(state: RootState) => state.auth?.user?.address
|
||||
@ -83,6 +90,12 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
const QappNames = useSelector(
|
||||
(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 [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
@ -99,6 +112,10 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
|
||||
const [playlistSetting, setPlaylistSetting] = useState<null | string>(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 imagePublisherRef = useRef<ImagePublisherRef>(null);
|
||||
@ -139,17 +156,16 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (editContent) {
|
||||
}
|
||||
}, [editContent]);
|
||||
|
||||
const onClose = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
async function publishQDNResource() {
|
||||
const publishQDNResource = async () => {
|
||||
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 (!userAddress) throw new Error("Unable to locate user address");
|
||||
if (!description) throw new Error("Please enter a description");
|
||||
@ -197,9 +213,9 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
.toLowerCase();
|
||||
if (!sanitizeTitle) throw new Error("Please enter a title");
|
||||
|
||||
let fileReferences = [];
|
||||
const fileReferences = [];
|
||||
|
||||
let listOfPublishes = [];
|
||||
const listOfPublishes = [];
|
||||
|
||||
const fullDescription = extractTextFromHTML(description);
|
||||
|
||||
@ -214,17 +230,17 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
if (fileExtensionSplit?.length > 1) {
|
||||
fileExtension = fileExtensionSplit?.pop() || "";
|
||||
}
|
||||
let firstPartName = fileExtensionSplit[0];
|
||||
const firstPartName = fileExtensionSplit[0];
|
||||
|
||||
let filename = firstPartName.slice(0, 15);
|
||||
|
||||
// Step 1: Replace all white spaces with underscores
|
||||
|
||||
// Replace all forms of whitespace (including non-standard ones) with underscores
|
||||
let stringWithUnderscores = filename.replace(/[\s\uFEFF\xA0]+/g, "_");
|
||||
const stringWithUnderscores = filename.replace(/[\s\uFEFF\xA0]+/g, "_");
|
||||
|
||||
// Remove all non-alphanumeric characters (except underscores)
|
||||
let alphanumericString = stringWithUnderscores.replace(
|
||||
const alphanumericString = stringWithUnderscores.replace(
|
||||
/[^a-zA-Z0-9_]/g,
|
||||
""
|
||||
);
|
||||
@ -236,7 +252,7 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
}
|
||||
|
||||
const categoryString = `**${categoryListRef.current?.getSelectedCategories()}**`;
|
||||
let metadescription = categoryString + fullDescription.slice(0, 150);
|
||||
const metadescription = categoryString + fullDescription.slice(0, 150);
|
||||
|
||||
const requestBodyFile: any = {
|
||||
action: "PUBLISH_QDN_RESOURCE",
|
||||
@ -275,6 +291,13 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
senderName: "",
|
||||
};
|
||||
|
||||
const isBountyNumber = isNumber(bounty);
|
||||
const bountyData: BountyData = {
|
||||
amount: isBountyNumber ? Number(bounty) : undefined,
|
||||
crowdfundLink: isBountyNumber ? undefined : bounty,
|
||||
coinType: coin,
|
||||
sourceCodeLink: sourceCode,
|
||||
};
|
||||
const issueObject: any = {
|
||||
title,
|
||||
version: 1,
|
||||
@ -286,6 +309,7 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
images: imagePublisherRef?.current?.getImageArray(),
|
||||
QappName: selectedQappName,
|
||||
feeData,
|
||||
bountyData,
|
||||
};
|
||||
|
||||
const QappNameString = autocompleteRef?.current?.getQappNameFetchString();
|
||||
@ -293,7 +317,7 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
categoryListRef.current?.getCategoriesFetchString(categoryList);
|
||||
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 length is: ", metadescription.length);
|
||||
@ -343,7 +367,7 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
if (!notificationObj) return;
|
||||
dispatch(setNotification(notificationObj));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const isShowQappNameTextField = () => {
|
||||
const QappID = "3";
|
||||
@ -460,11 +484,52 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
|
||||
/>
|
||||
</Box>
|
||||
{isShowQappNameTextField() && (
|
||||
<AutocompleteQappNames
|
||||
ref={autocompleteRef}
|
||||
namesList={QappNames}
|
||||
/>
|
||||
<>
|
||||
<AutocompleteQappNames
|
||||
ref={autocompleteRef}
|
||||
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} />
|
||||
<CustomInputField
|
||||
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,
|
||||
SubmitCommentButton,
|
||||
} 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 { maxCommentLength } from "../../../constants/Misc.ts";
|
||||
|
||||
@ -127,6 +130,10 @@ export const CommentEditor = ({
|
||||
address = user?.address;
|
||||
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) {
|
||||
errorMsg = "Cannot post: your address isn't available";
|
||||
}
|
||||
@ -180,9 +187,6 @@ export const CommentEditor = ({
|
||||
//
|
||||
// ${value.substring(0, maxNotificationLength)}`;
|
||||
|
||||
const notificationMessage = `This is an automated Q-Support notification indicating that someone has commented on your issue here:
|
||||
qortal://APP/Q-Support/issue/${postName}/${postId}`;
|
||||
|
||||
await sendQchatDM(postName, notificationMessage);
|
||||
}
|
||||
return resourceResponse;
|
||||
|
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 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 {
|
||||
iconSrc: string;
|
||||
label?: string;
|
||||
showBackupIcon?: boolean;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
export const IssueIcon = ({
|
||||
iconSrc,
|
||||
label,
|
||||
showBackupIcon = true,
|
||||
style,
|
||||
}: IssueIconProps) => {
|
||||
const displayFileIcon = !iconSrc && showBackupIcon;
|
||||
|
||||
const widthAndHeight = "50px";
|
||||
return (
|
||||
<>
|
||||
{iconSrc && (
|
||||
<img
|
||||
src={iconSrc}
|
||||
width="50px"
|
||||
height="50px"
|
||||
style={{
|
||||
borderRadius: "5px",
|
||||
...style,
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
title={<Typography fontSize={16}>{label}</Typography>}
|
||||
arrow
|
||||
disableHoverListener={!label}
|
||||
placement={"top"}
|
||||
>
|
||||
<img
|
||||
src={iconSrc}
|
||||
width={style?.width || widthAndHeight}
|
||||
height={style?.width || widthAndHeight}
|
||||
style={{
|
||||
borderRadius: "5px",
|
||||
...style,
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{displayFileIcon && (
|
||||
<AttachFileIcon
|
||||
sx={{
|
||||
...style,
|
||||
width: "40px",
|
||||
height: "40px",
|
||||
width: style?.width || widthAndHeight,
|
||||
height: style?.width || widthAndHeight,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@ -40,20 +52,34 @@ export const IssueIcon = ({
|
||||
};
|
||||
|
||||
interface IssueIconsProps {
|
||||
iconSources: string[];
|
||||
issueData: Issue;
|
||||
showBackupIcon?: boolean;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export const IssueIcons = ({
|
||||
iconSources,
|
||||
issueData,
|
||||
showBackupIcon = true,
|
||||
style,
|
||||
}: 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
|
||||
key={icon + index}
|
||||
iconSrc={icon}
|
||||
label={iconLabels ? iconLabels[index] : ""}
|
||||
style={{ ...style }}
|
||||
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 {
|
||||
Categories,
|
||||
Category,
|
||||
CategoryData,
|
||||
} from "../../components/common/CategoryList/CategoryList.tsx";
|
||||
import { getAllCategoriesWithIcons } from "./CategoryFunctions.ts";
|
||||
import CoreIcon from "../../assets/icons/Qortal-Core-Icon.webp";
|
||||
import UIicon from "../../assets/icons/Qortal-UI-Icon.webp";
|
||||
import QappIcon from "../../assets/icons/Q-App-Icon.webp";
|
||||
import UnknownIcon from "../../assets/icons/unknown.webp";
|
||||
|
||||
import BugReportIcon from "../../assets/icons/Bug-Report-Icon.webp";
|
||||
import FeatureRequestIcon from "../../assets/icons/Feature-Request-Icon.webp";
|
||||
import TechSupportIcon from "../../assets/icons/Tech-Support-Icon.webp";
|
||||
|
||||
import OpenIcon from "../../assets/icons/Open-Icon.webp";
|
||||
import ClosedIcon from "../../assets/icons/Closed-Icon.webp";
|
||||
import InProgressIcon from "../../assets/icons/In-Progress-Icon.webp";
|
||||
import CompleteIcon from "../../assets/icons/Complete-Icon.webp";
|
||||
|
||||
const issueLocationLabel = "Issue Location";
|
||||
export const issueLocation: Category[] = [
|
||||
@ -48,7 +47,7 @@ export const secondCategories: Categories = {};
|
||||
issueLocation.map(c => (secondCategories[c.id] = issueType));
|
||||
|
||||
const issueLabel = "Issue State";
|
||||
export const IssueState = [
|
||||
export const issueState = [
|
||||
{ id: 101, name: "Open", icon: OpenIcon, label: issueLabel },
|
||||
{ id: 102, name: "Closed", icon: ClosedIcon, label: issueLabel },
|
||||
{ id: 103, name: "In Progress", icon: InProgressIcon, label: issueLabel },
|
||||
@ -57,7 +56,16 @@ export const IssueState = [
|
||||
|
||||
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 = {
|
||||
category: issueLocation,
|
||||
|
@ -43,7 +43,7 @@ export const findAllCategoryData = (
|
||||
categories: string[],
|
||||
direction: Direction = "forward"
|
||||
) => {
|
||||
let foundIcons: Category[] = [];
|
||||
const foundIcons: Category[] = [];
|
||||
if (direction === "backward") categories.reverse();
|
||||
|
||||
categories.map(category => {
|
||||
@ -79,6 +79,15 @@ export const getAllCategoriesWithIcons = () => {
|
||||
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) => {
|
||||
const categories = getCategoriesFromObject(fileObj);
|
||||
const icons = categories.map(categoryID => {
|
||||
|
@ -10,6 +10,17 @@ export const FEE_BASE = useTestIdentifiers
|
||||
? "MYTEST_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 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 { store } from "../../../state/store.js";
|
||||
import { objectToBase64 } from "../../../utils/toBase64.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";
|
||||
|
||||
|
2
src/global.d.ts
vendored
2
src/global.d.ts
vendored
@ -39,6 +39,8 @@ interface QortalRequestOptions {
|
||||
excludeBlocked?: boolean;
|
||||
exactMatchNames?: boolean;
|
||||
message?: string;
|
||||
txType?: string[];
|
||||
confirmationStatus?: string;
|
||||
}
|
||||
|
||||
declare function qortalRequest(options: QortalRequestOptions): Promise<any>;
|
||||
|
@ -1,5 +1,11 @@
|
||||
import React from "react";
|
||||
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 {
|
||||
addFiles,
|
||||
addToHashMap,
|
||||
@ -19,13 +25,8 @@ import {
|
||||
} from "../state/features/globalSlice";
|
||||
import { RootState } from "../state/store";
|
||||
import { fetchAndEvaluateIssues } from "../utils/fetchVideos";
|
||||
import {
|
||||
QSUPPORT_FILE_BASE,
|
||||
QSUPPORT_PLAYLIST_BASE,
|
||||
} from "../constants/Identifiers.ts";
|
||||
import { getBountyAmounts } from "../utils/qortalRequests.ts";
|
||||
import { queue } from "../wrappers/GlobalWrapper";
|
||||
import { log } from "../constants/Misc.ts";
|
||||
import { verifyAllPayments } from "../constants/PublishFees/VerifyPayment.ts";
|
||||
|
||||
export const useFetchIssues = () => {
|
||||
const dispatch = useDispatch();
|
||||
@ -70,7 +71,7 @@ export const useFetchIssues = () => {
|
||||
|
||||
const getAvatar = React.useCallback(async (author: string) => {
|
||||
try {
|
||||
let url = await qortalRequest({
|
||||
const url = await qortalRequest({
|
||||
action: "GET_QDN_RESOURCE_URL",
|
||||
name: author,
|
||||
service: "THUMBNAIL",
|
||||
@ -83,14 +84,16 @@ export const useFetchIssues = () => {
|
||||
url,
|
||||
})
|
||||
);
|
||||
} catch (error) {}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getIssue = async (
|
||||
user: string,
|
||||
issueID: string,
|
||||
content: any,
|
||||
retries: number = 0
|
||||
retries = 0
|
||||
) => {
|
||||
try {
|
||||
const res = await fetchAndEvaluateIssues({
|
||||
@ -182,6 +185,7 @@ export const useFetchIssues = () => {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
dispatch(setIsLoadingGlobal(false));
|
||||
}
|
||||
@ -277,11 +281,16 @@ export const useFetchIssues = () => {
|
||||
}
|
||||
|
||||
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) => {
|
||||
return {
|
||||
...issue,
|
||||
feeData: verifiedIssues[index]?.feeData,
|
||||
bountyData: bountyIssues[index]?.bountyData,
|
||||
};
|
||||
});
|
||||
|
||||
@ -289,7 +298,6 @@ export const useFetchIssues = () => {
|
||||
else dispatch(upsertFiles(structureData));
|
||||
} catch (error) {
|
||||
console.log({ error });
|
||||
} finally {
|
||||
}
|
||||
},
|
||||
[videos, hashMapFiles]
|
||||
@ -349,7 +357,7 @@ export const useFetchIssues = () => {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
} finally {
|
||||
console.log(error);
|
||||
}
|
||||
},
|
||||
[filteredVideos, hashMapFiles]
|
||||
@ -389,12 +397,14 @@ export const useFetchIssues = () => {
|
||||
const newArray = responseData.slice(0, findVideo);
|
||||
dispatch(setCountNewFiles(newArray.length));
|
||||
return;
|
||||
} catch (error) {}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}, [videos]);
|
||||
|
||||
const getIssuesCount = React.useCallback(async () => {
|
||||
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, {
|
||||
method: "GET",
|
||||
@ -416,7 +426,6 @@ export const useFetchIssues = () => {
|
||||
dispatch(setFilesPerNamePublished(filesPerNamePublished));
|
||||
} catch (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 ReactDOM from "react-dom";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "../../state/store";
|
||||
import { IssueList } from "./IssueList.tsx";
|
||||
import { Box, Grid, Input, useTheme } from "@mui/material";
|
||||
import { useFetchIssues } from "../../hooks/useFetchIssues.tsx";
|
||||
import {
|
||||
AutocompleteQappNames,
|
||||
getPublishedQappNames,
|
||||
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 { FiltersCol, FiltersContainer } from "./IssueList-styles.tsx";
|
||||
import { SubtitleContainer, ThemeButton } from "./Home-styles";
|
||||
import { StatsData } from "../../components/StatsData.tsx";
|
||||
import {
|
||||
allCategories,
|
||||
allCategoryData,
|
||||
} from "../../constants/Categories/Categories.ts";
|
||||
import { useFetchIssues } from "../../hooks/useFetchIssues.tsx";
|
||||
import {
|
||||
changefilterName,
|
||||
changefilterSearch,
|
||||
changeFilterType,
|
||||
setQappNames,
|
||||
} from "../../state/features/fileSlice.ts";
|
||||
import {
|
||||
allCategoryData,
|
||||
IssueState,
|
||||
} from "../../constants/Categories/Categories.ts";
|
||||
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";
|
||||
import { RootState } from "../../state/store";
|
||||
import { SubtitleContainer, ThemeButton } from "./Home-styles";
|
||||
import { FiltersCol, FiltersContainer } from "./IssueList-styles.tsx";
|
||||
import { IssueList } from "./IssueList.tsx";
|
||||
|
||||
interface HomeProps {
|
||||
mode?: string;
|
||||
@ -109,11 +109,12 @@ export const Home = ({ mode }: HomeProps) => {
|
||||
const selectedCategories =
|
||||
categoryListRef.current?.getSelectedCategories() || [];
|
||||
const issueType = categorySelectRef?.current?.getSelectedCategory();
|
||||
if (issueType) selectedCategories[2] = issueType;
|
||||
let categoriesString = getCategoriesFetchString(selectedCategories);
|
||||
if (issueType) categoriesString = ":" + issueType + ";";
|
||||
await getIssues(
|
||||
{
|
||||
name: filterName,
|
||||
categories: getCategoriesFetchString(selectedCategories),
|
||||
categories: categoriesString,
|
||||
QappName: autocompleteRef?.current?.getQappNameFetchString(),
|
||||
keywords: filterSearch,
|
||||
type: filterType,
|
||||
@ -168,32 +169,6 @@ export const Home = ({ mode }: HomeProps) => {
|
||||
isFilterMode.current = false;
|
||||
}
|
||||
|
||||
// const interval = useRef<any>(null);
|
||||
|
||||
// const checkNewVideosFunc = useCallback(() => {
|
||||
// let isCalling = false;
|
||||
// interval.current = setInterval(async () => {
|
||||
// if (isCalling || !firstFetch.current) return;
|
||||
// isCalling = true;
|
||||
// await checkNewVideos();
|
||||
// isCalling = false;
|
||||
// }, 30000); // 1 second interval
|
||||
// }, [checkNewVideos]);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (isFiltering && interval.current) {
|
||||
// clearInterval(interval.current);
|
||||
// return;
|
||||
// }
|
||||
// checkNewVideosFunc();
|
||||
|
||||
// return () => {
|
||||
// if (interval?.current) {
|
||||
// clearInterval(interval.current);
|
||||
// }
|
||||
// };
|
||||
// }, [mode, checkNewVideosFunc, isFiltering]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!firstFetch.current &&
|
||||
@ -294,7 +269,7 @@ export const Home = ({ mode }: HomeProps) => {
|
||||
)}
|
||||
{showCategorySelect && (
|
||||
<CategorySelect
|
||||
categoryData={IssueState}
|
||||
categoryData={allCategories}
|
||||
ref={categorySelectRef}
|
||||
sx={{ marginTop: "20px" }}
|
||||
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 {
|
||||
BlockIconContainer,
|
||||
IconsBox,
|
||||
@ -9,24 +31,6 @@ import {
|
||||
VideoCardTitle,
|
||||
VideoUploadDate,
|
||||
} 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 {
|
||||
issues: Issue[];
|
||||
@ -35,7 +39,7 @@ export const IssueList = ({ issues }: FileListProps) => {
|
||||
const hashMapIssues = useSelector(
|
||||
(state: RootState) => state.file.hashMapFiles
|
||||
);
|
||||
|
||||
const theme = useTheme();
|
||||
const [showIcons, setShowIcons] = useState(null);
|
||||
const username = useSelector((state: RootState) => state.auth?.user?.name);
|
||||
const dispatch = useDispatch();
|
||||
@ -54,7 +58,9 @@ export const IssueList = ({ issues }: FileListProps) => {
|
||||
if (response === true) {
|
||||
dispatch(blockUser(user));
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredIssues = useMemo(() => {
|
||||
@ -71,12 +77,11 @@ export const IssueList = ({ issues }: FileListProps) => {
|
||||
issueObj = existingFile;
|
||||
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 (
|
||||
<Box
|
||||
sx={{
|
||||
@ -142,24 +147,34 @@ export const IssueList = ({ issues }: FileListProps) => {
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: "200px",
|
||||
width: "280px",
|
||||
}}
|
||||
>
|
||||
<IssueIcons
|
||||
iconSources={issueIcons}
|
||||
issueData={issueObj}
|
||||
style={{ marginRight: "20px" }}
|
||||
showBackupIcon={true}
|
||||
/>
|
||||
</div>
|
||||
<VideoCardTitle
|
||||
<Box
|
||||
sx={{
|
||||
width: "150px",
|
||||
fontSize: fontSizeMedium,
|
||||
display: "flex",
|
||||
justifyContent: "left",
|
||||
alignItems: "center",
|
||||
width: "250px",
|
||||
fontSize: fontSizeExLarge,
|
||||
fontFamily: "Cairo",
|
||||
letterSpacing: "0.4px",
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
{fileBytes > 0 && formatBytes(fileBytes)}
|
||||
</VideoCardTitle>
|
||||
<VideoCardTitle sx={{ fontWeight: "bold", width: "500px" }}>
|
||||
<BountyDisplay
|
||||
bountyData={bountyData}
|
||||
divStyle={{ marginLeft: "20px" }}
|
||||
/>
|
||||
</Box>
|
||||
<VideoCardTitle sx={{ fontWeight: "bold", width: "400px" }}>
|
||||
{issueObj.title}
|
||||
</VideoCardTitle>
|
||||
</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 { 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 { 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 {
|
||||
BottomParent,
|
||||
IssueCard,
|
||||
@ -15,15 +25,6 @@ import {
|
||||
VideoCardTitle,
|
||||
VideoUploadDate,
|
||||
} 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 {
|
||||
mode?: string;
|
||||
@ -97,9 +98,11 @@ export const IssueListComponentLevel = ({ mode }: VideoListProps) => {
|
||||
|
||||
const issueData = await Promise.all(verifiedIssuePromises);
|
||||
const verifiedIssues = await verifyAllPayments(issueData);
|
||||
setIssues(verifiedIssues);
|
||||
const bountyIssues = await getBountyAmounts(verifiedIssues);
|
||||
console.log("bountyIssues: ", bountyIssues);
|
||||
setIssues(bountyIssues);
|
||||
} catch (error) {
|
||||
} finally {
|
||||
console.log(error);
|
||||
}
|
||||
}, [issues, hashMapVideos]);
|
||||
|
||||
@ -140,12 +143,10 @@ export const IssueListComponentLevel = ({ mode }: VideoListProps) => {
|
||||
issueObj = existingFile;
|
||||
hasHash = true;
|
||||
}
|
||||
|
||||
const issueIcons = getIconsFromObject(issueObj);
|
||||
const fileBytes = issueObj?.files.reduce(
|
||||
(acc, cur) => acc + (cur?.size || 0),
|
||||
0
|
||||
);
|
||||
const bountyData: BountyData = {
|
||||
...issueObj.bountyData,
|
||||
...issue.bountyData,
|
||||
};
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@ -183,22 +184,32 @@ export const IssueListComponentLevel = ({ mode }: VideoListProps) => {
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: "200px",
|
||||
width: "280px",
|
||||
}}
|
||||
>
|
||||
<IssueIcons
|
||||
iconSources={issueIcons}
|
||||
issueData={issueObj}
|
||||
style={{ marginRight: "20px" }}
|
||||
showBackupIcon={true}
|
||||
/>
|
||||
</div>
|
||||
<VideoCardTitle
|
||||
<Box
|
||||
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)}
|
||||
</VideoCardTitle>
|
||||
<BountyDisplay
|
||||
bountyData={bountyData}
|
||||
divStyle={{ marginLeft: "20px" }}
|
||||
/>
|
||||
</Box>
|
||||
<VideoCardTitle
|
||||
sx={{ fontWeight: "bold", width: "500px" }}
|
||||
>
|
||||
@ -213,32 +224,40 @@ export const IssueListComponentLevel = ({ mode }: VideoListProps) => {
|
||||
)}
|
||||
<BottomParent>
|
||||
<NameAndDateContainer
|
||||
sx={{ width: "200px", height: "100%" }}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
navigate(`/channel/${issueObj?.user}`);
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
sx={{ height: 24, width: 24 }}
|
||||
src={`/arbitrary/THUMBNAIL/${issueObj?.user}/qortal_avatar`}
|
||||
alt={`${issueObj?.user}'s avatar`}
|
||||
/>
|
||||
<VideoCardName
|
||||
sx={{
|
||||
":hover": {
|
||||
textDecoration: "underline",
|
||||
},
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
width: "200px",
|
||||
}}
|
||||
>
|
||||
{issueObj?.user}
|
||||
</VideoCardName>
|
||||
</NameAndDateContainer>
|
||||
<Avatar
|
||||
sx={{ height: 24, width: 24, marginRight: "10px" }}
|
||||
src={`/arbitrary/THUMBNAIL/${issueObj?.user}/qortal_avatar`}
|
||||
alt={`${issueObj?.user}'s avatar`}
|
||||
/>
|
||||
<VideoCardName
|
||||
sx={{
|
||||
":hover": {
|
||||
textDecoration: "underline",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{issueObj?.user}
|
||||
</VideoCardName>
|
||||
</div>
|
||||
|
||||
{issueObj?.created && (
|
||||
<VideoUploadDate>
|
||||
{formatDate(issueObj.created)}
|
||||
</VideoUploadDate>
|
||||
)}
|
||||
{issueObj?.created && (
|
||||
<VideoUploadDate>
|
||||
{formatDate(issueObj.created)}
|
||||
</VideoUploadDate>
|
||||
)}
|
||||
</NameAndDateContainer>
|
||||
</BottomParent>
|
||||
</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 { styled } from "@mui/system";
|
||||
|
||||
export const FilePlayerContainer = styled(Box)(({ theme }) => ({
|
||||
maxWidth: "95%",
|
||||
width: "1000px",
|
||||
width: "90vw",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
@ -11,7 +11,6 @@ export const FilePlayerContainer = styled(Box)(({ theme }) => ({
|
||||
|
||||
export const FileTitle = styled(Typography)(({ theme }) => ({
|
||||
fontFamily: "Raleway",
|
||||
fontSize: "20px",
|
||||
color: theme.palette.text.primary,
|
||||
userSelect: "none",
|
||||
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 { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { setIsLoadingGlobal } from "../../state/features/globalSlice";
|
||||
import { Avatar, Box, Typography, useTheme } from "@mui/material";
|
||||
import { RootState } from "../../state/store";
|
||||
import QORTicon from "../../assets/icons/CoinIcons/qort.png";
|
||||
import { BountyDisplay } from "../../components/common/BountyDisplay.tsx";
|
||||
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 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 {
|
||||
AuthorTextComment,
|
||||
FileAttachmentContainer,
|
||||
@ -18,23 +47,6 @@ import {
|
||||
StyledCardColComment,
|
||||
StyledCardHeaderComment,
|
||||
} from "./IssueContent-styles.tsx";
|
||||
import { formatDate } from "../../utils/time";
|
||||
import { CommentSection } from "../../components/common/Comments/CommentSection";
|
||||
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
|
||||
import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml";
|
||||
import FileElement from "../../components/common/FileElement";
|
||||
import { allCategoryData } from "../../constants/Categories/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) {
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
@ -55,7 +67,6 @@ export const IssueContent = () => {
|
||||
const [descriptionHeight, setDescriptionHeight] = useState<null | number>(
|
||||
null
|
||||
);
|
||||
const [issueIcons, setIssueIcons] = useState<string[]>([]);
|
||||
const userAvatarHash = useSelector(
|
||||
(state: RootState) => state.global.userAvatarHash
|
||||
);
|
||||
@ -99,7 +110,7 @@ export const IssueContent = () => {
|
||||
}, [issueData]);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const getVideoData = React.useCallback(async (name: string, id: string) => {
|
||||
const getIssueData = React.useCallback(async (name: string, id: string) => {
|
||||
try {
|
||||
if (!name || !id) return;
|
||||
dispatch(setIsLoadingGlobal(true));
|
||||
@ -142,17 +153,16 @@ export const IssueContent = () => {
|
||||
};
|
||||
|
||||
verifyPayment(combinedData).then(feeData => {
|
||||
console.log(
|
||||
"async data: ",
|
||||
appendIsPaidToFeeData(combinedData, feeData)
|
||||
);
|
||||
setIssueData(appendIsPaidToFeeData(combinedData, feeData));
|
||||
const dataWithFees = appendIsPaidToFeeData(combinedData, feeData);
|
||||
console.log("dataWithFees: ", dataWithFees);
|
||||
setIssueData(dataWithFees);
|
||||
dispatch(addToHashMap(combinedData));
|
||||
checkforPlaylist(name, id, combinedData?.code);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
dispatch(setIsLoadingGlobal(false));
|
||||
}
|
||||
@ -212,7 +222,7 @@ export const IssueContent = () => {
|
||||
const responseDataSearchVid = await response.json();
|
||||
|
||||
if (responseDataSearchVid?.length > 0) {
|
||||
let resourceData2 = responseDataSearchVid[0];
|
||||
const resourceData2 = responseDataSearchVid[0];
|
||||
videos.push(resourceData2);
|
||||
}
|
||||
}
|
||||
@ -221,10 +231,12 @@ export const IssueContent = () => {
|
||||
setPlaylistData(combinedData);
|
||||
}
|
||||
}
|
||||
} catch (error) {}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (name && id) {
|
||||
const existingVideo = hashMapVideos[id];
|
||||
|
||||
@ -234,42 +246,11 @@ export const IssueContent = () => {
|
||||
checkforPlaylist(name, id, existingVideo?.code);
|
||||
});
|
||||
} else {
|
||||
getVideoData(name, id);
|
||||
getIssueData(name, id);
|
||||
}
|
||||
}
|
||||
}, [id, name]);
|
||||
|
||||
// const getAvatar = React.useCallback(async (author: string) => {
|
||||
// try {
|
||||
// let url = await qortalRequest({
|
||||
// action: 'GET_QDN_RESOURCE_URL',
|
||||
// name: author,
|
||||
// service: 'THUMBNAIL',
|
||||
// identifier: 'qortal_avatar'
|
||||
// })
|
||||
|
||||
// setAvatarUrl(url)
|
||||
// dispatch(setUserAvatarHash({
|
||||
// name: author,
|
||||
// url
|
||||
// }))
|
||||
// } catch (error) { }
|
||||
// }, [])
|
||||
|
||||
// React.useEffect(() => {
|
||||
// if (name && !avatarUrl) {
|
||||
// const existingAvatar = userAvatarHash[name]
|
||||
|
||||
// if (existingAvatar) {
|
||||
// setAvatarUrl(existingAvatar)
|
||||
// } else {
|
||||
// getAvatar(name)
|
||||
// }
|
||||
|
||||
// }
|
||||
|
||||
// }, [name, userAvatarHash])
|
||||
|
||||
useEffect(() => {
|
||||
if (contentRef.current) {
|
||||
const height = contentRef.current.offsetHeight;
|
||||
@ -279,49 +260,14 @@ export const IssueContent = () => {
|
||||
setDescriptionHeight(maxDescriptionHeight);
|
||||
}
|
||||
}
|
||||
if (issueData) {
|
||||
const icons = getIconsFromObject(issueData);
|
||||
setIssueIcons(icons);
|
||||
}
|
||||
}, [issueData]);
|
||||
|
||||
const categoriesDisplay = useMemo(() => {
|
||||
if (issueData) {
|
||||
const categoryList = getCategoriesFromObject(issueData);
|
||||
const categoryNames = categoryList.map((categoryID, index) => {
|
||||
let categoryName: Category;
|
||||
if (index === 0) {
|
||||
categoryName = allCategoryData.category.find(
|
||||
item => item?.id === +categoryList[0]
|
||||
);
|
||||
} else {
|
||||
const subCategories = allCategoryData.subCategories[index - 1];
|
||||
const selectedSubCategory = subCategories[categoryList[index - 1]];
|
||||
if (selectedSubCategory) {
|
||||
categoryName = selectedSubCategory.find(
|
||||
item => item?.id === +categoryList[index]
|
||||
);
|
||||
}
|
||||
}
|
||||
return categoryName?.name;
|
||||
});
|
||||
const filteredCategoryNames = categoryNames.filter(name => name);
|
||||
let categoryDisplay = "";
|
||||
const separator = " > ";
|
||||
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;
|
||||
const categoryNames = getCategoryNames(issueData);
|
||||
return categoryNamesToString(categoryNames, issueData?.QappName);
|
||||
}
|
||||
return "no videodata";
|
||||
return "No Issue Data";
|
||||
}, [issueData]);
|
||||
|
||||
return (
|
||||
@ -347,10 +293,12 @@ export const IssueContent = () => {
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<IssueIcons
|
||||
iconSources={issueIcons}
|
||||
style={{ marginRight: "20px" }}
|
||||
/>
|
||||
{issueData && (
|
||||
<IssueIcons
|
||||
issueData={issueData}
|
||||
style={{ marginRight: "20px" }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<FileTitle
|
||||
variant="h1"
|
||||
@ -367,9 +315,9 @@ export const IssueContent = () => {
|
||||
</div>
|
||||
{issueData?.created && (
|
||||
<Typography
|
||||
variant="h6"
|
||||
variant="h4"
|
||||
sx={{
|
||||
fontSize: "12px",
|
||||
fontSize: "18px",
|
||||
}}
|
||||
color={theme.palette.text.primary}
|
||||
>
|
||||
@ -413,11 +361,33 @@ export const IssueContent = () => {
|
||||
</StyledCardHeaderComment>
|
||||
</Box>
|
||||
<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>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: "bold",
|
||||
fontSize: "16px",
|
||||
userSelect: "none",
|
||||
}}
|
||||
>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { createSlice } from "@reduxjs/toolkit";
|
||||
import { PublishFeeData } from "../../constants/PublishFees/SendFeeFunctions.ts";
|
||||
import { BountyData } from "../../utils/qortalRequests.ts";
|
||||
|
||||
interface GlobalState {
|
||||
files: Issue[];
|
||||
@ -47,6 +48,7 @@ export interface Issue {
|
||||
isValid?: boolean;
|
||||
code?: string;
|
||||
feeData?: PublishFeeData;
|
||||
bountyData?: BountyData;
|
||||
paymentVerified?: boolean;
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import notificationsReducer from "./features/notificationsSlice";
|
||||
import authReducer from "./features/authSlice";
|
||||
import globalReducer from "./features/globalSlice";
|
||||
import authReducer from "./features/authSlice.js";
|
||||
import fileReducer from "./features/fileSlice.ts";
|
||||
import globalReducer from "./features/globalSlice.js";
|
||||
import notificationsReducer from "./features/notificationsSlice.js";
|
||||
|
||||
export const store = configureStore({
|
||||
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 { 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) => {
|
||||
return (await qortalRequest({
|
||||
@ -31,3 +38,216 @@ export const sendQchatDM = async (
|
||||
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];
|
||||
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);
|
||||
};
|
||||
|
@ -1,26 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"noImplicitAny": false,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"strictNullChecks": false,
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"noImplicitAny": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "node",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"strictNullChecks": false,
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user