包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、 antdesign(管理后台)、landingpage(营销落地页)、 数据库 SQL 和配置文件。
94 lines
3.9 KiB
TypeScript
94 lines
3.9 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import Image from "next/image";
|
|
import { useApp } from "@/contexts/AppContext";
|
|
import { useLTV, useBorrowBalance } from "@/hooks/useCollateral";
|
|
import { useSupplyAPY } from "@/hooks/useHealthFactor";
|
|
|
|
export default function RepayStats() {
|
|
const { t } = useApp();
|
|
const [mounted, setMounted] = useState(false);
|
|
|
|
const { ltvRaw } = useLTV();
|
|
const { formattedBalance: borrowedBalance } = useBorrowBalance();
|
|
const { apr: supplyApr } = useSupplyAPY();
|
|
|
|
useEffect(() => { setMounted(true); }, []);
|
|
|
|
const LIQUIDATION_LTV = 75;
|
|
const hasPosition = mounted && parseFloat(borrowedBalance) > 0;
|
|
const healthPct = hasPosition ? Math.max(0, Math.min(100, (1 - ltvRaw / LIQUIDATION_LTV) * 100)) : 0;
|
|
|
|
const getHealthLabel = () => {
|
|
if (!hasPosition) return 'No Position';
|
|
if (ltvRaw < 50) return `Safe ${healthPct.toFixed(0)}%`;
|
|
if (ltvRaw < 65) return `Warning ${healthPct.toFixed(0)}%`;
|
|
return `At Risk ${healthPct.toFixed(0)}%`;
|
|
};
|
|
|
|
const getHealthColor = () => {
|
|
if (!hasPosition) return 'text-text-tertiary dark:text-gray-400';
|
|
if (ltvRaw < 50) return 'text-[#10b981] dark:text-green-400';
|
|
if (ltvRaw < 65) return 'text-[#ff6900] dark:text-orange-400';
|
|
return 'text-[#ef4444] dark:text-red-400';
|
|
};
|
|
|
|
const getBarGradient = () => {
|
|
if (!hasPosition) return undefined;
|
|
if (ltvRaw < 50) return 'linear-gradient(90deg, rgba(0, 213, 190, 1) 0%, rgba(0, 188, 125, 1) 100%)';
|
|
if (ltvRaw < 65) return 'linear-gradient(90deg, #ff6900 0%, #ff9900 100%)';
|
|
return 'linear-gradient(90deg, #ef4444 0%, #dc2626 100%)';
|
|
};
|
|
|
|
// Net APR = getSupplyRate() directly (already represents net yield)
|
|
const netApr = supplyApr > 0 ? supplyApr.toFixed(4) : null;
|
|
|
|
return (
|
|
<div className="bg-bg-surface dark:bg-gray-800 rounded-3xl border border-border-gray dark:border-gray-700 p-4 md:p-8 flex flex-col gap-8 flex-1 shadow-md">
|
|
{/* Stats Info */}
|
|
<div className="flex flex-col gap-6">
|
|
{/* NET APR */}
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
<Image src="/components/repay/icon0.svg" alt="" width={18} height={18} className="flex-shrink-0" />
|
|
<span className="text-body-small font-regular text-text-tertiary dark:text-gray-400 leading-[150%]">
|
|
{t("repay.netApr")}
|
|
</span>
|
|
</div>
|
|
<span className={`text-body-small font-bold leading-[150%] whitespace-nowrap flex-shrink-0 ${netApr !== null && parseFloat(netApr) >= 0 ? 'text-[#10b981] dark:text-green-400' : 'text-[#ef4444]'}`}>
|
|
{netApr !== null ? `${parseFloat(netApr) >= 0 ? '+' : ''}${netApr}%` : '--'}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Position Health */}
|
|
<div className="flex items-center justify-between gap-2">
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
<Image src="/components/repay/icon2.svg" alt="" width={18} height={18} className="flex-shrink-0" />
|
|
<span className="text-body-small font-regular text-text-tertiary dark:text-gray-400 leading-[150%]">
|
|
{t("repay.positionHealth")}
|
|
</span>
|
|
</div>
|
|
<span className={`text-body-small font-bold leading-[150%] whitespace-nowrap flex-shrink-0 ${getHealthColor()}`}>
|
|
{!mounted ? '--' : getHealthLabel()}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Progress Bar */}
|
|
<div className="w-full">
|
|
<div className="w-full h-2 bg-[#f3f4f6] dark:bg-gray-700 rounded-full overflow-hidden">
|
|
<div
|
|
className="h-2 rounded-full transition-all duration-500"
|
|
style={{
|
|
width: `${hasPosition ? healthPct : 0}%`,
|
|
background: getBarGradient() || '#e5e7eb',
|
|
boxShadow: hasPosition && ltvRaw < 50 ? "0px 0px 15px 0px rgba(16, 185, 129, 0.3)" : undefined,
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|