RM / src /components /organisms /Sidebar /index.tsx
trretretret's picture
Comprehensive frontend fixes: Updated gitignore, fixed all import paths, enhanced lib utilities
49e7bf6
"use client";
import * as React from "react";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import {
LayoutDashboard,
Search,
Library,
FileEdit,
Settings,
ChevronLeft,
ChevronRight,
LogOut,
Microscope,
User as UserIcon
} from "lucide-react";
import { Avatar, AvatarFallback } from "@/components/atoms/Avatar";
import { Icon } from "@/components/atoms/Icon";
import { Spinner } from "@/components/atoms/Spinner";
import { cn } from "@/lib/utils";
const navItems = [
{ label: "Dashboard", href: "/dashboard", icon: LayoutDashboard },
{ label: "Explore", href: "/explore", icon: Search },
{ label: "Library", href: "/library", icon: Library },
{ label: "WriteSage", href: "/editor", icon: FileEdit },
];
/**
* Utility: Decodes JWT payload without a heavy library.
* Backend still handles actual verification.
*/
function decodeToken(token: string) {
try {
const base64Url = token.split(".")[1];
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
const jsonPayload = decodeURIComponent(
window
.atob(base64)
.split("")
.map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2))
.join("")
);
return JSON.parse(jsonPayload);
} catch (error) {
return null;
}
}
export function Sidebar() {
const pathname = usePathname();
const router = useRouter();
const [isCollapsed, setIsCollapsed] = React.useState(false);
const [isLoggingOut, setIsLoggingOut] = React.useState(false);
const [userData, setUserData] = React.useState<{ sub: string; is_premium?: boolean } | null>(null);
// 1. Sync User State with Token
React.useEffect(() => {
const token = localStorage.getItem("token");
if (token) {
const decoded = decodeToken(token);
if (decoded) setUserData(decoded);
}
}, []);
const handleLogout = () => {
setIsLoggingOut(true);
localStorage.removeItem("token");
router.push("/login");
// State reset is handled by the redirect/refresh
};
return (
<aside
className={cn(
"relative flex flex-col border-r bg-card transition-all duration-300 ease-in-out z-40",
isCollapsed ? "w-20" : "w-64"
)}
>
{/* Branding */}
<div className="flex h-16 items-center border-b px-6">
<Link href="/dashboard" className="flex items-center gap-3">
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary text-primary-foreground shadow-sm">
<Icon icon={Microscope} size={20} />
</div>
{!isCollapsed && (
<span className="text-lg font-bold tracking-tight text-foreground animate-in fade-in duration-300">
Romeo AI
</span>
)}
</Link>
</div>
{/* Navigation */}
<nav className="flex-1 space-y-1 p-4 overflow-y-auto">
{navItems.map((item) => {
const isActive = pathname.startsWith(item.href);
return (
<Link
key={item.href}
href={item.href}
className={cn(
"group flex items-center rounded-md px-3 py-2.5 text-sm font-medium transition-all",
isActive
? "bg-primary text-primary-foreground shadow-sm"
: "text-muted-foreground hover:bg-muted hover:text-foreground"
)}
>
<Icon
icon={item.icon}
className={cn("shrink-0", isCollapsed ? "mx-auto" : "mr-3")}
/>
{!isCollapsed && <span className="truncate">{item.label}</span>}
</Link>
);
})}
</nav>
{/* Bottom Context Section */}
<div className="border-t p-4 space-y-2">
<Link
href="/settings"
className={cn(
"flex items-center rounded-md px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-muted transition-colors",
pathname === "/settings" && "bg-muted text-foreground"
)}
>
<Icon icon={Settings} className={cn("shrink-0", isCollapsed ? "mx-auto" : "mr-3")} />
{!isCollapsed && <span>Settings</span>}
</Link>
{/* User Badge */}
<div className={cn(
"flex items-center gap-3 rounded-lg border bg-muted/30 p-2",
isCollapsed ? "justify-center" : "px-3"
)}>
<Avatar className="h-8 w-8 border">
<AvatarFallback className="bg-primary/5 text-primary">
<UserIcon size={14} />
</AvatarFallback>
</Avatar>
{!isCollapsed && (
<div className="flex flex-col min-w-0 flex-1">
<span className="truncate text-[11px] font-semibold leading-tight">
{userData?.sub || "Researcher"}
</span>
<span className="truncate text-[9px] text-muted-foreground font-medium uppercase tracking-wider">
{userData?.is_premium ? "Pro Plan" : "Free Plan"}
</span>
</div>
)}
</div>
<button
onClick={handleLogout}
disabled={isLoggingOut}
className={cn(
"flex w-full items-center rounded-md px-3 py-2 text-sm font-medium text-destructive hover:bg-destructive/10 transition-colors",
isCollapsed && "justify-center"
)}
>
{isLoggingOut ? (
<Spinner size={16} className={isCollapsed ? "" : "mr-3"} />
) : (
<Icon icon={LogOut} className={cn("shrink-0", isCollapsed ? "mx-auto" : "mr-3")} />
)}
{!isCollapsed && <span>{isLoggingOut ? "Exiting..." : "Logout"}</span>}
</button>
</div>
{/* Collapse Toggle */}
<button
onClick={() => setIsCollapsed(!isCollapsed)}
className="absolute -right-3 top-20 hidden md:flex h-6 w-6 items-center justify-center rounded-full border bg-background shadow-sm hover:bg-muted transition-colors"
>
<Icon icon={isCollapsed ? ChevronRight : ChevronLeft} size={12} />
</button>
</aside>
);
}