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> | |
| ); | |
| } | |