From 3f9003883d50a316b69ecda150f1c57aba48e83b Mon Sep 17 00:00:00 2001 From: Uncle Fatso Date: Tue, 7 Apr 2026 14:57:56 +0300 Subject: [PATCH] localStorage as context provider added to the app Signed-off-by: Uncle Fatso --- package.json | 2 +- src/containers/Bond/BondModal.jsx | 31 ++++++++---- src/containers/Bridge/Bridge.jsx | 16 +++--- src/containers/Dex/Dex.jsx | 49 +++++++++---------- src/containers/Governance/NewProposal.jsx | 16 +++--- src/containers/Governance/ProposalDetails.jsx | 14 +++--- .../Governance/components/ProposalsList.jsx | 14 +++--- .../Stake/components/StakeInputArea.jsx | 39 ++++++--------- src/hooks/localstorage/index.jsx | 27 ++++++++++ src/main.jsx | 5 +- 10 files changed, 121 insertions(+), 92 deletions(-) create mode 100644 src/hooks/localstorage/index.jsx diff --git a/package.json b/package.json index d41779b..a47a7af 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ghost-dao-interface", "private": true, - "version": "0.6.18", + "version": "0.7.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src/containers/Bond/BondModal.jsx b/src/containers/Bond/BondModal.jsx index f759958..d5dd8f4 100644 --- a/src/containers/Bond/BondModal.jsx +++ b/src/containers/Bond/BondModal.jsx @@ -1,8 +1,9 @@ import { ArrowBack } from "@mui/icons-material"; import { Box, Link, Skeleton, Typography } from "@mui/material"; -import { useEffect, useState, useMemo } from "react"; +import { useEffect, useState, useMemo, useCallback } from "react"; import { Link as RouterLink, useLocation, useParams, useNavigate } from "react-router-dom"; import { useAccount, useSwitchChain } from "wagmi"; +import { isAddress } from "viem"; import ReactGA from "react-ga4"; import PageTitle from "../../components/PageTitle/PageTitle"; @@ -20,6 +21,7 @@ import { DecimalBigNumber } from "../../helpers/DecimalBigNumber"; import { NetworkId } from "../../constants"; import { formatCurrency } from "../../helpers"; +import { useLocalStorage } from "../../hooks/localstorage"; import { useLiveBonds } from "../../hooks/bonds"; import { useFtsoPrice } from "../../hooks/prices"; @@ -38,29 +40,38 @@ export const BondModal = ({ bond, chainId, address, connect }) => { const { network } = useParams(); const { pathname } = useLocation(); - const [slippage, setSlippage] = useState(localStorage.getItem("bond-slippage") || "10"); - const [formatDecimals, setFormatDecimals] = useState(localStorage.getItem("bond-decimals") || "5"); const [isSettingsOpen, setSettingsOpen] = useState(false); - const [recipientAddress, setRecipientAddress] = useState(address); + const { getStorageValue, setStorageValue } = useLocalStorage(); + + const [slippage, setSlippage] = useState(() => getStorageValue(chainId, address, "bond-slippage", "10")); + const [formatDecimals, setFormatDecimals] = useState(() => getStorageValue(chainId, address, "bond-decimals", "5")); + const [recipientAddress, setRecipientAddressInner] = useState(() => getStorageValue(chainId, address, "bond-recipient", address)); useEffect(() => { ReactGA.send({ hitType: "pageview", page: pathname }); }, []) - const setSlippageInner = (value) => { + const setRecipientAddress = useCallback((value) => { + setRecipientAddressInner(value); + if (isAddress(value)) { + setStorageValue(chainId, address, "bond-recipient", value); + } + }, [chainId, address]); + + const setSlippageInner = useCallback((value) => { const maybeValue = parseFloat(value); if (!maybeValue || parseFloat(value) <= 100) { setSlippage(value); - localStorage.setItem("bond-slippage", value); + setStorageValue(chainId, address, "bond-slippage", value); } - } + }, [chainId, address]); - const setFormatDecimalsInner = (value) => { + const setFormatDecimalsInner = useCallback((value) => { if (Number(value) <= 17) { setFormatDecimals(value); - localStorage.setItem("bond-decimals", value); + setStorageValue(chainId, address, "bond-decimals", value); } - } + }, [chainId, address]); useEffect(() => { const handleKeyDown = (event) => { diff --git a/src/containers/Bridge/Bridge.jsx b/src/containers/Bridge/Bridge.jsx index 4b00d2c..a03b17e 100644 --- a/src/containers/Bridge/Bridge.jsx +++ b/src/containers/Bridge/Bridge.jsx @@ -46,14 +46,13 @@ import { useLatestBlockNumber, useEraIndex, } from "../../hooks/ghost"; +import { useLocalStorage } from "../../hooks/localstorage"; import { ValidatorTable } from "./ValidatorTable"; import { BridgeModal, BridgeConfirmModal } from "./BridgeModal"; import { BridgeHeader } from "./BridgeHeader"; import { BridgeCardAction, BridgeCardHistory } from "./BridgeCard"; -const STORAGE_PREFIX = "storedTransactions" - const Bridge = ({ chainId, address, config, connect }) => { const isBigScreen = useMediaQuery("(max-width: 980px)") const isSmallScreen = useMediaQuery("(max-width: 650px)"); @@ -67,14 +66,15 @@ const Bridge = ({ chainId, address, config, connect }) => { const [bridgeAction, setBridgeAction] = useState(true); const [currentTime, setCurrentTime] = useState(Date.now()); + const { getStorageValue, setStorageValue } = useLocalStorage(); + useEffect(() => { const interval = setInterval(() => setCurrentTime(Date.now()), 1000); return () => clearInterval(interval); }, []); - const initialStoredTransactions = localStorage.getItem(STORAGE_PREFIX); - const [storedTransactions, setStoredTransactions] = useState( - initialStoredTransactions ? JSON.parse(initialStoredTransactions) : [] + const [storedTransactions, setStoredTransactions] = useState(() => + getStorageValue(chainId, address, "bridge-txs", []) ); const { gatekeeperAddress } = useGatekeeperAddress(chainId); @@ -281,7 +281,7 @@ const Bridge = ({ chainId, address, config, connect }) => { const removeStoredRecord = useCallback(() => { const newStoredTransactions = storedTransactions.filter((_, index) => index !== activeTxIndex) setStoredTransactions(newStoredTransactions); - localStorage.setItem(storagePrefix, JSON.stringify(newStoredTransactions)); + setStorageValue(chainId, address, "bridge-txs", newStoredTransactions); setActiveTxIndex(-1); }, [storedTransactions, activeTxIndex, setStoredTransactions, setActiveTxIndex]); @@ -307,8 +307,8 @@ const Bridge = ({ chainId, address, config, connect }) => { const newStoredTransactions = [transaction, ...storedTransactions]; setStoredTransactions(newStoredTransactions); - localStorage.setItem(STORAGE_PREFIX, JSON.stringify(newStoredTransactions)); - setActiveTxIndex(0) + setStorageValue(chainId, address, "bridge-txs", newStoredTransactions); + setActiveTxIndex(0); } return ( diff --git a/src/containers/Dex/Dex.jsx b/src/containers/Dex/Dex.jsx index 35602e3..9826fe5 100644 --- a/src/containers/Dex/Dex.jsx +++ b/src/containers/Dex/Dex.jsx @@ -10,9 +10,10 @@ import { useTheme, } from "@mui/material"; import SettingsIcon from '@mui/icons-material/Settings'; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState, useCallback } from "react"; import { useParams, useLocation, useSearchParams } from "react-router-dom"; import { Helmet } from "react-helmet"; +import { isAddress } from "viem"; import ReactGA from "react-ga4"; import InfoTooltip from "../../components/Tooltip/InfoTooltip"; @@ -31,6 +32,7 @@ import { EMPTY_ADDRESS, WETH_ADDRESSES, } from "../../constants/addresses"; +import { useLocalStorage } from "../../hooks/localstorage"; import { useTokenSymbol } from "../../hooks/tokens"; import { getTokenAddress } from "../../hooks/helpers"; @@ -43,6 +45,7 @@ const Dex = ({ chainId, address, connect, config }) => { const pathname = useParams(); const theme = useTheme(); + const { getStorageValue, setStorageValue } = useLocalStorage(); const isSmallScreen = useMediaQuery("(max-width: 650px)"); const isVerySmallScreen = useMediaQuery("(max-width: 379px)"); @@ -55,10 +58,10 @@ const Dex = ({ chainId, address, connect, config }) => { const [topTokenListOpen, setTopTokenListOpen] = useState(false); const [bottomTokenListOpen, setBottomTokenListOpen] = useState(false); - const [secondsToWait, setSecondsToWait] = useState(localStorage.getItem("dex-deadline") || "60"); - const [slippage, setSlippage] = useState(localStorage.getItem("dex-slippage") || "5"); - const [formatDecimals, setFormatDecimals] = useState(localStorage.getItem("dex-decimals") || "5"); - const [actualDestinationAddress, setActualDestinationAddress] = useState(localStorage.getItem("dex-destination")); + const [secondsToWait, setSecondsToWait] = useState(() => getStorageValue(chainId, address, "dex-deadline", "60")); + const [slippage, setSlippage] = useState(() => getStorageValue(chainId, address, "dex-slippage", "5")); + const [formatDecimals, setFormatDecimals] = useState(() => getStorageValue(chainId, address, "dex-decimals", "5")); + const [actualDestinationAddress, setActualDestinationAddress] = useState(() => getStorageValue(chainId, address, "dex-destination", address)); const [destinationAddress, setDestinationAddress] = useState(actualDestinationAddress); const [tokenAddressTop, setTokenAddressTop] = useState(EMPTY_ADDRESS); @@ -180,38 +183,34 @@ const Dex = ({ chainId, address, connect, config }) => { setSearchParams(newQueryParameters); } - const setSlippageInner = (value) => { + const setSlippageInner = useCallback((value) => { const maybeValue = parseFloat(value); if (!maybeValue || parseFloat(value) <= 100) { setSlippage(value); - localStorage.setItem("dex-slippage", value); + setStorageValue(chainId, address, "dex-slippage", value); } - } + }, [chainId, address]); - const setSecondsToWaitInner = (value) => { - localStorage.setItem("dex-deadline", value); + const setSecondsToWaitInner = useCallback((value) => { setSecondsToWait(value); - } + setStorageValue(chainId, address, "dex-deadline", value); + }, [chainId, address]); - const setFormatDecimalsInner = (value) => { + const setFormatDecimalsInner = useCallback((value) => { if (Number(value) <= 17) { - localStorage.setItem("dex-decimals", value); setFormatDecimals(value); + setStorageValue(chainId, address, "dex-decimals", value); } - } + }, [chainId, address]); - const setDestinationAddressInner = (value) => { + const setDestinationAddressInner = useCallback((value) => { const cleanedValue = value.trim(); - const isEvmAddress = /^0x[a-fA-F0-9]{40}$/.test(cleanedValue); - if (isEvmAddress) { - localStorage.setItem("dex-destination", value); - setActualDestinationAddress(value); - } else if (!isEvmAddress && actualDestinationAddress) { - localStorage.removeItem("dex-destination"); - setActualDestinationAddress(undefined); - } setDestinationAddress(value); - } + if (isAddress(cleanedValue)) { + setActualDestinationAddress(value); + setStorageValue(chainId, address, "dex-destination", value); + } + }, [chainId, address]); const handleCloseSetting = () => { setDestinationAddress(undefined); @@ -255,7 +254,7 @@ const Dex = ({ chainId, address, connect, config }) => { }} > { const isVerySmallScreen = useMediaQuery("(max-width: 379px)"); const theme = useTheme(); + const { getStorageValue, setStorageValue } = useLocalStorage(); - const myStoredProposals = localStorage.getItem(`${MY_PROPOSALS_PREFIX}-${chainId}-${address}`); - const [myProposals, setMyProposals] = useState( - myStoredProposals ? JSON.parse(myStoredProposals).map(id => BigInt(id)) : [] - ); + const myStoredProposals = getStorageValue(chainId, address, MY_PROPOSALS_PREFIX, []); + const [myProposals, setMyProposalsInner] = useState(() => myStoredProposals.map(id => BigInt(id))); const [isPending, setIsPending] = useState(false); const [isModalOpened, setIsModalOpened] = useState(false); @@ -61,10 +61,10 @@ const NewProposal = ({ config, address, connect, chainId }) => { proposalDescription } = useProposalHash(chainId, proposalFunctions); - useEffect(() => { - const toStore = JSON.stringify(myProposals.map(id => id.toString())); - localStorage.setItem(`${MY_PROPOSALS_PREFIX}-${chainId}-${address}`, toStore); - }, [myProposals]); + const setMyProposals = useCallback((proposals) => { + setMyProposalsInner(proposals); + setStorageValue(chainId, address, MY_PROPOSALS_PREFIX, proposals.map(id => id.toString())) + }, [chainId, address]); useEffect(() => { ReactGA.send({ hitType: "pageview", page: "/governance/create" }); diff --git a/src/containers/Governance/ProposalDetails.jsx b/src/containers/Governance/ProposalDetails.jsx index d1361af..d33b212 100644 --- a/src/containers/Governance/ProposalDetails.jsx +++ b/src/containers/Governance/ProposalDetails.jsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useMemo } from "react"; +import { useEffect, useState, useMemo, useCallback } from "react"; import ReactGA from "react-ga4"; import { useParams } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; @@ -38,6 +38,7 @@ import { parseFunctionCalldata } from "./components/functions"; import { networkAvgBlockSpeed } from "../../constants"; +import { useLocalStorage } from "../../hooks/localstorage"; import { useTokenSymbol, usePastTotalSupply, usePastVotes, useBalance } from "../../hooks/tokens"; import { getVoteValue, @@ -75,7 +76,6 @@ import RepeatIcon from '@mui/icons-material/Repeat'; const HUNDRED = new DecimalBigNumber(100n, 0); - const ProposalDetails = ({ chainId, address, connect, config }) => { const { id } = useParams(); const proposalId = BigInt(id); @@ -89,6 +89,7 @@ const ProposalDetails = ({ chainId, address, connect, config }) => { const theme = useTheme(); const queryClient = useQueryClient(); + const { getStorageValue, setStorageValue } = useLocalStorage(); const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST"); const { balance } = useBalance(chainId, "GHST", address); @@ -149,20 +150,19 @@ const ProposalDetails = ({ chainId, address, connect, config }) => { return url; }, [proposalProposer, config]); - const handleVote = async (support) => { + const handleVote = useCallback(async (support) => { setIsPending(true); const result = await castVote(chainId, address, proposalId, support); if (result) { - const storedVotedProposals = localStorage.getItem(`${VOTED_PROPOSALS_PREFIX}-${chainId}-${address}`); + const storedVotedProposals = getStorageValue(chainId, address, VOTED_PROPOSALS_PREFIX, []); const proposals = JSON.parse(storedVotedProposals || "[]").map(id => BigInt(id)); proposals.push(proposalId); - const toStore = JSON.stringify(proposals.map(id => id.toString())); - localStorage.setItem(`${VOTED_PROPOSALS_PREFIX}-${chainId}-${address}`, toStore); + setStorageValue(chainId, address, VOTED_PROPOSALS_PREFIX, proposals.map(id => id.toString())); await queryClient.invalidateQueries(); } setIsPending(false); - } + }, [chainId, address, proposalId]); const handleExecute = async () => { setIsPending(true); diff --git a/src/containers/Governance/components/ProposalsList.jsx b/src/containers/Governance/components/ProposalsList.jsx index 48a2ba0..3148d88 100644 --- a/src/containers/Governance/components/ProposalsList.jsx +++ b/src/containers/Governance/components/ProposalsList.jsx @@ -42,6 +42,7 @@ import { import { useScreenSize } from "../../../hooks/useScreenSize"; import { useProposals } from "../../../hooks/governance"; +import { useLocalStorage } from "../../../hooks/localstorage"; const MAX_PROPOSALS_TO_SHOW = 10; @@ -51,18 +52,15 @@ const ProposalsList = ({ chainId, address, config }) => { const theme = useTheme(); const { network } = useParams(); + const { getStorageValue, setStorageValue } = useLocalStorage(); const [proposalsFilter, setProposalFilter] = useState("active"); - const myStoredProposals = localStorage.getItem(`${MY_PROPOSALS_PREFIX}-${chainId}-${address}`); - const [myProposals, setMyProposals] = useState( - myStoredProposals ? JSON.parse(myStoredProposals).map(id => BigInt(id)) : [] - ); + const myStoredProposals = getStorageValue(chainId, address, MY_PROPOSALS_PREFIX, []); + const [myProposals, setMyProposals] = useState(() => myStoredProposals.map(id => BigInt(id))); - const storedVotedProposals = localStorage.getItem(`${VOTED_PROPOSALS_PREFIX}-${chainId}-${address}`); - const [votedProposals, setVotedProposals] = useState( - storedVotedProposals ? JSON.parse(storedVotedProposals).map(id => BigInt(id)) : [] - ); + const storedVotedProposals = getStorageValue(chainId, address, VOTED_PROPOSALS_PREFIX, []); + const [votedProposals, setVotedProposals] = useState(() => storedVotedProposals.map(id => BigInt(id))); const searchedIndexes = useMemo(() => { switch (proposalsFilter) { diff --git a/src/containers/Stake/components/StakeInputArea.jsx b/src/containers/Stake/components/StakeInputArea.jsx index 5696d48..9d063e3 100644 --- a/src/containers/Stake/components/StakeInputArea.jsx +++ b/src/containers/Stake/components/StakeInputArea.jsx @@ -1,7 +1,7 @@ import { Avatar, Box, Link } from "@mui/material"; import { styled } from "@mui/material/styles"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useCallback } from "react"; import { useSearchParams } from "react-router-dom"; import { DecimalBigNumber } from "../../../helpers/DecimalBigNumber"; @@ -23,6 +23,7 @@ import { STAKING_ADDRESSES, } from "../../../constants/addresses"; import { useCurrentIndex } from "../../../hooks/staking"; +import { useLocalStorage } from "../../../hooks/localstorage"; import { useBalance, useTokenSymbol } from "../../../hooks/tokens"; import { formatNumber } from "../../../helpers"; @@ -82,20 +83,11 @@ export const StakeInputArea = ({ const [bottomTokenModalOpen, setBottomTokenModalOpen] = useState(false); const [confirmationModalOpen, setConfirmationModalOpen] = useState(false); - const [formatDecimals, setFormatDecimals] = useState(localStorage.getItem("stake-decimals") - ? localStorage.getItem("stake-decimals") - : "5" - ); + const { getStorageValue, setStorageValue } = useLocalStorage(); - const [isClaim, setIsClaim] = useState(localStorage.getItem("stake-isClaim") - ? localStorage.getItem("stake-isClaim") === 'true' - : true - ); - - const [isTrigger, setIsTrigger] = useState(localStorage.getItem("stake-isTrigger") - ? localStorage.getItem("stake-isTrigger") === 'true' - : true - ); + const [formatDecimals, setFormatDecimals] = useState(() => getStorageValue(chainId, address, "stake-decimals", "5")); + const [isClaim, setIsClaim] = useState(() => getStorageValue(chainId, address, "stake-isClaim", true)); + const [isTrigger, setIsTrigger] = useState(() => getStorageValue(chainId, address, "stake-isTrigger", true)); const [amount, setAmount] = useState(""); const [receiveAmount, setReceiveAmount] = useState(""); @@ -110,23 +102,22 @@ export const StakeInputArea = ({ const { symbol: stnkSymbol } = useTokenSymbol(chainId, "STNK"); const { symbol: ghstSymbol } = useTokenSymbol(chainId, "GHST"); - const setIsClaimInner = (value) => { + const setIsClaimInner = useCallback((value) => { setIsClaim(value); - localStorage.setItem("stake-isClaim", value); + setStorageValue(chainId, address, "stake-isClaim", value); + }, [chainId, address]); - } - - const setIsTriggerInner = (value) => { + const setIsTriggerInner = useCallback((value) => { setIsTrigger(value); - localStorage.setItem("stake-isTrigger", value); - } + setStorageValue(chainId, address, "stake-isTrigger", value); + }, [chainId, address]); - const setFormatDecimalsInner = (value) => { + const setFormatDecimalsInner = useCallback((value) => { if (Number(value) <= 17) { setFormatDecimals(value); - localStorage.setItem("staking-decimals", value); + setStorageValue(chainId, address, "stake-decimals", value); } - } + }, [chainId, address]); useEffect(() => { const innerBalance = upperToken === ghstSymbol ? diff --git a/src/hooks/localstorage/index.jsx b/src/hooks/localstorage/index.jsx new file mode 100644 index 0000000..a1e0d96 --- /dev/null +++ b/src/hooks/localstorage/index.jsx @@ -0,0 +1,27 @@ +import { createContext, useContext, useState, useEffect } from "react"; + +const LocalStorageContext = createContext(); +export const useLocalStorage = () => useContext(LocalStorageContext); + +export const LocalStorageProvider = ({ children }) => { + const getStorageKey = (chainId, address, target) => { + return `${chainId}:${address}:${target}`; + } + + const getStorageValue = (chainId, address, target, defaultValue) => { + const key = getStorageKey(chainId, address, target); + const stored = localStorage.getItem(key); + return stored ? JSON.parse(stored) : defaultValue; + } + + const setStorageValue = (chainId, address, target, value) => { + const key = getStorageKey(prefix, chainId, address, target); + localStorage.setItem(key, JSON.stringify(value)); + } + + return ( + + {children} + + ) +} diff --git a/src/main.jsx b/src/main.jsx index a53193e..73536b4 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -9,6 +9,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { WagmiProvider } from "wagmi"; import { config } from "./config"; import { UnstableProviderProvider, MetadataProviderProvider } from "./hooks/ghost" +import { LocalStorageProvider } from "./hooks/localstorage"; import ReactGA from "react-ga4"; const queryClient = new QueryClient(); @@ -24,7 +25,9 @@ ReactDOM.createRoot(document.getElementById('root')).render( - + + +