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", "ag-grid-react": "^32.0.1",
"axios": "^1.7.2", "axios": "^1.7.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jotai": "^2.12.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.30.1", "moment": "^2.30.1",
"qapp-core": "^1.0.22", "qapp-core": "^1.0.22",
@ -5968,6 +5969,26 @@
"node": ">=8" "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": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "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", "ag-grid-react": "^32.0.1",
"axios": "^1.7.2", "axios": "^1.7.2",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"jotai": "^2.12.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.30.1", "moment": "^2.30.1",
"qapp-core": "^1.0.22", "qapp-core": "^1.0.22",

View File

@ -6,6 +6,7 @@ export const ReusableModalContainer = styled(Box)(({ theme }) => ({
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
flexDirection: 'column',
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
position: "fixed", position: "fixed",
top: "50%", top: "50%",
@ -16,8 +17,10 @@ export const ReusableModalContainer = styled(Box)(({ theme }) => ({
maxWidth: '90vw', maxWidth: '90vw',
height: "auto", height: "auto",
borderRadius: "20px", borderRadius: "20px",
border: "20px solid #3F3F3F", border: "5px solid #3F3F3F",
zIndex: "100", zIndex: 9000,
maxHeight: '90vh',
overflow: 'auto',
boxShadow: 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)", "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", cursor: "pointer",
fontSize: "30px", fontSize: "30px",
position: "absolute", position: "absolute",
top: "20px", top: "10px",
right: "20px", right: "10px",
transition: "all 0.3s ease-in-out", transition: "all 0.3s ease-in-out",
"&:hover": { "&:hover": {
transform: "scale(1.1)", transform: "scale(1.1)",

View File

@ -1,26 +1,76 @@
import { import {
ReusableModalBackdrop, Dialog,
ReusableModalCloseIcon, DialogContent,
ReusableModalContainer, IconButton,
} from "./ReusableModal-styles"; useTheme,
SxProps,
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import { Spacer } from '../Spacer';
interface ReusableModalProps { interface ReusableModalProps {
backdrop?: boolean; backdrop?: boolean;
onClickClose: () => void; onClickClose: () => void;
children: React.ReactNode; children: React.ReactNode;
styles?: SxProps;
open: boolean;
} }
export const ReusableModal: React.FC<ReusableModalProps> = ({ export const ReusableModal: React.FC<ReusableModalProps> = ({
backdrop, backdrop = true,
children, children,
onClickClose, onClickClose,
styles,
open,
}) => { }) => {
const theme = useTheme();
return ( return (
<> <Dialog
<ReusableModalContainer> open={open}
<ReusableModalCloseIcon onClick={onClickClose} /> onClose={onClickClose}
{children} slotProps={{
</ReusableModalContainer> backdrop: {
{backdrop && <ReusableModalBackdrop onClick={onClickClose} />} 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", gap: "25px",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
width: "80%", width: "100%",
}); });
export const CoinActionRow = styled(Box)({ export const CoinActionRow = styled(Box)({

View File

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

View File

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

View File

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

View File

@ -67,11 +67,14 @@ export const Info = () => {
setOpenModal(false); setOpenModal(false);
}} }}
backdrop backdrop
> styles={{
<CoinActionContainer sx={{
width: '450px', width: '450px',
maxWidth: '95vw' maxWidth: '95vw',
}}> padding: '15px'
}}
open={openModal}
>
<CoinActionContainer>
<CoinActionRow> <CoinActionRow>
<HeaderRow> <HeaderRow>
<Typography <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); setOpenModal(false);
}} }}
backdrop backdrop
styles={{
width: '450px',
maxWidth: '95vw',
padding: '15px'
}}
open={openModal}
> >
<CoinActionContainer> <CoinActionContainer>
<CoinActionRow> <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 { body {
width: 100%; width: 100%;
height: 100%; height: 100%;
word-break: break-word;
} }
body { body {

View File

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