diff --git a/.gitignore b/.gitignore index 8cd357d0..33a7761f 100644 --- a/.gitignore +++ b/.gitignore @@ -54,4 +54,4 @@ next-env.d.ts CLAUDE.md FULLAUTO_CONTEXT.md -.claude/settings.local.json +.claude/settings.local.json \ No newline at end of file diff --git a/app/layout.tsx b/app/layout.tsx index 52ef6b7b..f60005f6 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -7,6 +7,7 @@ import RiskNotificationModal from '@/modals/risk-notification-modal'; import { VaultRegistryProvider } from '@/contexts/VaultRegistryContext'; import OnchainProviders from '@/OnchainProviders'; import { ModalRenderer } from '@/components/modals/ModalRenderer'; +import { StorageMigrator } from '@/components/StorageMigrator'; import { initAnalytics } from '@/utils/analytics'; import { ThemeProviders } from '../src/components/providers/ThemeProvider'; @@ -37,6 +38,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) + {children} diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 49a5ecb4..8929dbfd 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -7,19 +7,22 @@ import { IconSwitch } from '@/components/ui/icon-switch'; import Header from '@/components/layout/header/Header'; import { AdvancedRpcSettings } from '@/modals/settings/custom-rpc-settings'; import { VaultIdentity } from '@/features/autovault/components/vault-identity'; -import { defaultTrustedVaults, type TrustedVault } from '@/constants/vaults/known_vaults'; -import { useLocalStorage } from '@/hooks/useLocalStorage'; -import { useMarkets } from '@/hooks/useMarkets'; +import { defaultTrustedVaults } from '@/constants/vaults/known_vaults'; import { useModal } from '@/hooks/useModal'; -import { storageKeys } from '@/utils/storageKeys'; +import { useTrustedVaults } from '@/stores/useTrustedVaults'; +import { useAppSettings } from '@/stores/useAppSettings'; +import { useMarketPreferences } from '@/stores/useMarketPreferences'; export default function SettingsPage() { - const [usePermit2, setUsePermit2] = useLocalStorage('usePermit2', true); - const [includeUnknownTokens, setIncludeUnknownTokens] = useLocalStorage('includeUnknownTokens', false); - const [showUnknownOracle, setShowUnknownOracle] = useLocalStorage('showUnknownOracle', false); - const [userTrustedVaults, setUserTrustedVaults] = useLocalStorage(storageKeys.UserTrustedVaultsKey, defaultTrustedVaults); + // App settings from Zustand store + const { usePermit2, setUsePermit2, showUnwhitelistedMarkets, setShowUnwhitelistedMarkets, isAprDisplay, setIsAprDisplay } = + useAppSettings(); + + // Market preferences from Zustand store + const { includeUnknownTokens, setIncludeUnknownTokens, showUnknownOracle, setShowUnknownOracle } = useMarketPreferences(); + + const { vaults: userTrustedVaults } = useTrustedVaults(); - const { showUnwhitelistedMarkets, setShowUnwhitelistedMarkets, isAprDisplay, setIsAprDisplay } = useMarkets(); const { open: openModal } = useModal(); const [mounted, setMounted] = React.useState(false); @@ -184,12 +187,7 @@ export default function SettingsPage() { diff --git a/src/components/StorageMigrator.tsx b/src/components/StorageMigrator.tsx new file mode 100644 index 00000000..13d07e9f --- /dev/null +++ b/src/components/StorageMigrator.tsx @@ -0,0 +1,438 @@ +'use client'; + +import { useEffect } from 'react'; +import storage from 'local-storage-fallback'; +import type { TrustedVault } from '@/constants/vaults/known_vaults'; +import { useTrustedVaults } from '@/stores/useTrustedVaults'; +import { useMarketPreferences } from '@/stores/useMarketPreferences'; +import { useAppSettings } from '@/stores/useAppSettings'; +import { useHistoryPreferences } from '@/stores/useHistoryPreferences'; +import { usePositionsPreferences } from '@/stores/usePositionsPreferences'; +import { useBlacklistedMarkets, type BlacklistedMarket } from '@/stores/useBlacklistedMarkets'; +import { useCustomRpc, type CustomRpcUrls } from '@/stores/useCustomRpc'; +import { useTransactionFiltersStore } from '@/stores/useTransactionFilters'; +import { useUserMarketsCacheStore } from '@/stores/useUserMarketsCache'; +import { SortColumn } from '@/features/markets/components/constants'; +import { DEFAULT_MIN_SUPPLY_USD } from '@/constants/markets'; +import { DEFAULT_COLUMN_VISIBILITY } from '@/features/markets/components/column-visibility'; +import { + type MigrationDefinition, + isMigrationComplete, + isMigrationExpired, + markMigrationComplete, + executeAllMigrations, +} from '@/utils/storage-migration'; + +/** + * One-Time Storage Migrator Component + * + * **Purpose:** Migrate user data from old useLocalStorage format to new Zustand stores + * + * **Timeline:** + * - Created: January 2025 + * - Expires: February 1, 2025 + * + * + * **To delete (after Feb 2025):** + * 1. Remove `` from `app/layout.tsx` + * 2. Delete this file: `src/components/StorageMigrator.tsx` + * 3. Delete `src/utils/storage-migration.ts` + * + * @returns null - This component renders nothing + */ + +// Type definitions for migrations +type SymbolFilters = Record< + string, + { + minSupplyAmount: string; + minBorrowAmount: string; + } +>; + +type MarketIdentifier = { + marketUniqueKey: string; + chainId: number; +}; + +type UserMarketsCache = Record; + +/** + * Helper: Safely parse JSON from localStorage + */ +function _safeParseJSON(value: string | null, defaultValue: T): T { + if (!value) return defaultValue; + try { + return JSON.parse(value) as T; + } catch { + return defaultValue; + } +} + +/** + * Helper: Get localStorage value or default + */ +function getStorageValue(key: string, defaultValue: T): T { + const value = storage.getItem(key); + if (value === null) return defaultValue; + + // If default is a boolean/number, parse accordingly + if (typeof defaultValue === 'boolean') { + return (value === 'true') as T; + } + if (typeof defaultValue === 'number') { + const parsed = Number(value); + return (Number.isNaN(parsed) ? defaultValue : parsed) as T; + } + + // Try JSON parse + try { + return JSON.parse(value) as T; + } catch { + return value as T; + } +} + +export function StorageMigrator() { + useEffect(() => { + // Skip if already completed + if (isMigrationComplete()) { + console.log('โœ… Storage migrations already completed - skipping'); + return; + } + + // Skip if expired (with warning) + if (isMigrationExpired()) { + return; + } + + // Define all migrations + const migrations: MigrationDefinition[] = [ + // ======================================== + // Migration 1: Trusted Vaults + // ======================================== + { + oldKey: 'userTrustedVaults', + newKey: 'monarch_store_trustedVaults', + storeName: 'useTrustedVaults', + description: 'Migrated Trusted Vaults to Zustand store', + validate: (data): data is TrustedVault[] => { + if (!Array.isArray(data)) return false; + return data.every( + (item) => + item && + typeof item === 'object' && + 'address' in item && + 'chainId' in item && + typeof item.address === 'string' && + typeof item.chainId === 'number', + ); + }, + migrate: (oldData: TrustedVault[]) => { + try { + const store = useTrustedVaults.getState(); + store.setVaults(oldData); + return true; + } catch (error) { + console.error('Error migrating trusted vaults:', error); + return false; + } + }, + }, + + // ======================================== + // Migration 2: Market Preferences (MULTI-KEY) + // ======================================== + { + oldKey: 'monarch_marketsSortColumn', // Primary key to check + newKey: 'monarch_store_marketPreferences', + storeName: 'useMarketPreferences', + description: 'Migrated Market Preferences to Zustand store (15 keys)', + // No validate function - we handle all keys ourselves + migrate: () => { + try { + // Read all old market preference keys + const sortColumn = getStorageValue('monarch_marketsSortColumn', SortColumn.Supply); + const sortDirection = getStorageValue('monarch_marketsSortDirection', -1); + const entriesPerPage = getStorageValue('monarch_marketsEntriesPerPage', 8); + const includeUnknownTokens = getStorageValue('includeUnknownTokens', false); + const showUnknownOracle = getStorageValue('showUnknownOracle', false); + const trustedVaultsOnly = getStorageValue('monarch_marketsTrustedVaultsOnly', false); + const columnVisibility = getStorageValue('monarch_marketsColumnVisibility', DEFAULT_COLUMN_VISIBILITY); + const tableViewMode = getStorageValue('monarch_marketsTableViewMode', 'compact'); + const usdMinSupply = getStorageValue('monarch_marketsUsdMinSupply_2', DEFAULT_MIN_SUPPLY_USD.toString()); + const usdMinBorrow = getStorageValue('monarch_marketsUsdMinBorrow', ''); + const usdMinLiquidity = getStorageValue('monarch_marketsUsdMinLiquidity', ''); + const minSupplyEnabled = getStorageValue('monarch_minSupplyEnabled', false); + const minBorrowEnabled = getStorageValue('monarch_minBorrowEnabled', false); + const minLiquidityEnabled = getStorageValue('monarch_minLiquidityEnabled', false); + const starredMarkets = getStorageValue('monarch_marketsFavorites', []); + + // Write to store + const store = useMarketPreferences.getState(); + store.setAll({ + sortColumn, + sortDirection, + entriesPerPage, + includeUnknownTokens, + showUnknownOracle, + trustedVaultsOnly, + columnVisibility, + tableViewMode, + usdMinSupply, + usdMinBorrow, + usdMinLiquidity, + minSupplyEnabled, + minBorrowEnabled, + minLiquidityEnabled, + starredMarkets, + }); + + return true; + } catch (error) { + console.error('Error migrating market preferences:', error); + return false; + } + }, + }, + + // ======================================== + // Migration 3: App Settings (MULTI-KEY) + // ======================================== + { + oldKey: 'usePermit2', // Primary key to check + newKey: 'monarch_store_appSettings', + storeName: 'useAppSettings', + description: 'Migrated App Settings to Zustand store (5 keys)', + // No validate function - we handle all keys ourselves + migrate: () => { + try { + // Read all old app setting keys + const usePermit2 = getStorageValue('usePermit2', true); + const useEth = getStorageValue('useEth', false); + const showUnwhitelistedMarkets = getStorageValue('showUnwhitelistedMarkets', false); + const showFullRewardAPY = getStorageValue('showFullRewardAPY', false); + const isAprDisplay = getStorageValue('settings-apr-display', false); + + // Write to store + const store = useAppSettings.getState(); + store.setAll({ + usePermit2, + useEth, + showUnwhitelistedMarkets, + showFullRewardAPY, + isAprDisplay, + }); + + return true; + } catch (error) { + console.error('Error migrating app settings:', error); + return false; + } + }, + }, + + // ======================================== + // Migration 4: History Preferences (MULTI-KEY) + // ======================================== + { + oldKey: 'monarch_historyEntriesPerPage', // Primary key to check + newKey: 'monarch_store_historyPreferences', + storeName: 'useHistoryPreferences', + description: 'Migrated History Preferences to Zustand store (2 keys)', + // No validate function - we handle all keys ourselves + migrate: () => { + try { + // Read all old history preference keys + const entriesPerPage = getStorageValue('monarch_historyEntriesPerPage', 10); + const isGroupedView = getStorageValue('monarch_historyGroupedView', true); + + // Write to store + const store = useHistoryPreferences.getState(); + store.setAll({ + entriesPerPage, + isGroupedView, + }); + + return true; + } catch (error) { + console.error('Error migrating history preferences:', error); + return false; + } + }, + }, + + // ======================================== + // Migration 5: Positions Preferences + // ======================================== + { + oldKey: 'positions:show-collateral-exposure', + newKey: 'monarch_store_positionsPreferences', + storeName: 'usePositionsPreferences', + description: 'Migrated Positions Preferences to Zustand store', + // No validate function - we handle it ourselves + migrate: () => { + try { + const showCollateralExposure = getStorageValue('positions:show-collateral-exposure', true); + + const store = usePositionsPreferences.getState(); + store.setShowCollateralExposure(showCollateralExposure); + + return true; + } catch (error) { + console.error('Error migrating positions preferences:', error); + return false; + } + }, + }, + + // ======================================== + // Migration 6: Blacklisted Markets + // ======================================== + { + oldKey: 'customBlacklistedMarkets', + newKey: 'monarch_store_blacklistedMarkets', + storeName: 'useBlacklistedMarkets', + description: 'Migrated Blacklisted Markets to Zustand store', + validate: (data): data is BlacklistedMarket[] => { + if (!Array.isArray(data)) return false; + return data.every( + (item) => + item && + typeof item === 'object' && + 'uniqueKey' in item && + 'chainId' in item && + 'addedAt' in item && + typeof item.uniqueKey === 'string' && + typeof item.chainId === 'number' && + typeof item.addedAt === 'number', + ); + }, + migrate: (oldData: BlacklistedMarket[]) => { + try { + const store = useBlacklistedMarkets.getState(); + store.setAll({ customBlacklistedMarkets: oldData }); + return true; + } catch (error) { + console.error('Error migrating blacklisted markets:', error); + return false; + } + }, + }, + + // ======================================== + // Migration 7: Custom RPC URLs + // ======================================== + { + oldKey: 'customRpcUrls', + newKey: 'monarch_store_customRpc', + storeName: 'useCustomRpc', + description: 'Migrated Custom RPC URLs to Zustand store', + validate: (data): data is CustomRpcUrls => { + return typeof data === 'object' && data !== null; + }, + migrate: (oldData: CustomRpcUrls) => { + try { + const store = useCustomRpc.getState(); + store.setAll({ customRpcUrls: oldData }); + return true; + } catch (error) { + console.error('Error migrating custom RPC URLs:', error); + return false; + } + }, + }, + + // ======================================== + // Migration 8: Transaction Filters + // ======================================== + { + oldKey: 'monarch_transaction_filters_v2', + newKey: 'monarch_store_transactionFilters', + storeName: 'useTransactionFilters', + description: 'Migrated Transaction Filters to Zustand store', + validate: (data): data is SymbolFilters => { + return typeof data === 'object' && data !== null; + }, + migrate: (oldData: SymbolFilters) => { + try { + const store = useTransactionFiltersStore.getState(); + store.setAll({ filters: oldData }); + return true; + } catch (error) { + console.error('Error migrating transaction filters:', error); + return false; + } + }, + }, + + // ======================================== + // Migration 9: User Markets Cache + // ======================================== + { + oldKey: 'monarch_cache_market_unique_keys', + newKey: 'monarch_store_userMarketsCache', + storeName: 'useUserMarketsCache', + description: 'Migrated User Markets Cache to Zustand store', + validate: (data): data is UserMarketsCache => { + return typeof data === 'object' && data !== null; + }, + migrate: (oldData: UserMarketsCache) => { + try { + const store = useUserMarketsCacheStore.getState(); + store.setAll({ cache: oldData }); + return true; + } catch (error) { + console.error('Error migrating user markets cache:', error); + return false; + } + }, + }, + ]; + + // Execute all migrations + const results = executeAllMigrations(migrations); + + // Clean up ALL old localStorage keys after migrations + // (executeMigration only deletes the primary oldKey, not related keys) + const anySuccess = results.some((r) => r.status === 'success'); + if (anySuccess || results.every((r) => r.status === 'skipped')) { + console.log('๐Ÿงน Cleaning up old localStorage keys...'); + + // Market Preferences keys + const marketKeys = [ + 'monarch_marketsSortDirection', + 'monarch_marketsEntriesPerPage', + 'includeUnknownTokens', + 'showUnknownOracle', + 'monarch_marketsTrustedVaultsOnly', + 'monarch_marketsColumnVisibility', + 'monarch_marketsTableViewMode', + 'monarch_marketsUsdMinSupply_2', + 'monarch_marketsUsdMinBorrow', + 'monarch_marketsUsdMinLiquidity', + 'monarch_minSupplyEnabled', + 'monarch_minBorrowEnabled', + 'monarch_minLiquidityEnabled', + 'monarch_marketsFavorites', + ]; + + // App Settings keys + const appKeys = ['useEth', 'showUnwhitelistedMarkets', 'showFullRewardAPY', 'settings-apr-display']; + + // History Preferences keys + const historyKeys = ['monarch_historyGroupedView']; + + // Delete all secondary keys (primary keys already deleted by executeMigration) + [...marketKeys, ...appKeys, ...historyKeys].forEach((key) => { + storage.removeItem(key); + }); + + console.log('โœ… Cleanup complete'); + markMigrationComplete(); + } + }, []); // Run once on mount + + // Render nothing + return null; +} diff --git a/src/components/common/table-container-with-header.tsx b/src/components/common/table-container-with-header.tsx index 31034cdf..e976f029 100644 --- a/src/components/common/table-container-with-header.tsx +++ b/src/components/common/table-container-with-header.tsx @@ -3,6 +3,7 @@ type TableContainerWithHeaderProps = { actions?: React.ReactNode; children: React.ReactNode; className?: string; + noPadding?: boolean; }; /** @@ -29,14 +30,14 @@ type TableContainerWithHeaderProps = { * ...
* */ -export function TableContainerWithHeader({ title, actions, children, className = '' }: TableContainerWithHeaderProps) { +export function TableContainerWithHeader({ title, actions, children, className = '', noPadding = false }: TableContainerWithHeaderProps) { return (

{title}

{actions &&
{actions}
}
-
{children}
+
{children}
); } diff --git a/src/components/providers/CustomRpcProvider.tsx b/src/components/providers/CustomRpcProvider.tsx index 23abd81c..3f35570d 100644 --- a/src/components/providers/CustomRpcProvider.tsx +++ b/src/components/providers/CustomRpcProvider.tsx @@ -1,7 +1,7 @@ 'use client'; import { createContext, useContext, useEffect, useState, type ReactNode, useMemo } from 'react'; -import { useCustomRpc, type CustomRpcUrls } from '@/hooks/useCustomRpc'; +import { useCustomRpc, type CustomRpcUrls } from '@/stores/useCustomRpc'; import type { SupportedNetworks } from '@/utils/networks'; type CustomRpcContextType = { diff --git a/src/components/shared/rate-formatted.tsx b/src/components/shared/rate-formatted.tsx index 5599724d..a0caac01 100644 --- a/src/components/shared/rate-formatted.tsx +++ b/src/components/shared/rate-formatted.tsx @@ -1,4 +1,4 @@ -import { useMarkets } from '@/hooks/useMarkets'; +import { useAppSettings } from '@/stores/useAppSettings'; import { convertApyToApr } from '@/utils/rateMath'; type RateFormattedProps = { @@ -35,7 +35,7 @@ type RateFormattedProps = { * */ export function RateFormatted({ value, showLabel = false, precision = 2, className = '' }: RateFormattedProps) { - const { isAprDisplay } = useMarkets(); + const { isAprDisplay } = useAppSettings(); // Convert APY to APR if the user has enabled APR display mode const displayValue = isAprDisplay ? convertApyToApr(value) : value; diff --git a/src/components/status/loading-screen.tsx b/src/components/status/loading-screen.tsx index 010d013d..de59e3a9 100644 --- a/src/components/status/loading-screen.tsx +++ b/src/components/status/loading-screen.tsx @@ -100,9 +100,7 @@ export default function LoadingScreen({ message, className }: LoadingScreenProps return (
void) => void; refresh: () => Promise; - showUnwhitelistedMarkets: boolean; - setShowUnwhitelistedMarkets: (value: boolean) => void; - showFullRewardAPY: boolean; - setShowFullRewardAPY: (value: boolean) => void; - isAprDisplay: boolean; - setIsAprDisplay: (value: boolean) => void; - isBlacklisted: (uniqueKey: string) => boolean; - addBlacklistedMarket: (uniqueKey: string, chainId: number, reason?: string) => boolean; - removeBlacklistedMarket: (uniqueKey: string) => void; - isDefaultBlacklisted: (uniqueKey: string) => boolean; }; const MarketsContext = createContext(undefined); @@ -48,18 +38,14 @@ export function MarketsProvider({ children }: MarketsProviderProps) { // Store raw unfiltered markets to avoid refetching when blacklist changes const [rawMarkets, setRawMarkets] = useState([]); - // Global setting for showing unwhitelisted markets - const [showUnwhitelistedMarkets, setShowUnwhitelistedMarkets] = useLocalStorage('showUnwhitelistedMarkets', false); + // Global settings from Zustand store + const { showUnwhitelistedMarkets } = useAppSettings(); - // Global setting for showing full reward APY (base + external rewards) - const [showFullRewardAPY, setShowFullRewardAPY] = useLocalStorage('showFullRewardAPY', false); + // Blacklisted markets management from Zustand store (internal use only for filtering) + const { getAllBlacklistedKeys, customBlacklistedMarkets } = useBlacklistedMarkets(); - // Global setting for showing APR instead of APY - const [isAprDisplay, setIsAprDisplay] = useLocalStorage('settings-apr-display', false); - - // Blacklisted markets management - const { allBlacklistedMarketKeys, addBlacklistedMarket, removeBlacklistedMarket, isBlacklisted, isDefaultBlacklisted } = - useBlacklistedMarkets(); + // Get all blacklisted keys for filtering - memoize to prevent infinite loops + const allBlacklistedMarketKeys = useMemo(() => getAllBlacklistedKeys(), [customBlacklistedMarkets, getAllBlacklistedKeys]); // Oracle data context for enriching markets const { getOracleData } = useOracleDataContext(); @@ -302,38 +288,8 @@ export function MarketsProvider({ children }: MarketsProviderProps) { error: combinedError, refetch, refresh, - showUnwhitelistedMarkets, - setShowUnwhitelistedMarkets, - showFullRewardAPY, - setShowFullRewardAPY, - isAprDisplay, - setIsAprDisplay, - isBlacklisted, - addBlacklistedMarket, - removeBlacklistedMarket, - isDefaultBlacklisted, }), - [ - markets, - whitelistedMarkets, - allMarkets, - rawMarkets, - isLoading, - isRefetching, - combinedError, - refetch, - refresh, - showUnwhitelistedMarkets, - setShowUnwhitelistedMarkets, - showFullRewardAPY, - setShowFullRewardAPY, - isAprDisplay, - setIsAprDisplay, - isBlacklisted, - addBlacklistedMarket, - removeBlacklistedMarket, - isDefaultBlacklisted, - ], + [markets, whitelistedMarkets, allMarkets, rawMarkets, isLoading, isRefetching, combinedError, refetch, refresh], ); return {children}; diff --git a/src/features/autovault/components/vault-detail/allocations/allocations/market-view.tsx b/src/features/autovault/components/vault-detail/allocations/allocations/market-view.tsx index 7c99696e..aee5c989 100644 --- a/src/features/autovault/components/vault-detail/allocations/allocations/market-view.tsx +++ b/src/features/autovault/components/vault-detail/allocations/allocations/market-view.tsx @@ -1,6 +1,6 @@ import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table'; import { MarketIdentity, MarketIdentityFocus } from '@/features/markets/components/market-identity'; -import { useMarkets } from '@/hooks/useMarkets'; +import { useAppSettings } from '@/stores/useAppSettings'; import { useRateLabel } from '@/hooks/useRateLabel'; import type { MarketAllocation } from '@/types/vaultAllocations'; import { formatBalance, formatReadable } from '@/utils/balance'; @@ -18,7 +18,7 @@ type MarketViewProps = { }; export function MarketView({ allocations, totalAllocation, vaultAssetSymbol, vaultAssetDecimals, chainId }: MarketViewProps) { - const { isAprDisplay } = useMarkets(); + const { isAprDisplay } = useAppSettings(); const { short: rateLabel } = useRateLabel(); // Sort by allocation amount (most to least) diff --git a/src/features/autovault/components/vault-detail/modals/deposit-to-vault-modal.tsx b/src/features/autovault/components/vault-detail/modals/deposit-to-vault-modal.tsx index 88bda90d..cccff212 100644 --- a/src/features/autovault/components/vault-detail/modals/deposit-to-vault-modal.tsx +++ b/src/features/autovault/components/vault-detail/modals/deposit-to-vault-modal.tsx @@ -6,7 +6,7 @@ import { Modal, ModalBody, ModalHeader } from '@/components/common/Modal'; import Input from '@/components/Input/Input'; import { ExecuteTransactionButton } from '@/components/ui/ExecuteTransactionButton'; import { TokenIcon } from '@/components/shared/token-icon'; -import { useLocalStorage } from '@/hooks/useLocalStorage'; +import { useAppSettings } from '@/stores/useAppSettings'; import { useVaultV2Deposit } from '@/hooks/useVaultV2Deposit'; import { formatBalance } from '@/utils/balance'; import { VaultDepositProcessModal } from './vault-deposit-process-modal'; @@ -32,7 +32,7 @@ export function DepositToVaultModal({ onClose, onSuccess, }: DepositToVaultModalProps): JSX.Element { - const [usePermit2Setting] = useLocalStorage('usePermit2', true); + const { usePermit2: usePermit2Setting } = useAppSettings(); const { depositAmount, diff --git a/src/features/history/components/history-table.tsx b/src/features/history/components/history-table.tsx index 461617da..e36375a2 100644 --- a/src/features/history/components/history-table.tsx +++ b/src/features/history/components/history-table.tsx @@ -27,12 +27,11 @@ import { RebalanceDetail } from './rebalance-detail'; import { useMarkets } from '@/contexts/MarketsContext'; import useUserTransactions from '@/hooks/useUserTransactions'; import { useDisclosure } from '@/hooks/useDisclosure'; -import { useLocalStorage } from '@/hooks/useLocalStorage'; +import { useHistoryPreferences } from '@/stores/useHistoryPreferences'; import { useStyledToast } from '@/hooks/useStyledToast'; import { formatReadable } from '@/utils/balance'; import { getNetworkImg, getNetworkName } from '@/utils/networks'; import { groupTransactionsByHash, getWithdrawals, getSupplies, type GroupedTransaction } from '@/utils/transactionGrouping'; -import { storageKeys } from '@/utils/storageKeys'; import { UserTxTypes, type Market, type MarketPosition, type UserTransaction } from '@/utils/types'; type HistoryTableProps = { @@ -87,9 +86,8 @@ export function HistoryTable({ account, positions, isVaultAdapter = false }: His const [isInitialized, setIsInitialized] = useState(false); const [totalPages, setTotalPages] = useState(0); - // Settings state - const [pageSize, setPageSize] = useLocalStorage(storageKeys.HistoryEntriesPerPageKey, 10); - const [isGroupedView, setIsGroupedView] = useLocalStorage(storageKeys.HistoryGroupedViewKey, true); + // Settings state from Zustand store + const { entriesPerPage: pageSize, setEntriesPerPage: setPageSize, isGroupedView, setIsGroupedView } = useHistoryPreferences(); const [expandedRows, setExpandedRows] = useState>(new Set()); const { isOpen: isSettingsOpen, onOpen: onSettingsOpen, onOpenChange: onSettingsOpenChange } = useDisclosure(); diff --git a/src/features/market-detail/components/charts/rate-chart.tsx b/src/features/market-detail/components/charts/rate-chart.tsx index 8d6dc5d8..3b822cbc 100644 --- a/src/features/market-detail/components/charts/rate-chart.tsx +++ b/src/features/market-detail/components/charts/rate-chart.tsx @@ -7,7 +7,7 @@ import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, Legend, Responsi import ButtonGroup from '@/components/ui/button-group'; import { Spinner } from '@/components/ui/spinner'; import { CHART_COLORS } from '@/constants/chartColors'; -import { useMarkets } from '@/hooks/useMarkets'; +import { useAppSettings } from '@/stores/useAppSettings'; import { useRateLabel } from '@/hooks/useRateLabel'; import { convertApyToApr } from '@/utils/rateMath'; import type { MarketRates } from '@/utils/types'; @@ -23,7 +23,7 @@ type RateChartProps = { }; function RateChart({ historicalData, market, isLoading, selectedTimeframe, selectedTimeRange, handleTimeframeChange }: RateChartProps) { - const { isAprDisplay } = useMarkets(); + const { isAprDisplay } = useAppSettings(); const { short: rateLabel } = useRateLabel(); const [visibleLines, setVisibleLines] = useState({ diff --git a/src/features/market-detail/components/position-stats.tsx b/src/features/market-detail/components/position-stats.tsx index 52468240..bb2f4ba2 100644 --- a/src/features/market-detail/components/position-stats.tsx +++ b/src/features/market-detail/components/position-stats.tsx @@ -7,7 +7,7 @@ import { HiOutlineGlobeAsiaAustralia } from 'react-icons/hi2'; import { Spinner } from '@/components/ui/spinner'; import { TokenIcon } from '@/components/shared/token-icon'; import { useMarketCampaigns } from '@/hooks/useMarketCampaigns'; -import { useMarkets } from '@/hooks/useMarkets'; +import { useAppSettings } from '@/stores/useAppSettings'; import { useRateLabel } from '@/hooks/useRateLabel'; import { formatBalance, formatReadable } from '@/utils/balance'; import { getTruncatedAssetName } from '@/utils/oracle'; @@ -36,7 +36,7 @@ export function PositionStats({ market, userPosition, positionLoading, cardStyle // Default to user view if they have a position, otherwise global const [viewMode, setViewMode] = useState<'global' | 'user'>(userPosition && hasPosition(userPosition) ? 'user' : 'global'); - const { showFullRewardAPY, isAprDisplay } = useMarkets(); + const { showFullRewardAPY, isAprDisplay } = useAppSettings(); const { label: rateLabel } = useRateLabel({ prefix: 'Supply' }); const { label: borrowRateLabel } = useRateLabel({ prefix: 'Borrow' }); const { activeCampaigns, hasActiveRewards } = useMarketCampaigns({ diff --git a/src/features/market-detail/market-view.tsx b/src/features/market-detail/market-view.tsx index 13ff3191..88cb1af3 100644 --- a/src/features/market-detail/market-view.tsx +++ b/src/features/market-detail/market-view.tsx @@ -21,7 +21,7 @@ import { useModal } from '@/hooks/useModal'; import { useMarketData } from '@/hooks/useMarketData'; import { useMarketHistoricalData } from '@/hooks/useMarketHistoricalData'; import { useOraclePrice } from '@/hooks/useOraclePrice'; -import { useTransactionFilters } from '@/hooks/useTransactionFilters'; +import { useTransactionFilters } from '@/stores/useTransactionFilters'; import useUserPositions from '@/hooks/useUserPosition'; import MORPHO_LOGO from '@/imgs/tokens/morpho.svg'; import { getExplorerURL, getMarketURL } from '@/utils/external'; diff --git a/src/features/markets/components/apy-breakdown-tooltip.tsx b/src/features/markets/components/apy-breakdown-tooltip.tsx index d983ffb6..136734b0 100644 --- a/src/features/markets/components/apy-breakdown-tooltip.tsx +++ b/src/features/markets/components/apy-breakdown-tooltip.tsx @@ -2,7 +2,7 @@ import type React from 'react'; import { Tooltip } from '@/components/ui/tooltip'; import { TokenIcon } from '@/components/shared/token-icon'; import { useMarketCampaigns } from '@/hooks/useMarketCampaigns'; -import { useMarkets } from '@/hooks/useMarkets'; +import { useAppSettings } from '@/stores/useAppSettings'; import { useRateLabel } from '@/hooks/useRateLabel'; import type { SimplifiedCampaign } from '@/utils/merklTypes'; import { convertApyToApr } from '@/utils/rateMath'; @@ -19,7 +19,7 @@ type APYCellProps = { }; export function APYBreakdownTooltip({ baseAPY, activeCampaigns, children }: APYBreakdownTooltipProps) { - const { isAprDisplay } = useMarkets(); + const { isAprDisplay } = useAppSettings(); const { short: rateLabel } = useRateLabel(); // Convert base rate if APR display is enabled @@ -70,7 +70,7 @@ export function APYBreakdownTooltip({ baseAPY, activeCampaigns, children }: APYB } export function APYCell({ market }: APYCellProps) { - const { showFullRewardAPY, isAprDisplay } = useMarkets(); + const { showFullRewardAPY, isAprDisplay } = useAppSettings(); const { activeCampaigns, hasActiveRewards } = useMarketCampaigns({ marketId: market.uniqueKey, loanTokenAddress: market.loanAsset.address, diff --git a/src/features/markets/components/market-actions-dropdown.tsx b/src/features/markets/components/market-actions-dropdown.tsx index 7ebc6370..e2cab73d 100644 --- a/src/features/markets/components/market-actions-dropdown.tsx +++ b/src/features/markets/components/market-actions-dropdown.tsx @@ -12,28 +12,23 @@ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuIte import type { Market } from '@/utils/types'; import { BlacklistConfirmationModal } from './blacklist-confirmation-modal'; import { useModal } from '@/hooks/useModal'; +import { useStyledToast } from '@/hooks/useStyledToast'; +import { useMarketPreferences } from '@/stores/useMarketPreferences'; +import { useBlacklistedMarkets } from '@/stores/useBlacklistedMarkets'; type MarketActionsDropdownProps = { market: Market; - isStared: boolean; - starMarket: (id: string) => void; - unstarMarket: (id: string) => void; - addBlacklistedMarket?: (uniqueKey: string, chainId: number, reason?: string) => boolean; - isBlacklisted?: (uniqueKey: string) => boolean; }; -export function MarketActionsDropdown({ - market, - isStared, - starMarket, - unstarMarket, - addBlacklistedMarket, - isBlacklisted, -}: MarketActionsDropdownProps) { +export function MarketActionsDropdown({ market }: MarketActionsDropdownProps) { const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false); const { open: openModal } = useModal(); + const { starredMarkets, starMarket, unstarMarket } = useMarketPreferences(); + const { isBlacklisted, addBlacklistedMarket } = useBlacklistedMarkets(); + const { success: toastSuccess } = useStyledToast(); const router = useRouter(); + const isStared = starredMarkets.includes(market.uniqueKey); const handleClick = (e: React.MouseEvent) => { e.stopPropagation(); @@ -45,15 +40,13 @@ export function MarketActionsDropdown({ }; const handleBlacklistClick = () => { - if (!isBlacklisted?.(market.uniqueKey) && addBlacklistedMarket) { + if (!isBlacklisted(market.uniqueKey)) { setIsConfirmModalOpen(true); } }; const handleConfirmBlacklist = () => { - if (addBlacklistedMarket) { - addBlacklistedMarket(market.uniqueKey, market.morphoBlue.chain.id); - } + addBlacklistedMarket(market.uniqueKey, market.morphoBlue.chain.id); }; const onMarketClick = () => { @@ -99,8 +92,10 @@ export function MarketActionsDropdown({ onClick={() => { if (isStared) { unstarMarket(market.uniqueKey); + toastSuccess('Market unstarred', 'Removed from favorites'); } else { starMarket(market.uniqueKey); + toastSuccess('Market starred', 'Added to favorites'); } }} startContent={isStared ? : } @@ -111,10 +106,10 @@ export function MarketActionsDropdown({ } - className={isBlacklisted?.(market.uniqueKey) || !addBlacklistedMarket ? 'opacity-50 cursor-not-allowed' : ''} - disabled={isBlacklisted?.(market.uniqueKey) || !addBlacklistedMarket} + className={isBlacklisted(market.uniqueKey) ? 'opacity-50 cursor-not-allowed' : ''} + disabled={isBlacklisted(market.uniqueKey)} > - {isBlacklisted?.(market.uniqueKey) ? 'Blacklisted' : 'Blacklist'} + {isBlacklisted(market.uniqueKey) ? 'Blacklisted' : 'Blacklist'} diff --git a/src/features/markets/components/market-details-block.tsx b/src/features/markets/components/market-details-block.tsx index c13efa7b..ccefd60e 100644 --- a/src/features/markets/components/market-details-block.tsx +++ b/src/features/markets/components/market-details-block.tsx @@ -3,7 +3,7 @@ import { ChevronDownIcon, ChevronUpIcon, ExternalLinkIcon } from '@radix-ui/reac import { motion, AnimatePresence } from 'framer-motion'; import { formatUnits } from 'viem'; import { useMarketCampaigns } from '@/hooks/useMarketCampaigns'; -import { useMarkets } from '@/hooks/useMarkets'; +import { useAppSettings } from '@/stores/useAppSettings'; import { useRateLabel } from '@/hooks/useRateLabel'; import { formatBalance, formatReadable } from '@/utils/balance'; import { getIRMTitle, previewMarketState } from '@/utils/morpho'; @@ -36,7 +36,7 @@ export function MarketDetailsBlock({ }: MarketDetailsBlockProps): JSX.Element { const [isExpanded, setIsExpanded] = useState(!defaultCollapsed && !disableExpansion); - const { isAprDisplay } = useMarkets(); + const { isAprDisplay } = useAppSettings(); const { short: rateLabel } = useRateLabel(); const { activeCampaigns, hasActiveRewards } = useMarketCampaigns({ diff --git a/src/features/markets/components/market-settings-modal.tsx b/src/features/markets/components/market-settings-modal.tsx index 0b5b26e6..4a625f29 100644 --- a/src/features/markets/components/market-settings-modal.tsx +++ b/src/features/markets/components/market-settings-modal.tsx @@ -6,26 +6,15 @@ import { Button } from '@/components/ui/button'; import { IconSwitch } from '@/components/ui/icon-switch'; import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/common/Modal'; import { TrustedByCell } from '@/features/autovault/components/trusted-vault-badges'; -import { defaultTrustedVaults, type TrustedVault } from '@/constants/vaults/known_vaults'; -import { useMarkets } from '@/hooks/useMarkets'; import { useModal } from '@/hooks/useModal'; -import { useLocalStorage } from '@/hooks/useLocalStorage'; -import { storageKeys } from '@/utils/storageKeys'; +import { useTrustedVaults } from '@/stores/useTrustedVaults'; +import { useMarketPreferences } from '@/stores/useMarketPreferences'; +import { useAppSettings } from '@/stores/useAppSettings'; import { type ColumnVisibility, COLUMN_LABELS, COLUMN_DESCRIPTIONS, DEFAULT_COLUMN_VISIBILITY } from './column-visibility'; type MarketSettingsModalProps = { isOpen: boolean; onOpenChange: (isOpen: boolean) => void; - usdFilters: { - minSupply: string; - minBorrow: string; - minLiquidity: string; - }; - setUsdFilters: (filters: MarketSettingsModalProps['usdFilters']) => void; - entriesPerPage: number; - onEntriesPerPageChange: (value: number) => void; - columnVisibility: ColumnVisibility; - setColumnVisibility: (visibility: ColumnVisibility) => void; }; function SettingItem({ title, description, children }: { title: string; description: string; children: React.ReactNode }) { @@ -40,26 +29,31 @@ function SettingItem({ title, description, children }: { title: string; descript ); } -export default function MarketSettingsModal({ - isOpen, - onOpenChange, - usdFilters, - setUsdFilters, - entriesPerPage, - onEntriesPerPageChange, - columnVisibility, - setColumnVisibility, -}: MarketSettingsModalProps) { +export default function MarketSettingsModal({ isOpen, onOpenChange }: MarketSettingsModalProps) { + // Subscribe to Zustand stores directly - no prop drilling! + const { + columnVisibility, + setColumnVisibility, + entriesPerPage, + setEntriesPerPage, + usdMinSupply, + setUsdMinSupply, + usdMinBorrow, + setUsdMinBorrow, + usdMinLiquidity, + setUsdMinLiquidity, + } = useMarketPreferences(); + const [customEntries, setCustomEntries] = React.useState(entriesPerPage.toString()); - const [userTrustedVaults, setUserTrustedVaults] = useLocalStorage(storageKeys.UserTrustedVaultsKey, defaultTrustedVaults); + const { vaults: userTrustedVaults } = useTrustedVaults(); const totalVaults = userTrustedVaults.length; - const { showFullRewardAPY, setShowFullRewardAPY } = useMarkets(); + const { showFullRewardAPY, setShowFullRewardAPY } = useAppSettings(); const { open: openModal } = useModal(); const handleCustomEntriesSubmit = () => { const value = Number(customEntries); if (!Number.isNaN(value) && value > 0) { - onEntriesPerPageChange(value); + setEntriesPerPage(value); } setCustomEntries(value > 0 ? String(value) : entriesPerPage.toString()); }; @@ -67,7 +61,10 @@ export default function MarketSettingsModal({ const handleUsdFilterChange = (e: React.ChangeEvent) => { const { name, value } = e.target; if (/^\d*$/.test(value)) { - setUsdFilters({ ...usdFilters, [name]: value }); + // Update the corresponding store value + if (name === 'minSupply') setUsdMinSupply(value); + else if (name === 'minBorrow') setUsdMinBorrow(value); + else if (name === 'minLiquidity') setUsdMinLiquidity(value); } }; @@ -87,7 +84,7 @@ export default function MarketSettingsModal({ mainIcon={} onClose={onClose} /> - +

Filter Thresholds

@@ -102,7 +99,7 @@ export default function MarketSettingsModal({ aria-label="Minimum supply value" name="minSupply" placeholder="0" - value={usdFilters.minSupply} + value={usdMinSupply} onChange={handleUsdFilterChange} size="sm" type="text" @@ -122,7 +119,7 @@ export default function MarketSettingsModal({ aria-label="Minimum borrow value" name="minBorrow" placeholder="0" - value={usdFilters.minBorrow} + value={usdMinBorrow} onChange={handleUsdFilterChange} size="sm" type="text" @@ -142,7 +139,7 @@ export default function MarketSettingsModal({ aria-label="Minimum liquidity value" name="minLiquidity" placeholder="0" - value={usdFilters.minLiquidity} + value={usdMinLiquidity} onChange={handleUsdFilterChange} size="sm" type="text" @@ -177,10 +174,10 @@ export default function MarketSettingsModal({ id={`col-${key}`} selected={isVisible} onChange={(value) => - setColumnVisibility({ - ...columnVisibility, + setColumnVisibility((prev) => ({ + ...prev, [key]: value, - }) + })) } size="xs" color="primary" @@ -216,12 +213,7 @@ export default function MarketSettingsModal({

- - openModal('marketSettings', { - usdFilters, - setUsdFilters, - entriesPerPage, - onEntriesPerPageChange: setEntriesPerPage, - columnVisibility, - setColumnVisibility, - }) - } - /> + openModal('marketSettings', {})} /> {showSettings && ( - + {/* Hide expand/compact toggle on mobile */} @@ -181,17 +145,15 @@ function MarketsTable({ /> } > - - - + )} @@ -203,179 +165,208 @@ function MarketsTable({ /> } > - - - + ); + // Determine empty state hint based on active filters + const getEmptyStateHint = () => { + if (!includeUnknownTokens) { + return "Try enabling 'Show Unknown Tokens' in settings, or adjust your current filters."; + } + if (!showUnknownOracle) { + return "Try enabling 'Show Unknown Oracles' in settings, or adjust your oracle filters."; + } + if (trustedVaultsOnly) { + return 'Disable the Trusted Vaults filter or update your trusted list in Settings.'; + } + if (minSupplyEnabled || minBorrowEnabled || minLiquidityEnabled) { + return 'Try disabling USD filters in settings, or adjust your filter thresholds.'; + } + return 'Try adjusting your filters or search query to see more results.'; + }; + return (
- - - - : } - sortColumn={sortColumn} - titleOnclick={titleOnclick} - sortDirection={sortDirection} - targetColumn={SortColumn.Starred} - showDirection={false} - /> - Id - - - Oracle - - {columnVisibility.trustedBy && ( - - )} - {columnVisibility.totalSupply && ( - - )} - {columnVisibility.totalBorrow && ( - - )} - {columnVisibility.liquidity && ( - - )} - {columnVisibility.supplyAPY && ( + {loading ? ( + + ) : isEmpty ? ( +
+

No data available

+
+ ) : markets.length === 0 ? ( + + ) : ( +
+ + : } sortColumn={sortColumn} titleOnclick={titleOnclick} sortDirection={sortDirection} - targetColumn={SortColumn.SupplyAPY} + targetColumn={SortColumn.Starred} + showDirection={false} /> - )} - {columnVisibility.borrowAPY && ( + Id - )} - {columnVisibility.rateAtTarget && ( - )} - {columnVisibility.utilizationRate && ( + Oracle - )} - - {' '} - Risk{' '} - - - {' '} - Indicators{' '} - - - {' '} - Actions{' '} - - - - -
+ {columnVisibility.trustedBy && ( + + )} + {columnVisibility.totalSupply && ( + + )} + {columnVisibility.totalBorrow && ( + + )} + {columnVisibility.liquidity && ( + + )} + {columnVisibility.supplyAPY && ( + + )} + {columnVisibility.borrowAPY && ( + + )} + {columnVisibility.rateAtTarget && ( + + )} + {columnVisibility.utilizationRate && ( + + )} + + {' '} + Risk{' '} + + + {' '} + Indicators{' '} + + + {' '} + Actions{' '} + + + + + + )}
- + {!loading && !isEmpty && markets.length > 0 && ( + + )}
); } diff --git a/src/features/markets/markets-view.tsx b/src/features/markets/markets-view.tsx index 80e2f647..7b193bd1 100644 --- a/src/features/markets/markets-view.tsx +++ b/src/features/markets/markets-view.tsx @@ -5,27 +5,21 @@ import { useRouter } from 'next/navigation'; import Header from '@/components/layout/header/Header'; import { useTokens } from '@/components/providers/TokenProvider'; -import EmptyScreen from '@/components/status/empty-screen'; -import LoadingScreen from '@/components/status/loading-screen'; -import { DEFAULT_MIN_SUPPLY_USD, DEFAULT_MIN_LIQUIDITY_USD } from '@/constants/markets'; -import { defaultTrustedVaults, getVaultKey, type TrustedVault } from '@/constants/vaults/known_vaults'; -import { useLocalStorage } from '@/hooks/useLocalStorage'; +import { getVaultKey } from '@/constants/vaults/known_vaults'; import { useMarkets } from '@/hooks/useMarkets'; import { useModal } from '@/hooks/useModal'; import { usePagination } from '@/hooks/usePagination'; -import { useStaredMarkets } from '@/hooks/useStaredMarkets'; import { useStyledToast } from '@/hooks/useStyledToast'; +import { useTrustedVaults } from '@/stores/useTrustedVaults'; +import { useMarketPreferences } from '@/stores/useMarketPreferences'; import { filterMarkets, sortMarkets, createPropertySort, createStarredSort } from '@/utils/marketFilters'; -import { parseNumericThreshold } from '@/utils/markets'; import type { SupportedNetworks } from '@/utils/networks'; import type { PriceFeedVendors } from '@/utils/oracle'; -import { storageKeys } from '@/utils/storageKeys'; import type { ERC20Token, UnknownERC20Token } from '@/utils/tokens'; import type { Market } from '@/utils/types'; import AdvancedSearchBar, { ShortcutType } from './components/advanced-search-bar'; import AssetFilter from './components/filters/asset-filter'; -import { DEFAULT_COLUMN_VISIBILITY, type ColumnVisibility } from './components/column-visibility'; import { SortColumn } from './components/constants'; import MarketsTable from './components/table/markets-table'; import NetworkFilter from './components/filters/network-filter'; @@ -42,21 +36,7 @@ export default function Markets({ initialNetwork, initialCollaterals, initialLoa const toast = useStyledToast(); - const { - loading, - markets: rawMarkets, - refetch, - isRefetching, - showUnwhitelistedMarkets, - setShowUnwhitelistedMarkets, - addBlacklistedMarket: addBlacklistedMarketBase, - isBlacklisted, - } = useMarkets(); - const { staredIds, starMarket, unstarMarket } = useStaredMarkets(); - - // Use addBlacklistedMarket directly from context - // The context automatically reapplies the filter when blacklist changes - const addBlacklistedMarket = addBlacklistedMarketBase; + const { loading, markets: rawMarkets, refetch, isRefetching } = useMarkets(); // Initialize state with server-parsed values const [selectedCollaterals, setSelectedCollaterals] = useState(initialCollaterals); @@ -66,8 +46,21 @@ export default function Markets({ initialNetwork, initialCollaterals, initialLoa const [uniqueCollaterals, setUniqueCollaterals] = useState<(ERC20Token | UnknownERC20Token)[]>([]); const [uniqueLoanAssets, setUniqueLoanAssets] = useState<(ERC20Token | UnknownERC20Token)[]>([]); - const [sortColumn, setSortColumn] = useLocalStorage(storageKeys.MarketSortColumnKey, SortColumn.Supply); - const [sortDirection, setSortDirection] = useLocalStorage(storageKeys.MarketSortDirectionKey, -1); + // Market preferences from Zustand store + const { + sortColumn, + sortDirection, + includeUnknownTokens, + showUnknownOracle, + usdMinSupply, + usdMinBorrow, + usdMinLiquidity, + minSupplyEnabled, + minBorrowEnabled, + minLiquidityEnabled, + trustedVaultsOnly, + starredMarkets, + } = useMarketPreferences(); const [filteredMarkets, setFilteredMarkets] = useState([]); @@ -75,48 +68,11 @@ export default function Markets({ initialNetwork, initialCollaterals, initialLoa const [selectedOracles, setSelectedOracles] = useState([]); - const { currentPage, setCurrentPage, entriesPerPage, handleEntriesPerPageChange, resetPage } = usePagination(); - - const [includeUnknownTokens, setIncludeUnknownTokens] = useLocalStorage(storageKeys.MarketsShowUnknownTokens, false); - const [showUnknownOracle, setShowUnknownOracle] = useLocalStorage(storageKeys.MarketsShowUnknownOracle, false); + const { currentPage, setCurrentPage, resetPage } = usePagination(); const { allTokens, findToken } = useTokens(); - // USD Filter values - const [usdMinSupply, setUsdMinSupply] = useLocalStorage(storageKeys.MarketsUsdMinSupplyKey, DEFAULT_MIN_SUPPLY_USD.toString()); - const [usdMinBorrow, setUsdMinBorrow] = useLocalStorage(storageKeys.MarketsUsdMinBorrowKey, ''); - const [usdMinLiquidity, setUsdMinLiquidity] = useLocalStorage( - storageKeys.MarketsUsdMinLiquidityKey, - DEFAULT_MIN_LIQUIDITY_USD.toString(), - ); - - // USD Filter enabled states - const [minSupplyEnabled, setMinSupplyEnabled] = useLocalStorage( - storageKeys.MarketsMinSupplyEnabledKey, - true, // Default to enabled for backward compatibility - ); - const [minBorrowEnabled, setMinBorrowEnabled] = useLocalStorage(storageKeys.MarketsMinBorrowEnabledKey, false); - const [minLiquidityEnabled, setMinLiquidityEnabled] = useLocalStorage(storageKeys.MarketsMinLiquidityEnabledKey, false); - - const [trustedVaultsOnly, setTrustedVaultsOnly] = useLocalStorage(storageKeys.MarketsTrustedVaultsOnlyKey, false); - - // Column visibility state - const [columnVisibilityState, setColumnVisibilityState] = useLocalStorage( - storageKeys.MarketsColumnVisibilityKey, - DEFAULT_COLUMN_VISIBILITY, - ); - - const columnVisibility = useMemo(() => ({ ...DEFAULT_COLUMN_VISIBILITY, ...columnVisibilityState }), [columnVisibilityState]); - - const setColumnVisibility = useCallback( - (visibility: ColumnVisibility) => { - setColumnVisibilityState({ ...DEFAULT_COLUMN_VISIBILITY, ...visibility }); - }, - [setColumnVisibilityState], - ); - - // Table view mode: 'compact' (scrollable) or 'expanded' (full width) - const [tableViewMode, setTableViewMode] = useLocalStorage<'compact' | 'expanded'>(storageKeys.MarketsTableViewModeKey, 'compact'); + const { tableViewMode } = useMarketPreferences(); // Force compact mode on mobile - track window size const [isMobile, setIsMobile] = useState(false); @@ -134,10 +90,7 @@ export default function Markets({ initialNetwork, initialCollaterals, initialLoa // Effective table view mode - always compact on mobile const effectiveTableViewMode = isMobile ? 'compact' : tableViewMode; - const [userTrustedVaults, _setUserTrustedVaults] = useLocalStorage( - storageKeys.UserTrustedVaultsKey, - defaultTrustedVaults, - ); + const { vaults: userTrustedVaults } = useTrustedVaults(); const { open: openModal } = useModal(); const trustedVaultKeys = useMemo(() => { @@ -166,19 +119,6 @@ export default function Markets({ initialNetwork, initialCollaterals, initialLoa [usdMinSupply, usdMinBorrow, usdMinLiquidity], ); - const setUsdFilters = useCallback( - (filters: { minSupply: string; minBorrow: string; minLiquidity: string }) => { - setUsdMinSupply(filters.minSupply); - setUsdMinBorrow(filters.minBorrow); - setUsdMinLiquidity(filters.minLiquidity); - }, - [setUsdMinSupply, setUsdMinBorrow, setUsdMinLiquidity], - ); - - const effectiveMinSupply = parseNumericThreshold(usdFilters.minSupply); - const effectiveMinBorrow = parseNumericThreshold(usdFilters.minBorrow); - const effectiveMinLiquidity = parseNumericThreshold(usdFilters.minLiquidity); - useEffect(() => { // return if no markets if (!rawMarkets) return; @@ -298,7 +238,7 @@ export default function Markets({ initialNetwork, initialCollaterals, initialLoa // Apply sorting let sorted: Market[]; if (sortColumn === SortColumn.Starred) { - sorted = sortMarkets(filtered, createStarredSort(staredIds), 1); + sorted = sortMarkets(filtered, createStarredSort(starredMarkets), 1); } else if (sortColumn === SortColumn.TrustedBy) { sorted = sortMarkets(filtered, (a, b) => Number(hasTrustedVault(a)) - Number(hasTrustedVault(b)), sortDirection as 1 | -1); } else { @@ -335,7 +275,7 @@ export default function Markets({ initialNetwork, initialCollaterals, initialLoa selectedCollaterals, selectedLoanAssets, selectedOracles, - staredIds, + starredMarkets, findToken, usdFilters, minSupplyEnabled, @@ -369,24 +309,6 @@ export default function Markets({ initialNetwork, initialCollaterals, initialLoa resetPage, ]); - const titleOnclick = useCallback( - (column: number) => { - // Validate that column is a valid SortColumn value - const isValidColumn = Object.values(SortColumn).includes(column); - if (!isValidColumn) { - console.error(`Invalid sort column value: ${column}`); - return; - } - - setSortColumn(column); - - if (column === sortColumn) { - setSortDirection(-sortDirection); - } - }, - [sortColumn, sortDirection, setSortColumn, setSortDirection], - ); - // Add keyboard shortcut for search useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { @@ -494,89 +416,22 @@ export default function Markets({ initialNetwork, initialCollaterals, initialLoa {/* Table Section - centered when expanded, full width when compact */}
- {loading ? ( -
- -
- ) : rawMarkets == null ? ( -
No data
- ) : ( -
- {filteredMarkets.length > 0 ? ( - - openModal('marketSettings', { - usdFilters, - setUsdFilters, - entriesPerPage, - onEntriesPerPageChange: handleEntriesPerPageChange, - columnVisibility, - setColumnVisibility, - }) - } - onRefresh={handleRefresh} - isRefetching={isRefetching} - tableViewMode={tableViewMode} - setTableViewMode={setTableViewMode} - isMobile={isMobile} - /> - ) : ( - 0 || selectedLoanAssets.length > 0) && !includeUnknownTokens - ? "Try enabling 'Show Unknown Tokens' in settings, or adjust your current filters." - : selectedOracles.length > 0 && !showUnknownOracle - ? "Try enabling 'Show Unknown Oracles' in settings, or adjust your oracle filters." - : trustedVaultsOnly - ? 'Disable the Trusted Vaults filter or update your trusted list in Settings.' - : minSupplyEnabled || minBorrowEnabled || minLiquidityEnabled - ? 'Try disabling USD filters in settings, or adjust your filter thresholds.' - : 'Try adjusting your filters or search query to see more results.' - } - /> - )} -
- )} +
+ openModal('marketSettings', {})} + onRefresh={handleRefresh} + isRefetching={isRefetching} + isMobile={isMobile} + loading={loading} + isEmpty={rawMarkets == null} + /> +
); diff --git a/src/features/positions-report/components/report-table.tsx b/src/features/positions-report/components/report-table.tsx index 230baa77..b86c3c2b 100644 --- a/src/features/positions-report/components/report-table.tsx +++ b/src/features/positions-report/components/report-table.tsx @@ -12,7 +12,7 @@ import { Badge } from '@/components/ui/badge'; import { NetworkIcon } from '@/components/shared/network-icon'; import OracleVendorBadge from '@/features/markets/components/oracle-vendor-badge'; import { TokenIcon } from '@/components/shared/token-icon'; -import { useMarkets } from '@/hooks/useMarkets'; +import { useAppSettings } from '@/stores/useAppSettings'; import type { ReportSummary } from '@/hooks/usePositionReport'; import { useRateLabel } from '@/hooks/useRateLabel'; import { formatReadable } from '@/utils/balance'; @@ -63,7 +63,7 @@ const formatDays = (seconds: number) => { }; function MarketSummaryBlock({ market, interestEarned, decimals, symbol, apy, hasActivePosition }: MarketInfoBlockProps) { - const { isAprDisplay } = useMarkets(); + const { isAprDisplay } = useAppSettings(); const { short: rateLabel } = useRateLabel(); // Convert to APR if display mode is enabled @@ -121,7 +121,7 @@ function MarketSummaryBlock({ market, interestEarned, decimals, symbol, apy, has export function ReportTable({ report, asset, startDate, endDate, chainId }: ReportTableProps) { const [expandedMarkets, setExpandedMarkets] = useState>(new Set()); - const { isAprDisplay } = useMarkets(); + const { isAprDisplay } = useAppSettings(); const { short: rateLabel } = useRateLabel(); // Convert APY to APR if display mode is enabled diff --git a/src/features/positions/components/from-markets-table.tsx b/src/features/positions/components/from-markets-table.tsx index 1595cccf..8bcb8994 100644 --- a/src/features/positions/components/from-markets-table.tsx +++ b/src/features/positions/components/from-markets-table.tsx @@ -3,7 +3,7 @@ import { Button } from '@/components/ui/button'; import { TablePagination } from '@/components/shared/table-pagination'; import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table'; import { MarketIdentity, MarketIdentityMode, MarketIdentityFocus } from '@/features/markets/components/market-identity'; -import { useMarkets } from '@/hooks/useMarkets'; +import { useAppSettings } from '@/stores/useAppSettings'; import { useRateLabel } from '@/hooks/useRateLabel'; import { formatReadable } from '@/utils/balance'; import { previewMarketState } from '@/utils/morpho'; @@ -23,7 +23,7 @@ const PER_PAGE = 5; export function FromMarketsTable({ positions, selectedMarketUniqueKey, onSelectMarket, onSelectMax }: FromMarketsTableProps) { const [currentPage, setCurrentPage] = useState(1); - const { isAprDisplay } = useMarkets(); + const { isAprDisplay } = useAppSettings(); const { short: rateLabel } = useRateLabel(); const totalPages = Math.ceil(positions.length / PER_PAGE); diff --git a/src/features/positions/components/onboarding/setup-positions.tsx b/src/features/positions/components/onboarding/setup-positions.tsx index 63b74555..e1d058d1 100644 --- a/src/features/positions/components/onboarding/setup-positions.tsx +++ b/src/features/positions/components/onboarding/setup-positions.tsx @@ -9,7 +9,7 @@ import { ExecuteTransactionButton } from '@/components/ui/ExecuteTransactionButt import Input from '@/components/Input/Input'; import { MarketIdentity, MarketIdentityMode, MarketIdentityFocus } from '@/features/markets/components/market-identity'; import { SupplyProcessModal } from '@/modals/supply/supply-process-modal'; -import { useLocalStorage } from '@/hooks/useLocalStorage'; +import { useAppSettings } from '@/stores/useAppSettings'; import { useMultiMarketSupply } from '@/hooks/useMultiMarketSupply'; import { useRateLabel } from '@/hooks/useRateLabel'; import { useStyledToast } from '@/hooks/useStyledToast'; @@ -22,8 +22,7 @@ export function SetupPositions({ onClose }: { onClose: () => void }) { const toast = useStyledToast(); const { short: rateLabel } = useRateLabel(); const { selectedToken, selectedMarkets, balances, goToPrevStep } = useOnboarding(); - const [useEth] = useLocalStorage('useEth', false); - const [usePermit2Setting] = useLocalStorage('usePermit2', true); + const { useEth, usePermit2: usePermit2Setting } = useAppSettings(); const [totalAmount, setTotalAmount] = useState(''); const [totalAmountBigInt, setTotalAmountBigInt] = useState(0n); const [amounts, setAmounts] = useState>({}); diff --git a/src/features/positions/components/rebalance/rebalance-modal.tsx b/src/features/positions/components/rebalance/rebalance-modal.tsx index 62dc5fb2..559e9f99 100644 --- a/src/features/positions/components/rebalance/rebalance-modal.tsx +++ b/src/features/positions/components/rebalance/rebalance-modal.tsx @@ -7,7 +7,7 @@ import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/common/ import { Spinner } from '@/components/ui/spinner'; import { TokenIcon } from '@/components/shared/token-icon'; import { ExecuteTransactionButton } from '@/components/ui/ExecuteTransactionButton'; -import { useLocalStorage } from '@/hooks/useLocalStorage'; +import { useAppSettings } from '@/stores/useAppSettings'; import { useMarkets } from '@/hooks/useMarkets'; import { useRebalance } from '@/hooks/useRebalance'; import { useStyledToast } from '@/hooks/useStyledToast'; @@ -33,7 +33,7 @@ export function RebalanceModal({ groupedPosition, isOpen, onOpenChange, refetch, const [showProcessModal, setShowProcessModal] = useState(false); const [showToModal, setShowToModal] = useState(false); const toast = useStyledToast(); - const [usePermit2Setting] = useLocalStorage('usePermit2', true); + const { usePermit2: usePermit2Setting } = useAppSettings(); // Use computed markets based on user setting const { markets } = useMarkets(); diff --git a/src/features/positions/components/supplied-asset-filter-compact-switch.tsx b/src/features/positions/components/supplied-asset-filter-compact-switch.tsx index 41b9f06b..c304a110 100644 --- a/src/features/positions/components/supplied-asset-filter-compact-switch.tsx +++ b/src/features/positions/components/supplied-asset-filter-compact-switch.tsx @@ -12,52 +12,49 @@ import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/common/ import { TooltipContent } from '@/components/shared/tooltip-content'; import { MONARCH_PRIMARY } from '@/constants/chartColors'; import { formatReadable } from '@/utils/balance'; +import { useMarketPreferences } from '@/stores/useMarketPreferences'; +import { useAppSettings } from '@/stores/useAppSettings'; +import { parseNumericThreshold } from '@/utils/markets'; type SuppliedAssetFilterCompactSwitchProps = { - includeUnknownTokens: boolean; - setIncludeUnknownTokens: (value: boolean) => void; - showUnknownOracle: boolean; - setShowUnknownOracle: (value: boolean) => void; - showUnwhitelistedMarkets: boolean; - setShowUnwhitelistedMarkets: (value: boolean) => void; - trustedVaultsOnly: boolean; - setTrustedVaultsOnly: (value: boolean) => void; - minSupplyEnabled: boolean; - setMinSupplyEnabled: (value: boolean) => void; - minBorrowEnabled: boolean; - setMinBorrowEnabled: (value: boolean) => void; - minLiquidityEnabled: boolean; - setMinLiquidityEnabled: (value: boolean) => void; - thresholds: { - minSupply: number; - minBorrow: number; - minLiquidity: number; - }; onOpenSettings: () => void; className?: string; }; -export function SuppliedAssetFilterCompactSwitch({ - includeUnknownTokens, - setIncludeUnknownTokens, - showUnknownOracle, - setShowUnknownOracle, - showUnwhitelistedMarkets, - setShowUnwhitelistedMarkets, - trustedVaultsOnly, - setTrustedVaultsOnly, - minSupplyEnabled, - setMinSupplyEnabled, - minBorrowEnabled, - setMinBorrowEnabled, - minLiquidityEnabled, - setMinLiquidityEnabled, - thresholds, - onOpenSettings, - className, -}: SuppliedAssetFilterCompactSwitchProps) { +export function SuppliedAssetFilterCompactSwitch({ onOpenSettings, className }: SuppliedAssetFilterCompactSwitchProps) { const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure(); + // Get all filter values from stores + const { + includeUnknownTokens, + setIncludeUnknownTokens, + showUnknownOracle, + setShowUnknownOracle, + trustedVaultsOnly, + setTrustedVaultsOnly, + minSupplyEnabled, + setMinSupplyEnabled, + minBorrowEnabled, + setMinBorrowEnabled, + minLiquidityEnabled, + setMinLiquidityEnabled, + usdMinSupply, + usdMinBorrow, + usdMinLiquidity, + } = useMarketPreferences(); + + const { showUnwhitelistedMarkets, setShowUnwhitelistedMarkets } = useAppSettings(); + + // Compute thresholds from store values + const thresholds = useMemo( + () => ({ + minSupply: parseNumericThreshold(usdMinSupply), + minBorrow: parseNumericThreshold(usdMinBorrow), + minLiquidity: parseNumericThreshold(usdMinLiquidity), + }), + [usdMinSupply, usdMinBorrow, usdMinLiquidity], + ); + const thresholdCopy = useMemo( () => ({ minSupply: formatReadable(thresholds.minSupply), diff --git a/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx b/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx index 8d936e59..48af610f 100644 --- a/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx +++ b/src/features/positions/components/supplied-morpho-blue-grouped-table.tsx @@ -18,8 +18,8 @@ import { TooltipContent } from '@/components/shared/tooltip-content'; import { TableContainerWithHeader } from '@/components/common/table-container-with-header'; import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/common/Modal'; import { useDisclosure } from '@/hooks/useDisclosure'; -import { useLocalStorage } from '@/hooks/useLocalStorage'; -import { useMarkets } from '@/hooks/useMarkets'; +import { usePositionsPreferences } from '@/stores/usePositionsPreferences'; +import { useAppSettings } from '@/stores/useAppSettings'; import { computeMarketWarnings } from '@/hooks/useMarketWarnings'; import { useRateLabel } from '@/hooks/useRateLabel'; import { useStyledToast } from '@/hooks/useStyledToast'; @@ -28,7 +28,6 @@ import { formatReadable, formatBalance } from '@/utils/balance'; import { getNetworkImg } from '@/utils/networks'; import { getGroupedEarnings, groupPositionsByLoanAsset, processCollaterals } from '@/utils/positions'; import { convertApyToApr } from '@/utils/rateMath'; -import { storageKeys } from '@/utils/storageKeys'; import { type GroupedPosition, type MarketPositionWithEarnings, type WarningWithDetail, WarningCategory } from '@/utils/types'; import { RiskIndicator } from '@/features/markets/components/risk-indicator'; import { PositionActionsDropdown } from './position-actions-dropdown'; @@ -116,13 +115,11 @@ export function SuppliedMorphoBlueGroupedTable({ const [expandedRows, setExpandedRows] = useState>(new Set()); const [showRebalanceModal, setShowRebalanceModal] = useState(false); const [selectedGroupedPosition, setSelectedGroupedPosition] = useState(null); - const [showCollateralExposure, setShowCollateralExposure] = useLocalStorage( - storageKeys.PositionsShowCollateralExposureKey, - true, - ); + // Positions preferences from Zustand store + const { showCollateralExposure, setShowCollateralExposure } = usePositionsPreferences(); const { isOpen: isSettingsOpen, onOpen: onSettingsOpen, onOpenChange: onSettingsOpenChange } = useDisclosure(); const { address } = useConnection(); - const { isAprDisplay } = useMarkets(); + const { isAprDisplay } = useAppSettings(); const { short: rateLabel } = useRateLabel(); const toast = useStyledToast(); diff --git a/src/features/positions/components/user-vaults-table.tsx b/src/features/positions/components/user-vaults-table.tsx index 20e3c0ac..18eaf0f6 100644 --- a/src/features/positions/components/user-vaults-table.tsx +++ b/src/features/positions/components/user-vaults-table.tsx @@ -11,7 +11,7 @@ import { TooltipContent } from '@/components/shared/tooltip-content'; import { TableContainerWithHeader } from '@/components/common/table-container-with-header'; import type { UserVaultV2 } from '@/data-sources/subgraph/v2-vaults'; import { useTokens } from '@/components/providers/TokenProvider'; -import { useMarkets } from '@/hooks/useMarkets'; +import { useAppSettings } from '@/stores/useAppSettings'; import { useRateLabel } from '@/hooks/useRateLabel'; import { formatReadable } from '@/utils/balance'; import { getNetworkImg } from '@/utils/networks'; @@ -32,7 +32,7 @@ type UserVaultsTableProps = { export function UserVaultsTable({ vaults, account, refetch, isRefetching = false }: UserVaultsTableProps) { const [expandedRows, setExpandedRows] = useState>(new Set()); const { findToken } = useTokens(); - const { isAprDisplay } = useMarkets(); + const { isAprDisplay } = useAppSettings(); const { short: rateLabel } = useRateLabel(); const toggleRow = (rowKey: string) => { diff --git a/src/hooks/useBlacklistedMarkets.ts b/src/hooks/useBlacklistedMarkets.ts deleted file mode 100644 index f583df9e..00000000 --- a/src/hooks/useBlacklistedMarkets.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { useCallback, useMemo } from 'react'; -import { useStyledToast } from '@/hooks/useStyledToast'; -import { blacklistedMarkets as defaultBlacklistedMarkets } from '@/utils/markets'; -import { useLocalStorage } from './useLocalStorage'; - -type BlacklistedMarket = { - uniqueKey: string; - chainId: number; - reason?: string; - addedAt: number; -}; - -export function useBlacklistedMarkets() { - const [customBlacklistedMarkets, setCustomBlacklistedMarkets] = useLocalStorage('customBlacklistedMarkets', []); - const { success: toastSuccess } = useStyledToast(); - - // Combine default and custom blacklists - const allBlacklistedMarketKeys = useMemo(() => { - const customKeys = customBlacklistedMarkets.map((m) => m.uniqueKey); - return new Set([...defaultBlacklistedMarkets, ...customKeys]); - }, [customBlacklistedMarkets]); - - // Add a market to blacklist - const addBlacklistedMarket = useCallback( - (uniqueKey: string, chainId: number, reason?: string) => { - // Check if already blacklisted - if (allBlacklistedMarketKeys.has(uniqueKey)) { - return false; - } - - const newMarket: BlacklistedMarket = { - uniqueKey, - chainId, - reason, - addedAt: Date.now(), - }; - - setCustomBlacklistedMarkets((prev) => [...prev, newMarket]); - toastSuccess('Market blacklisted', 'Market added to blacklist'); - return true; - }, - [allBlacklistedMarketKeys, setCustomBlacklistedMarkets, toastSuccess], - ); - - // Remove a custom blacklisted market (cannot remove defaults) - const removeBlacklistedMarket = useCallback( - (uniqueKey: string) => { - setCustomBlacklistedMarkets((prev) => prev.filter((m) => m.uniqueKey !== uniqueKey)); - toastSuccess('Market removed from blacklist', 'Market is now visible'); - }, - [setCustomBlacklistedMarkets, toastSuccess], - ); - - // Check if a market is blacklisted - const isBlacklisted = useCallback( - (uniqueKey: string) => { - return allBlacklistedMarketKeys.has(uniqueKey); - }, - [allBlacklistedMarketKeys], - ); - - // Check if a market is in default blacklist - const isDefaultBlacklisted = useCallback((uniqueKey: string) => { - return defaultBlacklistedMarkets.includes(uniqueKey); - }, []); - - return { - allBlacklistedMarketKeys, - customBlacklistedMarkets, - addBlacklistedMarket, - removeBlacklistedMarket, - isBlacklisted, - isDefaultBlacklisted, - }; -} diff --git a/src/hooks/useBorrowTransaction.ts b/src/hooks/useBorrowTransaction.ts index f60ef7a6..751b76a3 100644 --- a/src/hooks/useBorrowTransaction.ts +++ b/src/hooks/useBorrowTransaction.ts @@ -6,12 +6,12 @@ import { formatBalance } from '@/utils/balance'; import { getBundlerV2, MONARCH_TX_IDENTIFIER } from '@/utils/morpho'; import type { Market } from '@/utils/types'; import { useERC20Approval } from './useERC20Approval'; -import { useLocalStorage } from './useLocalStorage'; import { useMorphoAuthorization } from './useMorphoAuthorization'; import { usePermit2 } from './usePermit2'; +import { useAppSettings } from '@/stores/useAppSettings'; import { useStyledToast } from './useStyledToast'; import { useTransactionWithToast } from './useTransactionWithToast'; -import { useUserMarketsCache } from './useUserMarketsCache'; +import { useUserMarketsCache } from '@/stores/useUserMarketsCache'; type UseBorrowTransactionProps = { market: Market; @@ -32,7 +32,7 @@ export type BorrowStepType = export function useBorrowTransaction({ market, collateralAmount, borrowAmount, onSuccess }: UseBorrowTransactionProps) { const [currentStep, setCurrentStep] = useState('approve_permit2'); const [showProcessModal, setShowProcessModal] = useState(false); - const [usePermit2Setting] = useLocalStorage('usePermit2', true); + const { usePermit2: usePermit2Setting } = useAppSettings(); const [useEth, setUseEth] = useState(false); const { address: account, chainId } = useConnection(); diff --git a/src/hooks/useCustomRpc.ts b/src/hooks/useCustomRpc.ts deleted file mode 100644 index 6a184b1a..00000000 --- a/src/hooks/useCustomRpc.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { type SupportedNetworks, getDefaultRPC } from '@/utils/networks'; -import { useLocalStorage } from './useLocalStorage'; - -export type CustomRpcUrls = Partial>; - -export function useCustomRpc() { - const [customRpcUrls, setCustomRpcUrls] = useLocalStorage('customRpcUrls', {}); - - const setRpcUrl = (chainId: SupportedNetworks, url: string | undefined) => { - setCustomRpcUrls((prev) => { - const newUrls = { ...prev }; - if (url === undefined || url === '' || url === getDefaultRPC(chainId)) { - delete newUrls[chainId]; - } else { - newUrls[chainId] = url; - } - return newUrls; - }); - }; - - const resetRpcUrl = (chainId: SupportedNetworks) => { - setRpcUrl(chainId, undefined); - }; - - const resetAllRpcUrls = () => { - setCustomRpcUrls({}); - }; - - const isUsingCustomRpc = (chainId: SupportedNetworks): boolean => { - return Boolean(customRpcUrls[chainId]); - }; - - const hasAnyCustomRpcs = (): boolean => { - return Object.keys(customRpcUrls).length > 0; - }; - - return { - customRpcUrls, - setRpcUrl, - resetRpcUrl, - resetAllRpcUrls, - isUsingCustomRpc, - hasAnyCustomRpcs, - }; -} diff --git a/src/hooks/useMultiMarketSupply.ts b/src/hooks/useMultiMarketSupply.ts index 95b3863e..814ca313 100644 --- a/src/hooks/useMultiMarketSupply.ts +++ b/src/hooks/useMultiMarketSupply.ts @@ -12,7 +12,7 @@ import type { Market } from '@/utils/types'; import { GAS_COSTS, GAS_MULTIPLIER } from '@/features/markets/components/constants'; import { useERC20Approval } from './useERC20Approval'; import { useStyledToast } from './useStyledToast'; -import { useUserMarketsCache } from './useUserMarketsCache'; +import { useUserMarketsCache } from '@/stores/useUserMarketsCache'; export type MarketSupply = { market: Market; amount: bigint; diff --git a/src/hooks/usePagination.ts b/src/hooks/usePagination.ts index 00916fd5..d3260d4f 100644 --- a/src/hooks/usePagination.ts +++ b/src/hooks/usePagination.ts @@ -1,25 +1,21 @@ -import { useState, useEffect, useCallback } from 'react'; -import storage from 'local-storage-fallback'; -import { storageKeys } from '@/utils/storageKeys'; +import { useState, useCallback } from 'react'; +import { useMarketPreferences } from '@/stores/useMarketPreferences'; -export function usePagination(initialEntriesPerPage = 6) { +/** + * Hook for managing pagination state. + * - currentPage: Local state (not persisted) + * - entriesPerPage: From Zustand store (persisted) + */ +export function usePagination() { const [currentPage, setCurrentPage] = useState(1); - const [entriesPerPage, setEntriesPerPage] = useState(initialEntriesPerPage); + const { entriesPerPage, setEntriesPerPage } = useMarketPreferences(); const resetPage = useCallback(() => { setCurrentPage(1); - }, [setCurrentPage]); - - useEffect(() => { - const storedEntriesPerPage = storage.getItem(storageKeys.MarketEntriesPerPageKey); - if (storedEntriesPerPage) { - setEntriesPerPage(Number.parseInt(storedEntriesPerPage, 10)); - } }, []); const handleEntriesPerPageChange = (value: number) => { setEntriesPerPage(value); - storage.setItem(storageKeys.MarketEntriesPerPageKey, value.toString()); setCurrentPage(1); // Reset to first page }; diff --git a/src/hooks/usePositionReport.ts b/src/hooks/usePositionReport.ts index 038d2d2a..e6b81e05 100644 --- a/src/hooks/usePositionReport.ts +++ b/src/hooks/usePositionReport.ts @@ -4,7 +4,7 @@ import type { SupportedNetworks } from '@/utils/networks'; import { fetchPositionsSnapshots } from '@/utils/positions'; import { estimatedBlockNumber, getClient } from '@/utils/rpc'; import type { Market, MarketPosition, UserTransaction } from '@/utils/types'; -import { useCustomRpc } from './useCustomRpc'; +import { useCustomRpc } from '@/stores/useCustomRpc'; import useUserTransactions from './useUserTransactions'; export type PositionReport = { diff --git a/src/hooks/useRateLabel.ts b/src/hooks/useRateLabel.ts index 7081f24c..425ec120 100644 --- a/src/hooks/useRateLabel.ts +++ b/src/hooks/useRateLabel.ts @@ -1,5 +1,5 @@ import { useMemo } from 'react'; -import { useMarkets } from './useMarkets'; +import { useAppSettings } from '@/stores/useAppSettings'; type RateLabelOptions = { /** @@ -44,7 +44,7 @@ type RateLabelReturn = { */ export function useRateLabel(options: RateLabelOptions = {}): RateLabelReturn { const { prefix, suffix } = options; - const { isAprDisplay } = useMarkets(); + const { isAprDisplay } = useAppSettings(); return useMemo(() => { const short = isAprDisplay ? 'APR' : 'APY'; diff --git a/src/hooks/useRebalance.ts b/src/hooks/useRebalance.ts index a00e8096..08fc72fc 100644 --- a/src/hooks/useRebalance.ts +++ b/src/hooks/useRebalance.ts @@ -7,11 +7,11 @@ import { getBundlerV2, MONARCH_TX_IDENTIFIER } from '@/utils/morpho'; import type { GroupedPosition, RebalanceAction } from '@/utils/types'; import { GAS_COSTS, GAS_MULTIPLIER } from '@/features/markets/components/constants'; import { useERC20Approval } from './useERC20Approval'; -import { useLocalStorage } from './useLocalStorage'; import { useMorphoAuthorization } from './useMorphoAuthorization'; import { usePermit2 } from './usePermit2'; +import { useAppSettings } from '@/stores/useAppSettings'; import { useStyledToast } from './useStyledToast'; -import { useUserMarketsCache } from './useUserMarketsCache'; +import { useUserMarketsCache } from '@/stores/useUserMarketsCache'; // Define more specific step types export type RebalanceStepType = @@ -31,7 +31,7 @@ export const useRebalance = (groupedPosition: GroupedPosition, onRebalance?: () const { address: account } = useConnection(); const bundlerAddress = getBundlerV2(groupedPosition.chainId); const toast = useStyledToast(); - const [usePermit2Setting] = useLocalStorage('usePermit2', true); // Read user setting + const { usePermit2: usePermit2Setting } = useAppSettings(); const totalAmount = rebalanceActions.reduce((acc, action) => acc + BigInt(action.amount), BigInt(0)); diff --git a/src/hooks/useRepayTransaction.ts b/src/hooks/useRepayTransaction.ts index 19097661..301bfaf4 100644 --- a/src/hooks/useRepayTransaction.ts +++ b/src/hooks/useRepayTransaction.ts @@ -6,8 +6,8 @@ import { formatBalance } from '@/utils/balance'; import { getBundlerV2, MONARCH_TX_IDENTIFIER } from '@/utils/morpho'; import type { Market, MarketPosition } from '@/utils/types'; import { useERC20Approval } from './useERC20Approval'; -import { useLocalStorage } from './useLocalStorage'; import { usePermit2 } from './usePermit2'; +import { useAppSettings } from '@/stores/useAppSettings'; import { useStyledToast } from './useStyledToast'; import { useTransactionWithToast } from './useTransactionWithToast'; @@ -30,7 +30,7 @@ export function useRepayTransaction({ }: UseRepayTransactionProps) { const [currentStep, setCurrentStep] = useState<'approve' | 'signing' | 'repaying'>('approve'); const [showProcessModal, setShowProcessModal] = useState(false); - const [usePermit2Setting] = useLocalStorage('usePermit2', true); + const { usePermit2: usePermit2Setting } = useAppSettings(); const { address: account, chainId } = useConnection(); const toast = useStyledToast(); diff --git a/src/hooks/useStaredMarkets.ts b/src/hooks/useStaredMarkets.ts deleted file mode 100644 index 427387b6..00000000 --- a/src/hooks/useStaredMarkets.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { useState, useCallback } from 'react'; -import storage from 'local-storage-fallback'; -import { useStyledToast } from '@/hooks/useStyledToast'; -import { storageKeys } from '@/utils/storageKeys'; - -const getInitialStaredMarkets = (): string[] => { - try { - const item = storage.getItem(storageKeys.MarketFavoritesKey) ?? '[]'; - return JSON.parse(item) as string[]; - } catch (error) { - console.error('Error parsing stared markets from localStorage', error); - return []; - } -}; - -export const useStaredMarkets = () => { - const [staredIds, setStaredIds] = useState(getInitialStaredMarkets); - const { success: toastSuccess } = useStyledToast(); - - const starMarket = useCallback( - (id: string) => { - if (staredIds.includes(id)) return; // Already stared - - const newStaredIds = [...staredIds, id]; - setStaredIds(newStaredIds); - storage.setItem(storageKeys.MarketFavoritesKey, JSON.stringify(newStaredIds)); - toastSuccess('Market starred', 'Market added to favorites'); - }, - [staredIds, toastSuccess], - ); - - const unstarMarket = useCallback( - (id: string) => { - if (!staredIds.includes(id)) return; // Not stared - - const newStaredIds = staredIds.filter((i) => i !== id); - setStaredIds(newStaredIds); - storage.setItem(storageKeys.MarketFavoritesKey, JSON.stringify(newStaredIds)); - toastSuccess('Market unstarred', 'Market removed from favorites'); - }, - [staredIds, toastSuccess], - ); - - return { staredIds, starMarket, unstarMarket }; -}; diff --git a/src/hooks/useSupplyMarket.ts b/src/hooks/useSupplyMarket.ts index 4c226599..23277d7d 100644 --- a/src/hooks/useSupplyMarket.ts +++ b/src/hooks/useSupplyMarket.ts @@ -3,11 +3,11 @@ import { type Address, encodeFunctionData, erc20Abi } from 'viem'; import { useConnection, useBalance, useReadContract } from 'wagmi'; import morphoBundlerAbi from '@/abis/bundlerV2'; import { useERC20Approval } from '@/hooks/useERC20Approval'; -import { useLocalStorage } from '@/hooks/useLocalStorage'; import { usePermit2 } from '@/hooks/usePermit2'; +import { useAppSettings } from '@/stores/useAppSettings'; import { useStyledToast } from '@/hooks/useStyledToast'; import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; -import { useUserMarketsCache } from '@/hooks/useUserMarketsCache'; +import { useUserMarketsCache } from '@/stores/useUserMarketsCache'; import { formatBalance } from '@/utils/balance'; import { getBundlerV2, MONARCH_TX_IDENTIFIER } from '@/utils/morpho'; import type { Market } from '@/utils/types'; @@ -50,7 +50,7 @@ export function useSupplyMarket(market: Market, onSuccess?: () => void): UseSupp const [useEth, setUseEth] = useState(false); const [showProcessModal, setShowProcessModal] = useState(false); const [currentStep, setCurrentStep] = useState('approve'); - const [usePermit2Setting] = useLocalStorage('usePermit2', true); + const { usePermit2: usePermit2Setting } = useAppSettings(); const { address: account, chainId } = useConnection(); const { batchAddUserMarkets } = useUserMarketsCache(account); diff --git a/src/hooks/useTransactionFilters.ts b/src/hooks/useTransactionFilters.ts deleted file mode 100644 index 3b9275c2..00000000 --- a/src/hooks/useTransactionFilters.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { useLocalStorage } from './useLocalStorage'; - -type SymbolFilters = Record< - string, - { - minSupplyAmount: string; - minBorrowAmount: string; - } ->; - -/** - * Hook to manage transaction filter settings with localStorage persistence - * Filters are cached per token symbol (e.g., all USDC markets share the same filter) - */ -export function useTransactionFilters(loanAssetSymbol: string) { - const [allFilters, setAllFilters] = useLocalStorage('monarch_transaction_filters_v2', {}); - - const currentFilters = allFilters[loanAssetSymbol] ?? { - minSupplyAmount: '0', - minBorrowAmount: '0', - }; - - const setMinSupplyAmount = (value: string) => { - setAllFilters((prev) => ({ - ...prev, - [loanAssetSymbol]: { - ...prev[loanAssetSymbol], - minSupplyAmount: value, - minBorrowAmount: prev[loanAssetSymbol]?.minBorrowAmount ?? '0', - }, - })); - }; - - const setMinBorrowAmount = (value: string) => { - setAllFilters((prev) => ({ - ...prev, - [loanAssetSymbol]: { - ...prev[loanAssetSymbol], - minSupplyAmount: prev[loanAssetSymbol]?.minSupplyAmount ?? '0', - minBorrowAmount: value, - }, - })); - }; - - return { - minSupplyAmount: currentFilters.minSupplyAmount, - minBorrowAmount: currentFilters.minBorrowAmount, - setMinSupplyAmount, - setMinBorrowAmount, - }; -} diff --git a/src/hooks/useUserMarketsCache.ts b/src/hooks/useUserMarketsCache.ts deleted file mode 100644 index 63af9a1a..00000000 --- a/src/hooks/useUserMarketsCache.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { useCallback } from 'react'; -import storage from 'local-storage-fallback'; -import { storageKeys } from '../utils/storageKeys'; - -type MarketIdentifier = { - marketUniqueKey: string; - chainId: number; -}; - -type UserMarketsCache = Record; - -export function useUserMarketsCache(address: string | undefined) { - const userAddress = address?.toLowerCase() ?? ''; - - // Load cache from localStorage - const loadCache = useCallback((): UserMarketsCache => { - try { - const cached = storage.getItem(storageKeys.CacheMarketPositionKeys); - if (cached) { - return JSON.parse(cached) as UserMarketsCache; - } - } catch (error) { - console.error('Failed to load markets cache:', error); - } - return {}; - }, []); - - // Save cache to localStorage - const saveCache = useCallback((cache: UserMarketsCache) => { - try { - storage.setItem(storageKeys.CacheMarketPositionKeys, JSON.stringify(cache)); - } catch (error) { - console.error('Failed to save markets cache:', error); - } - }, []); - - // Add markets to the user's known list - const addUserMarkets = useCallback( - (markets: MarketIdentifier[]) => { - if (!userAddress) return; - - const cache = loadCache(); - const userMarkets = cache[userAddress] ?? []; - - const updatedMarkets = [...userMarkets]; - let hasChanges = false; - - markets.forEach((market) => { - // Check if market already exists - const exists = updatedMarkets.some((m) => m.marketUniqueKey === market.marketUniqueKey && m.chainId === market.chainId); - - if (!exists) { - updatedMarkets.push(market); - hasChanges = true; - } - }); - - if (hasChanges) { - cache[userAddress] = updatedMarkets; - saveCache(cache); - } - }, - [userAddress, loadCache, saveCache], - ); - - // Get markets for the current user - const getUserMarkets = useCallback((): MarketIdentifier[] => { - if (!userAddress) return []; - - const cache = loadCache(); - return cache[userAddress] ?? []; - }, [userAddress, loadCache]); - - // Update cache with markets from API response - const batchAddUserMarkets = useCallback( - (apiMarkets: { marketUniqueKey: string; chainId: number }[]) => { - if (!userAddress || !apiMarkets.length) return; - - addUserMarkets( - apiMarkets.map((market) => ({ - marketUniqueKey: market.marketUniqueKey, - chainId: market.chainId, - })), - ); - }, - [userAddress, addUserMarkets], - ); - - return { - addUserMarkets, - getUserMarkets, - batchAddUserMarkets, - }; -} diff --git a/src/hooks/useUserPositions.ts b/src/hooks/useUserPositions.ts index f8796c31..eb13ca3b 100644 --- a/src/hooks/useUserPositions.ts +++ b/src/hooks/useUserPositions.ts @@ -8,8 +8,8 @@ import { SupportedNetworks } from '@/utils/networks'; import { fetchPositionsSnapshots, type PositionSnapshot } from '@/utils/positions'; import { getClient } from '@/utils/rpc'; import type { Market } from '@/utils/types'; -import { useUserMarketsCache } from '../hooks/useUserMarketsCache'; -import { useCustomRpc } from './useCustomRpc'; +import { useUserMarketsCache } from '@/stores/useUserMarketsCache'; +import { useCustomRpc } from '@/stores/useCustomRpc'; import { useMarkets } from './useMarkets'; // Type for market key and chain identifier diff --git a/src/hooks/useVaultV2Deposit.ts b/src/hooks/useVaultV2Deposit.ts index 4da4dbfd..279bcb26 100644 --- a/src/hooks/useVaultV2Deposit.ts +++ b/src/hooks/useVaultV2Deposit.ts @@ -3,8 +3,8 @@ import { type Address, encodeFunctionData, erc20Abi } from 'viem'; import { useConnection, useReadContract } from 'wagmi'; import morphoBundlerAbi from '@/abis/bundlerV2'; import { useERC20Approval } from '@/hooks/useERC20Approval'; -import { useLocalStorage } from '@/hooks/useLocalStorage'; import { usePermit2 } from '@/hooks/usePermit2'; +import { useAppSettings } from '@/stores/useAppSettings'; import { useStyledToast } from '@/hooks/useStyledToast'; import { useTransactionWithToast } from '@/hooks/useTransactionWithToast'; import { formatBalance } from '@/utils/balance'; @@ -61,7 +61,7 @@ export function useVaultV2Deposit({ const [inputError, setInputError] = useState(null); const [showProcessModal, setShowProcessModal] = useState(false); const [currentStep, setCurrentStep] = useState('approve'); - const [usePermit2Setting] = useLocalStorage('usePermit2', true); + const { usePermit2: usePermit2Setting } = useAppSettings(); const { address: account } = useConnection(); const toast = useStyledToast(); diff --git a/src/modals/borrow/components/add-collateral-and-borrow.tsx b/src/modals/borrow/components/add-collateral-and-borrow.tsx index f1fcba43..f85b338e 100644 --- a/src/modals/borrow/components/add-collateral-and-borrow.tsx +++ b/src/modals/borrow/components/add-collateral-and-borrow.tsx @@ -5,7 +5,7 @@ import { LTVWarning } from '@/components/shared/ltv-warning'; import { MarketDetailsBlock } from '@/features/markets/components/market-details-block'; import Input from '@/components/Input/Input'; import { useBorrowTransaction } from '@/hooks/useBorrowTransaction'; -import { useLocalStorage } from '@/hooks/useLocalStorage'; +import { useAppSettings } from '@/stores/useAppSettings'; import { formatBalance, formatReadable } from '@/utils/balance'; import { getNativeTokenSymbol } from '@/utils/networks'; import { isWrappedNativeToken } from '@/utils/tokens'; @@ -39,7 +39,7 @@ export function AddCollateralAndBorrow({ const [borrowAmount, setBorrowAmount] = useState(BigInt(0)); const [collateralInputError, setCollateralInputError] = useState(null); const [borrowInputError, setBorrowInputError] = useState(null); - const [usePermit2Setting] = useLocalStorage('usePermit2', true); + const { usePermit2: usePermit2Setting } = useAppSettings(); // lltv with 18 decimals const lltv = BigInt(market.lltv); diff --git a/src/modals/settings/blacklisted-markets-modal.tsx b/src/modals/settings/blacklisted-markets-modal.tsx index 39412c1e..0950f116 100644 --- a/src/modals/settings/blacklisted-markets-modal.tsx +++ b/src/modals/settings/blacklisted-markets-modal.tsx @@ -8,6 +8,8 @@ import { Button } from '@/components/ui/button'; import { Modal, ModalHeader, ModalBody, ModalFooter } from '@/components/common/Modal'; import { MarketIdentity, MarketIdentityMode, MarketIdentityFocus } from '@/features/markets/components/market-identity'; import { useMarkets } from '@/contexts/MarketsContext'; +import { useBlacklistedMarkets } from '@/stores/useBlacklistedMarkets'; +import { useStyledToast } from '@/hooks/useStyledToast'; import type { Market } from '@/utils/types'; type BlacklistedMarketsModalProps = { @@ -20,7 +22,10 @@ const ITEMS_PER_PAGE = 20; export function BlacklistedMarketsModal({ isOpen, onOpenChange }: BlacklistedMarketsModalProps) { const [searchQuery, setSearchQuery] = React.useState(''); const [currentPage, setCurrentPage] = React.useState(1); - const { rawMarketsUnfiltered, isBlacklisted, addBlacklistedMarket, removeBlacklistedMarket, isDefaultBlacklisted } = useMarkets(); + const { rawMarketsUnfiltered } = useMarkets(); + const { customBlacklistedMarkets, isBlacklisted, addBlacklistedMarket, removeBlacklistedMarket, isDefaultBlacklisted } = + useBlacklistedMarkets(); + const { success: toastSuccess } = useStyledToast(); // Reset to page 1 when search query changes React.useEffect(() => { @@ -44,7 +49,7 @@ export function BlacklistedMarketsModal({ isOpen, onOpenChange }: BlacklistedMar blacklistedMarkets: blacklisted.sort((a, b) => (a.loanAsset?.symbol ?? '').localeCompare(b.loanAsset?.symbol ?? '')), availableMarkets: available.sort((a, b) => (a.loanAsset?.symbol ?? '').localeCompare(b.loanAsset?.symbol ?? '')), }; - }, [rawMarketsUnfiltered, isBlacklisted]); + }, [rawMarketsUnfiltered, customBlacklistedMarkets, isBlacklisted]); // Filter available markets based on search query // Only show results if user has typed at least 2 characters @@ -125,7 +130,10 @@ export function BlacklistedMarketsModal({ isOpen, onOpenChange }: BlacklistedMar