|
|
@@ -0,0 +1,120 @@
|
|
|
+import { useNavigate, NavLink, Outlet } from 'react-router-dom'
|
|
|
+import {
|
|
|
+ LayoutDashboard,
|
|
|
+ Database,
|
|
|
+ FileText,
|
|
|
+ Search,
|
|
|
+ Settings,
|
|
|
+ FolderKanban,
|
|
|
+ KeyRound,
|
|
|
+ LogOut,
|
|
|
+} from 'lucide-react'
|
|
|
+import { useAuthStore } from '@/stores/authStore'
|
|
|
+import { ProjectSelector } from './ProjectSelector'
|
|
|
+import { Toast } from './Toast'
|
|
|
+
|
|
|
+interface NavItem {
|
|
|
+ to: string
|
|
|
+ label: string
|
|
|
+ icon: React.ReactNode
|
|
|
+}
|
|
|
+
|
|
|
+const mainNav: NavItem[] = [
|
|
|
+ { to: '/', label: 'Dashboard', icon: <LayoutDashboard size={18} /> },
|
|
|
+ { to: '/collections', label: 'Collections', icon: <Database size={18} /> },
|
|
|
+ { to: '/documents', label: 'Documents', icon: <FileText size={18} /> },
|
|
|
+ { to: '/rag', label: 'RAG Tester', icon: <Search size={18} /> },
|
|
|
+]
|
|
|
+
|
|
|
+const adminNav: NavItem[] = [
|
|
|
+ { to: '/settings', label: 'Settings', icon: <Settings size={18} /> },
|
|
|
+ { to: '/projects', label: 'Projects', icon: <FolderKanban size={18} /> },
|
|
|
+ { to: '/keys', label: 'Keys', icon: <KeyRound size={18} /> },
|
|
|
+]
|
|
|
+
|
|
|
+function SidebarLink({ item }: { item: NavItem }) {
|
|
|
+ return (
|
|
|
+ <NavLink
|
|
|
+ to={item.to}
|
|
|
+ end={item.to === '/'}
|
|
|
+ className={({ isActive }) =>
|
|
|
+ [
|
|
|
+ 'flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition',
|
|
|
+ isActive
|
|
|
+ ? 'bg-indigo-600/30 text-indigo-300'
|
|
|
+ : 'text-slate-400 hover:text-slate-200 hover:bg-slate-700/50',
|
|
|
+ ].join(' ')
|
|
|
+ }
|
|
|
+ >
|
|
|
+ {item.icon}
|
|
|
+ {item.label}
|
|
|
+ </NavLink>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+export function AppShell() {
|
|
|
+ const admin = useAuthStore((s) => s.admin)
|
|
|
+ const logout = useAuthStore((s) => s.logout)
|
|
|
+ const navigate = useNavigate()
|
|
|
+
|
|
|
+ const handleLogout = () => {
|
|
|
+ logout().finally(() => {
|
|
|
+ navigate('/login', { replace: true })
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="flex min-h-screen bg-[#0b0f17] text-slate-100">
|
|
|
+ {/* Sidebar */}
|
|
|
+ <aside className="w-56 shrink-0 flex flex-col bg-[#080c12] border-r border-slate-800">
|
|
|
+ {/* Brand */}
|
|
|
+ <div className="px-4 py-5 border-b border-slate-800">
|
|
|
+ <span className="text-lg font-bold tracking-tight text-indigo-300">VectorAPI</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Nav */}
|
|
|
+ <nav className="flex-1 px-3 py-4 flex flex-col gap-1">
|
|
|
+ {mainNav.map((item) => (
|
|
|
+ <SidebarLink key={item.to} item={item} />
|
|
|
+ ))}
|
|
|
+
|
|
|
+ {admin && (
|
|
|
+ <>
|
|
|
+ <div className="mt-4 mb-1 px-3">
|
|
|
+ <span className="text-[10px] uppercase tracking-widest text-slate-600 font-semibold">
|
|
|
+ Admin
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ {adminNav.map((item) => (
|
|
|
+ <SidebarLink key={item.to} item={item} />
|
|
|
+ ))}
|
|
|
+ </>
|
|
|
+ )}
|
|
|
+ </nav>
|
|
|
+ </aside>
|
|
|
+
|
|
|
+ {/* Main area */}
|
|
|
+ <div className="flex-1 flex flex-col min-w-0">
|
|
|
+ {/* Top bar */}
|
|
|
+ <header className="flex items-center justify-between px-6 py-3 border-b border-slate-800 bg-[#0b0f17]">
|
|
|
+ <ProjectSelector />
|
|
|
+ <button
|
|
|
+ onClick={handleLogout}
|
|
|
+ className="flex items-center gap-2 text-sm text-slate-400 hover:text-slate-200 transition"
|
|
|
+ >
|
|
|
+ <LogOut size={16} />
|
|
|
+ Logout
|
|
|
+ </button>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ {/* Page content */}
|
|
|
+ <main className="flex-1 p-6 overflow-auto">
|
|
|
+ <Outlet />
|
|
|
+ </main>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Global toasts */}
|
|
|
+ <Toast />
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|