116 lines
5.3 KiB
TypeScript
116 lines
5.3 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import { motion } from "motion/react";
|
|
import { LayoutDashboard, Trophy, ScrollText, Users, Activity, Sun, Moon, Monitor, Languages } from "lucide-react";
|
|
import { useI18n } from "@/lib/i18n";
|
|
import { useTheme, type Theme } from "@/lib/theme";
|
|
|
|
export function Sidebar() {
|
|
const pathname = usePathname();
|
|
const { t, locale, setLocale } = useI18n();
|
|
const { theme, setTheme, isEmbedded } = useTheme();
|
|
|
|
const nav = [
|
|
{ href: "/", label: t("nav.overview"), icon: LayoutDashboard },
|
|
{ href: "/rankings", label: t("nav.rankings"), icon: Trophy },
|
|
{ href: "/aggregation", label: t("nav.aggregation"), icon: Users },
|
|
{ href: "/logs", label: t("nav.logs"), icon: ScrollText },
|
|
];
|
|
|
|
const themes: { value: Theme; icon: typeof Sun; label: string }[] = [
|
|
{ value: "light", icon: Sun, label: t("theme.light") },
|
|
{ value: "dark", icon: Moon, label: t("theme.dark") },
|
|
{ value: "system", icon: Monitor, label: t("theme.system") },
|
|
];
|
|
|
|
return (
|
|
<aside className="fixed left-0 top-0 z-30 flex h-screen w-[220px] flex-col glass !rounded-none !border-l-0 !border-t-0 !border-b-0">
|
|
{/* Logo */}
|
|
<div className="flex h-16 items-center gap-3 border-b border-t px-5" style={{ borderColor: "var(--surface-border)" }}>
|
|
<div className="flex h-8 w-8 items-center justify-center rounded-lg border" style={{ borderColor: "var(--surface-border)", background: "var(--btn-active-bg)" }}>
|
|
<Activity className="h-4 w-4 text-t-accent" style={{ color: "var(--accent)" }} />
|
|
</div>
|
|
<div>
|
|
<span className="text-sm font-semibold tracking-wide text-t-primary" style={{ color: "var(--text-primary)" }}>Neural</span>
|
|
<span className="text-sm font-light tracking-wide" style={{ color: "var(--accent)" }}>Pulse</span>
|
|
</div>
|
|
</div>
|
|
|
|
<nav className="flex-1 space-y-1 p-3 pt-4">
|
|
{nav.map((item) => {
|
|
const active = item.href === "/" ? pathname === "/" : pathname.startsWith(item.href);
|
|
const Icon = item.icon;
|
|
return (
|
|
<Link key={item.href} href={item.href}>
|
|
<motion.div
|
|
className="relative flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm transition-colors"
|
|
style={{ color: active ? "var(--text-accent)" : "var(--text-muted)" }}
|
|
whileHover={{ x: 2 }}
|
|
transition={{ type: "spring", stiffness: 400, damping: 30 }}
|
|
>
|
|
{active && (
|
|
<motion.div
|
|
layoutId="sidebar-active"
|
|
className="absolute inset-0 rounded-lg"
|
|
style={{ background: "var(--btn-active-bg)", border: "1px solid var(--surface-border)" }}
|
|
transition={{ type: "spring", stiffness: 350, damping: 30 }}
|
|
/>
|
|
)}
|
|
<Icon className="relative z-10 h-4 w-4" />
|
|
<span className="relative z-10 font-medium">{item.label}</span>
|
|
{active && (
|
|
<motion.div
|
|
className="absolute left-0 top-1/2 h-5 w-[2px] -translate-y-1/2 rounded-full"
|
|
style={{ background: "var(--accent)" }}
|
|
layoutId="sidebar-indicator"
|
|
transition={{ type: "spring", stiffness: 350, damping: 30 }}
|
|
/>
|
|
)}
|
|
</motion.div>
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
|
|
{/* Controls */}
|
|
<div className="space-y-3 p-4 border-t" style={{ borderColor: "var(--surface-border)" }}>
|
|
{!isEmbedded && (
|
|
<div className="flex gap-1 rounded-lg p-0.5" style={{ background: "var(--row-hover)", border: "1px solid var(--surface-border)" }}>
|
|
{themes.map(({ value, icon: Icon }) => (
|
|
<button key={value} onClick={() => setTheme(value)}
|
|
className="flex-1 flex items-center justify-center rounded-md py-1.5 transition-colors"
|
|
style={{
|
|
background: theme === value ? "var(--btn-active-bg)" : "transparent",
|
|
color: theme === value ? "var(--text-accent)" : "var(--text-muted)",
|
|
border: theme === value ? "1px solid var(--surface-border)" : "1px solid transparent",
|
|
}}
|
|
title={themes.find(t => t.value === value)?.label}
|
|
>
|
|
<Icon className="h-3.5 w-3.5" />
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Language switcher */}
|
|
<button
|
|
onClick={() => setLocale(locale === "zh" ? "en" : "zh")}
|
|
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-xs transition-colors"
|
|
style={{ color: "var(--text-muted)", background: "var(--row-hover)", border: "1px solid var(--surface-border)" }}
|
|
>
|
|
<Languages className="h-3.5 w-3.5" />
|
|
<span className="font-medium">{locale === "zh" ? "English" : "中文"}</span>
|
|
</button>
|
|
|
|
{/* Status */}
|
|
<div className="flex items-center gap-2">
|
|
<div className="h-2 w-2 rounded-full bg-emerald-400 shadow-[0_0_8px_rgba(52,211,153,0.5)]" />
|
|
<span className="text-xs" style={{ color: "var(--text-muted)" }}>{t("common.systemOnline")}</span>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
);
|
|
}
|