diff --git a/src/features/market-detail/components/charts/rate-chart.tsx b/src/features/market-detail/components/charts/rate-chart.tsx index 3b822cbc..0e2e8351 100644 --- a/src/features/market-detail/components/charts/rate-chart.tsx +++ b/src/features/market-detail/components/charts/rate-chart.tsx @@ -9,32 +9,43 @@ import { Spinner } from '@/components/ui/spinner'; import { CHART_COLORS } from '@/constants/chartColors'; import { useAppSettings } from '@/stores/useAppSettings'; import { useRateLabel } from '@/hooks/useRateLabel'; +import { formatChartTime } from '@/utils/chart'; +import { useMarketHistoricalData } from '@/hooks/useMarketHistoricalData'; +import { useMarketDetailChartState } from '@/stores/useMarketDetailChartState'; import { convertApyToApr } from '@/utils/rateMath'; -import type { MarketRates } from '@/utils/types'; -import type { TimeseriesDataPoint, Market, TimeseriesOptions } from '@/utils/types'; +import type { Market } from '@/utils/types'; +import type { TimeseriesDataPoint } from '@/utils/types'; type RateChartProps = { - historicalData: MarketRates | undefined; + marketId: string; + chainId: number; market: Market; - isLoading: boolean; - selectedTimeframe: '1d' | '7d' | '30d'; - selectedTimeRange: TimeseriesOptions; - handleTimeframeChange: (timeframe: '1d' | '7d' | '30d') => void; }; -function RateChart({ historicalData, market, isLoading, selectedTimeframe, selectedTimeRange, handleTimeframeChange }: RateChartProps) { +function RateChart({ marketId, chainId, market }: RateChartProps) { + // ✅ All hooks at top level - no conditional returns before hooks! + const selectedTimeframe = useMarketDetailChartState((s) => s.selectedTimeframe); + const selectedTimeRange = useMarketDetailChartState((s) => s.selectedTimeRange); + const setTimeframe = useMarketDetailChartState((s) => s.setTimeframe); const { isAprDisplay } = useAppSettings(); const { short: rateLabel } = useRateLabel(); + // Component fetches its own data (React Query caches by marketId + chainId + timeRange) + const { data: historicalData, isLoading } = useMarketHistoricalData(marketId, chainId, selectedTimeRange); + const [visibleLines, setVisibleLines] = useState({ supplyApy: true, borrowApy: true, apyAtTarget: true, }); + const handleTimeframeChange = (timeframe: '1d' | '7d' | '30d') => { + setTimeframe(timeframe); + }; + const getChartData = useMemo(() => { - if (!historicalData) return []; - const { supplyApy, borrowApy, apyAtTarget } = historicalData; + if (!historicalData?.rates) return []; + const { supplyApy, borrowApy, apyAtTarget } = historicalData.rates; return supplyApy.map((point: TimeseriesDataPoint, index: number) => { // Convert values to APR if display mode is enabled @@ -59,8 +70,8 @@ function RateChart({ historicalData, market, isLoading, selectedTimeframe, selec }; const getAverageApyValue = (type: 'supply' | 'borrow') => { - if (!historicalData) return 0; - const data = type === 'supply' ? historicalData.supplyApy : historicalData.borrowApy; + if (!historicalData?.rates) return 0; + const data = type === 'supply' ? historicalData.rates.supplyApy : historicalData.rates.borrowApy; const avgApy = data.length > 0 ? data.reduce((sum: number, point: TimeseriesDataPoint) => sum + point.y, 0) / data.length : 0; return isAprDisplay ? convertApyToApr(avgApy) : avgApy; }; @@ -71,9 +82,10 @@ function RateChart({ historicalData, market, isLoading, selectedTimeframe, selec }; const getAverageapyAtTargetValue = () => { - if (!historicalData?.apyAtTarget || historicalData.apyAtTarget.length === 0) return 0; + if (!historicalData?.rates?.apyAtTarget || historicalData.rates.apyAtTarget.length === 0) return 0; const avgApy = - historicalData.apyAtTarget.reduce((sum: number, point: TimeseriesDataPoint) => sum + point.y, 0) / historicalData.apyAtTarget.length; + historicalData.rates.apyAtTarget.reduce((sum: number, point: TimeseriesDataPoint) => sum + point.y, 0) / + historicalData.rates.apyAtTarget.length; return isAprDisplay ? convertApyToApr(avgApy) : avgApy; }; @@ -82,26 +94,13 @@ function RateChart({ historicalData, market, isLoading, selectedTimeframe, selec }; const getAverageUtilizationRate = () => { - if (!historicalData?.utilization || historicalData.utilization.length === 0) return 0; + if (!historicalData?.rates?.utilization || historicalData.rates.utilization.length === 0) return 0; return ( - historicalData.utilization.reduce((sum: number, point: TimeseriesDataPoint) => sum + point.y, 0) / historicalData.utilization.length + historicalData.rates.utilization.reduce((sum: number, point: TimeseriesDataPoint) => sum + point.y, 0) / + historicalData.rates.utilization.length ); }; - const formatTime = (unixTime: number) => { - const date = new Date(unixTime * 1000); - if (selectedTimeRange.endTimestamp - selectedTimeRange.startTimestamp <= 86_400) { - return date.toLocaleTimeString(undefined, { - hour: '2-digit', - minute: '2-digit', - }); - } - return date.toLocaleDateString(undefined, { - month: 'short', - day: 'numeric', - }); - }; - const timeframeOptions = [ { key: '1d', label: '1D', value: '1d' }, { key: '7d', label: '7D', value: '7d' }, @@ -192,7 +191,7 @@ function RateChart({ historicalData, market, isLoading, selectedTimeframe, selec formatChartTime(time, selectedTimeRange.endTimestamp - selectedTimeRange.startTimestamp)} /> `${(value * 100).toFixed(2)}%`} /> void; - selectedTimeframe: '1d' | '7d' | '30d'; - selectedTimeRange: TimeseriesOptions; - handleTimeframeChange: (timeframe: '1d' | '7d' | '30d') => void; }; -function VolumeChart({ - historicalData, - market, - isLoading, - volumeView, - setVolumeView, - selectedTimeframe, - selectedTimeRange, - handleTimeframeChange, -}: VolumeChartProps) { +function VolumeChart({ marketId, chainId, market }: VolumeChartProps) { + // ✅ All hooks at top level - no conditional returns before hooks! + const selectedTimeframe = useMarketDetailChartState((s) => s.selectedTimeframe); + const selectedTimeRange = useMarketDetailChartState((s) => s.selectedTimeRange); + const volumeView = useMarketDetailChartState((s) => s.volumeView); + const setTimeframe = useMarketDetailChartState((s) => s.setTimeframe); + const setVolumeView = useMarketDetailChartState((s) => s.setVolumeView); + + // Component fetches its own data (React Query caches by marketId + chainId + timeRange) + const { data: historicalData, isLoading } = useMarketHistoricalData(marketId, chainId, selectedTimeRange); + + const [visibleLines, setVisibleLines] = useState({ + supply: true, + borrow: true, + liquidity: true, + }); + + const handleTimeframeChange = (timeframe: '1d' | '7d' | '30d') => { + setTimeframe(timeframe); + }; + const formatYAxis = (value: number) => { if (volumeView === 'USD') { return `$${formatReadable(value)}`; @@ -42,26 +51,12 @@ function VolumeChart({ return formatReadable(value); }; - const formatTime = (unixTime: number) => { - const date = new Date(unixTime * 1000); - if (selectedTimeRange.endTimestamp - selectedTimeRange.startTimestamp <= 24 * 60 * 60) { - return date.toLocaleTimeString(undefined, { - hour: '2-digit', - minute: '2-digit', - }); - } - return date.toLocaleDateString(undefined, { - month: 'short', - day: 'numeric', - }); - }; - const getVolumeChartData = () => { - if (!historicalData) return []; + if (!historicalData?.volumes) return []; - const supplyData = volumeView === 'USD' ? historicalData.supplyAssetsUsd : historicalData.supplyAssets; - const borrowData = volumeView === 'USD' ? historicalData.borrowAssetsUsd : historicalData.borrowAssets; - const liquidityData = volumeView === 'USD' ? historicalData.liquidityAssetsUsd : historicalData.liquidityAssets; + const supplyData = volumeView === 'USD' ? historicalData.volumes.supplyAssetsUsd : historicalData.volumes.supplyAssets; + const borrowData = volumeView === 'USD' ? historicalData.volumes.borrowAssetsUsd : historicalData.volumes.borrowAssets; + const liquidityData = volumeView === 'USD' ? historicalData.volumes.liquidityAssetsUsd : historicalData.volumes.liquidityAssets; // Process all data in a single loop return supplyData @@ -78,7 +73,7 @@ function VolumeChart({ volumeView === 'USD' ? liquidityPoint?.y || 0 : Number(formatUnits(BigInt(liquidityPoint?.y || 0), market.loanAsset.decimals)); // Check if any timestamps has USD value exceeds 100B - if (historicalData.supplyAssetsUsd[index].y >= 100_000_000_000) { + if (historicalData.volumes.supplyAssetsUsd[index].y >= 100_000_000_000) { return null; } @@ -98,7 +93,7 @@ function VolumeChart({ }; const getCurrentVolumeStats = (type: 'supply' | 'borrow' | 'liquidity') => { - const data = volumeView === 'USD' ? historicalData?.[`${type}AssetsUsd`] : historicalData?.[`${type}Assets`]; + const data = volumeView === 'USD' ? historicalData?.volumes[`${type}AssetsUsd`] : historicalData?.volumes[`${type}Assets`]; if (!data || data.length === 0) return { current: 0, netChange: 0, netChangePercentage: 0 }; const current = @@ -113,7 +108,7 @@ function VolumeChart({ }; const getAverageVolumeStats = (type: 'supply' | 'borrow' | 'liquidity') => { - const data = volumeView === 'USD' ? historicalData?.[`${type}AssetsUsd`] : historicalData?.[`${type}Assets`]; + const data = volumeView === 'USD' ? historicalData?.volumes[`${type}AssetsUsd`] : historicalData?.volumes[`${type}Assets`]; if (!data || data.length === 0) return 0; const sum = data.reduce( (acc: number, point: TimeseriesDataPoint) => @@ -134,12 +129,6 @@ function VolumeChart({ { key: '30d', label: '30D', value: '30d' }, ]; - const [visibleLines, setVisibleLines] = useState({ - supply: true, - borrow: true, - liquidity: true, - }); - // This is only for adaptive curve const targetUtilizationData = useMemo(() => { const supply = market.state.supplyAssets ? BigInt(market.state.supplyAssets) : 0n; @@ -249,7 +238,7 @@ function VolumeChart({ formatChartTime(time, selectedTimeRange.endTimestamp - selectedTimeRange.startTimestamp)} /> { - const endTimestamp = NOW; - let startTimestamp; - let interval: TimeseriesOptions['interval'] = 'HOUR'; - switch (timeframe) { - case '1d': - startTimestamp = endTimestamp - DAY_IN_SECONDS; - break; - case '30d': - startTimestamp = endTimestamp - 30 * DAY_IN_SECONDS; - // Use DAY interval for longer ranges if desired, adjust as needed - interval = 'DAY'; - break; - default: - startTimestamp = endTimestamp - WEEK_IN_SECONDS; - break; - } - return { startTimestamp, endTimestamp, interval }; -}; - function MarketContent() { // 1. Get URL params first - const { marketid, chainId } = useParams(); + const { marketid: marketId, chainId } = useParams(); // 2. Network setup const network = Number(chainId as string) as SupportedNetworks; @@ -78,11 +51,6 @@ function MarketContent() { // 3. Consolidated state const { open: openModal } = useModal(); const [showBorrowModal, setShowBorrowModal] = useState(false); - const [selectedTimeframe, setSelectedTimeframe] = useState<'1d' | '7d' | '30d'>('7d'); - const [selectedTimeRange, setSelectedTimeRange] = useState( - calculateTimeRange('7d'), // Initialize based on default timeframe - ); - const [volumeView, setVolumeView] = useState<'USD' | 'Asset'>('Asset'); const [isRefreshing, setIsRefreshing] = useState(false); const [showTransactionFiltersModal, setShowTransactionFiltersModal] = useState(false); const [showSupplierFiltersModal, setShowSupplierFiltersModal] = useState(false); @@ -96,19 +64,13 @@ function MarketContent() { isLoading: isMarketLoading, error: marketError, refetch: refetchMarket, - } = useMarketData(marketid as string, network); + } = useMarketData(marketId as string, network); // Transaction filters with localStorage persistence (per symbol) const { minSupplyAmount, minBorrowAmount, setMinSupplyAmount, setMinBorrowAmount } = useTransactionFilters( market?.loanAsset?.symbol ?? '', ); - const { - data: historicalData, - isLoading: isHistoricalLoading, - // No need for manual refetch on time change, queryKey handles it - } = useMarketHistoricalData(marketid as string, network, selectedTimeRange); // Use selectedTimeRange - // 5. Oracle price hook - safely handle undefined market const { price: oraclePrice } = useOraclePrice({ oracle: market?.oracleAddress as `0x${string}`, @@ -121,7 +83,7 @@ function MarketContent() { position: userPosition, loading: positionLoading, refetch: refetchUserPosition, - } = useUserPositions(address, network, marketid as string); + } = useUserPosition(address, network, marketId as string); // 6. All memoized values and callbacks const formattedOraclePrice = useMemo(() => { @@ -220,13 +182,6 @@ function MarketContent() { }); }, [handleRefreshAll]); - // Unified handler for timeframe changes - const handleTimeframeChange = useCallback((timeframe: '1d' | '7d' | '30d') => { - setSelectedTimeframe(timeframe); - setSelectedTimeRange(calculateTimeRange(timeframe)); - // No explicit refetch needed, change in selectedTimeRange (part of queryKey) triggers it - }, []); - // 7. Early returns for loading/error states if (isMarketLoading) { return ( @@ -267,7 +222,7 @@ function MarketContent() { {market.loanAsset.symbol}/{market.collateralAsset.symbol} Market

Volume

Rates

diff --git a/src/features/markets/components/table/markets-table.tsx b/src/features/markets/components/table/markets-table.tsx index 8bf2ff98..f94ed0b4 100644 --- a/src/features/markets/components/table/markets-table.tsx +++ b/src/features/markets/components/table/markets-table.tsx @@ -13,41 +13,38 @@ import { TableContainerWithHeader } from '@/components/common/table-container-wi import EmptyScreen from '@/components/status/empty-screen'; import LoadingScreen from '@/components/status/loading-screen'; import { SuppliedAssetFilterCompactSwitch } from '@/features/positions/components/supplied-asset-filter-compact-switch'; -import type { TrustedVault } from '@/constants/vaults/known_vaults'; import { useMarketsQuery } from '@/hooks/queries/useMarketsQuery'; +import { useFilteredMarkets } from '@/hooks/useFilteredMarkets'; import { useRateLabel } from '@/hooks/useRateLabel'; +import { useModal } from '@/hooks/useModal'; import { useMarketPreferences } from '@/stores/useMarketPreferences'; -import type { Market } from '@/utils/types'; +import { useTrustedVaults } from '@/stores/useTrustedVaults'; import { buildTrustedVaultMap } from '@/utils/vaults'; import { SortColumn } from '../constants'; import { MarketTableBody } from './market-table-body'; import { HTSortable } from './market-table-utils'; type MarketsTableProps = { - markets: Market[]; currentPage: number; setCurrentPage: (value: number) => void; - trustedVaults: TrustedVault[]; className?: string; tableClassName?: string; - onOpenSettings: () => void; onRefresh: () => void; isMobile: boolean; }; -function MarketsTable({ - markets, - currentPage, - setCurrentPage, - trustedVaults, - className, - tableClassName, - onOpenSettings, - onRefresh, - isMobile, -}: MarketsTableProps) { +function MarketsTable({ currentPage, setCurrentPage, className, tableClassName, onRefresh, isMobile }: MarketsTableProps) { // Get loading states directly from query (no prop drilling!) const { isLoading: loading, isRefetching, data: rawMarkets } = useMarketsQuery(); + + // Get trusted vaults directly from store (no prop drilling!) + const { vaults: trustedVaults } = useTrustedVaults(); + + // Get modal management directly from store (no prop drilling!) + const { open: openModal } = useModal(); + + const markets = useFilteredMarkets(); + const isEmpty = !rawMarkets; const [expandedRowId, setExpandedRowId] = useState(null); const { label: supplyRateLabel } = useRateLabel({ prefix: 'Supply' }); @@ -107,7 +104,7 @@ function MarketsTable({ // Header actions (filter, refresh, expand/compact, settings) const headerActions = ( <> - + openModal('marketSettings', {})} /> openModal('marketSettings', {})} > diff --git a/src/features/markets/markets-view.tsx b/src/features/markets/markets-view.tsx index 76699a95..8c228631 100644 --- a/src/features/markets/markets-view.tsx +++ b/src/features/markets/markets-view.tsx @@ -5,12 +5,9 @@ import type { Chain } from 'viem'; import Header from '@/components/layout/header/Header'; import { useTokensQuery } from '@/hooks/queries/useTokensQuery'; import { useMarketsQuery } from '@/hooks/queries/useMarketsQuery'; -import { useFilteredMarkets } from '@/hooks/useFilteredMarkets'; import { useMarketsFilters } from '@/stores/useMarketsFilters'; -import { useModal } from '@/hooks/useModal'; import { usePagination } from '@/hooks/usePagination'; import { useStyledToast } from '@/hooks/useStyledToast'; -import { useTrustedVaults } from '@/stores/useTrustedVaults'; import { useMarketPreferences } from '@/stores/useMarketPreferences'; import type { ERC20Token, UnknownERC20Token } from '@/utils/tokens'; @@ -29,9 +26,6 @@ export default function Markets() { // Filter state (persisted to localStorage) const filters = useMarketsFilters(); - // Derived data (filtered + sorted markets) - const filteredMarkets = useFilteredMarkets(); - // UI state const [uniqueCollaterals, setUniqueCollaterals] = useState<(ERC20Token | UnknownERC20Token)[]>([]); const [uniqueLoanAssets, setUniqueLoanAssets] = useState<(ERC20Token | UnknownERC20Token)[]>([]); @@ -41,8 +35,6 @@ export default function Markets() { const { currentPage, setCurrentPage, resetPage } = usePagination(); const { allTokens } = useTokensQuery(); const { tableViewMode, includeUnknownTokens } = useMarketPreferences(); - const { vaults: userTrustedVaults } = useTrustedVaults(); - const { open: openModal } = useModal(); // Force compact mode on mobile useEffect(() => { @@ -229,13 +221,10 @@ export default function Markets() {
openModal('marketSettings', {})} onRefresh={handleRefresh} isMobile={isMobile} /> diff --git a/src/stores/useMarketDetailChartState.ts b/src/stores/useMarketDetailChartState.ts new file mode 100644 index 00000000..f7fec2c9 --- /dev/null +++ b/src/stores/useMarketDetailChartState.ts @@ -0,0 +1,62 @@ +import { create } from 'zustand'; +import type { TimeseriesOptions } from '@/utils/types'; + +const DAY_IN_SECONDS = 24 * 60 * 60; +const WEEK_IN_SECONDS = 7 * DAY_IN_SECONDS; + +// Helper to calculate time range based on timeframe string +const calculateTimeRange = (timeframe: '1d' | '7d' | '30d'): TimeseriesOptions => { + const endTimestamp = Math.floor(Date.now() / 1000); + let startTimestamp; + let interval: TimeseriesOptions['interval'] = 'HOUR'; + switch (timeframe) { + case '1d': + startTimestamp = endTimestamp - DAY_IN_SECONDS; + break; + case '30d': + startTimestamp = endTimestamp - 30 * DAY_IN_SECONDS; + // Use DAY interval for longer ranges if desired, adjust as needed + interval = 'DAY'; + break; + default: + startTimestamp = endTimestamp - WEEK_IN_SECONDS; + break; + } + return { startTimestamp, endTimestamp, interval }; +}; + +type ChartState = { + selectedTimeframe: '1d' | '7d' | '30d'; + selectedTimeRange: TimeseriesOptions; + volumeView: 'USD' | 'Asset'; +}; + +type ChartActions = { + setTimeframe: (timeframe: '1d' | '7d' | '30d') => void; + setVolumeView: (view: 'USD' | 'Asset') => void; +}; + +type MarketDetailChartStore = ChartState & ChartActions; + +/** + * [No persist] Zustand store for market detail chart state (shared between VolumeChart and RateChart). + * + */ +export const useMarketDetailChartState = create((set) => ({ + // Default state + selectedTimeframe: '7d', + selectedTimeRange: calculateTimeRange('7d'), + volumeView: 'Asset', + + // Actions + setTimeframe: (timeframe) => { + set({ + selectedTimeframe: timeframe, + selectedTimeRange: calculateTimeRange(timeframe), + }); + }, + + setVolumeView: (view) => { + set({ volumeView: view }); + }, +})); diff --git a/src/utils/chart.ts b/src/utils/chart.ts new file mode 100644 index 00000000..71f1620a --- /dev/null +++ b/src/utils/chart.ts @@ -0,0 +1,21 @@ +/** + * Format Unix timestamp for chart x-axis labels. + */ +export function formatChartTime(unixTime: number, timeRangeDuration: number): string { + const date = new Date(unixTime * 1000); + const ONE_DAY = 24 * 60 * 60; // 86400 seconds + + // For 1-day timeframe, show hours:minutes + if (timeRangeDuration <= ONE_DAY) { + return date.toLocaleTimeString(undefined, { + hour: '2-digit', + minute: '2-digit', + }); + } + + // For longer timeframes, show month + day + return date.toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + }); +}