Add caching on tanstack queries
clean up Another try clean up fix build fix build fix build fix build Refactor Node page to accept identity_key in params fix build fix build fix buggy data on landing page graphs Try fix gas fee for redeem all rewards Another try to fix gas fee for redeem rewards Add fees "auto" to the cosmWasm client with offline signer comment out unused option add getOfflineSigner dependency to the callback fn comment out for good clean up, optimise homepage layout Dark theme fix build fix build add fixes Rebase onto develop, fix lint error fix build Fix tooltip Fix switch button on mobile header fix build clean up fix build Fix switch component fix build Add moniker to Magic Search, fix tooltip hover on landing page refactor urls fix build edit placeholder Fix styles fix error message
This commit is contained in:
@@ -2,8 +2,7 @@ export const CURRENT_EPOCH =
|
||||
"https://validator.nymtech.net/api/v1/epoch/current";
|
||||
export const CURRENT_EPOCH_REWARDS =
|
||||
"https://validator.nymtech.net/api/v1/epoch/reward_params";
|
||||
export const NYM_NODE_BONDED =
|
||||
"https://validator.nymtech.net/api/v1/nym-nodes/bonded";
|
||||
|
||||
export const NYM_ACCOUNT_ADDRESS =
|
||||
"https://explorer.nymtech.net/api/v1/tmp/unstable/account";
|
||||
export const NYM_PRICES_API = "https://api.nym.spectredao.net/api/v1/nym-price";
|
||||
@@ -18,3 +17,8 @@ export const DATA_OBSERVATORY_BALANCES_URL =
|
||||
"https://api.nym.spectredao.net/api/v1/balances";
|
||||
export const OBSERVATORY_GATEWAYS_URL =
|
||||
"https://mainnet-node-status-api.nymtech.cc/v2/gateways";
|
||||
|
||||
export const NS_API_MIXNODES_STATS =
|
||||
process.env.NEXT_PUBLIC_NS_API_MIXNODES_STATS;
|
||||
|
||||
export const NS_API_NODES = process.env.NEXT_PUBLIC_NS_API_NODES;
|
||||
|
||||
@@ -20,6 +20,7 @@ const CopyToClipboard = ({
|
||||
const [copiedText, setCopiedText] = useCopyToClipboard();
|
||||
const hasCopied = Boolean(copiedText);
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
|
||||
useEffect(() => {
|
||||
if (hasCopied) {
|
||||
@@ -33,7 +34,11 @@ const CopyToClipboard = ({
|
||||
|
||||
if (hasCopied) {
|
||||
return (
|
||||
<Typography sx={{ color: "pine.950" }} variant="h6" color="textSecondary">
|
||||
<Typography
|
||||
sx={{ color: isDarkMode ? "base.white" : "pine.950" }}
|
||||
variant="h6"
|
||||
color="textSecondary"
|
||||
>
|
||||
Copied
|
||||
</Typography>
|
||||
);
|
||||
@@ -41,8 +46,7 @@ const CopyToClipboard = ({
|
||||
|
||||
return (
|
||||
<IconButton size={size} onClick={() => setCopiedText(text)}>
|
||||
{Icon ||
|
||||
(theme.palette.mode === "dark" ? <CopyFileDark /> : <CopyFile />)}
|
||||
{Icon || (isDarkMode ? <CopyFileDark /> : <CopyFile />)}
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -51,7 +51,7 @@ export const DesktopHeader = () => {
|
||||
))}
|
||||
</Box>
|
||||
<ConnectWallet size="small" />
|
||||
<DarkLightSwitchDesktop defaultChecked />
|
||||
<DarkLightSwitchDesktop />
|
||||
</Wrapper>
|
||||
<Divider variant="fullWidth" sx={{ width: "100%" }} />
|
||||
</Box>
|
||||
|
||||
@@ -162,7 +162,7 @@ const MobileMenuHeader = ({
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{!drawerOpen && <DarkLightSwitchDesktop defaultChecked />}
|
||||
{!drawerOpen && <DarkLightSwitchDesktop />}
|
||||
<IconButton sx={{}} onClick={() => toggleDrawer(!drawerOpen)}>
|
||||
{drawerOpen ? <CloseIcon /> : <MenuIcon />}
|
||||
</IconButton>
|
||||
|
||||
@@ -52,27 +52,14 @@ export const DarkLightSwitch = styled(Switch)(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// export const DarkLightSwitchMobile: FCWithChildren = () => {
|
||||
// const { toggleMode } = useMainContext();
|
||||
// return (
|
||||
// <Button onClick={() => toggleMode()} data-testid="switch-button" sx={{ p: 0, minWidth: 0 }}>
|
||||
// <LightSwitchSVG />
|
||||
// </Button>
|
||||
// );
|
||||
// };
|
||||
|
||||
export const DarkLightSwitchDesktop: FCWithChildren<{
|
||||
defaultChecked: boolean;
|
||||
}> = ({ defaultChecked }) => {
|
||||
export const DarkLightSwitchDesktop = (): JSX.Element => {
|
||||
const [mode, setMode] = useLocalStorage<PaletteMode>("mode", "dark");
|
||||
|
||||
console.log("mode", mode);
|
||||
|
||||
const toggleMode = () => setMode((m) => (m !== "light" ? "light" : "dark"));
|
||||
|
||||
return (
|
||||
<Button onClick={() => toggleMode()} data-testid="switch-button">
|
||||
<DarkLightSwitch defaultChecked={defaultChecked} />
|
||||
<DarkLightSwitch checked={mode === "dark"} />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import type { IPacketsAndStakingData } from "../../app/api/types";
|
||||
import { formatBigNum } from "../../utils/formatBigNumbers";
|
||||
import ExplorerCard from "../cards/ExplorerCard";
|
||||
@@ -19,6 +20,8 @@ import { UpDownPriceIndicator } from "../price/UpDownPriceIndicator";
|
||||
export const NoiseCard = () => {
|
||||
const theme = useTheme();
|
||||
const isDarkMode = theme.palette.mode === "dark";
|
||||
const [tooltipOpen, setTooltipOpen] = useState(false);
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["noise"],
|
||||
queryFn: fetchNoise,
|
||||
@@ -110,6 +113,14 @@ export const NoiseCard = () => {
|
||||
})
|
||||
.filter((item) => item.numericData >= 2_500_000_000);
|
||||
|
||||
const handleTooltipOpen = () => {
|
||||
setTooltipOpen(true);
|
||||
};
|
||||
|
||||
const handleTooltipClose = () => {
|
||||
setTooltipOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<ExplorerCard label="Mixnet traffic" sx={{ height: "100%" }}>
|
||||
<Box display={"flex"} gap={2} flexDirection={{ xs: "column", sm: "row" }}>
|
||||
@@ -124,11 +135,22 @@ export const NoiseCard = () => {
|
||||
{noiseLast24HFormatted}
|
||||
</Typography>
|
||||
<Tooltip
|
||||
placement="left"
|
||||
placement="bottom"
|
||||
title={"Self reported noise volume"}
|
||||
open={tooltipOpen}
|
||||
onClose={handleTooltipClose}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
enterNextDelay={300}
|
||||
leaveDelay={200}
|
||||
>
|
||||
<Typography variant="h4" sx={{ color: "#8482FD", cursor: "pointer" }}>
|
||||
<Typography
|
||||
variant="h4"
|
||||
sx={{ color: "#8482FD", cursor: "pointer" }}
|
||||
onClick={handleTooltipOpen}
|
||||
onTouchStart={handleTooltipOpen}
|
||||
onMouseEnter={handleTooltipOpen}
|
||||
onMouseLeave={handleTooltipClose}
|
||||
>
|
||||
({formatedNoiseVolume})
|
||||
<InfoOutlinedIcon sx={{ fontSize: 16 }} />
|
||||
</Typography>
|
||||
|
||||
@@ -68,7 +68,7 @@ export const BasicInfoCard = ({ paramId }: Props) => {
|
||||
const selfBondFormatted = `${selfBond} NYM`;
|
||||
|
||||
return (
|
||||
<ExplorerCard label="Basic info">
|
||||
<ExplorerCard label="Basic info" sx={{ height: "100%" }}>
|
||||
<Stack gap={1}>
|
||||
<ExplorerListItem
|
||||
divider
|
||||
|
||||
@@ -1,17 +1,36 @@
|
||||
"use client";
|
||||
import type { NodeData } from "@/app/api/types";
|
||||
import type { IObservatoryNode } from "@/app/api/types";
|
||||
import { Search } from "@mui/icons-material";
|
||||
import { Button, CircularProgress, Stack, Typography } from "@mui/material";
|
||||
import {
|
||||
Autocomplete,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { NYM_NODE_BONDED } from "../../app/api/urls";
|
||||
import Input from "../input/Input";
|
||||
import { fetchObservatoryNodes } from "../../app/api";
|
||||
import { NYM_ACCOUNT_ADDRESS } from "@/app/api/urls";
|
||||
|
||||
const NodeAndAddressSearch = () => {
|
||||
const router = useRouter();
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [errorText, setErrorText] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [searchOptions, setSearchOptions] = useState<IObservatoryNode[]>([]);
|
||||
|
||||
// Use React Query to fetch nodes
|
||||
const { data: nymNodes = [], isLoading: isLoadingNodes } = useQuery({
|
||||
queryKey: ["nymNodes"],
|
||||
queryFn: fetchObservatoryNodes,
|
||||
staleTime: 10 * 60 * 1000, // 10 minutes
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnMount: false,
|
||||
});
|
||||
|
||||
const handleSearch = async () => {
|
||||
setErrorText(""); // Clear any previous error messages
|
||||
@@ -20,9 +39,7 @@ const NodeAndAddressSearch = () => {
|
||||
try {
|
||||
if (inputValue.startsWith("n1")) {
|
||||
// Fetch Nym Address data
|
||||
const response = await fetch(
|
||||
`https://explorer.nymtech.net/api/v1/tmp/unstable/account/${inputValue}`,
|
||||
);
|
||||
const response = await fetch(`${NYM_ACCOUNT_ADDRESS}/${inputValue}`);
|
||||
|
||||
if (response.ok) {
|
||||
try {
|
||||
@@ -33,7 +50,7 @@ const NodeAndAddressSearch = () => {
|
||||
}
|
||||
} catch {
|
||||
setErrorText(
|
||||
"It seems that this node or account does not exist. Please enter a complete Node ID or an existing Nym wallet address.",
|
||||
"No node found with the provided Name, Node ID or Identity Key. Please check your input and try again."
|
||||
);
|
||||
setIsLoading(false); // Stop loading
|
||||
|
||||
@@ -41,56 +58,156 @@ const NodeAndAddressSearch = () => {
|
||||
}
|
||||
} else {
|
||||
setErrorText(
|
||||
"It seems that this node or account does not exist. Please enter a complete Node ID or an existing Nym wallet address.",
|
||||
"No node found with the provided Name, Node ID or Identity Key. Please check your input and try again."
|
||||
);
|
||||
setIsLoading(false); // Stop loading
|
||||
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Fetch Nym Nodes data
|
||||
const response = await fetch(NYM_NODE_BONDED);
|
||||
|
||||
if (response.ok) {
|
||||
const nodes = await response.json();
|
||||
const matchingNode = nodes.data.find(
|
||||
(node: NodeData) =>
|
||||
node.bond_information.node.identity_key === inputValue,
|
||||
// Check if it's a node identity key
|
||||
if (nymNodes) {
|
||||
const matchingNode = nymNodes.find(
|
||||
(node) => node.identity_key === inputValue
|
||||
);
|
||||
|
||||
if (matchingNode) {
|
||||
router.push(`/nym-node/${matchingNode.bond_information.node_id}`);
|
||||
router.push(`/nym-node/${matchingNode.identity_key}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setErrorText(
|
||||
"It seems that this node or account does not exist. Please enter a complete Node ID or an existing Nym wallet address.",
|
||||
"No node found with the provided Name, Node ID or Identity Key. Please check your input and try again."
|
||||
);
|
||||
setIsLoading(false); // Stop loading
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorText(
|
||||
"It seems that this node or account does not exist. Please enter a complete Node ID or an existing Nym wallet address.",
|
||||
"No node found with the provided Name, Node ID or Identity Key. Please check your input and try again."
|
||||
);
|
||||
console.error(error);
|
||||
setIsLoading(false); // Stop loading
|
||||
}
|
||||
};
|
||||
|
||||
// Handle search input change
|
||||
const handleSearchInputChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const value = event.target.value;
|
||||
setInputValue(value);
|
||||
|
||||
// Clear error message when input is empty
|
||||
if (!value.trim()) {
|
||||
setErrorText("");
|
||||
}
|
||||
|
||||
// Filter nodes by moniker if input is not empty
|
||||
if (value.trim() !== "") {
|
||||
const filteredNodes = nymNodes.filter((node) =>
|
||||
node.self_description?.moniker
|
||||
?.toLowerCase()
|
||||
.includes(value.toLowerCase())
|
||||
);
|
||||
setSearchOptions(filteredNodes);
|
||||
} else {
|
||||
setSearchOptions([]);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle node selection from dropdown
|
||||
const handleNodeSelect = (
|
||||
event: React.SyntheticEvent,
|
||||
value: string | IObservatoryNode | null
|
||||
) => {
|
||||
if (value && typeof value !== "string") {
|
||||
setIsLoading(true); // Show loading spinner
|
||||
router.push(`/nym-node/${value.node_id}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack spacing={2} direction="column">
|
||||
<Stack spacing={1} direction="column">
|
||||
<Stack spacing={4} direction="row">
|
||||
<Input
|
||||
placeholder="Node Identity Key / Nym Address"
|
||||
fullWidth
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSearch();
|
||||
}
|
||||
<Autocomplete
|
||||
freeSolo
|
||||
options={searchOptions}
|
||||
getOptionLabel={(option: string | IObservatoryNode) => {
|
||||
if (typeof option === "string") return option;
|
||||
return option.self_description?.moniker || "";
|
||||
}}
|
||||
isOptionEqualToValue={(option, value) => {
|
||||
if (typeof option === "string" || typeof value === "string")
|
||||
return false;
|
||||
return option.node_id === value.node_id;
|
||||
}}
|
||||
renderOption={(props, option) => {
|
||||
if (typeof option === "string") return null;
|
||||
return (
|
||||
<li
|
||||
{...props}
|
||||
key={`${option.node_id}-${option.self_description?.moniker || ""}`}
|
||||
style={{ fontSize: "0.875rem" }}
|
||||
>
|
||||
{option.self_description?.moniker || "Unnamed Node"}
|
||||
</li>
|
||||
);
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
placeholder="Search by Node Name, Identity Key, or Nym Address"
|
||||
fullWidth
|
||||
onChange={handleSearchInputChange}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSearch();
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
"& .MuiOutlinedInput-root": {
|
||||
borderRadius: "28px",
|
||||
backgroundColor: "background.paper",
|
||||
"& fieldset": {
|
||||
borderColor: "divider",
|
||||
},
|
||||
"&:hover fieldset": {
|
||||
borderColor: "primary.main",
|
||||
},
|
||||
"&.Mui-focused fieldset": {
|
||||
borderColor: "primary.main",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
onChange={handleNodeSelect}
|
||||
loading={isLoadingNodes}
|
||||
loadingText="Loading nodes..."
|
||||
noOptionsText="No nodes found"
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: {
|
||||
marginTop: "4px",
|
||||
marginLeft: "10px",
|
||||
marginRight: "10px",
|
||||
width: "calc(100% - 20px)",
|
||||
borderRadius: "10px",
|
||||
"& .MuiAutocomplete-listbox": {
|
||||
padding: "8px 0",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
"& .MuiAutocomplete-popupIndicator": {
|
||||
color: "text.secondary",
|
||||
},
|
||||
"& .MuiAutocomplete-clearIndicator": {
|
||||
color: "text.secondary",
|
||||
},
|
||||
}}
|
||||
rounded
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -101,15 +218,26 @@ const NodeAndAddressSearch = () => {
|
||||
<Search />
|
||||
)
|
||||
}
|
||||
size="large"
|
||||
onClick={handleSearch}
|
||||
sx={{ height: "56px" }} // Match the TextField height
|
||||
disabled={isLoading}
|
||||
sx={{
|
||||
height: "56px",
|
||||
borderRadius: "28px",
|
||||
}}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
</Stack>
|
||||
{errorText && (
|
||||
<Typography color="error" variant="body4">
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="error"
|
||||
sx={{
|
||||
fontSize: "0.875rem",
|
||||
px: 2,
|
||||
display: "block",
|
||||
}}
|
||||
>
|
||||
{errorText}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user