"use client";
import { cn } from "@/lib/utils";
import {
IconMenu2,
IconX,
IconChevronDown,
IconLogout,
IconUser,
} from "@tabler/icons-react";
import {
motion,
AnimatePresence,
useScroll,
useMotionValueEvent,
} from "motion/react";
import React, { RefObject, useRef, useState } from "react";
import { Cmd } from "../commandMenu";
import { DevExLogoDark } from "../icons/logo";
interface NavbarProps {
children: React.ReactNode;
className?: string;
visible: boolean;
ref: RefObject<HTMLDivElement | null>;
}
interface NavBodyProps {
children: React.ReactNode;
className?: string;
visible?: boolean;
}
interface NavItemsProps {
items: {
name: string;
link: string;
}[];
className?: string;
onItemClick?: () => void;
}
interface MobileNavProps {
children: React.ReactNode;
className?: string;
visible?: boolean;
}
interface MobileNavHeaderProps {
children: React.ReactNode;
className?: string;
}
interface MobileNavMenuProps {
children: React.ReactNode;
className?: string;
isOpen: boolean;
onClose: () => void;
}
interface UserProfileDropdownProps {
user: {
id: number;
login: string;
name: string;
email: string;
avatar_url: string;
created_at: string;
};
onLogout: () => void;
className?: string;
visible: boolean;
}
interface MobileUserProfileProps {
user: {
id: number;
login: string;
name: string;
email: string;
avatar_url: string;
created_at: string;
};
onLogout: () => void;
onClose: () => void;
className?: string;
}
export const Navbar = ({ children, className, visible, ref }: NavbarProps) => {
return (
<motion.div
ref={ref}
// IMPORTANT: Change this to class of `fixed` if you want the navbar to be fixed
// className={cn("sticky inset-x-0 top-20 z-40 w-full", className)}
className={cn("fixed inset-x-0 z-40 w-full", className)}
>
{React.Children.map(children, (child) =>
React.isValidElement(child)
? React.cloneElement(
child as React.ReactElement<{ visible?: boolean }>,
{ visible },
)
: child,
)}
</motion.div>
);
};
export const NavBody = ({ children, className, visible }: NavBodyProps) => {
return (
<motion.div
animate={{
backdropFilter: visible ? "blur(10px)" : "blur(2px)",
boxShadow: visible
? "0 0 24px rgba(34, 42, 53, 0.06), 0 1px 1px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(34, 42, 53, 0.04), 0 0 4px rgba(34, 42, 53, 0.08), 0 16px 68px rgba(47, 48, 55, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1) inset"
: "none",
width: visible ? "40%" : "100%",
y: visible ? 20 : 0,
}}
transition={{
type: "spring",
stiffness: 200,
damping: 50,
}}
style={{
minWidth: "800px",
maxWidth: "100%",
}}
className={cn(
"relative z-[60] mx-auto hidden w-full max-w-7xl flex-row items-center justify-between self-start bg-transparent px-32 py-2 lg:flex dark:bg-transparent",
visible && "bg-white/80 dark:bg-neutral-950/80 rounded-full px-4",
className,
)}
>
{children}
</motion.div>
);
};
export const NavItems = ({ items, className, onItemClick }: NavItemsProps) => {
const [hovered, setHovered] = useState<number | null>(null);
return (
<motion.div
onMouseLeave={() => setHovered(null)}
className={cn(
"absolute inset-0 hidden flex-1 flex-row items-center justify-center gap-2 text-sm font-medium text-zinc-600 transition duration-200 hover:text-zinc-800 lg:flex lg:gap-0",
className,
)}
>
{items.map((item, idx) => (
<a
onMouseEnter={() => setHovered(idx)}
onClick={onItemClick}
className="relative px-4 py-2 text-neutral-600 dark:text-neutral-300"
key={`link-${idx}`}
href={item.link}
>
{hovered === idx && (
<motion.div
layoutId="hovered"
className="absolute inset-0 h-full w-full rounded-full bg-gray-100 dark:bg-neutral-800"
/>
)}
<span className="relative z-20">{item.name}</span>
</a>
))}
<Cmd />
</motion.div>
);
};
export const UserProfileDropdown = ({
user,
onLogout,
className,
visible,
}: UserProfileDropdownProps) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div className={cn("relative", className)}>
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2 rounded-full p-1 hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors duration-200"
>
<img
src={user.avatar_url}
alt={user.name.split("").slice(0, 3).join("")}
className="h-8 w-8 rounded-full object-cover"
/>
{!visible && (
<span className="hidden sm:block text-sm font-medium text-neutral-700 dark:text-neutral-300">
{user.login}
</span>
)}
<IconChevronDown className="h-4 w-4 text-neutral-500 dark:text-neutral-400" />
</button>
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-[70]"
onClick={() => setIsOpen(false)}
/>
{/* Dropdown Menu */}
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
className="absolute right-0 top-full mt-2 w-48 z-[80] rounded-lg bg-white dark:bg-neutral-900 shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset] border border-gray-200 dark:border-neutral-700"
>
<div className="p-3 border-b border-gray-200 dark:border-neutral-700">
<div className="flex items-center gap-2">
<img
src={user.avatar_url}
alt={user.name || user.login}
className="h-8 w-8 rounded-full object-cover"
/>
<div>
<div className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
{user.name || user.login}
</div>
<div className="text-xs text-neutral-500 dark:text-neutral-400">
@{user.login}
</div>
</div>
</div>
</div>
<div className="py-2">
<a
href="https://parthkapoor.me"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-3 py-2 text-sm text-neutral-700 dark:text-neutral-300 hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors duration-200"
onClick={() => setIsOpen(false)}
>
<IconUser className="h-4 w-4" />
Visit Developer
</a>
<button
onClick={() => {
setIsOpen(false);
onLogout();
}}
className="flex w-full items-center gap-2 px-3 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors duration-200"
>
<IconLogout className="h-4 w-4" />
Logout
</button>
</div>
</motion.div>
</>
)}
</AnimatePresence>
</div>
);
};
export const MobileUserProfile = ({
user,
onLogout,
onClose,
className,
}: MobileUserProfileProps) => {
return (
<div className={cn("w-full", className)}>
<div className="flex items-center gap-3 p-3 border-b border-gray-200 dark:border-neutral-700">
<img
src={user.avatar_url}
alt={user.name || user.login}
className="h-10 w-10 rounded-full object-cover"
/>
<div>
<div className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
{user.name || user.login}
</div>
<div className="text-xs text-neutral-500 dark:text-neutral-400">
@{user.login}
</div>
</div>
</div>
<div className="py-2">
<a
href="https://parthkapoor.me"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-3 py-2 text-sm text-neutral-700 dark:text-neutral-300 hover:bg-gray-100 dark:hover:bg-neutral-800 transition-colors duration-200"
onClick={onClose}
>
<IconUser className="h-4 w-4" />
Visit Developer
</a>
<button
onClick={() => {
onClose();
onLogout();
}}
className="flex w-full items-center gap-2 px-3 py-2 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors duration-200"
>
<IconLogout className="h-4 w-4" />
Logout
</button>
</div>
</div>
);
};
export const MobileNav = ({ children, className, visible }: MobileNavProps) => {
return (
<motion.div
animate={{
backdropFilter: visible ? "blur(10px)" : "none",
boxShadow: visible
? "0 0 24px rgba(34, 42, 53, 0.06), 0 1px 1px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(34, 42, 53, 0.04), 0 0 4px rgba(34, 42, 53, 0.08), 0 16px 68px rgba(47, 48, 55, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1) inset"
: "none",
width: visible ? "90%" : "100%",
paddingRight: visible ? "12px" : "0px",
paddingLeft: visible ? "12px" : "0px",
borderRadius: visible ? "4px" : "2rem",
y: visible ? 20 : 0,
}}
transition={{
type: "spring",
stiffness: 200,
damping: 50,
}}
className={cn(
"relative z-50 mx-auto flex w-full max-w-[calc(100vw-2rem)] flex-col items-center justify-between bg-transparent px-0 py-2 lg:hidden",
visible && "bg-white/80 dark:bg-neutral-950/80",
className,
)}
>
{children}
</motion.div>
);
};
export const MobileNavHeader = ({
children,
className,
}: MobileNavHeaderProps) => {
return (
<div
className={cn(
"flex w-full flex-row items-center justify-between",
className,
)}
>
{children}
</div>
);
};
export const MobileNavMenu = ({
children,
className,
isOpen,
onClose,
}: MobileNavMenuProps) => {
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className={cn(
"absolute inset-x-0 top-16 z-50 flex w-full flex-col items-start justify-start gap-4 rounded-lg bg-white px-4 py-8 shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset] dark:bg-neutral-950",
className,
)}
>
{children}
</motion.div>
)}
</AnimatePresence>
);
};
export const MobileNavToggle = ({
isOpen,
onClick,
}: {
isOpen: boolean;
onClick: () => void;
}) => {
return isOpen ? (
<IconX
className="text-black dark:text-white cursor-pointer"
onClick={onClick}
/>
) : (
<IconMenu2
className="text-black dark:text-white cursor-pointer"
onClick={onClick}
/>
);
};
export const NavbarLogo = () => {
return (
<a
href="/"
className="relative z-20 mr-4 flex items-center gap-2 px-2 py-1 text-sm font-normal text-black"
>
<DevExLogoDark />
<span className="font-medium text-lg text-black dark:text-white">
devX
</span>
</a>
);
};
export const NavbarButton = ({
href,
as: Tag = "a",
children,
className,
variant = "primary",
...props
}: {
href?: string;
as?: React.ElementType;
children: React.ReactNode;
className?: string;
variant?: "primary" | "secondary" | "dark" | "gradient";
} & (
| React.ComponentPropsWithoutRef<"a">
| React.ComponentPropsWithoutRef<"button">
)) => {
const baseStyles =
"px-4 py-2 rounded-md bg-white button bg-white text-black text-sm font-bold relative cursor-pointer hover:-translate-y-0.5 transition duration-200 inline-block text-center";
const variantStyles = {
primary:
"shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset]",
secondary: "bg-transparent shadow-none dark:text-white",
dark: "bg-black text-white shadow-[0_0_24px_rgba(34,_42,_53,_0.06),_0_1px_1px_rgba(0,_0,_0,_0.05),_0_0_0_1px_rgba(34,_42,_53,_0.04),_0_0_4px_rgba(34,_42,_53,_0.08),_0_16px_68px_rgba(47,_48,_55,_0.05),_0_1px_0_rgba(255,_255,_255,_0.1)_inset]",
gradient:
"bg-gradient-to-b from-blue-500 to-blue-700 text-white shadow-[0px_2px_0px_0px_rgba(255,255,255,0.3)_inset]",
};
return (
<Tag
href={href || undefined}
className={cn(baseStyles, variantStyles[variant], className)}
{...props}
>
{children}
</Tag>
);
};