initial commit
This commit is contained in:
263
components/fundmarket/ProductCard.tsx
Normal file
263
components/fundmarket/ProductCard.tsx
Normal file
@@ -0,0 +1,263 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { Button, Card, CardHeader, CardBody, CardFooter } from "@heroui/react";
|
||||
import { buttonStyles } from "@/lib/buttonStyles";
|
||||
interface ProductCardProps {
|
||||
name: string;
|
||||
category: string;
|
||||
categoryColor: "orange" | "green" | "blue" | "purple" | "red";
|
||||
iconType: "us-flag-1" | "hk-flag" | "us-flag-2" | "sg-flag" | "uk-flag";
|
||||
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,
|
||||
categoryColor,
|
||||
iconType,
|
||||
yieldAPY,
|
||||
poolCap,
|
||||
maturity,
|
||||
risk,
|
||||
riskLevel,
|
||||
lockUp,
|
||||
circulatingSupply,
|
||||
poolCapacityPercent,
|
||||
onInvest,
|
||||
}: ProductCardProps) {
|
||||
const getCategoryColors = () => {
|
||||
switch (categoryColor) {
|
||||
case "orange":
|
||||
return {
|
||||
bg: "bg-orange-50",
|
||||
text: "text-orange-600",
|
||||
gradient: "from-orange-500/5 via-orange-300/3 to-white/0",
|
||||
};
|
||||
case "green":
|
||||
return {
|
||||
bg: "bg-green-50",
|
||||
text: "text-green-600",
|
||||
gradient: "from-green-500/5 via-green-300/3 to-white/0",
|
||||
};
|
||||
case "blue":
|
||||
return {
|
||||
bg: "bg-blue-50",
|
||||
text: "text-blue-600",
|
||||
gradient: "from-blue-500/5 via-blue-300/3 to-white/0",
|
||||
};
|
||||
case "purple":
|
||||
return {
|
||||
bg: "bg-purple-50",
|
||||
text: "text-purple-600",
|
||||
gradient: "from-purple-500/5 via-purple-300/3 to-white/0",
|
||||
};
|
||||
case "red":
|
||||
return {
|
||||
bg: "bg-red-50",
|
||||
text: "text-red-600",
|
||||
gradient: "from-red-500/5 via-red-300/3 to-white/0",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
bg: "bg-orange-50",
|
||||
text: "text-orange-600",
|
||||
gradient: "from-orange-500/5 via-orange-300/3 to-white/0",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getRiskBars = () => {
|
||||
const bars = [
|
||||
{ height: "h-[5px]", active: riskLevel >= 1 },
|
||||
{ height: "h-[7px]", active: riskLevel >= 2 },
|
||||
{ height: "h-[11px]", active: riskLevel >= 3 },
|
||||
];
|
||||
|
||||
const activeColor =
|
||||
riskLevel === 1
|
||||
? "bg-green-500"
|
||||
: riskLevel === 2
|
||||
? "bg-amber-400"
|
||||
: "bg-red-500";
|
||||
|
||||
return bars.map((bar, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`${bar.height} w-[3px] rounded-sm ${
|
||||
bar.active ? activeColor : "bg-gray-400"
|
||||
}`}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
const getIconSrc = () => {
|
||||
switch (iconType) {
|
||||
case "us-flag-1":
|
||||
return "/frame-9230.svg";
|
||||
case "hk-flag":
|
||||
return "/hk0.svg";
|
||||
case "us-flag-2":
|
||||
return "/frame-9231.svg";
|
||||
case "sg-flag":
|
||||
return "/frame-9230.svg"; // TODO: Add Singapore flag
|
||||
case "uk-flag":
|
||||
return "/frame-9230.svg"; // TODO: Add UK flag
|
||||
default:
|
||||
return "/frame-9230.svg";
|
||||
}
|
||||
};
|
||||
|
||||
const colors = getCategoryColors();
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={`bg-bg-subtle dark:bg-gray-800 border border-border-gray dark:border-gray-700 shadow-lg h-full relative overflow-hidden bg-gradient-to-br ${colors.gradient}`}
|
||||
>
|
||||
{/* Product Header */}
|
||||
<CardHeader className="pb-6 px-6 pt-6">
|
||||
<div className="flex gap-4 items-center">
|
||||
{/* Icon Container */}
|
||||
<div className="bg-white/40 dark:bg-gray-700/40 rounded-2xl border border-white/80 dark:border-gray-600/80 w-16 h-16 flex items-center justify-center shadow-[0_0_0_4px_rgba(255,255,255,0.1)] flex-shrink-0">
|
||||
<div className="w-12 h-12 relative rounded-3xl border-[0.5px] border-gray-100 dark:border-gray-600 overflow-hidden flex items-center justify-center">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-full w-12 h-12 absolute left-0 top-0" />
|
||||
<div className="relative z-10 flex items-center justify-center w-12 h-12">
|
||||
<Image
|
||||
src={getIconSrc()}
|
||||
alt={name}
|
||||
width={48}
|
||||
height={48}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title and Category */}
|
||||
<div className="flex flex-col gap-0">
|
||||
<h3 className="text-text-primary dark:text-white text-body-large font-bold font-inter">
|
||||
{name}
|
||||
</h3>
|
||||
<div
|
||||
className={`${colors.bg} dark:${colors.bg}/50 rounded-full px-2 py-0.5 inline-flex self-start`}
|
||||
>
|
||||
<span
|
||||
className={`${colors.text} dark:${colors.text} text-caption-tiny font-medium font-inter`}
|
||||
>
|
||||
{category}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardBody className="py-0 px-6">
|
||||
{/* Yield APY & Pool Cap */}
|
||||
<div className="border-b border-gray-50 dark:border-gray-700 pb-6 flex">
|
||||
<div className="flex-1 flex flex-col">
|
||||
<span className="text-gray-600 dark:text-gray-400 text-caption-tiny font-bold font-inter">
|
||||
Yield APY
|
||||
</span>
|
||||
<span className="text-text-primary dark:text-white text-heading-h3 font-extrabold font-jetbrains">
|
||||
{yieldAPY}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col items-end">
|
||||
<span className="text-gray-600 dark:text-gray-400 text-caption-tiny font-bold font-inter text-right">
|
||||
Pool Cap
|
||||
</span>
|
||||
<span className="text-green-500 dark:text-green-400 text-heading-h3 font-extrabold font-jetbrains text-right">
|
||||
{poolCap}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Details Section */}
|
||||
<div className="pt-6 flex flex-col gap-4">
|
||||
{/* Maturity & Risk */}
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1 flex flex-col">
|
||||
<span className="text-text-tertiary dark:text-gray-400 text-[10px] font-medium leading-[150%] tracking-[0.01em] font-inter">
|
||||
Maturity
|
||||
</span>
|
||||
<span className="text-gray-600 dark:text-gray-300 text-body-small font-bold font-inter">
|
||||
{maturity}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col items-end">
|
||||
<span className="text-text-tertiary dark:text-gray-400 text-[10px] font-medium leading-[150%] tracking-[0.01em] font-inter text-right">
|
||||
Risk
|
||||
</span>
|
||||
<div className="flex gap-1 items-center">
|
||||
<span className="text-gray-600 dark:text-gray-300 text-body-small font-bold font-inter">
|
||||
{risk}
|
||||
</span>
|
||||
<div className="flex gap-0.5 items-end">{getRiskBars()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lock-Up & Circulating Supply */}
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-1 flex flex-col">
|
||||
<span className="text-text-tertiary dark:text-gray-400 text-[10px] font-medium leading-[150%] tracking-[0.01em] font-inter">
|
||||
Lock-Up
|
||||
</span>
|
||||
<span className="text-gray-600 dark:text-gray-300 text-body-small font-bold font-inter">
|
||||
{lockUp}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col items-end">
|
||||
<span className="text-text-tertiary dark:text-gray-400 text-[10px] font-medium leading-[150%] tracking-[0.01em] font-inter text-right">
|
||||
Circulating supply
|
||||
</span>
|
||||
<span className="text-gray-600 dark:text-gray-300 text-body-small font-bold font-inter text-right">
|
||||
{circulatingSupply}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
|
||||
{/* Pool Capacity & Invest Button */}
|
||||
<CardFooter className="pt-8 pb-6 px-6 flex-col gap-4">
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-text-tertiary dark:text-gray-400 text-caption-tiny font-bold font-inter">
|
||||
Pool Capacity
|
||||
</span>
|
||||
<span className="text-text-primary dark:text-white text-body-small font-bold font-inter">
|
||||
{poolCapacityPercent}% Filled
|
||||
</span>
|
||||
</div>
|
||||
<div className="bg-gray-50 dark:bg-gray-700 rounded-full h-2 relative overflow-hidden">
|
||||
<div
|
||||
className="absolute left-0 top-0 bottom-0 rounded-full shadow-[0_0_15px_0_rgba(15,23,42,0.2)]"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(90deg, rgba(30, 41, 59, 1) 0%, rgba(51, 65, 85, 1) 50%, rgba(15, 23, 42, 1) 100%)",
|
||||
width: `${poolCapacityPercent}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
color="default"
|
||||
variant="solid"
|
||||
onPress={onInvest}
|
||||
className={buttonStyles({ intent: "theme" })}
|
||||
>
|
||||
Invest
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
142
components/fundmarket/ProductCardList.tsx
Normal file
142
components/fundmarket/ProductCardList.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { Button, Card, CardBody } from "@heroui/react";
|
||||
import { buttonStyles } from "@/lib/buttonStyles";
|
||||
|
||||
interface ProductCardListProps {
|
||||
name: string;
|
||||
category: string;
|
||||
categoryColor: "orange" | "green" | "blue" | "purple" | "red";
|
||||
iconType: "us-flag-1" | "hk-flag" | "us-flag-2" | "sg-flag" | "uk-flag";
|
||||
poolCap: string;
|
||||
lockUp: string;
|
||||
poolCapacityPercent: number;
|
||||
onInvest?: () => void;
|
||||
}
|
||||
|
||||
export default function ProductCardList({
|
||||
name,
|
||||
category,
|
||||
categoryColor,
|
||||
iconType,
|
||||
poolCap,
|
||||
lockUp,
|
||||
poolCapacityPercent,
|
||||
onInvest,
|
||||
}: ProductCardListProps) {
|
||||
const getIconSrc = () => {
|
||||
switch (iconType) {
|
||||
case "us-flag-1":
|
||||
case "hk-flag":
|
||||
case "us-flag-2":
|
||||
default:
|
||||
return "/lr0.svg";
|
||||
}
|
||||
};
|
||||
|
||||
const getIconBg = () => {
|
||||
switch (categoryColor) {
|
||||
case "orange":
|
||||
return "bg-orange-50 dark:bg-orange-900/20";
|
||||
case "green":
|
||||
return "bg-green-50 dark:bg-green-900/20";
|
||||
case "blue":
|
||||
return "bg-blue-50 dark:bg-blue-900/20";
|
||||
case "purple":
|
||||
return "bg-purple-50 dark:bg-purple-900/20";
|
||||
case "red":
|
||||
return "bg-red-50 dark:bg-red-900/20";
|
||||
default:
|
||||
return "bg-orange-50 dark:bg-orange-900/20";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="bg-bg-subtle dark:bg-gray-800 border border-border-gray dark:border-gray-700">
|
||||
<CardBody className="py-6 px-8">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
{/* Icon and Title - Fixed width */}
|
||||
<div className="flex flex-row gap-4 items-center justify-start w-[280px] flex-shrink-0">
|
||||
<div
|
||||
className={`${getIconBg()} rounded-md border-[0.75px] border-black/10 dark:border-white/10 w-10 h-[30px] relative shadow-[inset_0_1.5px_3px_0_rgba(0,0,0,0.05)] overflow-hidden flex items-center justify-center flex-shrink-0`}
|
||||
>
|
||||
<Image
|
||||
src={getIconSrc()}
|
||||
alt={name}
|
||||
width={43}
|
||||
height={30}
|
||||
className="w-[107.69%] h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5 items-start min-w-0 flex-1">
|
||||
<div className="text-text-tertiary dark:text-gray-400 text-caption-tiny font-normal font-inter">
|
||||
{category}
|
||||
</div>
|
||||
<div className="text-text-primary dark:text-white text-body-default font-bold font-inter truncate w-full">
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Middle section with stats - evenly distributed */}
|
||||
<div className="flex flex-row items-center justify-between flex-1 px-12">
|
||||
{/* Lock-up */}
|
||||
<div className="flex flex-col gap-1 items-start w-[120px] flex-shrink-0">
|
||||
<div className="text-text-tertiary dark:text-gray-400 text-caption-tiny font-normal font-inter">
|
||||
Lock-up
|
||||
</div>
|
||||
<div className="text-text-primary dark:text-white text-body-small font-bold font-inter">
|
||||
{lockUp}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pool Cap */}
|
||||
<div className="flex flex-col gap-1 items-start w-[100px] flex-shrink-0">
|
||||
<div className="text-text-tertiary dark:text-gray-400 text-caption-tiny font-normal font-inter">
|
||||
Pool Cap
|
||||
</div>
|
||||
<div className="text-green-500 dark:text-green-400 text-body-small font-bold font-inter">
|
||||
{poolCap}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pool Capacity */}
|
||||
<div className="flex flex-col gap-1 items-start w-[260px] flex-shrink-0">
|
||||
<div className="text-text-tertiary dark:text-gray-400 text-caption-tiny font-normal font-inter">
|
||||
Pool Capacity
|
||||
</div>
|
||||
<div className="flex flex-row gap-3 items-center">
|
||||
<div className="bg-gray-100 dark:bg-gray-700 rounded-full w-24 h-2 relative overflow-hidden flex-shrink-0">
|
||||
<div
|
||||
className="rounded-full h-2"
|
||||
style={{
|
||||
background:
|
||||
"linear-gradient(90deg, rgba(20, 71, 230, 1) 0%, rgba(3, 43, 189, 1) 100%)",
|
||||
width: `${poolCapacityPercent}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-text-primary dark:text-white text-body-small font-bold font-inter whitespace-nowrap">
|
||||
{poolCapacityPercent}% Filled
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Invest Button */}
|
||||
<div className="flex-shrink-0">
|
||||
<Button
|
||||
color="default"
|
||||
variant="solid"
|
||||
onPress={onInvest}
|
||||
className={buttonStyles({ intent: "theme" })}
|
||||
>
|
||||
Invest
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
74
components/fundmarket/StatsCards.tsx
Normal file
74
components/fundmarket/StatsCards.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { Card, CardBody } from "@heroui/react";
|
||||
|
||||
interface StatData {
|
||||
label: string;
|
||||
value: string;
|
||||
change: string;
|
||||
isPositive: boolean;
|
||||
}
|
||||
|
||||
interface StatsCardsProps {
|
||||
stats?: StatData[];
|
||||
}
|
||||
|
||||
export default function StatsCards({ stats = [] }: StatsCardsProps) {
|
||||
if (!stats || stats.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-row gap-6 items-start justify-start self-stretch">
|
||||
{stats.map((stat, index) => {
|
||||
const isLastCard = index === stats.length - 1;
|
||||
const isFirstCard = index === 0;
|
||||
const valueColor = isLastCard && stat.value === "--" ? "text-green-500" : "text-text-primary dark:text-white";
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={index}
|
||||
className={`flex-1 rounded-[2rem] h-full relative overflow-hidden group bg-bg-subtle dark:bg-gray-800 border border-border-gray dark:border-gray-700 shadow-[0_8px_32px_rgba(0,0,0,0.03)] transition-all duration-500 hover:shadow-[0_16px_48px_rgba(0,0,0,0.06)]
|
||||
${index === 0 ? 'bg-gradient-to-br from-blue-50/80 to-transparent dark:from-blue-900/20 dark:to-transparent' : ''}
|
||||
${index === 1 ? 'bg-gradient-to-br from-green-50/80 to-transparent dark:from-green-900/20 dark:to-transparent' : ''}
|
||||
${index === 2 ? 'bg-gradient-to-br from-purple-50/80 to-transparent dark:from-purple-900/20 dark:to-transparent' : ''}
|
||||
`}
|
||||
>
|
||||
<CardBody className="p-7 flex flex-col gap-3">
|
||||
{/* Header with label and change badge */}
|
||||
<div className={`flex flex-row gap-2 items-center ${index === 1 || index === 2 || index === 3 ? 'justify-between' : 'justify-start'} self-stretch`}>
|
||||
<div className="text-text-tertiary dark:text-gray-400 text-[10px] font-bold leading-[150%] tracking-[0.01em] font-inter">
|
||||
{stat.label}
|
||||
</div>
|
||||
<div
|
||||
className={`bg-green-50 dark:bg-green-900/30 rounded-full px-1 py-0.5 flex flex-row gap-0 items-center justify-start h-5 ${
|
||||
isFirstCard ? 'opacity-0' : 'opacity-100'
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
src="/stats-icon.svg"
|
||||
alt=""
|
||||
width={12}
|
||||
height={12}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
<div className="text-green-500 dark:text-green-400 text-[10px] font-bold leading-[150%] tracking-[0.01em] font-inter">
|
||||
{stat.change}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Value */}
|
||||
<div className="flex flex-row gap-4 items-end justify-start">
|
||||
<div className={`${valueColor} text-heading-h2 font-extrabold font-jetbrains`}>
|
||||
{stat.value}
|
||||
</div>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
56
components/fundmarket/ViewToggle.tsx
Normal file
56
components/fundmarket/ViewToggle.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@heroui/react";
|
||||
import Image from "next/image";
|
||||
|
||||
type ViewMode = "grid" | "list";
|
||||
|
||||
interface ViewToggleProps {
|
||||
value: ViewMode;
|
||||
onChange: (mode: ViewMode) => void;
|
||||
}
|
||||
|
||||
export default function ViewToggle({ value, onChange }: ViewToggleProps) {
|
||||
return (
|
||||
<div className="bg-gray-200 dark:bg-gray-700 rounded-lg p-1 flex gap-0">
|
||||
<Button
|
||||
isIconOnly
|
||||
size="sm"
|
||||
variant="light"
|
||||
onPress={() => onChange("list")}
|
||||
className={`w-8 h-8 min-w-8 rounded-lg transition-all ${
|
||||
value === "list"
|
||||
? "bg-white dark:bg-gray-600 shadow-sm"
|
||||
: "bg-transparent hover:bg-gray-300 dark:hover:bg-gray-600"
|
||||
}`}
|
||||
style={{ padding: 0 }}
|
||||
>
|
||||
<Image
|
||||
src="/edit-list-unordered0.svg"
|
||||
alt="List view"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
size="sm"
|
||||
variant="light"
|
||||
onPress={() => onChange("grid")}
|
||||
className={`w-8 h-8 min-w-8 rounded-lg transition-all ${
|
||||
value === "grid"
|
||||
? "bg-white dark:bg-gray-600 shadow-sm"
|
||||
: "bg-transparent hover:bg-gray-300 dark:hover:bg-gray-600"
|
||||
}`}
|
||||
style={{ padding: 0 }}
|
||||
>
|
||||
<Image
|
||||
src="/menu-more-grid-small0.svg"
|
||||
alt="Grid view"
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user