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",
|
"categories": "Categories",
|
||||||
"tableOfContents": "Table of Contents",
|
"tableOfContents": "Table of Contents",
|
||||||
"all": "All",
|
"all": "All",
|
||||||
"noPostsFound": "No posts found"
|
"noPostsFound": "No posts found",
|
||||||
|
"allPosts": "All Posts"
|
||||||
},
|
},
|
||||||
"mail": {
|
"mail": {
|
||||||
"common": {
|
"common": {
|
||||||
|
@ -65,7 +65,8 @@
|
|||||||
"categories": "分类",
|
"categories": "分类",
|
||||||
"tableOfContents": "目录",
|
"tableOfContents": "目录",
|
||||||
"all": "全部",
|
"all": "全部",
|
||||||
"noPostsFound": "没有找到文章"
|
"noPostsFound": "没有找到文章",
|
||||||
|
"allPosts": "全部文章"
|
||||||
},
|
},
|
||||||
"mail": {
|
"mail": {
|
||||||
"common": {
|
"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 AllPostsButton from '@/components/blog/all-posts-button';
|
||||||
import { BlogToc } from '@/components/blog/blog-toc';
|
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 { LocaleLink } from '@/i18n/navigation';
|
||||||
import { getTableOfContents } from '@/lib/toc';
|
import { getTableOfContents } from '@/lib/toc';
|
||||||
import { getBaseUrl } from '@/lib/urls/get-base-url';
|
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 type { NextPageProps } from '@/types/next-page-props';
|
||||||
import { allPosts } from 'content-collections';
|
import { allPosts } from 'content-collections';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
@ -113,6 +113,12 @@ export default async function BlogPostPage(props: NextPageProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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 */}
|
{/* blog post title */}
|
||||||
<h1 className="text-3xl font-bold">{post.title}</h1>
|
<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">
|
<div className="bg-muted/50 rounded-lg p-6">
|
||||||
<h2 className="text-lg font-semibold mb-4">{t("publisher")}</h2>
|
<h2 className="text-lg font-semibold mb-4">{t("publisher")}</h2>
|
||||||
<div className="flex items-center gap-4">
|
<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 && (
|
{post.author?.avatar && (
|
||||||
<Image
|
<Image
|
||||||
src={post.author.avatar}
|
src={post.author.avatar}
|
||||||
@ -147,11 +153,7 @@ export default async function BlogPostPage(props: NextPageProps) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<span className="line-clamp-1">{post.author?.name}</span>
|
||||||
<span>{post.author?.name}</span>
|
|
||||||
|
|
||||||
<p className="text-sm text-muted-foreground">{date}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { marketingConfig } from "@/config/marketing";
|
import { marketingConfig } from "@/config/marketing";
|
||||||
import { Footer } from "@/components/layout/footer";
|
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 }) {
|
export default function MarketingLayout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
import { fontSourceSans, fontSourceSerif4 } from "@/assets/fonts";
|
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 { TailwindIndicator } from '@/components/tailwind-indicator';
|
||||||
import { marketingConfig } from '@/config/marketing';
|
|
||||||
import { routing } from '@/i18n/routing';
|
import { routing } from '@/i18n/routing';
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { GeistMono } from "geist/font/mono";
|
import { GeistMono } from "geist/font/mono";
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ArrowLeftIcon } from "lucide-react";
|
|
||||||
import { LocaleLink } from "@/i18n/navigation";
|
import { LocaleLink } from "@/i18n/navigation";
|
||||||
|
import { ArrowLeftIcon } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export default function AllPostsButton() {
|
export default function AllPostsButton() {
|
||||||
|
const t = useTranslations("BlogPage");
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
@ -13,11 +15,10 @@ export default function AllPostsButton() {
|
|||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
<LocaleLink href="/blog">
|
<LocaleLink href="/blog">
|
||||||
<ArrowLeftIcon
|
<ArrowLeftIcon className="w-5 h-5
|
||||||
className="w-5 h-5
|
transition-transform duration-200 group-hover:-translate-x-1"
|
||||||
transition-transform duration-200 group-hover:-translate-x-1"
|
|
||||||
/>
|
/>
|
||||||
<span>All Posts</span>
|
<span>{t("allPosts")}</span>
|
||||||
</LocaleLink>
|
</LocaleLink>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -1,12 +1,19 @@
|
|||||||
"use client";
|
"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 { Category } from "content-collections";
|
||||||
import { LayoutListIcon } from "lucide-react";
|
import { LayoutListIcon } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Drawer } from "vaul";
|
|
||||||
import FilterItemMobile from "@/components/shared/filter-item-mobile";
|
|
||||||
import { useTranslations } from "next-intl";
|
|
||||||
|
|
||||||
export type BlogCategoryListMobileProps = {
|
export type BlogCategoryListMobileProps = {
|
||||||
categoryList: Category[];
|
categoryList: Category[];
|
||||||
@ -27,8 +34,8 @@ export function BlogCategoryListMobile({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer.Root open={open} onClose={closeDrawer}>
|
<Drawer open={open} onClose={closeDrawer}>
|
||||||
<Drawer.Trigger
|
<DrawerTrigger
|
||||||
onClick={() => setOpen(true)}
|
onClick={() => setOpen(true)}
|
||||||
className="flex items-center w-full p-3 border-y text-foreground/90"
|
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")}
|
{selectedCategory?.name ? `${selectedCategory?.name}` : t("all")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Drawer.Trigger>
|
</DrawerTrigger>
|
||||||
<Drawer.Overlay
|
<DrawerPortal>
|
||||||
className="fixed inset-0 z-40 bg-background/80 backdrop-blur-sm"
|
<DrawerOverlay className="fixed inset-0 z-40 bg-background/50" />
|
||||||
onClick={closeDrawer}
|
<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>
|
||||||
<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>
|
|
||||||
<div className="sticky top-0 z-20 flex w-full items-center justify-center bg-inherit">
|
<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 className="my-3 h-1.5 w-16 rounded-full bg-muted-foreground/20" />
|
||||||
</div>
|
</div>
|
||||||
@ -71,9 +75,8 @@ export function BlogCategoryListMobile({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</Drawer.Content>
|
</DrawerContent>
|
||||||
<Drawer.Overlay />
|
</DrawerPortal>
|
||||||
</Drawer.Portal>
|
</Drawer>
|
||||||
</Drawer.Root>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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';
|
'use client';
|
||||||
|
|
||||||
import { Logo } from '@/components/logo';
|
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 { Button, buttonVariants } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
@ -27,7 +27,6 @@ export function NavbarMobile({
|
|||||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
const [open, setOpen] = React.useState<boolean>(false);
|
const [open, setOpen] = React.useState<boolean>(false);
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const isDocs = pathname.startsWith('/docs');
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handleRouteChangeStart = () => {
|
const handleRouteChangeStart = () => {
|
||||||
@ -90,11 +89,7 @@ export function NavbarMobile({
|
|||||||
{open && (
|
{open && (
|
||||||
<Portal asChild>
|
<Portal asChild>
|
||||||
<RemoveScroll allowPinchZoom enabled>
|
<RemoveScroll allowPinchZoom enabled>
|
||||||
{isDocs ? (
|
<MainMobileMenu onLinkClicked={handleToggleMobileMenu} />
|
||||||
<DocsMobileMenu onLinkClicked={handleToggleMobileMenu} />
|
|
||||||
) : (
|
|
||||||
<MainMobileMenu onLinkClicked={handleToggleMobileMenu} />
|
|
||||||
)}
|
|
||||||
</RemoveScroll>
|
</RemoveScroll>
|
||||||
</Portal>
|
</Portal>
|
||||||
)}
|
)}
|
||||||
@ -238,77 +233,3 @@ function MainMobileMenu({ onLinkClicked }: MainMobileMenuProps) {
|
|||||||
</div>
|
</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 { LoginWrapper } from '@/components/auth/login-button';
|
||||||
import Container from "@/components/container";
|
import Container from '@/components/container';
|
||||||
import { Icons } from "@/components/icons/icons";
|
import { ThemeSwitcher } from '@/components/layout/theme-switcher';
|
||||||
import { UserButton } from "@/components/layout/user-button";
|
import { UserButton } from '@/components/layout/user-button';
|
||||||
import { Logo } from "@/components/logo";
|
import { Logo } from '@/components/logo';
|
||||||
import { Button } from "@/components/ui/button";
|
import { MENU_LINKS } from '@/config/marketing-links';
|
||||||
|
import { NavbarMobile } from '@/components/layout/navbar-mobile';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
NavigationMenu,
|
NavigationMenu,
|
||||||
NavigationMenuContent,
|
NavigationMenuContent,
|
||||||
@ -13,274 +15,180 @@ import {
|
|||||||
NavigationMenuLink,
|
NavigationMenuLink,
|
||||||
NavigationMenuList,
|
NavigationMenuList,
|
||||||
NavigationMenuTrigger,
|
NavigationMenuTrigger,
|
||||||
navigationMenuTriggerStyle,
|
navigationMenuTriggerStyle
|
||||||
} from "@/components/ui/navigation-menu";
|
} from '@/components/ui/navigation-menu';
|
||||||
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
|
import { siteConfig } from '@/config/site';
|
||||||
import { siteConfig } from "@/config/site";
|
|
||||||
import { useScroll } from "@/hooks/use-scroll";
|
import { useScroll } from "@/hooks/use-scroll";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from '@/lib/auth-client';
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from '@/lib/utils';
|
||||||
import type { DashboardConfig, MarketingConfig, NestedNavItem } from "@/types";
|
import { Routes } from '@/routes';
|
||||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@radix-ui/react-accordion";
|
import { MarketingConfig } from '@/types';
|
||||||
import { MenuIcon } from "lucide-react";
|
import Link from 'next/link';
|
||||||
import Link from "next/link";
|
import { usePathname } from 'next/navigation';
|
||||||
import { usePathname } from "next/navigation";
|
import { ArrowUpRightIcon } from 'lucide-react';
|
||||||
import { useEffect, useState } from "react";
|
import LocaleSelector from '@/components/layout/locale-selector';
|
||||||
|
import { LocaleLink } from '@/i18n/navigation';
|
||||||
|
|
||||||
interface NavBarProps {
|
interface NavBarProps {
|
||||||
scroll?: boolean;
|
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 scrolled = useScroll(50);
|
||||||
const { data: session, error } = authClient.useSession();
|
const { data: session, error } = authClient.useSession();
|
||||||
const user = session?.user;
|
const user = session?.user;
|
||||||
console.log(`Navbar, user:`, user);
|
// console.log(`Navbar, user:`, user);
|
||||||
|
|
||||||
const pathname = usePathname();
|
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 (
|
return (
|
||||||
<div className="sticky top-0 z-40 w-full">
|
<section className={cn(
|
||||||
{/* Desktop View */}
|
"sticky inset-x-0 top-0 z-40 py-4 transition-all duration-300",
|
||||||
<header
|
scroll ? (
|
||||||
className={cn(
|
scrolled
|
||||||
"hidden md:flex justify-center bg-background/60 backdrop-blur-xl transition-all",
|
? "bg-background/80 backdrop-blur-md border-b supports-[backdrop-filter]:bg-background/60"
|
||||||
scroll ? (scrolled ? "border-b" : "bg-transparent") : "border-b",
|
: "bg-background"
|
||||||
)}
|
) : "border-b bg-background"
|
||||||
>
|
)}>
|
||||||
<Container className="flex h-16 items-center px-4">
|
<Container className="px-4">
|
||||||
{/* navbar left show logo and links */}
|
{/* desktop navbar */}
|
||||||
<div className="flex items-center gap-6 md:gap-10">
|
<nav className="hidden lg:flex">
|
||||||
{/* logo */}
|
{/* logo and name */}
|
||||||
<a href="/" className="flex items-center space-x-2">
|
<div className="flex items-center">
|
||||||
|
<LocaleLink href="/" className="flex items-center space-x-2">
|
||||||
<Logo />
|
<Logo />
|
||||||
|
<span className="text-xl font-semibold">{siteConfig.name}</span>
|
||||||
<span className="text-xl font-bold">{siteConfig.name}</span>
|
</LocaleLink>
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* links */}
|
{/* menu links */}
|
||||||
<div className="flex-1 flex justify-center">
|
<div className="flex-1 flex items-center justify-center space-x-2">
|
||||||
{menus && menus.length > 0 ? (
|
<NavigationMenu className="relative">
|
||||||
<NavigationMenu>
|
<NavigationMenuList className="flex items-center">
|
||||||
<NavigationMenuList>
|
{MENU_LINKS.map((item, index) =>
|
||||||
{menus.map((item) => renderMenuItem(item))}
|
item.items ? (
|
||||||
</NavigationMenuList>
|
<NavigationMenuItem key={index} className="relative">
|
||||||
</NavigationMenu>
|
<NavigationMenuTrigger
|
||||||
) : null}
|
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>
|
</div>
|
||||||
|
|
||||||
{/* navbar right show sign in or account */}
|
{/* navbar right show sign in or user */}
|
||||||
<div className="flex items-center gap-x-4">
|
<div className="flex items-center gap-x-4">
|
||||||
{user ? (
|
{user ? (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<UserButton />
|
<UserButton />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<LoginWrapper mode="modal" asChild>
|
<div className="flex items-center gap-x-4">
|
||||||
<Button
|
<LoginWrapper mode="modal" asChild>
|
||||||
className="flex gap-2"
|
<Button
|
||||||
variant="default"
|
variant="outline"
|
||||||
size="sm"
|
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))}
|
<span>Log in</span>
|
||||||
</Accordion>
|
</Button>
|
||||||
</div>
|
</LoginWrapper>
|
||||||
</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
|
<Button
|
||||||
className="flex gap-2"
|
|
||||||
variant="default"
|
variant="default"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
asChild
|
||||||
>
|
>
|
||||||
<span>Login</span>
|
<LocaleLink href={Routes.Register}>
|
||||||
{/* <ArrowRightIcon className="size-4" /> */}
|
Sign up
|
||||||
|
</LocaleLink>
|
||||||
</Button>
|
</Button>
|
||||||
</LoginWrapper>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<LocaleSelector />
|
||||||
|
<ThemeSwitcher />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</nav>
|
||||||
</header>
|
|
||||||
</div>
|
{/* 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 {
|
import {
|
||||||
AlertTriangle,
|
Alert,
|
||||||
Ban,
|
AlertDescription,
|
||||||
CircleAlert,
|
AlertTitle
|
||||||
CircleCheckBig,
|
} from '@/components/ui/alert';
|
||||||
FileText,
|
|
||||||
Info,
|
|
||||||
Lightbulb,
|
|
||||||
} from "lucide-react";
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
type CalloutProps = ComponentProps<typeof Alert> & {
|
||||||
|
icon?: string;
|
||||||
interface CalloutProps {
|
title?: string;
|
||||||
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",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: update
|
||||||
|
*/
|
||||||
export function Callout({
|
export function Callout({
|
||||||
|
title,
|
||||||
children,
|
children,
|
||||||
twClass,
|
icon,
|
||||||
type = "default",
|
|
||||||
...props
|
...props
|
||||||
}: CalloutProps) {
|
}: CalloutProps): React.JSX.Element {
|
||||||
const { icon: Icon, classes } = dataCallout[type];
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Alert {...props}>
|
||||||
{/* <div
|
{icon && <span className="mr-4 text-2xl">{icon}</span>}
|
||||||
className={cn(
|
{title && <AlertTitle>{title}</AlertTitle>}
|
||||||
"mt-6 flex items-start space-x-3 rounded-lg border px-4 py-3 text-[15.6px] dark:border-none",
|
<AlertDescription>{children}</AlertDescription>
|
||||||
classes,
|
</Alert>
|
||||||
twClass,
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div className="mt-1 shrink-0">
|
|
||||||
<Icon className="size-5" />
|
|
||||||
</div>
|
|
||||||
<div className="[&>p]:my-0">{children}</div>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import * as React from 'react';
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useMDXComponent } from '@content-collections/mdx/react';
|
import { useMDXComponent } from '@content-collections/mdx/react';
|
||||||
import { Callout } from '@/components/marketing/blog/callout';
|
import { Callout } from '@/components/shared/callout';
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionContent,
|
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 { FacebookIcon } from '@/components/icons/facebook';
|
||||||
import { InstagramIcon } from '@/components/icons/instagram';
|
import { InstagramIcon } from '@/components/icons/instagram';
|
||||||
import { LinkedInIcon } from '@/components/icons/linkedin';
|
import { LinkedInIcon } from '@/components/icons/linkedin';
|
||||||
import { TikTokIcon } from '@/components/icons/tiktok';
|
import { TikTokIcon } from '@/components/icons/tiktok';
|
||||||
import { XTwitterIcon } from '@/components/icons/x';
|
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 = [
|
export const MENU_LINKS = [
|
||||||
{
|
{
|
||||||
@ -32,11 +28,6 @@ export const MENU_LINKS = [
|
|||||||
href: Routes.Blog,
|
href: Routes.Blog,
|
||||||
external: false
|
external: false
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// title: 'Docs',
|
|
||||||
// href: Routes.Docs,
|
|
||||||
// external: false
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
title: 'AI',
|
title: 'AI',
|
||||||
items: [
|
items: [
|
||||||
@ -162,33 +153,3 @@ export const SOCIAL_LINKS = [
|
|||||||
icon: <TikTokIcon className="size-4 shrink-0" />
|
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
|
* @param request - The request to get the locale from
|
||||||
* @returns The locale from the request or the default locale
|
* @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") ?? "");
|
const cookies = parseCookies(request?.headers.get("cookie") ?? "");
|
||||||
return (
|
return (
|
||||||
(cookies[LOCALE_COOKIE_NAME] as Locale) ??
|
(cookies[LOCALE_COOKIE_NAME] as Locale) ??
|
||||||
routing.defaultLocale
|
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