feat: implement Product page with fund listing

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>
This commit is contained in:
2026-01-30 04:11:58 +00:00
parent 16aa079cba
commit f3b0c0db6e
6 changed files with 667 additions and 266 deletions

185
components/ProductCard.tsx Normal file
View File

@@ -0,0 +1,185 @@
"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>
);
}