feat: add marketing layout, navbar, footer, and configuration files

This commit is contained in:
javayhu 2025-02-21 23:29:05 +08:00
parent 0f04b73114
commit 603bf4e9ce
35 changed files with 1525 additions and 210 deletions

View File

@ -0,0 +1,63 @@
// import PromotekitScript from "@/components/affiliate/promotekit-stripe-checkout";
// import { HomeCallToAction } from "@/components/home/home-cta";
// import { HomeFAQ } from "@/components/home/home-faq";
// import HomeFeatures from "@/components/home/home-features";
// import { HomeFeaturesHeader } from "@/components/home/home-features-header";
// import { HomeFeaturesMore } from "@/components/home/home-features-more";
// import HomeHero from "@/components/home/home-hero";
// import { HomeHowItWorks } from "@/components/home/home-how-it-works";
// import { HomeIntroduction } from "@/components/home/home-introduction";
// import HomeMonetization from "@/components/home/home-monetization";
// import { HomeNewsletter } from "@/components/home/home-newsletter";
// import HomePowered from "@/components/home/home-powered";
// import HomePricing from "@/components/home/home-pricing";
// import { HomeShowcase } from "@/components/home/home-showcase";
// import { HomeTestimonials } from "@/components/home/home-testimonials";
// import HomeVideo from "@/components/home/home-video";
import { siteConfig } from "@/config/site";
import { constructMetadata } from "@/lib/metadata";
export const metadata = constructMetadata({
title: "",
canonicalUrl: `${siteConfig.url}/`,
});
export default async function HomePage() {
return (
<>
{/* <PromotekitScript /> */}
<div className="mt-12 flex flex-col gap-16">
{/* <HomeHero /> */}
{/* <HomeVideo /> */}
{/* <HomePowered /> */}
{/* <HomeMonetization /> */}
{/* <HomeHowItWorks /> */}
{/* <HomeFeaturesHeader /> */}
{/* <HomeFeatures /> */}
{/* <HomeFeaturesMore /> */}
{/* <HomePricing /> */}
{/* <HomeFAQ /> */}
{/* <HomeIntroduction /> */}
{/* <HomeTestimonials /> */}
{/* <HomeCallToAction /> */}
{/* <HomeShowcase /> */}
{/* <HomeNewsletter /> */}
</div>
</>
);
}

View File

@ -1,47 +1,21 @@
import "@/styles/globals.css";
import { TailwindIndicator } from "@/components/tailwind-indicator";
import { Toaster } from "@/components/ui/sonner";
import { cn } from "@/lib/utils";
import { ThemeProvider } from "next-themes";
import { Geist, Geist_Mono } from "next/font/google";
import { Footer } from "@/components/layout/footer";
import { Navbar } from "@/components/layout/navbar";
import { marketingConfig } from "@/config/marketing";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
interface RootLayoutProps {
interface MarketingLayoutProps {
children: React.ReactNode;
}
export default async function RootLayout({ children }: RootLayoutProps) {
export default async function MarketingLayout({
children,
}: MarketingLayoutProps) {
return (
<html lang="en" suppressHydrationWarning>
<head />
<body
className={cn(
"min-h-screen bg-background antialiased",
`${geistSans.variable} ${geistMono.variable}`
)}
>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
<div className="flex flex-col min-h-screen">
<Navbar scroll={true} config={marketingConfig} />
<Toaster richColors position="top-right" offset={64} />
<main className="flex-1">{children}</main>
<TailwindIndicator />
</ThemeProvider>
</body>
</html>
<Footer />
</div>
);
}

View File

@ -1,101 +0,0 @@
import Image from "next/image";
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/logo.png"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
src/app/page.tsx
</code>
.
</li>
<li>Save and see your changes instantly.</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:min-w-44"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-6 flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
);
}

View File

@ -1,29 +0,0 @@
export const BLOG_CATEGORIES: {
title: string;
slug: "news" | "education";
description: string;
}[] = [
{
title: "News",
slug: "news",
description: "Updates and announcements from MkSaaS Starter.",
},
{
title: "Education",
slug: "education",
description: "Educational content about SaaS management.",
},
];
export const BLOG_AUTHORS = {
mksaas: {
name: "mksaas",
image: "/_static/avatars/mksaas.png",
twitter: "mksaas",
},
mkdirs: {
name: "mkdirs",
image: "/_static/avatars/mkdirs.png",
twitter: "mkdirs",
},
};

View File

