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

Added features from Q-Support 1.1.0 posted on Q-Share:

Bounties can be added to an Issue. They can be either a direct payment from the publisher in ANY supported coin, or they can be a link to a Q-Fund.

Q-Funds that are still in progress have a donate button so users can support it without having to leave Q-Support and open the Q-Fund.

Any category can be searched for individually using the "Single Category" Combobox.

user can add source code to their Issue, so it is easier to see.

IssueIcons have a tooltip that displays the name of its category. If the category is Q-Apps/Websites, then the icon of its owner will also be displayed.
This commit is contained in:
Qortal Dev 2024-06-14 10:03:16 -06:00
parent 87c990c164
commit 64b4cc0304
30 changed files with 9136 additions and 8005 deletions

2193
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{ {
"name": "qsupport", "name": "qsupport",
"private": true, "private": true,
"version": "1.0.0", "version": "1.1.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@ -17,11 +17,12 @@
"@reduxjs/toolkit": "^1.9.3", "@reduxjs/toolkit": "^1.9.3",
"compressorjs": "^1.2.1", "compressorjs": "^1.2.1",
"dompurify": "^3.0.6", "dompurify": "^3.0.6",
"eslint": "^8.57.0",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"moment": "^2.29.4", "moment": "^2.29.4",
"prettier": "^3.2.4", "prettier": "^3.2.5",
"quill-image-resize-module-react": "^3.0.0", "quill-image-resize-module-react": "^3.0.0",
"react": "^18.2.0", "react": "^18.3.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",
"react-intersection-observer": "^9.4.3", "react-intersection-observer": "^9.4.3",
@ -36,13 +37,12 @@
"devDependencies": { "devDependencies": {
"@types/react": "^18.0.28", "@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^5.57.1", "@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.57.1", "@typescript-eslint/parser": "^5.62.0",
"@vitejs/plugin-react": "^4.0.0", "@vitejs/plugin-react": "^4.3.0",
"eslint": "^8.38.0", "eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.7",
"eslint-plugin-react-refresh": "^0.3.4",
"typescript": "^5.0.2", "typescript": "^5.0.2",
"vite": "6.0.0-alpha.1" "vite": "5.2.12"
} }
} }

View 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>
);
};

View File

