diff --git a/apps/web/src/components/sidebar.tsx b/apps/web/src/components/sidebar.tsx new file mode 100644 index 0000000000..50cc98224e --- /dev/null +++ b/apps/web/src/components/sidebar.tsx @@ -0,0 +1,520 @@ +import { Link, useRouterState } from "@tanstack/react-router"; +import { + BookOpen, + Building2, + ChevronRight, + FileText, + History, + LayoutTemplate, + Map, + Menu, + MessageCircle, + Newspaper, + X, +} from "lucide-react"; +import type { LucideIcon } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; + +import { cn } from "@hypr/utils"; + +import { SearchTrigger } from "@/components/search"; +import { getPlatformCTA, usePlatform } from "@/hooks/use-platform"; + +const featuresList = [ + { to: "/product/ai-notetaking", label: "AI Notetaking" }, + { to: "/product/search", label: "Searchable Notes" }, + { to: "/gallery?type=template", label: "Custom Templates" }, + { to: "/product/markdown", label: "Markdown Files" }, + { to: "/product/flexible-ai", label: "Flexible AI" }, + { to: "/opensource", label: "Open Source" }, +]; + +const solutionsList = [ + { to: "/solution/knowledge-workers", label: "For Knowledge Workers" }, + { to: "/enterprise", label: "For Enterprises" }, + { to: "/product/api", label: "For Developers" }, +]; + +const resourcesList: { + to: string; + label: string; + icon: LucideIcon; + external?: boolean; +}[] = [ + { to: "/blog/", label: "Blog", icon: FileText }, + { to: "/docs/", label: "Documentation", icon: BookOpen }, + { + to: "/gallery?type=template", + label: "Meeting Templates", + icon: LayoutTemplate, + }, + { to: "/updates/", label: "Updates", icon: Newspaper }, + { to: "/changelog/", label: "Changelog", icon: History }, + { to: "/roadmap/", label: "Roadmap", icon: Map }, + { to: "/company-handbook/", label: "Company Handbook", icon: Building2 }, + { + to: "https://discord.gg/hyprnote", + label: "Community", + icon: MessageCircle, + external: true, + }, +]; + +function CharLogo({ className }: { className?: string }) { + return ( + + + + + + + + + ); +} + +const navLinks = [ + { to: "/why-char/", label: "Why Char" }, + { to: "/product/ai-notetaking/", label: "Product", hasSubmenu: true }, + { to: "/docs/", label: "Resources", hasSubmenu: true }, + { to: "/pricing/", label: "Pricing" }, +] as const; + +export function Sidebar() { + const [isMobileOpen, setIsMobileOpen] = useState(false); + const router = useRouterState(); + const platform = usePlatform(); + const platformCTA = getPlatformCTA(platform); + const pathname = router.location.pathname; + + useEffect(() => { + setIsMobileOpen(false); + }, [pathname]); + + useEffect(() => { + if (isMobileOpen) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = ""; + } + return () => { + document.body.style.overflow = ""; + }; + }, [isMobileOpen]); + + return ( + <> + + + {isMobileOpen && ( +
setIsMobileOpen(false)} + /> + )} + + + + {/* Mobile slide-out sidebar */} + + + ); +} + +function MobileTopBar({ + isMobileOpen, + setIsMobileOpen, +}: { + isMobileOpen: boolean; + setIsMobileOpen: (open: boolean) => void; +}) { + return ( +
+ + + + +
+ ); +} + +function SidebarFlyout({ + label, + to, + isActive, +}: { + label: string; + to: string; + isActive: boolean; +}) { + const [isOpen, setIsOpen] = useState(false); + const timeoutRef = useRef | null>(null); + + const open = () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + setIsOpen(true); + }; + + const close = () => { + timeoutRef.current = setTimeout(() => setIsOpen(false), 150); + }; + + useEffect(() => { + return () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + }; + }, []); + + return ( +
+ + {label} + + + + {isOpen && ( +
+
+ {label === "Product" && } + {label === "Resources" && } +
+
+ )} +
+ ); +} + +function ProductFlyoutContent() { + return ( +
+
+ + Features + +
+ {featuresList.map((item) => ( + + {item.label} + + ))} +
+
+ + Solutions + +
+ {solutionsList.map((item) => ( + + {item.label} + + ))} +
+ ); +} + +function ResourcesFlyoutContent() { + return ( +
+ {resourcesList.map((item) => { + const Icon = item.icon; + if (item.external) { + return ( + + + {item.label} + + ); + } + return ( + + + {item.label} + + ); + })} +
+ ); +} + +function MobileSubmenu({ + label, + isActive, +}: { + label: string; + to: string; + isActive: boolean; +}) { + const [isExpanded, setIsExpanded] = useState(false); + + return ( +
+ + + {isExpanded && ( +
+ {label === "Product" && ( + <> + + Features + + {featuresList.map((item) => ( + + {item.label} + + ))} + + Solutions + + {solutionsList.map((item) => ( + + {item.label} + + ))} + + )} + {label === "Resources" && ( + <> + {resourcesList.map((item) => { + const IconComp = item.icon; + if (item.external) { + return ( + + + {item.label} + + ); + } + return ( + + + {item.label} + + ); + })} + + )} +
+ )} +
+ ); +} + +function SidebarCTA({ + platformCTA, +}: { + platformCTA: ReturnType; +}) { + const baseClass = + "flex h-9 items-center justify-center rounded-lg bg-neutral-800 text-sm text-neutral-300 transition-colors hover:bg-neutral-700 hover:text-neutral-100"; + + if (platformCTA.action === "download") { + return ( + + {platformCTA.label} + + ); + } + + return ( + + {platformCTA.label} + + ); +} diff --git a/apps/web/src/routes/_view/index.tsx b/apps/web/src/routes/_view/index.tsx index 28e825d3c0..e1f2ed81f9 100644 --- a/apps/web/src/routes/_view/index.tsx +++ b/apps/web/src/routes/_view/index.tsx @@ -114,10 +114,7 @@ function Component() { }; return ( -
+
-
+
-
-

+
+

{heroContent.title}

@@ -274,7 +271,7 @@ function HeroSection({ e.preventDefault(); form.handleSubmit(); }} - className="w-full max-w-md" + className="w-full max-w-md text-left" >

) : ( -
+
{heroCTA.subtextLink ? (
- {!isChoosePage &&
} -
- -
+ {/* Mobile top bar spacer */} +
+ + {/* Sidebar + content in a centered container */} +
+ {!isChoosePage && } +
+ +
+
+ {!isChoosePage &&