From f7f1cf832af0447a2e0b42335f68301f656db295 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Thu, 2 Apr 2026 20:52:28 +0100 Subject: [PATCH 1/3] [#772] Simplify Reader holdings + portfolio boxes, add trade history Holdings cards: replace 4-box grid with 2 boxes (Value with cost-basis % change, Balance). Remove PnL and First Traded boxes. Add recent 5 transactions list below each card via HoldingRecentTrades component. Portfolio dashboard: replace 4-box grid with 2 boxes (Value in USD with portfolio-level cost-basis % change, Holdings count). Remove PLOT and Best 24h boxes. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/profile/[address]/page.tsx | 130 ++++++++++++++++++----------- 1 file changed, 81 insertions(+), 49 deletions(-) diff --git a/src/app/profile/[address]/page.tsx b/src/app/profile/[address]/page.tsx index d7aa6678..62fbbd1f 100644 --- a/src/app/profile/[address]/page.tsx +++ b/src/app/profile/[address]/page.tsx @@ -1441,13 +1441,25 @@ function PortfolioTab({ address, isOwnProfile }: { address: string; isOwnProfile const totalValue = holdings?.reduce((sum, h) => sum + h.value, BigInt(0)) ?? BigInt(0); const reserveDecimals = holdings && holdings.length > 0 ? holdings[0].reserveDecimals : 18; - const bestPick = holdings && holdings.length > 0 - ? holdings.reduce((best, h) => - (h.priceChange ?? -Infinity) > (best.priceChange ?? -Infinity) ? h : best - ) - : null; const totalDonated = donationsGiven.reduce((sum, d) => sum + BigInt(d.amount), BigInt(0)); + // Compute portfolio-level cost basis % change + const portfolioCostPct = (() => { + if (!holdings || holdings.length === 0 || plotUsd == null) return null; + let totalCurrentUsd = 0; + let totalCostUsd = 0; + for (const h of holdings) { + const currentPrice = Number(formatUnits(h.price, 18)); + const balanceNum = Number(formatUnits(h.balance, 18)); + totalCurrentUsd += currentPrice * balanceNum * plotUsd; + if (h.entryPrice !== null && h.entryPrice > 0) { + totalCostUsd += h.entryPrice * balanceNum * plotUsd; + } + } + if (totalCostUsd === 0) return null; + return ((totalCurrentUsd - totalCostUsd) / totalCostUsd) * 100; + })(); + return (
{/* Portfolio summary */} @@ -1455,25 +1467,22 @@ function PortfolioTab({ address, isOwnProfile }: { address: string; isOwnProfile <>

Portfolio

-
-
-
{formatPrice(formatUnits(totalValue, reserveDecimals))}
-
{RESERVE_LABEL}
-
+
-
{plotUsd ? formatUsdValue(Number(formatUnits(totalValue, reserveDecimals)) * plotUsd) : "—"}
-
USD
+
+ {plotUsd ? formatUsdValue(Number(formatUnits(totalValue, reserveDecimals)) * plotUsd) : "—"} + {portfolioCostPct !== null && ( + = 0 ? "text-accent" : "text-error"}`}> + {portfolioCostPct >= 0 ? "+" : ""}{portfolioCostPct.toFixed(1)}% + + )} +
+
Value
{holdings!.length}
Holdings
-
-
= 0 ? "text-accent" : "text-error") : "text-foreground"}`}> - {bestPick && bestPick.priceChange !== null ? `${bestPick.priceChange >= 0 ? "+" : ""}${bestPick.priceChange.toFixed(1)}%` : "—"} -
-
Best 24h
-
@@ -1527,11 +1536,16 @@ function PortfolioTab({ address, isOwnProfile }: { address: string; isOwnProfile
{plotUsd ? formatUsdValue(Number(formatUnits(h.value, h.reserveDecimals)) * plotUsd) : "—"} - {h.priceChange !== null && ( - = 0 ? "text-accent" : "text-error"}`}> - {h.priceChange >= 0 ? "+" : ""}{h.priceChange.toFixed(1)}% - - )} + {(() => { + if (h.entryPrice == null || h.entryPrice <= 0 || plotUsd == null) return null; + const currentPrice = Number(formatUnits(h.price, 18)); + const costPct = ((currentPrice - h.entryPrice) / h.entryPrice) * 100; + return ( + = 0 ? "text-accent" : "text-error"}`}> + {costPct >= 0 ? "+" : ""}{costPct.toFixed(1)}% + + ); + })()}
Value
@@ -1540,33 +1554,9 @@ function PortfolioTab({ address, isOwnProfile }: { address: string; isOwnProfile
{formatCompact(Number(formatUnits(h.balance, 18)))}
Balance
- {/* PnL */} -
- {h.entryPrice !== null && h.entryPrice > 0 && plotUsd != null ? (() => { - const currentPrice = Number(formatUnits(h.price, 18)); - const balanceNum = Number(formatUnits(h.balance, 18)); - const pnlUsd = (currentPrice - h.entryPrice) * balanceNum * plotUsd; - const pnlPct = ((currentPrice - h.entryPrice) / h.entryPrice) * 100; - const isPositive = pnlUsd >= 0; - return ( -
- {isPositive ? "+" : "-"}{formatUsdValue(Math.abs(pnlUsd))} - {isPositive ? "+" : ""}{pnlPct.toFixed(1)}% -
- ); - })() : ( -
- )} -
PnL
-
- {/* First Traded */} -
-
- {h.firstTraded ? new Date(h.firstTraded).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }) : "—"} -
-
First Traded
-
+ {/* Recent transactions */} + @@ -1579,6 +1569,48 @@ function PortfolioTab({ address, isOwnProfile }: { address: string; isOwnProfile ); } +// --------------------------------------------------------------------------- +// Holding Recent Trades — last 5 transactions for a specific story token +// --------------------------------------------------------------------------- + +function HoldingRecentTrades({ address, storylineId, plotUsd }: { address: string; storylineId: number; plotUsd?: number | null }) { + const { data: trades, isLoading } = useQuery({ + queryKey: ["holding-recent-trades", address, storylineId], + queryFn: async () => { + if (!supabase) return []; + const { data } = await supabase + .from("trade_history") + .select("event_type, reserve_amount, block_timestamp") + .eq("user_address", address) + .eq("storyline_id", storylineId) + .eq("contract_address", MCV2_BOND.toLowerCase()) + .order("block_timestamp", { ascending: false }) + .limit(5); + return data ?? []; + }, + staleTime: 60000, + }); + + if (isLoading || !trades || trades.length === 0) return null; + + return ( +
+ {trades.map((t, i) => { + const isBuy = t.event_type === "mint"; + const date = new Date(t.block_timestamp).toLocaleDateString("en-US", { month: "short", day: "numeric" }); + const amount = plotUsd != null ? formatUsdValue(t.reserve_amount * plotUsd) : `${formatPrice(t.reserve_amount)} ${RESERVE_LABEL}`; + return ( +
+ {isBuy ? "Buy" : "Sell"} + {amount} + {date} +
+ ); + })} +
+ ); +} + // --------------------------------------------------------------------------- // Portfolio Trading History — paginated trades // --------------------------------------------------------------------------- From 8b724a47f273edc41a7d003df73499c84657fe9c Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Thu, 2 Apr 2026 20:54:45 +0100 Subject: [PATCH 2/3] [#772] Fix T2a review: exclude holdings without entry price from portfolio cost % Only include holdings with known entryPrice in both current value and cost basis calculations, preventing transferred/untracked holdings from inflating the portfolio percentage. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/profile/[address]/page.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/profile/[address]/page.tsx b/src/app/profile/[address]/page.tsx index 62fbbd1f..896e3331 100644 --- a/src/app/profile/[address]/page.tsx +++ b/src/app/profile/[address]/page.tsx @@ -1449,12 +1449,11 @@ function PortfolioTab({ address, isOwnProfile }: { address: string; isOwnProfile let totalCurrentUsd = 0; let totalCostUsd = 0; for (const h of holdings) { + if (h.entryPrice === null || h.entryPrice <= 0) continue; const currentPrice = Number(formatUnits(h.price, 18)); const balanceNum = Number(formatUnits(h.balance, 18)); totalCurrentUsd += currentPrice * balanceNum * plotUsd; - if (h.entryPrice !== null && h.entryPrice > 0) { - totalCostUsd += h.entryPrice * balanceNum * plotUsd; - } + totalCostUsd += h.entryPrice * balanceNum * plotUsd; } if (totalCostUsd === 0) return null; return ((totalCurrentUsd - totalCostUsd) / totalCostUsd) * 100; From 154f6b1a9459655b3097d9d3f2f1eded1356b843 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Thu, 2 Apr 2026 20:56:38 +0100 Subject: [PATCH 3/3] [#772] Fix T2a review: only show portfolio cost % when all holdings have entry prices Portfolio cost % is now hidden unless every holding has a known entry price, ensuring the displayed percentage always describes the full Value amount shown. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/profile/[address]/page.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/profile/[address]/page.tsx b/src/app/profile/[address]/page.tsx index 896e3331..2108efc5 100644 --- a/src/app/profile/[address]/page.tsx +++ b/src/app/profile/[address]/page.tsx @@ -1443,17 +1443,17 @@ function PortfolioTab({ address, isOwnProfile }: { address: string; isOwnProfile const reserveDecimals = holdings && holdings.length > 0 ? holdings[0].reserveDecimals : 18; const totalDonated = donationsGiven.reduce((sum, d) => sum + BigInt(d.amount), BigInt(0)); - // Compute portfolio-level cost basis % change + // Compute portfolio-level cost basis % change (only if all holdings have entry prices) const portfolioCostPct = (() => { if (!holdings || holdings.length === 0 || plotUsd == null) return null; + if (holdings.some(h => h.entryPrice === null || h.entryPrice <= 0)) return null; let totalCurrentUsd = 0; let totalCostUsd = 0; for (const h of holdings) { - if (h.entryPrice === null || h.entryPrice <= 0) continue; const currentPrice = Number(formatUnits(h.price, 18)); const balanceNum = Number(formatUnits(h.balance, 18)); totalCurrentUsd += currentPrice * balanceNum * plotUsd; - totalCostUsd += h.entryPrice * balanceNum * plotUsd; + totalCostUsd += h.entryPrice! * balanceNum * plotUsd; } if (totalCostUsd === 0) return null; return ((totalCurrentUsd - totalCostUsd) / totalCostUsd) * 100;