@ -1,32 +1,43 @@
import React, { useEffect, useRef, useState } from "react";
import {
ActionButton,
CrowdfundActionButtonRow,
CustomInputField,
ModalBody,
NewCrowdfundTitle,
} from "./EditIssue-styles.tsx";
import { Box, Modal, Typography, useTheme } from "@mui/material";
import RemoveIcon from "@mui/icons-material/Remove"; import RemoveIcon from "@mui/icons-material/Remove";
import {
Box,
MenuItem,
Modal,
TextField,
Typography,
useTheme,
} from "@mui/material";
import React, { useEffect, useRef, useState } from "react";
import { useDropzone } from "react-dropzone";
import { useDispatch, useSelector } from "react-redux";
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
import { useDispatch, useSelector } from "react-redux"; import { allCategoryData } from "../../constants/Categories/Categories.ts";
import { useDropzone } from "react-dropzone"; import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
import { log, titleFormatter } from "../../constants/Misc.ts";
import { setNotification } from "../../state/features/notificationsSlice"; import {
import { objectToBase64 } from "../../utils/toBase64"; feeAmountBase,
import { RootState } from "../../state/store"; supportedCoins,
} from "../../constants/PublishFees/FeeData.tsx";
import { CoinType } from "../../constants/PublishFees/FeePricePublish/FeePricePublish.ts";
import { payPublishFeeQORT } from "../../constants/PublishFees/SendFeeFunctions.ts";
import { verifyPayment } from "../../constants/PublishFees/VerifyPayment.ts";
import { ThemeButtonBright } from "../../pages/Home/Home-styles.tsx";
import { import {
setEditFile, setEditFile,
updateFile, updateFile,
updateInHashMap, updateInHashMap,
} from "../../state/features/fileSlice.ts"; } from "../../state/features/fileSlice.ts";
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll"; import { setNotification } from "../../state/features/notificationsSlice.ts";
import { TextEditor } from "../common/TextEditor/TextEditor"; import { RootState } from "../../state/store.ts";
import { extractTextFromHTML } from "../common/TextEditor/utils"; import { BountyData, validateBountyInput } from "../../utils/qortalRequests.ts";
import { allCategoryData } from "../../constants/Categories/Categories.ts"; import { objectToBase64 } from "../../utils/toBase64.js";
import { log, titleFormatter } from "../../constants/Misc.ts"; import { isNumber } from "../../utils/utilFunctions.ts";
import {
AutocompleteQappNames,
QappNamesRef,
} from "../common/AutocompleteQappNames.tsx";
import { import {
CategoryList, CategoryList,
CategoryListRef, CategoryListRef,
@ -36,14 +47,16 @@ import {
ImagePublisher, ImagePublisher,
ImagePublisherRef, ImagePublisherRef,
} from "../common/ImagePublisher/ImagePublisher.tsx"; } from "../common/ImagePublisher/ImagePublisher.tsx";
import { ThemeButtonBright } from "../../pages/Home/Home-styles.tsx"; import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll.js";
import { TextEditor } from "../common/TextEditor/TextEditor.js";
import { extractTextFromHTML } from "../common/TextEditor/utils.js";
import { import {
AutocompleteQappNames, ActionButton,
QappNamesRef, CrowdfundActionButtonRow,
} from "../common/AutocompleteQappNames.tsx"; CustomInputField,
import { payPublishFeeQORT } from "../../constants/PublishFees/SendFeeFunctions.ts"; ModalBody,
import { feeAmountBase } from "../../constants/PublishFees/FeeData.tsx"; NewCrowdfundTitle,
import { verifyPayment } from "../../constants/PublishFees/VerifyPayment.ts"; } from "./EditIssue-styles.tsx";
const uid = new ShortUniqueId(); const uid = new ShortUniqueId();
const shortuid = new ShortUniqueId({ length: 5 }); const shortuid = new ShortUniqueId({ length: 5 });
@ -81,6 +94,9 @@ export const EditIssue = () => {
); );
const [publishes, setPublishes] = useState<any>(null); const [publishes, setPublishes] = useState<any>(null);
const bountyData = editIssueProperties?.bountyData;
const [bounty, setBounty] = useState<string>("");
const [sourceCode, setSourceCode] = useState<string>("");
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false); const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false);
const [videoPropertiesToSetToRedux, setVideoPropertiesToSetToRedux] = const [videoPropertiesToSetToRedux, setVideoPropertiesToSetToRedux] =
useState(null); useState(null);
@ -92,6 +108,11 @@ export const EditIssue = () => {
const [files, setFiles] = useState<VideoFile[]>([]); const [files, setFiles] = useState<VideoFile[]>([]);
const [selectedCategories, setSelectedCategories] = useState<string[]>([]); const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [isIssuePaid, setIsIssuePaid] = useState<boolean>(true); const [isIssuePaid, setIsIssuePaid] = useState<boolean>(true);
const [coin, setCoin] = useState<CoinType>("QORT");
const [showCoins, setShowCoins] = useState<boolean>(false);
const categoryListRef = useRef<CategoryListRef>(null); const categoryListRef = useRef<CategoryListRef>(null);
const imagePublisherRef = useRef<ImagePublisherRef>(null); const imagePublisherRef = useRef<ImagePublisherRef>(null);
const autocompleteRef = useRef<QappNamesRef>(null); const autocompleteRef = useRef<QappNamesRef>(null);
@ -148,6 +169,14 @@ export const EditIssue = () => {
const categoriesFromEditFile = const categoriesFromEditFile =
getCategoriesFromObject(editIssueProperties); getCategoriesFromObject(editIssueProperties);
setSelectedCategories(categoriesFromEditFile); setSelectedCategories(categoriesFromEditFile);
setBounty(bountyData?.crowdfundLink || bountyData?.amount || "");
setShowCoins(
isNumber(editIssueProperties?.bountyData?.amount || undefined)
);
if (editIssueProperties?.bountyData?.coinType)
setCoin(editIssueProperties?.bountyData?.coinType);
if (editIssueProperties?.bountyData?.sourceCodeLink)
setCoin(editIssueProperties?.bountyData?.sourceCodeLink);
} }
}, [editIssueProperties]); }, [editIssueProperties]);
@ -162,6 +191,11 @@ export const EditIssue = () => {
async function publishQDNResource(payFee: boolean) { async function publishQDNResource(payFee: boolean) {
try { try {
if (bounty) {
const isValidated = await validateBountyInput(bounty);
if (!isValidated) throw new Error("Bounty is not valid");
}
if (!categoryListRef.current) throw new Error("No CategoryListRef found"); if (!categoryListRef.current) throw new Error("No CategoryListRef found");
if (!userAddress) throw new Error("Unable to locate user address"); if (!userAddress) throw new Error("Unable to locate user address");
if (!description) throw new Error("Please enter a description"); if (!description) throw new Error("Please enter a description");
@ -169,7 +203,7 @@ export const EditIssue = () => {
if (!allCategoriesSelected) if (!allCategoriesSelected)
throw new Error("All Categories must be selected"); throw new Error("All Categories must be selected");
console.log("categories", selectedCategories); if (log) console.log("categories", selectedCategories);
const QappsCategoryID = "3"; const QappsCategoryID = "3";
if ( if (
selectedCategories[0] === QappsCategoryID && selectedCategories[0] === QappsCategoryID &&
@ -208,9 +242,9 @@ export const EditIssue = () => {
.toLowerCase(); .toLowerCase();
if (!sanitizeTitle) throw new Error("Please enter a title"); if (!sanitizeTitle) throw new Error("Please enter a title");
let fileReferences = []; const fileReferences = [];
let listOfPublishes = []; const listOfPublishes = [];
const fullDescription = extractTextFromHTML(description); const fullDescription = extractTextFromHTML(description);
for (const publish of files) { for (const publish of files) {
@ -228,17 +262,17 @@ export const EditIssue = () => {
if (fileExtensionSplit?.length > 1) { if (fileExtensionSplit?.length > 1) {
fileExtension = fileExtensionSplit?.pop() || ""; fileExtension = fileExtensionSplit?.pop() || "";
} }
let firstPartName = fileExtensionSplit[0]; const firstPartName = fileExtensionSplit[0];
let filename = firstPartName.slice(0, 15); let filename = firstPartName.slice(0, 15);
// Step 1: Replace all white spaces with underscores // Step 1: Replace all white spaces with underscores
// Replace all forms of whitespace (including non-standard ones) with underscores // Replace all forms of whitespace (including non-standard ones) with underscores
let stringWithUnderscores = filename.replace(/[\s\uFEFF\xA0]+/g, "_"); const stringWithUnderscores = filename.replace(/[\s\uFEFF\xA0]+/g, "_");
// Remove all non-alphanumeric characters (except underscores) // Remove all non-alphanumeric characters (except underscores)
let alphanumericString = stringWithUnderscores.replace( const alphanumericString = stringWithUnderscores.replace(
/[^a-zA-Z0-9_]/g, /[^a-zA-Z0-9_]/g,
"" ""
); );
@ -250,7 +284,7 @@ export const EditIssue = () => {
} }
const categoryString = `**${categoryListRef.current?.getSelectedCategories()}**`; const categoryString = `**${categoryListRef.current?.getSelectedCategories()}**`;
let metadescription = categoryString + fullDescription.slice(0, 150); const metadescription = categoryString + fullDescription.slice(0, 150);
const requestBodyVideo: any = { const requestBodyVideo: any = {
action: "PUBLISH_QDN_RESOURCE", action: "PUBLISH_QDN_RESOURCE",
@ -275,6 +309,14 @@ export const EditIssue = () => {
} }
const selectedQappName = autocompleteRef?.current?.getSelectedValue(); const selectedQappName = autocompleteRef?.current?.getSelectedValue();
const isBountyNumber = isNumber(bounty);
const bountyData: BountyData = {
amount: isBountyNumber ? Number(bounty) : undefined,
crowdfundLink: isBountyNumber ? undefined : bounty,
coinType: coin,
sourceCodeLink: sourceCode,
};
const issueObject: any = { const issueObject: any = {
title, title,
version: editIssueProperties.version, version: editIssueProperties.version,
@ -286,6 +328,7 @@ export const EditIssue = () => {
images: imagePublisherRef?.current?.getImageArray(), images: imagePublisherRef?.current?.getImageArray(),
QappName: selectedQappName, QappName: selectedQappName,
feeData: editIssueProperties?.feeData, feeData: editIssueProperties?.feeData,
bountyData,
}; };
if (payFee) { if (payFee) {
const publishFeeResponse = await payPublishFeeQORT(feeAmountBase); const publishFeeResponse = await payPublishFeeQORT(feeAmountBase);
@ -309,7 +352,7 @@ export const EditIssue = () => {
categoryListRef.current?.getCategoriesFetchString(selectedCategories); categoryListRef.current?.getCategoriesFetchString(selectedCategories);
const metaDataString = `**${categoryString + QappNameString}**`; const metaDataString = `**${categoryString + QappNameString}**`;
let metadescription = metaDataString + fullDescription.slice(0, 150); const metadescription = metaDataString + fullDescription.slice(0, 150);
if (log) console.log("description is: ", metadescription); if (log) console.log("description is: ", metadescription);
if (log) console.log("description length is: ", metadescription.length); if (log) console.log("description length is: ", metadescription.length);
if (log) console.log("characters left:", 240 - metadescription.length); if (log) console.log("characters left:", 240 - metadescription.length);
@ -318,7 +361,6 @@ export const EditIssue = () => {
const fileObjectToBase64 = await objectToBase64(issueObject); const fileObjectToBase64 = await objectToBase64(issueObject);
// Description is obtained from raw data // Description is obtained from raw data
const requestBodyJson: any = { const requestBodyJson: any = {
action: "PUBLISH_QDN_RESOURCE", action: "PUBLISH_QDN_RESOURCE",
name: name, name: name,
@ -465,12 +507,53 @@ export const EditIssue = () => {
</Box> </Box>
</Box> </Box>
{isShowQappNameTextField() && ( {isShowQappNameTextField() && (
<>
<AutocompleteQappNames <AutocompleteQappNames
ref={autocompleteRef} ref={autocompleteRef}
namesList={QappNames} namesList={QappNames}
initialSelection={editIssueProperties?.QappName} initialSelection={editIssueProperties?.QappName}
/> />
</>
)} )}
<CustomInputField
name="q-app-source-code"
label="Link to Source Code"
variant="filled"
value={sourceCode}
onChange={e => setSourceCode(e.target.value.trim())}
inputProps={{ maxLength: 200 }}
/>
<CustomInputField
name="q-fund-link"
label="Bounty Amount or Q-Fund Link"
variant="filled"
value={bounty}
onChange={e => {
const bountyValue = e.target.value.trim();
setBounty(bountyValue);
const bountyIsNumber = isNumber(bountyValue);
setShowCoins(bountyIsNumber);
if (!bountyIsNumber) setCoin("QORT");
}}
inputProps={{ maxLength: 200 }}
/>
<TextField
label={"Select Coin"}
select
fullWidth
value={coin}
onChange={e => setCoin(e.target.value as CoinType)}
sx={{
display: showCoins ? "block" : "none",
width: "20%",
}}
>
{supportedCoins.map((coin, index) => (
<MenuItem value={coin} key={coin + index}>
{coin}
</MenuItem>
))}
</TextField>
<ImagePublisher <ImagePublisher
ref={imagePublisherRef} ref={imagePublisherRef}
initialImages={editIssueProperties?.images} initialImages={editIssueProperties?.images}

View File

@ -1,33 +1,46 @@
import React, { useEffect, useRef, useState } from "react";
import {
ActionButton,
ActionButtonRow,
CustomInputField,
ModalBody,
NewCrowdfundTitle,
StyledButton,
} from "./PublishIssue-styles.tsx";
import { Box, Modal, Typography, useTheme } from "@mui/material";
import RemoveIcon from "@mui/icons-material/Remove";
import ShortUniqueId from "short-unique-id";
import { useDispatch, useSelector } from "react-redux";
import AddBoxIcon from "@mui/icons-material/AddBox"; import AddBoxIcon from "@mui/icons-material/AddBox";
import RemoveIcon from "@mui/icons-material/Remove";
import {
Box,
MenuItem,
Modal,
TextField,
Typography,
useTheme,
} from "@mui/material";
import React, { useRef, useState } from "react";
import { useDropzone } from "react-dropzone"; import { useDropzone } from "react-dropzone";
import { useDispatch, useSelector } from "react-redux";
import { setNotification } from "../../state/features/notificationsSlice"; import ShortUniqueId from "short-unique-id";
import { objectToBase64 } from "../../utils/toBase64";
import { RootState } from "../../state/store";
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll";
import { TextEditor } from "../common/TextEditor/TextEditor";
import { extractTextFromHTML } from "../common/TextEditor/utils";
import { allCategoryData } from "../../constants/Categories/Categories.ts"; import { allCategoryData } from "../../constants/Categories/Categories.ts";
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
import { import {
fontSizeLarge, fontSizeLarge,
fontSizeSmall, fontSizeSmall,
log, log,
titleFormatter, titleFormatter,
} from "../../constants/Misc.ts"; } from "../../constants/Misc.ts";
import {
feeAmountBase,
feeDisclaimer,
supportedCoins,
} from "../../constants/PublishFees/FeeData.tsx";
import { CoinType } from "../../constants/PublishFees/FeePricePublish/FeePricePublish.ts";
import {
payPublishFeeQORT,
PublishFeeData,
} from "../../constants/PublishFees/SendFeeFunctions.ts";
import { ThemeButtonBright } from "../../pages/Home/Home-styles.tsx";
import { setNotification } from "../../state/features/notificationsSlice";
import { RootState } from "../../state/store";
import { BountyData, validateBountyInput } from "../../utils/qortalRequests.ts";
import { objectToBase64 } from "../../utils/toBase64";
import { isNumber } from "../../utils/utilFunctions.ts";
import {
AutocompleteQappNames,
QappNamesRef,
} from "../common/AutocompleteQappNames.tsx";
import { import {
CategoryList, CategoryList,
CategoryListRef, CategoryListRef,
@ -36,19 +49,17 @@ import {
ImagePublisher, ImagePublisher,
ImagePublisherRef, ImagePublisherRef,
} from "../common/ImagePublisher/ImagePublisher.tsx"; } from "../common/ImagePublisher/ImagePublisher.tsx";
import { ThemeButtonBright } from "../../pages/Home/Home-styles.tsx"; import { MultiplePublish } from "../common/MultiplePublish/MultiplePublishAll";
import { TextEditor } from "../common/TextEditor/TextEditor";
import { extractTextFromHTML } from "../common/TextEditor/utils";
import { import {
AutocompleteQappNames, ActionButton,
QappNamesRef, ActionButtonRow,
} from "../common/AutocompleteQappNames.tsx"; CustomInputField,
import { ModalBody,
feeAmountBase, NewCrowdfundTitle,
feeDisclaimer, StyledButton,
} from "../../constants/PublishFees/FeeData.tsx"; } from "./PublishIssue-styles.tsx";
import {
payPublishFeeQORT,
PublishFeeData,
} from "../../constants/PublishFees/SendFeeFunctions.ts";
const uid = new ShortUniqueId(); const uid = new ShortUniqueId();
const shortuid = new ShortUniqueId({ length: 5 }); const shortuid = new ShortUniqueId({ length: 5 });
@ -72,10 +83,6 @@ interface VideoFile {
export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => { export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
const theme = useTheme(); const theme = useTheme();
const dispatch = useDispatch(); const dispatch = useDispatch();
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false);
const [QappName, setQappName] = useState<string>("");
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const username = useSelector((state: RootState) => state.auth?.user?.name); const username = useSelector((state: RootState) => state.auth?.user?.name);
const userAddress = useSelector( const userAddress = useSelector(
(state: RootState) => state.auth?.user?.address (state: RootState) => state.auth?.user?.address
@ -83,6 +90,12 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
const QappNames = useSelector( const QappNames = useSelector(
(state: RootState) => state.file.publishedQappNames (state: RootState) => state.file.publishedQappNames
); );
const [isOpenMultiplePublish, setIsOpenMultiplePublish] = useState(false);
const [QappName, setQappName] = useState<string>("");
const [selectedCategories, setSelectedCategories] = useState<string[]>([]);
const [files, setFiles] = useState<VideoFile[]>([]); const [files, setFiles] = useState<VideoFile[]>([]);
const [isOpen, setIsOpen] = useState<boolean>(false); const [isOpen, setIsOpen] = useState<boolean>(false);
@ -99,6 +112,10 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
const [playlistSetting, setPlaylistSetting] = useState<null | string>(null); const [playlistSetting, setPlaylistSetting] = useState<null | string>(null);
const [publishes, setPublishes] = useState<any>(null); const [publishes, setPublishes] = useState<any>(null);
const [bounty, setBounty] = useState<string>("");
const [sourceCode, setSourceCode] = useState<string>("");
const [coin, setCoin] = useState<CoinType>("QORT");
const [showCoins, setShowCoins] = useState<boolean>(false);
const categoryListRef = useRef<CategoryListRef>(null); const categoryListRef = useRef<CategoryListRef>(null);
const imagePublisherRef = useRef<ImagePublisherRef>(null); const imagePublisherRef = useRef<ImagePublisherRef>(null);
@ -139,17 +156,16 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
}, },
}); });
useEffect(() => {
if (editContent) {
}
}, [editContent]);
const onClose = () => { const onClose = () => {
setIsOpen(false); setIsOpen(false);
}; };
async function publishQDNResource() { const publishQDNResource = async () => {
try { try {
if (bounty) {
const isValidated = await validateBountyInput(bounty);
if (!isValidated) throw new Error("Bounty is not valid");
}
if (!categoryListRef.current) throw new Error("No CategoryListRef found"); if (!categoryListRef.current) throw new Error("No CategoryListRef found");
if (!userAddress) throw new Error("Unable to locate user address"); if (!userAddress) throw new Error("Unable to locate user address");
if (!description) throw new Error("Please enter a description"); if (!description) throw new Error("Please enter a description");
@ -197,9 +213,9 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
.toLowerCase(); .toLowerCase();
if (!sanitizeTitle) throw new Error("Please enter a title"); if (!sanitizeTitle) throw new Error("Please enter a title");
let fileReferences = []; const fileReferences = [];
let listOfPublishes = []; const listOfPublishes = [];
const fullDescription = extractTextFromHTML(description); const fullDescription = extractTextFromHTML(description);
@ -214,17 +230,17 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
if (fileExtensionSplit?.length > 1) { if (fileExtensionSplit?.length > 1) {
fileExtension = fileExtensionSplit?.pop() || ""; fileExtension = fileExtensionSplit?.pop() || "";
} }
let firstPartName = fileExtensionSplit[0]; const firstPartName = fileExtensionSplit[0];
let filename = firstPartName.slice(0, 15); let filename = firstPartName.slice(0, 15);
// Step 1: Replace all white spaces with underscores // Step 1: Replace all white spaces with underscores
// Replace all forms of whitespace (including non-standard ones) with underscores // Replace all forms of whitespace (including non-standard ones) with underscores
let stringWithUnderscores = filename.replace(/[\s\uFEFF\xA0]+/g, "_"); const stringWithUnderscores = filename.replace(/[\s\uFEFF\xA0]+/g, "_");
// Remove all non-alphanumeric characters (except underscores) // Remove all non-alphanumeric characters (except underscores)
let alphanumericString = stringWithUnderscores.replace( const alphanumericString = stringWithUnderscores.replace(
/[^a-zA-Z0-9_]/g, /[^a-zA-Z0-9_]/g,
"" ""
); );
@ -236,7 +252,7 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
} }
const categoryString = `**${categoryListRef.current?.getSelectedCategories()}**`; const categoryString = `**${categoryListRef.current?.getSelectedCategories()}**`;
let metadescription = categoryString + fullDescription.slice(0, 150); const metadescription = categoryString + fullDescription.slice(0, 150);
const requestBodyFile: any = { const requestBodyFile: any = {
action: "PUBLISH_QDN_RESOURCE", action: "PUBLISH_QDN_RESOURCE",
@ -275,6 +291,13 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
senderName: "", senderName: "",
}; };
const isBountyNumber = isNumber(bounty);
const bountyData: BountyData = {
amount: isBountyNumber ? Number(bounty) : undefined,
crowdfundLink: isBountyNumber ? undefined : bounty,
coinType: coin,
sourceCodeLink: sourceCode,
};
const issueObject: any = { const issueObject: any = {
title, title,
version: 1, version: 1,
@ -286,6 +309,7 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
images: imagePublisherRef?.current?.getImageArray(), images: imagePublisherRef?.current?.getImageArray(),
QappName: selectedQappName, QappName: selectedQappName,
feeData, feeData,
bountyData,
}; };
const QappNameString = autocompleteRef?.current?.getQappNameFetchString(); const QappNameString = autocompleteRef?.current?.getQappNameFetchString();
@ -293,7 +317,7 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
categoryListRef.current?.getCategoriesFetchString(categoryList); categoryListRef.current?.getCategoriesFetchString(categoryList);
const metaDataString = `**${categoryString + QappNameString}**`; const metaDataString = `**${categoryString + QappNameString}**`;
let metadescription = metaDataString + fullDescription.slice(0, 150); const metadescription = metaDataString + fullDescription.slice(0, 150);
if (log) console.log("description is: ", metadescription); if (log) console.log("description is: ", metadescription);
if (log) console.log("description length is: ", metadescription.length); if (log) console.log("description length is: ", metadescription.length);
@ -343,7 +367,7 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
if (!notificationObj) return; if (!notificationObj) return;
dispatch(setNotification(notificationObj)); dispatch(setNotification(notificationObj));
} }
} };
const isShowQappNameTextField = () => { const isShowQappNameTextField = () => {
const QappID = "3"; const QappID = "3";
@ -460,11 +484,52 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
/> />
</Box> </Box>
{isShowQappNameTextField() && ( {isShowQappNameTextField() && (
<>
<AutocompleteQappNames <AutocompleteQappNames
ref={autocompleteRef} ref={autocompleteRef}
namesList={QappNames} namesList={QappNames}
/> />
</>
)} )}
<CustomInputField
name="q-app-source-code"
label="Link to Source Code"
variant="filled"
value={sourceCode}
onChange={e => setSourceCode(e.target.value.trim())}
inputProps={{ maxLength: 200 }}
/>
<CustomInputField
name="q-fund-link"
label="Bounty Amount or Q-Fund Link"
variant="filled"
value={bounty}
onChange={e => {
const bountyValue = e.target.value.trim();
setBounty(bountyValue);
const bountyIsNumber = isNumber(bountyValue);
setShowCoins(bountyIsNumber);
if (!bountyIsNumber) setCoin("QORT");
}}
inputProps={{ maxLength: 200 }}
/>
<TextField
label={"Select Coin"}
select
fullWidth
value={coin}
onChange={e => setCoin(e.target.value as CoinType)}
sx={{
display: showCoins ? "block" : "none",
width: "20%",
}}
>
{supportedCoins.map((coin, index) => (
<MenuItem value={coin} key={coin + index}>
{coin}
</MenuItem>
))}
</TextField>
<ImagePublisher ref={imagePublisherRef} /> <ImagePublisher ref={imagePublisherRef} />
<CustomInputField <CustomInputField
name="title" name="title"

View 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>
</>
);
}
};

View 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}
/>
);
};

View File

@ -9,7 +9,10 @@ import {
CommentInputContainer, CommentInputContainer,
SubmitCommentButton, SubmitCommentButton,
} from "./Comments-styles"; } from "./Comments-styles";
import { QSUPPORT_COMMENT_BASE } from "../../../constants/Identifiers.ts"; import {
QSUPPORT_COMMENT_BASE,
useTestIdentifiers,
} from "../../../constants/Identifiers.ts";
import { sendQchatDM } from "../../../utils/qortalRequests.ts"; import { sendQchatDM } from "../../../utils/qortalRequests.ts";
import { maxCommentLength } from "../../../constants/Misc.ts"; import { maxCommentLength } from "../../../constants/Misc.ts";
@ -127,6 +130,10 @@ export const CommentEditor = ({
address = user?.address; address = user?.address;
name = user?.name || ""; name = user?.name || "";
const notificationMessage = `This is an automated Q-Support notification indicating that someone has commented on your issue here:
qortal://APP/Q-Support/issue/${postName}/${postId}`;
if (useTestIdentifiers) await sendQchatDM(postName, notificationMessage);
if (!address) { if (!address) {
errorMsg = "Cannot post: your address isn't available"; errorMsg = "Cannot post: your address isn't available";
} }
@ -180,9 +187,6 @@ export const CommentEditor = ({
// //
// ${value.substring(0, maxNotificationLength)}`; // ${value.substring(0, maxNotificationLength)}`;
const notificationMessage = `This is an automated Q-Support notification indicating that someone has commented on your issue here:
qortal://APP/Q-Support/issue/${postName}/${postId}`;
await sendQchatDM(postName, notificationMessage); await sendQchatDM(postName, notificationMessage);
} }
return resourceResponse; return resourceResponse;

View 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,
}));

View 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>
);
};

View File

@ -1,37 +1,49 @@
import AttachFileIcon from "@mui/icons-material/AttachFile"; import AttachFileIcon from "@mui/icons-material/AttachFile";
import React, { CSSProperties } from "react"; import { Tooltip, Typography } from "@mui/material";
import React, { CSSProperties, useEffect, useMemo, useState } from "react";
import { getIconsAndLabels } from "../../pages/IssueContent/IssueContent-functions.ts";
import { Issue } from "../../state/features/fileSlice.ts";
interface IssueIconProps { interface IssueIconProps {
iconSrc: string; iconSrc: string;
label?: string;
showBackupIcon?: boolean; showBackupIcon?: boolean;
style?: CSSProperties; style?: CSSProperties;
} }
export const IssueIcon = ({ export const IssueIcon = ({
iconSrc, iconSrc,
label,
showBackupIcon = true, showBackupIcon = true,
style, style,
}: IssueIconProps) => { }: IssueIconProps) => {
const displayFileIcon = !iconSrc && showBackupIcon; const displayFileIcon = !iconSrc && showBackupIcon;
const widthAndHeight = "50px";
return ( return (
<> <>
{iconSrc && ( {iconSrc && (
<Tooltip
title={<Typography fontSize={16}>{label}</Typography>}
arrow
disableHoverListener={!label}
placement={"top"}
>
<img <img
src={iconSrc} src={iconSrc}
width="50px" width={style?.width || widthAndHeight}
height="50px" height={style?.width || widthAndHeight}
style={{ style={{
borderRadius: "5px", borderRadius: "5px",
...style, ...style,
}} }}
/> />
</Tooltip>
)} )}
{displayFileIcon && ( {displayFileIcon && (
<AttachFileIcon <AttachFileIcon
sx={{ sx={{
...style, ...style,
width: "40px", width: style?.width || widthAndHeight,
height: "40px", height: style?.width || widthAndHeight,
}} }}
/> />
)} )}
@ -40,20 +52,34 @@ export const IssueIcon = ({
}; };
interface IssueIconsProps { interface IssueIconsProps {
iconSources: string[]; issueData: Issue;
showBackupIcon?: boolean; showBackupIcon?: boolean;
style?: CSSProperties; style?: CSSProperties;
} }
export const IssueIcons = ({ export const IssueIcons = ({
iconSources, issueData,
showBackupIcon = true, showBackupIcon = true,
style, style,
}: IssueIconsProps) => { }: IssueIconsProps) => {
return iconSources.map((icon, index) => ( const [icons, setIcons] = useState<string[]>([]);
const [iconLabels, setIconLabels] = useState<string[]>([]);
useMemo(() => {
if (issueData) {
getIconsAndLabels(issueData).then(data => {
const [iconData, labels] = data;
if (iconData) setIcons(iconData);
if (labels) setIconLabels(labels);
});
}
}, [issueData]);
return icons.map((icon, index) => (
<IssueIcon <IssueIcon
key={icon + index} key={icon + index}
iconSrc={icon} iconSrc={icon}
label={iconLabels ? iconLabels[index] : ""}
style={{ ...style }} style={{ ...style }}
showBackupIcon={showBackupIcon} showBackupIcon={showBackupIcon}
/> />

View File

@ -1,22 +1,21 @@
import BugReportIcon from "../../assets/icons/Bug-Report-Icon.webp";
import ClosedIcon from "../../assets/icons/Closed-Icon.webp";
import CompleteIcon from "../../assets/icons/Complete-Icon.webp";
import FeatureRequestIcon from "../../assets/icons/Feature-Request-Icon.webp";
import InProgressIcon from "../../assets/icons/In-Progress-Icon.webp";
import OpenIcon from "../../assets/icons/Open-Icon.webp";
import QappIcon from "../../assets/icons/Q-App-Icon.webp";
import CoreIcon from "../../assets/icons/Qortal-Core-Icon.webp";
import UIicon from "../../assets/icons/Qortal-UI-Icon.webp";
import TechSupportIcon from "../../assets/icons/Tech-Support-Icon.webp";
import UnknownIcon from "../../assets/icons/unknown.webp";
import { import {
Categories, Categories,
Category, Category,
CategoryData, CategoryData,
} from "../../components/common/CategoryList/CategoryList.tsx"; } from "../../components/common/CategoryList/CategoryList.tsx";
import { getAllCategoriesWithIcons } from "./CategoryFunctions.ts"; import { getAllCategoriesWithIcons } from "./CategoryFunctions.ts";
import CoreIcon from "../../assets/icons/Qortal-Core-Icon.webp";
import UIicon from "../../assets/icons/Qortal-UI-Icon.webp";
import QappIcon from "../../assets/icons/Q-App-Icon.webp";
import UnknownIcon from "../../assets/icons/unknown.webp";
import BugReportIcon from "../../assets/icons/Bug-Report-Icon.webp";
import FeatureRequestIcon from "../../assets/icons/Feature-Request-Icon.webp";
import TechSupportIcon from "../../assets/icons/Tech-Support-Icon.webp";
import OpenIcon from "../../assets/icons/Open-Icon.webp";
import ClosedIcon from "../../assets/icons/Closed-Icon.webp";
import InProgressIcon from "../../assets/icons/In-Progress-Icon.webp";
import CompleteIcon from "../../assets/icons/Complete-Icon.webp";
const issueLocationLabel = "Issue Location"; const issueLocationLabel = "Issue Location";
export const issueLocation: Category[] = [ export const issueLocation: Category[] = [
@ -48,7 +47,7 @@ export const secondCategories: Categories = {};
issueLocation.map(c => (secondCategories[c.id] = issueType)); issueLocation.map(c => (secondCategories[c.id] = issueType));
const issueLabel = "Issue State"; const issueLabel = "Issue State";
export const IssueState = [ export const issueState = [
{ id: 101, name: "Open", icon: OpenIcon, label: issueLabel }, { id: 101, name: "Open", icon: OpenIcon, label: issueLabel },
{ id: 102, name: "Closed", icon: ClosedIcon, label: issueLabel }, { id: 102, name: "Closed", icon: ClosedIcon, label: issueLabel },
{ id: 103, name: "In Progress", icon: InProgressIcon, label: issueLabel }, { id: 103, name: "In Progress", icon: InProgressIcon, label: issueLabel },
@ -57,7 +56,16 @@ export const IssueState = [
export const thirdCategories: Categories = {}; export const thirdCategories: Categories = {};
issueType.map(issueType => (thirdCategories[issueType.id] = IssueState)); issueType.map(issueType => (thirdCategories[issueType.id] = issueState));
export const allCategories = [
...issueLocation,
...issueType,
...issueState,
].map(category => ({
...category,
label: "Single Category",
}));
export const allCategoryData: CategoryData = { export const allCategoryData: CategoryData = {
category: issueLocation, category: issueLocation,

View File

@ -43,7 +43,7 @@ export const findAllCategoryData = (
categories: string[], categories: string[],
direction: Direction = "forward" direction: Direction = "forward"
) => { ) => {
let foundIcons: Category[] = []; const foundIcons: Category[] = [];
if (direction === "backward") categories.reverse(); if (direction === "backward") categories.reverse();
categories.map(category => { categories.map(category => {
@ -79,6 +79,15 @@ export const getAllCategoriesWithIcons = () => {
return categoriesWithIcons; return categoriesWithIcons;
}; };
export const getnamesFromObject = (fileObj: any) => {
const categories = getCategoriesFromObject(fileObj);
const names = categories.map(categoryID => {
return iconCategories.find(category => category.id === +categoryID)?.name;
});
return names
};
export const getIconsFromObject = (fileObj: any) => { export const getIconsFromObject = (fileObj: any) => {
const categories = getCategoriesFromObject(fileObj); const categories = getCategoriesFromObject(fileObj);
const icons = categories.map(categoryID => { const icons = categories.map(categoryID => {

View File

@ -10,6 +10,17 @@ export const FEE_BASE = useTestIdentifiers
? "MYTEST_support_fees" ? "MYTEST_support_fees"
: "q_support_fees"; : "q_support_fees";
export const supportedCoins = [
"QORT",
"BTC",
"LTC",
"DOGE",
"DGB",
"RVN",
"ARRR",
].sort((a, b) => {
return a.localeCompare(b);
});
export const maxFeePublishTimeDiff = 10; // time in minutes before/after publish when fee is considered valid export const maxFeePublishTimeDiff = 10; // time in minutes before/after publish when fee is considered valid
export type FeeType = "default" | "comment" | "like" | "dislike" | "superlike"; export type FeeType = "default" | "comment" | "like" | "dislike" | "superlike";

View File

@ -1,8 +1,8 @@
import { appName, FEE_BASE, feeAmountBase, FeeType } from "../FeeData.tsx";
import { objectToBase64 } from "../../../utils/toBase64.ts";
import { store } from "../../../state/store.ts";
import { setFeeData } from "../../../state/features/globalSlice.ts"; import { setFeeData } from "../../../state/features/globalSlice.ts";
import { store } from "../../../state/store.js";
import { objectToBase64 } from "../../../utils/toBase64.ts";
import { useTestIdentifiers } from "../../Identifiers.ts"; import { useTestIdentifiers } from "../../Identifiers.ts";
import { appName, FEE_BASE, feeAmountBase, FeeType } from "../FeeData.tsx";
export type CoinType = "QORT" | "BTC" | "LTC" | "DOGE" | "DGB" | "RVN" | "ARRR"; export type CoinType = "QORT" | "BTC" | "LTC" | "DOGE" | "DGB" | "RVN" | "ARRR";

2
src/global.d.ts vendored
View File

@ -39,6 +39,8 @@ interface QortalRequestOptions {
excludeBlocked?: boolean; excludeBlocked?: boolean;
exactMatchNames?: boolean; exactMatchNames?: boolean;
message?: string; message?: string;
txType?: string[];
confirmationStatus?: string;
} }
declare function qortalRequest(options: QortalRequestOptions): Promise<any>; declare function qortalRequest(options: QortalRequestOptions): Promise<any>;

View File

@ -1,5 +1,11 @@
import React from "react"; import React from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import {
QSUPPORT_FILE_BASE,
QSUPPORT_PLAYLIST_BASE,
} from "../constants/Identifiers.ts";
import { log } from "../constants/Misc.ts";
import { verifyAllPayments } from "../constants/PublishFees/VerifyPayment.ts";
import { import {
addFiles, addFiles,
addToHashMap, addToHashMap,
@ -19,13 +25,8 @@ import {
} from "../state/features/globalSlice"; } from "../state/features/globalSlice";
import { RootState } from "../state/store"; import { RootState } from "../state/store";
import { fetchAndEvaluateIssues } from "../utils/fetchVideos"; import { fetchAndEvaluateIssues } from "../utils/fetchVideos";
import { import { getBountyAmounts } from "../utils/qortalRequests.ts";
QSUPPORT_FILE_BASE,
QSUPPORT_PLAYLIST_BASE,
} from "../constants/Identifiers.ts";
import { queue } from "../wrappers/GlobalWrapper"; import { queue } from "../wrappers/GlobalWrapper";
import { log } from "../constants/Misc.ts";
import { verifyAllPayments } from "../constants/PublishFees/VerifyPayment.ts";
export const useFetchIssues = () => { export const useFetchIssues = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -70,7 +71,7 @@ export const useFetchIssues = () => {
const getAvatar = React.useCallback(async (author: string) => { const getAvatar = React.useCallback(async (author: string) => {
try { try {
let url = await qortalRequest({ const url = await qortalRequest({
action: "GET_QDN_RESOURCE_URL", action: "GET_QDN_RESOURCE_URL",
name: author, name: author,
service: "THUMBNAIL", service: "THUMBNAIL",
@ -83,14 +84,16 @@ export const useFetchIssues = () => {
url, url,
}) })
); );
} catch (error) {} } catch (error) {
console.log(error);
}
}, []); }, []);
const getIssue = async ( const getIssue = async (
user: string, user: string,
issueID: string, issueID: string,
content: any, content: any,
retries: number = 0 retries = 0
) => { ) => {
try { try {
const res = await fetchAndEvaluateIssues({ const res = await fetchAndEvaluateIssues({
@ -182,6 +185,7 @@ export const useFetchIssues = () => {
} }
} }
} catch (error) { } catch (error) {
console.log(error);
} finally { } finally {
dispatch(setIsLoadingGlobal(false)); dispatch(setIsLoadingGlobal(false));
} }
@ -277,11 +281,16 @@ export const useFetchIssues = () => {
} }
const issues = await Promise.all(verifiedIssuePromises); const issues = await Promise.all(verifiedIssuePromises);
const verifiedIssues = await verifyAllPayments(issues); const [verifiedIssues, bountyIssues] = await Promise.all([
verifyAllPayments(issues),
getBountyAmounts(issues),
]);
structureData = structureData.map((issue, index) => { structureData = structureData.map((issue, index) => {
return { return {
...issue, ...issue,
feeData: verifiedIssues[index]?.feeData, feeData: verifiedIssues[index]?.feeData,
bountyData: bountyIssues[index]?.bountyData,
}; };
}); });
@ -289,7 +298,6 @@ export const useFetchIssues = () => {
else dispatch(upsertFiles(structureData)); else dispatch(upsertFiles(structureData));
} catch (error) { } catch (error) {
console.log({ error }); console.log({ error });
} finally {
} }
}, },
[videos, hashMapFiles] [videos, hashMapFiles]
@ -349,7 +357,7 @@ export const useFetchIssues = () => {
} }
} }
} catch (error) { } catch (error) {
} finally { console.log(error);
} }
}, },
[filteredVideos, hashMapFiles] [filteredVideos, hashMapFiles]
@ -389,12 +397,14 @@ export const useFetchIssues = () => {
const newArray = responseData.slice(0, findVideo); const newArray = responseData.slice(0, findVideo);
dispatch(setCountNewFiles(newArray.length)); dispatch(setCountNewFiles(newArray.length));
return; return;
} catch (error) {} } catch (error) {
console.log(error);
}
}, [videos]); }, [videos]);
const getIssuesCount = React.useCallback(async () => { const getIssuesCount = React.useCallback(async () => {
try { try {
let url = `/arbitrary/resources/search?mode=ALL&includemetadata=false&limit=0&service=DOCUMENT&identifier=${QSUPPORT_FILE_BASE}`; const url = `/arbitrary/resources/search?mode=ALL&includemetadata=false&limit=0&service=DOCUMENT&identifier=${QSUPPORT_FILE_BASE}`;
const response = await fetch(url, { const response = await fetch(url, {
method: "GET", method: "GET",
@ -416,7 +426,6 @@ export const useFetchIssues = () => {
dispatch(setFilesPerNamePublished(filesPerNamePublished)); dispatch(setFilesPerNamePublished(filesPerNamePublished));
} catch (error) { } catch (error) {
console.log({ error }); console.log({ error });
} finally {
} }
}, []); }, []);

View File

@ -1,38 +1,38 @@
import { Box, Grid, Input, useTheme } from "@mui/material";
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../state/store"; import {
import { IssueList } from "./IssueList.tsx"; AutocompleteQappNames,
import { Box, Grid, Input, useTheme } from "@mui/material"; getPublishedQappNames,
import { useFetchIssues } from "../../hooks/useFetchIssues.tsx"; QappNamesRef,
} from "../../components/common/AutocompleteQappNames.tsx";
import {
CategoryList,
CategoryListRef,
getCategoriesFetchString,
} from "../../components/common/CategoryList/CategoryList.tsx";
import {
CategorySelect,
CategorySelectRef,
} from "../../components/common/CategoryList/CategorySelect.tsx";
import LazyLoad from "../../components/common/LazyLoad"; import LazyLoad from "../../components/common/LazyLoad";
import { FiltersCol, FiltersContainer } from "./IssueList-styles.tsx"; import { StatsData } from "../../components/StatsData.tsx";
import { SubtitleContainer, ThemeButton } from "./Home-styles"; import {
allCategories,
allCategoryData,
} from "../../constants/Categories/Categories.ts";
import { useFetchIssues } from "../../hooks/useFetchIssues.tsx";
import { import {
changefilterName, changefilterName,
changefilterSearch, changefilterSearch,
changeFilterType, changeFilterType,
setQappNames, setQappNames,
} from "../../state/features/fileSlice.ts"; } from "../../state/features/fileSlice.ts";
import { import { RootState } from "../../state/store";
allCategoryData, import { SubtitleContainer, ThemeButton } from "./Home-styles";
IssueState, import { FiltersCol, FiltersContainer } from "./IssueList-styles.tsx";
} from "../../constants/Categories/Categories.ts"; import { IssueList } from "./IssueList.tsx";
import {
CategoryList,
CategoryListRef,
getCategoriesFetchString,
} from "../../components/common/CategoryList/CategoryList.tsx";
import { StatsData } from "../../components/StatsData.tsx";
import {
CategorySelect,
CategorySelectRef,
} from "../../components/common/CategoryList/CategorySelect.tsx";
import {
AutocompleteQappNames,
getPublishedQappNames,
QappNamesRef,
} from "../../components/common/AutocompleteQappNames.tsx";
interface HomeProps { interface HomeProps {
mode?: string; mode?: string;
@ -109,11 +109,12 @@ export const Home = ({ mode }: HomeProps) => {
const selectedCategories = const selectedCategories =
categoryListRef.current?.getSelectedCategories() || []; categoryListRef.current?.getSelectedCategories() || [];
const issueType = categorySelectRef?.current?.getSelectedCategory(); const issueType = categorySelectRef?.current?.getSelectedCategory();
if (issueType) selectedCategories[2] = issueType; let categoriesString = getCategoriesFetchString(selectedCategories);
if (issueType) categoriesString = ":" + issueType + ";";
await getIssues( await getIssues(
{ {
name: filterName, name: filterName,
categories: getCategoriesFetchString(selectedCategories), categories: categoriesString,
QappName: autocompleteRef?.current?.getQappNameFetchString(), QappName: autocompleteRef?.current?.getQappNameFetchString(),
keywords: filterSearch, keywords: filterSearch,
type: filterType, type: filterType,
@ -168,32 +169,6 @@ export const Home = ({ mode }: HomeProps) => {
isFilterMode.current = false; isFilterMode.current = false;
} }
// const interval = useRef<any>(null);
// const checkNewVideosFunc = useCallback(() => {
// let isCalling = false;
// interval.current = setInterval(async () => {
// if (isCalling || !firstFetch.current) return;
// isCalling = true;
// await checkNewVideos();
// isCalling = false;
// }, 30000); // 1 second interval
// }, [checkNewVideos]);
// useEffect(() => {
// if (isFiltering && interval.current) {
// clearInterval(interval.current);
// return;
// }
// checkNewVideosFunc();
// return () => {
// if (interval?.current) {
// clearInterval(interval.current);
// }
// };
// }, [mode, checkNewVideosFunc, isFiltering]);
useEffect(() => { useEffect(() => {
if ( if (
!firstFetch.current && !firstFetch.current &&
@ -294,7 +269,7 @@ export const Home = ({ mode }: HomeProps) => {
)} )}
{showCategorySelect && ( {showCategorySelect && (
<CategorySelect <CategorySelect
categoryData={IssueState} categoryData={allCategories}
ref={categorySelectRef} ref={categorySelectRef}
sx={{ marginTop: "20px" }} sx={{ marginTop: "20px" }}
afterChange={value => { afterChange={value => {

View File

@ -1,4 +1,26 @@
import { Avatar, Box, Skeleton } from "@mui/material"; import BlockIcon from "@mui/icons-material/Block";
import EditIcon from "@mui/icons-material/Edit";
import { Avatar, Box, Skeleton, useTheme } from "@mui/material";
import React, { useMemo, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import QORTicon from "../../assets/icons/CoinIcons/qort.png";
import { BountyDisplay } from "../../components/common/BountyDisplay.tsx";
import { IssueIcon, IssueIcons } from "../../components/common/IssueIcon.tsx";
import {
getIconsFromObject,
getnamesFromObject,
} from "../../constants/Categories/CategoryFunctions.ts";
import { fontSizeExLarge } from "../../constants/Misc.ts";
import {
blockUser,
Issue,
setEditFile,
} from "../../state/features/fileSlice.ts";
import { RootState } from "../../state/store.ts";
import { BountyData } from "../../utils/qortalRequests.ts";
import { formatDate } from "../../utils/time.ts";
import { import {
BlockIconContainer, BlockIconContainer,
IconsBox, IconsBox,
@ -9,24 +31,6 @@ import {
VideoCardTitle, VideoCardTitle,
VideoUploadDate, VideoUploadDate,
} from "./IssueList-styles.tsx"; } from "./IssueList-styles.tsx";
import EditIcon from "@mui/icons-material/Edit";
import {
blockUser,
Issue,
setEditFile,
} from "../../state/features/fileSlice.ts";
import BlockIcon from "@mui/icons-material/Block";
import { formatBytes } from "../IssueContent/IssueContent.tsx";
import { formatDate } from "../../utils/time.ts";
import React, { useMemo, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../../state/store.ts";
import { useNavigate } from "react-router-dom";
import { getIconsFromObject } from "../../constants/Categories/CategoryFunctions.ts";
import { IssueIcon, IssueIcons } from "../../components/common/IssueIcon.tsx";
import QORTicon from "../../assets/icons/qort.png";
import { fontSizeMedium } from "../../constants/Misc.ts";
interface FileListProps { interface FileListProps {
issues: Issue[]; issues: Issue[];
@ -35,7 +39,7 @@ export const IssueList = ({ issues }: FileListProps) => {
const hashMapIssues = useSelector( const hashMapIssues = useSelector(
(state: RootState) => state.file.hashMapFiles (state: RootState) => state.file.hashMapFiles
); );
const theme = useTheme();
const [showIcons, setShowIcons] = useState(null); const [showIcons, setShowIcons] = useState(null);
const username = useSelector((state: RootState) => state.auth?.user?.name); const username = useSelector((state: RootState) => state.auth?.user?.name);
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -54,7 +58,9 @@ export const IssueList = ({ issues }: FileListProps) => {
if (response === true) { if (response === true) {
dispatch(blockUser(user)); dispatch(blockUser(user));
} }
} catch (error) {} } catch (error) {
console.log(error);
}
}; };
const filteredIssues = useMemo(() => { const filteredIssues = useMemo(() => {
@ -71,12 +77,11 @@ export const IssueList = ({ issues }: FileListProps) => {
issueObj = existingFile; issueObj = existingFile;
hasHash = true; hasHash = true;
} }
const bountyData: BountyData = {
...issueObj.bountyData,
...issue.bountyData,
};
const issueIcons = getIconsFromObject(issueObj);
const fileBytes = issueObj?.files.reduce(
(acc, cur) => acc + (cur?.size || 0),
0
);
return ( return (
<Box <Box
sx={{ sx={{
@ -142,24 +147,34 @@ export const IssueList = ({ issues }: FileListProps) => {
style={{ style={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
width: "200px", width: "280px",
}} }}
> >
<IssueIcons <IssueIcons
iconSources={issueIcons} issueData={issueObj}
style={{ marginRight: "20px" }} style={{ marginRight: "20px" }}
showBackupIcon={true} showBackupIcon={true}
/> />
</div> </div>
<VideoCardTitle <Box
sx={{ sx={{
width: "150px", display: "flex",
fontSize: fontSizeMedium, justifyContent: "left",
alignItems: "center",
width: "250px",
fontSize: fontSizeExLarge,
fontFamily: "Cairo",
letterSpacing: "0.4px",
color: theme.palette.text.primary,
userSelect: "none",
}} }}
> >
{fileBytes > 0 && formatBytes(fileBytes)} <BountyDisplay
</VideoCardTitle> bountyData={bountyData}
<VideoCardTitle sx={{ fontWeight: "bold", width: "500px" }}> divStyle={{ marginLeft: "20px" }}
/>
</Box>
<VideoCardTitle sx={{ fontWeight: "bold", width: "400px" }}>
{issueObj.title} {issueObj.title}
</VideoCardTitle> </VideoCardTitle>
</Box> </Box>

View File

@ -1,11 +1,21 @@
import React, { useEffect, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { RootState } from "../../state/store";
import { Avatar, Box, Skeleton, useTheme } from "@mui/material"; import { Avatar, Box, Skeleton, useTheme } from "@mui/material";
import { useFetchIssues } from "../../hooks/useFetchIssues.tsx"; import React, { useEffect, useRef, useState } from "react";
import { useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import QORTicon from "../../assets/icons/CoinIcons/qort.png";
import { BountyDisplay } from "../../components/common/BountyDisplay.tsx";
import { IssueIcon, IssueIcons } from "../../components/common/IssueIcon.tsx";
import LazyLoad from "../../components/common/LazyLoad"; import LazyLoad from "../../components/common/LazyLoad";
import { getIconsFromObject } from "../../constants/Categories/CategoryFunctions.ts";
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
import { fontSizeExLarge } from "../../constants/Misc.ts";
import { verifyAllPayments } from "../../constants/PublishFees/VerifyPayment.ts";
import { useFetchIssues } from "../../hooks/useFetchIssues.tsx";
import { Issue } from "../../state/features/fileSlice.ts";
import { RootState } from "../../state/store";
import { BountyData, getBountyAmounts } from "../../utils/qortalRequests.ts";
import { formatDate } from "../../utils/time";
import { queue } from "../../wrappers/GlobalWrapper";
import { import {
BottomParent, BottomParent,
IssueCard, IssueCard,
@ -15,15 +25,6 @@ import {
VideoCardTitle, VideoCardTitle,
VideoUploadDate, VideoUploadDate,
} from "./IssueList-styles.tsx"; } from "./IssueList-styles.tsx";
import { formatDate } from "../../utils/time";
import { Issue } from "../../state/features/fileSlice.ts";
import { queue } from "../../wrappers/GlobalWrapper";
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
import { formatBytes } from "../IssueContent/IssueContent.tsx";
import { getIconsFromObject } from "../../constants/Categories/CategoryFunctions.ts";
import { IssueIcon, IssueIcons } from "../../components/common/IssueIcon.tsx";
import QORTicon from "../../assets/icons/qort.png";
import { verifyAllPayments } from "../../constants/PublishFees/VerifyPayment.ts";
interface VideoListProps { interface VideoListProps {
mode?: string; mode?: string;
@ -97,9 +98,11 @@ export const IssueListComponentLevel = ({ mode }: VideoListProps) => {
const issueData = await Promise.all(verifiedIssuePromises); const issueData = await Promise.all(verifiedIssuePromises);
const verifiedIssues = await verifyAllPayments(issueData); const verifiedIssues = await verifyAllPayments(issueData);
setIssues(verifiedIssues); const bountyIssues = await getBountyAmounts(verifiedIssues);
console.log("bountyIssues: ", bountyIssues);
setIssues(bountyIssues);
} catch (error) { } catch (error) {
} finally { console.log(error);
} }
}, [issues, hashMapVideos]); }, [issues, hashMapVideos]);
@ -140,12 +143,10 @@ export const IssueListComponentLevel = ({ mode }: VideoListProps) => {
issueObj = existingFile; issueObj = existingFile;
hasHash = true; hasHash = true;
} }
const bountyData: BountyData = {
const issueIcons = getIconsFromObject(issueObj); ...issueObj.bountyData,
const fileBytes = issueObj?.files.reduce( ...issue.bountyData,
(acc, cur) => acc + (cur?.size || 0), };
0
);
return ( return (
<Box <Box
sx={{ sx={{
@ -183,22 +184,32 @@ export const IssueListComponentLevel = ({ mode }: VideoListProps) => {
style={{ style={{
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
width: "200px", width: "280px",
}} }}
> >
<IssueIcons <IssueIcons
iconSources={issueIcons} issueData={issueObj}
style={{ marginRight: "20px" }} style={{ marginRight: "20px" }}
showBackupIcon={true} showBackupIcon={true}
/> />
</div> </div>
<VideoCardTitle <Box
sx={{ sx={{
width: "100px", display: "flex",
alignItems: "center",
width: "250px",
fontSize: fontSizeExLarge,
fontFamily: "Cairo",
letterSpacing: "0.4px",
color: theme.palette.text.primary,
userSelect: "none",
}} }}
> >
{fileBytes > 0 && formatBytes(fileBytes)} <BountyDisplay
</VideoCardTitle> bountyData={bountyData}
divStyle={{ marginLeft: "20px" }}
/>
</Box>
<VideoCardTitle <VideoCardTitle
sx={{ fontWeight: "bold", width: "500px" }} sx={{ fontWeight: "bold", width: "500px" }}
> >
@ -213,13 +224,20 @@ export const IssueListComponentLevel = ({ mode }: VideoListProps) => {
)} )}
<BottomParent> <BottomParent>
<NameAndDateContainer <NameAndDateContainer
sx={{ width: "200px", height: "100%" }}
onClick={e => { onClick={e => {
e.stopPropagation(); e.stopPropagation();
navigate(`/channel/${issueObj?.user}`); navigate(`/channel/${issueObj?.user}`);
}} }}
>
<div
style={{
display: "flex",
width: "200px",
}}
> >
<Avatar <Avatar
sx={{ height: 24, width: 24 }} sx={{ height: 24, width: 24, marginRight: "10px" }}
src={`/arbitrary/THUMBNAIL/${issueObj?.user}/qortal_avatar`} src={`/arbitrary/THUMBNAIL/${issueObj?.user}/qortal_avatar`}
alt={`${issueObj?.user}'s avatar`} alt={`${issueObj?.user}'s avatar`}
/> />
@ -232,13 +250,14 @@ export const IssueListComponentLevel = ({ mode }: VideoListProps) => {
> >
{issueObj?.user} {issueObj?.user}
</VideoCardName> </VideoCardName>
</NameAndDateContainer> </div>
{issueObj?.created && ( {issueObj?.created && (
<VideoUploadDate> <VideoUploadDate>
{formatDate(issueObj.created)} {formatDate(issueObj.created)}
</VideoUploadDate> </VideoUploadDate>
)} )}
</NameAndDateContainer>
</BottomParent> </BottomParent>
</IssueCard> </IssueCard>
</> </>

View 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;
});
};

View File

@ -1,9 +1,9 @@
import { styled } from "@mui/system";
import { Box, Typography } from "@mui/material"; import { Box, Typography } from "@mui/material";
import { styled } from "@mui/system";
export const FilePlayerContainer = styled(Box)(({ theme }) => ({ export const FilePlayerContainer = styled(Box)(({ theme }) => ({
maxWidth: "95%", maxWidth: "95%",
width: "1000px", width: "90vw",
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
alignItems: "flex-start", alignItems: "flex-start",
@ -11,7 +11,6 @@ export const FilePlayerContainer = styled(Box)(({ theme }) => ({
export const FileTitle = styled(Typography)(({ theme }) => ({ export const FileTitle = styled(Typography)(({ theme }) => ({
fontFamily: "Raleway", fontFamily: "Raleway",
fontSize: "20px",
color: theme.palette.text.primary, color: theme.palette.text.primary,
userSelect: "none", userSelect: "none",
wordBreak: "break-word", wordBreak: "break-word",

View File

@ -1,11 +1,40 @@
import DownloadIcon from "@mui/icons-material/Download";
import { Avatar, Box, Typography, useTheme } from "@mui/material";
import React, { useEffect, useMemo, useRef, useState } from "react"; import React, { useEffect, useMemo, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { setIsLoadingGlobal } from "../../state/features/globalSlice"; import QORTicon from "../../assets/icons/CoinIcons/qort.png";
import { Avatar, Box, Typography, useTheme } from "@mui/material"; import { BountyDisplay } from "../../components/common/BountyDisplay.tsx";
import { RootState } from "../../state/store"; import {
Category,
getCategoriesFromObject,
} from "../../components/common/CategoryList/CategoryList.tsx";
import { CommentSection } from "../../components/common/Comments/CommentSection";
import { Donate } from "../../components/common/Donate/Donate.tsx";
import FileElement from "../../components/common/FileElement";
import { IssueIcon, IssueIcons } from "../../components/common/IssueIcon.tsx";
import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml";
import { allCategoryData } from "../../constants/Categories/Categories.ts";
import {
getIconsFromObject,
getnamesFromObject,
} from "../../constants/Categories/CategoryFunctions.ts";
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
import { fontSizeExLarge } from "../../constants/Misc.ts";
import {
appendIsPaidToFeeData,
verifyPayment,
} from "../../constants/PublishFees/VerifyPayment.ts";
import { addToHashMap } from "../../state/features/fileSlice.ts"; import { addToHashMap } from "../../state/features/fileSlice.ts";
import DownloadIcon from "@mui/icons-material/Download"; import { setIsLoadingGlobal } from "../../state/features/globalSlice";
import { RootState } from "../../state/store";
import { getAvatarFromName } from "../../utils/qortalRequests.ts";
import { formatDate } from "../../utils/time";
import {
categoryNamesToString,
getCategoryNames,
getIconsAndLabels,
} from "./IssueContent-functions.ts";
import { import {
AuthorTextComment, AuthorTextComment,
FileAttachmentContainer, FileAttachmentContainer,
@ -18,23 +47,6 @@ import {
StyledCardColComment, StyledCardColComment,
StyledCardHeaderComment, StyledCardHeaderComment,
} from "./IssueContent-styles.tsx"; } from "./IssueContent-styles.tsx";
import { formatDate } from "../../utils/time";
import { CommentSection } from "../../components/common/Comments/CommentSection";
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
import { DisplayHtml } from "../../components/common/TextEditor/DisplayHtml";
import FileElement from "../../components/common/FileElement";
import { allCategoryData } from "../../constants/Categories/Categories.ts";
import {
Category,
getCategoriesFromObject,
} from "../../components/common/CategoryList/CategoryList.tsx";
import { getIconsFromObject } from "../../constants/Categories/CategoryFunctions.ts";
import { IssueIcon, IssueIcons } from "../../components/common/IssueIcon.tsx";
import QORTicon from "../../assets/icons/qort.png";
import {
appendIsPaidToFeeData,
verifyPayment,
} from "../../constants/PublishFees/VerifyPayment.ts";
export function formatBytes(bytes: number, decimals = 2) { export function formatBytes(bytes: number, decimals = 2) {
if (bytes === 0) return "0 Bytes"; if (bytes === 0) return "0 Bytes";
@ -55,7 +67,6 @@ export const IssueContent = () => {
const [descriptionHeight, setDescriptionHeight] = useState<null | number>( const [descriptionHeight, setDescriptionHeight] = useState<null | number>(
null null
); );
const [issueIcons, setIssueIcons] = useState<string[]>([]);
const userAvatarHash = useSelector( const userAvatarHash = useSelector(
(state: RootState) => state.global.userAvatarHash (state: RootState) => state.global.userAvatarHash
); );
@ -99,7 +110,7 @@ export const IssueContent = () => {
}, [issueData]); }, [issueData]);
const dispatch = useDispatch(); const dispatch = useDispatch();
const getVideoData = React.useCallback(async (name: string, id: string) => { const getIssueData = React.useCallback(async (name: string, id: string) => {
try { try {
if (!name || !id) return; if (!name || !id) return;
dispatch(setIsLoadingGlobal(true)); dispatch(setIsLoadingGlobal(true));
@ -142,17 +153,16 @@ export const IssueContent = () => {
}; };
verifyPayment(combinedData).then(feeData => { verifyPayment(combinedData).then(feeData => {
console.log( const dataWithFees = appendIsPaidToFeeData(combinedData, feeData);
"async data: ", console.log("dataWithFees: ", dataWithFees);
appendIsPaidToFeeData(combinedData, feeData) setIssueData(dataWithFees);
);
setIssueData(appendIsPaidToFeeData(combinedData, feeData));
dispatch(addToHashMap(combinedData)); dispatch(addToHashMap(combinedData));
checkforPlaylist(name, id, combinedData?.code); checkforPlaylist(name, id, combinedData?.code);
}); });
} }
} }
} catch (error) { } catch (error) {
console.log(error);
} finally { } finally {
dispatch(setIsLoadingGlobal(false)); dispatch(setIsLoadingGlobal(false));
} }
@ -212,7 +222,7 @@ export const IssueContent = () => {
const responseDataSearchVid = await response.json(); const responseDataSearchVid = await response.json();
if (responseDataSearchVid?.length > 0) { if (responseDataSearchVid?.length > 0) {
let resourceData2 = responseDataSearchVid[0]; const resourceData2 = responseDataSearchVid[0];
videos.push(resourceData2); videos.push(resourceData2);
} }
} }
@ -221,10 +231,12 @@ export const IssueContent = () => {
setPlaylistData(combinedData); setPlaylistData(combinedData);
} }
} }
} catch (error) {} } catch (error) {
console.log(error);
}
}, []); }, []);
React.useEffect(() => { useEffect(() => {
if (name && id) { if (name && id) {
const existingVideo = hashMapVideos[id]; const existingVideo = hashMapVideos[id];
@ -234,42 +246,11 @@ export const IssueContent = () => {
checkforPlaylist(name, id, existingVideo?.code); checkforPlaylist(name, id, existingVideo?.code);
}); });
} else { } else {
getVideoData(name, id); getIssueData(name, id);
} }
} }
}, [id, name]); }, [id, name]);
// const getAvatar = React.useCallback(async (author: string) => {
// try {
// let url = await qortalRequest({
// action: 'GET_QDN_RESOURCE_URL',
// name: author,
// service: 'THUMBNAIL',
// identifier: 'qortal_avatar'
// })
// setAvatarUrl(url)
// dispatch(setUserAvatarHash({
// name: author,
// url
// }))
// } catch (error) { }
// }, [])
// React.useEffect(() => {
// if (name && !avatarUrl) {
// const existingAvatar = userAvatarHash[name]
// if (existingAvatar) {
// setAvatarUrl(existingAvatar)
// } else {
// getAvatar(name)
// }
// }
// }, [name, userAvatarHash])
useEffect(() => { useEffect(() => {
if (contentRef.current) { if (contentRef.current) {
const height = contentRef.current.offsetHeight; const height = contentRef.current.offsetHeight;
@ -279,49 +260,14 @@ export const IssueContent = () => {
setDescriptionHeight(maxDescriptionHeight); setDescriptionHeight(maxDescriptionHeight);
} }
} }
if (issueData) {
const icons = getIconsFromObject(issueData);
setIssueIcons(icons);
}
}, [issueData]); }, [issueData]);
const categoriesDisplay = useMemo(() => { const categoriesDisplay = useMemo(() => {
if (issueData) { if (issueData) {
const categoryList = getCategoriesFromObject(issueData); const categoryNames = getCategoryNames(issueData);
const categoryNames = categoryList.map((categoryID, index) => { return categoryNamesToString(categoryNames, issueData?.QappName);
let categoryName: Category;
if (index === 0) {
categoryName = allCategoryData.category.find(
item => item?.id === +categoryList[0]
);
} else {
const subCategories = allCategoryData.subCategories[index - 1];
const selectedSubCategory = subCategories[categoryList[index - 1]];
if (selectedSubCategory) {
categoryName = selectedSubCategory.find(
item => item?.id === +categoryList[index]
);
} }
} return "No Issue Data";
return categoryName?.name;
});
const filteredCategoryNames = categoryNames.filter(name => name);
let categoryDisplay = "";
const separator = " > ";
const QappName = issueData?.QappName || "";
filteredCategoryNames.map((name, index) => {
if (QappName && index === 1) {
categoryDisplay += QappName + separator;
}
categoryDisplay += name;
if (index !== filteredCategoryNames.length - 1)
categoryDisplay += separator;
});
return categoryDisplay;
}
return "no videodata";
}, [issueData]); }, [issueData]);
return ( return (
@ -347,10 +293,12 @@ export const IssueContent = () => {
}} }}
> >
<div style={{ display: "flex", alignItems: "center" }}> <div style={{ display: "flex", alignItems: "center" }}>
{issueData && (
<IssueIcons <IssueIcons
iconSources={issueIcons} issueData={issueData}
style={{ marginRight: "20px" }} style={{ marginRight: "20px" }}
/> />
)}
</div> </div>
<FileTitle <FileTitle
variant="h1" variant="h1"
@ -367,9 +315,9 @@ export const IssueContent = () => {
</div> </div>
{issueData?.created && ( {issueData?.created && (
<Typography <Typography
variant="h6" variant="h4"
sx={{ sx={{
fontSize: "12px", fontSize: "18px",
}} }}
color={theme.palette.text.primary} color={theme.palette.text.primary}
> >
@ -413,11 +361,33 @@ export const IssueContent = () => {
</StyledCardHeaderComment> </StyledCardHeaderComment>
</Box> </Box>
<Spacer height="15px" /> <Spacer height="15px" />
<Box sx={{ display: "flex", direction: "row", alignItems: "center" }}>
{issueData?.bountyData && (
<div style={{ fontWeight: "bold" }}>
<BountyDisplay
bountyData={issueData?.bountyData}
timeDisplay={"BOTH"}
fontStyle={{ fontSize: fontSizeExLarge, fontWeight: "bold" }}
/>
{(issueData?.bountyData?.sourceCodeLink ||
issueData?.bountyData?.crowdfundLink) && (
<>
<div>Relevant Links:</div>
<div style={{ fontWeight: "bold" }}>
<div>{issueData?.bountyData?.crowdfundLink}</div>
<div>{issueData?.bountyData?.sourceCodeLink}</div>
</div>
</>
)}
<Donate crowdfundLink={issueData?.bountyData?.crowdfundLink} />
</div>
)}
</Box>
<Spacer height="15px" />
<Box> <Box>
<Typography <Typography
sx={{ sx={{
fontWeight: "bold", fontWeight: "bold",
fontSize: "16px",
userSelect: "none", userSelect: "none",
}} }}
> >

View File

@ -1,5 +1,6 @@
import { createSlice } from "@reduxjs/toolkit"; import { createSlice } from "@reduxjs/toolkit";
import { PublishFeeData } from "../../constants/PublishFees/SendFeeFunctions.ts"; import { PublishFeeData } from "../../constants/PublishFees/SendFeeFunctions.ts";
import { BountyData } from "../../utils/qortalRequests.ts";
interface GlobalState { interface GlobalState {
files: Issue[]; files: Issue[];
@ -47,6 +48,7 @@ export interface Issue {
isValid?: boolean; isValid?: boolean;
code?: string; code?: string;
feeData?: PublishFeeData; feeData?: PublishFeeData;
bountyData?: BountyData;
paymentVerified?: boolean; paymentVerified?: boolean;
} }

View File

@ -1,8 +1,8 @@
import { configureStore } from "@reduxjs/toolkit"; import { configureStore } from "@reduxjs/toolkit";
import notificationsReducer from "./features/notificationsSlice"; import authReducer from "./features/authSlice.js";
import authReducer from "./features/authSlice";
import globalReducer from "./features/globalSlice";
import fileReducer from "./features/fileSlice.ts"; import fileReducer from "./features/fileSlice.ts";
import globalReducer from "./features/globalSlice.js";
import notificationsReducer from "./features/notificationsSlice.js";
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {

View 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;

View File

@ -1,5 +1,12 @@
import moment from "moment";
import { CoinType } from "../constants/PublishFees/FeePricePublish/FeePricePublish.ts";
import { NameData } from "../constants/PublishFees/SendFeeFunctions.ts"; import { NameData } from "../constants/PublishFees/SendFeeFunctions.ts";
import { getUserAccountNames } from "../constants/PublishFees/VerifyPayment-Functions.ts"; import {
getUserAccount,
getUserAccountNames,
} from "../constants/PublishFees/VerifyPayment-Functions.ts";
import { Issue } from "../state/features/fileSlice.ts";
import { isNumber } from "./utilFunctions.ts";
export const getNameData = async (name: string) => { export const getNameData = async (name: string) => {
return (await qortalRequest({ return (await qortalRequest({
@ -31,3 +38,216 @@ export const sendQchatDM = async (
return false; return false;
} }
}; };
export interface BountyData {
amount: number;
coinType: CoinType;
crowdfundLink?: string;
sourceCodeLink?: string;
}
export const getATAmount = async crowdfundLink => {
const crowdfund = await getCrowdfund(crowdfundLink);
const atAddress = crowdfund?.deployedAT?.aTAddress;
if (!atAddress) return 0;
try {
const res = await qortalRequest({
action: "SEARCH_TRANSACTIONS",
txType: ["PAYMENT"],
confirmationStatus: "CONFIRMED",
address: atAddress,
limit: 0,
reverse: true,
});
if (res?.length > 0) {
const totalAmount: number = res.reduce(
(total: number, transaction) => total + parseFloat(transaction.amount),
0
);
return totalAmount;
}
} catch (e) {
console.log(e);
return 0;
}
};
export const getCrowdfund = async (crowdfundLink: string) => {
const splitLink = crowdfundLink.split("/");
const name = splitLink[5];
const identifier = splitLink[6];
console.log("fetching crowdfund");
return await qortalRequest({
action: "FETCH_QDN_RESOURCE",
service: "DOCUMENT",
name,
identifier,
});
};
export const getATInfo = async (atAddress: string) => {
try {
const url = `/at/${atAddress}`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (response.status === 200) {
return await response.json();
}
} catch (error) {
console.log(error);
return undefined;
}
};
export const getNodeInfo = async () => {
try {
const url = `/blocks/height`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const responseDataSearch = await response.json();
return { height: responseDataSearch };
} catch (error) {
console.log(error);
}
};
export const getCrowdfundEndDate = async (crowdfundLink: string) => {
const { deployedAT } = await getCrowdfund(crowdfundLink);
const ATinfo = await getATInfo(deployedAT.aTAddress);
const nodeInfo = await getNodeInfo();
if (!ATinfo?.sleepUntilHeight || !nodeInfo?.height) return null;
const blocksRemaining = +ATinfo?.sleepUntilHeight - +nodeInfo.height;
return moment().add(blocksRemaining, "minutes");
};
export type DayTime = { days: number; hours: number; minutes: number };
export const getDurationFromBlocks = (blocks: number, blockCount: number) => {
const minutesPerDay = 60 * 24;
const blocksPerMinute = blockCount / minutesPerDay;
const duration = blocks / blocksPerMinute;
const days = Math.floor(duration / minutesPerDay);
const hours = Math.floor((duration % minutesPerDay) / 60);
const minutes = Math.floor(duration % 60);
return { days, hours, minutes } as DayTime;
};
export const getATaddress = async (crowdfundLink: string) => {
const { deployedAT } = await getCrowdfund(crowdfundLink);
return deployedAT?.aTAddress;
};
export const getHasQFundEnded = async (atAddress: string) => {
const url = `/at/${atAddress}`;
const response = await fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
if (response.status === 200) {
const responseDataSearch = await response.json();
return !!(
Object.keys(responseDataSearch).length > 0 &&
responseDataSearch?.isFinished
);
}
};
export interface SummaryTransactionCounts {
arbitrary: number;
AT: number;
deployAt: number;
groupInvite: number;
joinGroup: number;
message: number;
payment: number;
registerName: number;
rewardShare: number;
updateName: number;
voteOnPoll: number;
}
export interface DaySummaryResponse {
assetsIssued: number;
blockCount: number;
namesRegistered: number;
totalTransactionCount: number;
transactionCountByType: SummaryTransactionCounts;
}
export const getDaySummary = async () => {
return (await qortalRequest({
action: "GET_DAY_SUMMARY",
})) as DaySummaryResponse;
};
export const validateBountyInput = async (input: string) => {
if (isNumber(input)) return true;
try {
const crowdfund = await getCrowdfund(input);
const ATaddress = crowdfund.aTAddress;
return ATaddress !== "";
} catch (e) {
console.log(e);
return false;
}
};
export const appendBountyAmount = (issue: Issue, bountyAmount: number) => {
return {
...issue,
bountyData: {
amount: bountyAmount || undefined,
coinType: issue?.bountyData?.coinType,
},
};
};
export const getBountyAmounts = async (issues: Issue[]) => {
const issuePromises = issues.map(issue => {
if (!issue?.bountyData?.crowdfundLink) {
const numberAsPromise = async (num: number) => num;
return numberAsPromise(Number(issue?.bountyData?.amount) || undefined);
}
return getATAmount(issue?.bountyData?.crowdfundLink);
});
const bountyAmounts = await Promise.all(issuePromises);
return issues.map((issue, index) => {
return appendBountyAmount(issue, bountyAmounts[index]);
});
};
export const getBalance = async (address: string) => {
return (await qortalRequest({
action: "GET_BALANCE",
address,
})) as number;
};
export const getUserBalance = async () => {
const accountInfo = await getUserAccount();
return (await getBalance(accountInfo.address)) as number;
};
export const getAvatarFromName = async (name: string) => {
return await qortalRequest({
action: "GET_QDN_RESOURCE_URL",
name,
service: "THUMBNAIL",
identifier: "qortal_avatar",
});
};

View File

@ -18,3 +18,13 @@ export const printVar = (variable: object) => {
const [key, value] = Object.entries(variable)[0]; const [key, value] = Object.entries(variable)[0];
console.log(key, " is: ", value); console.log(key, " is: ", value);
}; };
export const isNumber = (input: string) => {
if (input === "") return false;
const num = Number(input);
return !isNaN(num);
};
export const truncateNumber = (value: string | number, sigDigits: number) => {
return Number(value).toFixed(sigDigits);
};

View File

@ -5,9 +5,9 @@
"module": "ESNext", "module": "ESNext",
"skipLibCheck": true, "skipLibCheck": true,
"noImplicitAny": false, "noImplicitAny": false,
"allowSyntheticDefaultImports": true,
/* Bundler mode */ /* Bundler mode */
"moduleResolution": "bundler", "moduleResolution": "node",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,