refactor: consolidate blog components and improve file structure
- Move MDX component from marketing to shared folder - Remove deprecated blog post and blog posts components - Update blog page imports to use new MDX component location - Add reading time estimation utility function - Reorganize navbar and mobile navbar components - Update marketing links configuration - Improve component modularity and code organization
This commit is contained in:
parent
7875c19a3c
commit
431e61d2a5
@ -65,7 +65,8 @@
|
||||
"categories": "Categories",
|
||||
"tableOfContents": "Table of Contents",
|
||||
"all": "All",
|
||||
"noPostsFound": "No posts found"
|
||||
"noPostsFound": "No posts found",
|
||||
"allPosts": "All Posts"
|
||||
},
|
||||
"mail": {
|
||||
"common": {
|
||||
|
@ -65,7 +65,8 @@
|
||||
"categories": "分类",
|
||||
"tableOfContents": "目录",
|
||||
"all": "全部",
|
||||
"noPostsFound": "没有找到文章"
|
||||
"noPostsFound": "没有找到文章",
|
||||
"allPosts": "全部文章"
|
||||
},
|
||||
"mail": {
|
||||
"common": {
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 104 KiB |
@ -1,10 +1,10 @@
|
||||
import AllPostsButton from '@/components/blog/all-posts-button';
|
||||
import { BlogToc } from '@/components/blog/blog-toc';
|
||||
import { Mdx } from '@/components/marketing/blog/mdx-component';
|
||||
import { Mdx } from '@/components/shared/mdx-component';
|
||||
import { LocaleLink } from '@/i18n/navigation';
|
||||
import { getTableOfContents } from '@/lib/toc';
|
||||
import { getBaseUrl } from '@/lib/urls/get-base-url';
|
||||
import { getLocaleDate } from '@/lib/utils';
|
||||
import { estimateReadingTime, getLocaleDate } from '@/lib/utils';
|
||||
import type { NextPageProps } from '@/types/next-page-props';
|
||||
import { allPosts } from 'content-collections';
|
||||
import type { Metadata } from 'next';
|
||||
@ -113,6 +113,12 @@ export default async function BlogPostPage(props: NextPageProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* blog post date */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-sm text-muted-foreground">{date}</p>
|
||||
<p className="text-sm text-muted-foreground">{estimateReadingTime(post.body.raw)}</p>
|
||||
</div>
|
||||
|
||||
{/* blog post title */}
|
||||
<h1 className="text-3xl font-bold">{post.title}</h1>
|
||||
|
||||
@ -137,7 +143,7 @@ export default async function BlogPostPage(props: NextPageProps) {
|
||||
<div className="bg-muted/50 rounded-lg p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">{t("publisher")}</h2>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative h-12 w-12 flex-shrink-0">
|
||||
<div className="relative h-8 w-8 flex-shrink-0">
|
||||
{post.author?.avatar && (
|
||||
<Image
|
||||
src={post.author.avatar}
|
||||
@ -147,11 +153,7 @@ export default async function BlogPostPage(props: NextPageProps) {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<span>{post.author?.name}</span>
|
||||
|
||||
<p className="text-sm text-muted-foreground">{date}</p>
|
||||
</div>
|
||||
<span className="line-clamp-1">{post.author?.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { marketingConfig } from "@/config/marketing";
|
||||
import { Footer } from "@/components/layout/footer";
|
||||
import { Navbar } from "@/components/marketing/navbar";
|
||||
import { Navbar } from "@/components/layout/navbar";
|
||||
|
||||
export default function MarketingLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
|
@ -1,8 +1,5 @@
|
||||
import { fontSourceSans, fontSourceSerif4 } from "@/assets/fonts";
|
||||
import { Footer } from '@/components/layout/footer';
|
||||
import { Navbar } from '@/components/marketing/navbar';
|
||||
import { TailwindIndicator } from '@/components/tailwind-indicator';
|
||||
import { marketingConfig } from '@/config/marketing';
|
||||
import { routing } from '@/i18n/routing';
|
||||
import { cn } from "@/lib/utils";
|
||||
import { GeistMono } from "geist/font/mono";
|
||||
|
@ -1,10 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowLeftIcon } from "lucide-react";
|
||||
import { LocaleLink } from "@/i18n/navigation";
|
||||
import { ArrowLeftIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function AllPostsButton() {
|
||||
const t = useTranslations("BlogPage");
|
||||
return (
|
||||
<Button
|
||||
size="lg"
|
||||
@ -13,11 +15,10 @@ export default function AllPostsButton() {
|
||||
asChild
|
||||
>
|
||||
<LocaleLink href="/blog">
|
||||
<ArrowLeftIcon
|
||||
className="w-5 h-5
|
||||
transition-transform duration-200 group-hover:-translate-x-1"
|
||||
<ArrowLeftIcon className="w-5 h-5
|
||||
transition-transform duration-200 group-hover:-translate-x-1"
|
||||
/>
|
||||
<span>All Posts</span>
|
||||
<span>{t("allPosts")}</span>
|
||||
</LocaleLink>
|
||||
</Button>
|
||||
);
|
||||
|
@ -1,12 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import FilterItemMobile from "@/components/shared/filter-item-mobile";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerOverlay,
|
||||
DrawerPortal,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/components/ui/drawer";
|
||||
import { Category } from "content-collections";
|
||||
import { LayoutListIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { Drawer } from "vaul";
|
||||
import FilterItemMobile from "@/components/shared/filter-item-mobile";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export type BlogCategoryListMobileProps = {
|
||||
categoryList: Category[];
|
||||
@ -27,8 +34,8 @@ export function BlogCategoryListMobile({
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer.Root open={open} onClose={closeDrawer}>
|
||||
<Drawer.Trigger
|
||||
<Drawer open={open} onClose={closeDrawer}>
|
||||
<DrawerTrigger
|
||||
onClick={() => setOpen(true)}
|
||||
className="flex items-center w-full p-3 border-y text-foreground/90"
|
||||
>
|
||||
@ -41,14 +48,11 @@ export function BlogCategoryListMobile({
|
||||
{selectedCategory?.name ? `${selectedCategory?.name}` : t("all")}
|
||||
</span>
|
||||
</div>
|
||||
</Drawer.Trigger>
|
||||
<Drawer.Overlay
|
||||
className="fixed inset-0 z-40 bg-background/80 backdrop-blur-sm"
|
||||
onClick={closeDrawer}
|
||||
/>
|
||||
<Drawer.Portal>
|
||||
<Drawer.Content className="fixed inset-x-0 bottom-0 z-50 mt-24 overflow-hidden rounded-t-[10px] border bg-background">
|
||||
<Drawer.Title className="sr-only">{t("categories")}</Drawer.Title>
|
||||
</DrawerTrigger>
|
||||
<DrawerPortal>
|
||||
<DrawerOverlay className="fixed inset-0 z-40 bg-background/50" />
|
||||
<DrawerContent className="fixed inset-x-0 bottom-0 z-50 mt-24 overflow-hidden rounded-t-[10px] border bg-background">
|
||||
<DrawerTitle className="sr-only">{t("categories")}</DrawerTitle>
|
||||
<div className="sticky top-0 z-20 flex w-full items-center justify-center bg-inherit">
|
||||
<div className="my-3 h-1.5 w-16 rounded-full bg-muted-foreground/20" />
|
||||
</div>
|
||||
@ -71,9 +75,8 @@ export function BlogCategoryListMobile({
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</Drawer.Content>
|
||||
<Drawer.Overlay />
|
||||
</Drawer.Portal>
|
||||
</Drawer.Root>
|
||||
</DrawerContent>
|
||||
</DrawerPortal>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
@ -1,78 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { format } from 'date-fns';
|
||||
import { Mdx } from '@/components/marketing/blog/mdx-component';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { getInitials } from '@/lib/utils';
|
||||
import { Post } from 'content-collections';
|
||||
|
||||
type BlogPostProps = {
|
||||
post: Post;
|
||||
};
|
||||
|
||||
export function BlogPost({ post }: BlogPostProps): React.JSX.Element {
|
||||
console.log(post);
|
||||
console.log(post.author);
|
||||
return (
|
||||
<div className="border-b">
|
||||
<div className="container mx-auto flex max-w-3xl flex-col space-y-4 py-20">
|
||||
<div className="mx-auto w-full min-w-0">
|
||||
<Link
|
||||
href="/blog"
|
||||
className="group mb-12 flex items-center space-x-1 text-base leading-none text-foreground duration-200"
|
||||
>
|
||||
<span className="transition-transform group-hover:-translate-x-0.5">
|
||||
←
|
||||
</span>
|
||||
<span>All posts</span>
|
||||
</Link>
|
||||
<div className="space-y-8">
|
||||
<div className="flex flex-row items-center justify-between gap-4 text-base text-muted-foreground">
|
||||
<span className="flex flex-row items-center gap-2">
|
||||
{post.categories.map((c) => c?.name).join(', ')}
|
||||
</span>
|
||||
<span className="flex flex-row items-center gap-2">
|
||||
<time dateTime={post.date}>
|
||||
{format(post.date, 'dd MMM yyyy')}
|
||||
</time>
|
||||
</span>
|
||||
</div>
|
||||
<h1 className="font-heading text-3xl font-semibold tracking-tighter xl:text-5xl">
|
||||
{post.title}
|
||||
</h1>
|
||||
<p className="text-lg text-muted-foreground">{post.description}</p>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Avatar className="relative size-7 flex-none rounded-full">
|
||||
<AvatarImage
|
||||
src={post.author?.avatar}
|
||||
alt="avatar"
|
||||
/>
|
||||
<AvatarFallback className="size-7 text-[10px]">
|
||||
{getInitials(post.author?.name ?? '')}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span>{post.author?.name ?? ''}</span>
|
||||
</div>
|
||||
<div>{estimateReadingTime(post.body.raw)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="container mx-auto flex max-w-3xl py-20">
|
||||
<Mdx code={post.body.code} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function estimateReadingTime(
|
||||
text: string,
|
||||
wordsPerMinute: number = 250
|
||||
): string {
|
||||
const words = text.trim().split(/\s+/).length;
|
||||
const minutes = Math.ceil(words / wordsPerMinute);
|
||||
return minutes === 1 ? '1 minute read' : `${minutes} minutes read`;
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import Link from 'next/link';
|
||||
import { allPosts } from 'content-collections';
|
||||
import { format, isBefore } from 'date-fns';
|
||||
import { ArrowRightIcon } from 'lucide-react';
|
||||
import { GridSection } from '@/components/marketing/fragments/grid-section';
|
||||
import { SiteHeading } from '@/components/marketing/fragments/site-heading';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { FillRemainingSpace } from '@/components/fill-remaining-space';
|
||||
import { getBaseUrl } from '@/lib/urls/get-base-url';
|
||||
import { getInitials } from '@/lib/utils';
|
||||
import { useLocale } from 'next-intl';
|
||||
|
||||
export function BlogPosts(): React.JSX.Element {
|
||||
const locale = useLocale();
|
||||
|
||||
// Filter posts by current locale
|
||||
const localizedPosts = allPosts
|
||||
.filter(post => post.published && post.locale === locale)
|
||||
.slice()
|
||||
.sort((a, b) => (isBefore(a.date, b.date) ? 1 : -1));
|
||||
|
||||
return (
|
||||
<GridSection>
|
||||
<div className="container space-y-20 py-20">
|
||||
<SiteHeading
|
||||
badge="Blog Posts"
|
||||
title="Insights & News"
|
||||
description="Learn more about our products and the latest news."
|
||||
/>
|
||||
<div className="grid gap-x-12 gap-y-6 divide-y md:grid-cols-2 md:gap-x-6 md:divide-none xl:grid-cols-3">
|
||||
{localizedPosts.map((post, index) => (
|
||||
<Link
|
||||
key={index}
|
||||
href={`${getBaseUrl()}${post.slug}`}
|
||||
className="md:duration-2000 flex h-full flex-col space-y-4 text-clip border-dashed py-6 md:rounded-2xl md:px-6 md:shadow md:transition-shadow md:hover:shadow-xl dark:md:bg-accent/40 dark:md:hover:bg-accent"
|
||||
>
|
||||
<div className="flex flex-row items-center justify-between text-muted-foreground">
|
||||
<span className="text-sm">{post.categories.map((c) => c?.name).join(', ')}</span>
|
||||
<time
|
||||
className="text-sm"
|
||||
dateTime={post.date}
|
||||
>
|
||||
{format(post.date, 'dd MMM yyyy')}
|
||||
</time>
|
||||
</div>
|
||||
<h2 className="text-lg font-semibold md:mb-4 md:text-xl lg:mb-6">
|
||||
{post.title}
|
||||
</h2>
|
||||
<p className="line-clamp-3 text-muted-foreground md:mb-4 lg:mb-6">
|
||||
{post.description}
|
||||
</p>
|
||||
<FillRemainingSpace />
|
||||
<div className="flex flex-1 shrink flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<Avatar className="relative size-7 flex-none rounded-full">
|
||||
<AvatarImage
|
||||
src={post.author?.avatar}
|
||||
alt="avatar"
|
||||
/>
|
||||
<AvatarFallback className="size-7 text-[10px]">
|
||||
{getInitials(post.author?.name ?? '')}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="text-sm">{post.author?.name ?? ''}</span>
|
||||
</div>
|
||||
<div className="group flex items-center gap-2 text-sm duration-200 hover:underline">
|
||||
Read more
|
||||
<ArrowRightIcon className="size-4 shrink-0 transition-transform group-hover:translate-x-0.5" />
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</GridSection>
|
||||
);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { Logo } from '@/components/logo';
|
||||
import { DOCS_LINKS, MENU_LINKS } from '@/components/marketing/marketing-links';
|
||||
import { MENU_LINKS } from '@/config/marketing-links';
|
||||
import { Button, buttonVariants } from '@/components/ui/button';
|
||||
import {
|
||||
Collapsible,
|
||||
@ -27,7 +27,6 @@ export function NavbarMobile({
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
const [open, setOpen] = React.useState<boolean>(false);
|
||||
const pathname = usePathname();
|
||||
const isDocs = pathname.startsWith('/docs');
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleRouteChangeStart = () => {
|
||||
@ -90,11 +89,7 @@ export function NavbarMobile({
|
||||
{open && (
|
||||
<Portal asChild>
|
||||
<RemoveScroll allowPinchZoom enabled>
|
||||
{isDocs ? (
|
||||
<DocsMobileMenu onLinkClicked={handleToggleMobileMenu} />
|
||||
) : (
|
||||
<MainMobileMenu onLinkClicked={handleToggleMobileMenu} />
|
||||
)}
|
||||
<MainMobileMenu onLinkClicked={handleToggleMobileMenu} />
|
||||
</RemoveScroll>
|
||||
</Portal>
|
||||
)}
|
||||
@ -238,77 +233,3 @@ function MainMobileMenu({ onLinkClicked }: MainMobileMenuProps) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface 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/80 backdrop-blur-md supports-[backdrop-filter]:bg-background/60 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 py-4">
|
||||
<LocaleSelector />
|
||||
<ThemeSwitcherHorizontal />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { LoginWrapper } from "@/components/auth/login-button";
|
||||
import Container from "@/components/container";
|
||||
import { Icons } from "@/components/icons/icons";
|
||||
import { UserButton } from "@/components/layout/user-button";
|
||||
import { Logo } from "@/components/logo";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { LoginWrapper } from '@/components/auth/login-button';
|
||||
import Container from '@/components/container';
|
||||
import { ThemeSwitcher } from '@/components/layout/theme-switcher';
|
||||
import { UserButton } from '@/components/layout/user-button';
|
||||
import { Logo } from '@/components/logo';
|
||||
import { MENU_LINKS } from '@/config/marketing-links';
|
||||
import { NavbarMobile } from '@/components/layout/navbar-mobile';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
@ -13,274 +15,180 @@ import {
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
navigationMenuTriggerStyle,
|
||||
} from "@/components/ui/navigation-menu";
|
||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
|
||||
import { siteConfig } from "@/config/site";
|
||||
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 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 { authClient } from '@/lib/auth-client';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Routes } from '@/routes';
|
||||
import { MarketingConfig } from '@/types';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { ArrowUpRightIcon } from 'lucide-react';
|
||||
import LocaleSelector from '@/components/layout/locale-selector';
|
||||
import { LocaleLink } from '@/i18n/navigation';
|
||||
|
||||
interface NavBarProps {
|
||||
scroll?: boolean;
|
||||
config: DashboardConfig | MarketingConfig;
|
||||
config: MarketingConfig;
|
||||
}
|
||||
|
||||
export function Navbar({ scroll = false, config }: NavBarProps) {
|
||||
const customNavigationMenuTriggerStyle = cn(
|
||||
navigationMenuTriggerStyle(),
|
||||
"bg-transparent hover:bg-transparent hover:text-primary focus:bg-transparent focus:text-primary data-[active]:bg-transparent data-[active]:text-primary data-[state=open]:bg-transparent data-[state=open]:text-primary relative data-[active]:font-bold dark:text-gray-400 dark:hover:text-gray-300 dark:data-[active]:text-white"
|
||||
);
|
||||
|
||||
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);
|
||||
// console.log(`Navbar, user:`, user);
|
||||
|
||||
const pathname = usePathname();
|
||||
// console.log(`Navbar, pathname: ${pathname}`);
|
||||
const menus = config.menus;
|
||||
|
||||
const isMenuActive = (href: string) => {
|
||||
if (href === "/") {
|
||||
return pathname === "/";
|
||||
}
|
||||
// console.log(`Navbar, href: ${href}, pathname: ${pathname}`);
|
||||
return pathname.startsWith(href);
|
||||
};
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
// prevent body scroll when modal is open
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
document.body.style.overflow = "auto";
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-40 w-full">
|
||||
{/* Desktop View */}
|
||||
<header
|
||||
className={cn(
|
||||
"hidden md:flex justify-center bg-background/60 backdrop-blur-xl transition-all",
|
||||
scroll ? (scrolled ? "border-b" : "bg-transparent") : "border-b",
|
||||
)}
|
||||
>
|
||||
<Container className="flex h-16 items-center px-4">
|
||||
{/* navbar left show logo and links */}
|
||||
<div className="flex items-center gap-6 md:gap-10">
|
||||
{/* logo */}
|
||||
<a href="/" className="flex items-center space-x-2">
|
||||
<section className={cn(
|
||||
"sticky inset-x-0 top-0 z-40 py-4 transition-all duration-300",
|
||||
scroll ? (
|
||||
scrolled
|
||||
? "bg-background/80 backdrop-blur-md border-b supports-[backdrop-filter]:bg-background/60"
|
||||
: "bg-background"
|
||||
) : "border-b bg-background"
|
||||
)}>
|
||||
<Container className="px-4">
|
||||
{/* desktop navbar */}
|
||||
<nav className="hidden lg:flex">
|
||||
{/* logo and name */}
|
||||
<div className="flex items-center">
|
||||
<LocaleLink href="/" className="flex items-center space-x-2">
|
||||
<Logo />
|
||||
|
||||
<span className="text-xl font-bold">{siteConfig.name}</span>
|
||||
</a>
|
||||
<span className="text-xl font-semibold">{siteConfig.name}</span>
|
||||
</LocaleLink>
|
||||
</div>
|
||||
|
||||
{/* links */}
|
||||
<div className="flex-1 flex justify-center">
|
||||
{menus && menus.length > 0 ? (
|
||||
<NavigationMenu>
|
||||
<NavigationMenuList>
|
||||
{menus.map((item) => renderMenuItem(item))}
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
) : null}
|
||||
{/* 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)
|
||||
) ? "true" : undefined
|
||||
}
|
||||
className={cn(
|
||||
customNavigationMenuTriggerStyle,
|
||||
"data-[active]:text-primary data-[active]:font-bold dark:data-[active]:text-white"
|
||||
)}
|
||||
>
|
||||
{item.title}
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<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 className="flex-1">
|
||||
<div className="text-sm font-medium">
|
||||
{subItem.title}
|
||||
</div>
|
||||
{subItem.description && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{subItem.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{subItem.external && (
|
||||
<ArrowUpRightIcon className="size-4 shrink-0 text-muted-foreground transition-colors group-hover:text-foreground" />
|
||||
)}
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
) : (
|
||||
<NavigationMenuItem key={index}>
|
||||
<NavigationMenuLink
|
||||
asChild
|
||||
active={pathname.startsWith(item.href)}
|
||||
className={cn(
|
||||
customNavigationMenuTriggerStyle,
|
||||
"data-[active]:text-primary data-[active]:font-bold dark:data-[active]:text-white"
|
||||
)}
|
||||
>
|
||||
<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 account */}
|
||||
{/* navbar right show sign in or user */}
|
||||
<div className="flex items-center gap-x-4">
|
||||
{user ? (
|
||||
<div className="flex items-center">
|
||||
<UserButton />
|
||||
</div>
|
||||
) : (
|
||||
<LoginWrapper mode="modal" asChild>
|
||||
<Button
|
||||
className="flex gap-2"
|
||||
variant="default"
|
||||
size="sm"
|
||||
>
|
||||
<span>Login</span>
|
||||
{/* <ArrowRightIcon className="size-4" /> */}
|
||||
</Button>
|
||||
</LoginWrapper>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
</header>
|
||||
|
||||
{/* Mobile View */}
|
||||
<header className="md:hidden flex justify-center bg-background/60 backdrop-blur-xl transition-all">
|
||||
<div className="w-full px-4 h-16 flex items-center justify-between">
|
||||
{/* mobile navbar left show menu icon when closed & show sheet when menu is open */}
|
||||
<div className="flex items-center gap-x-4">
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-9 shrink-0"
|
||||
>
|
||||
<MenuIcon className="size-5" />
|
||||
<span className="sr-only">Toggle navigation menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="overflow-y-auto">
|
||||
<SheetHeader>
|
||||
<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="my-6 flex flex-col gap-6">
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
className="flex w-full flex-col gap-4"
|
||||
<div className="flex items-center gap-x-4">
|
||||
<LoginWrapper mode="modal" asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
{menus.map((item) => renderMobileMenuItem(item))}
|
||||
</Accordion>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<span>Log in</span>
|
||||
</Button>
|
||||
</LoginWrapper>
|
||||
|
||||
{/* logo */}
|
||||
<a href="/"
|
||||
className="flex items-center space-x-2"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<Logo className="size-8" />
|
||||
|
||||
<span className="text-xl font-bold">{siteConfig.name}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* mobile navbar right show sign in or user button */}
|
||||
<div className="flex items-center gap-x-4">
|
||||
{user ? (
|
||||
<div className="flex items-center">
|
||||
<UserButton />
|
||||
</div>
|
||||
) : (
|
||||
<LoginWrapper mode="redirect" asChild>
|
||||
<Button
|
||||
className="flex gap-2"
|
||||
variant="default"
|
||||
size="sm"
|
||||
asChild
|
||||
>
|
||||
<span>Login</span>
|
||||
{/* <ArrowRightIcon className="size-4" /> */}
|
||||
<LocaleLink href={Routes.Register}>
|
||||
Sign up
|
||||
</LocaleLink>
|
||||
</Button>
|
||||
</LoginWrapper>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<LocaleSelector />
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* mobile navbar */}
|
||||
<NavbarMobile className="lg:hidden" />
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
286
src/components/legacy/navbar.tsx
Normal file
286
src/components/legacy/navbar.tsx
Normal file
@ -0,0 +1,286 @@
|
||||
"use client";
|
||||
|
||||
import { LoginWrapper } from "@/components/auth/login-button";
|
||||
import Container from "@/components/container";
|
||||
import { Icons } from "@/components/icons/icons";
|
||||
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, 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, 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";
|
||||
|
||||
interface NavBarProps {
|
||||
scroll?: boolean;
|
||||
config: DashboardConfig | MarketingConfig;
|
||||
}
|
||||
|
||||
export function Navbar({ scroll = false, config }: NavBarProps) {
|
||||
const scrolled = useScroll(50);
|
||||
const { data: session, error } = authClient.useSession();
|
||||
const user = session?.user;
|
||||
console.log(`Navbar, user:`, user);
|
||||
|
||||
const pathname = usePathname();
|
||||
// console.log(`Navbar, pathname: ${pathname}`);
|
||||
const menus = config.menus;
|
||||
|
||||
const isMenuActive = (href: string) => {
|
||||
if (href === "/") {
|
||||
return pathname === "/";
|
||||
}
|
||||
// console.log(`Navbar, href: ${href}, pathname: ${pathname}`);
|
||||
return pathname.startsWith(href);
|
||||
};
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
// prevent body scroll when modal is open
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
document.body.style.overflow = "hidden";
|
||||
} else {
|
||||
document.body.style.overflow = "auto";
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-40 w-full">
|
||||
{/* Desktop View */}
|
||||
<header
|
||||
className={cn(
|
||||
"hidden md:flex justify-center bg-background/60 backdrop-blur-xl transition-all",
|
||||
scroll ? (scrolled ? "border-b" : "bg-transparent") : "border-b",
|
||||
)}
|
||||
>
|
||||
<Container className="flex h-16 items-center px-4">
|
||||
{/* navbar left show logo and links */}
|
||||
<div className="flex items-center gap-6 md:gap-10">
|
||||
{/* logo */}
|
||||
<a href="/" className="flex items-center space-x-2">
|
||||
<Logo />
|
||||
|
||||
<span className="text-xl font-bold">{siteConfig.name}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* links */}
|
||||
<div className="flex-1 flex justify-center">
|
||||
{menus && menus.length > 0 ? (
|
||||
<NavigationMenu>
|
||||
<NavigationMenuList>
|
||||
{menus.map((item) => renderMenuItem(item))}
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* navbar right show sign in or account */}
|
||||
<div className="flex items-center gap-x-4">
|
||||
{user ? (
|
||||
<div className="flex items-center">
|
||||
<UserButton />
|
||||
</div>
|
||||
) : (
|
||||
<LoginWrapper mode="modal" asChild>
|
||||
<Button
|
||||
className="flex gap-2"
|
||||
variant="default"
|
||||
size="sm"
|
||||
>
|
||||
<span>Login</span>
|
||||
{/* <ArrowRightIcon className="size-4" /> */}
|
||||
</Button>
|
||||
</LoginWrapper>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
</header>
|
||||
|
||||
{/* Mobile View */}
|
||||
<header className="md:hidden flex justify-center bg-background/60 backdrop-blur-xl transition-all">
|
||||
<div className="w-full px-4 h-16 flex items-center justify-between">
|
||||
{/* mobile navbar left show menu icon when closed & show sheet when menu is open */}
|
||||
<div className="flex items-center gap-x-4">
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-9 shrink-0"
|
||||
>
|
||||
<MenuIcon className="size-5" />
|
||||
<span className="sr-only">Toggle navigation menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="overflow-y-auto">
|
||||
<SheetHeader>
|
||||
<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="my-6 flex flex-col gap-6">
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
className="flex w-full flex-col gap-4"
|
||||
>
|
||||
{menus.map((item) => renderMobileMenuItem(item))}
|
||||
</Accordion>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
{/* logo */}
|
||||
<a href="/"
|
||||
className="flex items-center space-x-2"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<Logo className="size-8" />
|
||||
|
||||
<span className="text-xl font-bold">{siteConfig.name}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* mobile navbar right show sign in or user button */}
|
||||
<div className="flex items-center gap-x-4">
|
||||
{user ? (
|
||||
<div className="flex items-center">
|
||||
<UserButton />
|
||||
</div>
|
||||
) : (
|
||||
<LoginWrapper mode="redirect" asChild>
|
||||
<Button
|
||||
className="flex gap-2"
|
||||
variant="default"
|
||||
size="sm"
|
||||
>
|
||||
<span>Login</span>
|
||||
{/* <ArrowRightIcon className="size-4" /> */}
|
||||
</Button>
|
||||
</LoginWrapper>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
};
|
@ -1,30 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { ComponentProps } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle
|
||||
} from '@/components/ui/alert';
|
||||
|
||||
type CalloutProps = ComponentProps<typeof Alert> & {
|
||||
icon?: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* TODO: update
|
||||
*/
|
||||
export function Callout({
|
||||
title,
|
||||
children,
|
||||
icon,
|
||||
...props
|
||||
}: CalloutProps): React.JSX.Element {
|
||||
return (
|
||||
<Alert {...props}>
|
||||
{icon && <span className="mr-4 text-2xl">{icon}</span>}
|
||||
{title && <AlertTitle>{title}</AlertTitle>}
|
||||
<AlertDescription>{children}</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export type GridSectionProps = React.HtmlHTMLAttributes<HTMLDivElement> & {
|
||||
hideVerticalGridLines?: boolean;
|
||||
hideBottomGridLine?: boolean;
|
||||
containerProps?: React.HtmlHTMLAttributes<HTMLDivElement>;
|
||||
};
|
||||
|
||||
/**
|
||||
* TODO: remove
|
||||
*/
|
||||
export function GridSection({
|
||||
children,
|
||||
hideVerticalGridLines,
|
||||
hideBottomGridLine,
|
||||
containerProps: { className = '', ...containerProps } = {},
|
||||
...other
|
||||
}: GridSectionProps): React.JSX.Element {
|
||||
return (
|
||||
<section {...other}>
|
||||
<div
|
||||
className={cn('px-2 sm:container', className)}
|
||||
{...containerProps}
|
||||
>
|
||||
<div className="relative grid">
|
||||
{!hideVerticalGridLines && (
|
||||
<>
|
||||
<div className="absolute inset-y-0 block w-px bg-border" />
|
||||
<div className="absolute inset-y-0 right-0 w-px bg-border" />
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
{!hideBottomGridLine && <div className="h-px w-full bg-border" />}
|
||||
</section>
|
||||
);
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
export type SiteHeadingProps = {
|
||||
badge?: React.ReactNode;
|
||||
title?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* TODO: remove
|
||||
*/
|
||||
export function SiteHeading({
|
||||
badge,
|
||||
title,
|
||||
description
|
||||
}: SiteHeadingProps): React.JSX.Element {
|
||||
return (
|
||||
<div className="mx-auto flex max-w-5xl flex-col items-center gap-6 text-center">
|
||||
{badge && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="h-8 rounded-full px-3 text-sm font-medium shadow-sm"
|
||||
>
|
||||
{badge}
|
||||
</Badge>
|
||||
)}
|
||||
{title && (
|
||||
<h1 className="text-pretty text-5xl font-bold lg:text-6xl">{title}</h1>
|
||||
)}
|
||||
{description && (
|
||||
<p className="text-lg text-muted-foreground lg:text-xl">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,194 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { LoginWrapper } from '@/components/auth/login-button';
|
||||
import Container from '@/components/container';
|
||||
import { ThemeSwitcher } from '@/components/layout/theme-switcher';
|
||||
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 { Routes } from '@/routes';
|
||||
import { MarketingConfig } from '@/types';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { ArrowUpRightIcon } from 'lucide-react';
|
||||
import LocaleSelector from '@/components/layout/locale-selector';
|
||||
import { LocaleLink } from '@/i18n/navigation';
|
||||
|
||||
interface NavBarProps {
|
||||
scroll?: boolean;
|
||||
config: MarketingConfig;
|
||||
}
|
||||
|
||||
const customNavigationMenuTriggerStyle = cn(
|
||||
navigationMenuTriggerStyle(),
|
||||
"bg-transparent hover:bg-transparent hover:text-primary focus:bg-transparent focus:text-primary data-[active]:bg-transparent data-[active]:text-primary data-[state=open]:bg-transparent data-[state=open]:text-primary relative data-[active]:font-bold dark:text-gray-400 dark:hover:text-gray-300 dark:data-[active]:text-white"
|
||||
);
|
||||
|
||||
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 py-4 transition-all duration-300",
|
||||
scroll ? (
|
||||
scrolled
|
||||
? "bg-background/80 backdrop-blur-md border-b supports-[backdrop-filter]:bg-background/60"
|
||||
: "bg-background"
|
||||
) : "border-b bg-background"
|
||||
)}>
|
||||
<Container className="px-4">
|
||||
{/* desktop navbar */}
|
||||
<nav className="hidden lg:flex">
|
||||
{/* logo and name */}
|
||||
<div className="flex items-center">
|
||||
<LocaleLink href="/" className="flex items-center space-x-2">
|
||||
<Logo />
|
||||
<span className="text-xl font-semibold">{siteConfig.name}</span>
|
||||
</LocaleLink>
|
||||
</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)
|
||||
) ? "true" : undefined
|
||||
}
|
||||
className={cn(
|
||||
customNavigationMenuTriggerStyle,
|
||||
"data-[active]:text-primary data-[active]:font-bold dark:data-[active]:text-white"
|
||||
)}
|
||||
>
|
||||
{item.title}
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<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 className="flex-1">
|
||||
<div className="text-sm font-medium">
|
||||
{subItem.title}
|
||||
</div>
|
||||
{subItem.description && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{subItem.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{subItem.external && (
|
||||
<ArrowUpRightIcon className="size-4 shrink-0 text-muted-foreground transition-colors group-hover:text-foreground" />
|
||||
)}
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
) : (
|
||||
<NavigationMenuItem key={index}>
|
||||
<NavigationMenuLink
|
||||
asChild
|
||||
active={pathname.startsWith(item.href)}
|
||||
className={cn(
|
||||
customNavigationMenuTriggerStyle,
|
||||
"data-[active]:text-primary data-[active]:font-bold dark:data-[active]:text-white"
|
||||
)}
|
||||
>
|
||||
<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
|
||||
>
|
||||
<LocaleLink href={Routes.Register}>
|
||||
Sign up
|
||||
</LocaleLink>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<LocaleSelector />
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* mobile navbar */}
|
||||
<NavbarMobile className="lg:hidden" />
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
86
src/components/shared/callout-custom.tsx
Normal file
86
src/components/shared/callout-custom.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import {
|
||||
AlertTriangle,
|
||||
Ban,
|
||||
CircleAlert,
|
||||
CircleCheckBig,
|
||||
FileText,
|
||||
Info,
|
||||
Lightbulb,
|
||||
} from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface CalloutProps {
|
||||
twClass?: string;
|
||||
children?: React.ReactNode;
|
||||
type?: keyof typeof dataCallout;
|
||||
}
|
||||
|
||||
const dataCallout = {
|
||||
default: {
|
||||
icon: Info,
|
||||
classes:
|
||||
"border-zinc-200 bg-gray-50 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-200",
|
||||
},
|
||||
danger: {
|
||||
icon: CircleAlert,
|
||||
classes:
|
||||
"border-red-200 bg-red-50 text-red-900 dark:bg-red-950 dark:text-red-200",
|
||||
},
|
||||
error: {
|
||||
icon: Ban,
|
||||
classes:
|
||||
"border-red-200 bg-red-50 text-red-900 dark:bg-red-950 dark:text-red-200",
|
||||
},
|
||||
idea: {
|
||||
icon: Lightbulb,
|
||||
classes:
|
||||
"border-blue-200 bg-blue-50 text-blue-800 dark:bg-blue-950 dark:text-blue-200",
|
||||
},
|
||||
info: {
|
||||
icon: Info,
|
||||
classes:
|
||||
"border-blue-200 bg-blue-50 text-blue-800 dark:bg-blue-950 dark:text-blue-200",
|
||||
},
|
||||
note: {
|
||||
icon: FileText,
|
||||
classes:
|
||||
"border-blue-200 bg-blue-50 text-blue-800 dark:bg-blue-950 dark:text-blue-200",
|
||||
},
|
||||
success: {
|
||||
icon: CircleCheckBig,
|
||||
classes:
|
||||
"border-green-200 bg-green-50 text-green-800 dark:bg-green-400/20 dark:text-green-300",
|
||||
},
|
||||
warning: {
|
||||
icon: AlertTriangle,
|
||||
classes:
|
||||
"border-orange-200 bg-orange-50 text-orange-800 dark:bg-orange-400/20 dark:text-orange-300",
|
||||
},
|
||||
};
|
||||
|
||||
export function Callout({
|
||||
children,
|
||||
twClass,
|
||||
type = "default",
|
||||
...props
|
||||
}: CalloutProps) {
|
||||
const { icon: Icon, classes } = dataCallout[type];
|
||||
return (
|
||||
<div>
|
||||
{/* <div
|
||||
className={cn(
|
||||
"mt-6 flex items-start space-x-3 rounded-lg border px-4 py-3 text-[15.6px] dark:border-none",
|
||||
classes,
|
||||
twClass,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="mt-1 shrink-0">
|
||||
<Icon className="size-5" />
|
||||
</div>
|
||||
<div className="[&>p]:my-0">{children}</div>
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,86 +1,30 @@
|
||||
import * as React from 'react';
|
||||
import { ComponentProps } from 'react';
|
||||
import {
|
||||
AlertTriangle,
|
||||
Ban,
|
||||
CircleAlert,
|
||||
CircleCheckBig,
|
||||
FileText,
|
||||
Info,
|
||||
Lightbulb,
|
||||
} from "lucide-react";
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertTitle
|
||||
} from '@/components/ui/alert';
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface CalloutProps {
|
||||
twClass?: string;
|
||||
children?: React.ReactNode;
|
||||
type?: keyof typeof dataCallout;
|
||||
}
|
||||
|
||||
const dataCallout = {
|
||||
default: {
|
||||
icon: Info,
|
||||
classes:
|
||||
"border-zinc-200 bg-gray-50 text-zinc-900 dark:bg-zinc-800 dark:text-zinc-200",
|
||||
},
|
||||
danger: {
|
||||
icon: CircleAlert,
|
||||
classes:
|
||||
"border-red-200 bg-red-50 text-red-900 dark:bg-red-950 dark:text-red-200",
|
||||
},
|
||||
error: {
|
||||
icon: Ban,
|
||||
classes:
|
||||
"border-red-200 bg-red-50 text-red-900 dark:bg-red-950 dark:text-red-200",
|
||||
},
|
||||
idea: {
|
||||
icon: Lightbulb,
|
||||
classes:
|
||||
"border-blue-200 bg-blue-50 text-blue-800 dark:bg-blue-950 dark:text-blue-200",
|
||||
},
|
||||
info: {
|
||||
icon: Info,
|
||||
classes:
|
||||
"border-blue-200 bg-blue-50 text-blue-800 dark:bg-blue-950 dark:text-blue-200",
|
||||
},
|
||||
note: {
|
||||
icon: FileText,
|
||||
classes:
|
||||
"border-blue-200 bg-blue-50 text-blue-800 dark:bg-blue-950 dark:text-blue-200",
|
||||
},
|
||||
success: {
|
||||
icon: CircleCheckBig,
|
||||
classes:
|
||||
"border-green-200 bg-green-50 text-green-800 dark:bg-green-400/20 dark:text-green-300",
|
||||
},
|
||||
warning: {
|
||||
icon: AlertTriangle,
|
||||
classes:
|
||||
"border-orange-200 bg-orange-50 text-orange-800 dark:bg-orange-400/20 dark:text-orange-300",
|
||||
},
|
||||
type CalloutProps = ComponentProps<typeof Alert> & {
|
||||
icon?: string;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* TODO: update
|
||||
*/
|
||||
export function Callout({
|
||||
title,
|
||||
children,
|
||||
twClass,
|
||||
type = "default",
|
||||
icon,
|
||||
...props
|
||||
}: CalloutProps) {
|
||||
const { icon: Icon, classes } = dataCallout[type];
|
||||
}: CalloutProps): React.JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
{/* <div
|
||||
className={cn(
|
||||
"mt-6 flex items-start space-x-3 rounded-lg border px-4 py-3 text-[15.6px] dark:border-none",
|
||||
classes,
|
||||
twClass,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="mt-1 shrink-0">
|
||||
<Icon className="size-5" />
|
||||
</div>
|
||||
<div className="[&>p]:my-0">{children}</div>
|
||||
</div> */}
|
||||
</div>
|
||||
<Alert {...props}>
|
||||
{icon && <span className="mr-4 text-2xl">{icon}</span>}
|
||||
{title && <AlertTitle>{title}</AlertTitle>}
|
||||
<AlertDescription>{children}</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import * as React from 'react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useMDXComponent } from '@content-collections/mdx/react';
|
||||
import { Callout } from '@/components/marketing/blog/callout';
|
||||
import { Callout } from '@/components/shared/callout';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
@ -1,20 +1,16 @@
|
||||
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 '@/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';
|
||||
import { Routes } from '@/routes';
|
||||
import { CubeIcon, PaperPlaneIcon } from '@radix-ui/react-icons';
|
||||
import {
|
||||
BookOpenIcon,
|
||||
FileBarChartIcon,
|
||||
LayoutIcon,
|
||||
PlayIcon
|
||||
} from 'lucide-react';
|
||||
|
||||
export const MENU_LINKS = [
|
||||
{
|
||||
@ -32,11 +28,6 @@ export const MENU_LINKS = [
|
||||
href: Routes.Blog,
|
||||
external: false
|
||||
},
|
||||
// {
|
||||
// title: 'Docs',
|
||||
// href: Routes.Docs,
|
||||
// external: false
|
||||
// },
|
||||
{
|
||||
title: 'AI',
|
||||
items: [
|
||||
@ -162,33 +153,3 @@ export const SOCIAL_LINKS = [
|
||||
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: []
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
@ -72,10 +72,26 @@ export function getLocaleDate(input: string | number): string {
|
||||
* @param request - The request to get the locale from
|
||||
* @returns The locale from the request or the default locale
|
||||
*/
|
||||
export const getLocaleFromRequest = (request?: Request) => {
|
||||
export function getLocaleFromRequest(request?: Request): Locale {
|
||||
const cookies = parseCookies(request?.headers.get("cookie") ?? "");
|
||||
return (
|
||||
(cookies[LOCALE_COOKIE_NAME] as Locale) ??
|
||||
routing.defaultLocale
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Estimates the reading time of a text
|
||||
*
|
||||
* @param text - The text to estimate the reading time of
|
||||
* @param wordsPerMinute - The number of words per minute to use for the estimate
|
||||
* @returns The estimated reading time
|
||||
*/
|
||||
export function estimateReadingTime(
|
||||
text: string,
|
||||
wordsPerMinute: number = 250
|
||||
): string {
|
||||
const words = text.trim().split(/\s+/).length;
|
||||
const minutes = Math.ceil(words / wordsPerMinute);
|
||||
return minutes === 1 ? '1 minute read' : `${minutes} minutes read`;
|
||||
}
|
Loading…
Reference in New Issue
Block a user