Created a complete Product page based on prototype design with: Page Features: - New /product route with full page layout - AssetX Fund Market title section - 4 statistics cards (TVL, Cumulative Yield, Balance, Earnings) - Assets listing section with view toggle (list/grid) - Product cards grid layout ProductCard Component: - Reusable product card component with HeroUI integration - Product header with icon and category badge - Two-column metric display (Yield APY, Pool Cap, Maturity, etc.) - Risk indicator with color-coded bars (Low/Medium/High) - Pool capacity progress bar with gradient - Invest button with HeroUI Button component - Hover effects and transitions - Color-coded category badges (Quant Strategy, Real Estate) Assets: - Copied view toggle icons from prototype - edit-list-unordered0.svg (list view) - menu-more-grid-small0.svg (grid view) Internationalization: - Added productPage section to en.json and zh.json - All labels translated (English and Chinese) - Consistent with existing i18n pattern Technical Implementation: - Full responsive design - Dark mode support - Gradient styling for visual appeal - Smooth animations and transitions - Proper TypeScript types - Follows existing design system Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
186 lines
6.5 KiB
TypeScript
186 lines
6.5 KiB
TypeScript
"use client";
|
||
|
||
import Image from "next/image";
|
||
import { Button } from "@heroui/react";
|
||
|
||
interface ProductCardProps {
|
||
name: string;
|
||
category: string;
|
||
icon?: string;
|
||
yieldAPY: string;
|
||
poolCap: string;
|
||
maturity: string;
|
||
risk: string;
|
||
riskLevel: 1 | 2 | 3;
|
||
lockUp: string;
|
||
circulatingSupply: string;
|
||
poolCapacityPercent: number;
|
||
onInvest?: () => void;
|
||
}
|
||
|
||
export default function ProductCard({
|
||
name,
|
||
category,
|
||
icon,
|
||
yieldAPY,
|
||
poolCap,
|
||
maturity,
|
||
risk,
|
||
riskLevel,
|
||
lockUp,
|
||
circulatingSupply,
|
||
poolCapacityPercent,
|
||
onInvest,
|
||
}: ProductCardProps) {
|
||
const getRiskColor = (level: number) => {
|
||
switch (level) {
|
||
case 1:
|
||
return "bg-green-500";
|
||
case 2:
|
||
return "bg-yellow-500";
|
||
case 3:
|
||
return "bg-red-500";
|
||
default:
|
||
return "bg-gray-300";
|
||
}
|
||
};
|
||
|
||
const getCategoryColor = (cat: string) => {
|
||
if (cat.includes("Quant")) {
|
||
return "bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800 text-blue-700 dark:text-blue-300";
|
||
} else if (cat.includes("Real Estate")) {
|
||
return "bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800 text-green-700 dark:text-green-300";
|
||
}
|
||
return "bg-bg-subtle dark:bg-gray-700 border-border-normal dark:border-gray-600 text-text-tertiary dark:text-gray-400";
|
||
};
|
||
|
||
return (
|
||
<div className="bg-bg-surface dark:bg-gray-800 rounded-2xl border border-border-gray dark:border-gray-700 p-6 flex flex-col gap-6 hover:shadow-lg transition-shadow">
|
||
{/* Product Header */}
|
||
<div className="flex items-start gap-4">
|
||
<div className="w-16 h-16 rounded-full bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center flex-shrink-0">
|
||
{icon ? (
|
||
<Image src={icon} alt={name} width={32} height={32} />
|
||
) : (
|
||
<span className="text-2xl">🏛️</span>
|
||
)}
|
||
</div>
|
||
<div className="flex flex-col gap-2 flex-1">
|
||
<h3 className="text-body-large font-bold text-text-primary dark:text-white leading-tight">
|
||
{name}
|
||
</h3>
|
||
<div
|
||
className={`inline-flex self-start px-3 py-1 rounded-full border text-caption-tiny font-medium ${getCategoryColor(
|
||
category
|
||
)}`}
|
||
>
|
||
{category}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Product Metrics - Two Columns */}
|
||
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
|
||
{/* Column 1 */}
|
||
<div className="flex flex-col gap-4">
|
||
<div className="flex flex-col gap-1">
|
||
<span className="text-body-small text-text-tertiary dark:text-gray-400">
|
||
Yield APY
|
||
</span>
|
||
<span className="text-body-large font-bold text-green-600 dark:text-green-400">
|
||
{yieldAPY}
|
||
</span>
|
||
</div>
|
||
<div className="flex flex-col gap-1">
|
||
<span className="text-body-small text-text-tertiary dark:text-gray-400">
|
||
Maturity
|
||
</span>
|
||
<span className="text-body-default font-medium text-text-primary dark:text-white">
|
||
{maturity}
|
||
</span>
|
||
</div>
|
||
<div className="flex flex-col gap-1">
|
||
<span className="text-body-small text-text-tertiary dark:text-gray-400">
|
||
Lock-Up
|
||
</span>
|
||
<span className="text-body-default font-medium text-text-primary dark:text-white">
|
||
{lockUp}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Column 2 */}
|
||
<div className="flex flex-col gap-4">
|
||
<div className="flex flex-col gap-1">
|
||
<span className="text-body-small text-text-tertiary dark:text-gray-400">
|
||
Pool CaP
|
||
</span>
|
||
<span className="text-body-large font-bold text-text-primary dark:text-white">
|
||
{poolCap}
|
||
</span>
|
||
</div>
|
||
<div className="flex flex-col gap-1">
|
||
<span className="text-body-small text-text-tertiary dark:text-gray-400">
|
||
Risk
|
||
</span>
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-body-default font-medium text-text-primary dark:text-white">
|
||
{risk}
|
||
</span>
|
||
<div className="flex items-center gap-1">
|
||
{[1, 2, 3].map((level) => (
|
||
<div
|
||
key={level}
|
||
className={`w-1.5 h-4 rounded-sm transition-colors ${
|
||
level <= riskLevel
|
||
? getRiskColor(riskLevel)
|
||
: "bg-gray-300 dark:bg-gray-600"
|
||
}`}
|
||
/>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex flex-col gap-1">
|
||
<span className="text-body-small text-text-tertiary dark:text-gray-400">
|
||
Circulating supply
|
||
</span>
|
||
<span className="text-body-default font-medium text-text-primary dark:text-white">
|
||
{circulatingSupply}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Divider */}
|
||
<div className="border-t border-border-gray dark:border-gray-700" />
|
||
|
||
{/* Pool Capacity & Invest Button */}
|
||
<div className="flex items-end justify-between gap-6">
|
||
<div className="flex-1 flex flex-col gap-2">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-body-small text-text-tertiary dark:text-gray-400">
|
||
Pool Capacity
|
||
</span>
|
||
<span className="text-body-small font-bold text-text-primary dark:text-white">
|
||
{poolCapacityPercent}% Filled
|
||
</span>
|
||
</div>
|
||
<div className="w-full h-2.5 bg-bg-subtle dark:bg-gray-700 rounded-full overflow-hidden">
|
||
<div
|
||
className="h-full bg-gradient-to-r from-blue-500 to-purple-600 rounded-full transition-all duration-300"
|
||
style={{ width: `${poolCapacityPercent}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<Button
|
||
onPress={onInvest}
|
||
className="px-8 py-3 bg-text-primary dark:bg-blue-600 text-white font-bold text-body-default rounded-xl h-auto min-w-[120px]"
|
||
>
|
||
Invest
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|