diff --git a/apps/webapp/app/components/Sidebar.tsx b/apps/webapp/app/components/Sidebar.tsx index bbc9755..9f0fa6a 100644 --- a/apps/webapp/app/components/Sidebar.tsx +++ b/apps/webapp/app/components/Sidebar.tsx @@ -1,30 +1,56 @@ "use client"; +import md5 from "md5"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; -import { useSidebar } from "./SidebarContext"; +import { useEffect, useRef, useState } from "react"; import { trpc } from "../../utils/trpc"; -import md5 from "md5"; +import { useSidebar } from "./SidebarContext"; + +const dashboardIconPath = + "M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"; +const uploadIconPath = + "M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"; +const editorIconPath = + "M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"; +const chatIconPath = + "M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"; +const dataSourcesIconPath = + "M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"; +const workspacesIconPath = + "M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"; +const workerLogsIconPath = + "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"; +const profileSettingsIconPath = + "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"; +const profileSettingsUserIconPath = "M15 12a3 3 0 11-6 0 3 3 0 016 0z"; +const logoutIconPath = + "M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"; + +const navItems = [ + { href: "/dashboard", label: "Dashboard", icon: dashboardIconPath }, + { href: "/upload", label: "Upload Files", icon: uploadIconPath }, + { href: "/editor", label: "Editor", icon: editorIconPath }, + { href: "/chat", label: "Chat", icon: chatIconPath }, + { href: "/data-sources", label: "Data Sources", icon: dataSourcesIconPath }, +]; + +const bottomQuickActions = [ + { id: "workspaces", href: "/workspaces", label: "Workspaces", iconPath: workspacesIconPath }, + { id: "worker-logs", href: "/worker-logs", label: "Worker Logs", iconPath: workerLogsIconPath }, +]; const getGravatarUrl = (email: string, size: number = 80) => { const hash = md5(email.toLowerCase().trim()); return `https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon`; }; -const navItems = [ - { href: "/dashboard", label: "Dashboard", icon: "M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" }, - { href: "/upload", label: "Upload Files", icon: "M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" }, - { href: "/editor", label: "Editor", icon: "M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" }, - { href: "/chat", label: "Chat", icon: "M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" }, - { href: "/workspaces", label: "Workspaces", icon: "M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" }, - { href: "/worker-logs", label: "Worker Logs", icon: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" }, - { href: "/data-sources", label: "Data Sources", icon: "M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" }, -]; - export function Sidebar() { const pathname = usePathname(); const router = useRouter(); const { isCollapsed, setIsCollapsed } = useSidebar(); + const [isProfileMenuOpen, setIsProfileMenuOpen] = useState(false); + const profileMenuRef = useRef(null); const { data: userData } = trpc.auth.me.useQuery(); const logoutMutation = trpc.auth.logout.useMutation({ @@ -39,17 +65,73 @@ export function Sidebar() { const isActive = (path: string) => pathname === path; + useEffect(() => { + if (!isProfileMenuOpen) return; + + const onMouseDown = (event: MouseEvent) => { + const target = event.target as Node | null; + if (!target) return; + if (profileMenuRef.current?.contains(target)) return; + setIsProfileMenuOpen(false); + }; + + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") setIsProfileMenuOpen(false); + }; + + document.addEventListener("mousedown", onMouseDown); + document.addEventListener("keydown", onKeyDown); + return () => { + document.removeEventListener("mousedown", onMouseDown); + document.removeEventListener("keydown", onKeyDown); + }; + }, [isProfileMenuOpen]); + if (!userData?.user) return null; const user = userData.user; + const profileMenuItems = ( + <> + setIsProfileMenuOpen(false)} + className={`flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-mono transition-colors ${ + isActive("/profile") ? "bg-primary/10 text-primary" : "hover:bg-base-200" + }`} + > + + + + + Profile Settings + + + + ); + return (