大更新
This commit is contained in:
53
components/layout/Breadcrumb.tsx
Normal file
53
components/layout/Breadcrumb.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
45
components/layout/LanguageSwitch.tsx
Normal file
45
components/layout/LanguageSwitch.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
59
components/layout/NavItem.tsx
Normal file
59
components/layout/NavItem.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
19
components/layout/PageTitle.tsx
Normal file
19
components/layout/PageTitle.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
17
components/layout/SectionHeader.tsx
Normal file
17
components/layout/SectionHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
119
components/layout/Sidebar.tsx
Normal file
119
components/layout/Sidebar.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import Image from "next/image";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { Listbox, ListboxItem } from "@heroui/react";
|
||||
import { useApp } from "@/contexts/AppContext";
|
||||
|
||||
export default function Sidebar() {
|
||||
const { t } = useApp();
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
const navigationItems = [
|
||||
{ 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 [currentKey, setCurrentKey] = useState("FundMarket");
|
||||
|
||||
useEffect(() => {
|
||||
const matched = navigationItems.find(item => item.path === pathname)?.key;
|
||||
if (matched) {
|
||||
setCurrentKey(matched);
|
||||
}
|
||||
}, [pathname]);
|
||||
|
||||
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 items-center px-6 py-8 gap-8 h-screen w-[222px] overflow-y-auto">
|
||||
{/* Logo */}
|
||||
<div className="w-full h-10">
|
||||
<Image
|
||||
src="/logo.svg"
|
||||
alt="ASSETX Logo"
|
||||
width={174}
|
||||
height={40}
|
||||
className="w-full h-full dark:invert"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<Listbox
|
||||
aria-label="Navigation"
|
||||
selectedKeys={[currentKey]}
|
||||
onAction={(key) => {
|
||||
const item = navigationItems.find(i => i.key === key);
|
||||
if (item) router.push(item.path);
|
||||
}}
|
||||
classNames={{
|
||||
base: "w-full p-0",
|
||||
list: "gap-1",
|
||||
}}
|
||||
>
|
||||
{navigationItems.map((item) => (
|
||||
<ListboxItem
|
||||
key={item.key}
|
||||
classNames={{
|
||||
base: [
|
||||
"h-11 px-3 rounded-xl gap-3",
|
||||
"data-[selected=true]:bg-fill-tertiary-normal dark:data-[selected=true]:bg-gray-700",
|
||||
"data-[selected=true]:text-text-primary dark:data-[selected=true]:text-white",
|
||||
"hover:bg-fill-quaternary dark:hover:bg-gray-700",
|
||||
].join(" "),
|
||||
}}
|
||||
startContent={
|
||||
<div className="w-[22px] h-[22px] flex-shrink-0 relative">
|
||||
<Image
|
||||
src={item.icon}
|
||||
alt={item.label}
|
||||
width={22}
|
||||
height={22}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{item.label}
|
||||
</ListboxItem>
|
||||
))}
|
||||
</Listbox>
|
||||
|
||||
{/* Spacer */}
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Global TVL Section */}
|
||||
<div className="w-full border-t border-border-gray dark:border-gray-700 pt-8">
|
||||
<div className="bg-bg-subtle dark:bg-gray-700 rounded-xl p-4 flex flex-col gap-1 h-[85px]">
|
||||
<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-base font-extrabold leading-[150%] font-jetbrains">
|
||||
$465,000,000
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* FAQs Link */}
|
||||
<div className="rounded-xl flex items-center h-[42px] mt-8">
|
||||
<div className="flex items-center gap-0">
|
||||
<Image
|
||||
src="/icon-faq.png"
|
||||
alt="FAQ"
|
||||
width={24}
|
||||
height={24}
|
||||
className="object-cover"
|
||||
/>
|
||||
<span className="text-text-primary dark:text-white text-sm font-bold leading-[150%]">
|
||||
{t("nav.faqs")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
53
components/layout/ThemeSwitch.tsx
Normal file
53
components/layout/ThemeSwitch.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
62
components/layout/TopBar.tsx
Normal file
62
components/layout/TopBar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user