added settings modal

This commit is contained in:
PhilReact 2025-05-03 16:14:01 +03:00
parent ff8e09eed6
commit e75fadf1bd
14 changed files with 422 additions and 36 deletions

21
package-lock.json generated
View File

@ -16,6 +16,7 @@
"ag-grid-react": "^32.0.1",
"axios": "^1.7.2",
"file-saver": "^2.0.5",
"jotai": "^2.12.3",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"qapp-core": "^1.0.22",
@ -5968,6 +5969,26 @@
"node": ">=8"
}
},
"node_modules/jotai": {
"version": "2.12.3",
"resolved": "https://registry.npmjs.org/jotai/-/jotai-2.12.3.tgz",
"integrity": "sha512-DpoddSkmPGXMFtdfnoIHfueFeGP643nqYUWC6REjUcME+PG2UkAtYnLbffRDw3OURI9ZUTcRWkRGLsOvxuWMCg==",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=17.0.0",
"react": ">=17.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"react": {
"optional": true
}
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",

View File

@ -18,6 +18,7 @@
"ag-grid-react": "^32.0.1",
"axios": "^1.7.2",
"file-saver": "^2.0.5",
"jotai": "^2.12.3",
"lodash": "^4.17.21",
"moment": "^2.30.1",
"qapp-core": "^1.0.22",

View File

