feat: add marketing layout, navbar, footer, and configuration files
This commit is contained in:
parent
0f04b73114
commit
603bf4e9ce
63
src/app/(marketing)/(home)/page.tsx
Normal file
63
src/app/(marketing)/(home)/page.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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",
|
||||
},
|
||||
};
|
||||
@ -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
47
src/app/layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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
36
src/app/manifest.ts
Normal 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'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
18
src/components/container.tsx
Normal file
18
src/components/container.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
149
src/components/layout/footer.tsx
Normal file
149
src/components/layout/footer.tsx
Normal 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 © {new Date().getFullYear()} All Rights Reserved.
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
41
src/components/layout/mode-toggle.tsx
Normal file
41
src/components/layout/mode-toggle.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
259
src/components/layout/navbar.tsx
Normal file
259
src/components/layout/navbar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
186
src/components/layout/user-button.tsx
Normal file
186
src/components/layout/user-button.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
23
src/components/shared/built-with-button.tsx
Normal file
23
src/components/shared/built-with-button.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
28
src/components/shared/user-avatar.tsx
Normal file
28
src/components/shared/user-avatar.tsx
Normal 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
39
src/config/footer.ts
Normal 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
28
src/config/hero.ts
Normal 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
501
src/config/landing.ts
Normal 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
36
src/config/marketing.ts
Normal 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",
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -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
21
src/config/user-button.ts
Normal 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",
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -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
16
src/hooks/use-scroll.ts
Normal 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;
|
||||
}
|
||||
@ -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";
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user