@ -2,10 +2,6 @@
@tailwind components;
@tailwind utilities;
body {
font-family: Arial, Helvetica, sans-serif;
}
@layer base {
:root {
--background: 0 0% 100%;
@ -90,7 +86,7 @@ body {
@layer base {
* {
@apply border-border;
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;

47
src/app/layout.tsx Normal file
View File

@ -0,0 +1,47 @@
import "@/app/globals.css";
import type { Metadata, Viewport } from "next";
import type { PropsWithChildren } from "react";
import { constructMetadata } from "@/lib/metadata";
import { cn } from "@/lib/utils";
import { ThemeProvider } from "next-themes";
import { Toaster } from "@/components/ui/sonner";
import { TailwindIndicator } from "@/components/tailwind-indicator";
export const metadata: Metadata = constructMetadata();
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
minimumScale: 1,
maximumScale: 1,
themeColor: [
{ media: '(prefers-color-scheme: light)', color: 'white' },
{ media: '(prefers-color-scheme: dark)', color: 'black' }
]
};
export default function RootLayout({ children }: PropsWithChildren) {
return (
<html lang="en" suppressHydrationWarning>
<head />
<body
className={cn(
"min-h-screen bg-background antialiased"
)}
>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
<Toaster richColors position="top-right" offset={64} />
<TailwindIndicator />
</ThemeProvider>
</body>
</html>
);
}

View File

@ -1,16 +0,0 @@
export const metadata = {
title: 'Next.js',
description: 'Generated by Next.js',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}

36
src/app/manifest.ts Normal file
View File

@ -0,0 +1,36 @@
import { type MetadataRoute } from 'next';
import { siteConfig } from '@/config/site';
/**
* Generates the Web App Manifest for the application
*
* The manifest.json provides metadata used when the web app is installed on a
* user's mobile device or desktop. See https://web.dev/add-manifest/
*
* @returns {MetadataRoute.Manifest} The manifest configuration object
*/
export default function manifest(): MetadataRoute.Manifest {
return {
name: siteConfig.name,
short_name: siteConfig.name,
description: siteConfig.description,
start_url: '/',
display: 'standalone',
background_color: '#ffffff',
theme_color: '#ffffff',
icons: [
{
src: '/android-chrome-192x192.png',
sizes: '192x192',
type: 'image/png',
purpose: 'maskable'
},
{
src: '/android-chrome-512x512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'maskable'
}
]
};
}

View File

@ -0,0 +1,18 @@
import { cn } from "@/lib/utils";
import type { ReactNode } from "react";
export default function Container({
className,
children,
}: {
id?: string;
className?: string;
children?: ReactNode;
}) {
// add mx-auto to make it center
return (
<div className={cn("container", "mx-auto max-w-7xl", className)}>
{children}
</div>
);
}

View File

@ -0,0 +1,149 @@
"use client";
import { Icons } from "@/components/icons/icons";
import { ModeToggle } from "@/components/layout/mode-toggle";
import { footerConfig } from "@/config/footer";
import { siteConfig } from "@/config/site";
import { cn } from "@/lib/utils";
import { useTheme } from "next-themes";
import Link from "next/link";
import type * as React from "react";
import Container from "@/components/container";
import { Logo } from "@/components/logo";
import BuiltWithButton from "@/components/shared/built-with-button";
export function Footer({ className }: React.HTMLAttributes<HTMLElement>) {
const { theme } = useTheme();
return (
<footer className={cn("border-t", className)}>
<Container>
<div className="grid grid-cols-2 gap-8 py-12 md:grid-cols-6">
<div className="flex flex-col items-start col-span-full md:col-span-2">
<div className="space-y-4">
<div className="items-center space-x-2 flex">
<Logo />
<span className="text-xl font-bold">{siteConfig.name}</span>
</div>
<p className="text-muted-foreground text-base p4-4 md:pr-12">
{siteConfig.tagline}
</p>
<div className="flex items-center gap-2">
{siteConfig.links.github && (
<Link
href={siteConfig.links.github}
target="_blank"
rel="noreferrer"
aria-label="GitHub"
className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground"
>
<Icons.github className="size-4" aria-hidden="true" />
</Link>
)}
{siteConfig.links.twitter && (
<Link
href={siteConfig.links.twitter}
target="_blank"
rel="noreferrer"
aria-label="Twitter"
className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground"
>
<Icons.twitter className="size-4" aria-hidden="true" />
</Link>
)}
{siteConfig.links.twitter_cn && (
<Link
href={siteConfig.links.twitter_cn}
target="_blank"
rel="noreferrer"
aria-label="Twitter(CN)"
className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground"
>
<Icons.twitter className="size-4" aria-hidden="true" />
</Link>
)}
{siteConfig.links.bluesky && (
<Link
href={siteConfig.links.bluesky}
target="_blank"
rel="noreferrer"
aria-label="Bluesky"
className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground"
>
<Icons.bluesky className="size-4" aria-hidden="true" />
</Link>
)}
{siteConfig.links.youtube && (
<Link
href={siteConfig.links.youtube}
target="_blank"
rel="noreferrer"
aria-label="YouTube"
className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground"
>
<Icons.youtube className="size-4" aria-hidden="true" />
</Link>
)}
{siteConfig.mail && (
<Link
href={`mailto:${siteConfig.mail}`}
target="_blank"
rel="noreferrer"
aria-label="Email"
className="inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-accent hover:text-accent-foreground"
>
<Icons.email className="size-4" aria-hidden="true" />
</Link>
)}
</div>
<BuiltWithButton />
</div>
</div>
{footerConfig.links.map((section) => (
<div
key={section.title}
className="col-span-1 md:col-span-1 items-start"
>
<span className="text-sm font-semibold uppercase">
{section.title}
</span>
<ul className="mt-4 list-inside space-y-3">
{section.items?.map(
(link) =>
link.href && (
<li key={link.title}>
<Link
href={link.href}
target={link.external ? "_blank" : undefined}
className="text-sm text-muted-foreground hover:text-primary"
>
{link.title}
</Link>
</li>
),
)}
</ul>
</div>
))}
</div>
</Container>
<div className="border-t py-4">
<Container className="flex items-center justify-between">
<span className="text-muted-foreground text-sm">
Copyright &copy; {new Date().getFullYear()} All Rights Reserved.
</span>
<div className="flex items-center gap-3">
<ModeToggle />
</div>
</Container>
</div>
</footer>
);
}

View File

@ -0,0 +1,41 @@
"use client";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { LaptopIcon, MoonIcon, SunIcon } from "lucide-react";
import { useTheme } from "next-themes";
export function ModeToggle() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="px-0">
<SunIcon className="rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<MoonIcon className="absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")} className="cursor-pointer">
<SunIcon className="mr-2 size-4" />
<span>Light</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")} className="cursor-pointer">
<MoonIcon className="mr-2 size-4" />
<span>Dark</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")} className="cursor-pointer">
<LaptopIcon className="mr-2 size-4" />
<span>System</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -0,0 +1,259 @@
"use client";
import { LoginWrapper } from "@/components/auth/login-button";
import Container from "@/components/container";
import { Icons } from "@/components/icons/icons";
import { ModeToggle } from "@/components/layout/mode-toggle";
import { UserButton } from "@/components/layout/user-button";
import { Button } from "@/components/ui/button";
import {
NavigationMenu,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
import { siteConfig } from "@/config/site";
// import { useCurrentUser } from "@/hooks/use-current-user";
import { useScroll } from "@/hooks/use-scroll";
import { cn } from "@/lib/utils";
import type { DashboardConfig, MarketingConfig } from "@/types";
import { ArrowRightIcon, MenuIcon } from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import React from "react";
import { Logo } from "@/components/logo";
interface NavBarProps {
scroll?: boolean;
config: DashboardConfig | MarketingConfig;
}
export function Navbar({ scroll = false, config }: NavBarProps) {
const scrolled = useScroll(50);
// const user = useCurrentUser();
// console.log(`navbar: user:`, user);
const user = {
name: "John Doe",
email: "john.doe@example.com",
image: "https://example.com/john.jpg",
};
const pathname = usePathname();
// console.log(`Navbar, pathname: ${pathname}`);
const links = config.menus;
// console.log(`Navbar, links: ${links.map((link) => link.title)}`);
const isLinkActive = (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">
{/* navbar left show logo and links */}
<div className="flex items-center gap-6 md:gap-10">
{/* logo */}
<Link href="/" className="flex items-center space-x-2">
<Logo />
<span className="text-xl font-bold">{siteConfig.name}</span>
</Link>
</div>
{/* links */}
<div className="flex-1 flex justify-center">
{links && links.length > 0 ? (
<NavigationMenu>
<NavigationMenuList>
{links.map((item) => (
<NavigationMenuItem key={item.title}>
<NavigationMenuLink
href={item.disabled ? "#" : item.href}
target={item.external ? "_blank" : ""}
className={cn(
navigationMenuTriggerStyle(),
"px-4 bg-transparent focus:bg-transparent text-base",
isLinkActive(item.href)
? "text-foreground font-semibold"
: "text-foreground/60",
item.disabled && "cursor-not-allowed opacity-80",
)}
>
{item.title}
</NavigationMenuLink>
</NavigationMenuItem>
))}
</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 px-5 rounded-full"
variant="default"
size="default"
>
<span>Sign In</span>
<ArrowRightIcon className="size-4" />
</Button>
</LoginWrapper> */}
{/* <Button
className="rounded-full"
variant="default"
size="default"
asChild
>
<Link href="/submit">
<span>Submit</span>
</Link>
</Button> */}
</>
)}
<ModeToggle />
</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="flex flex-col p-0">
<div className="flex h-screen flex-col">
{/* logo */}
<Link
href="/"
className="flex items-center space-x-2 pl-4 pt-4"
onClick={() => setOpen(false)}
>
<Logo />
<span className="text-xl font-bold">{siteConfig.name}</span>
</Link>
<nav className="flex flex-1 flex-col gap-2 p-2 pt-8 font-medium">
{links.map((item) => {
const Icon = Icons[item.icon || "arrowRight"];
return (
<Link
key={item.title}
href={item.disabled ? "#" : item.href}
target={item.external ? "_blank" : ""}
onClick={() => {
if (!item.disabled) setOpen(false);
}}
className={cn(
"flex items-center rounded-md gap-2 p-2 text-sm font-medium hover:bg-muted",
isLinkActive(item.href)
? "bg-muted text-foreground"
: "text-muted-foreground hover:text-foreground",
item.disabled &&
"cursor-not-allowed opacity-80 hover:bg-transparent hover:text-muted-foreground",
)}
>
<Icon className="size-5" />
{item.title}
</Link>
);
})}
</nav>
</div>
</SheetContent>
</Sheet>
{/* logo */}
<Link
href="/"
className="flex items-center space-x-2"
onClick={() => setOpen(false)}
>
<Logo className="size-8" />
<span className="text-xl font-bold">{siteConfig.name}</span>
</Link>
</div>
{/* mobile 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="redirect" asChild>
<Button
className="flex gap-2 px-5 rounded-full"
variant="default"
size="default"
>
<span>Sign In</span>
<ArrowRightIcon className="size-4" />
</Button>
</LoginWrapper> */}
{/* <Button
className="rounded-full"
variant="default"
size="default"
asChild
>
<Link href="/submit">
<span>Submit</span>
</Link>
</Button> */}
</>
)}
<ModeToggle />
</div>
</div>
</header>
</div>
);
}

View File

@ -0,0 +1,186 @@
"use client";
import { Icons } from "@/components/icons/icons";
import { UserAvatar } from "@/components/shared/user-avatar";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { userButtonConfig } from "@/config/user-button";
// import { useCurrentUser } from "@/hooks/use-current-user";
import { useMediaQuery } from "@/hooks/use-media-query";
import { LogOutIcon } from "lucide-react";
// import { signOut } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
// import { Drawer } from "vaul";
export function UserButton() {
const router = useRouter();
// const user = useCurrentUser();
// console.log('UserButton, user:', user);
const user = {
name: "John Doe",
email: "john.doe@example.com",
image: "https://example.com/john.jpg",
};
const [open, setOpen] = useState(false);
const closeDrawer = () => {
setOpen(false);
};
const { isMobile } = useMediaQuery();
// if (!user) {
// return (
// <div className="size-8 animate-pulse rounded-full border bg-muted" />
// );
// }
// Mobile View, use Drawer
// if (isMobile) {
// return (
// <Drawer.Root open={open} onClose={closeDrawer}>
// <Drawer.Trigger onClick={() => setOpen(true)}>
// <UserAvatar
// name={user.name || null}
// image={user.image || null}
// className="size-8 border"
// />
// </Drawer.Trigger>
// <Drawer.Portal>
// <Drawer.Overlay
// className="fixed inset-0 z-40 h-full bg-background/80 backdrop-blur-sm"
// onClick={closeDrawer}
// />
// <Drawer.Content className="fixed inset-x-0 bottom-0 z-50 mt-24 overflow-hidden rounded-t-[10px] border bg-background px-3 text-sm">
// <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>
// <div className="flex items-center justify-start gap-2 p-2">
// <div className="flex flex-col">
// {user.name && <p className="font-medium">{user.name}</p>}
// {user.email && (
// <p className="w-[200px] truncate text-muted-foreground">
// {user?.email}
// </p>
// )}
// </div>
// </div>
// <ul className="mb-14 mt-1 w-full text-muted-foreground">
// {userButtonConfig.menus.map((item) => {
// const Icon = Icons[item.icon || "arrowRight"];
// return (
// <li
// key={item.href}
// className="rounded-lg text-foreground hover:bg-muted"
// >
// <Link
// href={item.href}
// onClick={closeDrawer}
// className="flex w-full items-center gap-3 px-2.5 py-2"
// >
// <Icon className="size-4" />
// <p className="text-sm">{item.title}</p>
// </Link>
// </li>
// );
// })}
// <li
// key="logout"
// className="rounded-lg text-foreground hover:bg-muted"
// >
// <Link
// href="#"
// onClick={(event) => {
// event.preventDefault();
// closeDrawer();
// // signOut({
// // callbackUrl: `${window.location.origin}/`,
// // redirect: true,
// // });
// }}
// className="flex w-full items-center gap-3 px-2.5 py-2"
// >
// <LogOutIcon className="size-4" />
// <p className="text-sm">Log out</p>
// </Link>
// </li>
// </ul>
// </Drawer.Content>
// <Drawer.Overlay />
// </Drawer.Portal>
// </Drawer.Root>
// );
// }
// Desktop View, use DropdownMenu
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger>
<UserAvatar
name={user.name || undefined}
image={user.image || undefined}
className="size-8 border"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<div className="flex items-center justify-start gap-2 p-2">
<div className="flex flex-col space-y-1 leading-none">
{user.name && <p className="font-medium">{user.name}</p>}
{user.email && (
<p className="w-[200px] truncate text-sm text-muted-foreground">
{user?.email}
</p>
)}
</div>
</div>
<DropdownMenuSeparator />
{userButtonConfig.menus.map((item) => {
const Icon = Icons[item.icon || "arrowRight"];
return (
<DropdownMenuItem
key={item.href}
asChild
className="cursor-pointer"
onClick={() => {
router.push(item.href);
}}
>
<div className="flex items-center space-x-2.5">
<Icon className="size-4" />
<p className="text-sm">{item.title}</p>
</div>
</DropdownMenuItem>
);
})}
<DropdownMenuSeparator />
<DropdownMenuItem
className="cursor-pointer"
onSelect={(event) => {
event.preventDefault();
// signOut({
// callbackUrl: `${window.location.origin}/`,
// redirect: true,
// });
}}
>
<div className="flex items-center space-x-2.5">
<LogOutIcon className="size-4" />
<p className="text-sm">Log out</p>
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -0,0 +1,23 @@
import { cn } from "@/lib/utils";
import Link from "next/link";
import { Logo } from "@/components/logo";
import { buttonVariants } from "@/components/ui/button";
export default function BuiltWithButton() {
return (
<Link
target="_blank"
href="https://mkdirs.com?utm_source=mkdirs&utm_medium=website&utm_campaign=built-with-mkdirs-button&utm_content=built-with-mkdirs"
className={cn(
buttonVariants({ variant: "outline", size: "sm" }),
"px-4 rounded-md",
)}
>
<span>Built with</span>
<span>
<Logo className="size-4 rounded-full" />
</span>
<span className="font-bold">MkSaaS</span>
</Link>
);
}

View File

@ -0,0 +1,28 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import type { AvatarProps } from "@radix-ui/react-avatar";
import { UserIcon } from "lucide-react";
interface UserAvatarProps extends AvatarProps {
name?: string;
image?: string;
}
export function UserAvatar({ name, image, ...props }: UserAvatarProps) {
return (
<Avatar {...props}>
{image ? (
<AvatarImage
alt={name || "user avatar"}
title={name || "user avatar"}
src={image}
referrerPolicy="no-referrer"
/>
) : (
<AvatarFallback>
<span className="sr-only">{name}</span>
<UserIcon className="size-4" />
</AvatarFallback>
)}
</Avatar>
);
}

39
src/config/footer.ts Normal file
View File

@ -0,0 +1,39 @@
import type { FooterConfig } from "@/types";
import { siteConfig } from "./site";
export const footerConfig: FooterConfig = {
links: [
{
title: "Product",
items: [
{ title: "Features", href: "/#features" },
{ title: "Pricing", href: "/#pricing" },
{ title: "FAQ", href: "/#faq" },
],
},
{
title: "Resources",
items: [
{ title: "Showcase", href: "/showcase" },
{ title: "Blog", href: "/blog" },
{ title: "Documentation", href: "/docs", },
],
},
{
title: "Support",
items: [
...(siteConfig.links.twitter ? [{ title: "Twitter", href: siteConfig.links.twitter, external: true }] : []),
...(siteConfig.links.bluesky ? [{ title: "Bluesky", href: siteConfig.links.bluesky, external: true }] : []),
...(siteConfig.links.youtube ? [{ title: "Youtube", href: siteConfig.links.youtube, external: true }] : []),
],
},
{
title: "Company",
items: [
{ title: "About Us", href: "/about" },
{ title: "Privacy Policy", href: "/privacy" },
{ title: "Terms of Service", href: "/terms" },
],
},
],
};

28
src/config/hero.ts Normal file
View File

@ -0,0 +1,28 @@
import type { HeroConfig } from "@/types";
export const heroConfig: HeroConfig = {
title: {
first: "Launch",
second: "AI SaaS websites in minutes",
},
subtitle:
"The best AI SaaS boilerplate, packed with AI, Payment, Blog, Authentication, Newsletter, SEO, Themes and more.",
label: {
text: "🎁 Special Gift - 50% OFF 🎁",
href: "/#pricing",
icon: "gift",
external: false,
},
primaryButton: {
text: "Get Now",
href: "/#pricing",
icon: "rocket",
variant: "default",
},
secondaryButton: {
text: "Live Demo",
href: "https://demo.mksaas.com",
icon: "eye",
variant: "outline",
},
};

501
src/config/landing.ts Normal file
View File

@ -0,0 +1,501 @@
import type { FeatureLdg, InfoLdg, MkdirsInfoLdg, PoweredLdg, TestimonialType } from "@/types";
export const homeFeatures: MkdirsInfoLdg[] = [
{
title: "Feature Rich Directory",
description: "Everything you need to run a directory website.",
images: ["/images/feature-item-detail-page.png", "/images/feature-search-page.png", "/images/feature-filter.png", "/images/feature-filter-category.png"],
list: [
{
title: "Advanced Search",
description:
"Search by keywords from name, description, or rich text content.",
icon: "search",
},
{
title: "Filterable and Sortable",
description:
"Filter items by categories, tags, or sort them by latest, or name.",
icon: "filter",
},
{
title: "Detailed Item Page",
description:
"Each item has a detailed page with rich text content (Markdown).",
icon: "blog",
},
],
button: {
text: "Explore the demo directory",
icon: "eye",
href: "https://demo.mkdirs.com",
variant: "default",
},
},
{
title: "Sanity CMS Integrated",
description: "Deeply integrated with the world-class headless CMS, Sanity.",
images: ["/images/feature-sanity-item-content.png", "/images/feature-sanity-item-image.png", "/images/feature-sanity-item-desc.png", "/images/feature-sanity-category.png"],
list: [
{
title: "Content Management",
description:
"Manage items, categories, tags, blogs, users, and more in Sanity Studio.",
icon: "dashboard",
},
{
title: "Fully Customizable",
description:
"Tailor Sanity Studio with versatile plugins to meet your needs.",
icon: "wrench",
},
{
title: "No Database or Storage Setup",
description:
"Sanity integration eliminates database and storage configuration.",
icon: "database",
},
],
button: {
text: "Contact us to preview the Sanity Studio",
icon: "dashboard",
href: "mailto:support@mkdirs.com",
variant: "default",
},
},
{
title: "Secure Authentication",
description: "Built-in secure authentication system, powered by Auth.js.",
images: ["/images/feature-account-register.png", "/images/feature-account-login.png", "/images/feature-account.png", "/images/feature-account-forget-password.png"],
list: [
{
title: "Email/Password Login",
description: "Email verification and password reset are supported.",
icon: "email",
},
{
title: "Google or GitHub Login",
description:
"Supports Google/GitHub login, easy to add more social logins.",
icon: "githubLucide",
},
{
title: "User Account Settings",
description:
"Users can update their name or link or password in settings.",
icon: "shieldCheck",
},
],
button: {
text: "Explore the Login Page",
icon: "user",
href: "https://demo.mkdirs.com/login",
variant: "default",
},
},
{
title: "Built-in Submission",
description: "Free and paid submissions, easily monetize your directory.",
images: ["/images/feature-submit.png", "/images/feature-submit-payment.png", "/images/feature-submit-publish.png", "/images/feature-submit-dashboard.png", "/images/feature-submit-edit.png", "/images/feature-sanity-item-desc.png"],
list: [
{
title: "Submission Form (AI Suppported)",
description:
"AI Autofill supported, rich text editing and image uploads.",
icon: "submit",
},
{
title: "Paid Submission",
description:
"Paid submission with Stripe, easy to monetize your directory.",
icon: "money",
},
{
title: "Sponsor Ad Submission",
description:
"Show sponsor ad in the listing page and detail page once paid.",
icon: "sponsor",
},
],
button: {
text: "Explore the Submission Workflow",
icon: "submit",
href: "https://demo.mkdirs.com/submit",
variant: "default",
},
},
{
title: "Blog",
description: "Blog system, easy to share your content.",
images: ["/images/feature-blog.png", "/images/feature-blog-category.png", "/images/feature-blog-detail.png", "/images/feature-blog-image.png", "/images/feature-blog-more.png", "/images/feature-blog-sanity.png"],
list: [
{
title: "Blog System",
description: "Blog with categories, authors and rich text content.",
icon: "newspaper",
},
{
title: "Blog Categories",
description:
"Blog with categories, easy to organize your blog posts.",
icon: "category",
},
{
title: "Rich Text Content",
description:
"Supports Image and Code blocks, easy to write blog posts.",
icon: "notebook",
},
],
button: {
text: "Explore the Blog Page",
icon: "blog",
href: "https://demo.mkdirs.com/blog",
variant: "default",
},
},
{
title: "Email",
description: "React email templates, easy to send emails.",
images: ["/images/feature-email-newsletter.png", "/images/feature-email-example.png", "/images/feature-email-preview.png", "/images/feature-email-preview-submission.png"],
list: [
{
title: "Email Templates",
description:
"Built-in emails, automatically send emails to the user and admin.",
icon: "mailbox",
},
{
title: "Customizable Email",
description:
"Support for customizing and previewing email templates.",
icon: "email",
},
{
title: "Newsletter Subscription",
description: "Support for newsletter subscription and unsubscription.",
icon: "mailcheck",
},
],
button: {
text: "Explore the Newsletter subscription",
icon: "mailbox",
href: "https://demo.mkdirs.com/#newsletter",
variant: "default",
},
},
{
title: "Layouts and Components",
description: "Built-in layouts and components, easy to customize your directory website.",
images: ["/images/feature-layout-1.png", "/images/feature-layout-2.png", "/images/feature-layout-3.png", "/images/feature-layout-4.png"],
list: [
{
title: "Layouts",
description:
"Multiple pre-built page layouts to showcase your directory.",
icon: "layout",
},
{
title: "Components",
description:
"Ready-to-use UI components for search, filters, cards, and more.",
icon: "component",
},
{
title: "Item Cards",
description:
"Flexible item cards with both icon and image layouts.",
icon: "grid",
},
],
button: {
text: "Explore the demo directory",
icon: "mailbox",
href: "https://demo.mkdirs.com/home2",
variant: "default",
},
},
{
title: "Docs and Videos",
description: "Comprehensive documentation and video tutorials to help you get started.",
images: ["/images/feature-docs-en.png", "/images/feature-docs-cn.png", "/images/feature-video-home.png", "/images/feature-video-playlist.png"],
list: [
{
title: "Documentation",
description:
"Comprehensive documentation to help you get started.",
icon: "docs",
},
{
title: "Video Tutorials",
description:
"High-quality video tutorials to help you get started.",
icon: "video",
},
{
title: "Multiple Languages",
description:
"Docs and videos are available in English and Chinese.",
icon: "globe",
},
],
button: {
text: "Explore the Documentation",
icon: "mailbox",
href: "https://docs.mkdirs.com",
variant: "default",
},
},
{
title: "SEO Optimization",
description: "SEO optimized, including OG metadata and auto-generated sitemap.",
images: ["/images/feature-seo.png", "/images/feature-seo-item.png", "/images/feature-seo-item-image.png", "/images/feature-seo-item-heading.png", "/images/feature-seo-blog.png"] ,
list: [
{
title: "SEO Metadata",
description:
"Built-in SEO metadata for all pages (especially items and blogs).",
icon: "blog",
},
{
title: "Open Graph",
description: "Built-in Open Graph metadata for social media sharing.",
icon: "image",
},
{
title: "Auto-generated Sitemap",
description: "Auto-generated sitemap for search engines.",
icon: "map",
},
],
button: {
text: "View the performance results",
icon: "chartNoAxes",
href: "https://pagespeed.web.dev/analysis/https-demo-mkdirs-com/egj0638v8m?form_factor=desktop",
variant: "default",
},
},
{
title: "Batteries Included",
description: "Dark mode, responsive design, and customizable theme.",
images: ["/images/feature-ui-theme.png", "/images/feature-ui-theme-blue.png", "/images/feature-ui-dark.png", "/images/feature-ui-dark-item.png", "/images/feature-ui-responsive.png"] ,
list: [
{
title: "Customizable Theme",
description: "Customize the theme to match your brand and style.",
icon: "palette",
},
{
title: "Dark Mode & Responsive",
description:
"Supports dark mode and responsive design.",
icon: "image",
},
{
title: "Built-in Analytics",
description: "Supports Google Analytics and OpenPanel Analytics.",
icon: "chartLine",
},
],
button: {
text: "Explore the demo directory",
icon: "eye",
href: "https://demo.mkdirs.com",
variant: "default",
},
},
];
export const powereds: PoweredLdg[] = [
{
title: "Next.js",
description: "Full stack React framework for production.",
link: "https://nextjs.org/",
icon: "nextjs",
},
{
title: "Auth.js",
description: "Open source authentication library for Next.js.",
link: "https://authjs.dev/",
icon: "authjs",
},
{
title: "Shadcn UI",
description: "Components for building modern websites.",
link: "https://ui.shadcn.com/",
icon: "shadcnui",
},
{
title: "Tailwind CSS",
description: "CSS framework for rapid UI development.",
link: "https://tailwindcss.com/",
icon: "tailwindcss",
},
{
title: "Sanity",
description: "Headless CMS for modern websites.",
link: "https://www.sanity.io/",
icon: "sanity",
},
{
title: "Resend",
description: "Modern email service for developers.",
link: "https://resend.com/",
icon: "resend",
},
{
title: "Stripe",
description: "Best and most secure online payment service.",
link: "https://stripe.com/",
icon: "stripe",
},
{
title: "Vercel AI SDK",
description: "The open source AI Toolkit for TypeScript.",
link: "https://sdk.vercel.ai/",
icon: "vercel",
},
];
export const infos: InfoLdg[] = [
{
title: "Empower your projects",
description:
"Unlock the full potential of your projects with our open-source SaaS platform. Collaborate seamlessly, innovate effortlessly, and scale limitlessly.",
image: "/og.png",
list: [
{
title: "Collaborative",
description: "Work together with your team members in real-time.",
icon: "settings",
},
{
title: "Innovative",
description: "Stay ahead of the curve with access constant updates.",
icon: "settings",
},
{
title: "Scalable",
description:
"Our platform offers the scalability needed to adapt to your needs.",
icon: "search",
},
],
},
{
title: "Seamless Integration",
description:
"Integrate our open-source SaaS seamlessly into your existing workflows. Effortlessly connect with your favorite tools and services for a streamlined experience.",
image: "/og.png",
list: [
{
title: "Flexible",
description:
"Customize your integrations to fit your unique requirements.",
icon: "settings",
},
{
title: "Efficient",
description: "Streamline your processes and reducing manual effort.",
icon: "search",
},
{
title: "Reliable",
description:
"Rely on our robust infrastructure and comprehensive documentation.",
icon: "settings",
},
],
},
];
export const features: FeatureLdg[] = [
{
title: "Feature 1",
description:
"Amet praesentium deserunt ex commodi tempore fuga voluptatem. Sit, sapiente.",
link: "/",
icon: "settings",
},
{
title: "Feature 2",
description:
"Amet praesentium deserunt ex commodi tempore fuga voluptatem. Sit, sapiente.",
link: "/",
icon: "settings",
},
{
title: "Feature 3",
description:
"Amet praesentium deserunt ex commodi tempore fuga voluptatem. Sit, sapiente.",
link: "/",
icon: "settings",
},
{
title: "Feature 4",
description:
"Amet praesentium deserunt ex commodi tempore fuga voluptatem. Sit, sapiente.",
link: "/",
icon: "settings",
},
{
title: "Feature 5",
description:
"Amet praesentium deserunt ex commodi tempore fuga voluptatem. Sit, sapiente.",
link: "/",
icon: "settings",
},
{
title: "Feature 6",
description:
"Amet praesentium deserunt ex commodi tempore fuga voluptatem. Sit, sapiente.",
link: "/",
icon: "settings",
},
];
// The documentation is clear and concise, making it easy to navigate through the setup process.
export const testimonials: TestimonialType[] = [
{
name: "Tom Anderson",
job: "Niche Blogger",
image: "https://randomuser.me/api/portraits/men/9.jpg",
review:
"This directory website template has revolutionized how I present my curated content. It's intuitive, visually appealing, and has significantly improved my site's organization. My readers can now easily find the resources they need. Highly recommended for any content curator!",
},
{
name: "Mike Johnson",
job: "Local Business Owner",
image: "https://randomuser.me/api/portraits/men/4.jpg",
review:
"As a small business owner, I needed an efficient way to showcase local services. This template made creating a town directory website a piece of cake. It's user-friendly, looks professional, and has boosted community engagement significantly.",
},
{
name: "Carlos Mendoza",
job: "Tech Review Blogger",
image: "https://randomuser.me/api/portraits/men/18.jpg",
review:
"I used this template to build a comprehensive tech product directory, and I'm impressed with the results. The category system is flexible, and the search function works flawlessly. It's helped me organize my reviews in a way that's much more accessible to my readers.",
},
{
name: "Ryan Zhang",
job: "Affiliate Marketer",
image: "https://randomuser.me/api/portraits/men/32.jpg",
review:
"I've tried several directory templates, but this one is a cut above. It's SEO-friendly, mobile-responsive, and a breeze to customize. My affiliate links are now organized beautifully, leading to increased click-through rates. It's become an essential tool in my marketing arsenal.",
},
{
name: "Ahmed Hassan",
job: "Online Course Creator",
image: "https://randomuser.me/api/portraits/men/19.jpg",
review:
"This directory website template has been perfect for organizing my online courses and resources. The clean layout and easy navigation have received praise from my students. It's made managing and presenting my educational content so much easier.",
},
{
name: "Daniel Lee",
job: "Freelance Web Designer",
image: "https://randomuser.me/api/portraits/men/7.jpg",
review:
"As a web designer, I appreciate the thought put into this template. It's a solid foundation that I can easily customize for clients needing directory sites. The code is clean, well-documented, and saves me tons of time on each project. A real asset to my business!",
},
];

36
src/config/marketing.ts Normal file
View File

@ -0,0 +1,36 @@
import type { MarketingConfig } from "@/types";
export const marketingConfig: MarketingConfig = {
menus: [
{
title: "Features",
href: "/#features",
icon: "features",
},
{
title: "Pricing",
href: "/#pricing",
icon: "pricing",
},
{
title: "Blog",
href: "/blog",
icon: "blog",
},
{
title: "Documentation",
href: "/docs",
icon: "docs",
},
{
title: "Login",
href: "/auth/login",
icon: "user",
},
{
title: "Dashboard",
href: "/dashboard",
icon: "dashboard",
},
],
};

View File

@ -3,36 +3,36 @@ import type { SiteConfig } from "@/types";
const SITE_URL = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000";
export const siteConfig: SiteConfig = {
name: "Mkdirs",
title: "Mkdirs - The Best Directory Boilerplate with AI",
name: "MkSaaS",
title: "MkSaaS - The Best AI SaaS Boilerplate",
tagline:
"Launch AI-powered directory websites in minutes, simply and effortlessly",
"Launch AI SaaS websites in minutes, simply and effortlessly",
description:
"Mkdirs is the best directory website boilerplate with AI. Launch AI-powered directory websites in minutes, simply and effortlessly",
"MkSaaS is the best AI SaaS boilerplate. Launch AI SaaS websites in minutes, simply and effortlessly",
keywords: [
"Directory",
"Directory Website",
"Directory Website Template",
"Directory Website Boilerplate",
"Directory Website Builder",
"SaaS",
"SaaS Website",
"SaaS Website Template",
"SaaS Website Boilerplate",
"SaaS Website Builder",
],
author: "Mkdirs",
author: "MkSaaS",
url: SITE_URL ?? "",
image: `${SITE_URL}/og.png?v=20250119`,
mail: "support@mkdirs.com",
image: `${SITE_URL}/og.png`,
mail: "support@mksaas.com",
utm: {
source: "mkdirs.com",
source: "mksaas.com",
medium: "referral",
campaign: "navigation",
},
links: {
twitter: "https://x.com/javay_hu",
bluesky: "https://bsky.app/profile/javayhu.com",
github: "https://github.com/MkdirsHQ",
youtube: "https://www.youtube.com/@MkdirsHQ",
docs: "https://docs.mkdirs.com",
demo: "https://demo.mkdirs.com",
studio: "https://demo.mkdirs.com/studio",
showcase: "https://mkdirs.com/showcase",
github: "https://github.com/MkSaaS",
youtube: "https://www.youtube.com/@MkSaaS",
docs: "https://docs.mksaas.com",
demo: "https://demo.mksaas.com",
studio: "https://demo.mksaas.com/studio",
showcase: "https://mksaas.com/showcase",
},
};

21
src/config/user-button.ts Normal file
View File

@ -0,0 +1,21 @@
import type { UserButtonConfig } from "@/types";
export const userButtonConfig: UserButtonConfig = {
menus: [
{
title: "Dashboard",
href: "/dashboard",
icon: "dashboard",
},
{
title: "Settings",
href: "/settings",
icon: "settings",
},
{
title: "Submit Directory",
href: "/submit",
icon: "submit",
},
],
};

View File

@ -1,6 +1,6 @@
import type { ObjectValues } from '@/types/object-values';
import packageInfo from '../../../package.json';
import packageInfo from '../../package.json';
/**
* TODO: update

16
src/hooks/use-scroll.ts Normal file
View File

@ -0,0 +1,16 @@
import { useCallback, useEffect, useState } from "react";
export function useScroll(threshold: number) {
const [scrolled, setScrolled] = useState(false);
const onScroll = useCallback(() => {
setScrolled(window.pageYOffset > threshold);
}, [threshold]);
useEffect(() => {
window.addEventListener("scroll", onScroll);
return () => window.removeEventListener("scroll", onScroll);
}, [onScroll]);
return scrolled;
}

View File

@ -1,4 +1,4 @@
import { AppInfo } from "@/app/constants/app-info";
import { AppInfo } from "@/constants/app-info";
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";