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}
+
+
-
- {/* 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}
+
+
+
+
+ {item.items.map((subItem) => {
+ const CustomMenuIcon = Icons[subItem.icon || "arrowRight"];
+ return (
+ -
+
+ {subItem.icon && }
+
+
+
+ );
+ })}
+
+
+
+
+ );
+ }
+
+ 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 && }
+
+
+ );
+ })}
+
+
+ );
+ }
+
+ 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;
};