| "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 }, |
| ]; |
|
|
| |
| |
| |
| |
| 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); |
|
|
| |
| 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"); |
| |
| }; |
|
|
| |
| const getUserInitials = () => { |
| if (userData?.sub) { |
| return userData.sub.slice(0, 2).toUpperCase(); |
| } |
| return "RA"; |
| }; |
|
|
| 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 alt={getUserInitials()} className="bg-primary/5 text-primary text-xs font-semibold" /> |
| </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> |
| ); |
| } |
|
|