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:
159
app/product/page.tsx
Normal file
159
app/product/page.tsx
Normal 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
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>
|
||||
);
|
||||
}
|
||||
@@ -129,5 +129,27 @@
|
||||
"primeBroker": "Prime Broker",
|
||||
"usEquityPortfolio": "US Equity Portfolio",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,5 +129,27 @@
|
||||
"primeBroker": "主经纪商",
|
||||
"usEquityPortfolio": "美国股票投资组合",
|
||||
"days": "天"
|
||||
},
|
||||
"productPage": {
|
||||
"title": "AssetX 基金市场",
|
||||
"assets": "资产",
|
||||
"totalValueLocked": "总锁仓量",
|
||||
"cumulativeYield": "累计收益",
|
||||
"yourTotalBalance": "您的总余额",
|
||||
"yourTotalEarning": "您的总收益",
|
||||
"yieldAPY": "收益率 APY",
|
||||
"poolCap": "池容量",
|
||||
"maturity": "到期日",
|
||||
"risk": "风险",
|
||||
"lockUp": "锁仓期",
|
||||
"circulatingSupply": "流通量",
|
||||
"poolCapacity": "池容量",
|
||||
"filled": "已填充",
|
||||
"invest": "投资",
|
||||
"quantStrategy": "量化策略",
|
||||
"realEstate": "房地产",
|
||||
"low": "低",
|
||||
"medium": "中",
|
||||
"high": "高"
|
||||
}
|
||||
}
|
||||
|
||||
3
public/edit-list-unordered0.svg
Normal file
3
public/edit-list-unordered0.svg
Normal 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 |
10
public/menu-more-grid-small0.svg
Normal file
10
public/menu-more-grid-small0.svg
Normal 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 |
Reference in New Issue
Block a user