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

215 lines
7.7 KiB
TypeScript
Raw Normal View History

"use client";
import Image from "next/image";
import { useApp } from "@/contexts/AppContext";
import { ProductDetail } from "@/lib/api/fundmarket";
interface OverviewItemProps {
icon: string;
label: string;
value: string;
}
function OverviewItem({ icon, label, value }: OverviewItemProps) {
return (
<div className="flex flex-col gap-1 md:flex-row md:items-center md:justify-between w-full">
<div className="flex items-center gap-1">
<div className="w-5 h-5 flex-shrink-0">
<Image src={icon} alt={label} width={20} height={20} />
</div>
<span className="text-xs font-medium leading-[150%] text-[#9ca1af] dark:text-gray-400">
{label}
</span>
</div>
<span className="text-sm font-semibold leading-[150%] text-[#111827] dark:text-white pl-6 md:pl-0">
{value}
</span>
</div>
);
}
interface AssetOverviewCardProps {
product: ProductDetail;
vaultInfo?: any[];
isVaultLoading?: boolean;
vaultTimedOut?: boolean;
}
export default function AssetOverviewCard({
product,
vaultInfo,
isVaultLoading,
vaultTimedOut,
}: AssetOverviewCardProps) {
const { t } = useApp();
const hasContract = !!product.contractAddress;
const vaultReady = !hasContract || vaultInfo !== undefined || vaultTimedOut;
const loading = hasContract && isVaultLoading && !vaultTimedOut;
// nextRedemptionTime: getVaultInfo[8]
const nextRedemptionTime = vaultInfo ? Number(vaultInfo[8]) : 0;
const maturityDisplay = (() => {
if (loading) return '...';
if (!nextRedemptionTime) return '--';
return new Date(nextRedemptionTime * 1000).toLocaleDateString('en-GB', {
day: '2-digit', month: 'short', year: 'numeric',
});
})();
// ytPrice: getVaultInfo[7], 30 decimals
const ytPriceRaw: bigint = vaultInfo ? (vaultInfo[7] as bigint) ?? 0n : 0n;
const currentPriceDisplay = (() => {
if (loading) return '...';
if (!ytPriceRaw || ytPriceRaw <= 0n) return '--';
const divisor = 10n ** 30n;
const intPart = ytPriceRaw / divisor;
const fracScaled = ((ytPriceRaw % divisor) * 1_000_000n) / divisor;
return `$${intPart}.${fracScaled.toString().padStart(6, '0')}`;
})();
// Pool Capacity %: totalSupply[4] / hardCap[5]
const totalSupplyRaw: bigint = vaultInfo ? (vaultInfo[4] as bigint) ?? 0n : 0n;
const hardCapRaw: bigint = vaultInfo ? (vaultInfo[5] as bigint) ?? 0n : 0n;
const livePoolCapPercent = (vaultReady && hardCapRaw > 0n)
? Math.min((Number(totalSupplyRaw) / Number(hardCapRaw)) * 100, 100)
: null;
const displayPoolCapPercent = livePoolCapPercent !== null
? livePoolCapPercent
: vaultReady ? product.poolCapacityPercent : null;
const poolCapDisplay = !vaultReady
? '...'
: `${(displayPoolCapPercent ?? 0).toFixed(4)}%`;
// Format USD values
const formatUSD = (value: number) => {
if (value >= 1000000) return `$${(value / 1000000).toFixed(1)}M`;
if (value >= 1000) return `$${(value / 1000).toFixed(1)}K`;
return `$${value.toFixed(2)}`;
};
// Risk badge
const getRiskColor = (riskLevel: number) => {
switch (riskLevel) {
case 1: return { bg: "#e1f8ec", border: "#b8ecd2", color: "#10b981" };
case 2: return { bg: "#fffbf5", border: "#ffedd5", color: "#ffb933" };
case 3: return { bg: "#fee2e2", border: "#fecaca", color: "#ef4444" };
default: return { bg: "#fffbf5", border: "#ffedd5", color: "#ffb933" };
}
};
const riskColors = getRiskColor(product.riskLevel);
const getRiskBars = () => {
const bars = [
{ height: 5, active: product.riskLevel >= 1 },
{ height: 7, active: product.riskLevel >= 2 },
{ height: 11, active: product.riskLevel >= 3 },
];
return bars.map((bar, i) => (
<div
key={i}
className="w-[3px] rounded-sm flex-shrink-0"
style={{
height: `${bar.height}px`,
backgroundColor: bar.active ? riskColors.color : '#d1d5db',
}}
/>
));
};
return (
<div
className="bg-white dark:bg-gray-800 border border-[#f3f4f6] dark:border-gray-700 flex flex-col rounded-2xl md:rounded-3xl p-4 md:p-8 gap-5 md:gap-6"
>
{/* Header */}
<div className="flex items-center justify-between w-full">
<h3 className="text-lg font-bold leading-[150%] text-[#111827] dark:text-white">
{t("assetOverview.title")}
</h3>
<div
className="rounded-full border flex items-center"
style={{
backgroundColor: riskColors.bg,
borderColor: riskColors.border,
padding: '6px 12px',
gap: '8px',
}}
>
<div
className="rounded-full flex-shrink-0"
style={{ backgroundColor: riskColors.color, width: '6px', height: '6px' }}
/>
<span className="text-xs font-semibold leading-4" style={{ color: riskColors.color }}>
{product.riskLabel}
</span>
<div className="flex items-end gap-[2px]">{getRiskBars()}</div>
</div>
</div>
{/* Overview Items - 2 per row */}
<div className="flex flex-col w-full" style={{ gap: '16px' }}>
<div className="grid grid-cols-2 gap-x-4 gap-y-5 w-full">
<OverviewItem
icon="/components/product/component-11.svg"
label={t("assetOverview.underlyingAssets")}
value={product.underlyingAssets}
/>
<OverviewItem
icon="/components/product/component-12.svg"
label={t("assetOverview.maturityRange")}
value={maturityDisplay}
/>
<OverviewItem
icon="/components/product/component-13.svg"
label={t("assetOverview.cap")}
value={formatUSD(product.poolCapUsd)}
/>
<OverviewItem
icon="/components/product/component-15.svg"
label={t("assetOverview.poolCapacity")}
value={poolCapDisplay}
/>
</div>
{/* Progress Bar - full width */}
<div
className="w-full bg-[#f3f4f6] dark:bg-gray-600 rounded-full overflow-hidden"
style={{ height: '10px' }}
>
<div
className="h-full rounded-full transition-all duration-500"
style={{
width: `${displayPoolCapPercent ?? 0}%`,
background: 'linear-gradient(90deg, #1447e6 0%, #032bbd 100%)',
}}
/>
</div>
</div>
{/* Divider */}
<div className="w-full border-t border-[#f3f4f6] dark:border-gray-700" style={{ height: '1px' }} />
{/* Current Price */}
<div
className="bg-[#f9fafb] dark:bg-gray-700 border border-[#f3f4f6] dark:border-gray-600 flex flex-col md:flex-row items-center justify-center md:justify-between gap-2 w-full"
style={{ borderRadius: '16px', padding: '16px' }}
>
<div className="flex items-center" style={{ gap: '12px' }}>
<div className="w-5 h-6 flex-shrink-0">
<Image src="/components/product/component-16.svg" alt="Price" width={20} height={24} />
</div>
<span
className="text-sm font-medium uppercase"
style={{ color: '#4b5563', letterSpacing: '0.7px', lineHeight: '20px' }}
>
{t("assetOverview.currentPrice")}
</span>
</div>
<div className="text-xl font-bold leading-[140%]">
<span className="text-[#111827] dark:text-white">1 {product.tokenSymbol} = </span>
<span style={{ color: "#10b981" }}>{currentPriceDisplay}</span>
</div>
</div>
</div>
);
}