@ -6,6 +6,7 @@ export const ReusableModalContainer = styled(Box)(({ theme }) => ({
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: 'column',
backgroundColor: theme.palette.background.default,
position: "fixed",
top: "50%",
@ -16,8 +17,10 @@ export const ReusableModalContainer = styled(Box)(({ theme }) => ({
maxWidth: '90vw',
height: "auto",
borderRadius: "20px",
border: "20px solid #3F3F3F",
zIndex: "100",
border: "5px solid #3F3F3F",
zIndex: 9000,
maxHeight: '90vh',
overflow: 'auto',
boxShadow:
"0px 4px 5px 0px hsla(0,0%,0%,0.14), \n\t\t0px 1px 10px 0px hsla(0,0%,0%,0.12), \n\t\t0px 2px 4px -1px hsla(0,0%,0%,0.2)",
}));
@ -49,8 +52,8 @@ export const ReusableModalCloseIcon = styled(CloseIcon)(({ theme }) => ({
cursor: "pointer",
fontSize: "30px",
position: "absolute",
top: "20px",
right: "20px",
top: "10px",
right: "10px",
transition: "all 0.3s ease-in-out",
"&:hover": {
transform: "scale(1.1)",

View File

@ -1,26 +1,76 @@
import {
ReusableModalBackdrop,
ReusableModalCloseIcon,
ReusableModalContainer,
} from "./ReusableModal-styles";
Dialog,
DialogContent,
IconButton,
useTheme,
SxProps,
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import { Spacer } from '../Spacer';
interface ReusableModalProps {
backdrop?: boolean;
onClickClose: () => void;
children: React.ReactNode;
styles?: SxProps;
open: boolean;
}
export const ReusableModal: React.FC<ReusableModalProps> = ({
backdrop,
backdrop = true,
children,
onClickClose,
styles,
open,
}) => {
const theme = useTheme();
return (
<>
<ReusableModalContainer>
<ReusableModalCloseIcon onClick={onClickClose} />
{children}
</ReusableModalContainer>
{backdrop && <ReusableModalBackdrop onClick={onClickClose} />}
</>
<Dialog
open={open}
onClose={onClickClose}
slotProps={{
backdrop: {
sx: {
backgroundColor: backdrop ? 'rgba(0, 0, 0, 0.5)' : 'transparent',
backdropFilter: backdrop ? 'blur(1px)' : 'none',
},
},
paper: {
sx: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
backgroundColor: theme.palette.background.default,
borderRadius: '20px',
border: '5px solid #3F3F3F',
width: '629px',
maxWidth: '90vw',
minHeight: '446px',
maxHeight: '90vh',
overflowY: 'auto',
position: 'relative',
margin: 0,
...styles,
},
},
}}
>
<IconButton
onClick={onClickClose}
sx={{
position: 'absolute',
top: 10,
right: 10,
color: theme.palette.text.primary,
'&:hover': { transform: 'scale(1.1)' },
}}
>
<CloseIcon fontSize="large" />
</IconButton>
<Spacer height="50px" />
<DialogContent sx={{ width: '100%', padding: 0 }}>{children}</DialogContent>
</Dialog>
);
};

View File

@ -228,7 +228,7 @@ export const CoinActionContainer = styled(Box)({
gap: "25px",
alignItems: "center",
justifyContent: "center",
width: "80%",
width: "100%",
});
export const CoinActionRow = styled(Box)({

View File

@ -68,6 +68,7 @@ import { NotificationContext } from "../../contexts/notificationContext";
import UnsignedFees from "../sell/UnsignedFees";
import { FeeManager } from "../sell/FeeManager";
import { Info } from "../sell/Info";
import { Settings } from "../sell/Settings";
const checkIfLocal = async () => {
try {
@ -126,7 +127,7 @@ const getCoinIcon = (coin) => {
return img;
};
const SelectRow = ({ coin }) => {
export const SelectRow = ({ coin }) => {
let img = getCoinIcon(coin);
return (
@ -367,7 +368,14 @@ export const Header = ({
<Username>{cropAddress(userInfo?.address)}</Username>
) : null}
</NameRow>
<Terms />
<Box sx={{
display: 'flex',
gap: '10px'
}}>
<Terms />
<Settings />
</Box>
</Box>
<RightColumn
@ -541,6 +549,7 @@ export const Header = ({
setSenderAddress("");
}}
backdrop
open={openCoinActionModal}
>
<CoinActionContainer>
{openCoinActionModal.type === "send" ? (

View File

@ -26,6 +26,7 @@ export const CustomLabel = styled(InputLabel)`
font-size: 14px;
line-height: 1.2;
color: rgba(255, 255, 255, 0.5);
white-space: normal;
`;
export const minimumAmountSellTrades = {

View File

@ -29,6 +29,8 @@ import { Spacer } from "../common/Spacer";
import { usePublish, Service, QortalGetMetadata } from "qapp-core";
import { SetLeftFeature } from "ag-grid-community";
import { formatTimestampForum } from "../../utils/formatTime";
import { useAtom } from "jotai/react";
import { selectedFeePublisherAtom } from "../../global/state";
function calculateFeeFromRate(feePerKb, sizeInBytes) {
const fee = (feePerKb / 1000) * sizeInBytes;
@ -46,6 +48,8 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => {
service: "JSON",
});
const { resource } = usePublish(3, "JSON", feeLocation);
const [selectedFeePublisher, setSelectedFeePublisher] = useAtom(selectedFeePublisherAtom)
const [editFee, setEditFee] = useState("");
const [openModal, setOpenModal] = useState(false);
const [recommendedFee, setRecommendedFee] = useState("m");
@ -104,12 +108,7 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => {
const recommendedFeeData = useMemo(() => {
if (!resource?.data) return null;
if (
!resource?.data?.["BTC"] ||
!resource?.data?.["LTC"] ||
!resource?.data?.["DOGE"]
)
return null;
return resource.data;
}, [resource?.data]);
@ -183,7 +182,7 @@ export const FeeManager = ({ selectedCoin, setFee, fee }) => {
const getLatestFees = useCallback(async () => {
try {
const res = await fetch(
`/arbitrary/resources/searchsimple?service=JSON&identifier=foreign-fee&name=Foreign-Fee-Publisher&prefix=true&limit=1&reverse=true`
`/arbitrary/resources/searchsimple?service=JSON&identifier=foreign-fee&name=${selectedFeePublisher}&prefix=true&limit=1&reverse=true`
);
const data = await res.json();
if (data && data?.length > 0) {
@ -196,7 +195,7 @@ const timestampSec = parseInt(parts[2], 10);
} catch (error) {
console.error(error)
}
}, []);
}, [selectedFeePublisher]);
useEffect(() => {
getLatestFees();
@ -239,12 +238,15 @@ const timestampSec = parseInt(parts[2], 10);
setOpenModal(false);
setEditFee(fee);
}}
open={openModal}
backdrop
>
<CoinActionContainer sx={{
styles={{
width: '450px',
maxWidth: '95vw'
}}>
maxWidth: '95vw',
padding: '15px'
}}
>
<CoinActionContainer >
<CoinActionRow>
<HeaderRow>
<Typography
@ -389,7 +391,9 @@ const timestampSec = parseInt(parts[2], 10);
<Typography>Update fee</Typography>
</ButtonBase>
{!hideRecommendations && feeTimestamp && (
<CustomLabel >*Recommended fees last updated: {formatTimestampForum(feeTimestamp)}</CustomLabel>
<CustomLabel sx={{
textAlign: 'center'
}}>*Recommended fees last updated: {formatTimestampForum(feeTimestamp)}</CustomLabel>
)}
</CoinActionContainer>
</ReusableModal>

View File

@ -67,11 +67,14 @@ export const Info = () => {
setOpenModal(false);
}}
backdrop
>
<CoinActionContainer sx={{
styles={{
width: '450px',
maxWidth: '95vw'
}}>
maxWidth: '95vw',
padding: '15px'
}}
open={openModal}
>
<CoinActionContainer>
<CoinActionRow>
<HeaderRow>
<Typography

View File

@ -0,0 +1,275 @@
import React, {
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import gameContext from "../../contexts/gameContext";
import {
Alert,
Box,
Button,
ButtonBase,
IconButton,
MenuItem,
Select,
Snackbar,
SnackbarCloseReason,
ToggleButton,
ToggleButtonGroup,
Typography,
} from "@mui/material";
import ChangeCircleIcon from "@mui/icons-material/ChangeCircle";
import { ReusableModal } from "../common/reusable-modal/ReusableModal";
import QuestionMarkIcon from "@mui/icons-material/QuestionMark";
import SettingsIcon from "@mui/icons-material/Settings";
import {
CoinActionContainer,
CoinActionRow,
CoinSelectRow,
HeaderRow,
} from "../header/Header-styles";
import { CustomInput, CustomLabel } from "./CreateSell";
import { Spacer } from "../common/Spacer";
import { usePublish, Service, QortalGetMetadata } from "qapp-core";
import { SetLeftFeature } from "ag-grid-community";
import { formatTimestampForum } from "../../utils/formatTime";
import { SelectRow } from "../header/Header";
import { useAtom } from "jotai/react";
import { selectedFeePublisherAtom } from "../../global/state";
export const Settings = () => {
const [openModal, setOpenModal] = useState(false);
const [lockingFee, setLockingFee] = useState("");
const [editLockingFee, setEditLockingFee] = useState("");
const [openAlert, setOpenAlert] = useState(false);
const [info, setInfo] = useState<any>(null);
const [selectedCoin, setSelectedCoin] = useState("LTC");
const [selectedFeePublisher, setSelectedFeePublisher] = useAtom(selectedFeePublisherAtom)
const handleCloseAlert = (
event?: React.SyntheticEvent | Event,
reason?: SnackbarCloseReason
) => {
if (reason === "clickaway") {
return;
}
setOpenAlert(false);
setInfo(null);
};
const updateLockingFee = async () => {
const typeRequest = "feekb";
try {
const feeToSave = editLockingFee;
const response = await qortalRequestWithTimeout(
{
action: "UPDATE_FOREIGN_FEE",
coin: selectedCoin,
type: typeRequest,
value: feeToSave,
},
1800000
);
if (response && !isNaN(+response)) {
setLockingFee(response);
setEditLockingFee(response);
setOpenAlert(true);
setInfo({
type: "success",
message: "Fee updated!",
});
setOpenModal(false);
} else throw new Error("Unable to update fee");
} catch (error) {
setOpenAlert(true);
setInfo({
type: "error",
message: error?.message || "Unable to update fee",
});
}
};
const establishUpdateFeeForm = useCallback(async (coin) => {
setLockingFee("");
setEditLockingFee("");
// if the coin or type is not set, then abort
if (!coin) {
return;
}
// const coinRequest = coin.current.toLowerCase();
const typeRequest = "feekb";
try {
const response = await qortalRequestWithTimeout(
{
action: "GET_FOREIGN_FEE",
coin: coin.toLowerCase(),
type: typeRequest,
},
1800000
);
if (response !== null && response !== undefined && !isNaN(+response)) {
setLockingFee(response);
setEditLockingFee(response);
}
} catch (error) {
setLockingFee("");
setEditLockingFee("");
console.error(error);
}
}, []);
useEffect(() => {
establishUpdateFeeForm(selectedCoin);
}, [selectedCoin, establishUpdateFeeForm]);
return (
<>
<Button
variant="outlined"
onClick={() => {
setOpenModal(true);
setEditLockingFee(lockingFee);
}}
>
<SettingsIcon
sx={{
color: "white",
}}
/>
</Button>
{openModal && (
<ReusableModal
onClickClose={() => {
setOpenModal(false);
setEditLockingFee(lockingFee);
}}
backdrop
styles={{
width: "450px",
maxWidth: "95vw",
padding: "15px",
}}
open={openModal}
>
<CoinActionContainer sx={{
border: '1px solid #3F3F3F',
borderRadius: '5px',
padding: '5px'
}}>
<Typography>Locking fees</Typography>
<CoinSelectRow sx={{
gap: '20px'
}}>
<Select
size="small"
value={selectedCoin}
onChange={(e) => {
setLockingFee("");
setEditLockingFee("");
setSelectedCoin(e.target.value);
}}
>
<MenuItem value={"LTC"}>
<SelectRow coin="LTC" />
</MenuItem>
<MenuItem value={"DOGE"}>
<SelectRow coin="DOGE" />
</MenuItem>
<MenuItem value={"BTC"}>
<SelectRow coin="BTC" />
</MenuItem>
<MenuItem value={"DGB"}>
<SelectRow coin="DGB" />
</MenuItem>
<MenuItem value={"RVN"}>
<SelectRow coin="RVN" />
</MenuItem>
</Select>
<Box>
<CustomLabel htmlFor="standard-adornment-name">
Locking fee for {selectedCoin} (sats)
</CustomLabel>
<Spacer height="5px" />
<CustomInput
id="standard-adornment-name"
type="number"
value={editLockingFee}
onChange={(e) => setEditLockingFee(e.target.value)}
autoComplete="off"
/>
</Box>
</CoinSelectRow>
<ButtonBase
onClick={updateLockingFee}
disabled={!editLockingFee}
sx={{
minHeight: "42px",
border: "1px solid gray",
color: "white",
display: "flex",
alignItems: "center",
padding: "5px 20px",
gap: "10px",
borderRadius: "5px",
"&:hover": {
border: "1px solid white", // Border color on hover
},
}}
>
<ChangeCircleIcon
sx={{
color: "white",
}}
/>
<Typography>Update locking fee</Typography>
</ButtonBase>
</CoinActionContainer>
<Spacer height="20px"/>
<CoinActionContainer sx={{
border: '1px solid #3F3F3F',
borderRadius: '5px',
padding: '5px'
}}>
<Typography>Fee publisher</Typography>
<Select
size="small"
value={selectedFeePublisher}
onChange={(e) => {
setSelectedFeePublisher(e.target.value);
}}
>
<MenuItem value={"Foreign-Fee-Publisher"}>
<SelectRow coin="Foreign-Fee-Publisher" />
</MenuItem>
<MenuItem value={"Ice.JSON"}>
<SelectRow coin="Ice.JSON" />
</MenuItem>
</Select>
</CoinActionContainer>
</ReusableModal>
)}
<Snackbar
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
open={openAlert}
onClose={handleCloseAlert}
autoHideDuration={6000}
>
<Alert
onClose={handleCloseAlert}
severity={info?.type}
variant="filled"
sx={{ width: "100%" }}
>
{info?.message}
</Alert>
</Snackbar>
</>
);
};

View File

@ -171,6 +171,12 @@ export default function UnsignedFees({ qortAddress }) {
setOpenModal(false);
}}
backdrop
styles={{
width: '450px',
maxWidth: '95vw',
padding: '15px'
}}
open={openModal}
>
<CoinActionContainer>
<CoinActionRow>

5
src/global/state.ts Normal file
View File

@ -0,0 +1,5 @@
import { atomWithReset } from 'jotai/utils';
export const selectedFeePublisherAtom = atomWithReset('Foreign-Fee-Publisher');

View File

@ -25,6 +25,7 @@ html,
body {
width: 100%;
height: 100%;
word-break: break-word;
}
body {

View File

@ -154,6 +154,13 @@ const darkTheme = createTheme({
disableRipple: true,
},
},
MuiDialog: {
styleOverrides: {
paper: {
backgroundImage: 'none',
},
},
},
},
});