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

Merge pull request #12 from QortalSeth/main

FeeHistoryModal is now editable by app owner
This commit is contained in:
Qortal Dev 2024-07-26 15:48:23 -06:00 committed by GitHub
commit 69aab3f604
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 557 additions and 158 deletions

View File

@ -15,10 +15,7 @@ import ShortUniqueId from "short-unique-id";
import { allCategoryData } from "../../constants/Categories/Categories.ts";
import { QSUPPORT_FILE_BASE } from "../../constants/Identifiers.ts";
import { log, titleFormatter } from "../../constants/Misc.ts";
import {
feeAmountBase,
supportedCoins,
} from "../../constants/PublishFees/FeeData.tsx";
import { 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";
@ -332,7 +329,7 @@ export const EditIssue = () => {
bountyData,
};
if (payFee) {
const publishFeeResponse = await payPublishFeeQORT(feeAmountBase);
const publishFeeResponse = await payPublishFeeQORT("default", "QORT");
if (!publishFeeResponse) {
dispatch(
setNotification({
@ -365,7 +362,7 @@ export const EditIssue = () => {
action: "PUBLISH_QDN_RESOURCE",
name: name,
service: "DOCUMENT",
file: objectToFile(issueObject),
data64: await objectToBase64(issueObject),
title: title.slice(0, 50),
description: metadescription,
identifier: editIssueProperties.id,

View File

@ -326,7 +326,7 @@ export const EditPlaylist = () => {
action: "PUBLISH_QDN_RESOURCE",
name: username,
service: "PLAYLIST",
file: objectToFile(playlistObject),
data64: await objectToBase64(playlistObject),
title: title.slice(0, 50),
description: metadescription,
identifier: identifier,

View File

@ -21,8 +21,9 @@ import {
titleFormatter,
} from "../../constants/Misc.ts";
import {
feeAmountBase,
feeDisclaimer,
appName,
feeDataDefault,
feePriceToString,
supportedCoins,
} from "../../constants/PublishFees/FeeData.tsx";
import { CoinType } from "../../constants/PublishFees/FeePricePublish/FeePricePublish.ts";
@ -36,6 +37,7 @@ import { setNotification } from "../../state/features/notificationsSlice";
import { RootState } from "../../state/store";
import { BountyData, validateBountyInput } from "../../utils/qortalRequests.ts";
import { objectToBase64, objectToFile } from "../../utils/PublishFormatter.ts";
import { StateCheckBox } from "../../utils/StateCheckBox.tsx";
import { isNumber } from "../../utils/utilFunctions.ts";
import {
AutocompleteQappNames,
@ -116,7 +118,7 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
const [sourceCode, setSourceCode] = useState<string>("");
const [coin, setCoin] = useState<CoinType>("QORT");
const [showCoins, setShowCoins] = useState<boolean>(false);
const [payFee, setPayFee] = useState<boolean>(true);
const categoryListRef = useRef<CategoryListRef>(null);
const imagePublisherRef = useRef<ImagePublisherRef>(null);
const autocompleteRef = useRef<QappNamesRef>(null);
@ -283,12 +285,14 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
const selectedQappName = autocompleteRef?.current?.getSelectedValue();
const publishFeeResponse = await payPublishFeeQORT(feeAmountBase);
if (log) console.log("feeResponse: ", publishFeeResponse);
let publishFeeResponse = undefined;
const feeData: PublishFeeData = {
if (payFee) {
publishFeeResponse = await payPublishFeeQORT("default", "QORT");
if (log) console.log("feeResponse: ", publishFeeResponse);
}
const feeData = {
signature: publishFeeResponse,
senderName: "",
};
const isBountyNumber = isNumber(bounty);
@ -311,7 +315,6 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
feeData,
bountyData,
};
const QappNameString = autocompleteRef?.current?.getQappNameFetchString();
const categoryString =
categoryListRef.current?.getCategoriesFetchString(categoryList);
@ -330,13 +333,15 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
action: "PUBLISH_QDN_RESOURCE",
name: name,
service: "DOCUMENT",
file: objectToFile(issueObject),
data64: await objectToBase64(issueObject),
title: title.slice(0, 50),
description: metadescription,
identifier: identifier + "_metadata",
tag1: QSUPPORT_FILE_BASE,
filename: `video_metadata.json`,
};
console.log("issue object: ", issueObject);
console.log("request body: ", requestBodyJson);
listOfPublishes.push(requestBodyJson);
const multiplePublish = {
@ -559,6 +564,11 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
</>
</>
)}
<StateCheckBox
label={`I agree to pay a fee of ${feePriceToString(feeDataDefault)} for further development of ${appName}`}
defaultChecked
onChange={b => setPayFee(b)}
/>
<ActionButtonRow>
<ActionButton
onClick={() => {
@ -593,7 +603,6 @@ export const PublishIssue = ({ editId, editContent }: NewCrowdfundProps) => {
</ThemeButtonBright>
</Box>
</ActionButtonRow>
{feeDisclaimer}
</ModalBody>
</Modal>

View File

@ -161,7 +161,7 @@ export const CommentEditor = ({
action: "PUBLISH_QDN_RESOURCE",
name: name,
service: "BLOG_COMMENT",
file: stringToFile(value),
data64: utf8ToBase64(value),
identifier: identifier,
});

View File

@ -11,3 +11,5 @@ export const QSUPPORT_PLAYLIST_BASE = useTestIdentifiers
export const QSUPPORT_COMMENT_BASE = useTestIdentifiers
? "qcomment_v1_MYTEST_support_"
: "qcomment_v1_q_support_";
// have fee payment as checkbox

View File

@ -1,14 +1,23 @@
import { CheckBox } from "@mui/icons-material";
import { Box } from "@mui/material";
import React from "react";
import { StateCheckBox } from "../../utils/StateCheckBox.tsx";
import { useTestIdentifiers } from "../Identifiers.ts";
import {
FeePrice,
fetchCurrentPriceData,
} from "./FeePricePublish/FeePricePublish.ts";
export const appName = "Q-Support";
export const feeDestinationName = "Q-Support";
export const feeAmountBase = useTestIdentifiers ? 0.000001 : 0.25;
export const FEE_BASE = useTestIdentifiers
? "MYTEST_support_fees"
: "q_support_fees";
export const feeDataDefault = await fetchCurrentPriceData("default", "QORT");
export const feePriceToString = (feePrice: FeePrice) => {
return `${feePrice?.feeAmount} ${feePrice?.coinType}`;
};
export const supportedCoins = [
"QORT",
@ -24,17 +33,26 @@ export const supportedCoins = [
export const maxFeePublishTimeDiff = 10; // time in minutes before/after publish when fee is considered valid
export type FeeType = "default" | "comment" | "like" | "dislike" | "superlike";
export const feeDisclaimerString = `When Publishing (but not editing) Issues ${feeAmountBase} \n
QORT is requested to fund continued development of Q-Support.`;
export const feeDisclaimer = (
<Box
sx={{
fontSize: "28px",
color: "#f44336",
fontWeight: 600,
}}
>
{feeDisclaimerString}
</Box>
// use wherever the fee is communicated to the user
const feeCheckBox = (
<StateCheckBox
label={`I agree to pay a fee of ${feePriceToString(feeDataDefault)} to support further development of ${appName}`}
defaultChecked
/>
);
// // Test senderName removal on Publish
// export const feeDisclaimerString = `When Publishing (but not editing) Issues ${feeAmountBase} \n
// QORT is requested to fund continued development of Q-Support.`;
//
// export const feeDisclaimer = (
// <Box
// sx={{
// fontSize: "28px",
// color: "#f44336",
// fontWeight: 600,
// }}
// >
// {feeDisclaimerString}
// </Box>
// );

View File

@ -0,0 +1,259 @@
import {
Box,
Button,
MenuItem,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField,
} from "@mui/material";
import { key } from "localforage";
import React, { useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { s } from "vite/dist/node/types.d-aGj9QkWt";
import { ThemeButton } from "../../../pages/Home/Home-styles.tsx";
import { setNotification } from "../../../state/features/notificationsSlice.ts";
import BoundedNumericTextField from "../../../utils/BoundedNumericTextField.tsx";
import {
objectToBase64,
objectToFile,
} from "../../../utils/PublishFormatter.ts";
import StateTextField from "../../../utils/StateTextField.tsx";
import { objectIndexToKey } from "../../../utils/utilFunctions.ts";
import { appName, FEE_BASE, supportedCoins } from "../FeeData.tsx";
import { DataTableProps } from "./DataTable.tsx";
import { CoinType, FeePrice, feesPublishService } from "./FeePricePublish.ts";
export const DataEditor = ({ columnNames, data, sx }: DataTableProps) => {
const [editedData, setEditedData] = useState<FeePrice[]>(data);
const addRow = () => {
setEditedData(editedData => [
...editedData,
{
time: undefined,
feeAmount: undefined,
feeType: undefined,
coinType: undefined,
},
]);
};
const removeRow = () => {
setEditedData(editedData => editedData.slice(0, -1));
};
const dispatch = useDispatch();
const updateData = (
value: string | number,
rowIndex: number,
cellIndex: number
) => {
const rowData = editedData[rowIndex];
const key = objectIndexToKey(rowData, cellIndex);
const newData: FeePrice[] = editedData.map((row, rIndex) => {
return rIndex === rowIndex
? { ...row, [key]: value || undefined }
: { ...row };
});
setEditedData(newData);
};
useEffect(() => {
console.log("editedData is: ", editedData);
}, [editedData]);
const getCellForm = (rowIndex: number, cellIndex: number) => {
const rowData = editedData[rowIndex];
const key = objectIndexToKey(rowData, cellIndex);
const value = rowData[key]?.toString();
const feeAmount = (
<BoundedNumericTextField
variant={"standard"}
value={value}
initialValue={value || ""}
addIconButtons={false}
sx={{ width: "60%" }}
onBlur={s => {
updateData(+s, rowIndex, cellIndex);
}}
/>
);
const feeType = (
<StateTextField
variant={"standard"}
value={value}
initialValue={value || ""}
sx={{ width: "55%" }}
onBlur={e => {
updateData(e.target.value, rowIndex, cellIndex);
}}
/>
);
const coinTypeAC = (
<StateTextField
variant={"outlined"}
select
fullWidth
value={value}
initialValue={value || undefined}
onBlur={e =>
updateData(e.target.value as CoinType, rowIndex, cellIndex)
}
sx={{
width: "100%",
}}
options={supportedCoins}
></StateTextField>
);
switch (cellIndex) {
case 0:
return value ? new Date(+value).toDateString() : "";
case 1:
return feeAmount;
case 2:
return feeType;
case 3:
return coinTypeAC;
}
};
const isValidPublish = (publishData: FeePrice[]) => {
return publishData.every(value => {
return Object.values(value).every(value => !!value);
});
};
const publish = async (feeData: FeePrice[]) => {
qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: appName,
identifier: FEE_BASE,
service: feesPublishService,
data64: await objectToBase64(feeData),
}).then(response => {
dispatch(
setNotification({
msg: "Issue published",
alertType: "success",
})
);
setEditedData(feeData);
});
};
const publishFeeData = () => {
const dataLength = editedData.length - 1;
const dateEmpty = !editedData[dataLength]?.time;
const dataWithDate: FeePrice[] = editedData.map((row, rIndex) => {
return rIndex === dataLength && dateEmpty
? { ...row, time: Date.now() }
: { ...row };
});
if (isValidPublish(dataWithDate)) publish(dataWithDate);
else {
const notificationObj = {
msg: "Publish Fee Data is not Valid",
alertType: "error",
};
dispatch(setNotification(notificationObj));
}
};
const boldSX = {
fontSize: "30px",
textAlign: "center",
fontWeight: "bold",
};
const cellSX = {
fontSize: "25px",
fontWeight: "normal",
textAlign: "center",
};
const AddRemoveRowButtonSX = {
color: "text.primary",
fontSize: "20px",
fontWeight: "bold",
width: "30%",
};
return (
<>
<TableContainer sx={{ width: "100%", ...sx }}>
<Table align="center" stickyHeader>
<TableHead>
<TableRow>
{columnNames.map((columnName, index) => (
<TableCell sx={boldSX} key={columnName + index}>
{columnName}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{editedData.map((tableRow, rowIndex) => {
return (
<TableRow key={tableRow.toString() + rowIndex}>
{<TableCell sx={boldSX}>{rowIndex + 1}</TableCell>}
{Object.values(tableRow).map((tableCell, cellIndex) => (
<TableCell sx={cellSX} key={rowIndex + cellIndex}>
{getCellForm(rowIndex, cellIndex)}
</TableCell>
))}
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
<Box
sx={{
display: "flex",
flexDirection: "row",
width: "100%",
justifyContent: "center",
gap: "5%",
}}
>
<Button
sx={AddRemoveRowButtonSX}
variant={"contained"}
color={"success"}
onClick={addRow}
>
Add Row
</Button>
<Button
sx={AddRemoveRowButtonSX}
variant={"contained"}
color={"error"}
onClick={removeRow}
>
Remove Row
</Button>
</Box>
<ThemeButton
sx={{
fontSize: "20px",
fontWeight: "bold",
}}
onClick={publishFeeData}
>
Publish
</ThemeButton>
</>
);
};

View File

@ -8,46 +8,51 @@ import {
} from "@mui/material";
import React from "react";
import { SxProps } from "@mui/material/styles";
import { FeePrice } from "./FeePricePublish.ts";
export interface DataTableProps {
columnNames: string[];
data: string[][];
data: FeePrice[];
sx?: SxProps;
}
export const DataTable = ({ columnNames, data, sx }: DataTableProps) => {
const boldSX = {
fontSize: "30px",
textAlign: "center",
fontWeight: "bold",
};
const cellSX = {
fontSize: "25px",
fontWeight: "normal",
textAlign: "center",
};
const formatCell = (s: string, index: number) => {
if (index === 0) return new Date(s).toDateString();
else return s;
};
return (
<TableContainer sx={{ ...sx }}>
<Table align="center" stickyHeader>
<TableHead>
<TableRow>
{columnNames.map((columnName, index) => (
<TableCell
sx={{
fontSize: "30px",
textAlign: "center",
fontWeight: "bold",
}}
key={columnName + index}
>
<TableCell sx={boldSX} key={columnName + index}>
{columnName}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{data.map((tableRow, index) => {
{data.map((tableRow, rowIndex) => {
return (
<TableRow key={tableRow.toString() + index}>
{tableRow.map((tableCell, index) => (
<TableCell
sx={{
fontSize: index === 0 ? "30px" : "25px",
fontWeight: index === 0 ? "bold" : "normal",
textAlign: "center",
}}
key={tableCell + index}
>
{tableCell}
<TableRow key={tableRow.toString() + rowIndex}>
{<TableCell sx={boldSX}>{rowIndex + 1}</TableCell>}
{Object.values(tableRow).map((tableCell, cellIndex) => (
<TableCell sx={cellSX} key={tableCell + cellIndex}>
{formatCell(tableCell, cellIndex)}
</TableCell>
))}
</TableRow>

View File

@ -4,21 +4,19 @@ import { appName } from "../FeeData.tsx";
import { ModalBody } from "./FeePricePublish-styles.tsx";
import { useEffect, useState } from "react";
import { userHasName } from "../VerifyPayment-Functions.ts";
import { FeeHistoryTable } from "./FeeHistoryTable.tsx";
import { FeeHistoryModalBody } from "./FeeHistoryModalBody.tsx";
export const FeeHistoryModal = () => {
const [open, setOpen] = useState<boolean>(false);
const [userOwnsApp, setUserOwnsApp] = useState<boolean>(false);
const theme = useTheme();
useEffect(() => {
userHasName(appName).then(userHasName => setUserOwnsApp(userHasName));
}, []);
const buttonSX = {
fontSize: "20px",
color: theme.palette.secondary.main,
fontWeight: "bold",
};
if (theme.palette.mode === "light")
buttonSX["&:hover"] = { backgroundColor: theme.palette.primary.dark };
@ -37,7 +35,7 @@ export const FeeHistoryModal = () => {
onClose={() => setOpen(false)}
>
<ModalBody sx={{ width: "75vw", maxWidth: "75vw" }}>
<FeeHistoryTable />
<FeeHistoryModalBody />
<Button sx={buttonSX} onClick={() => setOpen(false)}>
Close
</Button>

View File

@ -0,0 +1,38 @@
import { appName } from "../FeeData.tsx";
import { userHasName } from "../VerifyPayment-Functions.ts";
import { DataEditor } from "./DataEditor.tsx";
import { DataTable } from "./DataTable.tsx";
import { FeePrice, fetchFees } from "./FeePricePublish.ts";
import React, { useEffect, useState } from "react";
export interface FeeHistoryModalBodyProps {
showFeeType?: boolean;
showCoinType?: boolean;
filterData?: () => string[][];
}
export const FeeHistoryModalBody = ({
showFeeType = true,
showCoinType = true,
}: FeeHistoryModalBodyProps) => {
const [feeData, setFeeData] = useState<FeePrice[]>([]);
const [userOwnsApp, setUserOwnsApp] = useState<boolean>(false);
const fetchFeesOnStartup = () => {
fetchFees().then(feeResponse => {
setFeeData(feeResponse);
});
userHasName(appName).then(userHasName => setUserOwnsApp(userHasName));
};
useEffect(fetchFeesOnStartup, []);
const columnNames = ["ID", "Date", "Fee Amount"];
if (showFeeType) columnNames.push("Fee Type");
if (showCoinType) columnNames.push("Coin Type");
return userOwnsApp ? (
<DataEditor columnNames={columnNames} data={feeData} />
) : (
<DataTable columnNames={columnNames} data={feeData} />
);
};

View File

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

View File

@ -1,11 +1,7 @@
import { setFeeData } from "../../../state/features/globalSlice.ts";
import { store } from "../../../state/store.js";
import {
objectToBase64,
objectToFile,
} from "../../../utils/PublishFormatter.ts";
import { useTestIdentifiers } from "../../Identifiers.ts";
import { appName, FEE_BASE, feeAmountBase, FeeType } from "../FeeData.tsx";
import { objectToFile } from "../../../utils/PublishFormatter.ts";
import { appName, FEE_BASE, FeeType } from "../FeeData.tsx";
export type CoinType = "QORT" | "BTC" | "LTC" | "DOGE" | "DGB" | "RVN" | "ARRR";
@ -16,7 +12,7 @@ export interface FeePrice {
coinType: CoinType;
}
const feesPublishService = "DOCUMENT";
export const feesPublishService = "DOCUMENT";
export const fetchFees = async () => {
const feeData = store.getState().global.feeData;
@ -48,45 +44,39 @@ export const fetchFeesRedux = () => {
fetchFees().then(feeData => store.dispatch(setFeeData(feeData)));
};
export const addFeePrice = async (
feeAmount = feeAmountBase,
export const fetchCurrentPriceData = async (
feeType: FeeType = "default",
coinType: CoinType = "QORT"
) => {
const fees = await fetchFees();
if (fees?.length === 0 || !fees) return undefined;
fees.push({
time: Date.now(),
feeAmount,
feeType,
coinType,
});
const filteredFees = fees.filter(
price => price.feeType === feeType && price.coinType === coinType
);
console.log("fees are: ", fees);
await qortalRequest({
action: "PUBLISH_QDN_RESOURCE",
name: appName,
identifier: FEE_BASE,
service: feesPublishService,
file: objectToFile(fees),
});
return filteredFees.at(-1) as FeePrice;
};
const feeFilter = (fee: FeePrice, feeToVerify: FeePrice) => {
const nameCheck = fee.feeType === feeToVerify.feeType;
const coinTypeCheck = fee.coinType === feeToVerify.coinType;
const timeCheck = feeToVerify.time <= feeToVerify.time;
const timeCheck = fee.time <= feeToVerify.time;
const filter = nameCheck && coinTypeCheck && timeCheck;
return nameCheck && coinTypeCheck && timeCheck;
return filter;
};
export const verifyFeeAmount = async (feeToVerify: FeePrice) => {
if (useTestIdentifiers) return true;
const fees = await fetchFees();
const filteredFees = fees.filter(fee => feeFilter(fee, feeToVerify));
if (filteredFees.length === 0) return false;
if (filteredFees.length === 0) {
console.log("no filtered fees");
return false;
}
// gets fee that applies at the time of feeToVerify
const feeToCheck = filteredFees.at(-1);
const isFeeAmountValid = feeToVerify.feeAmount >= feeToCheck.feeAmount;
const feeToCheck = filteredFees[filteredFees.length - 1]; // gets fee that applies at the time of feeToVerify
return feeToVerify.feeAmount >= feeToCheck.feeAmount;
return isFeeAmountValid;
};

View File

@ -1,5 +1,8 @@
import { feeDestinationName, FeeType } from "./FeeData.tsx";
import { CoinType } from "./FeePricePublish/FeePricePublish.ts";
import {
CoinType,
fetchCurrentPriceData,
} from "./FeePricePublish/FeePricePublish.ts";
export interface NameData {
name: string;
@ -58,7 +61,7 @@ export const sendQORTtoName = async (name: string, amount: number) => {
export interface PublishFeeData {
signature: string;
senderName: string;
senderName?: string;
createdTimestamp?: number; //timestamp of the metadata publish, NOT the send feeAmount publish, added after publish is fetched
updatedTimestamp?: number;
feeType?: FeeType;
@ -73,7 +76,11 @@ export interface CommentObject {
feeData: PublishFeeData;
}
export const payPublishFeeQORT = async (feeAmount: number) => {
const publish = await sendQORTtoName(feeDestinationName, feeAmount);
export const payPublishFeeQORT = async (
feeType: FeeType = "default",
coinType: CoinType = "QORT"
) => {
const feeData = await fetchCurrentPriceData(feeType, coinType);
const publish = await sendQORTtoName(feeDestinationName, feeData.feeAmount);
return publish?.signature;
};

View File

@ -45,7 +45,7 @@ const verifySignature = async (feeData: PublishFeeData) => {
});
const signatureTime = signatureData.timestamp;
let doesTimeMatch: boolean = false;
let doesTimeMatch = false;
if (!updatedTimestamp) {
const timeDiff = createdTimestamp - signatureTime;
const timeDiffMinutes = Math.abs(timeDiff) / 1000 / 60;

View File

@ -10,19 +10,20 @@ import React, { useRef, useState } from "react";
type eventType = React.ChangeEvent<HTMLInputElement>;
type BoundedNumericTextFieldProps = {
minValue: number;
maxValue: number;
minValue?: number;
maxValue?: number;
addIconButtons?: boolean;
allowDecimals?: boolean;
allowNegatives?: boolean;
onChange?: (s: string) => void;
onBlur?: (s: string) => void;
initialValue?: string;
maxSigDigits?: number;
} & TextFieldProps;
export const BoundedNumericTextField = ({
minValue,
maxValue,
minValue = 0,
maxValue = Number.MAX_VALUE,
addIconButtons = true,
allowDecimals = true,
allowNegatives = false,
@ -30,6 +31,8 @@ export const BoundedNumericTextField = ({
maxSigDigits = 6,
...props
}: BoundedNumericTextFieldProps) => {
const { onChange, onBlur, ...noChangeProps } = { ...props };
const [textFieldValue, setTextFieldValue] = useState<string>(
initialValue || ""
);
@ -108,6 +111,7 @@ export const BoundedNumericTextField = ({
let value = e.target.value;
if (stringIsEmpty(value) || value === ".") {
setTextFieldValue("");
if (onBlur) onBlur("");
return;
}
@ -116,9 +120,9 @@ export const BoundedNumericTextField = ({
if (isAllZerosNum.test(value)) value = minValue.toString();
setTextFieldValue(value);
if (onBlur) onBlur(value);
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { onChange, ...noChangeProps } = { ...props };
return (
<TextField
{...noChangeProps}

View File

@ -0,0 +1,45 @@
import AddIcon from "@mui/icons-material/Add";
import RemoveIcon from "@mui/icons-material/Remove";
import { CheckboxProps, FormControlLabel, Checkbox } from "@mui/material";
import React, { useRef, useState } from "react";
import { supportedCoins } from "../constants/PublishFees/FeeData.tsx";
import StateTextField from "./StateTextField.tsx";
type eventType = React.ChangeEvent<HTMLInputElement>;
type StateCheckBoxProps = {
onChange?: (b: boolean) => void;
label?: string;
} & CheckboxProps;
export const StateCheckBox = ({ ...props }: StateCheckBoxProps) => {
const { onChange, defaultChecked, label, ...noChangeProps } = { ...props };
const [checkValue, setCheckValue] = useState<boolean>(defaultChecked);
const ref = useRef<HTMLInputElement | null>(null);
const stringIsEmpty = (value: string) => {
return value === "";
};
const listeners = (e: eventType) => {
const newValue = e.target.checked;
setCheckValue(newValue);
if (onChange) onChange(newValue);
};
return (
<FormControlLabel
label={label}
control={
<Checkbox
{...noChangeProps}
onChange={e => listeners(e as eventType)}
checked={checkValue}
/>
}
/>
);
};
export default StateTextField;

View File

@ -0,0 +1,64 @@
import AddIcon from "@mui/icons-material/Add";
import RemoveIcon from "@mui/icons-material/Remove";
import {
IconButton,
InputAdornment,
MenuItem,
TextField,
TextFieldProps,
} from "@mui/material";
import React, { useRef, useState } from "react";
import { supportedCoins } from "../constants/PublishFees/FeeData.tsx";
type eventType = React.ChangeEvent<HTMLInputElement>;
type StateTextFieldProps = {
onChange?: (s: string) => void;
initialValue?: string;
options?: string[];
} & TextFieldProps;
export const StateTextField = ({
initialValue,
options,
...props
}: StateTextFieldProps) => {
const { onChange, ...noChangeProps } = { ...props };
const [textFieldValue, setTextFieldValue] = useState<string>(
initialValue || ""
);
const ref = useRef<HTMLInputElement | null>(null);
const stringIsEmpty = (value: string) => {
return value === "";
};
const listeners = (e: eventType) => {
const newValue = e.target.value;
setTextFieldValue(newValue);
if (onChange) onChange(newValue);
};
return (
<TextField
{...noChangeProps}
InputProps={{
...props?.InputProps,
}}
onChange={e => listeners(e as eventType)}
autoComplete="off"
value={textFieldValue}
inputRef={ref}
>
{options &&
props?.select &&
options.map((option, index) => (
<MenuItem value={option} key={option + index}>
{option}
</MenuItem>
))}
</TextField>
);
};
export default StateTextField;

View File

@ -76,7 +76,7 @@ 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",

View File

@ -28,3 +28,12 @@ export const isNumber = (input: string) => {
export const truncateNumber = (value: string | number, sigDigits: number) => {
return Number(value).toFixed(sigDigits);
};
export const objectIndexToKey = (obj: object, index: number) => {
const keys = Object.keys(obj);
if (index < 0 || index >= keys.length) {
throw new Error(`Invalid index: ${index}`);
}
const key = keys[index];
return key;
};

View File

@ -1,8 +1,11 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
base: ""
})
base: "",
build: {
target: "esnext", //browsers can handle the latest ES features
},
});