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

159
app/product/page.tsx Normal file
View File

@@ -0,0 +1,159 @@
"use client";
import { useApp } from "@/contexts/AppContext";
import Sidebar from "@/components/Sidebar";
import TopBar from "@/components/TopBar";
import StatsCards from "@/components/StatsCards";
import ProductCard from "@/components/ProductCard";
import Image from "next/image";
import { Button } from "@heroui/react";
export default function ProductPage() {
const { t } = useApp();
const statsData = [
{
label: "Total Value Locked",
value: "$465.0M",
change: "+2.4%",
isPositive: true,
},
{
label: "Cumulative Yield",
value: "$505,232",
change: "+2.4%",
isPositive: true,
},
{
label: "Your Total Balance",
value: "$10,000",
change: "+2.4%",
isPositive: true,
},
{
label: "Your Total Earning",
value: "--",
change: "+2.4%",
isPositive: true,
},
];
const products = [
{
id: 1,
name: "High-Yield US Equity",
category: "Quant Strategy",
icon: "/product-us-equity.svg",
yieldAPY: "22.0%",
poolCap: "10M",
maturity: "05 Feb 2026",
risk: "Medium",
riskLevel: 2,
lockUp: "12 Months",
circulatingSupply: "$2.5M",
poolCapacityPercent: 75,
},
{
id: 2,
name: "HK Commercial RE",
category: "Real Estate",
icon: "/product-hk-re.svg",
yieldAPY: "22.0%",
poolCap: "10M",
maturity: "05 Feb 2026",
risk: "LOW",
riskLevel: 1,
lockUp: "12 Months",
circulatingSupply: "$2.5M",
poolCapacityPercent: 75,
},
{
id: 3,
name: "High-Yield US Equity",
category: "Quant Strategy",
icon: "/product-us-equity-2.svg",
yieldAPY: "22.0%",
poolCap: "10M",
maturity: "05 Feb 2026",
risk: "High",
riskLevel: 3,
lockUp: "12 Months",
circulatingSupply: "$2.5M",
poolCapacityPercent: 75,
},
];
return (
<div className="min-h-screen bg-bg-subtle dark:bg-gray-900 flex">
<Sidebar />
<div className="flex-1 flex flex-col ml-[222px]">
<div className="bg-bg-surface dark:bg-gray-800 border-b border-border-normal dark:border-gray-700 px-8 py-6">
<TopBar />
</div>
<div className="flex-1 px-8 py-8">
{/* Page Title */}
<div className="mb-8">
<h1 className="text-heading-h2 font-bold text-text-primary dark:text-white">
AssetX Fund Market
</h1>
</div>
{/* Stats Cards */}
<div className="mb-8">
<StatsCards stats={statsData} />
</div>
{/* Assets Section */}
<div className="flex flex-col gap-6">
{/* Section Header */}
<div className="flex items-center justify-between">
<h2 className="text-heading-h3 font-bold text-text-primary dark:text-white">
Assets
</h2>
<div className="flex items-center gap-2">
<button className="p-2 rounded-lg hover:bg-bg-subtle dark:hover:bg-gray-700 transition-colors">
<Image
src="/edit-list-unordered0.svg"
alt="List view"
width={20}
height={20}
/>
</button>
<button className="p-2 rounded-lg hover:bg-bg-subtle dark:hover:bg-gray-700 transition-colors">
<Image
src="/menu-more-grid-small0.svg"
alt="Grid view"
width={20}
height={20}
/>
</button>
</div>
</div>
{/* Product Cards Grid */}
<div className="grid grid-cols-1 gap-6">
{products.map((product) => (
<ProductCard
key={product.id}
name={product.name}
category={product.category}
icon={product.icon}
yieldAPY={product.yieldAPY}
poolCap={product.poolCap}
maturity={product.maturity}
risk={product.risk}
riskLevel={product.riskLevel}
lockUp={product.lockUp}
circulatingSupply={product.circulatingSupply}
poolCapacityPercent={product.poolCapacityPercent}
onInvest={() => console.log("Invest in", product.name)}
/>
))}
</div>
</div>
</div>
</div>
</div>
);
}

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>
);
}

View File

