diff --git a/apps/common/contexts/useYearn.tsx b/apps/common/contexts/useYearn.tsx index bd05e276a..ef393a0a4 100755 --- a/apps/common/contexts/useYearn.tsx +++ b/apps/common/contexts/useYearn.tsx @@ -1,12 +1,9 @@ -import {createContext, memo, useContext, useEffect, useMemo} from 'react'; +import {createContext, memo, useContext, useMemo} from 'react'; import {STACKING_TO_VAULT} from '@vaults/constants/optRewards'; -import {toast} from '@yearn-finance/web-lib/components/yToast'; import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; -import {useChainID} from '@yearn-finance/web-lib/hooks/useChainID'; import {useLocalStorage} from '@yearn-finance/web-lib/hooks/useLocalStorage'; import {toAddress} from '@yearn-finance/web-lib/utils/address'; import {useFetch} from '@common/hooks/useFetch'; -import {useYDaemonStatus} from '@common/hooks/useYDaemonStatus'; import {yDaemonEarnedSchema} from '@common/schemas/yDaemonEarnedSchema'; import {yDaemonPricesChainSchema} from '@common/schemas/yDaemonPricesSchema'; import {Solver} from '@common/schemas/yDaemonTokenListBalances'; @@ -66,9 +63,7 @@ const YearnContext = createContext({ }); export const YearnContextApp = memo(function YearnContextApp({children}: {children: ReactElement}): ReactElement { - const {safeChainID} = useChainID(); const {yDaemonBaseUri: yDaemonBaseUriWithoutChain} = useYDaemonBaseURI(); - const result = useYDaemonStatus({chainID: safeChainID}); const {address, currentPartner} = useWeb3(); const [zapSlippage, set_zapSlippage] = useLocalStorage('yearn.fi/zap-slippage', DEFAULT_SLIPPAGE); const [zapProvider, set_zapProvider] = useLocalStorage('yearn.fi/zap-provider', Solver.enum.Cowswap); @@ -77,12 +72,6 @@ export const YearnContextApp = memo(function YearnContextApp({children}: {childr true ); - useEffect((): void => { - if (result?.error?.code === 'ERR_NETWORK') { - toast({type: 'error', content: 'AxiosError: Network Error'}); - } - }, [result?.error?.code]); - const {data: prices} = useFetch({ endpoint: `${yDaemonBaseUriWithoutChain}/prices/all`, schema: yDaemonPricesChainSchema diff --git a/apps/common/hooks/useBalance.ts b/apps/common/hooks/useBalance.ts index 3e7a14fcc..6967562ec 100644 --- a/apps/common/hooks/useBalance.ts +++ b/apps/common/hooks/useBalance.ts @@ -22,7 +22,7 @@ export function useBalance({ return source?.[toAddress(address)] || toNormalizedBN(0); } return getBalance({address: toAddress(address), chainID: chainID}); - }, [source, getBalance, address]); + }, [source, getBalance, address, chainID]); return balance; } diff --git a/apps/common/hooks/useMultichainBalances.ts b/apps/common/hooks/useMultichainBalances.ts index c06551303..7d7a85667 100644 --- a/apps/common/hooks/useMultichainBalances.ts +++ b/apps/common/hooks/useMultichainBalances.ts @@ -159,7 +159,7 @@ async function getBalances( ** This hook can be used to fetch balance information for any ERC20 tokens. **************************************************************************/ export function useBalances(props?: TUseBalancesReq): TUseBalancesRes { - const {address: userAddress, isActive, provider} = useWeb3(); + const {address: userAddress, isActive} = useWeb3(); const chainID = useChainId(); const {onLoadStart, onLoadDone} = useUI(); const [nonce, set_nonce] = useState(0); @@ -213,7 +213,7 @@ export function useBalances(props?: TUseBalancesReq): TUseBalancesRes { ** send in a worker. **************************************************************************/ const onUpdate = useCallback(async (): Promise => { - if (!userAddress || !provider) { + if (!userAddress) { return {}; } const tokenList = deserialize(stringifiedTokens) || []; @@ -289,7 +289,7 @@ export function useBalances(props?: TUseBalancesReq): TUseBalancesRes { onLoadDone(); return updated; - }, [onLoadDone, onLoadStart, provider, stringifiedTokens, userAddress]); + }, [onLoadDone, onLoadStart, stringifiedTokens, userAddress]); /* 🔵 - Yearn Finance ****************************************************** ** onUpdateSome takes a list of tokens and fetches the balances for each @@ -400,7 +400,7 @@ export function useBalances(props?: TUseBalancesReq): TUseBalancesRes { ** to fetch the balances, preventing the UI to freeze. **************************************************************************/ useAsyncTrigger(async (): Promise => { - if (!isActive || !userAddress || !provider) { + if (!isActive || !userAddress) { return; } set_status({ @@ -439,7 +439,7 @@ export function useBalances(props?: TUseBalancesReq): TUseBalancesRes { } onLoadDone(); set_status({...defaultStatus, isSuccess: true, isFetched: true}); - }, [stringifiedTokens, isActive, userAddress, provider, onLoadStart, updateBalancesCall, onLoadDone]); + }, [stringifiedTokens, isActive, userAddress, onLoadStart, updateBalancesCall, onLoadDone]); const contextValue = useMemo( (): TUseBalancesRes => ({ diff --git a/apps/vaults/components/ListHero.tsx b/apps/vaults/components/ListHero.tsx index 2a6bfec34..97d6b22c3 100644 --- a/apps/vaults/components/ListHero.tsx +++ b/apps/vaults/components/ListHero.tsx @@ -1,4 +1,5 @@ import {useMemo} from 'react'; +import {ALL_CATEGORIES} from '@vaults/contexts/useAppSettings'; import {IconArbitrumChain} from '@yearn-finance/web-lib/icons/chains/IconArbitrumChain'; import {IconBaseChain} from '@yearn-finance/web-lib/icons/chains/IconBaseChain'; import {IconEtherumChain} from '@yearn-finance/web-lib/icons/chains/IconEtherumChain'; @@ -11,111 +12,67 @@ import type {ReactElement} from 'react'; import type {TMultiSelectOptionProps} from '@common/components/MultiSelectDropdown'; type TListHero = { - categories: string; - selectedChains: string; + categories: string[]; + chains: number[]; searchValue: string; - set_categories: (categories: string) => void; - set_selectedChains: (chains: string) => void; + onChangeCategories: (categories: string[]) => void; + onChangeChains: (chains: number[]) => void; onSearch: (searchValue: string) => void; }; export function ListHero({ categories, - set_categories, + onChangeCategories, searchValue, - selectedChains, + chains, onSearch, - set_selectedChains + onChangeChains }: TListHero): ReactElement { - const chainsFromJSON = useMemo((): number[] => JSON.parse(selectedChains || '[]') as number[], [selectedChains]); - const categoriesFromJSON = useMemo((): string[] => JSON.parse(categories || '[]') as string[], [categories]); - const chainOptions = useMemo((): TMultiSelectOptionProps[] => { return [ { label: 'Ethereum', value: 1, - isSelected: chainsFromJSON.includes(1), + isSelected: chains.includes(1), icon: }, { label: 'OP Mainnet', value: 10, - isSelected: chainsFromJSON.includes(10), + isSelected: chains.includes(10), icon: }, { label: 'Fantom', value: 250, - isSelected: chainsFromJSON.includes(250), + isSelected: chains.includes(250), icon: }, { label: 'Base', value: 8453, - isSelected: chainsFromJSON.includes(8453), + isSelected: chains.includes(8453), icon: }, { label: 'Arbitrum One', value: 42161, - isSelected: chainsFromJSON.includes(42161), + isSelected: chains.includes(42161), icon: } ]; - }, [chainsFromJSON]); + }, [chains]); const categoryOptions = useMemo((): TMultiSelectOptionProps[] => { - const options: TMultiSelectOptionProps[] = []; - - options.push({ - value: 'Holdings', - label: 'Holdings', - isSelected: categoriesFromJSON.includes('Holdings') - }); - // options.push({ - // value: 'Featured Vaults', - // label: 'Featured', - // isSelected: categoriesFromJSON.includes('Featured Vaults') - // }); - options.push({ - value: 'Crypto Vaults', - label: 'Crypto', - isSelected: categoriesFromJSON.includes('Crypto Vaults') - }); - options.push({ - value: 'Stables Vaults', - label: 'Stables', - isSelected: categoriesFromJSON.includes('Stables Vaults') - }); - options.push({ - value: 'Curve Vaults', - label: 'Curve', - isSelected: categoriesFromJSON.includes('Curve Vaults') - }); - options.push({ - value: 'Balancer Vaults', - label: 'Balancer', - isSelected: categoriesFromJSON.includes('Balancer Vaults') - }); - options.push({ - value: 'Boosted Vaults', - label: 'Boosted', - isSelected: categoriesFromJSON.includes('Boosted Vaults') - }); - options.push({ - value: 'Velodrome Vaults', - label: 'Velodrome', - isSelected: categoriesFromJSON.includes('Velodrome Vaults') - }); - options.push({ - value: 'Aerodrome Vaults', - label: 'Aerodrome', - isSelected: categoriesFromJSON.includes('Aerodrome Vaults') - }); - + const options: TMultiSelectOptionProps[] = Object.entries(ALL_CATEGORIES).map( + ([key, value]): TMultiSelectOptionProps => ({ + value: key, + label: value, + isSelected: categories.includes(key) + }) + ); return options; - }, [categoriesFromJSON]); + }, [categories]); return (
@@ -129,7 +86,7 @@ export function ListHero({ const selectedChains = options .filter((o): boolean => o.isSelected) .map((option): number => Number(option.value)); - set_selectedChains(JSON.stringify(selectedChains)); + onChangeChains(selectedChains); }} />
@@ -143,7 +100,7 @@ export function ListHero({ const selectedCategories = options .filter((o): boolean => o.isSelected) .map((option): string => String(option.value)); - set_categories(JSON.stringify(selectedCategories)); + onChangeCategories(selectedCategories); }} /> diff --git a/apps/vaults/components/list/VaultsListEmpty.tsx b/apps/vaults/components/list/VaultsListEmpty.tsx index 31c8ff1f4..6e8906ea3 100755 --- a/apps/vaults/components/list/VaultsListEmpty.tsx +++ b/apps/vaults/components/list/VaultsListEmpty.tsx @@ -1,4 +1,4 @@ -import {ALL_CATEGORIES, ALL_CHAINS, useAppSettings} from '@vaults/contexts/useAppSettings'; +import {ALL_CATEGORIES_KEYS, ALL_CHAINS, useAppSettings} from '@vaults/contexts/useAppSettings'; import {Button} from '@yearn-finance/web-lib/components/Button'; import {isZero} from '@yearn-finance/web-lib/utils/isZero'; @@ -9,14 +9,18 @@ export function VaultsListEmpty({ sortedVaultsToDisplay, currentCategories, currentChains, + onChangeCategories, + onChangeChains, isLoading }: { sortedVaultsToDisplay: TYDaemonVaults; currentCategories: string[]; currentChains: number[]; + onChangeCategories: (value: string[]) => void; + onChangeChains: (value: number[]) => void; isLoading: boolean; }): ReactElement { - const {searchValue, category, set_category, set_selectedChains} = useAppSettings(); + const {searchValue} = useAppSettings(); if (isLoading && isZero(sortedVaultsToDisplay.length)) { return ( @@ -34,7 +38,7 @@ export function VaultsListEmpty({ !isLoading && isZero(sortedVaultsToDisplay.length) && currentCategories.length === 1 && - currentCategories.includes('Holdings') + currentCategories.includes('holdings') ) { return (
@@ -50,7 +54,7 @@ export function VaultsListEmpty({ return (
{'No data, reeeeeeeeeeee'} - {category === 'All Vaults' ? ( + {currentCategories.length === ALL_CATEGORIES_KEYS.length ? (

{`The vault "${searchValue}" does not exist`}

) : ( <> @@ -60,8 +64,8 @@ export function VaultsListEmpty({ @@ -82,8 +86,8 @@ export function VaultsListEmpty({ diff --git a/apps/vaults/contexts/useAppSettings.tsx b/apps/vaults/contexts/useAppSettings.tsx index cb1dbab7d..093df711b 100755 --- a/apps/vaults/contexts/useAppSettings.tsx +++ b/apps/vaults/contexts/useAppSettings.tsx @@ -4,33 +4,34 @@ import {useSessionStorage} from '@yearn-finance/web-lib/hooks/useSessionStorage' import type {ReactElement} from 'react'; -export const ALL_CATEGORIES = - '["Holdings","Crypto Vaults","Stables Vaults","Curve Vaults","Balancer Vaults","Boosted Vaults","Velodrome Vaults","Aerodrome Vaults"]'; -export const ALL_CHAINS = '[1,10,250,8453,42161]'; +export const ALL_CATEGORIES = { + holdings: 'Holdings', + crypto: 'Crypto Vaults', + stables: 'Stables Vaults', + curve: 'Curve Vaults', + balancer: 'Balancer Vaults', + boosted: 'Boosted Vaults', + velodrome: 'Velodrome Vaults', + aerodrome: 'Aerodrome Vaults' +}; +export const ALL_CATEGORIES_KEYS = Object.keys(ALL_CATEGORIES); +export const ALL_CHAINS = [1, 10, 250, 8453, 42161]; export type TAppSettingsContext = { - category: string; - selectedChains: string; searchValue: string; shouldHideDust: boolean; shouldHideLowTVLVaults: boolean; onSwitchHideDust: VoidFunction; onSwitchHideLowTVLVaults: VoidFunction; - set_category: (v: string) => void; set_searchValue: (v: string) => void; - set_selectedChains: (v: string) => void; }; const defaultProps: TAppSettingsContext = { - category: '', - selectedChains: '[1]', searchValue: '', shouldHideDust: false, shouldHideLowTVLVaults: false, onSwitchHideDust: (): void => undefined, onSwitchHideLowTVLVaults: (): void => undefined, - set_category: (): void => undefined, - set_searchValue: (): void => undefined, - set_selectedChains: (): void => undefined + set_searchValue: (): void => undefined }; const AppSettingsContext = createContext(defaultProps); @@ -43,8 +44,6 @@ export const AppSettingsContextApp = memo(function AppSettingsContextApp({ * @deprecated Use use-query-params instead */ const [searchValue, set_searchValue] = useSessionStorage('yearn.fi/vaults-search@0.0.1', ''); - const [category, set_category] = useSessionStorage('yearn.fi/vaults-categories@0.0.1', ALL_CATEGORIES); - const [selectedChains, set_selectedChains] = useSessionStorage('yearn.fi/selected-chains@0.0.1', ALL_CHAINS); const [shouldHideDust, set_shouldHideDust] = useLocalStorage('yearn.fi/should-hide-dust@0.0.1', false); const [shouldHideLowTVLVaults, set_shouldHideLowTVLVaults] = useLocalStorage('yearn.fi/hide-low-tvl@0.0.1', false); @@ -57,22 +56,14 @@ export const AppSettingsContextApp = memo(function AppSettingsContextApp({ onSwitchHideDust: (): void => set_shouldHideDust(!shouldHideDust), shouldHideLowTVLVaults, onSwitchHideLowTVLVaults: (): void => set_shouldHideLowTVLVaults(!shouldHideLowTVLVaults), - category, - selectedChains, searchValue, - set_category, - set_searchValue, - set_selectedChains + set_searchValue }), [ shouldHideDust, shouldHideLowTVLVaults, - category, - selectedChains, searchValue, - set_category, set_searchValue, - set_selectedChains, set_shouldHideDust, set_shouldHideLowTVLVaults ] diff --git a/apps/vaults/contexts/useWalletForZaps.tsx b/apps/vaults/contexts/useWalletForZaps.tsx index 97d3d32e4..34878156f 100755 --- a/apps/vaults/contexts/useWalletForZaps.tsx +++ b/apps/vaults/contexts/useWalletForZaps.tsx @@ -133,7 +133,7 @@ export const WalletForZapAppContextApp = memo(function WalletForZapAppContextApp getPrice, refresh: refresh }), - [zapTokens, listTokens, getToken, getBalance, getPrice, refresh, tokensList] + [listTokens, getToken, getBalance, getPrice, refresh, tokensList] ); return {children}; diff --git a/apps/vaults/hooks/useFilteredVaults.ts b/apps/vaults/hooks/useFilteredVaults.ts index 8e1aa13ec..c80103633 100644 --- a/apps/vaults/hooks/useFilteredVaults.ts +++ b/apps/vaults/hooks/useFilteredVaults.ts @@ -19,23 +19,24 @@ export function useFilteredVaults( ); } -export function useVaultFilter(): { +export function useVaultFilter( + categories: string[], + chains: number[] +): { activeVaults: TYDaemonVault[]; retiredVaults: TYDaemonVault[]; migratableVaults: TYDaemonVault[]; } { const {vaults, vaultsMigrations, vaultsRetired} = useYearn(); const {getToken} = useWallet(); - const {shouldHideDust, category, selectedChains} = useAppSettings(); - const chainsFromJSON = useMemo((): number[] => JSON.parse(selectedChains || '[]') as number[], [selectedChains]); - const categoriesFromJSON = useMemo((): string[] => JSON.parse(category || '[]') as string[], [category]); + const {shouldHideDust} = useAppSettings(); const filterHoldingsCallback = useCallback( (address: TAddress, chainID: number): boolean => { const holding = getToken({address, chainID}); // [Optimism] Check if staked vaults have holdings - if (chainsFromJSON.includes(10)) { + if (chains.includes(10)) { const stakedVaultAddress = STACKING_TO_VAULT[toAddress(address)]; const stakedHolding = getToken({address: stakedVaultAddress, chainID}); const hasValidStakedBalance = stakedHolding.balance.raw > 0n; @@ -55,7 +56,7 @@ export function useVaultFilter(): { } return false; }, - [getToken, chainsFromJSON, shouldHideDust] + [getToken, chains, shouldHideDust] ); const filterMigrationCallback = useCallback( @@ -98,34 +99,34 @@ export function useVaultFilter(): { const activeVaults = useDeepCompareMemo((): TYDaemonVault[] => { let _vaultList: TYDaemonVault[] = []; - if (categoriesFromJSON.includes('Featured Vaults')) { + if (categories.includes('featured')) { _vaultList.sort( (a, b): number => (b.tvl.tvl || 0) * (b?.apr?.netAPR || 0) - (a.tvl.tvl || 0) * (a?.apr?.netAPR || 0) ); _vaultList = _vaultList.slice(0, 10); } - if (categoriesFromJSON.includes('Curve Vaults')) { + if (categories.includes('curve')) { _vaultList = [..._vaultList, ...curveVaults]; } - if (categoriesFromJSON.includes('Balancer Vaults')) { + if (categories.includes('balancer')) { _vaultList = [..._vaultList, ...balancerVaults]; } - if (categoriesFromJSON.includes('Velodrome Vaults')) { + if (categories.includes('velodrome')) { _vaultList = [..._vaultList, ...velodromeVaults]; } - if (categoriesFromJSON.includes('Aerodrome Vaults')) { + if (categories.includes('aerodrome')) { _vaultList = [..._vaultList, ...aerodromeVaults]; } - if (categoriesFromJSON.includes('Boosted Vaults')) { + if (categories.includes('boosted')) { _vaultList = [..._vaultList, ...boostedVaults]; } - if (categoriesFromJSON.includes('Stables Vaults')) { + if (categories.includes('stables')) { _vaultList = [..._vaultList, ...stablesVaults]; } - if (categoriesFromJSON.includes('Crypto Vaults')) { + if (categories.includes('crypto')) { _vaultList = [..._vaultList, ...cryptoVaults]; } - if (categoriesFromJSON.includes('Holdings')) { + if (categories.includes('holdings')) { _vaultList = [..._vaultList, ...holdingsVaults]; } @@ -136,7 +137,7 @@ export function useVaultFilter(): { return _vaultList; }, [ - categoriesFromJSON, + categories, curveVaults, balancerVaults, velodromeVaults, diff --git a/apps/vaults/hooks/useSortVaults.ts b/apps/vaults/hooks/useSortVaults.ts index 908f0f28e..32ddc629f 100644 --- a/apps/vaults/hooks/useSortVaults.ts +++ b/apps/vaults/hooks/useSortVaults.ts @@ -11,7 +11,7 @@ import {numberSort, stringSort} from '@common/utils/sort'; import type {TYDaemonVaults} from '@common/schemas/yDaemonVaultsSchemas'; import type {TSortDirection} from '@common/types/types'; -export type TPossibleSortBy = 'apr' | 'forwardAPR' | 'tvl' | 'name' | 'deposited' | 'available' | 'featuringScore'; +export type TPossibleSortBy = 'apr' | 'estAPR' | 'tvl' | 'name' | 'deposited' | 'available' | 'featuringScore'; export function useSortVaults( vaultList: TYDaemonVaults, @@ -119,7 +119,7 @@ export function useSortVaults( if (sortBy === 'name') { return sortedByName(); } - if (sortBy === 'forwardAPR') { + if (sortBy === 'estAPR') { return sortedByForwardAPR(); } if (sortBy === 'apr') { diff --git a/apps/vaults/hooks/useVaultsQueryArgs.ts b/apps/vaults/hooks/useVaultsQueryArgs.ts new file mode 100644 index 000000000..7e3e4e96c --- /dev/null +++ b/apps/vaults/hooks/useVaultsQueryArgs.ts @@ -0,0 +1,154 @@ +import {useEffect, useState} from 'react'; +import { + DelimitedArrayParam, + DelimitedNumericArrayParam, + StringParam, + useQueryParam, + withDefault +} from 'use-query-params'; +import {ALL_CATEGORIES_KEYS, ALL_CHAINS} from '@vaults/contexts/useAppSettings'; + +import type {TSortDirection} from '@common/types/types'; +import type {TPossibleSortBy} from '@vaults/hooks/useSortVaults'; + +const CategoriesParams = withDefault(DelimitedArrayParam, ALL_CATEGORIES_KEYS); +const ChainsParams = withDefault(DelimitedNumericArrayParam, ALL_CHAINS); +const SortDirectionParams = withDefault(StringParam, 'desc'); +const SortByParams = withDefault(StringParam, 'featuringScore'); +type TQueryArgs = { + search: string | null | undefined; + categories: string[]; + chains: number[]; + sortDirection: TSortDirection; + sortBy: TPossibleSortBy; + onSearch: (value: string) => void; + onChangeCategories: (value: string[]) => void; + onChangeChains: (value: number[]) => void; + onChangeSortDirection: (value: TSortDirection) => void; + onChangeSortBy: (value: TPossibleSortBy) => void; +}; +function useQueryArguments(): TQueryArgs { + /** 🔵 - Yearn ********************************************************************************* + ** Theses elements are not exported, they are just used to keep the state of the query, which + ** is slower than the state of the component. + *********************************************************************************************/ + const [searchParam, set_searchParam] = useQueryParam('search', StringParam); + const [categoriesParam, set_categoriesParam] = useQueryParam('categories', CategoriesParams); + const [chainsParam, set_chainsParam] = useQueryParam('chains', ChainsParams); + const [sortDirectionParam, set_sortDirectionParam] = useQueryParam('sortDir', SortDirectionParams); + const [sortByParam, set_sortByParam] = useQueryParam('sortBy', SortByParams); + + /** 🔵 - Yearn ********************************************************************************* + ** Theses are our actual state. + *********************************************************************************************/ + const [search, set_search] = useState(searchParam); + const [categories, set_categories] = useState(categoriesParam); + const [chains, set_chains] = useState(chainsParam); + const [sortDirection, set_sortDirection] = useState(sortDirectionParam); + const [sortBy, set_sortBy] = useState(sortByParam); + + /** 🔵 - Yearn ********************************************************************************* + ** This useEffect hook is used to synchronize the search state with the query parameter + *********************************************************************************************/ + useEffect((): void => { + if (searchParam === search) { + return; + } + if (search === undefined && searchParam !== undefined) { + set_search(searchParam); + return; + } + if (!search) { + set_searchParam(undefined); + } else { + set_searchParam(search); + } + }, [searchParam, search, set_searchParam]); + + /** 🔵 - Yearn ********************************************************************************* + ** This useEffect hook is used to synchronize the categories + *********************************************************************************************/ + useEffect((): void => { + if (categoriesParam === categories) { + return; + } + if (categories === undefined && categoriesParam !== undefined) { + set_categories(categoriesParam); + return; + } + if (!categories) { + set_categoriesParam(ALL_CATEGORIES_KEYS); + } else { + set_categoriesParam(categories); + } + }, [categoriesParam, categories, set_categoriesParam]); + + /** 🔵 - Yearn ********************************************************************************* + ** This useEffect hook is used to synchronize the chains + *********************************************************************************************/ + useEffect((): void => { + if (chainsParam === chains) { + return; + } + if (chains === undefined && chainsParam !== undefined) { + set_chains(chainsParam); + return; + } + if (!chains) { + set_chainsParam(ALL_CHAINS); + } else { + set_chainsParam(chains); + } + }, [chainsParam, chains, set_chainsParam]); + + /** 🔵 - Yearn ********************************************************************************* + ** This useEffect hook is used to synchronize the sortDirection + *********************************************************************************************/ + useEffect((): void => { + if (sortDirectionParam === sortDirection) { + return; + } + if (sortDirection === undefined && sortDirectionParam !== undefined) { + set_sortDirection(sortDirectionParam); + return; + } + if (!sortDirection) { + set_sortDirectionParam(undefined); + } else { + set_sortDirectionParam(sortDirection); + } + }, [sortDirectionParam, sortDirection, set_sortDirectionParam]); + + /** 🔵 - Yearn ********************************************************************************* + ** This useEffect hook is used to synchronize the sortOrder + *********************************************************************************************/ + useEffect((): void => { + if (sortByParam === sortBy) { + return; + } + if (sortBy === undefined && sortByParam !== undefined) { + set_sortBy(sortByParam); + return; + } + if (!sortBy) { + set_sortByParam(undefined); + } else { + set_sortByParam(sortBy); + } + }, [sortByParam, sortBy, set_sortByParam]); + + return { + search, + categories: (categories || []) as string[], + chains: (chains || []) as number[], + sortDirection: sortDirection as TSortDirection, + sortBy: sortBy as TPossibleSortBy, + onSearch: set_search, + onChangeCategories: set_categories, + onChangeChains: set_chains, + onChangeSortDirection: set_sortDirection, + onChangeSortBy: set_sortBy + }; +} + +export {useQueryArguments}; diff --git a/pages/vaults/index.tsx b/pages/vaults/index.tsx index 6ee708f6c..ca202fb9d 100644 --- a/pages/vaults/index.tsx +++ b/pages/vaults/index.tsx @@ -1,5 +1,5 @@ -import {Fragment, useCallback, useEffect, useMemo, useState} from 'react'; -import {QueryParamProvider, StringParam, useQueryParams} from 'use-query-params'; +import {Fragment, useEffect, useMemo} from 'react'; +import {QueryParamProvider} from 'use-query-params'; import {motion, useSpring, useTransform} from 'framer-motion'; import {VaultListOptions} from '@vaults/components/list/VaultListOptions'; import {VaultsListEmpty} from '@vaults/components/list/VaultsListEmpty'; @@ -7,14 +7,13 @@ import {VaultsListInternalMigrationRow} from '@vaults/components/list/VaultsList import {VaultsListRetired} from '@vaults/components/list/VaultsListRetired'; import {VaultsListRow} from '@vaults/components/list/VaultsListRow'; import {ListHero} from '@vaults/components/ListHero'; -import {useAppSettings} from '@vaults/contexts/useAppSettings'; import {useVaultFilter} from '@vaults/hooks/useFilteredVaults'; import {useSortVaults} from '@vaults/hooks/useSortVaults'; +import {useQueryArguments} from '@vaults/hooks/useVaultsQueryArgs'; import {Wrapper} from '@vaults/Wrapper'; import {Button} from '@yearn-finance/web-lib/components/Button'; import {Renderable} from '@yearn-finance/web-lib/components/Renderable'; import {useWeb3} from '@yearn-finance/web-lib/contexts/useWeb3'; -import {useSessionStorage} from '@yearn-finance/web-lib/hooks/useSessionStorage'; import {IconChain} from '@yearn-finance/web-lib/icons/IconChain'; import {formatAmount} from '@yearn-finance/web-lib/utils/format.number'; import {isZero} from '@yearn-finance/web-lib/utils/isZero'; @@ -91,48 +90,21 @@ function HeaderUserPosition(): ReactElement { ); } -function Index(): ReactElement { +function ListOfVaults(): ReactElement { const {isLoadingVaultList} = useYearn(); - const [sort, set_sort] = useSessionStorage<{ - sortBy: TPossibleSortBy; - sortDirection: TSortDirection; - }>('yVaultsSorting', {sortBy: 'featuringScore', sortDirection: 'desc'}); - const {category, selectedChains, set_category, set_selectedChains} = useAppSettings(); - const chainsFromJSON = useMemo((): number[] => JSON.parse(selectedChains || '[]') as number[], [selectedChains]); - const categoriesFromJSON = useMemo((): string[] => JSON.parse(category || '[]') as string[], [category]); - const {activeVaults, migratableVaults, retiredVaults} = useVaultFilter(); - const [searchParam, set_searchParam] = useQueryParams({search: StringParam}); - const [search, set_search] = useState(searchParam?.search); - - /** 🔵 - Yearn ********************************************************************************* - ** This useEffect hook is used to synchronize the search state with the query parameter - ** It checks if the search state and the search query parameter are the same, if they are, - ** it does nothing. - ** If the search state is undefined and the search query parameter is not, it sets the search - ** state to the value of the search query parameter. - ** If the search state is not undefined, it updates the search query parameter to match the - ** search state. - ** If the search state is undefined, it removes the search query parameter. - *********************************************************************************************/ - useEffect((): void => { - // If the search state and the search query parameter are the same, do nothing - if (searchParam.search === search) { - return; - } - // If the search state is undefined and the search query parameter is not, set the search - // state to the value of the search query parameter - if (search === undefined && searchParam.search !== undefined) { - set_search(searchParam.search); - return; - } - // If the search state is not undefined, update the search query parameter to match - // the search state - if (!search) { - set_searchParam({}, 'push'); - } else { - set_searchParam({search: search}, 'push'); - } - }, [searchParam, search, set_searchParam]); + const { + search, + categories, + chains, + sortDirection, + sortBy, + onSearch, + onChangeCategories, + onChangeChains, + onChangeSortDirection, + onChangeSortBy + } = useQueryArguments(); + const {activeVaults, migratableVaults, retiredVaults} = useVaultFilter(categories, chains); /* 🔵 - Yearn Finance ************************************************************************** ** Then, on the activeVaults list, we apply the search filter. The search filter is @@ -144,14 +116,11 @@ function Index(): ReactElement { } return activeVaults.filter((vault: TYDaemonVault): boolean => { const lowercaseSearch = search.toLowerCase(); - return ( - vault.name.toLowerCase().startsWith(lowercaseSearch) || - vault.symbol.toLowerCase().startsWith(lowercaseSearch) || - vault.token.name.toLowerCase().startsWith(lowercaseSearch) || - vault.token.symbol.toLowerCase().startsWith(lowercaseSearch) || - vault.address.toLowerCase().startsWith(lowercaseSearch) || - vault.token.address.toLowerCase().startsWith(lowercaseSearch) - ); + const splitted = + `${vault.name} ${vault.symbol} ${vault.token.name} ${vault.token.symbol} ${vault.address} ${vault.token.address}` + .toLowerCase() + .split(' '); + return splitted.some((word): boolean => word.startsWith(lowercaseSearch)); }); }, [activeVaults, search]); @@ -160,48 +129,24 @@ function Index(): ReactElement { ** is done via a custom method that will sort the vaults based on the sortBy and ** sortDirection values. **********************************************************************************************/ - const sortedVaultsToDisplay = useSortVaults([...searchedVaultsToDisplay], sort.sortBy, sort.sortDirection); - - /* 🔵 - Yearn Finance ************************************************************************** - ** Callback method used to sort the vaults list. - ** The use of useCallback() is to prevent the method from being re-created on every render. - **********************************************************************************************/ - const onSort = useCallback( - (newSortBy: string, newSortDirection: string): void => { - set_sort({ - sortBy: newSortBy as TPossibleSortBy, - sortDirection: newSortDirection as TSortDirection - }); - }, - [set_sort] - ); + const sortedVaultsToDisplay = useSortVaults([...searchedVaultsToDisplay], sortBy, sortDirection); /* 🔵 - Yearn Finance ************************************************************************** ** The VaultList component is memoized to prevent it from being re-created on every render. ** It contains either the list of vaults, is some are available, or a message to the user. **********************************************************************************************/ const VaultList = useMemo((): ReactNode => { - const filteredByChains = sortedVaultsToDisplay.filter((vault): boolean => - chainsFromJSON.includes(vault.chainID) - ); + const filteredByChains = sortedVaultsToDisplay.filter(({chainID}): boolean => chains.includes(chainID)); - if (isLoadingVaultList && categoriesFromJSON.includes('Holdings')) { - return ( - - ); - } - if (isLoadingVaultList || isZero(filteredByChains.length) || chainsFromJSON.length === 0) { + if (isLoadingVaultList || isZero(filteredByChains.length) || chains.length === 0) { return ( ); } @@ -216,87 +161,95 @@ function Index(): ReactElement { /> ); }); - }, [categoriesFromJSON, chainsFromJSON, isLoadingVaultList, sortedVaultsToDisplay]); + }, [categories, chains, isLoadingVaultList, onChangeCategories, onChangeChains, sortedVaultsToDisplay]); return ( -
- - -
-
- +
+
+ +
+ + + 0}> +
+ {retiredVaults + .filter((vault): boolean => !!vault) + .map( + (vault): ReactNode => ( + + ) + )}
- set_search(value)} - /> - - 0}> -
- {retiredVaults - .filter((vault): boolean => !!vault) - .map( - (vault): ReactNode => ( - - ) - )} -
-
- - 0}> -
- {migratableVaults - .filter((vault): boolean => !!vault) - .map( - (vault): ReactNode => ( - - ) - )} -
-
- -
- , value: 'chain', sortable: false, className: 'col-span-1'}, - {label: 'Token', value: 'name', sortable: true}, - {label: 'Est. APR', value: 'forwardAPR', sortable: true, className: 'col-span-2'}, - {label: 'Hist. APR', value: 'apr', sortable: true, className: 'col-span-2'}, - {label: 'Available', value: 'available', sortable: true, className: 'col-span-2'}, - {label: 'Deposited', value: 'deposited', sortable: true, className: 'col-span-2'}, - {label: 'TVL', value: 'tvl', sortable: true, className: 'col-span-2'} - ]} - /> + + + 0}> +
+ {migratableVaults + .filter((vault): boolean => !!vault) + .map( + (vault): ReactNode => ( + + ) + )} +
+
+ +
+ { + onChangeSortBy(newSortBy as TPossibleSortBy); + onChangeSortDirection(newSortDirection as TSortDirection); + }} + items={[ + {label: , value: 'chain', sortable: false, className: 'col-span-1'}, + {label: 'Token', value: 'name', sortable: true}, + {label: 'Est. APR', value: 'estAPR', sortable: true, className: 'col-span-2'}, + {label: 'Hist. APR', value: 'apr', sortable: true, className: 'col-span-2'}, + {label: 'Available', value: 'available', sortable: true, className: 'col-span-2'}, + {label: 'Deposited', value: 'deposited', sortable: true, className: 'col-span-2'}, + {label: 'TVL', value: 'tvl', sortable: true, className: 'col-span-2'} + ]} + /> + + {VaultList} +
+ ); +} - {VaultList} -
+function Index(): ReactElement { + return ( +
+ + + +
); } Index.getLayout = function getLayout(page: ReactElement, router: NextRouter): ReactElement { - return ( - - {page} - - ); + return {page}; }; export default Index;