Commit 780033b0 authored by Lê Bảo Hồng Đức's avatar Lê Bảo Hồng Đức

Merge branch 'fix/header' into 'develop-news'

fix header

See merge request !56
parents 67f22283 b6c2eab7
...@@ -57,7 +57,7 @@ function FeaturedNews() { ...@@ -57,7 +57,7 @@ function FeaturedNews() {
{primaryItem.categories[0]?.name || "Tin nổi bật"} {primaryItem.categories[0]?.name || "Tin nổi bật"}
</span> </span>
<h3 className="max-w-3xl text-[20px] font-bold leading-[1.28] text-white md:text-[28px] xl:text-[32px]"> <h3 className="max-w-3xl line-clamp-3 text-[20px] font-bold leading-[1.28] text-white md:text-[28px] xl:text-[32px]">
{primaryItem.title} {primaryItem.title}
</h3> </h3>
...@@ -70,8 +70,8 @@ function FeaturedNews() { ...@@ -70,8 +70,8 @@ function FeaturedNews() {
</div> </div>
</Link> </Link>
) : ( ) : (
<div className="relative min-h-[260px] overflow-hidden rounded-[24px] bg-[#e9eef8] shadow-[0_18px_38px_rgba(28,52,120,0.12)] md:min-h-[320px] xl:min-h-[350px]"> <div className="relative min-h-[260px] overflow-hidden rounded-3xl bg-[#e9eef8] shadow-[0_18px_38px_rgba(28,52,120,0.12)] md:min-h-[320px] xl:min-h-[350px]">
<div className="flex h-full min-h-[260px] flex-col justify-end p-4 md:min-h-[320px] md:p-5 xl:min-h-[350px]"> <div className="flex h-full min-h-[260px] flex-col justify-end p-4 md:min-h-80 md:p-5 xl:min-h-[350px]">
<span className="mb-2 h-8 w-28 rounded-[10px] bg-white/80" /> <span className="mb-2 h-8 w-28 rounded-[10px] bg-white/80" />
<div className="h-8 w-3/4 rounded bg-white/90 md:h-10" /> <div className="h-8 w-3/4 rounded bg-white/90 md:h-10" />
<div className="mt-2 h-5 w-28 rounded bg-white/70" /> <div className="mt-2 h-5 w-28 rounded bg-white/70" />
......
...@@ -93,7 +93,7 @@ export default function ArticlePage({ category, allCategories }: ArticlePageProp ...@@ -93,7 +93,7 @@ export default function ArticlePage({ category, allCategories }: ArticlePageProp
{categoryMenu.length > 0 ? <ListCategory categories={categoryMenu} /> : <br />} {categoryMenu.length > 0 ? <ListCategory categories={categoryMenu} /> : <br />}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<main className="lg:col-span-2 bg-background"> <main className="lg:col-span-2 bg-white">
<div className="pb-5 overflow-hidden"> <div className="pb-5 overflow-hidden">
{paginatedPosts.length ? ( {paginatedPosts.length ? (
paginatedPosts.map((item) => { paginatedPosts.map((item) => {
......
"use client"; "use client";
import React, { useState } from "react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { Menu, X, Facebook, Linkedin, Twitter, Youtube } from "lucide-react"; import { Facebook, Linkedin, Menu, Twitter, X, Youtube } from "lucide-react";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import logo from "@/assets/VCCI-HCM-logo-VN-2025.png";
import Image from "next/image"; import Image from "next/image";
import MenuItem from "@/components/base/menu-item";
import Link from "next/link"; import Link from "next/link";
import logo from "@/assets/VCCI-HCM-logo-VN-2025.png";
import MenuItem from "@/components/base/menu-item";
import { useCustomClient } from "@/api/mutator/custom-client"; import { useCustomClient } from "@/api/mutator/custom-client";
import type { Category } from "@/api/models/category"; import type { Category } from "@/api/models/category";
import { getCategoryFallbackResponse } from "@/mockdata/categories"; import { getCategoryFallbackResponse } from "@/mockdata/categories";
...@@ -81,155 +82,225 @@ function buildHeaderMenuTree(rows?: Category[]) { ...@@ -81,155 +82,225 @@ function buildHeaderMenuTree(rows?: Category[]) {
} }
function Header() { function Header() {
const [toggleMenu, setToggleMenu] = useState<boolean>(false); const [toggleMenu, setToggleMenu] = useState(false);
const [isTopBarHidden, setIsTopBarHidden] = useState(false);
const router = useRouter(); const router = useRouter();
const handleDesktopMenuWheel = useCallback(
(event: React.WheelEvent<HTMLElement>) => {
const element = event.currentTarget;
if (element.scrollWidth <= element.clientWidth) return;
if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) return;
event.preventDefault();
element.scrollLeft += event.deltaY;
},
[],
);
const { data: categoriesResponse } = useQuery({ const { data: categoriesResponse } = useQuery({
queryKey: ["header-categories"], queryKey: ["header-categories"],
queryFn: () => queryFn: () =>
useCustomClient<CategoryListResponse>( useCustomClient<CategoryListResponse>(
"/category?page=1&pageSize=200&sortField=sort_order&sortOrder=ASC", "/category?page=1&pageSize=200&sortField=sort_order&sortOrder=ASC",
).catch(() => getCategoryFallbackResponse()), ).catch(() => getCategoryFallbackResponse()),
staleTime: 5 * 60 * 1000,
}); });
const menuItems = React.useMemo( const menuItems = useMemo(
() => buildHeaderMenuTree(categoriesResponse?.responseData?.rows), () => buildHeaderMenuTree(categoriesResponse?.responseData?.rows),
[categoriesResponse?.responseData?.rows], [categoriesResponse?.responseData?.rows],
); );
useEffect(() => {
const handleScroll = () => {
setIsTopBarHidden(window.scrollY > 0);
};
handleScroll();
window.addEventListener("scroll", handleScroll, { passive: true });
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
return ( return (
<> <header className="sticky top-0 z-50 shadow-[0_1px_0_rgba(15,23,42,0.05)]">
<div className="sticky top-0 w-full h-14 hidden lg:flex items-center justify-center bg-[#063e8e]"> <div
<div className="container w-full px-4 flex items-center justify-between"> className={`hidden w-full items-center justify-center overflow-hidden bg-[#25439a] ${
<div className="flex items-center gap-3"> isTopBarHidden ? "lg:hidden" : "h-10 lg:flex"
<div className="w-35 h-9 bg-[#e8c518] flex items-center justify-center border-3 rounded-sm border-[#647792]"> }`}
>
<div className="mx-auto flex h-full w-full max-w-[1460px] items-center justify-between gap-6 px-6 xl:px-8">
<div className="flex items-center gap-2">
<div className="flex h-7 items-center justify-center rounded-[4px] bg-[#f2b500] px-4 shadow-[inset_0_-1px_0_rgba(0,0,0,0.15)]">
<Link <Link
className="font-bold text-[14px] text-primary hover:text-white transition" className="text-[13px] font-semibold leading-none text-[#15357a] transition hover:opacity-85"
href="https://vccihcm.vn/dang-ky" href="https://vccihcm.vn/dang-ky"
> >
Đăng Ký Hội Viên {"\u0110\u0103ng K\u00fd H\u1ed9i Vi\u00ean"}
</Link> </Link>
</div> </div>
<Link <Link
className="px-3 py-2 text-[14px] text-white hover:opacity-80" className="px-3 py-1 text-[13px] font-medium text-white transition hover:opacity-80"
href="/site-map" href="/site-map"
> >
Sitemap Sitemap
</Link> </Link>
<Link <Link
className="px-3 py-2 text-[14px] text-white hover:opacity-80" className="px-3 py-1 text-[13px] font-medium text-white transition hover:opacity-80"
href="https://vccihcm.vn/lien-he" href="https://vccihcm.vn/lien-he"
> >
Liên hệ {"Li\u00ean h\u1ec7"}
</Link> </Link>
</div> </div>
<div className="flex items-center gap-8"> <div className="flex items-center gap-4">
<input <input
className="bg-white h-10 rounded-sm outline-none px-4 w-64 placeholder:text-sm" className="h-[28px] w-[176px] rounded-[4px] border border-[#3a57b4] bg-[#3554b7] px-3 text-[13px] text-white outline-none placeholder:text-[13px] placeholder:text-[#b5c4ff]"
type="text" type="text"
placeholder="Tìm kiếm" placeholder={"T\u00ecm ki\u1ebfm"}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") { if (e.key === "Enter") {
const value = const value = (e.currentTarget as HTMLInputElement).value || "";
(e.currentTarget as HTMLInputElement).value || "";
const encoded = encodeURIComponent(value); const encoded = encodeURIComponent(value);
router.push(`/search?q=${encoded}&page=1`); router.push(`/search?q=${encoded}&page=1`);
} }
}} }}
/> />
<div className="flex gap-2">
<div className="flex items-center gap-2">
<a <a
href="https://www.facebook.com/VCCIHCMC/" href="https://www.facebook.com/VCCIHCMC/"
target="_blank" target="_blank"
className="bg-white size-7 rounded-full flex items-center justify-center text-[#063e8e] hover:opacity-80 transition" rel="noreferrer"
className="flex size-[22px] items-center justify-center rounded-full bg-white text-[#2f57ff] transition hover:opacity-80"
> >
<Facebook size={16} /> <Facebook size={12} fill="currentColor" />
</a> </a>
<a <a
href="https://twitter.com/VCCI_HCM" href="https://twitter.com/VCCI_HCM"
target="_blank" target="_blank"
className="bg-white size-7 rounded-full flex items-center justify-center text-[#063e8e] hover:opacity-80 transition" rel="noreferrer"
className="flex size-[22px] items-center justify-center rounded-full bg-white text-[#2f57ff] transition hover:opacity-80"
> >
<Twitter size={16} /> <Twitter size={12} fill="currentColor" />
</a> </a>
<a <a
href="https://www.youtube.com/user/VCCIHCMC" href="https://www.youtube.com/user/VCCIHCMC"
target="_blank" target="_blank"
className="bg-white size-7 rounded-full flex items-center justify-center text-[#063e8e] hover:opacity-80 transition" rel="noreferrer"
className="flex size-[22px] items-center justify-center rounded-full bg-white text-[#2f57ff] transition hover:opacity-80"
> >
<Youtube size={16} /> <Youtube size={12} fill="currentColor" />
</a> </a>
<a <a
href="https://www.linkedin.com/company/vietnam-chamber-of-commerce-and-industry-ho-chi-minh-city-branch-vcci-hcm-?trk=biz-companies-cym" href="https://www.linkedin.com/company/vietnam-chamber-of-commerce-and-industry-ho-chi-minh-city-branch-vcci-hcm-?trk=biz-companies-cym"
target="_blank" target="_blank"
className="bg-white size-7 rounded-full flex items-center justify-center text-[#063e8e] hover:opacity-80 transition" rel="noreferrer"
className="flex size-[22px] items-center justify-center rounded-full bg-white text-[#2f57ff] transition hover:opacity-80"
> >
<Linkedin size={16} /> <Linkedin size={12} fill="currentColor" />
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div className="sticky top-0 z-50 bg-[#ededed] shadow-md py-2"> <div className="border-b border-slate-200 bg-white">
<div className="container m-auto"> <div className="mx-auto flex h-[80px] w-full max-w-[1460px] items-center justify-between gap-10 px-6 xl:px-8">
<div className="w-full flex justify-between items-center"> <Link href="/" className="flex w-[136px] shrink-0 items-center xl:w-[152px]">
{/* Logo */}
<Link href="/">
<Image <Image
className="w-[140px] object-contain" className="h-auto w-[108px] object-contain"
src={logo} src={logo}
alt="VCCI-HCM" alt="VCCI-HCM"
priority
/> />
</Link> </Link>
{/* Desktop Menu */} <div className="hidden min-w-0 flex-1 justify-end pl-6 lg:flex xl:pl-10">
<nav className="hidden lg:flex items-center"> <nav
className="header-menu-scroll min-w-0 max-w-full overflow-x-auto overflow-y-hidden"
onWheel={handleDesktopMenuWheel}
>
<div className="flex w-max min-w-full items-center justify-end gap-4 whitespace-nowrap pr-1 xl:gap-6">
{menuItems.map((category) => ( {menuItems.map((category) => (
<MenuItem <MenuItem
key={category.id} key={category.id}
title={category.name} title={category.name}
link={category.url} link={category.url}
items={[ items={category.children.map((child) => ({
...category.children.map((child) => ({
title: child.name, title: child.name,
link: child.url, link: child.url,
})), }))}
]}
/> />
))} ))}
</div>
</nav> </nav>
</div>
{/* Mobile Button */}
<button <button
onClick={() => setToggleMenu((prev) => !prev)} onClick={() => setToggleMenu((prev) => !prev)}
className="lg:hidden h-10 p-2 bg-[#063e8e] text-white rounded-sm mr-5" className="inline-flex h-9 w-9 items-center justify-center rounded-md border border-slate-300 bg-white text-[#163b73] transition hover:bg-slate-50 lg:hidden"
aria-label={"M\u1edf menu"}
> >
{toggleMenu ? <X size={20} /> : <Menu size={20} />} {toggleMenu ? <X size={18} /> : <Menu size={18} />}
</button> </button>
</div> </div>
</div> </div>
{/* Mobile Menu */}
<div <div
className={`lg:hidden bg-white shadow-lg transition-all duration-300 overflow-hidden ${toggleMenu ? "max-h-[500px] opacity-100" : "max-h-0 opacity-0" className={`overflow-hidden border-t border-slate-200 bg-white transition-all duration-300 lg:hidden ${
toggleMenu ? "max-h-[520px] opacity-100" : "max-h-0 opacity-0"
}`} }`}
> >
<div className="px-4 py-3">
<input
className="h-11 w-full rounded-md border border-slate-200 px-4 text-sm outline-none placeholder:text-slate-400 focus:border-[#2f57ff]"
type="text"
placeholder={"T\u00ecm ki\u1ebfm"}
onKeyDown={(e) => {
if (e.key === "Enter") {
const value = (e.currentTarget as HTMLInputElement).value || "";
const encoded = encodeURIComponent(value);
router.push(`/search?q=${encoded}&page=1`);
setToggleMenu(false);
}
}}
/>
</div>
<div className="pb-3">
{menuItems.map((category) => ( {menuItems.map((category) => (
<div key={category.id} className="border-b border-gray-200"> <div key={category.id} className="border-t border-slate-100 first:border-t-0">
<Link <Link
href={category.url || "#"} href={category.url || "#"}
className="block py-3 text-center hover:bg-[#124588] hover:text-white text-[16px] font-medium" className="block px-5 py-3 text-[15px] font-medium text-slate-700 transition hover:bg-slate-50 hover:text-[#2f57ff]"
onClick={() => setToggleMenu(false)} onClick={() => setToggleMenu(false)}
> >
{category.name} {category.name}
</Link> </Link>
{category.children.length > 0 ? (
<div className="pb-2 pl-8 pr-5">
{category.children.map((child) => (
<Link
key={child.id}
href={child.url || "#"}
className="block py-2 text-sm text-slate-500 transition hover:text-[#2f57ff]"
onClick={() => setToggleMenu(false)}
>
{child.name}
</Link>
))}
</div>
) : null}
</div> </div>
))} ))}
</div> </div>
</div> </div>
</> </header>
); );
} }
......
...@@ -9,7 +9,7 @@ export default function Layout({ ...@@ -9,7 +9,7 @@ export default function Layout({
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<main className="flex flex-col min-h-screen bg-background"> <main className="flex flex-col min-h-screen bg-white">
<Header /> <Header />
<div className="flex-1">{children}</div> <div className="flex-1">{children}</div>
<ScrollToTopButton /> <ScrollToTopButton />
......
...@@ -22,7 +22,7 @@ const ListCategory: React.FC<{ categories?: Category[] }> = ({ categories = [] } ...@@ -22,7 +22,7 @@ const ListCategory: React.FC<{ categories?: Category[] }> = ({ categories = [] }
<div className="border-t border-gray-200 bg-white py-2"> <div className="border-t border-gray-200 bg-white py-2">
<div className="w-full px-4 sm:px-6 lg:px-8"> <div className="w-full px-4 sm:px-6 lg:px-8">
<div className="py-3"> <div className="py-3">
<div className="flex flex-wrap items-center max-w-full overflow-x-auto"> <div className="flex max-w-full items-center gap-3 overflow-x-auto pb-1">
{categories.map((category) => { {categories.map((category) => {
const href = resolveHref(category); const href = resolveHref(category);
const menu = { id: category.id, name: category.name, link: href }; const menu = { id: category.id, name: category.name, link: href };
...@@ -30,7 +30,7 @@ const ListCategory: React.FC<{ categories?: Category[] }> = ({ categories = [] } ...@@ -30,7 +30,7 @@ const ListCategory: React.FC<{ categories?: Category[] }> = ({ categories = [] }
return ( return (
<div key={category.id} className="shrink-0"> <div key={category.id} className="shrink-0">
<MenuItem menu={menu} active={active} /> <MenuItem menu={menu} active={active} variant="secondary" />
</div> </div>
); );
})} })}
......
'use client' 'use client'
type Menu = { type Menu = {
id: string | number id: string | number
name: string name: string
...@@ -7,19 +8,22 @@ type Menu = { ...@@ -7,19 +8,22 @@ type Menu = {
} }
import { buttonVariants } from '@components/ui/button' import { buttonVariants } from '@components/ui/button'
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@components/ui/hover-card'
import { cn } from '@lib/utils' import { cn } from '@lib/utils'
import { useCallback, useMemo } from 'react'
import { HoverCard, HoverCardTrigger, HoverCardContent } from '@components/ui/hover-card'
import { cva } from 'class-variance-authority'
import { usePathname } from 'next/navigation'
import Link from 'next/link' import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useCallback, useMemo } from 'react'
export function MenuItem(props: { variant?: 'main' | 'secondary'; menu: Menu; active?: boolean }) { export function MenuItem(props: { variant?: 'main' | 'secondary'; menu: Menu; active?: boolean }) {
const { menu, variant = 'main', active } = props const { menu, variant = 'main', active } = props
const pathname = usePathname() const pathname = usePathname()
const isActive = pathname.startsWith(menu.link ?? ''); const normalizedLink = menu.link && menu.link !== '#' ? menu.link : '/'
const hasChildren = Boolean(menu.children?.length)
const isRoot = normalizedLink === '/'
const isActive = active || (isRoot ? pathname === '/' : pathname.startsWith(normalizedLink))
const linkId = useMemo(() => `trigger_${menu.id}`, [menu.id]) const linkId = useMemo(() => `trigger_${menu.id}`, [menu.id])
const hoverCardRef = useCallback( const hoverCardRef = useCallback(
(element: HTMLDivElement) => { (element: HTMLDivElement) => {
if (!element) return if (!element) return
...@@ -28,76 +32,67 @@ export function MenuItem(props: { variant?: 'main' | 'secondary'; menu: Menu; ac ...@@ -28,76 +32,67 @@ export function MenuItem(props: { variant?: 'main' | 'secondary'; menu: Menu; ac
[linkId] [linkId]
) )
return ( const trigger = (
<HoverCard openDelay={0} closeDelay={0}>
<HoverCardTrigger asChild>
<Link <Link
aria-selected={active || isActive} aria-selected={isActive}
id={linkId} id={linkId}
target={(menu.link ?? '').startsWith('/') ? '_self' : '_blank'} target={normalizedLink.startsWith('/') ? '_self' : '_blank'}
href={menu.link ?? '/'} href={normalizedLink}
className={menuItemTriggerVariant({ variant })} className={menuItemTriggerClass(variant)}
> >
{menu.name} <span className="relative z-10 truncate">{menu.name}</span>
{variant === 'main' ? <span className="menu-item-underline" aria-hidden="true" /> : null}
</Link> </Link>
</HoverCardTrigger> )
if (!hasChildren) {
return trigger
}
{menu.children && ( return (
<HoverCardContent ref={hoverCardRef} className={menuItemHoverBoxVariant({ variant })}> <HoverCard openDelay={80} closeDelay={120}>
{menu.children.map((subMenu) => ( <HoverCardTrigger asChild>{trigger}</HoverCardTrigger>
<Link key={subMenu.id} href={subMenu.link ?? '/'} className={menuItemChildVariant({ variant })}> <HoverCardContent ref={hoverCardRef} className={menuItemHoverBoxVariant(variant)}>
{menu.children?.map((subMenu) => (
<Link key={subMenu.id} href={subMenu.link ?? '/'} className={menuItemChildVariant(variant)}>
{subMenu.name} {subMenu.name}
</Link> </Link>
))} ))}
</HoverCardContent> </HoverCardContent>
)}
</HoverCard> </HoverCard>
) )
} }
const menuItemTriggerVariant = cva( function menuItemTriggerClass(variant: 'main' | 'secondary') {
cn(buttonVariants({ variant: 'ghost' }), 'font-semibold focus-visible:ring-0 focus-visible:ring-offset-0 py-'), if (variant === 'secondary') {
{ return cn(
variants: { 'inline-flex h-[36px] items-center justify-center rounded-full border border-[#d6dfeb] bg-white px-5 text-[13px] font-medium leading-none text-[#5f6b7d] shadow-none transition-colors duration-150',
variant: { 'hover:border-[#c5d2e3] hover:bg-[#f7faff] hover:text-[#1b5aa1]',
main: cn( 'aria-selected:border-[#16559d] aria-selected:bg-[#16559d] aria-selected:text-white',
'font-semibold text-[#363636] text-2xl hover:text-muted-foreground hover:bg-white py-3.5 px-5', 'focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0'
'aria-selected:text-muted-foreground'
),
secondary: cn(
'font-boldtext-primary border-t-2 border-t-transparent rounded-none',
'hover:text-primary/90',
'aria-selected:border-t-secondary aria-selected:bg-accent',
'aria-selected:bg-[#E9C826]'
) )
} }
},
defaultVariants: {
variant: 'main'
}
}
)
const menuItemHoverBoxVariant = cva('flex w-full flex-col gap-2 p-0', { return cn(
variants: { buttonVariants({ variant: 'ghost' }),
variant: { 'group relative inline-flex h-[60px] rounded-none border-b-2 border-transparent px-3 py-0 text-[15px] font-semibold text-slate-700 shadow-none transition-colors duration-150',
main: 'bg-secondary', 'hover:bg-transparent hover:text-[#2f57ff]',
secondary: 'bg-muted ' 'aria-selected:bg-transparent aria-selected:text-[#2f57ff]',
} 'focus-visible:ring-0 focus-visible:ring-offset-0 xl:px-4'
}, )
defaultVariants: { }
variant: 'main'
}
})
const menuItemChildVariant = cva(cn(buttonVariants({ variant: 'ghost' }), 'justify-start'), { function menuItemHoverBoxVariant(variant: 'main' | 'secondary') {
variants: { return cn(
variant: { 'mt-1 flex w-full min-w-[220px] flex-col gap-1 rounded-md border border-slate-200 bg-white p-2 shadow-[0_12px_30px_rgba(15,23,42,0.12)]',
main: 'text-secondary-foreground hover:text-muted-foreground hover:bg-secondary', variant === 'secondary' ? 'bg-white' : ''
secondary: 'text-accent-foreground hover:text-primary/90 ' )
} }
},
defaultVariants: { function menuItemChildVariant(_variant: 'main' | 'secondary') {
variant: 'main' return cn(
} buttonVariants({ variant: 'ghost' }),
}) 'h-10 justify-start rounded-md px-3 text-sm font-medium text-slate-600 transition-colors',
'hover:bg-slate-50 hover:text-[#2f57ff]'
)
}
import { usePathname } from "next/navigation"; 'use client'
import Link from "next/link";
import { buttonVariants } from '@components/ui/button'
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@components/ui/hover-card'
import { cn } from '@lib/utils'
import { cva } from 'class-variance-authority'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useCallback, useMemo } from 'react'
type MenuItemProps = { type MenuItemProps = {
title: string; title: string
link?: string; link?: string
items: { title: string; link: string }[]; items: { title: string; link: string }[]
}; }
const MenuItem = ({ title, link, items }: MenuItemProps) => { const MenuItem = ({ title, link, items }: MenuItemProps) => {
const pathname = usePathname(); const pathname = usePathname()
const isActive = !!link && (pathname === link || (link !== "/" && pathname.startsWith(link))); const normalizedLink = link && link !== '#' ? link : '/'
const hasChildren = items.length > 0
const isRoot = normalizedLink === '/'
const isActive = isRoot ? pathname === '/' : pathname === normalizedLink || pathname.startsWith(normalizedLink)
const linkId = useMemo(() => `header-trigger-${title}`, [title])
return ( const hoverCardRef = useCallback(
<div className="group relative"> (element: HTMLDivElement) => {
if (!element) return
const triggerWidth = document.getElementById(linkId)?.offsetWidth ?? 220
element.style.minWidth = `${Math.max(triggerWidth, 320)}px`
element.style.maxWidth = '420px'
},
[linkId]
)
const trigger = (
<Link <Link
href={link ?? "#"} id={linkId}
className={`px-3 py-5 text-[16px] font-semibold transition block href={normalizedLink}
${isActive ? "text-[#E8C518]" : "text-[#124588] hover:text-[#E8C518]"} aria-selected={isActive}
`} className={menuItemTriggerVariant()}
> >
{title} <span className="relative z-10 whitespace-nowrap">{title}</span>
<span
className={`absolute bottom-[11px] left-1/2 h-[2px] -translate-x-1/2 rounded-full bg-[#2f57ff] transition-all duration-200 ${
isActive ? 'w-[44px]' : 'w-0 group-hover:w-[44px]'
}`}
aria-hidden="true"
/>
</Link> </Link>
)
if (!hasChildren) {
return <div className="relative shrink-0">{trigger}</div>
}
return (
<HoverCard openDelay={0} closeDelay={90}>
<HoverCardTrigger asChild>{trigger}</HoverCardTrigger>
<HoverCardContent
ref={hoverCardRef}
align="start"
sideOffset={0}
className={menuItemHoverBoxVariant()}
>
{items.map((item) => {
const isItemActive = pathname === item.link
{/* Dropdown */}
<div className="absolute left-0 top-full hidden group-hover:block bg-[#124588]/98 text-white text-[14px] font-medium min-w-[220px] shadow-lg">
{items.map((item, i) => {
const isItemActive = pathname === item.link;
return ( return (
<Link <Link
key={i} key={item.link}
href={item.link} href={item.link}
className={`block px-5 py-3 cursor-pointer whitespace-nowrap transition ${isItemActive ? "bg-[#e8c518]/80" : "hover:bg-[#e8c518]/80" className={menuItemChildVariant({ active: isItemActive })}
}`}
> >
{item.title} {item.title}
</Link> </Link>
); )
})} })}
</div> </HoverCardContent>
</div> </HoverCard>
); )
}; }
const menuItemTriggerVariant = cva(
cn(
buttonVariants({ variant: 'ghost' }),
'group relative inline-flex h-[58px] shrink-0 items-center whitespace-nowrap rounded-none bg-transparent px-[4px] py-0 text-[14px] font-semibold leading-none tracking-normal text-[#43506a] shadow-none transition',
'hover:bg-transparent hover:text-[#2f57ff]',
'aria-selected:bg-transparent aria-selected:text-[#2f57ff]',
'focus-visible:ring-0 focus-visible:ring-offset-0'
)
)
const menuItemHoverBoxVariant = cva(
'z-[80] flex w-auto flex-col gap-1 rounded-b-md rounded-t-none border border-slate-200 bg-white p-2 text-[13px] font-medium text-slate-600 shadow-[0_18px_36px_rgba(15,23,42,0.16)]'
)
const menuItemChildVariant = cva(
cn(
buttonVariants({ variant: 'ghost' }),
'h-auto min-h-10 justify-start rounded-md px-3 py-2.5 text-left text-sm font-medium leading-6 whitespace-normal break-words transition'
),
{
variants: {
active: {
true: 'bg-[#eef3ff] text-[#2f57ff]',
false: 'text-slate-600 hover:bg-[#eef3ff] hover:text-[#2f57ff]'
}
},
defaultVariants: {
active: false
}
}
)
export default MenuItem; export default MenuItem
...@@ -13,6 +13,7 @@ const HoverCardContent = React.forwardRef< ...@@ -13,6 +13,7 @@ const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>, React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content> React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Portal>
<HoverCardPrimitive.Content <HoverCardPrimitive.Content
ref={ref} ref={ref}
align={align} align={align}
...@@ -23,6 +24,7 @@ const HoverCardContent = React.forwardRef< ...@@ -23,6 +24,7 @@ const HoverCardContent = React.forwardRef<
)} )}
{...props} {...props}
/> />
</HoverCardPrimitive.Portal>
)) ))
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
......
...@@ -13,6 +13,24 @@ ...@@ -13,6 +13,24 @@
@apply fixed top-0 right-0 left-0; @apply fixed top-0 right-0 left-0;
} }
.header-menu-scroll {
-ms-overflow-style: none;
scrollbar-width: none;
}
.header-menu-scroll::-webkit-scrollbar {
display: none;
}
.menu-item-underline {
@apply absolute bottom-0 left-1/2 h-[2px] w-0 -translate-x-1/2 rounded-full bg-[#2f57ff] transition-all duration-200;
}
[aria-selected="true"] > .menu-item-underline,
a:hover > .menu-item-underline {
@apply w-[44px];
}
/* Scrollbar */ /* Scrollbar */
.scrollbar { .scrollbar {
scrollbar-color: rgba(6, 62, 142, 0.38) rgba(219, 232, 255, 0.38); scrollbar-color: rgba(6, 62, 142, 0.38) rgba(219, 232, 255, 0.38);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment