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:
javayhu 2025-03-08 13:50:08 +08:00
parent 7875c19a3c
commit 431e61d2a5
22 changed files with 613 additions and 943 deletions

View File

@ -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": {

View File

@ -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

View File

@ -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>

View File

@ -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 (

View File

@ -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";

View File

@ -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>
);

View File

@ -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>
);
}

View File

@ -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`;
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
};

View 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>
);
};

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View 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>
);
}

View File

@ -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>
);
}

View File

@ -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,

View File

@ -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: []
}
]
}
];

View File

@ -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`;
}