feat: enhance navbar with nested menu, mobile responsiveness, and marketing links
This commit is contained in:
parent
4d60d96180
commit
7ab78c95dc
@ -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",
|
||||
|
25
pnpm-lock.yaml
generated
25
pnpm-lock.yaml
generated
@ -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
|
||||
|
@ -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 (
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<Navbar scroll={true} config={marketingConfig} />
|
||||
{/* <Navbar scroll={true} config={marketingConfig} /> */}
|
||||
<Navbar scroll={false} config={marketingConfig} />
|
||||
|
||||
<main className="flex-1">{children}</main>
|
||||
|
||||
|
@ -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 */}
|
||||
<div className="flex-1 flex justify-center">
|
||||
{links && links.length > 0 ? (
|
||||
{menus && menus.length > 0 ? (
|
||||
<NavigationMenu>
|
||||
<NavigationMenuList>
|
||||
{links.map((item) => (
|
||||
<NavigationMenuItem key={item.title}>
|
||||
<NavigationMenuLink
|
||||
href={item.disabled ? "#" : item.href}
|
||||
target={item.external ? "_blank" : ""}
|
||||
className={cn(
|
||||
navigationMenuTriggerStyle(),
|
||||
"px-4 bg-transparent focus:bg-transparent text-base",
|
||||
isLinkActive(item.href)
|
||||
? "text-foreground font-semibold"
|
||||
: "text-foreground/60",
|
||||
item.disabled && "cursor-not-allowed opacity-80",
|
||||
)}
|
||||
>
|
||||
{item.title}
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
))}
|
||||
{menus.map((item) => renderMenuItem(item))}
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
) : null}
|
||||
@ -117,17 +100,17 @@ export function Navbar({ scroll = false, config }: NavBarProps) {
|
||||
) : (
|
||||
<LoginWrapper mode="modal" asChild>
|
||||
<Button
|
||||
className="flex gap-2 px-5 rounded-full"
|
||||
className="flex gap-2"
|
||||
variant="default"
|
||||
size="default"
|
||||
size="sm"
|
||||
>
|
||||
<span>Sign In</span>
|
||||
<ArrowRightIcon className="size-4" />
|
||||
<span>Login</span>
|
||||
{/* <ArrowRightIcon className="size-4" /> */}
|
||||
</Button>
|
||||
</LoginWrapper>
|
||||
)}
|
||||
|
||||
<ModeToggle />
|
||||
{/* <ModeToggle /> */}
|
||||
</div>
|
||||
</Container>
|
||||
</header>
|
||||
@ -148,47 +131,26 @@ export function Navbar({ scroll = false, config }: NavBarProps) {
|
||||
<span className="sr-only">Toggle navigation menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="flex flex-col p-0">
|
||||
<SheetContent side="left" className="overflow-y-auto">
|
||||
<SheetHeader>
|
||||
<SheetTitle />
|
||||
<SheetTitle>
|
||||
<a href="/"
|
||||
className="flex items-center space-x-2"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<Logo />
|
||||
<span className="text-xl font-bold">{siteConfig.name}</span>
|
||||
</a>
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="flex h-screen flex-col">
|
||||
{/* logo */}
|
||||
<a href="/"
|
||||
className="flex items-center space-x-2 pl-4 pt-4"
|
||||
onClick={() => setOpen(false)}
|
||||
<div className="my-6 flex flex-col gap-6">
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
className="flex w-full flex-col gap-4"
|
||||
>
|
||||
<Logo />
|
||||
|
||||
<span className="text-xl font-bold">{siteConfig.name}</span>
|
||||
</a>
|
||||
|
||||
<nav className="flex flex-1 flex-col gap-2 p-2 pt-8 font-medium">
|
||||
{links.map((item) => {
|
||||
const Icon = Icons[item.icon || "arrowRight"];
|
||||
return (
|
||||
<Link
|
||||
key={item.title}
|
||||
href={item.disabled ? "#" : item.href}
|
||||
target={item.external ? "_blank" : ""}
|
||||
onClick={() => {
|
||||
if (!item.disabled) setOpen(false);
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center rounded-md gap-2 p-2 text-sm font-medium hover:bg-muted",
|
||||
isLinkActive(item.href)
|
||||
? "bg-muted text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
item.disabled &&
|
||||
"cursor-not-allowed opacity-80 hover:bg-transparent hover:text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-5" />
|
||||
{item.title}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
{menus.map((item) => renderMobileMenuItem(item))}
|
||||
</Accordion>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
@ -204,7 +166,7 @@ export function Navbar({ scroll = false, config }: NavBarProps) {
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* mobile navbar right show sign in or account */}
|
||||
{/* mobile navbar right show sign in or user button */}
|
||||
<div className="flex items-center gap-x-4">
|
||||
{user ? (
|
||||
<div className="flex items-center">
|
||||
@ -213,20 +175,116 @@ export function Navbar({ scroll = false, config }: NavBarProps) {
|
||||
) : (
|
||||
<LoginWrapper mode="redirect" asChild>
|
||||
<Button
|
||||
className="flex gap-2 px-5 rounded-full"
|
||||
className="flex gap-2"
|
||||
variant="default"
|
||||
size="default"
|
||||
size="sm"
|
||||
>
|
||||
<span>Sign In</span>
|
||||
<ArrowRightIcon className="size-4" />
|
||||
<span>Login</span>
|
||||
{/* <ArrowRightIcon className="size-4" /> */}
|
||||
</Button>
|
||||
</LoginWrapper>
|
||||
)}
|
||||
|
||||
<ModeToggle />
|
||||
{/* <ModeToggle /> */}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const renderMenuItem = (item: NestedNavItem) => {
|
||||
if (item.items) {
|
||||
return (
|
||||
<NavigationMenuItem key={item.title} className="text-muted-foreground">
|
||||
<NavigationMenuTrigger className="text-base">
|
||||
{item.title}
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<ul className="w-80 p-4">
|
||||
<NavigationMenuLink>
|
||||
{item.items.map((subItem) => {
|
||||
const CustomMenuIcon = Icons[subItem.icon || "arrowRight"];
|
||||
return (
|
||||
<li key={subItem.title}>
|
||||
<a
|
||||
className="flex items-center select-none gap-4 rounded-md p-4 leading-none no-underline outline-none transition-colors hover:bg-muted hover:text-accent-foreground"
|
||||
href={subItem.href}
|
||||
>
|
||||
{subItem.icon && <CustomMenuIcon className="size-4 shrink-0" />}
|
||||
<div>
|
||||
<div className="text-base text-foreground/60 hover:text-foreground">
|
||||
{subItem.title}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</NavigationMenuLink>
|
||||
</ul>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationMenuItem key={item.title}>
|
||||
<Link
|
||||
href={item.disabled ? "#" : item.href || "#"}
|
||||
target={item.external ? "_blank" : ""}
|
||||
className={cn(
|
||||
navigationMenuTriggerStyle(),
|
||||
"px-4 bg-transparent focus:bg-transparent text-base",
|
||||
"text-foreground/60 hover:text-foreground",
|
||||
item.disabled && "cursor-not-allowed opacity-80"
|
||||
)}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
</NavigationMenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMobileMenuItem = (item: NestedNavItem) => {
|
||||
console.log(`renderMobileMenuItem, item:`, item, `, items:`, item.items);
|
||||
if (item.items) {
|
||||
return (
|
||||
<AccordionItem key={item.title} value={item.title} className="border-b-0">
|
||||
<AccordionTrigger className="py-0 hover:no-underline">
|
||||
{item.title}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="mt-2">
|
||||
{item.items.map((subItem) => {
|
||||
const CustomMenuIcon = Icons[subItem.icon || "arrowRight"];
|
||||
return (
|
||||
<Link
|
||||
key={subItem.title}
|
||||
className="flex select-none gap-4 rounded-md p-3 leading-none outline-none transition-colors hover:bg-muted hover:text-accent-foreground"
|
||||
href={subItem.disabled ? "#" : subItem.href}
|
||||
>
|
||||
{subItem.icon && <CustomMenuIcon className="size-4 shrink-0" />}
|
||||
<div>
|
||||
<div className="text-sm font-semibold">{subItem.title}</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
);
|
||||
}
|
||||
|
||||
const CustomMenuIcon = Icons[item.icon || "arrowRight"];
|
||||
return (
|
||||
<Link
|
||||
key={item.title}
|
||||
href={item.disabled ? "#" : item.href || "#"}
|
||||
target={item.external ? "_blank" : ""}
|
||||
className="flex items-center rounded-md gap-2 p-2 text-sm font-medium hover:bg-muted text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
{item.icon && <CustomMenuIcon className="size-4 shrink-0" />}
|
||||
{item.title}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
195
src/components/marketing/marketing-links.tsx
Normal file
195
src/components/marketing/marketing-links.tsx
Normal file
@ -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: <CubeIcon className="size-5 shrink-0" />,
|
||||
href: '#',
|
||||
external: false
|
||||
},
|
||||
{
|
||||
title: 'Pricing',
|
||||
description: 'Short description here',
|
||||
icon: <PlayIcon className="size-5 shrink-0" />,
|
||||
href: '#',
|
||||
external: false
|
||||
},
|
||||
{
|
||||
title: 'Testimonials',
|
||||
description: 'Short description here',
|
||||
icon: <CircuitBoardIcon className="size-5 shrink-0" />,
|
||||
href: '#',
|
||||
external: false
|
||||
},
|
||||
{
|
||||
title: 'FAQ',
|
||||
description: 'Short description here',
|
||||
icon: <LayoutIcon className="size-5 shrink-0" />,
|
||||
href: '#',
|
||||
external: false
|
||||
},
|
||||
{
|
||||
title: 'Roadmap',
|
||||
description: 'Short description here',
|
||||
icon: <FileBarChartIcon className="size-5 shrink-0" />,
|
||||
href: '#',
|
||||
external: false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Pages',
|
||||
items: [
|
||||
{
|
||||
title: 'Contact',
|
||||
description: 'Short description here',
|
||||
icon: <PaperPlaneIcon className="size-5 shrink-0" />,
|
||||
href: Routes.Contact,
|
||||
external: false
|
||||
},
|
||||
{
|
||||
title: 'Roadmap',
|
||||
description: 'Short description here',
|
||||
icon: <LayoutIcon className="size-5 shrink-0" />,
|
||||
href: Routes.Roadmap,
|
||||
external: true
|
||||
},
|
||||
{
|
||||
title: 'Waitlist',
|
||||
description: 'Short description here',
|
||||
icon: <BookOpenIcon className="size-5 shrink-0" />,
|
||||
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: <XTwitterIcon className="size-4 shrink-0" />
|
||||
},
|
||||
{
|
||||
name: 'LinkedIn',
|
||||
href: '#',
|
||||
icon: <LinkedInIcon className="size-4 shrink-0" />
|
||||
},
|
||||
{
|
||||
name: 'Facebook',
|
||||
href: '#',
|
||||
icon: <FacebookIcon className="size-4 shrink-0" />
|
||||
},
|
||||
{
|
||||
name: 'Instagram',
|
||||
href: '#',
|
||||
icon: <InstagramIcon className="size-4 shrink-0" />
|
||||
},
|
||||
{
|
||||
name: 'TikTok',
|
||||
href: '#',
|
||||
icon: <TikTokIcon className="size-4 shrink-0" />
|
||||
}
|
||||
];
|
||||
|
||||
export const DOCS_LINKS = [
|
||||
{
|
||||
title: 'Getting Started',
|
||||
icon: <CuboidIcon className="size-4 shrink-0 text-muted-foreground" />,
|
||||
items: [
|
||||
{
|
||||
title: 'Introduction',
|
||||
href: '/docs',
|
||||
items: []
|
||||
},
|
||||
{
|
||||
title: 'Dependencies',
|
||||
href: '/docs/dependencies',
|
||||
items: []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Guides',
|
||||
icon: <BookIcon className="size-4 shrink-0 text-muted-foreground" />,
|
||||
items: [
|
||||
{
|
||||
title: 'Using MDX',
|
||||
href: '/docs/using-mdx',
|
||||
items: []
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
313
src/components/marketing/navbar-mobile.tsx
Normal file
313
src/components/marketing/navbar-mobile.tsx
Normal file
@ -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<HTMLDivElement>) {
|
||||
const [open, setOpen] = React.useState<boolean>(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 (
|
||||
<>
|
||||
<div
|
||||
className={cn('flex items-center justify-between', className)}
|
||||
{...other}
|
||||
>
|
||||
{/* navbar left shows logo */}
|
||||
<Link href={Routes.Root} className="flex items-center gap-2">
|
||||
<Logo />
|
||||
<span className="text-xl font-semibold">{siteConfig.name}</span>
|
||||
</Link>
|
||||
|
||||
{/* navbar right shows menu icon */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-expanded={open}
|
||||
aria-label="Toggle Mobile Menu"
|
||||
onClick={handleToggleMobileMenu}
|
||||
className="flex aspect-square h-fit select-none items-center justify-center rounded-md border"
|
||||
>
|
||||
{open ? (
|
||||
<X className="size-8" />
|
||||
) : (
|
||||
<MenuIcon className="size-8" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* mobile menu */}
|
||||
{open && (
|
||||
<Portal asChild>
|
||||
<RemoveScroll allowPinchZoom enabled>
|
||||
{isDocs ? (
|
||||
<DocsMobileMenu onLinkClicked={handleToggleMobileMenu} />
|
||||
) : (
|
||||
<MainMobileMenu onLinkClicked={handleToggleMobileMenu} />
|
||||
)}
|
||||
</RemoveScroll>
|
||||
</Portal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type MainMobileMenuProps = {
|
||||
onLinkClicked: () => void;
|
||||
};
|
||||
|
||||
function MainMobileMenu({ onLinkClicked }: MainMobileMenuProps) {
|
||||
const [expanded, setExpanded] = React.useState<Record<string, boolean>>({});
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 mt-[72px] overflow-y-auto bg-background animate-in fade-in-0">
|
||||
<div className="flex size-full flex-col items-start space-y-4 p-4">
|
||||
{/* action buttons */}
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
<Link
|
||||
href={Routes.Login}
|
||||
onClick={onLinkClicked}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: 'outline',
|
||||
size: 'lg'
|
||||
}),
|
||||
'w-full'
|
||||
)}
|
||||
>
|
||||
Log in
|
||||
</Link>
|
||||
<Link
|
||||
href={Routes.SignUp}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: 'default',
|
||||
size: 'lg'
|
||||
}),
|
||||
'w-full'
|
||||
)}
|
||||
onClick={onLinkClicked}
|
||||
>
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* main menu */}
|
||||
<ul className="w-full">
|
||||
{MENU_LINKS.map((item) => (
|
||||
<li key={item.title} className="py-2">
|
||||
{item.items ? (
|
||||
<Collapsible
|
||||
open={expanded[item.title.toLowerCase()]}
|
||||
onOpenChange={(isOpen) =>
|
||||
setExpanded((prev) => ({
|
||||
...prev,
|
||||
[item.title.toLowerCase()]: isOpen
|
||||
}))
|
||||
}
|
||||
>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="flex h-8 w-full items-center justify-between text-left"
|
||||
>
|
||||
<span className="text-base font-medium">
|
||||
{item.title}
|
||||
</span>
|
||||
{expanded[item.title.toLowerCase()] ? (
|
||||
<ChevronUp className="size-4" />
|
||||
) : (
|
||||
<ChevronDown className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<ul className="mt-2 pl-4">
|
||||
{item.items.map((subItem) => (
|
||||
<li key={subItem.title}>
|
||||
<Link
|
||||
href={subItem.href || '#'}
|
||||
target={subItem.external ? '_blank' : undefined}
|
||||
rel={
|
||||
subItem.external
|
||||
? 'noopener noreferrer'
|
||||
: undefined
|
||||
}
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'm-0 h-auto w-full justify-start gap-4 p-2'
|
||||
)}
|
||||
onClick={onLinkClicked}
|
||||
>
|
||||
<div className="flex size-8 shrink-0 items-center justify-center text-muted-foreground transition-colors group-hover:text-foreground">
|
||||
{subItem.icon}
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-medium">
|
||||
{subItem.title}
|
||||
{subItem.external && (
|
||||
<Icons.externalLink className="-mt-2 ml-1 inline text-muted-foreground" />
|
||||
)}
|
||||
</span>
|
||||
{subItem.description && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{subItem.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
) : (
|
||||
<Link
|
||||
href={item.href || '#'}
|
||||
target={item.external ? '_blank' : undefined}
|
||||
rel={item.external ? 'noopener noreferrer' : undefined}
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'w-full justify-start'
|
||||
)}
|
||||
onClick={onLinkClicked}
|
||||
>
|
||||
<span className="text-base">{item.title}</span>
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
{/* bottom buttons */}
|
||||
<div className="flex w-full items-center justify-between gap-2 border-t border-border/40 p-4">
|
||||
<div className="text-base font-medium"></div>
|
||||
<ThemeSwitcherHorizontal />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type DocsMobileMenuProps = {
|
||||
onLinkClicked: () => void;
|
||||
};
|
||||
|
||||
function DocsMobileMenu({
|
||||
onLinkClicked
|
||||
}: DocsMobileMenuProps): React.JSX.Element {
|
||||
const [expanded, setExpanded] = React.useState<Record<string, boolean>>({});
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 mt-[69px] overflow-y-auto bg-background animate-in fade-in-0">
|
||||
<div className="flex size-full flex-col items-start space-y-3 p-4">
|
||||
<ul className="w-full">
|
||||
{DOCS_LINKS.map((item) => (
|
||||
<li
|
||||
key={item.title}
|
||||
className="py-2"
|
||||
>
|
||||
<Collapsible
|
||||
open={expanded[item.title.toLowerCase()]}
|
||||
onOpenChange={(isOpen) =>
|
||||
setExpanded((prev) => ({
|
||||
...prev,
|
||||
[item.title.toLowerCase()]: isOpen
|
||||
}))
|
||||
}
|
||||
>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
className="flex h-9 w-full items-center justify-between px-4 text-left"
|
||||
>
|
||||
<div className="flex flex-row items-center gap-2 text-base font-medium">
|
||||
{item.icon}
|
||||
{item.title}
|
||||
</div>
|
||||
{expanded[item.title.toLowerCase()] ? (
|
||||
<ChevronUp className="size-4" />
|
||||
) : (
|
||||
<ChevronDown className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<ul className="mt-2 pl-4">
|
||||
{item.items.map((subItem) => (
|
||||
<li key={subItem.title}>
|
||||
<Link
|
||||
href={subItem.href || '#'}
|
||||
className={cn(
|
||||
buttonVariants({ variant: 'ghost' }),
|
||||
'm-0 h-auto w-full justify-start gap-4 p-1.5 text-sm font-medium'
|
||||
)}
|
||||
onClick={onLinkClicked}
|
||||
>
|
||||
{subItem.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex w-full items-center justify-between gap-2 border-y border-border/40 p-4">
|
||||
<div className="text-base font-medium"></div>
|
||||
<ThemeSwitcherHorizontal />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
184
src/components/marketing/navbar.tsx
Normal file
184
src/components/marketing/navbar.tsx
Normal file
@ -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 (
|
||||
<section className={cn(
|
||||
"sticky inset-x-0 top-0 z-40 bg-background py-4",
|
||||
scroll ? (scrolled ? "border-b" : "bg-transparent") : "border-b"
|
||||
)}>
|
||||
<Container className="px-4">
|
||||
{/* desktop navbar */}
|
||||
<nav className="hidden lg:flex">
|
||||
{/* logo and name */}
|
||||
<div className="flex items-center">
|
||||
<a href="/" className="flex items-center space-x-2">
|
||||
<Logo />
|
||||
<span className="text-xl font-semibold">{siteConfig.name}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* menu links */}
|
||||
<div className="flex-1 flex items-center justify-center space-x-2">
|
||||
<NavigationMenu className="relative">
|
||||
<NavigationMenuList className="flex items-center">
|
||||
{MENU_LINKS.map((item, index) =>
|
||||
item.items ? (
|
||||
<NavigationMenuItem key={index} className="relative">
|
||||
<NavigationMenuTrigger
|
||||
data-active={
|
||||
item.items.some((subItem) =>
|
||||
pathname.startsWith(subItem.href)
|
||||
)
|
||||
? ''
|
||||
: undefined
|
||||
}
|
||||
className="data-[active]:bg-accent"
|
||||
>
|
||||
{item.title}
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
{/* set the width of the menu content to the width of the navigation menu */}
|
||||
<ul className="w-96 list-none p-2">
|
||||
{item.items.map((subItem, subIndex) => (
|
||||
<li key={subIndex}>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link
|
||||
href={subItem.href || '#'}
|
||||
target={
|
||||
subItem.external ? '_blank' : undefined
|
||||
}
|
||||
rel={
|
||||
subItem.external
|
||||
? 'noopener noreferrer'
|
||||
: undefined
|
||||
}
|
||||
className="group flex select-none flex-row items-center gap-4 rounded-md p-2 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
|
||||
>
|
||||
<div className="flex size-8 shrink-0 items-center justify-center text-muted-foreground transition-colors group-hover:text-foreground">
|
||||
{subItem.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium">
|
||||
{subItem.title}
|
||||
{subItem.external && (
|
||||
<Icons.externalLink className="-mt-2 ml-1 inline text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
{subItem.description && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{subItem.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
) : (
|
||||
<NavigationMenuItem key={index}>
|
||||
<NavigationMenuLink
|
||||
asChild
|
||||
active={pathname.startsWith(item.href)}
|
||||
className={cn(
|
||||
navigationMenuTriggerStyle(),
|
||||
'data-[active]:bg-accent'
|
||||
)}
|
||||
>
|
||||
<Link
|
||||
href={item.href || '#'}
|
||||
target={item.external ? '_blank' : undefined}
|
||||
rel={
|
||||
item.external ? 'noopener noreferrer' : undefined
|
||||
}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
)
|
||||
)}
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
</div>
|
||||
|
||||
{/* navbar right show sign in or user */}
|
||||
<div className="flex items-center gap-x-4">
|
||||
{user ? (
|
||||
<div className="flex items-center">
|
||||
<UserButton />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-x-4">
|
||||
<LoginWrapper mode="modal" asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
<span>Log in</span>
|
||||
</Button>
|
||||
</LoginWrapper>
|
||||
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
asChild
|
||||
>
|
||||
<a href={AUTH_ROUTE_REGISTER}>
|
||||
Sign up
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* mobile navbar */}
|
||||
<NavbarMobile className="lg:hidden" />
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
11
src/types/index.d.ts
vendored
11
src/types/index.d.ts
vendored
@ -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;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user