AppHeader.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. import { Link, usePage } from '@inertiajs/react';
  2. import { BookOpen, Folder, LayoutGrid, Menu, Search } from 'lucide-react';
  3. import { AppLogo } from '@/common/AppLogo';
  4. import { AppLogoIcon } from '@/common/AppLogoIcon';
  5. import { Breadcrumbs } from '@/common/Breadcrumbs';
  6. import { Icon } from '@/common/Icon';
  7. import { UserMenuContent } from '@/common/UserMenuContent';
  8. import { cn } from '@/common/helpers/cn';
  9. import { useInitials } from '@/common/hooks/useInitials';
  10. import { type BreadcrumbItem, type NavItem, type SharedData } from '@/common/types';
  11. import { dashboard } from '@/routes';
  12. import { Avatar, AvatarFallback, AvatarImage } from '@/shadcn/avatar';
  13. import { Button } from '@/shadcn/button';
  14. import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/shadcn/dropdown-menu';
  15. import { NavigationMenu, NavigationMenuItem, NavigationMenuList, navigationMenuTriggerStyle } from '@/shadcn/navigation-menu';
  16. import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/shadcn/sheet';
  17. import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/shadcn/tooltip';
  18. const mainNavItems: NavItem[] = [
  19. {
  20. title: 'Dashboard',
  21. href: dashboard(),
  22. icon: LayoutGrid,
  23. },
  24. ];
  25. const rightNavItems: NavItem[] = [
  26. {
  27. title: 'Repository',
  28. href: 'https://github.com/laravel/react-starter-kit',
  29. icon: Folder,
  30. },
  31. {
  32. title: 'Documentation',
  33. href: 'https://laravel.com/docs/starter-kits#react',
  34. icon: BookOpen,
  35. },
  36. ];
  37. const activeItemStyles = 'text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100';
  38. interface AppHeaderProps {
  39. breadcrumbs?: BreadcrumbItem[];
  40. }
  41. export function AppHeader({ breadcrumbs = [] }: AppHeaderProps) {
  42. const page = usePage<SharedData>();
  43. const { auth } = page.props;
  44. const getInitials = useInitials();
  45. return (
  46. <>
  47. <div className="border-b border-sidebar-border/80">
  48. <div className="mx-auto flex h-16 items-center px-4 md:max-w-7xl">
  49. {/* Mobile Menu */}
  50. <div className="lg:hidden">
  51. <Sheet>
  52. <SheetTrigger asChild>
  53. <Button variant="ghost" size="icon" className="mr-2 h-[34px] w-[34px]">
  54. <Menu className="h-5 w-5" />
  55. </Button>
  56. </SheetTrigger>
  57. <SheetContent side="left" className="flex h-full w-64 flex-col items-stretch justify-between bg-sidebar">
  58. <SheetTitle className="sr-only">Navigation Menu</SheetTitle>
  59. <SheetHeader className="flex justify-start text-left">
  60. <AppLogoIcon className="h-6 w-6 fill-current text-black dark:text-white" />
  61. </SheetHeader>
  62. <div className="flex h-full flex-1 flex-col space-y-4 p-4">
  63. <div className="flex h-full flex-col justify-between text-sm">
  64. <div className="flex flex-col space-y-4">
  65. {mainNavItems.map((item) => (
  66. <Link key={item.title} href={item.href} className="flex items-center space-x-2 font-medium">
  67. {item.icon && <Icon iconNode={item.icon} className="h-5 w-5" />}
  68. <span>{item.title}</span>
  69. </Link>
  70. ))}
  71. </div>
  72. <div className="flex flex-col space-y-4">
  73. {rightNavItems.map((item) => (
  74. <a
  75. key={item.title}
  76. href={typeof item.href === 'string' ? item.href : item.href.url}
  77. target="_blank"
  78. rel="noopener noreferrer"
  79. className="flex items-center space-x-2 font-medium"
  80. >
  81. {item.icon && <Icon iconNode={item.icon} className="h-5 w-5" />}
  82. <span>{item.title}</span>
  83. </a>
  84. ))}
  85. </div>
  86. </div>
  87. </div>
  88. </SheetContent>
  89. </Sheet>
  90. </div>
  91. <Link href={dashboard()} prefetch className="flex items-center space-x-2">
  92. <AppLogo />
  93. </Link>
  94. {/* Desktop Navigation */}
  95. <div className="ml-6 hidden h-full items-center space-x-6 lg:flex">
  96. <NavigationMenu className="flex h-full items-stretch">
  97. <NavigationMenuList className="flex h-full items-stretch space-x-2">
  98. {mainNavItems.map((item, index) => (
  99. <NavigationMenuItem key={index} className="relative flex h-full items-center">
  100. <Link
  101. href={item.href}
  102. className={cn(
  103. navigationMenuTriggerStyle(),
  104. page.url === (typeof item.href === 'string' ? item.href : item.href.url) && activeItemStyles,
  105. 'h-9 cursor-pointer px-3',
  106. )}
  107. >
  108. {item.icon && <Icon iconNode={item.icon} className="mr-2 h-4 w-4" />}
  109. {item.title}
  110. </Link>
  111. {page.url === item.href && (
  112. <div className="absolute bottom-0 left-0 h-0.5 w-full translate-y-px bg-black dark:bg-white"></div>
  113. )}
  114. </NavigationMenuItem>
  115. ))}
  116. </NavigationMenuList>
  117. </NavigationMenu>
  118. </div>
  119. <div className="ml-auto flex items-center space-x-2">
  120. <div className="relative flex items-center space-x-1">
  121. <Button variant="ghost" size="icon" className="group h-9 w-9 cursor-pointer">
  122. <Search className="!size-5 opacity-80 group-hover:opacity-100" />
  123. </Button>
  124. <div className="hidden lg:flex">
  125. {rightNavItems.map((item) => (
  126. <TooltipProvider key={item.title} delayDuration={0}>
  127. <Tooltip>
  128. <TooltipTrigger>
  129. <a
  130. href={typeof item.href === 'string' ? item.href : item.href.url}
  131. target="_blank"
  132. rel="noopener noreferrer"
  133. className="group ml-1 inline-flex h-9 w-9 items-center justify-center rounded-md bg-transparent p-0 text-sm font-medium text-accent-foreground ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50"
  134. >
  135. <span className="sr-only">{item.title}</span>
  136. {item.icon && <Icon iconNode={item.icon} className="size-5 opacity-80 group-hover:opacity-100" />}
  137. </a>
  138. </TooltipTrigger>
  139. <TooltipContent>
  140. <p>{item.title}</p>
  141. </TooltipContent>
  142. </Tooltip>
  143. </TooltipProvider>
  144. ))}
  145. </div>
  146. </div>
  147. <DropdownMenu>
  148. <DropdownMenuTrigger asChild>
  149. <Button variant="ghost" className="size-10 rounded-full p-1">
  150. <Avatar className="size-8 overflow-hidden rounded-full">
  151. <AvatarImage src={auth.user.avatar} alt={auth.user.name} />
  152. <AvatarFallback className="rounded-lg bg-neutral-200 text-black dark:bg-neutral-700 dark:text-white">
  153. {getInitials(auth.user.name)}
  154. </AvatarFallback>
  155. </Avatar>
  156. </Button>
  157. </DropdownMenuTrigger>
  158. <DropdownMenuContent className="w-56" align="end">
  159. <UserMenuContent user={auth.user} />
  160. </DropdownMenuContent>
  161. </DropdownMenu>
  162. </div>
  163. </div>
  164. </div>
  165. {breadcrumbs.length > 1 && (
  166. <div className="flex w-full border-b border-sidebar-border/70">
  167. <div className="mx-auto flex h-12 w-full items-center justify-start px-4 text-neutral-500 md:max-w-7xl">
  168. <Breadcrumbs breadcrumbs={breadcrumbs} />
  169. </div>
  170. </div>
  171. )}
  172. </>
  173. );
  174. }