Files
assetx/webapp/components/product/OverviewTab.tsx

155 lines
6.5 KiB
TypeScript
Raw Normal View History

"use client";
import { useState, useEffect, useMemo } from 'react';
import { useAccount, useReadContract } from 'wagmi';
import { bscTestnet } from 'wagmi/chains';
import { useQuery } from '@tanstack/react-query';
import { useTokenBalance } from '@/hooks/useBalance';
import { useTokenBySymbol } from '@/hooks/useTokenBySymbol';
import { useApp } from '@/contexts/AppContext';
import { fetchContracts } from '@/lib/api/contracts';
import { abis } from '@/lib/contracts';
import ProductHeader from "./ProductHeader";
import StatsCards from "@/components/fundmarket/StatsCards";
import AssetOverviewCard from "./AssetOverviewCard";
import APYHistoryCard from "./APYHistoryCard";
import AssetDescriptionCard from "./AssetDescriptionCard";
import MintSwapPanel from "./MintSwapPanel";
import ProtocolInformation from "./ProtocolInformation";
import PerformanceAnalysis from "./PerformanceAnalysis";
import Season1Rewards from "./Season1Rewards";
import AssetCustodyVerification from "./AssetCustodyVerification";
import { ProductDetail } from "@/lib/api/fundmarket";
interface OverviewTabProps {
product: ProductDetail;
}
const formatUSD = (v: number) =>
"$" + v.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
export default function OverviewTab({ product }: OverviewTabProps) {
const { t } = useApp();
const { isConnected, chainId } = useAccount();
const ytToken = useTokenBySymbol(product.tokenSymbol);
const { formattedBalance: ytBalance } = useTokenBalance(ytToken?.contractAddress, ytToken?.decimals ?? 18);
// Shared getVaultInfo read — single source of truth for all children
const { data: contractConfigs = [] } = useQuery({
queryKey: ['contract-registry'],
queryFn: fetchContracts,
staleTime: 5 * 60 * 1000,
});
const factoryAddress = useMemo(() => {
const c = contractConfigs.find(c => c.name === 'YTAssetFactory' && c.chain_id === product.chainId);
return c?.address as `0x${string}` | undefined;
}, [contractConfigs, product.chainId]);
const hasContract = !!factoryAddress && !!product.contractAddress;
const { data: vaultInfo, isLoading: isVaultLoading, refetch: refetchVault } = useReadContract({
address: factoryAddress,
abi: abis.YTAssetFactory as any,
functionName: 'getVaultInfo',
args: product.contractAddress ? [product.contractAddress as `0x${string}`] : undefined,
chainId: product.chainId,
query: { enabled: hasContract },
});
// 3s timeout — after this, fall back to DB snapshot values
const [vaultTimedOut, setVaultTimedOut] = useState(false);
useEffect(() => {
if (!hasContract) return;
setVaultTimedOut(false);
const t = setTimeout(() => setVaultTimedOut(true), 3000);
return () => clearTimeout(t);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasContract]);
const vaultReady = !hasContract || vaultInfo !== undefined || vaultTimedOut;
// Real-time TVL: getVaultInfo[1] = totalAssets (USDC)
const usdcDecimals = product.chainId === 421614 ? 6 : 18;
const totalAssetsRaw: bigint = vaultInfo ? ((vaultInfo as any[])[1] as bigint) ?? 0n : 0n;
const liveTVL = totalAssetsRaw > 0n ? Number(totalAssetsRaw) / Math.pow(10, usdcDecimals) : null;
const tvlDisplay = !vaultReady
? "..."
: liveTVL !== null
? formatUSD(liveTVL)
: formatUSD(product.tvlUsd);
const balanceUSD = parseFloat(ytBalance) * (product.currentPrice || 1);
const balanceDisplay = !isConnected
? "--"
: chainId !== bscTestnet.id
? "-- (Switch to BSC)"
: formatUSD(balanceUSD);
const volume24hDisplay = product.volume24hUsd > 0 ? formatUSD(product.volume24hUsd) : "--";
const volChange = product.volumeChangeVsAvg ?? 0;
const volChangeStr = volChange !== 0
? `${volChange > 0 ? "↑" : "↓"} ${Math.abs(volChange).toFixed(0)}% vs Avg`
: "";
const stats = [
{ label: t("productPage.totalValueLocked"), value: tvlDisplay, change: "", isPositive: true },
{ label: t("productPage.volume24h"), value: volume24hDisplay, change: volChangeStr, isPositive: volChange >= 0 },
{ label: t("productPage.cumulativeYield"), value: formatUSD(0), change: "", isPositive: true },
{ label: t("productPage.yourTotalBalance"), value: balanceDisplay, change: "", isPositive: true },
{ label: t("productPage.yourTotalEarning"), value: "--", change: "", isPositive: true },
];
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 md:gap-8 w-full">
{/* ① Header + Stats — 始终在最顶部桌面端跨3列 */}
<div className="order-1 md:col-span-3 min-w-0">
<div className="bg-white dark:bg-gray-800 border border-[#f3f4f6] dark:border-gray-700 flex flex-col overflow-hidden rounded-2xl md:rounded-3xl p-4 md:p-8 gap-5 md:gap-6">
<ProductHeader product={product} />
<StatsCards stats={stats} />
</div>
</div>
{/* ② 交易板 + Protocol — 移动端排第2桌面端在右列 */}
<div className="order-2 md:order-3 md:col-span-1 min-w-0 flex flex-col gap-6 md:gap-8">
<MintSwapPanel
tokenType={product.tokenSymbol}
decimals={product.decimals}
onVaultRefresh={refetchVault}
/>
<ProtocolInformation product={product} />
</div>
{/* ③ Asset Overview + APY + Description — 移动端排第3桌面端在左列 */}
<div className="order-3 md:order-2 md:col-span-2 min-w-0 flex flex-col gap-6 md:gap-8">
<AssetOverviewCard
product={product}
vaultInfo={vaultInfo as any[] | undefined}
isVaultLoading={isVaultLoading}
vaultTimedOut={vaultTimedOut}
/>
<APYHistoryCard productId={product.id} />
<div id="asset-description">
<AssetDescriptionCard product={product} />
</div>
</div>
{/* ④ Season 1 Rewards — 全宽 */}
<div className="order-4 md:col-span-3 min-w-0">
<Season1Rewards />
</div>
{/* ⑤ Performance Analysis (Daily Net Returns) — 全宽 */}
<div id="performance-analysis" className="order-5 md:col-span-3 min-w-0">
<PerformanceAnalysis productId={product.id} />
</div>
{/* ⑥ Asset Custody & Verification — 全宽 */}
<div id="asset-custody" className="order-6 md:col-span-3 min-w-0">
<AssetCustodyVerification product={product} />
</div>
</div>
);
}