215 lines
7.7 KiB
TypeScript
215 lines
7.7 KiB
TypeScript
|
|
"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>
|
||
|
|
);
|
||
|
|
}
|