mirror of
https://github.com/Qortal/q-trade.git
synced 2025-06-15 10:51:21 +00:00
748 lines
21 KiB
TypeScript
748 lines
21 KiB
TypeScript
import { useState, useEffect, useRef, useContext, ChangeEvent, useMemo } from "react";
|
|
import {
|
|
BubbleCardColored1,
|
|
CoinActionContainer,
|
|
CoinActionRow,
|
|
CoinActionsRow,
|
|
CoinCancelBtn,
|
|
CoinConfirmSendBtn,
|
|
CoinReceiveBtn,
|
|
CoinSelectRow,
|
|
CoinSendBtn,
|
|
CustomInputField,
|
|
HeaderNav,
|
|
HeaderRow,
|
|
HeaderText,
|
|
LogoColumn,
|
|
NameRow,
|
|
RightColumn,
|
|
SendFont,
|
|
TotalCol,
|
|
Username,
|
|
} from "./Header-styles";
|
|
import gameContext from "../../contexts/gameContext";
|
|
import { UserContext } from "../../contexts/userContext";
|
|
import { cropAddress } from "../../utils/cropAddress";
|
|
import qtradeLogo from "../../components/common/icons/qtradeLogo.png";
|
|
import qortIcon from "../../assets/img/qort.png";
|
|
import ErrorIcon from "@mui/icons-material/Error";
|
|
import { CopyToClipboard } from "react-copy-to-clipboard";
|
|
import Copy from "../../assets/SVG/Copy.svg";
|
|
import {AddressQRCode} from './AddressQRCode'
|
|
import {FallingLines} from 'react-loader-spinner'
|
|
import {
|
|
Alert,
|
|
AppBar,
|
|
Avatar,
|
|
Box,
|
|
Card,
|
|
CardContent,
|
|
FormControl,
|
|
FormControlLabel,
|
|
MenuItem,
|
|
Select,
|
|
Snackbar,
|
|
SnackbarCloseReason,
|
|
Switch,
|
|
Typography,
|
|
styled,
|
|
} from "@mui/material";
|
|
import { sendRequestToExtension } from "../../App";
|
|
import { Terms } from "../Terms";
|
|
import ltcIcon from "../../assets/img/ltc.png";
|
|
import btcIcon from "../../assets/img/btc.png";
|
|
import dogeIcon from "../../assets/img/doge.png";
|
|
import rvnIcon from "../../assets/img/rvn.png";
|
|
import dgbIcon from "../../assets/img/dgb.png";
|
|
import arrrIcon from "../../assets/img/arrr.png";
|
|
import { Spacer } from "../common/Spacer";
|
|
import { ReusableModal } from "../common/reusable-modal/ReusableModal";
|
|
import { NotificationContext } from "../../contexts/notificationContext";
|
|
|
|
const checkIfLocal = async () => {
|
|
try {
|
|
const response = await sendRequestToExtension("CHECK_IF_LOCAL");
|
|
|
|
if (!response.error) {
|
|
return response;
|
|
}
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
};
|
|
|
|
export const Label = styled("label")(
|
|
({ theme }) => `
|
|
font-family: 'IBM Plex Sans', sans-serif;
|
|
font-size: 14px;
|
|
display: block;
|
|
margin-bottom: 4px;
|
|
font-weight: 400;
|
|
`
|
|
);
|
|
|
|
interface CoinModalProps {
|
|
coin: string;
|
|
type: string;
|
|
}
|
|
|
|
const getCoinIcon = (coin) => {
|
|
let img;
|
|
|
|
switch (coin) {
|
|
case "LTC":
|
|
img = ltcIcon;
|
|
break;
|
|
case "BTC":
|
|
img = btcIcon;
|
|
break;
|
|
|
|
case "DOGE":
|
|
img = dogeIcon;
|
|
break;
|
|
case "RVN":
|
|
img = rvnIcon;
|
|
break;
|
|
|
|
case "ARRR":
|
|
img = arrrIcon;
|
|
break;
|
|
case "DGB":
|
|
img = dgbIcon;
|
|
break;
|
|
default:
|
|
null;
|
|
}
|
|
return img;
|
|
};
|
|
|
|
const SelectRow = ({ coin }) => {
|
|
let img = getCoinIcon(coin);
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: "10px",
|
|
height: "25px",
|
|
}}
|
|
>
|
|
<img
|
|
style={{
|
|
height: "20px",
|
|
width: "auto",
|
|
}}
|
|
src={img}
|
|
/>
|
|
<p>{coin}</p>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const Header = ({ qortBalance, foreignCoinBalance }: any) => {
|
|
const [openDropdown, setOpenDropdown] = useState<boolean>(false);
|
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
const buttonRef = useRef<HTMLDivElement>(null);
|
|
const [checked, setChecked] = useState(false);
|
|
const [open, setOpen] = useState(false);
|
|
const [info, setInfo] = useState<any>(null);
|
|
const [openCoinActionModal, setOpenCoinActionModal] =
|
|
useState<CoinModalProps | null>(null);
|
|
const [receiverAddress, setReceiverAddress] = useState<string>("");
|
|
const [senderAddress, setSenderAddress] = useState<string>("");
|
|
const [amount, setAmount] = useState<string>("");
|
|
const [coinAddresses, setCoinAddresses] = useState({});
|
|
const { isUsingGateway } = useContext(gameContext);
|
|
|
|
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
|
setChecked(false);
|
|
setOpen(true);
|
|
setInfo({
|
|
type: "error",
|
|
message: "Change the node you are using at the authentication page",
|
|
});
|
|
};
|
|
const { userInfo, selectedCoin, setSelectedCoin, getCoinLabel } =
|
|
useContext(gameContext);
|
|
const { setNotification } = useContext(NotificationContext);
|
|
|
|
|
|
const LocalNodeSwitch = styled(Switch)(({ theme }) => ({
|
|
padding: 8,
|
|
"& .MuiSwitch-track": {
|
|
borderRadius: 22 / 2,
|
|
"&::before, &::after": {
|
|
content: '""',
|
|
position: "absolute",
|
|
top: "50%",
|
|
transform: "translateY(-50%)",
|
|
width: 16,
|
|
height: 16,
|
|
},
|
|
"&::before": {
|
|
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 24 24"><path fill="${encodeURIComponent(
|
|
theme.palette.getContrastText(theme.palette.primary.main)
|
|
)}" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"/></svg>')`,
|
|
left: 12,
|
|
},
|
|
"&::after": {
|
|
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16" viewBox="0 0 24 24"><path fill="${encodeURIComponent(
|
|
theme.palette.getContrastText(theme.palette.primary.main)
|
|
)}" d="M19,13H5V11H19V13Z" /></svg>')`,
|
|
right: 12,
|
|
},
|
|
},
|
|
"& .MuiSwitch-thumb": {
|
|
boxShadow: "none",
|
|
width: 16,
|
|
height: 16,
|
|
margin: 2,
|
|
},
|
|
}));
|
|
|
|
const handleClose = (
|
|
event?: React.SyntheticEvent | Event,
|
|
reason?: SnackbarCloseReason
|
|
) => {
|
|
if (reason === "clickaway") {
|
|
return;
|
|
}
|
|
|
|
setOpen(false);
|
|
setInfo(null);
|
|
};
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (
|
|
dropdownRef.current &&
|
|
!dropdownRef.current.contains(event.target as Node) &&
|
|
buttonRef.current &&
|
|
!buttonRef.current.contains(event.target as Node)
|
|
) {
|
|
setOpenDropdown(false);
|
|
}
|
|
};
|
|
|
|
document.addEventListener("click", handleClickOutside);
|
|
|
|
return () => {
|
|
document.removeEventListener("click", handleClickOutside);
|
|
};
|
|
}, []);
|
|
|
|
// // Fetch avatar on userInfo change
|
|
// useEffect(() => {
|
|
// if (userInfo?.name) {
|
|
// getAvatar();
|
|
// }
|
|
// }, [userInfo]);
|
|
|
|
const sendCoin = async ()=> {
|
|
try {
|
|
const coin = openCoinActionModal.coin === "QORT" ? 'QORT' : getCoinLabel()
|
|
if(!coin) return
|
|
setOpen(true);
|
|
setInfo({
|
|
type: "info",
|
|
message: "Sending Coin...",
|
|
autoHideDurationOff: true
|
|
});
|
|
const response = await qortalRequest({
|
|
action: "SEND_COIN",
|
|
coin,
|
|
destinationAddress: senderAddress,
|
|
amount: +amount
|
|
});
|
|
if(response?.error){
|
|
throw new Error(response?.error || "Failed to send coin.")
|
|
}
|
|
setOpen(true);
|
|
setInfo({
|
|
type: "success",
|
|
message: "Coin sent",
|
|
});
|
|
setAmount('')
|
|
} catch (error) {
|
|
setOpen(true);
|
|
setInfo({
|
|
type: "error",
|
|
message: error?.error || error?.message,
|
|
});
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<AppBar
|
|
position="sticky"
|
|
sx={{
|
|
background: "rgba(39, 40, 44, 1)",
|
|
}}
|
|
>
|
|
<Box
|
|
sx={{
|
|
display: "flex",
|
|
gap: "20px",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
width: "100%",
|
|
padding: "10px",
|
|
}}
|
|
>
|
|
<LogoColumn>
|
|
<img
|
|
src={qtradeLogo}
|
|
style={{
|
|
height: "40px",
|
|
}}
|
|
/>
|
|
</LogoColumn>
|
|
<FormControlLabel
|
|
sx={{
|
|
color: "white",
|
|
}}
|
|
control={
|
|
<LocalNodeSwitch
|
|
checked={isUsingGateway}
|
|
onChange={handleChange}
|
|
/>
|
|
}
|
|
label="Is using Gateway"
|
|
/>
|
|
</Box>
|
|
</AppBar>
|
|
<HeaderNav
|
|
sx={{
|
|
flexDirection: "column",
|
|
gap: "10px",
|
|
}}
|
|
>
|
|
<Spacer height="10px" />
|
|
<Box
|
|
sx={{
|
|
display: "flex",
|
|
gap: "20px",
|
|
alignItems: "center",
|
|
width: "100%",
|
|
justifyContent: "space-between",
|
|
}}
|
|
>
|
|
<NameRow>
|
|
{userInfo?.name ? (
|
|
<Avatar
|
|
sx={{
|
|
height: "30px",
|
|
width: "30px",
|
|
fontSize: "16px",
|
|
}}
|
|
src={`/arbitrary/THUMBNAIL/${userInfo?.name}/qortal_avatar?async=true`}
|
|
alt={`${userInfo?.name}`}
|
|
>
|
|
{userInfo?.name?.charAt(0)?.toUpperCase()}
|
|
</Avatar>
|
|
) : userInfo?.address ? (
|
|
<BubbleCardColored1 style={{ height: "35px", width: "35px" }} />
|
|
) : null}
|
|
{userInfo?.name ? (
|
|
<Username>{userInfo?.name}</Username>
|
|
) : userInfo?.address ? (
|
|
<Username>{cropAddress(userInfo?.address)}</Username>
|
|
) : null}
|
|
</NameRow>
|
|
<Terms />
|
|
</Box>
|
|
|
|
<RightColumn
|
|
sx={{
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
<Card
|
|
variant="outlined"
|
|
sx={{
|
|
backgroundColor: "#292929",
|
|
"&.MuiCard-root": {
|
|
cursor: "default",
|
|
},
|
|
}}
|
|
>
|
|
<CardContent>
|
|
<HeaderText>Total Balance</HeaderText>
|
|
<Spacer height="10px" />
|
|
<TotalCol>
|
|
<Box
|
|
sx={{
|
|
display: "flex",
|
|
gap: "10px",
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
<img
|
|
src={qortIcon}
|
|
style={{
|
|
height: "25px",
|
|
width: "auto",
|
|
}}
|
|
/>
|
|
<HeaderText>{qortBalance} QORT</HeaderText>
|
|
</Box>
|
|
<CoinActionsRow>
|
|
<CoinSendBtn
|
|
onClick={() => {
|
|
setOpenCoinActionModal({
|
|
coin: "QORT",
|
|
type: "send",
|
|
});
|
|
}}
|
|
>
|
|
Send
|
|
</CoinSendBtn>
|
|
<CoinReceiveBtn
|
|
onClick={() => {
|
|
setOpenCoinActionModal({
|
|
coin: "QORT",
|
|
type: "receive",
|
|
});
|
|
}}
|
|
>
|
|
Receive
|
|
</CoinReceiveBtn>
|
|
</CoinActionsRow>
|
|
</TotalCol>
|
|
<Spacer height="10px" />
|
|
<TotalCol>
|
|
<Box
|
|
sx={{
|
|
display: "flex",
|
|
gap: "10px",
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
<img
|
|
src={getCoinIcon(getCoinLabel())}
|
|
style={{
|
|
height: "25px",
|
|
width: "auto",
|
|
}}
|
|
/>
|
|
{foreignCoinBalance === null ? (
|
|
<FallingLines
|
|
color="white"
|
|
width="30"
|
|
visible={true}
|
|
/>
|
|
) : foreignCoinBalance}{" "}
|
|
{getCoinLabel()}
|
|
</Box>
|
|
<CoinActionsRow>
|
|
<CoinSendBtn
|
|
onClick={() => {
|
|
setOpenCoinActionModal({
|
|
coin: selectedCoin,
|
|
type: "send",
|
|
});
|
|
}}
|
|
>
|
|
Send
|
|
</CoinSendBtn>
|
|
<CoinReceiveBtn
|
|
onClick={() => {
|
|
setOpenCoinActionModal({
|
|
coin: selectedCoin,
|
|
type: "receive",
|
|
});
|
|
}}
|
|
>
|
|
Receive
|
|
</CoinReceiveBtn>
|
|
</CoinActionsRow>
|
|
</TotalCol>
|
|
</CardContent>
|
|
</Card>
|
|
</RightColumn>
|
|
|
|
<CoinSelectRow>
|
|
<Select
|
|
size="small"
|
|
value={selectedCoin}
|
|
onChange={(e) => setSelectedCoin(e.target.value)}
|
|
>
|
|
<MenuItem value={"LITECOIN"}>
|
|
<SelectRow coin="LTC" />
|
|
</MenuItem>
|
|
<MenuItem value={"DOGECOIN"}>
|
|
<SelectRow coin="DOGE" />
|
|
</MenuItem>
|
|
<MenuItem value={"BITCOIN"}>
|
|
<SelectRow coin="BTC" />
|
|
</MenuItem>
|
|
<MenuItem value={"DIGIBYTE"}>
|
|
<SelectRow coin="DGB" />
|
|
</MenuItem>
|
|
<MenuItem value={"RAVENCOIN"}>
|
|
<SelectRow coin="RVN" />
|
|
</MenuItem>
|
|
<MenuItem value={"PIRATECHAIN"}>
|
|
<SelectRow coin="ARRR" />
|
|
</MenuItem>
|
|
</Select>
|
|
</CoinSelectRow>
|
|
<Snackbar
|
|
anchorOrigin={{ vertical: "bottom", horizontal: "center" }}
|
|
open={open}
|
|
autoHideDuration={info?.autoHideDurationOff ? null : 6000}
|
|
onClose={handleClose}
|
|
>
|
|
{info?.type && (
|
|
<Alert
|
|
onClose={handleClose}
|
|
severity={info?.type}
|
|
variant="filled"
|
|
sx={{ width: "100%" }}
|
|
>
|
|
{info?.message}
|
|
</Alert>
|
|
)}
|
|
|
|
</Snackbar>
|
|
{openCoinActionModal && (
|
|
<ReusableModal
|
|
onClickClose={() => {
|
|
setOpenCoinActionModal(null);
|
|
setAmount('')
|
|
setSenderAddress('')
|
|
}}
|
|
backdrop
|
|
>
|
|
<CoinActionContainer>
|
|
{openCoinActionModal.type === "send" ? <>
|
|
<CoinActionRow>
|
|
<HeaderRow>
|
|
{openCoinActionModal.type === "send" &&
|
|
openCoinActionModal.coin === "QORT" ? (
|
|
<>
|
|
<SendFont>Send {openCoinActionModal.coin}</SendFont>
|
|
<img
|
|
src={qortIcon}
|
|
style={{
|
|
height: "25px",
|
|
width: "auto",
|
|
}}
|
|
/>
|
|
</>
|
|
) : openCoinActionModal.type === "send" &&
|
|
openCoinActionModal.coin !== "QORT" ? (
|
|
<>
|
|
<SendFont>Send {openCoinActionModal.coin}</SendFont>
|
|
<img
|
|
src={getCoinIcon(getCoinLabel())}
|
|
style={{
|
|
height: "25px",
|
|
width: "auto",
|
|
}}
|
|
/>
|
|
</>
|
|
) : null}
|
|
</HeaderRow>
|
|
</CoinActionRow>
|
|
<CoinActionRow>
|
|
<FormControl fullWidth>
|
|
<CustomInputField
|
|
style={{ flexGrow: 1 }}
|
|
name={
|
|
openCoinActionModal.type === "send"
|
|
? `${openCoinActionModal.coin === "QORT" ? 'Recipient Address or Name' : 'Recipient Address'}`
|
|
: "Receive Address"
|
|
}
|
|
label={
|
|
openCoinActionModal.type === "send"
|
|
? `${openCoinActionModal.coin === "QORT" ? 'Recipient Address or Name' : 'Recipient Address'}`
|
|
: "Receive Address"
|
|
}
|
|
variant="filled"
|
|
value={
|
|
openCoinActionModal.type === "send"
|
|
? senderAddress
|
|
: receiverAddress
|
|
}
|
|
required
|
|
onChange={(e) => {
|
|
if (openCoinActionModal.type === "send") {
|
|
setSenderAddress(e.target.value);
|
|
} else {
|
|
setReceiverAddress(e.target.value);
|
|
}
|
|
}}
|
|
/>
|
|
</FormControl>
|
|
</CoinActionRow>
|
|
{openCoinActionModal.type === "send" && (
|
|
<CoinActionRow>
|
|
<FormControl fullWidth>
|
|
<CustomInputField
|
|
style={{ flexGrow: 1 }}
|
|
name="Amount"
|
|
label="Amount"
|
|
variant="filled"
|
|
type="number"
|
|
value={
|
|
amount
|
|
}
|
|
required
|
|
onChange={(e) => {
|
|
setAmount(e.target.value)
|
|
}}
|
|
/>
|
|
</FormControl>
|
|
</CoinActionRow>
|
|
)}
|
|
</> : (
|
|
<>
|
|
<ReceiveCoin setOpen={setOpen} setInfo={setInfo} coinAddresses={coinAddresses} setCoinAddresses={setCoinAddresses} selectedCoin={openCoinActionModal.coin === "QORT" ? 'QORT' :getCoinLabel()} />
|
|
</>
|
|
)}
|
|
{openCoinActionModal.type === 'send' && (
|
|
<CoinActionRow style={{gap: "10px"}}>
|
|
{/* <CoinCancelBtn onClick={() => setOpenCoinActionModal(null)}>
|
|
Cancel
|
|
</CoinCancelBtn> */}
|
|
<CoinConfirmSendBtn
|
|
onClick={() => {
|
|
if(openCoinActionModal.type === 'send'){
|
|
sendCoin()
|
|
}
|
|
setNotification({
|
|
alertType: "alertInfo",
|
|
msg: "Sending...",
|
|
});
|
|
}}
|
|
>
|
|
{openCoinActionModal.type === "send" ? "Send" : "Receive"}
|
|
</CoinConfirmSendBtn>
|
|
</CoinActionRow>
|
|
)}
|
|
|
|
|
|
|
|
</CoinActionContainer>
|
|
</ReusableModal>
|
|
)}
|
|
</HeaderNav>
|
|
|
|
</>
|
|
);
|
|
};
|
|
|
|
export const AddressBox = styled(Box)`
|
|
display: flex;
|
|
border: 1px solid var(--50-white, rgba(255, 255, 255, 0.5));
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
width: auto;
|
|
word-break: break-word;
|
|
padding: 5px 15px 5px 15px;
|
|
gap: 5px;
|
|
border-radius: 100px;
|
|
font-family: Inter;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
line-height: 14.52px;
|
|
text-align: left;
|
|
color: var(--50-white, rgba(255, 255, 255, 0.5));
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
&:hover {
|
|
background-color: rgba(41, 41, 43, 1);
|
|
color: white;
|
|
svg path {
|
|
fill: white; // Fill color changes to white on hover
|
|
}
|
|
}
|
|
|
|
`
|
|
|
|
|
|
const ReceiveCoin = ({coinAddresses, setCoinAddresses, selectedCoin, setOpen, setInfo})=> {
|
|
const [errorMsg, setErrorMsg] = useState('')
|
|
const foreignAddress = useMemo(()=> {
|
|
return coinAddresses[selectedCoin] || null
|
|
}, [coinAddresses, selectedCoin])
|
|
|
|
const getForeignAddress = async (coin)=> {
|
|
try {
|
|
setOpen(true);
|
|
setInfo({
|
|
type: "info",
|
|
message: "Retrieving address...",
|
|
});
|
|
const response = await qortalRequest({
|
|
action: "GET_USER_WALLET",
|
|
coin
|
|
});
|
|
if(response?.address){
|
|
setCoinAddresses((prev)=> {
|
|
return {
|
|
...prev,
|
|
[coin]: response.address
|
|
}
|
|
})
|
|
}
|
|
if(response?.error){
|
|
throw new Error(response?.error || "Failed to send coin.")
|
|
}
|
|
} catch (error) {
|
|
setErrorMsg(error?.message)
|
|
} finally {
|
|
setOpen(false);
|
|
setInfo(null);
|
|
}
|
|
}
|
|
|
|
useEffect(()=> {
|
|
if(!selectedCoin) return
|
|
if(!coinAddresses[selectedCoin]){
|
|
getForeignAddress(selectedCoin)
|
|
}
|
|
}, [selectedCoin, coinAddresses])
|
|
|
|
return (
|
|
<Box sx={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center'
|
|
}}>
|
|
<Typography sx={{
|
|
color: 'white'
|
|
}}>{`Send ${selectedCoin} to your address below`}</Typography>
|
|
<Spacer height="20px" />
|
|
{foreignAddress && (
|
|
<CopyToClipboard text={foreignAddress} onCopy={()=> {
|
|
setOpen(true);
|
|
setInfo({
|
|
type: "info",
|
|
message: "Address copied!",
|
|
});
|
|
}}>
|
|
<AddressBox>
|
|
{foreignAddress} <img src={Copy} />
|
|
</AddressBox>
|
|
</CopyToClipboard>
|
|
)}
|
|
{foreignAddress && (
|
|
<>
|
|
<AddressQRCode targetAddress={foreignAddress} />
|
|
</>
|
|
)}
|
|
{errorMsg && (
|
|
<>
|
|
<Spacer height="20px" />
|
|
<Typography sx={{
|
|
color: 'white'
|
|
}}>{errorMsg}</Typography>
|
|
</>
|
|
)}
|
|
</Box>
|
|
)
|
|
} |