@@ -129,5 +129,27 @@
"primeBroker": "Prime Broker", "primeBroker": "Prime Broker",
"usEquityPortfolio": "US Equity Portfolio", "usEquityPortfolio": "US Equity Portfolio",
"days": "days" "days": "days"
},
"productPage": {
"title": "AssetX Fund Market",
"assets": "Assets",
"totalValueLocked": "Total Value Locked",
"cumulativeYield": "Cumulative Yield",
"yourTotalBalance": "Your Total Balance",
"yourTotalEarning": "Your Total Earning",
"yieldAPY": "Yield APY",
"poolCap": "Pool CaP",
"maturity": "Maturity",
"risk": "Risk",
"lockUp": "Lock-Up",
"circulatingSupply": "Circulating supply",
"poolCapacity": "Pool Capacity",
"filled": "Filled",
"invest": "Invest",
"quantStrategy": "Quant Strategy",
"realEstate": "Real Estate",
"low": "LOW",
"medium": "Medium",
"high": "High"
} }
} }

View File

@@ -129,5 +129,27 @@
"primeBroker": "主经纪商", "primeBroker": "主经纪商",
"usEquityPortfolio": "美国股票投资组合", "usEquityPortfolio": "美国股票投资组合",
"days": "天" "days": "天"
},
"productPage": {
"title": "AssetX 基金市场",
"assets": "资产",
"totalValueLocked": "总锁仓量",
"cumulativeYield": "累计收益",
"yourTotalBalance": "您的总余额",
"yourTotalEarning": "您的总收益",
"yieldAPY": "收益率 APY",
"poolCap": "池容量",
"maturity": "到期日",
"risk": "风险",
"lockUp": "锁仓期",
"circulatingSupply": "流通量",
"poolCapacity": "池容量",
"filled": "已填充",
"invest": "投资",
"quantStrategy": "量化策略",
"realEstate": "房地产",
"low": "低",
"medium": "中",
"high": "高"
} }
} }

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 17H19M9 12H19M9 7H19M5.00195 17V17.002L5 17.002V17H5.00195ZM5.00195 12V12.002L5 12.002V12H5.00195ZM5.00195 7V7.002L5 7.00195V7H5.00195Z" stroke="#111827" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 335 B

View File

@@ -0,0 +1,10 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.5 15.75C14.5 16.4404 15.0596 17 15.75 17C16.4404 17 17 16.4404 17 15.75C17 15.0596 16.4404 14.5 15.75 14.5C15.0596 14.5 14.5 15.0596 14.5 15.75Z" fill="#4B5563"/>
<path d="M7 15.75C7 16.4404 7.55964 17 8.25 17C8.94036 17 9.5 16.4404 9.5 15.75C9.5 15.0596 8.94036 14.5 8.25 14.5C7.55964 14.5 7 15.0596 7 15.75Z" fill="#4B5563"/>
<path d="M14.5 8.25C14.5 8.94036 15.0596 9.5 15.75 9.5C16.4404 9.5 17 8.94036 17 8.25C17 7.55964 16.4404 7 15.75 7C15.0596 7 14.5 7.55964 14.5 8.25Z" fill="#4B5563"/>
<path d="M7 8.25C7 8.94036 7.55964 9.5 8.25 9.5C8.94036 9.5 9.5 8.94036 9.5 8.25C9.5 7.55964 8.94036 7 8.25 7C7.55964 7 7 7.55964 7 8.25Z" fill="#4B5563"/>
<path d="M14.5 15.75C14.5 16.4404 15.0596 17 15.75 17C16.4404 17 17 16.4404 17 15.75C17 15.0596 16.4404 14.5 15.75 14.5C15.0596 14.5 14.5 15.0596 14.5 15.75Z" stroke="#4B5563" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 15.75C7 16.4404 7.55964 17 8.25 17C8.94036 17 9.5 16.4404 9.5 15.75C9.5 15.0596 8.94036 14.5 8.25 14.5C7.55964 14.5 7 15.0596 7 15.75Z" stroke="#4B5563" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14.5 8.25C14.5 8.94036 15.0596 9.5 15.75 9.5C16.4404 9.5 17 8.94036 17 8.25C17 7.55964 16.4404 7 15.75 7C15.0596 7 14.5 7.55964 14.5 8.25Z" stroke="#4B5563" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 8.25C7 8.94036 7.55964 9.5 8.25 9.5C8.94036 9.5 9.5 8.94036 9.5 8.25C9.5 7.55964 8.94036 7 8.25 7C7.55964 7 7 7.55964 7 8.25Z" stroke="#4B5563" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB