initial commit

This commit is contained in:
YoRHa
2026-02-04 13:39:12 +08:00
commit 199940e958
175 changed files with 19963 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
"use client";
import Image from "next/image";
import Link from "next/link";
interface BreadcrumbItem {
label: string;
href?: string;
}
interface BreadcrumbProps {
items: BreadcrumbItem[];
}
export default function Breadcrumb({ items }: BreadcrumbProps) {
return (
<nav className="flex items-center gap-[3px] h-5">
{items.map((item, index) => {
const isLast = index === items.length - 1;
const content = (
<span
className={`text-sm font-medium leading-[150%] ${
isLast
? "text-text-primary dark:text-white font-bold"
: "text-text-tertiary dark:text-gray-400 hover:text-text-secondary dark:hover:text-gray-300 transition-colors cursor-pointer"
}`}
>
{item.label}
</span>
);
return (
<div key={index} className="flex items-center gap-[3px]">
{!isLast && item.href ? (
<Link href={item.href}>{content}</Link>
) : (
content
)}
{!isLast && (
<Image
src="/icon-chevron-right.svg"
alt=""
width={14}
height={14}
className="flex-shrink-0 dark:invert"
/>
)}
</div>
);
})}
</nav>
);
}

View File

@@ -0,0 +1,45 @@
"use client";
import { useApp } from "@/contexts/AppContext";
import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem, Button } from "@heroui/react";
export default function LanguageSwitch() {
const { language, setLanguage } = useApp();
const languages = [
{ key: "zh", label: "中文" },
{ key: "en", label: "English" },
];
const handleSelectionChange = (key: React.Key) => {
setLanguage(key as "zh" | "en");
};
return (
<Dropdown>
<DropdownTrigger>
<Button
variant="bordered"
className="bg-bg-surface dark:bg-gray-800 border-border-normal dark:border-gray-700 min-w-10 h-10 px-3 rounded-lg"
>
<span className="text-sm font-medium text-text-primary dark:text-white">
{language === "zh" ? "中" : "EN"}
</span>
</Button>
</DropdownTrigger>
<DropdownMenu
aria-label="Language selection"
selectedKeys={new Set([language])}
selectionMode="single"
onSelectionChange={(keys) => {
const key = Array.from(keys)[0];
if (key) handleSelectionChange(key);
}}
>
{languages.map((lang) => (
<DropdownItem key={lang.key}>{lang.label}</DropdownItem>
))}
</DropdownMenu>
</Dropdown>
);
}

View File

@@ -0,0 +1,59 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
interface NavItemProps {
icon: string;
label: string;
href: string;
}
export default function NavItem({ icon, label, href }: NavItemProps) {
const pathname = usePathname();
const isActive = pathname === href;
return (
<Link
href={href}
className={`
rounded-xl
pl-4
flex
items-center
gap-2
h-[42px]
w-full
overflow-hidden
transition-colors
${isActive
? 'bg-fill-secondary-click dark:bg-gray-700'
: 'hover:bg-gray-50 dark:hover:bg-gray-700'
}
`}
>
<div className="w-[22px] h-[22px] flex-shrink-0 relative">
<Image
src={icon}
alt={label}
width={22}
height={22}
className="w-full h-full"
/>
</div>
<span
className={`
text-sm
leading-[150%]
${isActive
? 'text-text-primary dark:text-white font-bold'
: 'text-text-tertiary dark:text-gray-400 font-medium'
}
`}
>
{label}
</span>
</Link>
);
}

View File

@@ -0,0 +1,19 @@
interface PageTitleProps {
title: string;
subtitle?: string;
}
export default function PageTitle({ title, subtitle }: PageTitleProps) {
return (
<div className="mb-8">
<h1 className="text-heading-h2 font-bold text-text-primary dark:text-white">
{title}
</h1>
{subtitle && (
<p className="text-body-large text-text-secondary dark:text-gray-400 mt-2">
{subtitle}
</p>
)}
</div>
);
}

View File

@@ -0,0 +1,17 @@
import { ReactNode } from "react";
interface SectionHeaderProps {
title: string;
children?: ReactNode;
}
export default function SectionHeader({ title, children }: SectionHeaderProps) {
return (
<div className="flex flex-row items-center justify-between">
<h2 className="text-text-primary dark:text-white text-heading-h3 font-bold">
{title}
</h2>
{children && <div>{children}</div>}
</div>
);
}

View File

@@ -0,0 +1,134 @@
"use client";
import Image from "next/image";
import { usePathname, useRouter } from "next/navigation";
import { ScrollShadow } from "@heroui/react";
import { useApp } from "@/contexts/AppContext";
interface NavItem {
icon: string;
label: string;
key: string;
path: string;
}
export default function Sidebar() {
const { t } = useApp();
const pathname = usePathname();
const router = useRouter();
const navigationItems: NavItem[] = [
{ icon: "/icon-lending.svg", label: t("nav.fundMarket"), key: "FundMarket", path: "/" },
{ icon: "/icon-alp.svg", label: t("nav.alp"), key: "ALP", path: "/alp" },
{ icon: "/icon-swap.svg", label: t("nav.swap"), key: "Swap", path: "/swap" },
{ icon: "/icon-lending.svg", label: t("nav.lending"), key: "Lending", path: "/lending" },
{ icon: "/icon-transparency.svg", label: t("nav.transparency"), key: "Transparency", path: "/transparency" },
{ icon: "/icon-ecosystem.svg", label: t("nav.ecosystem"), key: "Ecosystem", path: "/ecosystem" },
{ icon: "/icon-points.svg", label: t("nav.points"), key: "Points", path: "/points" },
];
const isActive = (path: string) => {
if (path === "/") {
return pathname === "/";
}
return pathname.startsWith(path);
};
return (
<aside className="fixed left-0 top-0 bg-bg-surface dark:bg-gray-800 border-r border-border-normal dark:border-gray-700 flex flex-col h-screen w-[240px] overflow-hidden">
{/* Logo */}
<div className="flex-shrink-0 px-6 py-8">
<div className="h-10 relative cursor-pointer" onClick={() => router.push("/")}>
<Image
src="/logo.svg"
alt="ASSETX Logo"
fill
className="object-contain dark:invert"
/>
</div>
</div>
{/* Navigation */}
<ScrollShadow className="flex-1 px-4" hideScrollBar>
<nav className="flex flex-col gap-1 py-1">
{navigationItems.map((item) => {
const active = isActive(item.path);
return (
<div key={item.key} className="relative group">
{/* Active indicator */}
{active && (
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-text-primary dark:bg-white rounded-r-full" />
)}
{/* Hover sliding indicator */}
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-text-primary/0 dark:bg-white/0 group-hover:bg-text-primary/20 dark:group-hover:bg-white/20 rounded-r-full transition-all duration-300 scale-y-0 group-hover:scale-y-100 origin-center" />
<button
onClick={() => router.push(item.path)}
className={`
w-full h-11 px-4 rounded-xl gap-3 flex items-center relative
transition-all duration-200
${active
? "bg-fill-secondary-click dark:bg-gray-700 font-bold"
: "hover:bg-gray-100 dark:hover:bg-gray-700"
}
`}
>
<div className={`w-5 h-5 flex-shrink-0 relative transition-all duration-200 ${active ? "opacity-100" : "opacity-70 group-hover:opacity-100"}`}>
<Image
src={item.icon}
alt={item.label}
fill
className={`object-contain transition-all duration-200 ${active ? "brightness-[0.25] drop-shadow-[0_0_0.5px_rgba(0,0,0,0.15)]" : "brightness-[0.45]"}`}
/>
</div>
<span className={`
text-sm leading-[150%] transition-all duration-200
${active
? "text-text-primary dark:text-white translate-x-0"
: "text-text-secondary dark:text-gray-400 group-hover:translate-x-1"
}
`}>
{item.label}
</span>
</button>
</div>
);
})}
</nav>
</ScrollShadow>
{/* Bottom Section */}
<div className="flex-shrink-0 px-4 py-6 border-t border-border-gray dark:border-gray-700">
{/* Global TVL */}
<div className="bg-bg-subtle dark:bg-gray-700 rounded-xl p-4 mb-4">
<p className="text-text-tertiary dark:text-gray-400 text-[10px] font-medium leading-[150%] tracking-[0.01em]">
{t("nav.globalTVL")}
</p>
<p className="text-text-primary dark:text-white text-lg font-extrabold leading-[150%] font-jetbrains mt-1">
$465,000,000
</p>
</div>
{/* FAQs Link */}
<button
onClick={() => router.push("/faq")}
className="w-full rounded-xl flex items-center gap-3 h-11 px-4 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors duration-200"
>
<div className="w-6 h-6 relative">
<Image
src="/icon-faq.png"
alt="FAQ"
fill
className="object-cover"
/>
</div>
<span className="text-text-primary dark:text-white text-sm font-bold leading-[150%]">
{t("nav.faqs")}
</span>
</button>
</div>
</aside>
);
}

