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:
185
components/ProductCard.tsx
Normal file
185
components/ProductCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user