diff --git a/app/markets/components/markets.tsx b/app/markets/components/markets.tsx index d8e72a41..cc426ed0 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,9 +42,6 @@ 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[]; export default function Markets() { const router = useRouter(); @@ -52,6 +50,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 +81,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 +126,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..88b8c15d 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,47 @@ 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; +}; + +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} +
+ + ); +} + export function FromAndToMarkets({ eligibleMarkets, fromMarkets, @@ -57,23 +101,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 +205,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 +281,20 @@ export function FromAndToMarkets({ )}{' '} {marketPosition.market.loanAsset.symbol}
+ + {/* max button */}