View File

@@ -0,0 +1,53 @@
"use client";
import { useApp } from "@/contexts/AppContext";
import { Button } from "@heroui/react";
export default function ThemeSwitch() {
const { theme, setTheme } = useApp();
const toggleTheme = () => {
setTheme(theme === "light" ? "dark" : "light");
};
return (
<Button
isIconOnly
variant="bordered"
onPress={toggleTheme}
className="bg-bg-surface dark:bg-gray-800 border-border-normal dark:border-gray-700 min-w-10 h-10 rounded-lg"
aria-label={`Switch to ${theme === "light" ? "dark" : "light"} mode`}
>
{theme === "light" ? (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path
d="M10 15C12.7614 15 15 12.7614 15 10C15 7.23858 12.7614 5 10 5C7.23858 5 5 7.23858 5 10C5 12.7614 7.23858 15 10 15Z"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
className="text-text-primary dark:text-white"
/>
<path
d="M10 1V3M10 17V19M19 10H17M3 10H1M16.07 16.07L14.64 14.64M5.36 5.36L3.93 3.93M16.07 3.93L14.64 5.36M5.36 14.64L3.93 16.07"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
className="text-text-primary dark:text-white"
/>
</svg>
) : (
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path
d="M17 10.79C16.8427 12.4922 16.1039 14.0754 14.9116 15.2662C13.7192 16.4571 12.1503 17.1913 10.4501 17.3464C8.74989 17.5016 7.04992 17.0676 5.63182 16.1159C4.21372 15.1642 3.15973 13.7534 2.63564 12.1102C2.11155 10.467 2.14637 8.68739 2.73477 7.06725C3.32317 5.44711 4.43113 4.07931 5.88616 3.18637C7.3412 2.29343 9.05859 1.93047 10.7542 2.15507C12.4498 2.37967 13.9989 3.17747 15.16 4.41"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
className="text-text-primary dark:text-white"
/>
</svg>
)}
</Button>
);
}

View File

@@ -0,0 +1,62 @@
"use client";
import Image from "next/image";
import { Button } from "@heroui/react";
import Breadcrumb from "./Breadcrumb";
import LanguageSwitch from "./LanguageSwitch";
import ThemeSwitch from "./ThemeSwitch";
interface BreadcrumbItem {
label: string;
href?: string;
}
interface TopBarProps {
breadcrumbItems?: BreadcrumbItem[];
}
export default function TopBar({ breadcrumbItems }: TopBarProps) {
return (
<div className="flex items-center justify-between w-full">
{/* Left: Breadcrumb */}
<div className="flex items-center gap-2">
{breadcrumbItems && <Breadcrumb items={breadcrumbItems} />}
</div>
{/* Right: Actions */}
<div className="flex items-center gap-4">
{/* Language Switch */}
<LanguageSwitch />
{/* Theme Switch */}
<ThemeSwitch />
{/* Wallet Button */}
<Button
isIconOnly
variant="bordered"
className="bg-bg-surface border-border-normal min-w-10 h-10 rounded-lg"
>
<div className="flex items-center justify-center">
<Image src="/icon-wallet.svg" alt="Wallet" width={20} height={20} />
<Image
src="/icon-notification.svg"
alt="Notification"
width={14}
height={14}
className="ml-1"
/>
</div>
</Button>
{/* Address Button */}
<Button
className="bg-text-primary text-white font-bold font-jetbrains text-sm h-10 px-4 rounded-lg"
startContent={<Image src="/icon-copy.svg" alt="Copy" width={16} height={16} />}
>
0x12...4F82
</Button>
</div>
</div>
);
}