Files
assetx/webapp/components/layout/Sidebar.tsx
default 2ee4553b71 init: 初始化 AssetX 项目仓库
包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、
antdesign(管理后台)、landingpage(营销落地页)、
数据库 SQL 和配置文件。
2026-03-27 11:26:43 +00:00

245 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState } from "react";
import Image from "next/image";
import { usePathname, useRouter } from "next/navigation";
import { ScrollShadow } from "@heroui/react";
import {
Menu, X,
TrendingUp, BarChart3, ArrowLeftRight, Landmark,
ShieldCheck, Network, Sparkles, HelpCircle,
type LucideIcon,
} from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { useApp } from "@/contexts/AppContext";
import WalletButton from "@/components/wallet/WalletButton";
import LanguageSwitch from "./LanguageSwitch";
import { fetchStats } from "@/lib/api/fundmarket";
interface NavItem {
Icon: LucideIcon;
label: string;
key: string;
path: string;
}
export default function Sidebar() {
const { t } = useApp();
const pathname = usePathname();
const router = useRouter();
const [mobileOpen, setMobileOpen] = useState(false);
const { data: stats = [] } = useQuery({
queryKey: ["fundmarket-stats"],
queryFn: fetchStats,
staleTime: 5 * 60 * 1000,
});
const tvlValue = stats[0]?.value ?? "$0";
const navigationItems: NavItem[] = [
{ Icon: TrendingUp, label: t("nav.fundMarket"), key: "FundMarket", path: "/market" },
{ Icon: BarChart3, label: t("nav.alp"), key: "ALP", path: "/alp" },
{ Icon: ArrowLeftRight, label: t("nav.swap"), key: "Swap", path: "/swap" },
{ Icon: Landmark, label: t("nav.lending"), key: "Lending", path: "/lending" },
{ Icon: ShieldCheck, label: t("nav.transparency"), key: "Transparency", path: "/transparency" },
{ Icon: Network, label: t("nav.ecosystem"), key: "Ecosystem", path: "/ecosystem" },
{ Icon: Sparkles, label: t("nav.points"), key: "Points", path: "/points" },
];
const isActive = (path: string) =>
pathname === path || pathname.startsWith(path + "/");
// 桌面端导航项
const renderDesktopNavItems = (onClick: (path: string) => void) =>
navigationItems.map(({ Icon, label, key, path }) => {
const active = isActive(path);
return (
<div key={key} className="relative group">
{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" />
)}
<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={() => onClick(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"
}
`}
>
<Icon
size={18}
className={`flex-shrink-0 transition-all duration-200 ${
active
? "text-text-primary dark:text-white"
: "text-text-secondary dark:text-gray-400 group-hover:text-text-primary dark:group-hover:text-white"
}`}
/>
<span className={`
text-sm leading-[150%] transition-all duration-200
${active
? "text-text-primary dark:text-white"
: "text-text-secondary dark:text-gray-400 group-hover:translate-x-1"
}
`}>
{label}
</span>
</button>
</div>
);
});
// 移动端抽屉导航项pill 样式)
const renderMobileNavItems = () =>
navigationItems.map(({ Icon, label, key, path }) => {
const active = isActive(path);
return (
<button
key={key}
onClick={() => { router.push(path); setMobileOpen(false); }}
className={`
w-full flex items-center gap-4 px-4 py-3.5 rounded-2xl
transition-all duration-200 text-left
${active
? "bg-gray-900 dark:bg-white"
: "hover:bg-gray-100 dark:hover:bg-gray-700/60"
}
`}
>
<Icon
size={18}
className={`flex-shrink-0 ${
active ? "text-white dark:text-gray-900" : "text-gray-400 dark:text-gray-400"
}`}
/>
<span className={`text-[15px] font-semibold leading-[150%] ${
active ? "text-white dark:text-gray-900" : "text-gray-700 dark:text-gray-200"
}`}>
{label}
</span>
{active && (
<div className="ml-auto w-1.5 h-1.5 rounded-full bg-white dark:bg-gray-900" />
)}
</button>
);
});
return (
<>
{/* ── MOBILE NAV BAR ── */}
<nav className="md:hidden fixed top-0 left-0 right-0 z-50 h-14 bg-bg-surface dark:bg-gray-800 border-b border-border-normal dark:border-gray-700 flex items-center justify-between px-4">
<div
className="h-8 w-28 relative cursor-pointer"
onClick={() => { router.push("/market"); setMobileOpen(false); }}
>
<Image src="/logos/logo.svg" alt="ASSETX Logo" fill className="object-contain dark:invert" />
</div>
<div className="flex items-center gap-2">
<WalletButton compact />
<button
onClick={() => setMobileOpen(!mobileOpen)}
className="w-10 h-10 flex items-center justify-center rounded-lg border border-border-normal dark:border-gray-700 bg-bg-surface dark:bg-gray-800"
aria-label="Toggle menu"
>
{mobileOpen ? <X size={20} /> : <Menu size={20} />}
</button>
</div>
</nav>
{/* ── MOBILE DRAWER BACKDROP ── */}
<div
className={`md:hidden fixed inset-0 z-40 bg-black/50 backdrop-blur-sm transition-opacity duration-300 ${mobileOpen ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none"}`}
onClick={() => setMobileOpen(false)}
/>
{/* ── MOBILE DRAWER (右侧滑出) ── */}
<aside
className={`md:hidden fixed top-0 right-0 bottom-0 z-50 w-[85vw] max-w-[320px] bg-white dark:bg-gray-900 flex flex-col shadow-[-12px_0_40px_rgba(0,0,0,0.15)] transition-transform duration-300 ease-out ${mobileOpen ? "translate-x-0" : "translate-x-full"}`}
>
{/* 顶部Logo + 关闭按钮 */}
<div className="flex-shrink-0 flex items-center justify-between px-5 pt-12 pb-6 landscape:pt-4 landscape:pb-3">
<div className="h-7 w-24 relative">
<Image src="/logos/logo.svg" alt="ASSETX Logo" fill className="object-contain dark:invert" />
</div>
<button
onClick={() => setMobileOpen(false)}
className="w-9 h-9 flex items-center justify-center rounded-full bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400"
>
<X size={18} />
</button>
</div>
{/* 导航项 */}
<ScrollShadow className="flex-1 px-3 overflow-y-auto" hideScrollBar>
<nav className="flex flex-col gap-1">
{renderMobileNavItems()}
</nav>
</ScrollShadow>
{/* 底部TVL + Language + FAQ */}
<div className="flex-shrink-0 px-5 py-4 landscape:py-2 space-y-3 landscape:space-y-2">
<div className="bg-gray-50 dark:bg-gray-800 rounded-2xl px-4 py-2 landscape:py-1.5">
<p className="text-gray-400 dark:text-gray-500 text-[11px] font-semibold uppercase tracking-wider mb-0.5">
{t("nav.globalTVL")}
</p>
<p className="text-gray-900 dark:text-white text-lg font-extrabold font-inter landscape:text-base">
{tvlValue}
</p>
</div>
<div className="flex items-center justify-between">
<button
onClick={() => { router.push("/faq"); setMobileOpen(false); }}
className="flex items-center gap-2 h-10 px-4 rounded-xl bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 text-sm font-semibold hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
>
<HelpCircle size={16} />
{t("nav.faqs")}
</button>
<LanguageSwitch />
</div>
</div>
</aside>
{/* ── DESKTOP SIDEBAR ── */}
<aside className="hidden md:flex fixed left-0 top-0 bg-bg-surface dark:bg-gray-800 border-r border-border-normal dark:border-gray-700 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("/market")}>
<Image src="/logos/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">
{renderDesktopNavItems((path) => router.push(path))}
</nav>
</ScrollShadow>
{/* Bottom Section */}
<div className="flex-shrink-0 px-4 py-6 border-t border-border-gray dark:border-gray-700">
<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-inter mt-1">
{tvlValue}
</p>
</div>
<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"
>
<HelpCircle size={18} className="text-text-primary dark:text-white flex-shrink-0" />
<span className="text-text-primary dark:text-white text-sm font-bold leading-[150%]">
{t("nav.faqs")}
</span>
</button>
</div>
</aside>
</>
);
}