From b9f513c2135aa221da289b29113bcc6e2ba475fb Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sun, 18 May 2025 13:21:48 +0800 Subject: [PATCH 1/3] feat: styling and sorting on rebalance modal --- app/markets/components/markets.tsx | 25 +- app/positions/components/FromAndToMarkets.tsx | 259 ++++++++++++++---- src/hooks/useStaredMarkets.ts | 45 +++ 3 files changed, 247 insertions(+), 82 deletions(-) create mode 100644 src/hooks/useStaredMarkets.ts 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..b5d43557 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,46 @@ 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 +100,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,8 +204,7 @@ export function FromAndToMarkets({ Market - Collateral - LLTV + Collateral / LLTV APY Supplied Amount @@ -131,31 +225,35 @@ export function FromAndToMarkets({ {marketPosition.market.uniqueKey.slice(2, 8)} -
- - 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)}% @@ -240,10 +338,27 @@ export function FromAndToMarkets({ Market - Collateral - LLTV - APY - Total Supply + + + Util Rate Risks @@ -264,32 +379,58 @@ export function FromAndToMarkets({ }`} > - {market.uniqueKey.slice(2, 8)} +
+ {market.uniqueKey.slice(2, 8)} + {staredIds.includes(market.uniqueKey) && ( + + + + )} + {fromMarkets.some( + (fm) => fm.market.uniqueKey === market.uniqueKey, + ) && ( + } + className="rounded-sm" + placement="top" + > + + + + + )} +
-
- - e.stopPropagation()} - className="flex items-center gap-1 no-underline hover:underline" - > - {market.collateralAsset.symbol} - + - {formatUnits(BigInt(market.lltv), 16)}% {formatReadable(market.state.supplyApy * 100)}% {formatReadable( diff --git a/src/hooks/useStaredMarkets.ts b/src/hooks/useStaredMarkets.ts new file mode 100644 index 00000000..7eb686e4 --- /dev/null +++ b/src/hooks/useStaredMarkets.ts @@ -0,0 +1,45 @@ +import { useState, useCallback } from 'react'; +import storage from 'local-storage-fallback'; +import { useStyledToast } from '@/hooks/useStyledToast'; +import * as keys from '@/utils/storageKeys'; + +const getInitialStaredMarkets = (): string[] => { + try { + const item = storage.getItem(keys.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(keys.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(keys.MarketFavoritesKey, JSON.stringify(newStaredIds)); + toastSuccess('Market unstarred', 'Market removed from favorites'); + }, + [staredIds, toastSuccess], + ); + + return { staredIds, starMarket, unstarMarket }; +}; \ No newline at end of file From 816c18939b5cbef0c11ca2b408a9d6469d48567d Mon Sep 17 00:00:00 2001 From: antoncoding Date: Sun, 18 May 2025 13:37:52 +0800 Subject: [PATCH 2/3] fix: add liquidity consideration to max amount button --- app/positions/components/FromAndToMarkets.tsx | 45 +++++++++++-------- src/hooks/useStaredMarkets.ts | 2 +- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/app/positions/components/FromAndToMarkets.tsx b/app/positions/components/FromAndToMarkets.tsx index b5d43557..b49cdb0b 100644 --- a/app/positions/components/FromAndToMarkets.tsx +++ b/app/positions/components/FromAndToMarkets.tsx @@ -68,8 +68,9 @@ function SortableHeader({ className = 'px-4 py-2 text-left', }: SortableHeaderProps) { const isSorted = currentSortColumn === column; - const commonClass = "flex items-center gap-1"; - const sortIcon = isSorted && (currentSortDirection === 1 ? : ); + const commonClass = 'flex items-center gap-1'; + const sortIcon = + isSorted && (currentSortDirection === 1 ? : ); return ( {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 (
- + {formatUnits(BigInt(marketPosition.market.lltv), 16)}%
@@ -267,25 +281,20 @@ export function FromAndToMarkets({ )}{' '} {marketPosition.market.loanAsset.symbol} + + {/* max button */}