ohn/components/header.tsx
2025-03-16 16:50:44 +07:00

447 lines
13 KiB
TypeScript

"use client";
import Image from "next/image";
import { ReactNode, useEffect, useRef, useState } from "react";
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
export default function Header(props: { fixed?: boolean }) {
const { fixed } = props;
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [scrolled, setScrolled] = useState(false);
const path = usePathname();
const router = useRouter();
const [showDropdown, setShowDropdown] = useState<number | null>(null);
const [dropDownData, setDropDownData] = useState<DropDownItem[] | null>(null);
const [outDropdownData, setOutDropdownData] = useState<DropDownItem[] | null>(
null
);
const headerContainerRef = useRef<HTMLDivElement>(null);
const triangleRef = useRef<SVGSVGElement>(null);
const [dropDownHeight, setDropDownHeight] = useState(0);
// This type describes the dropdown items and nav items.
type DropDownItem = {
name: string;
description: string;
image: string;
href: string;
};
type NavigationItem = {
content: ReactNode;
} & (
| { type: "link"; href: string }
| { type: "section"; id: string; page: string }
| { type: "button"; onClick: () => void }
| { type: "dropdown"; items: DropDownItem[] }
);
// Hard-coded navigation config.
const productsItems: DropDownItem[] = [
{
name: "CARE",
description:
"War ready HMIS software, empowering thousands of ICU beds across India. All built on open source.",
image: "/features/care-desktop.png",
href: "/care",
},
/*{
name: "Ayushma",
description:
"AI powered chatbot to assist doctors and nurses in managing patient care.",
image: "/features/care-desktop.png",
href: "/ayushma",
},
{
name: "Leaderboard",
description:
"Tracking the progress of open source contributors and rewarding them for their contributions.",
image: "/features/care-desktop.png",
href: "/leaderboard",
},*/
];
const communityItems: DropDownItem[] = [
{
name: "Github",
description: "Contribute to our open source projects on Github.",
image: "/dropdownicons/github.webp",
href: "https://git.ohn.foundation/OpenHealthcareNetwork",
},
{
name: "X",
description: "Join our X community to connect with other contributors.",
image: "/dropdownicons/x.jpeg",
href: "https://x.com/openshealthcare",
},
];
const navigation: NavigationItem[] = [
{ type: "dropdown", content: "Products", items: productsItems },
{ type: "dropdown", content: "Community", items: communityItems },
{ type: "link", content: "Supporters", href: "/supporters" },
{ type: "link", content: "Timeline", href: "/timeline" },
{ type: "section", content: "Contact", id: "contact", page: "/" },
{
type: "link",
content: (
<Image
alt="Github"
src={`/logos/github-mark-white.svg`}
width={50}
height={50}
className={`md:w-[25px] ${
scrolled ? "brightness-0" : ""
} transition-all`}
/>
),
href: "https://git.ohn.foundation/OpenHealthcareNetwork",
},
];
useEffect(() => {
const handleScroll = () => setScrolled(window.scrollY > 200);
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
useEffect(() => {
const nav = navigation[(showDropdown || 0) - 1];
const items = nav?.type === "dropdown" ? nav.items : [];
setOutDropdownData(dropDownData);
setDropDownData(items);
}, [showDropdown]);
useEffect(() => {
const onMouseMove = (e: MouseEvent) => {
const target = e.target as Element;
if (
!target.closest(".nav-button") &&
!target.closest(".nav-dropdown") &&
!target.closest("#dropdown-triangle")
) {
setShowDropdown(null);
}
};
window.addEventListener("mousemove", onMouseMove);
return () => window.removeEventListener("mousemove", onMouseMove);
}, []);
useEffect(() => {
setDropDownHeight(
document.querySelector(".dropdown-animate-in")?.clientHeight || 0
);
}, [dropDownData]);
function isActive(item: NavigationItem): boolean {
// If it's a link, we check if path === item.href.
if (item.type === "link") {
return path === item.href;
}
// If it's a section, we check if path === item.page.
if (item.type === "section") {
return false; // ignoring sections, or do path === item.page if needed.
}
// If it's a dropdown, we check if any of the sub-items have an href === path.
if (item.type === "dropdown") {
return item.items.some((subItem) => path === subItem.href);
}
return false;
}
function Dot({ active }: { active: boolean }) {
return (
<>
{/* Mobile Dot: 20px below */}
<span
className={`
absolute
block
md:hidden
left-[-30px]
top-1/2
-translate-y-1/2
w-5
h-5
rounded-full
transition
${active ? "opacity-100" : "opacity-0"}
`}
style={{ backgroundColor: "currentColor" }}
/>
{/* Desktop Dot: 12px below */}
<span
className={`
absolute
hidden
md:block
left-1/2
-translate-x-1/2
-bottom-[12px]
w-2
h-2
rounded-full
transition
${active ? "opacity-100" : "opacity-0"}
`}
style={{ backgroundColor: "currentColor" }}
/>
</>
);
}
const NavigationItemRender = (props: {
item: NavigationItem;
onHover?: (hoverstate: boolean, leftOffset: number) => void;
}) => {
const { item, onHover } = props;
const active = isActive(item);
const className = `relative font-black md:font-semibold ${
scrolled ? "md:hover:text-black/100" : "md:hover:text-white/100"
} transition-all px-3 flex items-center md:justify-center h-full`;
switch (item.type) {
case "dropdown": {
return (
<button
className={"nav-button " + className}
onMouseOver={() => {
onHover?.(true, 0);
}}
onMouseOut={() => {
onHover?.(false, 0);
}}
>
<span className="relative">
{item.content}
{active && <Dot active={true} />}
</span>
</button>
);
}
case "link": {
// If it's a link, we can just render a Next Link. Show dot if active.
return (
<Link href={item.href} className={className}>
<span className="relative">
{item.content}
{active && <Dot active={true} />}
</span>
</Link>
);
}
case "section": {
return (
<Link
href={item.page + "#" + item.id}
className={className}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (path === item.page) {
document
.getElementById(item.id)
?.scrollIntoView({ behavior: "smooth" });
} else {
router.push(item.page + "#" + item.id);
}
}}
>
{item.content}
</Link>
);
}
case "button": {
return (
<button className={className} onClick={item.onClick}>
{item.content}
</button>
);
}
}
};
const DropDownRender = (props: {
items: DropDownItem[];
className?: string;
}) => {
const { items, className } = props;
return (
<div
className={`flex items-stretch p-4 absolute top-0 inset-x-0 ${
className || ""
}`}
>
{items?.map((item, i) => (
<Link
href={item.href}
key={i}
className={`p-4 w-[200px] text-left rounded-lg ${
scrolled ? "hover:bg-black/5" : "hover:bg-black/10"
} transition-all flex flex-col gap-2`}
>
<Image
src={item.image}
alt={item.name}
width={200}
height={150}
className="rounded-lg"
/>
<div className="font-black text-sm">{item.name}</div>
<p
className={`text-xs ${
scrolled ? "text-black/80" : "text-white/80"
}`}
>
{item.description}
</p>
</Link>
))}
</div>
);
};
return (
<header
className={`fixed inset-x-0 top-0 z-50 transition-all ${
scrolled ? "text-black/80" : "text-white/80"
}`}
id="header"
>
{/* Background when scrolled. */}
<div
className={`absolute inset-x-0 h-full bg-white/70 backdrop-blur-xl -z-10 transition-all ${
scrolled ? "top-0" : "-top-full"
}`}
/>
{/* Main nav */}
<nav
className={`flex relative items-stretch justify-between transition-all px-4 md:px-6 lg:px-8`}
aria-label="Global"
>
{/* Left side: logo */}
<div
id="header-container"
ref={headerContainerRef}
className={`flex lg:flex-1 transition-all ${
scrolled ? "py-3" : "py-6"
}`}
>
<Link href="/" className="">
<span className="sr-only">Open Healthcare Network</span>
<Image
src="/ohc_logo_white.svg"
alt=""
width={125}
height={40}
className={`h-[46px] ${
scrolled ? "brightness-0" : ""
} transition-all`}
/>
</Link>
</div>
{/* Mobile menu button */}
<div className="flex md:hidden mr-4">
<button
type="button"
className="inline-flex items-center justify-center rounded-md p-2.5 text-gray-400"
onClick={() => setMobileMenuOpen(true)}
>
<span className="sr-only">Open main menu</span>
<Bars3Icon
color={scrolled ? "black" : "white"}
className="h-6 w-6"
aria-hidden="true"
/>
</button>
</div>
{/* The menu (mobile or desktop) */}
<div
className={`flex md:items-center p-6 md:p-0 fixed ${
mobileMenuOpen ? "right-0" : "right-[-100vw]"
} md:right-auto transition-all md:static ${
scrolled ? "bg-white/50" : "bg-black/50"
} pb-[300px] md:pb-0 md:bg-transparent backdrop-blur-lg md:backdrop-blur-none h-screen md:h-auto top-0 md:top-auto w-screen md:w-auto flex-col md:flex-row text-5xl md:text-base`}
>
{/* Mobile menu close button */}
<button
className="md:hidden block absolute top-6 right-8 z-50"
onClick={() => setMobileMenuOpen(false)}
>
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
</button>
{/* Render each navigation item */}
{navigation.map((item, i) => (
<NavigationItemRender
item={item}
key={i}
onHover={(hoverstate, leftOffset) => {
if (hoverstate) {
setShowDropdown(i + 1);
if (triangleRef.current) {
triangleRef.current.style.setProperty(
"left",
`${leftOffset - (triangleRef.current.clientWidth / 2)}px`
);
}
}
}}
/>
))}
</div>
{/* The little triangle shown below dropdown if needed */}
<svg
id="dropdown-triangle"
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="black"
className={`hidden md:block absolute bottom-[-1px] ${
!!showDropdown
? scrolled
? "opacity-5"
: "opacity-20"
: "opacity-0"
} transition-all`}
ref={triangleRef}
>
<path d="M24 22h-24l12-20z" />
</svg>
</nav>
{/* The dropdown container for subitems (Products, Community, etc.) */}
<div
className={`nav-dropdown ${scrolled ? "bg-black/5" : "bg-black/20"} ${
scrolled ? "" : "backdrop-blur md:rounded-xl md:mx-10"
} transition-all overflow-hidden fixed bottom-0 md:bottom-auto inset-x-0 md:inset-x-auto md:relative ${
!!showDropdown ? "max-h-[400px]" : "max-h-0"
}`}
style={{ height: dropDownHeight }}
>
<DropDownRender
items={dropDownData || []}
className="dropdown-animate-in"
/>
<DropDownRender
items={outDropdownData || []}
className="dropdown-animate-out"
/>
</div>
</header>
);
}