diff --git a/package.json b/package.json index 21f7f09..bf4fae5 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-navigation-menu": "^1.2.5", + "@radix-ui/react-portal": "^1.1.4", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tabs": "^1.1.3", @@ -35,6 +36,7 @@ "date-fns": "^4.1.0", "dotenv": "^16.4.7", "drizzle-orm": "^0.39.3", + "framer-motion": "^12.4.7", "geist": "^1.3.1", "lucide-react": "^0.475.0", "mdast-util-toc": "^7.1.0", @@ -46,6 +48,7 @@ "react": "^19.0.0", "react-dom": "^19.0.0", "react-hook-form": "^7.54.2", + "react-remove-scroll": "^2.6.3", "rehype-autolink-headings": "^7.1.0", "rehype-pretty-code": "^0.14.0", "rehype-slug": "^6.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c75b7b..a8b652b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: '@radix-ui/react-navigation-menu': specifier: ^1.2.5 version: 1.2.5(@types/react-dom@19.0.3(@types/react@19.0.9))(@types/react@19.0.9)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-portal': + specifier: ^1.1.4 + version: 1.1.4(@types/react-dom@19.0.3(@types/react@19.0.9))(@types/react@19.0.9)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-separator': specifier: ^1.1.2 version: 1.1.2(@types/react-dom@19.0.3(@types/react@19.0.9))(@types/react@19.0.9)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -86,6 +89,9 @@ importers: drizzle-orm: specifier: ^0.39.3 version: 0.39.3(@neondatabase/serverless@0.10.4)(@opentelemetry/api@1.9.0)(@types/pg@8.11.11)(kysely@0.27.5)(pg@8.13.3) + framer-motion: + specifier: ^12.4.7 + version: 12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0) geist: specifier: ^1.3.1 version: 1.3.1(next@15.1.7(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)) @@ -119,6 +125,9 @@ importers: react-hook-form: specifier: ^7.54.2 version: 7.54.2(react@19.0.0) + react-remove-scroll: + specifier: ^2.6.3 + version: 2.6.3(@types/react@19.0.9)(react@19.0.0) rehype-autolink-headings: specifier: ^7.1.0 version: 7.1.0 @@ -2355,8 +2364,8 @@ packages: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} - framer-motion@12.4.3: - resolution: {integrity: sha512-rsMeO7w3dKyNG09o3cGwSH49iHU+VgDmfSSfsX+wfkO3zDA6WWkh4sUsMXd155YROjZP+7FTIhDrBYfgZeHjKQ==} + framer-motion@12.4.7: + resolution: {integrity: sha512-VhrcbtcAMXfxlrjeHPpWVu2+mkcoR31e02aNSR7OUS/hZAciKa8q6o3YN2mA1h+jjscRsSyKvX6E1CiY/7OLMw==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -2811,8 +2820,8 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} - motion-dom@12.0.0: - resolution: {integrity: sha512-CvYd15OeIR6kHgMdonCc1ihsaUG4MYh/wrkz8gZ3hBX/uamyZCXN9S9qJoYF03GqfTt7thTV/dxnHYX4+55vDg==} + motion-dom@12.4.5: + resolution: {integrity: sha512-Q2xmhuyYug1CGTo0jdsL05EQ4RhIYXlggFS/yPhQQRNzbrhjKQ1tbjThx5Plv68aX31LsUQRq4uIkuDxdO5vRQ==} motion-utils@12.0.0: resolution: {integrity: sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==} @@ -5562,9 +5571,9 @@ snapshots: format@0.2.2: {} - framer-motion@12.4.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + framer-motion@12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - motion-dom: 12.0.0 + motion-dom: 12.4.5 motion-utils: 12.0.0 tslib: 2.8.1 optionalDependencies: @@ -6361,7 +6370,7 @@ snapshots: minipass@7.1.2: {} - motion-dom@12.0.0: + motion-dom@12.4.5: dependencies: motion-utils: 12.0.0 @@ -6369,7 +6378,7 @@ snapshots: motion@12.4.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - framer-motion: 12.4.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + framer-motion: 12.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0) tslib: 2.8.1 optionalDependencies: react: 19.0.0 diff --git a/src/app/(public)/layout.tsx b/src/app/(public)/layout.tsx index 27196b1..d454882 100644 --- a/src/app/(public)/layout.tsx +++ b/src/app/(public)/layout.tsx @@ -1,5 +1,5 @@ import { Footer } from "@/components/layout/footer"; -import { Navbar } from "@/components/layout/navbar"; +import { Navbar } from "@/components/marketing/navbar"; import { marketingConfig } from "@/config/marketing"; interface MarketingLayoutProps { @@ -11,7 +11,8 @@ export default async function MarketingLayout({ }: MarketingLayoutProps) { return (
- + {/* */} +
{children}
diff --git a/src/components/layout/navbar.tsx b/src/components/layout/navbar.tsx index d5c466f..4a080f6 100644 --- a/src/components/layout/navbar.tsx +++ b/src/components/layout/navbar.tsx @@ -3,28 +3,29 @@ import { LoginWrapper } from "@/components/auth/login-button"; import Container from "@/components/container"; import { Icons } from "@/components/icons/icons"; -import { ModeToggle } from "@/components/layout/mode-toggle"; import { UserButton } from "@/components/layout/user-button"; +import { Logo } from "@/components/logo"; import { Button } from "@/components/ui/button"; import { NavigationMenu, + NavigationMenuContent, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, + NavigationMenuTrigger, navigationMenuTriggerStyle, } from "@/components/ui/navigation-menu"; -import { Sheet, SheetContent, SheetTitle, SheetHeader, SheetTrigger } from "@/components/ui/sheet"; +import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; import { siteConfig } from "@/config/site"; import { useScroll } from "@/hooks/use-scroll"; +import { authClient } from "@/lib/auth-client"; import { cn } from "@/lib/utils"; -import type { DashboardConfig, MarketingConfig } from "@/types"; -import { ArrowRightIcon, MenuIcon } from "lucide-react"; +import type { DashboardConfig, MarketingConfig, NestedNavItem } from "@/types"; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@radix-ui/react-accordion"; +import { MenuIcon } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useEffect, useState } from "react"; -import React from "react"; -import { Logo } from "@/components/logo"; -import { authClient } from "@/lib/auth-client"; interface NavBarProps { scroll?: boolean; @@ -39,10 +40,9 @@ export function Navbar({ scroll = false, config }: NavBarProps) { const pathname = usePathname(); // console.log(`Navbar, pathname: ${pathname}`); - const links = config.menus; - // console.log(`Navbar, links: ${links.map((link) => link.title)}`); + const menus = config.menus; - const isLinkActive = (href: string) => { + const isMenuActive = (href: string) => { if (href === "/") { return pathname === "/"; } @@ -82,27 +82,10 @@ export function Navbar({ scroll = false, config }: NavBarProps) { {/* links */}
- {links && links.length > 0 ? ( + {menus && menus.length > 0 ? ( - {links.map((item) => ( - - - {item.title} - - - ))} + {menus.map((item) => renderMenuItem(item))} ) : null} @@ -117,17 +100,17 @@ export function Navbar({ scroll = false, config }: NavBarProps) { ) : ( )} - + {/* */}
@@ -148,47 +131,26 @@ export function Navbar({ scroll = false, config }: NavBarProps) { Toggle navigation menu - + - + + setOpen(false)} + > + + {siteConfig.name} + + -
- {/* logo */} - setOpen(false)} +
+ - - - {siteConfig.name} - - - + {menus.map((item) => renderMobileMenuItem(item))} +
@@ -204,7 +166,7 @@ export function Navbar({ scroll = false, config }: NavBarProps) {
- {/* mobile navbar right show sign in or account */} + {/* mobile navbar right show sign in or user button */}
{user ? (
@@ -213,20 +175,116 @@ export function Navbar({ scroll = false, config }: NavBarProps) { ) : ( )} - + {/* */}
); } + +const renderMenuItem = (item: NestedNavItem) => { + if (item.items) { + return ( + + + {item.title} + + + + + + ); + } + + return ( + + + {item.title} + + + ); +}; + +const renderMobileMenuItem = (item: NestedNavItem) => { + console.log(`renderMobileMenuItem, item:`, item, `, items:`, item.items); + if (item.items) { + return ( + + + {item.title} + + + {item.items.map((subItem) => { + const CustomMenuIcon = Icons[subItem.icon || "arrowRight"]; + return ( + + {subItem.icon && } +
+
{subItem.title}
+
+ + ); + })} +
+
+ ); + } + + const CustomMenuIcon = Icons[item.icon || "arrowRight"]; + return ( + + {item.icon && } + {item.title} + + ); +}; diff --git a/src/components/marketing/marketing-links.tsx b/src/components/marketing/marketing-links.tsx new file mode 100644 index 0000000..33f7fbe --- /dev/null +++ b/src/components/marketing/marketing-links.tsx @@ -0,0 +1,195 @@ +import * as React from 'react'; +import { CubeIcon, PaperPlaneIcon } from '@radix-ui/react-icons'; +import { + BookIcon, + BookOpenIcon, + CircuitBoardIcon, + CuboidIcon, + FileBarChartIcon, + LayoutIcon, + PlayIcon +} from 'lucide-react'; +import { Routes } from '@/constants/routes'; +import { FacebookIcon } from '@/components/icons/facebook'; +import { InstagramIcon } from '@/components/icons/instagram'; +import { LinkedInIcon } from '@/components/icons/linkedin'; +import { TikTokIcon } from '@/components/icons/tiktok'; +import { XTwitterIcon } from '@/components/icons/x'; +export const MENU_LINKS = [ + { + title: 'Features', + href: Routes.Pricing, + external: false + }, + { + title: 'Pricing', + href: Routes.Pricing, + external: false + }, + { + title: 'Blog', + href: Routes.Blog, + external: false + }, + { + title: 'AI', + items: [ + { + title: 'Features', + description: 'Short description here', + icon: , + href: '#', + external: false + }, + { + title: 'Pricing', + description: 'Short description here', + icon: , + href: '#', + external: false + }, + { + title: 'Testimonials', + description: 'Short description here', + icon: , + href: '#', + external: false + }, + { + title: 'FAQ', + description: 'Short description here', + icon: , + href: '#', + external: false + }, + { + title: 'Roadmap', + description: 'Short description here', + icon: , + href: '#', + external: false + } + ] + }, + { + title: 'Pages', + items: [ + { + title: 'Contact', + description: 'Short description here', + icon: , + href: Routes.Contact, + external: false + }, + { + title: 'Roadmap', + description: 'Short description here', + icon: , + href: Routes.Roadmap, + external: true + }, + { + title: 'Waitlist', + description: 'Short description here', + icon: , + href: Routes.Docs, + external: false + } + ] + }, +]; + +export const FOOTER_LINKS = [ + { + title: 'Product', + links: [ + { name: 'Feature 1', href: '#', external: false }, + { name: 'Feature 2', href: '#', external: false }, + { name: 'Feature 3', href: '#', external: false }, + { name: 'Feature 4', href: '#', external: false }, + { name: 'Feature 5', href: '#', external: false } + ] + }, + { + title: 'Resources', + links: [ + { name: 'Contact', href: Routes.Contact, external: false }, + { name: 'Roadmap', href: Routes.Roadmap, external: true }, + { name: 'Docs', href: Routes.Docs, external: false } + ] + }, + { + title: 'About', + links: [ + { name: 'Story', href: Routes.Story, external: false }, + { name: 'Blog', href: Routes.Blog, external: false }, + { name: 'Careers', href: Routes.Careers, external: false } + ] + }, + { + title: 'Legal', + links: [ + { name: 'Terms of Use', href: Routes.TermsOfUse, external: false }, + { name: 'Privacy Policy', href: Routes.PrivacyPolicy, external: false }, + { name: 'Cookie Policy', href: Routes.CookiePolicy, external: false } + ] + } +]; + +export const SOCIAL_LINKS = [ + { + name: 'X (formerly Twitter)', + href: '#', + icon: + }, + { + name: 'LinkedIn', + href: '#', + icon: + }, + { + name: 'Facebook', + href: '#', + icon: + }, + { + name: 'Instagram', + href: '#', + icon: + }, + { + name: 'TikTok', + href: '#', + icon: + } +]; + +export const DOCS_LINKS = [ + { + title: 'Getting Started', + icon: , + items: [ + { + title: 'Introduction', + href: '/docs', + items: [] + }, + { + title: 'Dependencies', + href: '/docs/dependencies', + items: [] + } + ] + }, + { + title: 'Guides', + icon: , + items: [ + { + title: 'Using MDX', + href: '/docs/using-mdx', + items: [] + } + ] + } +]; diff --git a/src/components/marketing/navbar-mobile.tsx b/src/components/marketing/navbar-mobile.tsx new file mode 100644 index 0000000..af47d8e --- /dev/null +++ b/src/components/marketing/navbar-mobile.tsx @@ -0,0 +1,313 @@ +'use client'; + +import { Logo } from '@/components/logo'; +import { DOCS_LINKS, MENU_LINKS } from '@/components/marketing/marketing-links'; +import { Button, buttonVariants } from '@/components/ui/button'; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger +} from '@/components/ui/collapsible'; +import { siteConfig } from '@/config/site'; +import { Routes } from '@/constants/routes'; +import { cn } from '@/lib/utils'; +import { Portal } from '@radix-ui/react-portal'; +import { ChevronDown, ChevronUp, MenuIcon, X } from 'lucide-react'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import * as React from 'react'; +import { RemoveScroll } from 'react-remove-scroll'; +import { Icons } from '../icons/icons'; +import { ThemeSwitcherHorizontal } from '@/components/layout/theme-switcher-horizontal'; + +export function NavbarMobile({ + className, + ...other +}: React.HTMLAttributes) { + const [open, setOpen] = React.useState(false); + const pathname = usePathname(); + const isDocs = pathname.startsWith('/docs'); + + React.useEffect(() => { + const handleRouteChangeStart = () => { + if (document.activeElement instanceof HTMLInputElement) { + document.activeElement.blur(); + } + + setOpen(false); + }; + + handleRouteChangeStart(); + }, [pathname]); + + const handleChange = () => { + const mediaQueryList = window.matchMedia('(min-width: 1024px)'); + setOpen((open) => (open ? !mediaQueryList.matches : false)); + }; + + React.useEffect(() => { + handleChange(); + const mediaQueryList = window.matchMedia('(min-width: 1024px)'); + mediaQueryList.addEventListener('change', handleChange); + return () => mediaQueryList.removeEventListener('change', handleChange); + }, []); + + const handleToggleMobileMenu = (): void => { + setOpen((open) => !open); + }; + + return ( + <> +
+ {/* navbar left shows logo */} + + + {siteConfig.name} + + + {/* navbar right shows menu icon */} + +
+ + {/* mobile menu */} + {open && ( + + + {isDocs ? ( + + ) : ( + + )} + + + )} + + ); +} + +type MainMobileMenuProps = { + onLinkClicked: () => void; +}; + +function MainMobileMenu({ onLinkClicked }: MainMobileMenuProps) { + const [expanded, setExpanded] = React.useState>({}); + return ( +
+
+ {/* action buttons */} +
+ + Log in + + + Sign up + +
+ + {/* main menu */} +
    + {MENU_LINKS.map((item) => ( +
  • + {item.items ? ( + + setExpanded((prev) => ({ + ...prev, + [item.title.toLowerCase()]: isOpen + })) + } + > + + + + +
      + {item.items.map((subItem) => ( +
    • + +
      + {subItem.icon} +
      +
      + + {subItem.title} + {subItem.external && ( + + )} + + {subItem.description && ( +

      + {subItem.description} +

      + )} +
      + +
    • + ))} +
    +
    +
    + ) : ( + + {item.title} + + )} +
  • + ))} +
+ + {/* bottom buttons */} +
+
+ +
+
+
+ ); +} + +type DocsMobileMenuProps = { + onLinkClicked: () => void; +}; + +function DocsMobileMenu({ + onLinkClicked +}: DocsMobileMenuProps): React.JSX.Element { + const [expanded, setExpanded] = React.useState>({}); + return ( +
+
+
    + {DOCS_LINKS.map((item) => ( +
  • + + setExpanded((prev) => ({ + ...prev, + [item.title.toLowerCase()]: isOpen + })) + } + > + + + + +
      + {item.items.map((subItem) => ( +
    • + + {subItem.title} + +
    • + ))} +
    +
    +
    +
  • + ))} +
+
+
+ +
+
+
+ ); +} diff --git a/src/components/marketing/navbar.tsx b/src/components/marketing/navbar.tsx new file mode 100644 index 0000000..014a3ff --- /dev/null +++ b/src/components/marketing/navbar.tsx @@ -0,0 +1,184 @@ +'use client'; + +import { LoginWrapper } from '@/components/auth/login-button'; +import Container from '@/components/container'; +import { Icons } from '@/components/icons/icons'; +import { ThemeSwitcher } from '@/components/layout/theme-swticher'; +import { UserButton } from '@/components/layout/user-button'; +import { Logo } from '@/components/logo'; +import { MENU_LINKS } from '@/components/marketing/marketing-links'; +import { NavbarMobile } from '@/components/marketing/navbar-mobile'; +import { Button } from '@/components/ui/button'; +import { + NavigationMenu, + NavigationMenuContent, + NavigationMenuItem, + NavigationMenuLink, + NavigationMenuList, + NavigationMenuTrigger, + navigationMenuTriggerStyle +} from '@/components/ui/navigation-menu'; +import { siteConfig } from '@/config/site'; +import { useScroll } from "@/hooks/use-scroll"; +import { authClient } from '@/lib/auth-client'; +import { cn } from '@/lib/utils'; +import { AUTH_ROUTE_REGISTER } from '@/routes'; +import { MarketingConfig } from '@/types'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +interface NavBarProps { + scroll?: boolean; + config: MarketingConfig; +} + +export function Navbar({ scroll, config }: NavBarProps) { + const scrolled = useScroll(50); + const { data: session, error } = authClient.useSession(); + const user = session?.user; + console.log(`Navbar, user:`, user); + + const pathname = usePathname(); + + return ( +
+ + {/* desktop navbar */} + + + {/* mobile navbar */} + + +
+ ); +} diff --git a/src/config/marketing.ts b/src/config/marketing.ts index 2610b7a..ec79257 100644 --- a/src/config/marketing.ts +++ b/src/config/marketing.ts @@ -18,9 +18,26 @@ export const marketingConfig: MarketingConfig = { icon: "blog", }, { - title: "About", - href: "/about", + title: "Pages", + href: "#", icon: "about", - }, + items: [ + { + title: "Waitlist", + href: "/waitlist", + icon: "about", + }, + { + title: "Contact", + href: "/contact", + icon: "about", + }, + { + title: "About", + href: "/about", + icon: "about", + }, + ], + }, ], }; diff --git a/src/types/index.d.ts b/src/types/index.d.ts index a2c7b52..47b3d79 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -17,7 +17,6 @@ export type SiteConfig = { links: { github?: string; twitter?: string; - twitter_cn?: string; bluesky?: string; youtube?: string; docs?: string; @@ -54,11 +53,11 @@ export type HeroConfig = { }; export type MarketingConfig = { - menus: NavItem[]; + menus: NestedNavItem[]; }; export type DashboardConfig = { - menus: NavItem[]; + menus: NestedNavItem[]; }; export type UserButtonConfig = { @@ -81,7 +80,11 @@ export type NavItem = { export type NestedNavItem = { title: string; - items: NavItem[]; + items?: NavItem[]; + href?: string; + badge?: number; + disabled?: boolean; + external?: boolean; authorizeOnly?: UserRole; icon?: keyof typeof Icons; };