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-icons": "^1.3.2",
|
||||||
"@radix-ui/react-label": "^2.1.2",
|
"@radix-ui/react-label": "^2.1.2",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.5",
|
"@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-separator": "^1.1.2",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
"@radix-ui/react-tabs": "^1.1.3",
|
"@radix-ui/react-tabs": "^1.1.3",
|
||||||
@ -35,6 +36,7 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"drizzle-orm": "^0.39.3",
|
"drizzle-orm": "^0.39.3",
|
||||||
|
"framer-motion": "^12.4.7",
|
||||||
"geist": "^1.3.1",
|
"geist": "^1.3.1",
|
||||||
"lucide-react": "^0.475.0",
|
"lucide-react": "^0.475.0",
|
||||||
"mdast-util-toc": "^7.1.0",
|
"mdast-util-toc": "^7.1.0",
|
||||||
@ -46,6 +48,7 @@
|
|||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
|
"react-remove-scroll": "^2.6.3",
|
||||||
"rehype-autolink-headings": "^7.1.0",
|
"rehype-autolink-headings": "^7.1.0",
|
||||||
"rehype-pretty-code": "^0.14.0",
|
"rehype-pretty-code": "^0.14.0",
|
||||||
"rehype-slug": "^6.0.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':
|
'@radix-ui/react-navigation-menu':
|
||||||
specifier: ^1.2.5
|
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)
|
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':
|
'@radix-ui/react-separator':
|
||||||
specifier: ^1.1.2
|
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)
|
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:
|
drizzle-orm:
|
||||||
specifier: ^0.39.3
|
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)
|
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:
|
geist:
|
||||||
specifier: ^1.3.1
|
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))
|
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:
|
react-hook-form:
|
||||||
specifier: ^7.54.2
|
specifier: ^7.54.2
|
||||||
version: 7.54.2(react@19.0.0)
|
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:
|
rehype-autolink-headings:
|
||||||
specifier: ^7.1.0
|
specifier: ^7.1.0
|
||||||
version: 7.1.0
|
version: 7.1.0
|
||||||
@ -2355,8 +2364,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
||||||
engines: {node: '>=0.4.x'}
|
engines: {node: '>=0.4.x'}
|
||||||
|
|
||||||
framer-motion@12.4.3:
|
framer-motion@12.4.7:
|
||||||
resolution: {integrity: sha512-rsMeO7w3dKyNG09o3cGwSH49iHU+VgDmfSSfsX+wfkO3zDA6WWkh4sUsMXd155YROjZP+7FTIhDrBYfgZeHjKQ==}
|
resolution: {integrity: sha512-VhrcbtcAMXfxlrjeHPpWVu2+mkcoR31e02aNSR7OUS/hZAciKa8q6o3YN2mA1h+jjscRsSyKvX6E1CiY/7OLMw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@emotion/is-prop-valid': '*'
|
'@emotion/is-prop-valid': '*'
|
||||||
react: ^18.0.0 || ^19.0.0
|
react: ^18.0.0 || ^19.0.0
|
||||||
@ -2811,8 +2820,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
|
||||||
motion-dom@12.0.0:
|
motion-dom@12.4.5:
|
||||||
resolution: {integrity: sha512-CvYd15OeIR6kHgMdonCc1ihsaUG4MYh/wrkz8gZ3hBX/uamyZCXN9S9qJoYF03GqfTt7thTV/dxnHYX4+55vDg==}
|
resolution: {integrity: sha512-Q2xmhuyYug1CGTo0jdsL05EQ4RhIYXlggFS/yPhQQRNzbrhjKQ1tbjThx5Plv68aX31LsUQRq4uIkuDxdO5vRQ==}
|
||||||
|
|
||||||
motion-utils@12.0.0:
|
motion-utils@12.0.0:
|
||||||
resolution: {integrity: sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==}
|
resolution: {integrity: sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==}
|
||||||
@ -5562,9 +5571,9 @@ snapshots:
|
|||||||
|
|
||||||
format@0.2.2: {}
|
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:
|
dependencies:
|
||||||
motion-dom: 12.0.0
|
motion-dom: 12.4.5
|
||||||
motion-utils: 12.0.0
|
motion-utils: 12.0.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@ -6361,7 +6370,7 @@ snapshots:
|
|||||||
|
|
||||||
minipass@7.1.2: {}
|
minipass@7.1.2: {}
|
||||||
|
|
||||||
motion-dom@12.0.0:
|
motion-dom@12.4.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
motion-utils: 12.0.0
|
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):
|
motion@12.4.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||||
dependencies:
|
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
|
tslib: 2.8.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
react: 19.0.0
|
react: 19.0.0
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Footer } from "@/components/layout/footer";
|
import { Footer } from "@/components/layout/footer";
|
||||||
import { Navbar } from "@/components/layout/navbar";
|
import { Navbar } from "@/components/marketing/navbar";
|
||||||
import { marketingConfig } from "@/config/marketing";
|
import { marketingConfig } from "@/config/marketing";
|
||||||
|
|
||||||
interface MarketingLayoutProps {
|
interface MarketingLayoutProps {
|
||||||
@ -11,7 +11,8 @@ export default async function MarketingLayout({
|
|||||||
}: MarketingLayoutProps) {
|
}: MarketingLayoutProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col min-h-screen">
|
<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>
|
<main className="flex-1">{children}</main>
|
||||||
|
|
||||||
|
@ -3,28 +3,29 @@
|
|||||||
import { LoginWrapper } from "@/components/auth/login-button";
|
import { LoginWrapper } from "@/components/auth/login-button";
|
||||||
import Container from "@/components/container";
|
import Container from "@/components/container";
|
||||||
import { Icons } from "@/components/icons/icons";
|
import { Icons } from "@/components/icons/icons";
|
||||||
import { ModeToggle } from "@/components/layout/mode-toggle";
|
|
||||||
import { UserButton } from "@/components/layout/user-button";
|
import { UserButton } from "@/components/layout/user-button";
|
||||||
|
import { Logo } from "@/components/logo";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
NavigationMenu,
|
NavigationMenu,
|
||||||
|
NavigationMenuContent,
|
||||||
NavigationMenuItem,
|
NavigationMenuItem,
|
||||||
NavigationMenuLink,
|
NavigationMenuLink,
|
||||||
NavigationMenuList,
|
NavigationMenuList,
|
||||||
|
NavigationMenuTrigger,
|
||||||
navigationMenuTriggerStyle,
|
navigationMenuTriggerStyle,
|
||||||
} from "@/components/ui/navigation-menu";
|
} 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 { siteConfig } from "@/config/site";
|
||||||
import { useScroll } from "@/hooks/use-scroll";
|
import { useScroll } from "@/hooks/use-scroll";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { DashboardConfig, MarketingConfig } from "@/types";
|
import type { DashboardConfig, MarketingConfig, NestedNavItem } from "@/types";
|
||||||
import { ArrowRightIcon, MenuIcon } from "lucide-react";
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@radix-ui/react-accordion";
|
||||||
|
import { MenuIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import React from "react";
|
|
||||||
import { Logo } from "@/components/logo";
|
|
||||||
import { authClient } from "@/lib/auth-client";
|
|
||||||
|
|
||||||
interface NavBarProps {
|
interface NavBarProps {
|
||||||
scroll?: boolean;
|
scroll?: boolean;
|
||||||
@ -39,10 +40,9 @@ export function Navbar({ scroll = false, config }: NavBarProps) {
|
|||||||
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
// console.log(`Navbar, pathname: ${pathname}`);
|
// console.log(`Navbar, pathname: ${pathname}`);
|
||||||
const links = config.menus;
|
const menus = config.menus;
|
||||||
// console.log(`Navbar, links: ${links.map((link) => link.title)}`);
|
|
||||||
|
|
||||||
const isLinkActive = (href: string) => {
|
const isMenuActive = (href: string) => {
|
||||||
if (href === "/") {
|
if (href === "/") {
|
||||||
return pathname === "/";
|
return pathname === "/";
|
||||||
}
|
}
|
||||||
@ -82,27 +82,10 @@ export function Navbar({ scroll = false, config }: NavBarProps) {
|
|||||||
|
|
||||||
{/* links */}
|
{/* links */}
|
||||||
<div className="flex-1 flex justify-center">
|
<div className="flex-1 flex justify-center">
|
||||||
{links && links.length > 0 ? (
|
{menus && menus.length > 0 ? (
|
||||||
<NavigationMenu>
|
<NavigationMenu>
|
||||||
<NavigationMenuList>
|
<NavigationMenuList>
|
||||||
{links.map((item) => (
|
{menus.map((item) => renderMenuItem(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>
|
|
||||||
))}
|
|
||||||
</NavigationMenuList>
|
</NavigationMenuList>
|
||||||
</NavigationMenu>
|
</NavigationMenu>
|
||||||
) : null}
|
) : null}
|
||||||
@ -117,17 +100,17 @@ export function Navbar({ scroll = false, config }: NavBarProps) {
|
|||||||
) : (
|
) : (
|
||||||
<LoginWrapper mode="modal" asChild>
|
<LoginWrapper mode="modal" asChild>
|
||||||
<Button
|
<Button
|
||||||
className="flex gap-2 px-5 rounded-full"
|
className="flex gap-2"
|
||||||
variant="default"
|
variant="default"
|
||||||
size="default"
|
size="sm"
|
||||||
>
|
>
|
||||||
<span>Sign In</span>
|
<span>Login</span>
|
||||||
<ArrowRightIcon className="size-4" />
|
{/* <ArrowRightIcon className="size-4" /> */}
|
||||||
</Button>
|
</Button>
|
||||||
</LoginWrapper>
|
</LoginWrapper>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ModeToggle />
|
{/* <ModeToggle /> */}
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</header>
|
</header>
|
||||||
@ -148,47 +131,26 @@ export function Navbar({ scroll = false, config }: NavBarProps) {
|
|||||||
<span className="sr-only">Toggle navigation menu</span>
|
<span className="sr-only">Toggle navigation menu</span>
|
||||||
</Button>
|
</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent side="left" className="flex flex-col p-0">
|
<SheetContent side="left" className="overflow-y-auto">
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle />
|
<SheetTitle>
|
||||||
</SheetHeader>
|
|
||||||
<div className="flex h-screen flex-col">
|
|
||||||
{/* logo */}
|
|
||||||
<a href="/"
|
<a href="/"
|
||||||
className="flex items-center space-x-2 pl-4 pt-4"
|
className="flex items-center space-x-2"
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
>
|
>
|
||||||
<Logo />
|
<Logo />
|
||||||
|
|
||||||
<span className="text-xl font-bold">{siteConfig.name}</span>
|
<span className="text-xl font-bold">{siteConfig.name}</span>
|
||||||
</a>
|
</a>
|
||||||
|
</SheetTitle>
|
||||||
<nav className="flex flex-1 flex-col gap-2 p-2 pt-8 font-medium">
|
</SheetHeader>
|
||||||
{links.map((item) => {
|
<div className="my-6 flex flex-col gap-6">
|
||||||
const Icon = Icons[item.icon || "arrowRight"];
|
<Accordion
|
||||||
return (
|
type="single"
|
||||||
<Link
|
collapsible
|
||||||
key={item.title}
|
className="flex w-full flex-col gap-4"
|
||||||
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" />
|
{menus.map((item) => renderMobileMenuItem(item))}
|
||||||
{item.title}
|
</Accordion>
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
@ -204,7 +166,7 @@ export function Navbar({ scroll = false, config }: NavBarProps) {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</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">
|
<div className="flex items-center gap-x-4">
|
||||||
{user ? (
|
{user ? (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@ -213,20 +175,116 @@ export function Navbar({ scroll = false, config }: NavBarProps) {
|
|||||||
) : (
|
) : (
|
||||||
<LoginWrapper mode="redirect" asChild>
|
<LoginWrapper mode="redirect" asChild>
|
||||||
<Button
|
<Button
|
||||||
className="flex gap-2 px-5 rounded-full"
|
className="flex gap-2"
|
||||||
variant="default"
|
variant="default"
|
||||||
size="default"
|
size="sm"
|
||||||
>
|
>
|
||||||
<span>Sign In</span>
|
<span>Login</span>
|
||||||
<ArrowRightIcon className="size-4" />
|
{/* <ArrowRightIcon className="size-4" /> */}
|
||||||
</Button>
|
</Button>
|
||||||
</LoginWrapper>
|
</LoginWrapper>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ModeToggle />
|
{/* <ModeToggle /> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
@ -17,10 +17,27 @@ export const marketingConfig: MarketingConfig = {
|
|||||||
href: "/blog",
|
href: "/blog",
|
||||||
icon: "blog",
|
icon: "blog",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Pages",
|
||||||
|
href: "#",
|
||||||
|
icon: "about",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
title: "Waitlist",
|
||||||
|
href: "/waitlist",
|
||||||
|
icon: "about",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Contact",
|
||||||
|
href: "/contact",
|
||||||
|
icon: "about",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "About",
|
title: "About",
|
||||||
href: "/about",
|
href: "/about",
|
||||||
icon: "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: {
|
links: {
|
||||||
github?: string;
|
github?: string;
|
||||||
twitter?: string;
|
twitter?: string;
|
||||||
twitter_cn?: string;
|
|
||||||
bluesky?: string;
|
bluesky?: string;
|
||||||
youtube?: string;
|
youtube?: string;
|
||||||
docs?: string;
|
docs?: string;
|
||||||
@ -54,11 +53,11 @@ export type HeroConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type MarketingConfig = {
|
export type MarketingConfig = {
|
||||||
menus: NavItem[];
|
menus: NestedNavItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DashboardConfig = {
|
export type DashboardConfig = {
|
||||||
menus: NavItem[];
|
menus: NestedNavItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserButtonConfig = {
|
export type UserButtonConfig = {
|
||||||
@ -81,7 +80,11 @@ export type NavItem = {
|
|||||||
|
|
||||||
export type NestedNavItem = {
|
export type NestedNavItem = {
|
||||||
title: string;
|
title: string;
|
||||||
items: NavItem[];
|
items?: NavItem[];
|
||||||
|
href?: string;
|
||||||
|
badge?: number;
|
||||||
|
disabled?: boolean;
|
||||||
|
external?: boolean;
|
||||||
authorizeOnly?: UserRole;
|
authorizeOnly?: UserRole;
|
||||||
icon?: keyof typeof Icons;
|
icon?: keyof typeof Icons;
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user