From dd9fae2e336f195a33d32268ee6bedc6e69c233f Mon Sep 17 00:00:00 2001 From: The Joel Date: Thu, 30 Jul 2026 00:45:10 +0100 Subject: [PATCH] Add holdings sorting by total value descending with unit tests - Add calculatePositionTotalValue utility to compute individual position value - Add sortHoldingsByTotalValue utility to sort holdings by total value descending - Apply sorting in LandingPage component before rendering holdings list - Add comprehensive unit tests covering all acceptance criteria: - Holdings rendered in descending total value order (1200, 500, 300) - Order updates when cache data changes (300 -> 1500 reorders to 1500, 1200, 500) - Stable secondary sort by creator ID for equal total values - Handles missing prices and zero quantities gracefully - Does not mutate original array - Sort applied before render via useMemo in LandingPage --- src/pages/LandingPage.tsx | 62 +++++--- .../__tests__/portfolioValue.utils.test.ts | 140 ++++++++++++++++++ src/utils/portfolioValue.utils.ts | 37 +++++ 3 files changed, 221 insertions(+), 18 deletions(-) diff --git a/src/pages/LandingPage.tsx b/src/pages/LandingPage.tsx index b61b52ca..244aa680 100644 --- a/src/pages/LandingPage.tsx +++ b/src/pages/LandingPage.tsx @@ -37,6 +37,10 @@ import showToast from '@/utils/toast.util'; import { getSignatureErrorMessage } from '@/utils/errorHandling.utils'; import { formatCompactNumber, formatNumber } from '@/utils/numberFormat.utils'; import { formatOwnershipPercent } from '@/utils/ownership.utils'; +import { + calculatePortfolioValue, + sortHoldingsByTotalValue, +} from '@/utils/portfolioValue.utils'; import PrecisionModeToggle, { type PrecisionMode, } from '@/components/common/PrecisionModeToggle'; @@ -299,7 +303,10 @@ function LandingPage() { const prefersReducedMotion = usePrefersReducedMotion(); const [sortOption, setSortOption] = useState(() => { const sort = searchParams.get('sort') as SortOption | null; - if (sort && ['featured', 'price-asc', 'price-desc', 'supply-desc'].includes(sort)) { + if ( + sort && + ['featured', 'price-asc', 'price-desc', 'supply-desc'].includes(sort) + ) { sortOptionRef.current = sort; return sort; } @@ -386,7 +393,13 @@ function LandingPage() { setSearchQuery(''); } const sort = searchParams.get('sort') as SortOption | null; - if (sort && ['featured', 'price-asc', 'price-desc', 'supply-desc'].includes(sort) && sort !== sortOptionRef.current) { + if ( + sort && + ['featured', 'price-asc', 'price-desc', 'supply-desc'].includes( + sort + ) && + sort !== sortOptionRef.current + ) { setSortOption(sort); } else if (sort === null && sortOptionRef.current !== 'featured') { setSortOption('featured'); @@ -648,17 +661,19 @@ function LandingPage() { const heldKeyPositions = useMemo( () => - creators.map((creator, index) => ({ - creatorId: creator.id, - quantity: - index === 0 - ? featuredHoldings - : (DEMO_HELD_KEY_QUANTITIES[index] ?? 0), - priceStroops: creator.priceStroops, - price: creator.price, - isPriceLoading: isPriceRefreshing, - isPriceStale: creatorsAreStale, - })), + sortHoldingsByTotalValue( + creators.map((creator, index) => ({ + creatorId: creator.id, + quantity: + index === 0 + ? featuredHoldings + : (DEMO_HELD_KEY_QUANTITIES[index] ?? 0), + priceStroops: creator.priceStroops, + price: creator.price, + isPriceLoading: isPriceRefreshing, + isPriceStale: creatorsAreStale, + })) + ), [creators, creatorsAreStale, featuredHoldings, isPriceRefreshing] ); const portfolioValue = useMemo( @@ -831,7 +846,10 @@ function LandingPage() { Shortcut -