From cabdeb55f91ea9a0fdbae73b4be17b47586ea458 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Thu, 2 Apr 2026 14:41:26 +0100 Subject: [PATCH 1/2] [#756] Replace Reader holdings inline stats with 4-box grid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces inline label-value text (Balance, Price, Entry, Traded) with a 2×2 stat-box grid matching the Writer tab pattern: - Value: USD with 24h % change (colored green/red) - Balance: compact K/M format via formatCompact helper - PnL: USD profit/loss with % (or dash if no entry price) - First Traded: full date with year from first mint timestamp Also adds firstTraded field to PortfolioHolding interface, derived from the existing first-mint query. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/profile/[address]/page.tsx | 81 ++++++++++++++++++++---------- 1 file changed, 55 insertions(+), 26 deletions(-) diff --git a/src/app/profile/[address]/page.tsx b/src/app/profile/[address]/page.tsx index aefcc593..cf646d03 100644 --- a/src/app/profile/[address]/page.tsx +++ b/src/app/profile/[address]/page.tsx @@ -192,6 +192,12 @@ export default function ProfilePage() { ); } +function formatCompact(n: number): string { + if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; + if (n >= 1_000) return `${(n / 1_000).toFixed(0)}K`; + return n.toLocaleString(); +} + // --------------------------------------------------------------------------- // Profile Header — Social Credibility Trust Dashboard // --------------------------------------------------------------------------- @@ -1231,6 +1237,7 @@ interface PortfolioHolding { value: bigint; entryPrice: number | null; lastTraded: string | null; + firstTraded: string | null; priceChange: number | null; reserveDecimals: number; } @@ -1294,6 +1301,7 @@ function PortfolioTab({ address, isOwnProfile }: { address: string; isOwnProfile // Derive entry price from first mint in trade_history let entryPrice: number | null = null; let lastTraded: string | null = null; + let firstTraded: string | null = null; if (supabase) { const { data: firstMint } = await supabase .from("trade_history") @@ -1306,6 +1314,7 @@ function PortfolioTab({ address, isOwnProfile }: { address: string; isOwnProfile .limit(1); if (firstMint && firstMint.length > 0) { entryPrice = firstMint[0].price_per_token; + firstTraded = firstMint[0].block_timestamp; } const { data: lastTrade } = await supabase .from("trade_history") @@ -1322,7 +1331,7 @@ function PortfolioTab({ address, isOwnProfile }: { address: string; isOwnProfile return { storyline: sl, balance, price: priceBI, value, - entryPrice, lastTraded, + entryPrice, lastTraded, firstTraded, priceChange: priceChangeResult?.changePercent ?? null, reserveDecimals, }; @@ -1495,32 +1504,52 @@ function PortfolioTab({ address, isOwnProfile }: { address: string; isOwnProfile -
-
- {formatPrice(formatUnits(h.value, h.reserveDecimals))} {RESERVE_LABEL} - {plotUsd && ({formatUsdValue(Number(formatUnits(h.value, h.reserveDecimals)) * plotUsd)})} - {h.priceChange !== null && ( - = 0 ? "text-accent" : "text-error"}`}> - {h.priceChange >= 0 ? "+" : ""}{h.priceChange.toFixed(1)}% - - )} -
-
Balance: {formatPrice(formatUnits(h.balance, 18))} tokens
-
- Price:{" "} - {formatPrice(formatUnits(h.price, 18))} {RESERVE_LABEL} - {plotUsd != null && ({formatUsdValue(Number(formatUnits(h.price, 18)) * plotUsd)})} -
- {h.entryPrice !== null && h.entryPrice > 0 && ( -
- Entry:{" "} - {formatPrice(h.entryPrice)} {RESERVE_LABEL} - {plotUsd != null && ({formatUsdValue(h.entryPrice * plotUsd)})} +
+
+ {/* Value */} +
+
+ {plotUsd ? formatUsdValue(Number(formatUnits(h.value, h.reserveDecimals)) * plotUsd) : `${formatPrice(formatUnits(h.value, h.reserveDecimals))} ${RESERVE_LABEL}`} + {h.priceChange !== null && ( + = 0 ? "text-accent" : "text-error"}`}> + {h.priceChange >= 0 ? "+" : ""}{h.priceChange.toFixed(1)}% + + )} +
+
Value
- )} - {h.lastTraded && ( -
Traded: {new Date(h.lastTraded).toLocaleDateString("en-US", { month: "short", day: "numeric" })}
- )} + {/* Balance */} +
+
{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
+
+
From 2ae30f4f1cf0276ff474ff3f9c1ede58c676fcd1 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Thu, 2 Apr 2026 14:45:46 +0100 Subject: [PATCH 2/2] [#756] Fix T2a review: USD-only Value fallback, firstTraded from any trade type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Value box now shows "—" instead of PLOT when USD price unavailable - First Traded derives from any trade type (not just mints), so readers who acquired via transfer are covered Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/profile/[address]/page.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/app/profile/[address]/page.tsx b/src/app/profile/[address]/page.tsx index cf646d03..fff5452d 100644 --- a/src/app/profile/[address]/page.tsx +++ b/src/app/profile/[address]/page.tsx @@ -1314,7 +1314,18 @@ function PortfolioTab({ address, isOwnProfile }: { address: string; isOwnProfile .limit(1); if (firstMint && firstMint.length > 0) { entryPrice = firstMint[0].price_per_token; - firstTraded = firstMint[0].block_timestamp; + } + // First trade of any type (mint or transfer-in) + const { data: firstTrade } = await supabase + .from("trade_history") + .select("block_timestamp") + .eq("user_address", address) + .eq("storyline_id", sl.storyline_id) + .eq("contract_address", MCV2_BOND.toLowerCase()) + .order("block_timestamp", { ascending: true }) + .limit(1); + if (firstTrade && firstTrade.length > 0) { + firstTraded = firstTrade[0].block_timestamp; } const { data: lastTrade } = await supabase .from("trade_history") @@ -1509,7 +1520,7 @@ function PortfolioTab({ address, isOwnProfile }: { address: string; isOwnProfile {/* Value */}
- {plotUsd ? formatUsdValue(Number(formatUnits(h.value, h.reserveDecimals)) * plotUsd) : `${formatPrice(formatUnits(h.value, h.reserveDecimals))} ${RESERVE_LABEL}`} + {plotUsd ? formatUsdValue(Number(formatUnits(h.value, h.reserveDecimals)) * plotUsd) : "—"} {h.priceChange !== null && ( = 0 ? "text-accent" : "text-error"}`}> {h.priceChange >= 0 ? "+" : ""}{h.priceChange.toFixed(1)}%