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:
Yana
2025-03-20 20:09:40 +02:00
parent 84db9f6bcd
commit 7ca2559f99
8 changed files with 205 additions and 60 deletions
+6 -2
View File
@@ -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>
+2 -15
View File
@@ -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>
)}