diff --git a/app/markets/components/markets.tsx b/app/markets/components/markets.tsx index d8e72a41..97924c60 100644 --- a/app/markets/components/markets.tsx +++ b/app/markets/components/markets.tsx @@ -15,6 +15,7 @@ import { SupplyModalV2 } from '@/components/SupplyModalV2'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { useMarkets } from '@/hooks/useMarkets'; import { usePagination } from '@/hooks/usePagination'; +import { useStaredMarkets } from '@/hooks/useStaredMarkets'; import { useStyledToast } from '@/hooks/useStyledToast'; import { SupportedNetworks } from '@/utils/networks'; import { OracleVendors, parseOracleVendors } from '@/utils/oracle'; @@ -41,10 +42,12 @@ const defaultSortColumn = Object.values(SortColumn).includes(storedSortColumn) : SortColumn.Supply; const defaultSortDirection = Number(storage.getItem(keys.MarketSortDirectionKey) ?? '-1'); -const defaultStaredMarkets = JSON.parse( - storage.getItem(keys.MarketFavoritesKey) ?? '[]', -) as string[]; +/** + * Displays a list of financial markets with advanced filtering, sorting, search, and pagination features. + * + * Integrates user preferences, starred markets, and modal dialogs for market settings and supply actions. Synchronizes filter state with URL parameters and persists user settings in local storage. + */ export default function Markets() { const router = useRouter(); const searchParams = useSearchParams(); @@ -52,6 +55,7 @@ export default function Markets() { const toast = useStyledToast(); const { loading, markets: rawMarkets, refetch, isRefetching } = useMarkets(); + const { staredIds, starMarket, unstarMarket } = useStaredMarkets(); const { isOpen: isSettingsModalOpen, @@ -82,8 +86,6 @@ export default function Markets() { const [showSupplyModal, setShowSupplyModal] = useState(false); const [selectedMarket, setSelectedMarket] = useState(undefined); - const [staredIds, setStaredIds] = useState(defaultStaredMarkets); - const [filteredMarkets, setFilteredMarkets] = useState([]); const prevParamsRef = useRef(''); @@ -129,24 +131,6 @@ export default function Markets() { } }, [searchParams]); - const starMarket = useCallback( - (id: string) => { - setStaredIds([...staredIds, id]); - storage.setItem(keys.MarketFavoritesKey, JSON.stringify([...staredIds, id])); - toast.success('Market starred', 'Market added to favorites', { icon: 🌟 }); - }, - [staredIds, toast], - ); - - const unstarMarket = useCallback( - (id: string) => { - setStaredIds(staredIds.filter((i) => i !== id)); - storage.setItem(keys.MarketFavoritesKey, JSON.stringify(staredIds.filter((i) => i !== id))); - toast.success('Market unstarred', 'Market removed from favorites', { icon: 🌟 }); - }, - [staredIds, toast], - ); - useEffect(() => { // return if no markets if (!rawMarkets) return; diff --git a/app/positions/components/FromAndToMarkets.tsx b/app/positions/components/FromAndToMarkets.tsx index 9b5b44b2..4a21fa07 100644 --- a/app/positions/components/FromAndToMarkets.tsx +++ b/app/positions/components/FromAndToMarkets.tsx @@ -1,9 +1,12 @@ -import React from 'react'; -import { Input } from '@nextui-org/react'; +import React, { useState, useMemo } from 'react'; +import { Input, Tooltip } from '@nextui-org/react'; import { Pagination } from '@nextui-org/react'; import { Button } from '@nextui-org/react'; +import { FaArrowUp, FaArrowDown, FaStar, FaUser } from 'react-icons/fa'; import { formatUnits } from 'viem'; import { TokenIcon } from '@/components/TokenIcon'; +import { TooltipContent } from '@/components/TooltipContent'; +import { useStaredMarkets } from '@/hooks/useStaredMarkets'; import { formatReadable } from '@/utils/balance'; import { getAssetURL } from '@/utils/external'; import { Market } from '@/utils/types'; @@ -41,6 +44,82 @@ type MarketTablesProps = { selectedToMarketUniqueKey: string; }; +enum ToMarketSortColumn { + APY, + TotalSupply, + LLTV, // Added for future use +} + +type SortableHeaderProps = { + label: string; + column: ToMarketSortColumn; + currentSortColumn: ToMarketSortColumn | null; + currentSortDirection: number; + onClick: (column: ToMarketSortColumn) => void; + className?: string; +}; + +/** + * Renders a sortable table header cell with a clickable label and sort direction indicator. + * + * Displays an up or down arrow icon if the column is currently sorted. + * + * @param label - The text label for the header cell. + * @param column - The column enum value associated with this header. + * @param currentSortColumn - The currently sorted column. + * @param currentSortDirection - The current sort direction (1 for ascending, -1 for descending). + * @param onClick - Callback invoked with the column enum when the header is clicked. + * @param className - Optional CSS class for the header cell. + */ +function SortableHeader({ + label, + column, + currentSortColumn, + currentSortDirection, + onClick, + className = 'px-4 py-2 text-left', +}: SortableHeaderProps) { + const isSorted = currentSortColumn === column; + const commonClass = 'flex items-center gap-1'; + const sortIcon = + isSorted && (currentSortDirection === 1 ? : ); + + return ( + onClick(column)} + > +
+ {label} + {sortIcon} +
+ + ); +} + +/** + * Displays two interactive tables for managing market positions and selecting markets for rebalancing. + * + * Renders "Your Market Positions" and "Available Markets for Rebalancing" tables with filtering, sorting, pagination, and selection capabilities. Users can filter markets by ID or collateral, sort available markets by APY, total supply, or LLTV, and select or maximize positions for rebalancing actions. + * + * @param eligibleMarkets - List of all eligible markets with full details for risk indicators. + * @param fromMarkets - User's current market positions. + * @param toMarkets - Markets available for rebalancing. + * @param fromFilter - Current filter string for the "fromMarkets" table. + * @param toFilter - Current filter string for the "toMarkets" table. + * @param onFromFilterChange - Callback for updating the "fromMarkets" filter. + * @param onToFilterChange - Callback for updating the "toMarkets" filter. + * @param onFromMarketSelect - Callback when a market is selected in "fromMarkets". + * @param onToMarketSelect - Callback when a market is selected in "toMarkets". + * @param onSelectMax - Optional callback when the "Max" button is clicked for a market position. + * @param fromPagination - Pagination state and handlers for "fromMarkets". + * @param toPagination - Pagination state and handlers for "toMarkets". + * @param selectedFromMarketUniqueKey - Unique key of the currently selected "fromMarkets" row. + * @param selectedToMarketUniqueKey - Unique key of the currently selected "toMarkets" row. + * + * @remark + * The "Max" button in the "Your Market Positions" table is disabled if the maximum transferable amount is zero or less, reflecting both user supply and market liquidity. Sorting in the "Available Markets for Rebalancing" table is applied after filtering and before pagination. + */ export function FromAndToMarkets({ eligibleMarkets, fromMarkets, @@ -57,23 +136,75 @@ export function FromAndToMarkets({ selectedFromMarketUniqueKey, selectedToMarketUniqueKey, }: MarketTablesProps) { + const { staredIds } = useStaredMarkets(); + + const [toSortColumn, setToSortColumn] = useState( + ToMarketSortColumn.TotalSupply, + ); + const [toSortDirection, setToSortDirection] = useState(-1); // -1 for desc, 1 for asc + + const handleToSortChange = (column: ToMarketSortColumn) => { + if (toSortColumn === column) { + setToSortDirection(toSortDirection * -1); + } else { + setToSortColumn(column); + setToSortDirection(-1); // Default to descending for new column + } + }; + const filteredFromMarkets = fromMarkets.filter( (marketPosition) => marketPosition.market.uniqueKey.toLowerCase().includes(fromFilter.toLowerCase()) || marketPosition.market.collateralAsset.symbol.toLowerCase().includes(fromFilter.toLowerCase()), ); - const filteredToMarkets = toMarkets.filter( - (market) => - market.uniqueKey.toLowerCase().includes(toFilter.toLowerCase()) || - market.collateralAsset.symbol.toLowerCase().includes(toFilter.toLowerCase()), - ); + const filteredToMarkets = useMemo(() => { + return toMarkets.filter( + (market) => + market.uniqueKey.toLowerCase().includes(toFilter.toLowerCase()) || + market.collateralAsset.symbol.toLowerCase().includes(toFilter.toLowerCase()), + ); + }, [toMarkets, toFilter]); + + const sortedAndFilteredToMarkets = useMemo(() => { + let sorted = [...filteredToMarkets]; + if (toSortColumn !== null) { + sorted.sort((a, b) => { + let valA: number | bigint = 0; + let valB: number | bigint = 0; + + switch (toSortColumn) { + case ToMarketSortColumn.APY: + valA = a.state.supplyApy; + valB = b.state.supplyApy; + break; + case ToMarketSortColumn.TotalSupply: + // Ensure consistent comparison, potentially convert to number if safe + // For now, using BigInt comparison which is fine for sorting + valA = BigInt(a.state.supplyAssets); + valB = BigInt(b.state.supplyAssets); + break; + case ToMarketSortColumn.LLTV: + valA = BigInt(a.lltv); + valB = BigInt(b.lltv); + break; + default: + return 0; + } + + if (valA < valB) return -1 * toSortDirection; + if (valA > valB) return 1 * toSortDirection; + return 0; + }); + } + return sorted; + }, [filteredToMarkets, toSortColumn, toSortDirection]); const paginatedFromMarkets = filteredFromMarkets.slice( (fromPagination.currentPage - 1) * PER_PAGE, fromPagination.currentPage * PER_PAGE, ); - const paginatedToMarkets = filteredToMarkets.slice( + const paginatedToMarkets = sortedAndFilteredToMarkets.slice( (toPagination.currentPage - 1) * PER_PAGE, toPagination.currentPage * PER_PAGE, ); @@ -109,14 +240,26 @@ export function FromAndToMarkets({ Market - Collateral - LLTV + Collateral / LLTV APY Supplied Amount {paginatedFromMarkets.map((marketPosition) => { + const userConfirmedSupply = BigInt(marketPosition.state.supplyAssets); + const pendingDeltaBigInt = BigInt(marketPosition.pendingDelta); + const userNetSupply = userConfirmedSupply + pendingDeltaBigInt; + + const rawMarketLiquidity = BigInt(marketPosition.market.state.liquidityAssets); + + const adjustedMarketLiquidity = rawMarketLiquidity + pendingDeltaBigInt; + + const maxTransferableAmount = + userNetSupply < adjustedMarketLiquidity + ? userNetSupply + : adjustedMarketLiquidity; + return ( -
- - e.stopPropagation()} - className="flex items-center gap-1 no-underline hover:underline" - > - {marketPosition.market.collateralAsset.symbol} - + - - {formatUnits(BigInt(marketPosition.market.lltv), 16)}% - {formatReadable(marketPosition.market.state.supplyApy * 100)}% @@ -169,25 +316,20 @@ export function FromAndToMarkets({ )}{' '} {marketPosition.market.loanAsset.symbol}
+ + {/* max button */}