Header.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. "use client";
  2. import Image from "next/image";
  3. import Link from "next/link";
  4. import { usePathname } from "next/navigation";
  5. import { useState } from "react";
  6. import { useNotifications } from "./NotificationContext";
  7. import NotificationsPanel from "./NotificationsPanel";
  8. import ThemeToggle from "./ThemeToggle";
  9. const nav = [
  10. { href: "/", label: "Dashboard" },
  11. { href: "/files", label: "Files" },
  12. { href: "/duplicates", label: "Duplicates" },
  13. { href: "/indexing", label: "Indexing" },
  14. { href: "/tasks", label: "Tasks" },
  15. { href: "/settings", label: "Settings" },
  16. ];
  17. function Header() {
  18. const [menuOpen, setMenuOpen] = useState(false);
  19. const [notificationsOpen, setNotificationsOpen] = useState(false);
  20. const pathname = usePathname();
  21. const { unreadCount } = useNotifications();
  22. return (
  23. <nav className="sticky top-0 z-50 bg-white/80 dark:bg-gray-950/80 backdrop-blur-md border-b border-gray-200/50 dark:border-gray-800/50">
  24. <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
  25. <div className="flex h-16 items-center justify-between">
  26. <div className="flex items-center">
  27. <div className="flex-shrink-0">
  28. <Link href="/" className="flex items-center gap-2">
  29. <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-indigo-500 to-purple-600 shadow-sm">
  30. <svg
  31. viewBox="0 0 24 24"
  32. fill="none"
  33. stroke="currentColor"
  34. strokeWidth="2"
  35. className="h-5 w-5 text-white"
  36. aria-hidden="true"
  37. >
  38. <path d="M2.457 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.543-7z" />
  39. <path d="M12 9a3 3 0 100 6 3 3 0 000-6z" />
  40. </svg>
  41. </div>
  42. <span className="text-lg font-semibold text-gray-900 dark:text-white">
  43. Watch Turbo
  44. </span>
  45. </Link>
  46. </div>
  47. <div className="hidden md:block">
  48. <div className="ml-10 flex items-baseline space-x-1">
  49. {nav.map((item) => (
  50. <Link
  51. key={item.href}
  52. href={item.href}
  53. className={`relative rounded-lg px-3 py-2 text-sm font-medium transition-all duration-200 ${
  54. pathname === item.href
  55. ? "bg-indigo-50 text-indigo-700 dark:bg-indigo-950 dark:text-indigo-300"
  56. : "text-gray-600 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white"
  57. }`}
  58. aria-current={pathname === item.href ? "page" : undefined}
  59. >
  60. {item.label}
  61. {pathname === item.href && (
  62. <div className="absolute inset-x-0 -bottom-px h-px bg-gradient-to-r from-indigo-500 to-purple-500" />
  63. )}
  64. </Link>
  65. ))}
  66. </div>
  67. </div>
  68. </div>
  69. <div className="hidden md:block">
  70. <div className="ml-4 flex items-center md:ml-6">
  71. <ThemeToggle />
  72. <button
  73. type="button"
  74. onClick={() => setNotificationsOpen(true)}
  75. className="relative rounded-full p-1 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 focus:outline-2 focus:outline-offset-2 focus:outline-indigo-500"
  76. >
  77. <span className="absolute -inset-1.5"></span>
  78. <span className="sr-only">View notifications</span>
  79. <svg
  80. viewBox="0 0 24 24"
  81. fill="none"
  82. stroke="currentColor"
  83. strokeWidth="1.5"
  84. aria-hidden="true"
  85. className="size-6"
  86. >
  87. <path
  88. d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0"
  89. strokeLinecap="round"
  90. strokeLinejoin="round"
  91. />
  92. </svg>
  93. {unreadCount > 0 && (
  94. <span className="absolute -top-1 -right-1 inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-white transform translate-x-1/2 -translate-y-1/2 bg-red-500 rounded-full min-w-[18px] h-[18px]">
  95. {unreadCount > 99 ? "99+" : unreadCount}
  96. </span>
  97. )}
  98. </button>
  99. {/* Profile dropdown placeholder */}
  100. <div className="relative ml-3">
  101. <button className="relative flex max-w-xs items-center rounded-full focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500">
  102. <span className="absolute -inset-1.5"></span>
  103. <span className="sr-only">Open user menu</span>
  104. <svg
  105. viewBox="0 0 24 24"
  106. fill="none"
  107. stroke="currentColor"
  108. strokeWidth="2"
  109. className="size-8 rounded-full bg-gray-200 dark:bg-gray-700 p-1 text-gray-600 dark:text-gray-300"
  110. aria-hidden="true"
  111. >
  112. <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
  113. <circle cx="12" cy="7" r="4" />
  114. </svg>
  115. </button>
  116. </div>
  117. </div>
  118. </div>
  119. <div className="-mr-2 flex md:hidden">
  120. <button
  121. type="button"
  122. className="relative inline-flex items-center justify-center rounded-md p-2 text-gray-500 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-200 focus:outline-2 focus:outline-offset-2 focus:outline-indigo-500"
  123. onClick={() => setMenuOpen((v) => !v)}
  124. >
  125. <span className="absolute -inset-0.5"></span>
  126. <span className="sr-only">Open main menu</span>
  127. {/* Hamburger icon */}
  128. <svg
  129. viewBox="0 0 24 24"
  130. fill="none"
  131. stroke="currentColor"
  132. strokeWidth="1.5"
  133. aria-hidden="true"
  134. className={`size-6 ${!menuOpen ? "" : "hidden"}`}
  135. >
  136. <path
  137. d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
  138. strokeLinecap="round"
  139. strokeLinejoin="round"
  140. />
  141. </svg>
  142. {/* Close icon */}
  143. <svg
  144. viewBox="0 0 24 24"
  145. fill="none"
  146. stroke="currentColor"
  147. strokeWidth="1.5"
  148. aria-hidden="true"
  149. className={`size-6 ${menuOpen ? "" : "hidden"}`}
  150. >
  151. <path
  152. d="M6 18 18 6M6 6l12 12"
  153. strokeLinecap="round"
  154. strokeLinejoin="round"
  155. />
  156. </svg>
  157. </button>
  158. </div>
  159. </div>
  160. </div>
  161. {/* Mobile menu */}
  162. {menuOpen && (
  163. <div className="md:hidden">
  164. <div className="space-y-1 px-2 pt-2 pb-3 sm:px-3">
  165. {nav.map((item) => (
  166. <Link
  167. key={item.href}
  168. href={item.href}
  169. className={`block rounded-md px-3 py-2 text-base font-medium ${pathname === item.href ? "bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-white" : "text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white"}`}
  170. aria-current={pathname === item.href ? "page" : undefined}
  171. >
  172. {item.label}
  173. </Link>
  174. ))}
  175. </div>
  176. <div className="border-t border-gray-200 dark:border-gray-700 pt-4 pb-3">
  177. <div className="flex items-center px-5">
  178. <div className="shrink-0">
  179. <Image
  180. src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
  181. alt="User"
  182. width={40}
  183. height={40}
  184. className="size-10 rounded-full outline -outline-offset-1 outline-gray-300 dark:outline-gray-600"
  185. />
  186. </div>
  187. <div className="ml-3">
  188. <div className="text-base font-medium text-gray-900 dark:text-white">
  189. Tom Cook
  190. </div>
  191. <div className="text-sm font-medium text-gray-500 dark:text-gray-400">
  192. tom@example.com
  193. </div>
  194. </div>
  195. <button
  196. type="button"
  197. onClick={() => setNotificationsOpen(true)}
  198. className="relative ml-auto shrink-0 rounded-full p-1 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 focus:outline-2 focus:outline-offset-2 focus:outline-indigo-500"
  199. >
  200. <span className="absolute -inset-1.5"></span>
  201. <span className="sr-only">View notifications</span>
  202. <svg
  203. viewBox="0 0 24 24"
  204. fill="none"
  205. stroke="currentColor"
  206. strokeWidth="1.5"
  207. aria-hidden="true"
  208. className="size-6"
  209. >
  210. <path
  211. d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0"
  212. strokeLinecap="round"
  213. strokeLinejoin="round"
  214. />
  215. </svg>
  216. {unreadCount > 0 && (
  217. <span className="absolute -top-1 -right-1 inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-white transform translate-x-1/2 -translate-y-1/2 bg-red-500 rounded-full min-w-[18px] h-[18px]">
  218. {unreadCount > 99 ? "99+" : unreadCount}
  219. </span>
  220. )}
  221. </button>
  222. </div>
  223. <div className="mt-3 space-y-1 px-2">
  224. <Link
  225. href="#"
  226. className="block rounded-md px-3 py-2 text-base font-medium text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white"
  227. >
  228. Your profile
  229. </Link>
  230. <Link
  231. href="#"
  232. className="block rounded-md px-3 py-2 text-base font-medium text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white"
  233. >
  234. Settings
  235. </Link>
  236. <Link
  237. href="#"
  238. className="block rounded-md px-3 py-2 text-base font-medium text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800 hover:text-gray-900 dark:hover:text-white"
  239. >
  240. Sign out
  241. </Link>
  242. </div>
  243. </div>
  244. </div>
  245. )}
  246. <div className="flex items-center gap-2 md:hidden px-4 pb-2">
  247. <ThemeToggle />
  248. </div>
  249. <NotificationsPanel
  250. isOpen={notificationsOpen}
  251. onClose={() => setNotificationsOpen(false)}
  252. />
  253. </nav>
  254. );
  255. }
  256. export default Header;