diff options
Diffstat (limited to 'src/components/sections')
| -rw-r--r-- | src/components/sections/footer.tsx | 112 | ||||
| -rw-r--r-- | src/components/sections/header/index.tsx | 174 | ||||
| -rw-r--r-- | src/components/sections/header/menu.tsx | 120 | ||||
| -rw-r--r-- | src/components/sections/header/navbar.tsx | 55 |
4 files changed, 461 insertions, 0 deletions
diff --git a/src/components/sections/footer.tsx b/src/components/sections/footer.tsx new file mode 100644 index 0000000..8d0d876 --- /dev/null +++ b/src/components/sections/footer.tsx @@ -0,0 +1,112 @@ +import { baseOptions, linkItems, postsPerPage } from '@/app/layout.config'; +import { InlineLink } from '@/components/inline-link'; +import { getSortedByDatePosts, getTags } from '@/lib/source'; +import { cn } from '@/lib/utils'; +import { getLinks } from 'fumadocs-ui/layouts/shared'; +import { ActiveLink } from '../active-link'; + +export function Footer() { + const links = getLinks(linkItems, baseOptions.githubUrl); + const navItems = links.filter((item) => + ['nav', 'all'].includes(item.on ?? 'all'), + ); + + const posts = getSortedByDatePosts(); + const tags = getTags(); + + return ( + <footer className={cn('flex flex-col gap-4')}> + <div + className={cn( + 'grid gap-8 text-muted-foreground text-sm sm:grid-cols-4', + 'container mx-auto sm:gap-16 sm:px-8 sm:py-16', + 'border-border/70 border-b border-dashed dark:border-border', + )} + > + <div className='flex flex-col gap-6'> + <p className='font-medium text-foreground'>Pages</p> + + <ul className='flex flex-col gap-3'> + <li className='flex items-center gap-2'> + <ActiveLink href={'/'}>Home</ActiveLink> + </li> + {navItems + .filter( + (item) => + item.type !== 'menu' && + item.type !== 'custom' && + item.type !== 'icon', + ) + .map((item, i) => ( + <li key={item.url}> + <ActiveLink key={i.toString()} href={item.url}> + {item.text} + </ActiveLink> + </li> + ))} + </ul> + </div> + + <div className='flex flex-col gap-6'> + <p className='font-medium text-foreground'>Posts</p> + + <ul className='flex flex-col gap-3'> + {posts.slice(0, postsPerPage).map((post, i) => ( + <li key={post.url}> + <ActiveLink key={i.toString()} href={post.url}> + {post.data.title} + </ActiveLink> + </li> + ))} + </ul> + </div> + + <div className='flex flex-col gap-6'> + <p className='font-medium text-foreground'>Tags</p> + + <ul className='flex flex-col gap-3'> + {tags.slice(0, postsPerPage).map((name, i) => ( + <li key={`/tags/${name}`}> + <ActiveLink key={i.toString()} href={`/tags/${name}`}> + <span className='capitalize'>{name}</span> + </ActiveLink> + </li> + ))} + </ul> + </div> + + <div className='flex flex-col gap-6'> + <p className='font-medium text-foreground'>Socials</p> + + <ul className='flex flex-col gap-3'> + {navItems + .filter((item) => item.type === 'icon') + .map((item, i) => ( + <li key={item.url}> + <InlineLink + key={i.toString()} + href={item.url} + className='inline-flex items-center gap-1.5 text-muted-foreground no-underline [&_svg]:size-4' + > + {item.icon} + {item.text} + </InlineLink> + </li> + ))} + </ul> + </div> + </div> + {/* <Design /> */} + </footer> + ); +} + +function Design() { + return ( + <div className='footer'> + <span className='footer-text font-mono'>john•doe</span> + <div className='footer-grid' /> + <div className='footer-gradient' /> + </div> + ); +} 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' + ); +}; diff --git a/src/components/sections/header/menu.tsx b/src/components/sections/header/menu.tsx new file mode 100644 index 0000000..57ddf12 --- /dev/null +++ b/src/components/sections/header/menu.tsx @@ -0,0 +1,120 @@ +'use client'; + +import { cva } from 'class-variance-authority'; +import Link from 'fumadocs-core/link'; +import { cn } from 'fumadocs-ui/components/api'; +import { buttonVariants } from 'fumadocs-ui/components/ui/button'; +import { + NavigationMenuContent, + NavigationMenuItem, + NavigationMenuLink, + NavigationMenuTrigger, +} from 'fumadocs-ui/components/ui/navigation-menu'; +import { BaseLinkItem, type LinkItemType } from 'fumadocs-ui/layouts/links'; +import type { ComponentPropsWithoutRef } from 'react'; + +const menuItemVariants = cva('', { + variants: { + variant: { + main: 'inline-flex items-center gap-2 py-1.5 transition-colors hover:text-fd-popover-foreground/50 data-[active=true]:font-medium data-[active=true]:text-fd-primary [&_svg]:size-4', + icon: buttonVariants({ + size: 'icon', + color: 'ghost', + }), + button: buttonVariants({ + color: 'secondary', + className: 'gap-1.5 [&_svg]:size-4', + }), + }, + }, + defaultVariants: { + variant: 'main', + }, +}); + +export const MenuLinkItem = ({ + item, + ...props +}: { + item: LinkItemType; + className?: string; +}) => { + if (item.type === 'custom') + return <div className={cn('grid', props.className)}>{item.children}</div>; + + if (item.type === 'menu') { + const header = ( + <> + {item.icon} + {item.text} + </> + ); + + return ( + <div className={cn('mb-4 flex flex-col', props.className)}> + <p className='mb-1 text-fd-muted-foreground text-sm'> + {item.url ? ( + <NavigationMenuLink asChild> + <Link href={item.url}>{header}</Link> + </NavigationMenuLink> + ) : ( + header + )} + </p> + {item.items.map((child, i) => ( + <MenuLinkItem key={i.toString()} item={child} /> + ))} + </div> + ); + } + + return ( + <NavigationMenuLink asChild> + <BaseLinkItem + item={item} + className={cn( + menuItemVariants({ variant: item.type }), + props.className, + )} + aria-label={item.type === 'icon' ? item.label : undefined} + > + {item.icon} + {item.type === 'icon' ? undefined : item.text} + </BaseLinkItem> + </NavigationMenuLink> + ); +}; + +export const Menu = NavigationMenuItem; + +export const MenuTrigger = ({ + ...props +}: ComponentPropsWithoutRef<typeof NavigationMenuTrigger> & {}) => { + return ( + <NavigationMenuTrigger + {...props} + className={cn( + buttonVariants({ + size: 'icon', + color: 'ghost', + }), + props.className, + )} + > + {props.children} + </NavigationMenuTrigger> + ); +}; + +export const MenuContent = ( + props: ComponentPropsWithoutRef<typeof NavigationMenuContent>, +) => { + return ( + <NavigationMenuContent + {...props} + className={cn('flex flex-col p-4', props.className)} + > + {props.children} + </NavigationMenuContent> + ); +}; diff --git a/src/components/sections/header/navbar.tsx b/src/components/sections/header/navbar.tsx new file mode 100644 index 0000000..25850a0 --- /dev/null +++ b/src/components/sections/header/navbar.tsx @@ -0,0 +1,55 @@ +'use client'; + +import Link, { type LinkProps } from 'fumadocs-core/link'; +import { cn } from 'fumadocs-ui/components/api'; +import { + NavigationMenu, + NavigationMenuLink, + NavigationMenuViewport, +} from 'fumadocs-ui/components/ui/navigation-menu'; +import { type HTMLAttributes, useState } from 'react'; + +export const Navbar = (props: HTMLAttributes<HTMLElement>) => { + const [value, setValue] = useState(''); + + return ( + <NavigationMenu value={value} onValueChange={setValue} asChild> + <header + id='nd-nav' + {...props} + className={cn( + 'sticky top-[var(--fd-banner-height)] z-30 box-content w-full bg-fd-background/80 backdrop-blur-lg transition-colors', + 'border-border/70 border-b border-dashed dark:border-border', + // value.length > 0 ? 'shadow-lg' : 'shadow-xs', + props.className, + )} + > + <div + className={cn( + 'container mx-auto flex size-full h-14 flex-row items-center px-4 md:gap-1.5 lg:px-6', + 'border-border/70 border-dashed sm:border-x dark:border-border', + )} + > + {props.children} + </div> + <NavigationMenuViewport /> + </header> + </NavigationMenu> + ); +}; + +export const NavbarMenuLink = (props: LinkProps) => { + return ( + <NavigationMenuLink asChild> + <Link + {...props} + className={cn( + 'flex flex-col gap-2 rounded-lg border bg-fd-card p-3 transition-colors hover:bg-fd-accent/80 hover:text-fd-accent-foreground', + props.className, + )} + > + {props.children} + </Link> + </NavigationMenuLink> + ); +}; |
