From 202ca37745ef2ff7c8a9891aee1c5c3cd58cdc95 Mon Sep 17 00:00:00 2001 From: Yana Date: Mon, 16 Jun 2025 20:57:20 +0300 Subject: [PATCH] refactor EnvironmentProvider, add environment to NodesTable --- explorer-v2/src/app/api/index.tsx | 29 ++++--- .../src/components/header/Environment.tsx | 1 - .../components/nodeTable/AdvancedFilters.tsx | 79 +++++++++++++------ .../nodeTable/NodeTableWithAction.tsx | 8 +- .../toggleButton/NodeFilterButtonGroup.tsx | 4 +- .../src/providers/EnvironmentProvider.tsx | 55 +++++++------ 6 files changed, 113 insertions(+), 63 deletions(-) diff --git a/explorer-v2/src/app/api/index.tsx b/explorer-v2/src/app/api/index.tsx index da48ce8095..b8d49569da 100644 --- a/explorer-v2/src/app/api/index.tsx +++ b/explorer-v2/src/app/api/index.tsx @@ -22,6 +22,7 @@ import { NYM_ACCOUNT_ADDRESS, NYM_PRICES_API, OBSERVATORY_GATEWAYS_URL, + SANDBOX_NS_API_NODES, } from "./urls"; // Fetch function for epoch rewards @@ -118,7 +119,9 @@ export const fetchBalances = async (address: string): Promise => { }; // Fetch function to get total staker rewards -export const fetchTotalStakerRewards = async (address: string): Promise => { +export const fetchTotalStakerRewards = async ( + address: string +): Promise => { const response = await fetch(`${DATA_OBSERVATORY_BALANCES_URL}/${address}`, { headers: { Accept: "application/json", @@ -205,8 +208,13 @@ export const fetchNymPrice = async (): Promise => { return data; }; -export const fetchNSApiNodes = async (): Promise => { - if (!NS_API_NODES) { +export const fetchNSApiNodes = async ( + environment: Environment +): Promise => { + const baseUrl = + environment === "sandbox" ? SANDBOX_NS_API_NODES : NS_API_NODES; + + if (!baseUrl) { throw new Error("NS_API_NODES URL is not defined"); } @@ -217,15 +225,12 @@ export const fetchNSApiNodes = async (): Promise => { let hasMoreData = true; while (hasMoreData) { - const response = await fetch( - `${NS_API_NODES}?page=${page}&size=${PAGE_SIZE}`, - { - headers: { - Accept: "application/json", - "Content-Type": "application/json; charset=utf-8", - }, - } - ); + const response = await fetch(`${baseUrl}?page=${page}&size=${PAGE_SIZE}`, { + headers: { + Accept: "application/json", + "Content-Type": "application/json; charset=utf-8", + }, + }); if (!response.ok) { throw new Error( diff --git a/explorer-v2/src/components/header/Environment.tsx b/explorer-v2/src/components/header/Environment.tsx index 4bd7b35f0e..bef7a6978c 100644 --- a/explorer-v2/src/components/header/Environment.tsx +++ b/explorer-v2/src/components/header/Environment.tsx @@ -8,7 +8,6 @@ export const Environment: React.FC = () => { const theme = useTheme(); const { environment, setEnvironment } = useEnvironment(); - console.log("environment", environment); const explorerName = environment ? `${environment} Explorer` diff --git a/explorer-v2/src/components/nodeTable/AdvancedFilters.tsx b/explorer-v2/src/components/nodeTable/AdvancedFilters.tsx index 70858a0825..fe24dc961e 100644 --- a/explorer-v2/src/components/nodeTable/AdvancedFilters.tsx +++ b/explorer-v2/src/components/nodeTable/AdvancedFilters.tsx @@ -1,5 +1,5 @@ "use client"; -import React from "react"; +import React, { useEffect } from "react"; import { Box, Button, @@ -14,7 +14,9 @@ import FilterAltIcon from "@mui/icons-material/FilterAlt"; import AccessTimeIcon from "@mui/icons-material/AccessTime"; import PieChartIcon from "@mui/icons-material/PieChart"; import PercentIcon from "@mui/icons-material/Percent"; -import NodeFilterButtonGroup from "../toggleButton/NodeFilterButtonGroup"; +import NodeFilterButtonGroup, { + type Option, +} from "../toggleButton/NodeFilterButtonGroup"; import { RECOMMENDED_NODES } from "@/app/constants"; type AdvancedFiltersProps = { @@ -36,6 +38,7 @@ type AdvancedFiltersProps = { mixnodes: number; gateways: number; }; + environment: "mainnet" | "sandbox"; }; export default function AdvancedFilters({ @@ -51,6 +54,7 @@ export default function AdvancedFilters({ activeFilter, setActiveFilter, nodeCounts, + environment, }: AdvancedFiltersProps) { const theme = useTheme(); const green = "#14e76f"; // from theme colours @@ -248,6 +252,54 @@ export default function AdvancedFilters({ ); + const filterOptions = + environment === "mainnet" + ? [ + { + label: `Recommended servers (${RECOMMENDED_NODES.length})`, + isSelected: activeFilter === "recommended", + value: "recommended" as const, + }, + { + label: `All servers (${nodeCounts.all})`, + isSelected: activeFilter === "all", + value: "all" as const, + }, + { + label: `Mixnodes (${nodeCounts.mixnodes})`, + isSelected: activeFilter === "mixnodes", + value: "mixnodes" as const, + }, + { + label: `Gateways (${nodeCounts.gateways})`, + isSelected: activeFilter === "gateways", + value: "gateways" as const, + }, + ] + : [ + { + label: `All servers (${nodeCounts.all})`, + isSelected: activeFilter === "all", + value: "all" as const, + }, + { + label: `Mixnodes (${nodeCounts.mixnodes})`, + isSelected: activeFilter === "mixnodes", + value: "mixnodes" as const, + }, + { + label: `Gateways (${nodeCounts.gateways})`, + isSelected: activeFilter === "gateways", + value: "gateways" as const, + }, + ]; + + useEffect(() => { + if (environment === "sandbox" && activeFilter === "recommended") { + setActiveFilter("all"); + } + }, [environment, activeFilter, setActiveFilter]); + return ( diff --git a/explorer-v2/src/components/nodeTable/NodeTableWithAction.tsx b/explorer-v2/src/components/nodeTable/NodeTableWithAction.tsx index cff26e76a3..d5bf7a128a 100644 --- a/explorer-v2/src/components/nodeTable/NodeTableWithAction.tsx +++ b/explorer-v2/src/components/nodeTable/NodeTableWithAction.tsx @@ -10,6 +10,7 @@ import NodeTable from "./NodeTable"; import { useState, useEffect } from "react"; import AdvancedFilters from "./AdvancedFilters"; import { RECOMMENDED_NODES } from "@/app/constants"; +import { useEnvironment } from "@/providers/EnvironmentProvider"; // Utility function to calculate node saturation point function getNodeSaturationPoint( @@ -114,6 +115,8 @@ const NodeTableWithAction = () => { return stored ? JSON.parse(stored) : false; }); + const { environment } = useEnvironment(); + // Wrapper functions to handle filter changes and sessionStorage const handleActiveFilterChange = ( newFilter: "all" | "mixnodes" | "gateways" | "recommended" @@ -171,8 +174,8 @@ const NodeTableWithAction = () => { isLoading: isNSApiNodesLoading, isError: isNSApiNodesError, } = useQuery({ - queryKey: ["nsApiNodes"], - queryFn: fetchNSApiNodes, + queryKey: ["nsApiNodes", environment], + queryFn: () => fetchNSApiNodes(environment), staleTime: 10 * 60 * 1000, // 10 minutes refetchOnWindowFocus: false, // Prevents unnecessary refetching refetchOnReconnect: false, @@ -290,6 +293,7 @@ const NodeTableWithAction = () => { activeFilter={activeFilter} setActiveFilter={handleActiveFilterChange} nodeCounts={nodeCounts} + environment={environment} /> diff --git a/explorer-v2/src/components/toggleButton/NodeFilterButtonGroup.tsx b/explorer-v2/src/components/toggleButton/NodeFilterButtonGroup.tsx index 8789acc9bc..16a08df327 100644 --- a/explorer-v2/src/components/toggleButton/NodeFilterButtonGroup.tsx +++ b/explorer-v2/src/components/toggleButton/NodeFilterButtonGroup.tsx @@ -1,13 +1,13 @@ "use client"; import { Button, ButtonGroup, Stack } from "@mui/material"; -type Option = { +export type Option = { label: string; isSelected: boolean; value: "all" | "mixnodes" | "gateways" | "recommended"; }; -type Options = [Option, Option, Option, Option]; +type Options = Option[]; const NodeFilterButtonGroup = ({ size = "small", diff --git a/explorer-v2/src/providers/EnvironmentProvider.tsx b/explorer-v2/src/providers/EnvironmentProvider.tsx index f8766f90cf..1ef69de321 100644 --- a/explorer-v2/src/providers/EnvironmentProvider.tsx +++ b/explorer-v2/src/providers/EnvironmentProvider.tsx @@ -1,5 +1,6 @@ "use client"; -import React, { createContext, useContext, useEffect, useState } from "react"; +import React, { createContext, useContext } from "react"; +import dynamic from "next/dynamic"; type Environment = "mainnet" | "sandbox"; @@ -14,30 +15,40 @@ const EnvironmentContext = createContext( const ENVIRONMENT_STORAGE_KEY = "environment"; +const ClientStorage = dynamic( + () => + import("@uidotdev/usehooks").then((mod) => { + const { useLocalStorage } = mod; + return function ClientStorageComponent({ + children, + }: { + children: React.ReactNode; + }) { + const [stored, setStored] = useLocalStorage<{ env: Environment }>( + ENVIRONMENT_STORAGE_KEY, + { env: "mainnet" } + ); + + const setEnvironment = (env: Environment) => { + setStored({ env }); + }; + + return ( + + {children} + + ); + }; + }), + { ssr: false } +); + export const EnvironmentProvider: React.FC<{ children: React.ReactNode }> = ({ children, }) => { - const [environment, setEnvironmentState] = useState(() => { - // Try to get the environment from localStorage on initial load - const storedEnv = localStorage.getItem(ENVIRONMENT_STORAGE_KEY); - return (storedEnv as Environment) || "mainnet"; - }); - - const setEnvironment = (env: Environment) => { - setEnvironmentState(env); - localStorage.setItem(ENVIRONMENT_STORAGE_KEY, env); - }; - - // Update localStorage when environment changes - useEffect(() => { - localStorage.setItem(ENVIRONMENT_STORAGE_KEY, environment); - }, [environment]); - - return ( - - {children} - - ); + return {children}; }; export const useEnvironment = () => {