mirror of
https://github.com/Qortal/Qortal-Hub.git
synced 2025-04-24 20:07:51 +00:00
user lookup feature
This commit is contained in:
parent
f9b1e2784f
commit
8af3977178
50
src/App.tsx
50
src/App.tsx
@ -35,6 +35,7 @@ import RefreshIcon from "@mui/icons-material/Refresh";
|
||||
import Logo2 from "./assets/svgs/Logo2.svg";
|
||||
import Copy from "./assets/svgs/Copy.svg";
|
||||
import ltcLogo from "./assets/ltc.png";
|
||||
import PersonSearchIcon from '@mui/icons-material/PersonSearch';
|
||||
import qortLogo from "./assets/qort.png";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import Download from "./assets/svgs/Download.svg";
|
||||
@ -113,6 +114,7 @@ import {
|
||||
canSaveSettingToQdnAtom,
|
||||
enabledDevModeAtom,
|
||||
fullScreenAtom,
|
||||
groupsPropertiesAtom,
|
||||
hasSettingsChangedAtom,
|
||||
isDisabledEditorEnterAtom,
|
||||
isUsingImportExportSettingsAtom,
|
||||
@ -145,6 +147,8 @@ import { QMailStatus } from "./components/QMailStatus";
|
||||
import { GlobalActions } from "./components/GlobalActions/GlobalActions";
|
||||
import { useBlockedAddresses } from "./components/Group/useBlockUsers";
|
||||
import { WalletIcon } from "./assets/Icons/WalletIcon";
|
||||
import { DrawerUserLookup } from "./components/Drawer/DrawerUserLookup";
|
||||
import { UserLookup } from "./components/UserLookup.tsx/UserLookup";
|
||||
|
||||
type extStates =
|
||||
| "not-authenticated"
|
||||
@ -400,6 +404,7 @@ function App() {
|
||||
const [openSnack, setOpenSnack] = useState(false);
|
||||
const [hasLocalNode, setHasLocalNode] = useState(false);
|
||||
const [isOpenDrawerProfile, setIsOpenDrawerProfile] = useState(false);
|
||||
const [isOpenDrawerLookup, setIsOpenDrawerLookup] = useState(false)
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [isOpenSendQort, setIsOpenSendQort] = useState(false);
|
||||
const [isOpenSendQortSuccess, setIsOpenSendQortSuccess] = useState(false);
|
||||
@ -488,7 +493,7 @@ function App() {
|
||||
const resetAtomOldPinnedAppsAtom = useResetRecoilState(oldPinnedAppsAtom);
|
||||
const resetAtomQMailLastEnteredTimestampAtom = useResetRecoilState(qMailLastEnteredTimestampAtom)
|
||||
const resetAtomMailsAtom = useResetRecoilState(mailsAtom)
|
||||
|
||||
const resetGroupPropertiesAtom = useResetRecoilState(groupsPropertiesAtom)
|
||||
const resetAllRecoil = () => {
|
||||
resetAtomSortablePinnedAppsAtom();
|
||||
resetAtomCanSaveSettingToQdnAtom();
|
||||
@ -498,6 +503,8 @@ function App() {
|
||||
resetAtomIsUsingImportExportSettingsAtom()
|
||||
resetAtomQMailLastEnteredTimestampAtom()
|
||||
resetAtomMailsAtom()
|
||||
resetGroupPropertiesAtom()
|
||||
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!isMobile) return;
|
||||
@ -1696,6 +1703,38 @@ function App() {
|
||||
/>
|
||||
</Tooltip>
|
||||
</ButtonBase>
|
||||
<Spacer height="20px" />
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setIsOpenDrawerLookup(true);
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>USER LOOKUP</span>}
|
||||
placement="left"
|
||||
arrow
|
||||
sx={{ fontSize: "24" }}
|
||||
slotProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
color: "#ffffff",
|
||||
backgroundColor: "#444444",
|
||||
},
|
||||
},
|
||||
arrow: {
|
||||
sx: {
|
||||
color: "#444444",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<PersonSearchIcon
|
||||
sx={{
|
||||
color: "rgba(255, 255, 255, 0.5)",
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</ButtonBase>
|
||||
|
||||
{desktopViewMode !== 'home' && (
|
||||
<>
|
||||
@ -2055,7 +2094,7 @@ function App() {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
zIndex: 6,
|
||||
zIndex: 10000,
|
||||
}}
|
||||
>
|
||||
<Spacer height="22px" />
|
||||
@ -3110,7 +3149,7 @@ function App() {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
zIndex: 6,
|
||||
zIndex: 10000,
|
||||
}}
|
||||
>
|
||||
<Spacer height="48px" />
|
||||
@ -3210,6 +3249,9 @@ function App() {
|
||||
open={isShow}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
sx={{
|
||||
zIndex: 10001
|
||||
}}
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">{message.paymentFee ? "Payment" : "Publish"}</DialogTitle>
|
||||
<DialogContent>
|
||||
@ -3635,7 +3677,7 @@ function App() {
|
||||
>
|
||||
{renderProfileLeft()}
|
||||
</DrawerComponent>
|
||||
|
||||
<UserLookup isOpenDrawerLookup={isOpenDrawerLookup} setIsOpenDrawerLookup={setIsOpenDrawerLookup} />
|
||||
</GlobalContext.Provider>
|
||||
{extState === "create-wallet" && walletToBeDownloaded && (
|
||||
<ButtonBase onClick={()=> {
|
||||
|
@ -170,3 +170,8 @@ export const mailsAtom = atom({
|
||||
key: 'mailsAtom',
|
||||
default: [],
|
||||
});
|
||||
|
||||
export const groupsPropertiesAtom = atom({
|
||||
key: 'groupsPropertiesAtom',
|
||||
default: {},
|
||||
});
|
@ -845,7 +845,7 @@ export async function getNameInfoForOthers(address) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
async function getAddressInfo(address) {
|
||||
export async function getAddressInfo(address) {
|
||||
const validApi = await getBaseApi();
|
||||
const response = await fetch(validApi + "/addresses/" + address);
|
||||
const data = await response.json();
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useContext, useState } from "react";
|
||||
import React, { useContext, useMemo, useState } from "react";
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
@ -18,7 +18,7 @@ import {
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { useHandlePrivateApps } from "./useHandlePrivateApps";
|
||||
import { useRecoilState, useSetRecoilState } from "recoil";
|
||||
import { myGroupsWhereIAmAdminAtom } from "../../atoms/global";
|
||||
import { groupsPropertiesAtom, myGroupsWhereIAmAdminAtom } from "../../atoms/global";
|
||||
import { Label } from "../Group/AddGroup";
|
||||
import { Spacer } from "../../common/Spacer";
|
||||
import {
|
||||
@ -43,14 +43,22 @@ export const AppsPrivate = ({myName}) => {
|
||||
const [logo, setLogo] = useState(null);
|
||||
const [qortalUrl, setQortalUrl] = useState("");
|
||||
const [selectedGroup, setSelectedGroup] = useState(0);
|
||||
|
||||
const [groupsProperties] = useRecoilState(groupsPropertiesAtom)
|
||||
const [valueTabPrivateApp, setValueTabPrivateApp] = useState(0);
|
||||
const [myGroupsWhereIAmAdmin, setMyGroupsWhereIAmAdmin] = useRecoilState(
|
||||
const [myGroupsWhereIAmAdminFromGlobal] = useRecoilState(
|
||||
myGroupsWhereIAmAdminAtom
|
||||
);
|
||||
|
||||
const myGroupsWhereIAmAdmin = useMemo(()=> {
|
||||
return myGroupsWhereIAmAdminFromGlobal?.filter((group)=> groupsProperties[group?.groupId]?.isOpen === false)
|
||||
}, [myGroupsWhereIAmAdminFromGlobal, groupsProperties])
|
||||
const [isOpenPrivateModal, setIsOpenPrivateModal] = useState(false);
|
||||
const { show, setInfoSnackCustom, setOpenSnackGlobal, memberGroups } = useContext(MyContext);
|
||||
|
||||
|
||||
const myGroupsPrivate = useMemo(()=> {
|
||||
return memberGroups?.filter((group)=> groupsProperties[group?.groupId]?.isOpen === false)
|
||||
}, [memberGroups, groupsProperties])
|
||||
const [privateAppValues, setPrivateAppValues] = useState({
|
||||
name: "",
|
||||
service: "DOCUMENT",
|
||||
@ -95,7 +103,7 @@ export const AppsPrivate = ({myName}) => {
|
||||
|
||||
await openApp(privateAppValues, true);
|
||||
} catch (error) {
|
||||
console.log('error', error?.message)
|
||||
console.error(error)
|
||||
|
||||
}
|
||||
};
|
||||
@ -311,7 +319,7 @@ export const AppsPrivate = ({myName}) => {
|
||||
>
|
||||
<MenuItem value={0}>No group selected</MenuItem>
|
||||
|
||||
{memberGroups
|
||||
{myGroupsPrivate
|
||||
?.filter((item) => !item?.isOpen)
|
||||
.map((group) => {
|
||||
return (
|
||||
|
@ -194,7 +194,7 @@ export const GroupAnnouncements = ({
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
console.error("error", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
22
src/components/Drawer/DrawerUserLookup.tsx
Normal file
22
src/components/Drawer/DrawerUserLookup.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
|
||||
export const DrawerUserLookup = ({open, setOpen, children}) => {
|
||||
|
||||
const toggleDrawer = (newOpen: boolean) => () => {
|
||||
setOpen(newOpen);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Drawer disableEnforceFocus hideBackdrop={true} open={open} onClose={toggleDrawer(false)}>
|
||||
<Box sx={{ width: '70vw', height: '100%', maxWidth: '1000px' }} role="presentation">
|
||||
|
||||
{children}
|
||||
</Box>
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -74,8 +74,8 @@ import { HubsIcon } from "../../assets/Icons/HubsIcon";
|
||||
import { MessagingIcon } from "../../assets/Icons/MessagingIcon";
|
||||
import { formatEmailDate } from "./QMailMessages";
|
||||
import { AdminSpace } from "../Chat/AdminSpace";
|
||||
import { useSetRecoilState } from "recoil";
|
||||
import { addressInfoControllerAtom, selectedGroupIdAtom } from "../../atoms/global";
|
||||
import { useRecoilState, useSetRecoilState } from "recoil";
|
||||
import { addressInfoControllerAtom, groupsPropertiesAtom, selectedGroupIdAtom } from "../../atoms/global";
|
||||
import { sortArrayByTimestampAndGroupName } from "../../utils/time";
|
||||
import BlockIcon from '@mui/icons-material/Block';
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
@ -446,7 +446,7 @@ export const Group = ({
|
||||
const [isOpenSideViewGroups, setIsOpenSideViewGroups] = useState(false)
|
||||
const [isForceShowCreationKeyPopup, setIsForceShowCreationKeyPopup] = useState(false)
|
||||
|
||||
const [groupsProperties, setGroupsProperties] = useState({})
|
||||
const [groupsProperties, setGroupsProperties] = useRecoilState(groupsPropertiesAtom)
|
||||
const setUserInfoForLevels = useSetRecoilState(addressInfoControllerAtom);
|
||||
|
||||
const isPrivate = useMemo(()=> {
|
||||
|
@ -1,209 +1,257 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { getBaseApiReact } from '../../App';
|
||||
import { Box, Tooltip, Typography } from '@mui/material';
|
||||
import { BarSpinner } from '../../common/Spinners/BarSpinner/BarSpinner';
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { getBaseApiReact } from "../../App";
|
||||
import { Box, Tooltip, Typography } from "@mui/material";
|
||||
import { BarSpinner } from "../../common/Spinners/BarSpinner/BarSpinner";
|
||||
import { formatDate } from "../../utils/time";
|
||||
|
||||
function getAverageLtcPerQort(trades) {
|
||||
let totalQort = 0;
|
||||
let totalLtc = 0;
|
||||
let totalQort = 0;
|
||||
let totalLtc = 0;
|
||||
|
||||
trades.forEach((trade) => {
|
||||
const qort = parseFloat(trade.qortAmount);
|
||||
const ltc = parseFloat(trade.foreignAmount);
|
||||
trades.forEach((trade) => {
|
||||
const qort = parseFloat(trade.qortAmount);
|
||||
const ltc = parseFloat(trade.foreignAmount);
|
||||
|
||||
totalQort += qort;
|
||||
totalLtc += ltc;
|
||||
});
|
||||
totalQort += qort;
|
||||
totalLtc += ltc;
|
||||
});
|
||||
|
||||
// Avoid division by zero
|
||||
if (totalQort === 0) return 0;
|
||||
// Avoid division by zero
|
||||
if (totalQort === 0) return 0;
|
||||
|
||||
// Weighted average price
|
||||
return parseFloat((totalLtc / totalQort).toFixed(8));
|
||||
}
|
||||
// Weighted average price
|
||||
return parseFloat((totalLtc / totalQort).toFixed(8));
|
||||
}
|
||||
|
||||
function getTwoWeeksAgoTimestamp() {
|
||||
const now = new Date();
|
||||
now.setDate(now.getDate() - 14); // Subtract 14 days
|
||||
return now.getTime(); // Get timestamp in milliseconds
|
||||
}
|
||||
|
||||
function formatWithCommasAndDecimals(number) {
|
||||
|
||||
return Number(number).toLocaleString();
|
||||
}
|
||||
function getTwoWeeksAgoTimestamp() {
|
||||
const now = new Date();
|
||||
now.setDate(now.getDate() - 14); // Subtract 14 days
|
||||
return now.getTime(); // Get timestamp in milliseconds
|
||||
}
|
||||
|
||||
function formatWithCommasAndDecimals(number) {
|
||||
return Number(number).toLocaleString();
|
||||
}
|
||||
|
||||
export const QortPrice = () => {
|
||||
const [ltcPerQort, setLtcPerQort] = useState(null)
|
||||
const [supply, setSupply] = useState(null)
|
||||
const [lastBlock, setLastBlock] = useState(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [ltcPerQort, setLtcPerQort] = useState(null);
|
||||
const [supply, setSupply] = useState(null);
|
||||
const [lastBlock, setLastBlock] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const getPrice = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const getPrice = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const response = await fetch(`${getBaseApiReact()}/crosschain/trades?foreignBlockchain=LITECOIN&minimumTimestamp=${getTwoWeeksAgoTimestamp()}&limit=20&reverse=true`);
|
||||
const data = await response.json();
|
||||
const response = await fetch(
|
||||
`${getBaseApiReact()}/crosschain/trades?foreignBlockchain=LITECOIN&minimumTimestamp=${getTwoWeeksAgoTimestamp()}&limit=20&reverse=true`
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
setLtcPerQort(getAverageLtcPerQort(data));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
setLtcPerQort(getAverageLtcPerQort(data));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false)
|
||||
const getLastBlock = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
}
|
||||
}, [])
|
||||
const response = await fetch(`${getBaseApiReact()}/blocks/last`);
|
||||
const data = await response.json();
|
||||
|
||||
const getLastBlock = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setLastBlock(data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const response = await fetch(`${getBaseApiReact()}/blocks/last`);
|
||||
const data = await response.json();
|
||||
const getSupplyInCirculation = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
setLastBlock(data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false)
|
||||
const response = await fetch(
|
||||
`${getBaseApiReact()}/stats/supply/circulating`
|
||||
);
|
||||
const data = await response.text();
|
||||
formatWithCommasAndDecimals(data);
|
||||
setSupply(formatWithCommasAndDecimals(data));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
}
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
getPrice();
|
||||
getSupplyInCirculation();
|
||||
getLastBlock();
|
||||
const interval = setInterval(() => {
|
||||
getPrice();
|
||||
getSupplyInCirculation();
|
||||
getLastBlock();
|
||||
}, 900000);
|
||||
|
||||
const getSupplyInCirculation = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
return () => clearInterval(interval);
|
||||
}, [getPrice]);
|
||||
|
||||
const response = await fetch(`${getBaseApiReact()}/stats/supply/circulating`);
|
||||
const data = await response.text();
|
||||
formatWithCommasAndDecimals(data)
|
||||
setSupply(formatWithCommasAndDecimals(data));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setLoading(false)
|
||||
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
getPrice();
|
||||
getSupplyInCirculation()
|
||||
getLastBlock()
|
||||
const interval = setInterval(() => {
|
||||
getPrice();
|
||||
getSupplyInCirculation()
|
||||
getLastBlock()
|
||||
}, 900000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
|
||||
}, [getPrice]);
|
||||
|
||||
console.log('supply', supply)
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
flexWrap: "wrap",
|
||||
flexDirection: 'column',
|
||||
width: "322px"
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
title={<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>Based on the latest 20 trades</span>}
|
||||
placement="bottom"
|
||||
arrow
|
||||
sx={{ fontSize: "24" }}
|
||||
slotProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
color: "#ffffff",
|
||||
backgroundColor: "#444444",
|
||||
},
|
||||
},
|
||||
arrow: {
|
||||
sx: {
|
||||
color: "#444444",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={{
|
||||
width: "322px",
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: '10px',
|
||||
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<Typography sx={{
|
||||
fontSize: "1rem",
|
||||
fontWeight: 'bold'
|
||||
}}>Price</Typography>
|
||||
{!ltcPerQort ? (
|
||||
<BarSpinner width="16px" color="white" />
|
||||
): (
|
||||
<Typography sx={{
|
||||
fontSize: "1rem",
|
||||
}}>{ltcPerQort} LTC/QORT</Typography>
|
||||
)}
|
||||
|
||||
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Box sx={{
|
||||
gap: "20px",
|
||||
flexWrap: "wrap",
|
||||
flexDirection: "column",
|
||||
width: "322px",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: '10px',
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
title={
|
||||
<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>
|
||||
Based on the latest 20 trades
|
||||
</span>
|
||||
}
|
||||
placement="bottom"
|
||||
arrow
|
||||
sx={{ fontSize: "24" }}
|
||||
slotProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
color: "#ffffff",
|
||||
backgroundColor: "#444444",
|
||||
},
|
||||
},
|
||||
arrow: {
|
||||
sx: {
|
||||
color: "#444444",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "322px",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: "10px",
|
||||
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<Typography sx={{
|
||||
fontSize: "1rem",
|
||||
fontWeight: 'bold'
|
||||
}}>Supply</Typography>
|
||||
{!supply ? (
|
||||
<BarSpinner width="16px" color="white" />
|
||||
): (
|
||||
<Typography sx={{
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "1rem",
|
||||
}}>{supply} QORT</Typography>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
<Box sx={{
|
||||
width: "322px",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: '10px',
|
||||
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<Typography sx={{
|
||||
fontSize: "1rem",
|
||||
fontWeight: 'bold'
|
||||
}}>Last height</Typography>
|
||||
{!lastBlock?.height ? (
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
Price
|
||||
</Typography>
|
||||
{!ltcPerQort ? (
|
||||
<BarSpinner width="16px" color="white" />
|
||||
): (
|
||||
<Typography sx={{
|
||||
fontSize: "1rem",
|
||||
}}>{lastBlock?.height}</Typography>
|
||||
|
||||
) : (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "1rem",
|
||||
}}
|
||||
>
|
||||
{ltcPerQort} LTC/QORT
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Box
|
||||
sx={{
|
||||
width: "322px",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: "10px",
|
||||
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "1rem",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
Supply
|
||||
</Typography>
|
||||
{!supply ? (
|
||||
<BarSpinner width="16px" color="white" />
|
||||
) : (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "1rem",
|
||||
}}
|
||||
>
|
||||
{supply} QORT
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Tooltip
|
||||
title={
|
||||
<span style={{ color: "white", fontSize: "14px", fontWeight: 700 }}>
|
||||
{lastBlock?.timestamp && formatDate(lastBlock?.timestamp)}
|
||||
</span>
|
||||
}
|
||||
placement="bottom"
|
||||
arrow
|
||||
sx={{ fontSize: "24" }}
|
||||
slotProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
color: "#ffffff",
|
||||
backgroundColor: "#444444",
|
||||
},
|
||||
},
|
||||
arrow: {
|
||||
sx: {
|
||||
color: "#444444",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: "322px",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: "10px",
|
||||
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "1rem",
|
||||
fontWeight: "bold",
|
||||
}}
|
||||
>
|
||||
Last height
|
||||
</Typography>
|
||||
{!lastBlock?.height ? (
|
||||
<BarSpinner width="16px" color="white" />
|
||||
) : (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: "1rem",
|
||||
}}
|
||||
>
|
||||
{lastBlock?.height}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
495
src/components/UserLookup.tsx/UserLookup.tsx
Normal file
495
src/components/UserLookup.tsx/UserLookup.tsx
Normal file
@ -0,0 +1,495 @@
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { DrawerUserLookup } from "../Drawer/DrawerUserLookup";
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
ButtonBase,
|
||||
Card,
|
||||
Divider,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography,
|
||||
Table,
|
||||
CircularProgress,
|
||||
} from "@mui/material";
|
||||
import { getAddressInfo, getNameOrAddress } from "../../background";
|
||||
import { getBaseApiReact } from "../../App";
|
||||
import { getNameInfo } from "../Group/Group";
|
||||
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
|
||||
import { Spacer } from "../../common/Spacer";
|
||||
import { formatTimestamp } from "../../utils/time";
|
||||
import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import { executeEvent, subscribeToEvent, unsubscribeFromEvent } from "../../utils/events";
|
||||
|
||||
function formatAddress(str) {
|
||||
if (str.length <= 12) return str;
|
||||
|
||||
const first6 = str.slice(0, 6);
|
||||
const last6 = str.slice(-6);
|
||||
|
||||
return `${first6}....${last6}`;
|
||||
}
|
||||
|
||||
export const UserLookup = ({ isOpenDrawerLookup, setIsOpenDrawerLookup }) => {
|
||||
const [nameOrAddress, setNameOrAddress] = useState("");
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const [addressInfo, setAddressInfo] = useState(null);
|
||||
const [isLoadingUser, setIsLoadingUser] = useState(false);
|
||||
const [isLoadingPayments, setIsLoadingPayments] = useState(false);
|
||||
const [payments, setPayments] = useState([]);
|
||||
const lookupFunc = useCallback(async (messageAddressOrName) => {
|
||||
try {
|
||||
setErrorMessage('')
|
||||
setIsLoadingUser(true)
|
||||
setPayments([])
|
||||
setAddressInfo(null)
|
||||
const inputAddressOrName = messageAddressOrName || nameOrAddress
|
||||
if (!inputAddressOrName?.trim())
|
||||
throw new Error("Please insert a name or address");
|
||||
const owner = await getNameOrAddress(inputAddressOrName);
|
||||
if (!owner) throw new Error("Name does not exist");
|
||||
const addressInfoRes = await getAddressInfo(owner);
|
||||
if (!addressInfoRes?.publicKey) {
|
||||
throw new Error("Address does not exist on blockchain");
|
||||
}
|
||||
const name = await getNameInfo(owner);
|
||||
const balanceRes = await fetch(
|
||||
`${getBaseApiReact()}/addresses/balance/${owner}`
|
||||
);
|
||||
const balanceData = await balanceRes.json();
|
||||
setAddressInfo({
|
||||
...addressInfoRes,
|
||||
balance: balanceData,
|
||||
name,
|
||||
});
|
||||
setIsLoadingUser(false)
|
||||
setIsLoadingPayments(true)
|
||||
|
||||
const getPayments = await fetch(
|
||||
`${getBaseApiReact()}/transactions/search?txType=PAYMENT&address=${owner}&confirmationStatus=CONFIRMED&limit=20&reverse=true`
|
||||
);
|
||||
const paymentsData = await getPayments.json();
|
||||
setPayments(paymentsData);
|
||||
|
||||
} catch (error) {
|
||||
setErrorMessage(error?.message)
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsLoadingUser(false)
|
||||
setIsLoadingPayments(false)
|
||||
}
|
||||
}, [nameOrAddress]);
|
||||
|
||||
const openUserLookupDrawerFunc = useCallback((e) => {
|
||||
setIsOpenDrawerLookup(true)
|
||||
const message = e.detail?.addressOrName;
|
||||
if(message){
|
||||
lookupFunc(message)
|
||||
}
|
||||
}, [lookupFunc, setIsOpenDrawerLookup]);
|
||||
|
||||
useEffect(() => {
|
||||
subscribeToEvent("openUserLookupDrawer", openUserLookupDrawerFunc);
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEvent("openUserLookupDrawer", openUserLookupDrawerFunc);
|
||||
};
|
||||
}, [openUserLookupDrawerFunc]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<DrawerUserLookup open={isOpenDrawerLookup} setOpen={setIsOpenDrawerLookup}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
padding: "15px",
|
||||
height: "100vh",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "5px",
|
||||
alignItems: "center",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
value={nameOrAddress}
|
||||
onChange={(e) => setNameOrAddress(e.target.value)}
|
||||
size="small"
|
||||
placeholder="Address or Name"
|
||||
autoComplete="off"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && nameOrAddress) {
|
||||
lookupFunc();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ButtonBase onClick={lookupFunc} >
|
||||
<SearchIcon sx={{
|
||||
color: 'white',
|
||||
marginRight: '20px'
|
||||
}} />
|
||||
</ButtonBase>
|
||||
<ButtonBase sx={{
|
||||
marginLeft: 'auto',
|
||||
|
||||
}} onClick={()=> {
|
||||
setIsOpenDrawerLookup(false)
|
||||
}}>
|
||||
<CloseFullscreenIcon sx={{
|
||||
color: 'white'
|
||||
}} />
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
flexGrow: 1,
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{!isLoadingUser && errorMessage && (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
justifyContent: 'center',
|
||||
marginTop: '40px'
|
||||
}}>
|
||||
<Typography>{errorMessage}</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{isLoadingUser && (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
justifyContent: 'center',
|
||||
marginTop: '40px'
|
||||
}}>
|
||||
<CircularProgress sx={{
|
||||
color: 'white'
|
||||
}} />
|
||||
</Box>
|
||||
)}
|
||||
{!isLoadingUser && addressInfo && (
|
||||
<>
|
||||
|
||||
<Spacer height="30px" />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
flexWrap: "wrap",
|
||||
flexDirection: "row",
|
||||
width: "100%",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
sx={{
|
||||
padding: "15px",
|
||||
minWidth: "320px",
|
||||
alignItems: "center",
|
||||
minHeight: "200px",
|
||||
background: "var(--bg-primary)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{addressInfo?.name ?? "Name not registered"}
|
||||
</Typography>
|
||||
<Spacer height="20px" />
|
||||
<Divider>
|
||||
{addressInfo?.name ? (
|
||||
<Avatar
|
||||
sx={{
|
||||
height: "50px",
|
||||
width: "50px",
|
||||
"& img": {
|
||||
objectFit: "fill",
|
||||
},
|
||||
}}
|
||||
alt={addressInfo?.name}
|
||||
src={`${getBaseApiReact()}/arbitrary/THUMBNAIL/${
|
||||
addressInfo?.name
|
||||
}/qortal_avatar?async=true`}
|
||||
>
|
||||
<AccountCircleIcon
|
||||
sx={{
|
||||
fontSize: "50px",
|
||||
}}
|
||||
/>
|
||||
</Avatar>
|
||||
) : (
|
||||
<AccountCircleIcon
|
||||
sx={{
|
||||
fontSize: "50px",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Divider>
|
||||
<Spacer height="20px" />
|
||||
<Typography
|
||||
sx={{
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Level {addressInfo?.level}
|
||||
</Typography>
|
||||
</Card>
|
||||
<Card
|
||||
sx={{
|
||||
padding: "15px",
|
||||
minWidth: "320px",
|
||||
minHeight: "200px",
|
||||
gap: "20px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
background: "var(--bg-primary)",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Typography>Address</Typography>
|
||||
</Box>
|
||||
<Tooltip
|
||||
title={
|
||||
<span
|
||||
style={{
|
||||
color: "white",
|
||||
fontSize: "14px",
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
copy address
|
||||
</span>
|
||||
}
|
||||
placement="bottom"
|
||||
arrow
|
||||
sx={{ fontSize: "24" }}
|
||||
slotProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
color: "#ffffff",
|
||||
backgroundColor: "#444444",
|
||||
},
|
||||
},
|
||||
arrow: {
|
||||
sx: {
|
||||
color: "#444444",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(addressInfo?.address);
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
textAlign: "end",
|
||||
}}
|
||||
>
|
||||
{addressInfo?.address}
|
||||
</Typography>
|
||||
</ButtonBase>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: "20px",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Typography>Balance</Typography>
|
||||
<Typography>{addressInfo?.balance}</Typography>
|
||||
</Box>
|
||||
<Spacer height="20px" />
|
||||
<Button variant="contained" onClick={()=> {
|
||||
executeEvent('openPaymentInternal', {
|
||||
address: addressInfo?.address,
|
||||
name: addressInfo?.name,
|
||||
});
|
||||
}}>Send QORT</Button>
|
||||
</Card>
|
||||
</Box>
|
||||
|
||||
|
||||
</>
|
||||
)}
|
||||
<Spacer height="40px" />
|
||||
{isLoadingPayments && (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<CircularProgress sx={{
|
||||
color: 'white'
|
||||
}} />
|
||||
</Box>
|
||||
)}
|
||||
{!isLoadingPayments && addressInfo && (
|
||||
<Card
|
||||
sx={{
|
||||
padding: "15px",
|
||||
overflow: "auto",
|
||||
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
background: "var(--bg-primary)",
|
||||
}}
|
||||
>
|
||||
<Typography>20 most recent payments</Typography>
|
||||
<Spacer height="20px" />
|
||||
{!isLoadingPayments && payments?.length === 0 && (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
<Typography>No payments</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Sender</TableCell>
|
||||
<TableCell>Reciver</TableCell>
|
||||
<TableCell>Amount</TableCell>
|
||||
<TableCell>Time</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{payments.map((payment, index) => (
|
||||
<TableRow key={payment?.signature}>
|
||||
<TableCell>
|
||||
<Tooltip
|
||||
title={
|
||||
<span
|
||||
style={{
|
||||
color: "white",
|
||||
fontSize: "14px",
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
copy address
|
||||
</span>
|
||||
}
|
||||
placement="bottom"
|
||||
arrow
|
||||
sx={{ fontSize: "24" }}
|
||||
slotProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
color: "#ffffff",
|
||||
backgroundColor: "#444444",
|
||||
},
|
||||
},
|
||||
arrow: {
|
||||
sx: {
|
||||
color: "#444444",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(
|
||||
payment?.creatorAddress
|
||||
);
|
||||
}}
|
||||
>
|
||||
{formatAddress(payment?.creatorAddress)}
|
||||
</ButtonBase>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Tooltip
|
||||
title={
|
||||
<span
|
||||
style={{
|
||||
color: "white",
|
||||
fontSize: "14px",
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
copy address
|
||||
</span>
|
||||
}
|
||||
placement="bottom"
|
||||
arrow
|
||||
sx={{ fontSize: "24" }}
|
||||
slotProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
color: "#ffffff",
|
||||
backgroundColor: "#444444",
|
||||
},
|
||||
},
|
||||
arrow: {
|
||||
sx: {
|
||||
color: "#444444",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(payment?.recipient);
|
||||
}}
|
||||
>
|
||||
{formatAddress(payment?.recipient)}
|
||||
|
||||
</ButtonBase>
|
||||
</Tooltip>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{payment?.amount}
|
||||
</TableCell>
|
||||
<TableCell>{formatTimestamp(payment?.timestamp)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
</Box>
|
||||
</DrawerUserLookup>
|
||||
);
|
||||
};
|
@ -121,6 +121,42 @@ export const WrapperUserAction = ({ children, address, name, disabled }) => {
|
||||
>
|
||||
Copy address
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
executeEvent('openPaymentInternal', {
|
||||
address,
|
||||
name,
|
||||
});
|
||||
handleClose();
|
||||
|
||||
}}
|
||||
sx={{
|
||||
color: 'white',
|
||||
justifyContent: 'flex-start'
|
||||
}}
|
||||
>
|
||||
Send QORT
|
||||
</Button>
|
||||
<Button
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
executeEvent('openUserLookupDrawer', {
|
||||
addressOrName: name || address
|
||||
})
|
||||
handleClose();
|
||||
|
||||
}}
|
||||
sx={{
|
||||
color: 'white',
|
||||
justifyContent: 'flex-start'
|
||||
}}
|
||||
>
|
||||
User lookup
|
||||
</Button>
|
||||
|
||||
|
||||
<BlockUser handleClose={handleClose} address={address} name={name} />
|
||||
</Box>
|
||||
</Popover>
|
||||
|
@ -12,7 +12,7 @@ export function formatTimestamp(timestamp: number): string {
|
||||
} else if (elapsedTime < 1440) {
|
||||
return `${Math.floor(elapsedTime / 60)}h ago`
|
||||
} else {
|
||||
return timestampMoment.format('MMM D')
|
||||
return timestampMoment.format('MMM D, YYYY')
|
||||
}
|
||||
}
|
||||
export function formatTimestampForum(timestamp: number): string {
|
||||
|
Loading…
x
Reference in New Issue
Block a user