|
| 1 | +'use client'; |
| 2 | + |
| 3 | +import { CaretDownIcon, ListIcon, XIcon } from '@phosphor-icons/react'; |
| 4 | +import { AnimatePresence, motion } from 'motion/react'; |
| 5 | +import Link from 'next/link'; |
| 6 | +import { useState } from 'react'; |
| 7 | +import { ThemeToggle } from '@/components/theme-toggle'; |
| 8 | +import { Logo } from './logo'; |
| 9 | +import { NavLink } from './nav-link'; |
| 10 | +import { contents } from './sidebar-content'; |
| 11 | + |
| 12 | +export type DocsNavbarProps = { |
| 13 | + stars?: number | null; |
| 14 | +}; |
| 15 | + |
| 16 | +export const DocsNavbar = ({ stars }: DocsNavbarProps) => { |
| 17 | + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); |
| 18 | + const [openSection, setOpenSection] = useState<number>(0); // Default first section open |
| 19 | + |
| 20 | + const toggleMobileMenu = () => { |
| 21 | + setIsMobileMenuOpen(!isMobileMenuOpen); |
| 22 | + }; |
| 23 | + |
| 24 | + const toggleSection = (index: number) => { |
| 25 | + setOpenSection(openSection === index ? -1 : index); |
| 26 | + }; |
| 27 | + |
| 28 | + return ( |
| 29 | + <div className="sticky top-0 z-30 flex flex-col border-b backdrop-blur-md"> |
| 30 | + <nav> |
| 31 | + <div className="mx-auto w-full px-2 sm:px-2 md:px-6 lg:px-8"> |
| 32 | + <div className="flex h-16 items-center justify-between"> |
| 33 | + {/* Logo Section */} |
| 34 | + <div className="flex-shrink-0"> |
| 35 | + <Logo /> |
| 36 | + </div> |
| 37 | + |
| 38 | + {/* Desktop Navigation */} |
| 39 | + <div className="hidden md:block"> |
| 40 | + <ul className="flex items-center"> |
| 41 | + {navMenu.map((menu) => ( |
| 42 | + <NavLink |
| 43 | + external={menu.external} |
| 44 | + href={menu.path} |
| 45 | + key={menu.name} |
| 46 | + > |
| 47 | + {menu.name} |
| 48 | + </NavLink> |
| 49 | + ))} |
| 50 | + <NavLink |
| 51 | + external |
| 52 | + href="https://github.com/databuddy-analytics/Databuddy" |
| 53 | + > |
| 54 | + <span className="inline-flex items-center gap-2"> |
| 55 | + <svg |
| 56 | + className="transition-transform duration-200 hover:scale-110" |
| 57 | + height="1.4em" |
| 58 | + viewBox="0 0 496 512" |
| 59 | + width="1.4em" |
| 60 | + xmlns="http://www.w3.org/2000/svg" |
| 61 | + > |
| 62 | + <title>GitHub</title> |
| 63 | + <path |
| 64 | + d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6c-3.3.3-5.6-1.3-5.6-3.6c0-2 2.3-3.6 5.2-3.6c3-.3 5.6 1.3 5.6 3.6m-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9c2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3m44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9c.3 2 2.9 3.3 5.9 2.6c2.9-.7 4.9-2.6 4.6-4.6c-.3-1.9-3-3.2-5.9-2.9M244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2c12.8 2.3 17.3-5.6 17.3-12.1c0-6.2-.3-40.4-.3-61.4c0 0-70 15-84.7-29.8c0 0-11.4-29.1-27.8-36.6c0 0-22.9-15.7 1.6-15.4c0 0 24.9 2 38.6 25.8c21.9 38.6 58.6 27.5 72.9 20.9c2.3-16 8.8-27.1 16-33.7c-55.9-6.2-112.3-14.3-112.3-110.5c0-27.5 7.6-41.3 23.6-58.9c-2.6-6.5-11.1-33.3 2.6-67.9c20.9-6.5 69 27 69 27c20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27c13.7 34.7 5.2 61.4 2.6 67.9c16 17.7 25.8 31.5 25.8 58.9c0 96.5-58.9 104.2-114.8 110.5c9.2 7.9 17 22.9 17 46.4c0 33.7-.3 75.4-.3 83.6c0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252C496 113.3 383.5 8 244.8 8M97.2 352.9c-1.3 1-1 3.3.7 5.2c1.6 1.6 3.9 2.3 5.2 1c1.3-1 1-3.3-.7-5.2c-1.6-1.6-3.9-2.3-5.2-1m-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9c1.6 1 3.6.7 4.3-.7c.7-1.3-.3-2.9-2.3-3.9c-2-.6-3.6-.3-4.3.7m32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2c2.3 2.3 5.2 2.6 6.5 1c1.3-1.3.7-4.3-1.3-6.2c-2.2-2.3-5.2-2.6-6.5-1m-11.4-14.7c-1.6 1-1.6 3.6 0 5.9s4.3 3.3 5.6 2.3c1.6-1.3 1.6-3.9 0-6.2c-1.4-2.3-4-3.3-5.6-2" |
| 65 | + fill="currentColor" |
| 66 | + /> |
| 67 | + </svg> |
| 68 | + {typeof stars === 'number' && ( |
| 69 | + <span |
| 70 | + className="rounded border border-border/40 bg-muted/40 px-2 py-0.5 text-foreground/80 text-xs" |
| 71 | + title="GitHub stars" |
| 72 | + > |
| 73 | + ★ {stars.toLocaleString()} |
| 74 | + </span> |
| 75 | + )} |
| 76 | + </span> |
| 77 | + </NavLink> |
| 78 | + <li className="ml-2"> |
| 79 | + <ThemeToggle /> |
| 80 | + </li> |
| 81 | + </ul> |
| 82 | + </div> |
| 83 | + |
| 84 | + {/* Mobile Menu Button */} |
| 85 | + <button |
| 86 | + aria-label="Toggle mobile menu" |
| 87 | + className="group relative rounded-lg border border-transparent p-2.5 transition-all duration-200 hover:border-border/30 hover:bg-muted/50 active:bg-muted/70 md:hidden" |
| 88 | + onClick={toggleMobileMenu} |
| 89 | + type="button" |
| 90 | + > |
| 91 | + <div className="relative h-6 w-6"> |
| 92 | + <ListIcon |
| 93 | + className={`absolute inset-0 h-6 w-6 transition-all duration-300 ease-out ${ |
| 94 | + isMobileMenuOpen |
| 95 | + ? 'rotate-90 scale-90 opacity-0' |
| 96 | + : 'rotate-0 scale-100 opacity-100' |
| 97 | + }`} |
| 98 | + weight="duotone" |
| 99 | + /> |
| 100 | + <XIcon |
| 101 | + className={`absolute inset-0 h-6 w-6 transition-all duration-300 ease-out ${ |
| 102 | + isMobileMenuOpen |
| 103 | + ? 'rotate-0 scale-100 opacity-100' |
| 104 | + : '-rotate-90 scale-90 opacity-0' |
| 105 | + }`} |
| 106 | + /> |
| 107 | + </div> |
| 108 | + </button> |
| 109 | + </div> |
| 110 | + </div> |
| 111 | + </nav> |
| 112 | + |
| 113 | + {/* Mobile Documentation Menu */} |
| 114 | + <div |
| 115 | + className={`overflow-hidden transition-all duration-300 ease-out md:hidden ${ |
| 116 | + isMobileMenuOpen |
| 117 | + ? 'max-h-[80vh] border-border/50 border-b opacity-100' |
| 118 | + : 'max-h-0 opacity-0' |
| 119 | + }`} |
| 120 | + > |
| 121 | + <div className="bg-background/95 backdrop-blur-sm"> |
| 122 | + <div |
| 123 | + className="mx-auto max-w-7xl overflow-y-auto px-4 py-4 sm:px-6 lg:px-8" |
| 124 | + style={{ maxHeight: '70vh' }} |
| 125 | + > |
| 126 | + {/* Documentation sections */} |
| 127 | + <div className="space-y-2"> |
| 128 | + {contents.map((section, sectionIndex) => ( |
| 129 | + <div key={section.title}> |
| 130 | + <button |
| 131 | + className="flex w-full items-center justify-between rounded-lg px-2 py-2 text-left transition-colors hover:bg-muted/50 active:bg-muted/70" |
| 132 | + onClick={() => toggleSection(sectionIndex)} |
| 133 | + type="button" |
| 134 | + > |
| 135 | + <div className="flex items-center gap-2"> |
| 136 | + <section.Icon |
| 137 | + className="h-4 w-4 text-muted-foreground" |
| 138 | + weight="duotone" |
| 139 | + /> |
| 140 | + <h3 className="font-medium text-foreground text-sm"> |
| 141 | + {section.title} |
| 142 | + </h3> |
| 143 | + </div> |
| 144 | + <motion.div |
| 145 | + animate={{ |
| 146 | + rotate: openSection === sectionIndex ? 180 : 0, |
| 147 | + }} |
| 148 | + transition={{ duration: 0.2 }} |
| 149 | + > |
| 150 | + <CaretDownIcon |
| 151 | + className="h-4 w-4 text-muted-foreground" |
| 152 | + weight="duotone" |
| 153 | + /> |
| 154 | + </motion.div> |
| 155 | + </button> |
| 156 | + <AnimatePresence initial={false}> |
| 157 | + {openSection === sectionIndex && ( |
| 158 | + <motion.div |
| 159 | + animate={{ opacity: 1, height: 'auto' }} |
| 160 | + className="relative overflow-hidden" |
| 161 | + exit={{ opacity: 0, height: 0 }} |
| 162 | + initial={{ opacity: 0, height: 0 }} |
| 163 | + transition={{ duration: 0.3, ease: 'easeInOut' }} |
| 164 | + > |
| 165 | + <div className="ml-6 space-y-1 pb-2"> |
| 166 | + {section.list.map((item, itemIndex) => ( |
| 167 | + <div key={item.title}> |
| 168 | + {item.group ? ( |
| 169 | + <div className="px-2 py-1"> |
| 170 | + <p className="font-medium text-muted-foreground text-xs uppercase tracking-wider"> |
| 171 | + {item.title} |
| 172 | + </p> |
| 173 | + </div> |
| 174 | + ) : ( |
| 175 | + <Link |
| 176 | + className={`block transform rounded-lg px-3 py-2 text-muted-foreground text-sm transition-all duration-200 hover:translate-x-1 hover:bg-muted/50 hover:text-foreground focus:outline-none focus:ring-2 focus:ring-primary/20 active:bg-muted/70 ${ |
| 177 | + isMobileMenuOpen |
| 178 | + ? 'translate-x-0 opacity-100' |
| 179 | + : '-translate-x-4 opacity-0' |
| 180 | + }`} |
| 181 | + href={item.href || '#'} |
| 182 | + onClick={() => setIsMobileMenuOpen(false)} |
| 183 | + style={{ |
| 184 | + transitionDelay: isMobileMenuOpen |
| 185 | + ? `${(sectionIndex * section.list.length + itemIndex) * 30}ms` |
| 186 | + : '0ms', |
| 187 | + }} |
| 188 | + > |
| 189 | + <div className="flex items-center gap-2"> |
| 190 | + <item.icon |
| 191 | + className="h-4 w-4 flex-shrink-0" |
| 192 | + weight="duotone" |
| 193 | + /> |
| 194 | + <span>{item.title}</span> |
| 195 | + {item.isNew && ( |
| 196 | + <span className="rounded border border-border/40 bg-muted/40 px-1.5 py-0.5 text-foreground/80 text-xs"> |
| 197 | + New |
| 198 | + </span> |
| 199 | + )} |
| 200 | + </div> |
| 201 | + </Link> |
| 202 | + )} |
| 203 | + </div> |
| 204 | + ))} |
| 205 | + </div> |
| 206 | + </motion.div> |
| 207 | + )} |
| 208 | + </AnimatePresence> |
| 209 | + </div> |
| 210 | + ))} |
| 211 | + </div> |
| 212 | + |
| 213 | + {/* Separator */} |
| 214 | + <div className="my-4 h-px bg-border" /> |
| 215 | + |
| 216 | + {/* Regular nav items at bottom */} |
| 217 | + <div className="space-y-1"> |
| 218 | + {navMenu |
| 219 | + .filter((menu) => menu.name !== 'Docs') |
| 220 | + .map((menu, index) => ( |
| 221 | + <Link |
| 222 | + className={`block transform rounded-lg px-3 py-2 font-medium text-sm transition-all duration-200 hover:translate-x-1 hover:bg-muted/50 focus:outline-none focus:ring-2 focus:ring-primary/20 active:bg-muted/70 ${ |
| 223 | + isMobileMenuOpen |
| 224 | + ? 'translate-x-0 opacity-100' |
| 225 | + : '-translate-x-4 opacity-0' |
| 226 | + }`} |
| 227 | + href={menu.path} |
| 228 | + key={menu.name} |
| 229 | + onClick={() => setIsMobileMenuOpen(false)} |
| 230 | + style={{ |
| 231 | + transitionDelay: isMobileMenuOpen |
| 232 | + ? `${(contents.length * 5 + index) * 30}ms` |
| 233 | + : '0ms', |
| 234 | + }} |
| 235 | + {...(menu.external && { |
| 236 | + target: '_blank', |
| 237 | + rel: 'noopener noreferrer', |
| 238 | + })} |
| 239 | + > |
| 240 | + {menu.name} |
| 241 | + </Link> |
| 242 | + ))} |
| 243 | + <Link |
| 244 | + className={`flex transform items-center gap-3 rounded-lg border border-border/30 px-3 py-2 font-medium text-sm transition-all duration-200 hover:translate-x-1 hover:border-border/50 hover:bg-muted/50 focus:outline-none focus:ring-2 focus:ring-primary/20 active:bg-muted/70 ${ |
| 245 | + isMobileMenuOpen |
| 246 | + ? 'translate-x-0 opacity-100' |
| 247 | + : '-translate-x-4 opacity-0' |
| 248 | + }`} |
| 249 | + href="https://github.com/databuddy-analytics" |
| 250 | + onClick={() => setIsMobileMenuOpen(false)} |
| 251 | + rel="noopener noreferrer" |
| 252 | + style={{ |
| 253 | + transitionDelay: isMobileMenuOpen |
| 254 | + ? `${(contents.length * 5 + navMenu.length) * 30}ms` |
| 255 | + : '0ms', |
| 256 | + }} |
| 257 | + target="_blank" |
| 258 | + > |
| 259 | + <svg |
| 260 | + className="flex-shrink-0 transition-transform duration-200 group-hover:scale-110" |
| 261 | + height="1.2em" |
| 262 | + viewBox="0 0 496 512" |
| 263 | + width="1.2em" |
| 264 | + xmlns="http://www.w3.org/2000/svg" |
| 265 | + > |
| 266 | + <title>GitHub</title> |
| 267 | + <path |
| 268 | + d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6c-3.3.3-5.6-1.3-5.6-3.6c0-2 2.3-3.6 5.2-3.6c3-.3 5.6 1.3 5.6 3.6m-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9c2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3m44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9c.3 2 2.9 3.3 5.9 2.6c2.9-.7 4.9-2.6 4.6-4.6c-.3-1.9-3-3.2-5.9-2.9M244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2c12.8 2.3 17.3-5.6 17.3-12.1c0-6.2-.3-40.4-.3-61.4c0 0-70 15-84.7-29.8c0 0-11.4-29.1-27.8-36.6c0 0-22.9-15.7 1.6-15.4c0 0 24.9 2 38.6 25.8c21.9 38.6 58.6 27.5 72.9 20.9c2.3-16 8.8-27.1 16-33.7c-55.9-6.2-112.3-14.3-112.3-110.5c0-27.5 7.6-41.3 23.6-58.9c-2.6-6.5-11.1-33.3 2.6-67.9c20.9-6.5 69 27 69 27c20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27c13.7 34.7 5.2 61.4 2.6 67.9c16 17.7 25.8 31.5 25.8 58.9c0 96.5-58.9 104.2-114.8 110.5c9.2 7.9 17 22.9 17 46.4c0 33.7-.3 75.4-.3 83.6c0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252C496 113.3 383.5 8 244.8 8M97.2 352.9c-1.3 1-1 3.3.7 5.2c1.6 1.6 3.9 2.3 5.2 1c1.3-1 1-3.3-.7-5.2c-1.6-1.6-3.9-2.3-5.2-1m-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9c1.6 1 3.6.7 4.3-.7c.7-1.3-.3-2.9-2.3-3.9c-2-.6-3.6-.3-4.3.7m32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2c2.3 2.3 5.2 2.6 6.5 1c1.3-1.3.7-4.3-1.3-6.2c-2.2-2.3-5.2-2.6-6.5-1m-11.4-14.7c-1.6 1-1.6 3.6 0 5.9s4.3 3.3 5.6 2.3c1.6-1.3 1.6-3.9 0-6.2c-1.4-2.3-4-3.3-5.6-2" |
| 269 | + fill="currentColor" |
| 270 | + /> |
| 271 | + </svg> |
| 272 | + <span className="flex items-center gap-2"> |
| 273 | + GitHub |
| 274 | + {typeof stars === 'number' && ( |
| 275 | + <span |
| 276 | + className="rounded border border-border/40 bg-muted/40 px-2 py-0.5 text-foreground/80 text-xs" |
| 277 | + title="GitHub stars" |
| 278 | + > |
| 279 | + ★ {stars.toLocaleString()} |
| 280 | + </span> |
| 281 | + )} |
| 282 | + </span> |
| 283 | + </Link> |
| 284 | + </div> |
| 285 | + </div> |
| 286 | + </div> |
| 287 | + </div> |
| 288 | + </div> |
| 289 | + ); |
| 290 | +}; |
| 291 | + |
| 292 | +export const navMenu = [ |
| 293 | + { |
| 294 | + name: 'Docs', |
| 295 | + path: '/docs', |
| 296 | + }, |
| 297 | + { |
| 298 | + name: 'Blog', |
| 299 | + path: '/blog', |
| 300 | + }, |
| 301 | + { |
| 302 | + name: 'Pricing', |
| 303 | + path: '/pricing', |
| 304 | + }, |
| 305 | + { |
| 306 | + name: 'Dashboard', |
| 307 | + path: 'https://app.databuddy.cc', |
| 308 | + external: true, |
| 309 | + }, |
| 310 | +]; |
0 commit comments