summaryrefslogtreecommitdiff
path: root/src/components/sections
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/sections')
-rw-r--r--src/components/sections/footer.tsx112
-rw-r--r--src/components/sections/header/index.tsx174
-rw-r--r--src/components/sections/header/menu.tsx120
-rw-r--r--src/components/sections/header/navbar.tsx55
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>
+ );
+};