diff options
| author | Bertrand Yuan <bert.yuan@outlook.com> | 2025-12-15 23:48:10 +0800 |
|---|---|---|
| committer | Bertrand Yuan <bert.yuan@outlook.com> | 2025-12-15 23:48:10 +0800 |
| commit | 5b7ccf0b671e2999b62befc729a3e517a0433728 (patch) | |
| tree | 8bf476dc7c75914c221042546840dc76267366df /src/components/sections/header/index.tsx | |
initial commit -- the front-end prototype
The initial code is base on Anirudh's work.
More to see at:
https://github.com/techwithanirudh/shadcn-blog
Therefore, the code in this commit is under MIT license.
Diffstat (limited to 'src/components/sections/header/index.tsx')
| -rw-r--r-- | src/components/sections/header/index.tsx | 174 |
1 files changed, 174 insertions, 0 deletions
diff --git a/src/components/sections/header/index.tsx b/src/components/sections/header/index.tsx new file mode 100644 index 0000000..4280e01 --- /dev/null +++ b/src/components/sections/header/index.tsx @@ -0,0 +1,174 @@ +import Link from 'fumadocs-core/link'; +import { + LanguageToggle, + LanguageToggleText, +} from 'fumadocs-ui/components/layout/language-toggle'; +import { + LargeSearchToggle, + SearchToggle, +} from 'fumadocs-ui/components/layout/search-toggle'; +import { NavigationMenuList } from 'fumadocs-ui/components/ui/navigation-menu'; +import type { HomeLayoutProps } from 'fumadocs-ui/layouts/home'; +import { + NavbarLink, + NavbarMenu, + NavbarMenuContent, + NavbarMenuTrigger, +} from 'fumadocs-ui/layouts/home/navbar'; +import type { LinkItemType } from 'fumadocs-ui/layouts/links'; +import { SearchOnly } from 'fumadocs-ui/provider'; +import { ChevronDown, Languages } from 'lucide-react'; +import { ThemeToggle } from '../../theme-toggle'; +import { Menu, MenuContent, MenuLinkItem, MenuTrigger } from './menu'; +import { Navbar, NavbarMenuLink } from './navbar'; + +export const Header = ({ + nav: { enableSearch = true, ...nav } = {}, + i18n = false, + finalLinks, +}: HomeLayoutProps & { + finalLinks: LinkItemType[]; +}) => { + const navItems = finalLinks.filter((item) => + ['nav', 'all'].includes(item.on ?? 'all'), + ); + const menuItems = finalLinks.filter((item) => + ['menu', 'all'].includes(item.on ?? 'all'), + ); + + return ( + <Navbar> + <Link + href={nav.url ?? '/'} + className='inline-flex items-center gap-2.5 font-semibold' + > + {nav.title} + </Link> + {nav.children} + <NavigationMenuList className='ml-2 flex flex-row items-center gap-2 max-sm:hidden'> + {navItems + .filter((item) => !isSecondary(item)) + .map((item, i) => ( + <NavbarLinkItem + key={i.toString()} + item={item} + className='text-sm' + /> + ))} + </NavigationMenuList> + <div className='flex flex-1 flex-row items-center justify-end lg:gap-1.5'> + {enableSearch ? ( + <SearchOnly> + <SearchToggle className='lg:hidden' /> + <LargeSearchToggle className='w-full max-w-[240px] max-lg:hidden' /> + </SearchOnly> + ) : null} + <ThemeToggle className='max-lg:hidden' /> + {navItems.filter(isSecondary).map((item, i) => ( + <NavbarLinkItem + key={i.toString()} + item={item} + className='-me-1.5 max-lg:hidden' + /> + ))} + <Menu className='lg:hidden'> + <MenuTrigger className='group -me-2'> + <ChevronDown className='size-3 transition-transform duration-300 group-data-[state=open]:rotate-180' /> + </MenuTrigger> + <MenuContent className='sm:flex-row sm:items-center sm:justify-end'> + {menuItems + .filter((item) => !isSecondary(item)) + .map((item, i) => ( + <MenuLinkItem + key={i.toString()} + item={item} + className='sm:hidden' + /> + ))} + <div className='-ms-1.5 flex flex-row items-center gap-1.5 max-sm:mt-2'> + {menuItems.filter(isSecondary).map((item, i) => ( + <MenuLinkItem + key={i.toString()} + item={item} + className='-me-1.5' + /> + ))} + <div className='flex-1' /> + {i18n ? ( + <LanguageToggle> + <Languages className='size-5' /> + <LanguageToggleText /> + <ChevronDown className='size-3 text-fd-muted-foreground' /> + </LanguageToggle> + ) : null} + <ThemeToggle /> + </div> + </MenuContent> + </Menu> + </div> + </Navbar> + ); +}; + +const NavbarLinkItem = ({ + item, + ...props +}: { + item: LinkItemType; + className?: string; +}) => { + if (item.type === 'custom') return <div {...props}>{item.children}</div>; + + if (item.type === 'menu') { + const children = item.items.map((child, j) => { + if (child.type === 'custom') + return <div key={j.toString()}>{child.children}</div>; + + const { banner, footer, ...rest } = child.menu ?? {}; + + return ( + <NavbarMenuLink key={j.toString()} href={child.url} {...rest}> + {banner ?? + (child.icon ? ( + <div className='w-fit rounded-md border bg-fd-muted p-1 [&_svg]:size-4'> + {child.icon} + </div> + ) : null)} + <p className='-mb-1 font-medium text-sm'>{child.text}</p> + {child.description ? ( + <p className='text-[13px] text-fd-muted-foreground'> + {child.description} + </p> + ) : null} + {footer} + </NavbarMenuLink> + ); + }); + + return ( + <NavbarMenu> + <NavbarMenuTrigger {...props}> + {item.url ? <Link href={item.url}>{item.text}</Link> : item.text} + </NavbarMenuTrigger> + <NavbarMenuContent>{children}</NavbarMenuContent> + </NavbarMenu> + ); + } + + return ( + <NavbarLink + {...props} + item={item} + variant={item.type} + aria-label={item.type === 'icon' ? item.label : undefined} + > + {item.type === 'icon' ? item.icon : item.text} + </NavbarLink> + ); +}; + +const isSecondary = (item: LinkItemType): boolean => { + return ( + ('secondary' in item && item.secondary === true) || item.type === 'icon' + ); +}; |
