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) => {
......
This diff is collapsed.
...@@ -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}> <Link
<HoverCardTrigger asChild> aria-selected={isActive}
<Link id={linkId}
aria-selected={active || isActive} target={normalizedLink.startsWith('/') ? '_self' : '_blank'}
id={linkId} href={normalizedLink}
target={(menu.link ?? '').startsWith('/') ? '_self' : '_blank'} className={menuItemTriggerClass(variant)}
href={menu.link ?? '/'} >
className={menuItemTriggerVariant({ variant })} <span className="relative z-10 truncate">{menu.name}</span>
> {variant === 'main' ? <span className="menu-item-underline" aria-hidden="true" /> : null}
{menu.name} </Link>
</Link> )
</HoverCardTrigger>
{menu.children && ( if (!hasChildren) {
<HoverCardContent ref={hoverCardRef} className={menuItemHoverBoxVariant({ variant })}> return trigger
{menu.children.map((subMenu) => ( }
<Link key={subMenu.id} href={subMenu.link ?? '/'} className={menuItemChildVariant({ variant })}>
{subMenu.name} return (
</Link> <HoverCard openDelay={80} closeDelay={120}>
))} <HoverCardTrigger asChild>{trigger}</HoverCardTrigger>
</HoverCardContent> <HoverCardContent ref={hoverCardRef} className={menuItemHoverBoxVariant(variant)}>
)} {menu.children?.map((subMenu) => (
<Link key={subMenu.id} href={subMenu.link ?? '/'} className={menuItemChildVariant(variant)}>
{subMenu.name}
</Link>
))}
</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])
const hoverCardRef = useCallback(
(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
id={linkId}
href={normalizedLink}
aria-selected={isActive}
className={menuItemTriggerVariant()}
>
<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>
)
if (!hasChildren) {
return <div className="relative shrink-0">{trigger}</div>
}
return ( return (
<div className="group relative"> <HoverCard openDelay={0} closeDelay={90}>
<Link <HoverCardTrigger asChild>{trigger}</HoverCardTrigger>
href={link ?? "#"} <HoverCardContent
className={`px-3 py-5 text-[16px] font-semibold transition block ref={hoverCardRef}
${isActive ? "text-[#E8C518]" : "text-[#124588] hover:text-[#E8C518]"} align="start"
`} sideOffset={0}
className={menuItemHoverBoxVariant()}
> >
{title} {items.map((item) => {
</Link> 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,16 +13,18 @@ const HoverCardContent = React.forwardRef< ...@@ -13,16 +13,18 @@ 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.Content <HoverCardPrimitive.Portal>
ref={ref} <HoverCardPrimitive.Content
align={align} ref={ref}
sideOffset={sideOffset} align={align}
className={cn( sideOffset={sideOffset}
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-hover-card-content-transform-origin]", className={cn(
className "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-hover-card-content-transform-origin]",
)} className
{...props} )}
/> {...props}
/>
</HoverCardPrimitive.Portal>
)) ))
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
......
...@@ -9,10 +9,28 @@ ...@@ -9,10 +9,28 @@
@apply sticky top-0; @apply sticky top-0;
} }
.header-fixed { .header-fixed {
@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