feat: restructure app layout and add marketing pages
This commit is contained in:
parent
f6b029a843
commit
0842c3c5db
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
54
src/app/(marketing)/error.tsx
Normal file
54
src/app/(marketing)/error.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import { Logo } from "@/components/logo";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Loader2Icon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTransition } from "react";
|
||||
|
||||
/**
|
||||
* Learned how to recover from a server component error in Next.js from @asidorenko_
|
||||
*
|
||||
* https://x.com/asidorenko_/status/1841547623712407994
|
||||
*/
|
||||
export default function ErrorPage({ reset }: { reset: () => void }) {
|
||||
const router = useRouter();
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center gap-8">
|
||||
<Logo className="size-12" />
|
||||
|
||||
<h1 className="text-2xl text-center">Oops! Something went wrong!</h1>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="default"
|
||||
disabled={isPending}
|
||||
onClick={() => {
|
||||
startTransition(() => {
|
||||
router.refresh();
|
||||
reset();
|
||||
});
|
||||
}}
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2Icon className="mr-2 size-4 animate-spin" />
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
Try again
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
variant="outline"
|
||||
onClick={() => router.push("/")}
|
||||
>
|
||||
Back to home
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
47
src/app/(marketing)/layout.tsx
Normal file
47
src/app/(marketing)/layout.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
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";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
interface RootLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default async function RootLayout({ children }: RootLayoutProps) {
|
||||
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}
|
||||
|
||||
<Toaster richColors position="top-right" offset={64} />
|
||||
|
||||
<TailwindIndicator />
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
5
src/app/(marketing)/loading.tsx
Normal file
5
src/app/(marketing)/loading.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import { Loader2Icon } from "lucide-react";
|
||||
|
||||
export default function Loading() {
|
||||
return <Loader2Icon className="my-32 mx-auto size-6 animate-spin" />;
|
||||
}
|
21
src/app/(marketing)/not-found.tsx
Normal file
21
src/app/(marketing)/not-found.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { Logo } from "@/components/logo";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center justify-center gap-8">
|
||||
<Logo className="size-12" />
|
||||
|
||||
<h1 className="text-4xl font-bold">404</h1>
|
||||
|
||||
<p className="text-balance text-center text-xl font-medium px-4">
|
||||
Sorry, the page you are looking for does not exist.
|
||||
</p>
|
||||
|
||||
<Button asChild size="lg" variant="default">
|
||||
<Link href="/">Back to home</Link>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 25 KiB |
@ -1,34 +0,0 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
15
src/components/logo.tsx
Normal file
15
src/components/logo.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
|
||||
export function Logo({ className }: { className?: string }) {
|
||||
return (
|
||||
<Image
|
||||
src="/logo.png"
|
||||
alt="Logo"
|
||||
title="Logo"
|
||||
width={96}
|
||||
height={96}
|
||||
className={cn("size-8 rounded-md", className)}
|
||||
/>
|
||||
);
|
||||
}
|
14
src/components/tailwind-indicator.tsx
Normal file
14
src/components/tailwind-indicator.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
export function TailwindIndicator() {
|
||||
if (process.env.NODE_ENV === "production") return null;
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-1 left-1 z-50 flex size-6 items-center justify-center rounded-full bg-gray-800 p-3 font-mono text-xs text-white">
|
||||
<div className="block sm:hidden">xs</div>
|
||||
<div className="hidden sm:max-md:block">sm</div>
|
||||
<div className="hidden md:max-lg:block">md</div>
|
||||
<div className="hidden lg:max-xl:block">lg</div>
|
||||
<div className="hidden xl:max-2xl:block">xl</div>
|
||||
<div className="hidden 2xl:block">2xl</div>
|
||||
</div>
|
||||
);
|
||||
}
|
38
src/config/site.ts
Normal file
38
src/config/site.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import type { SiteConfig } from "@/types";
|
||||
|
||||
const SITE_URL = process.env.NEXT_PUBLIC_APP_URL;
|
||||
|
||||
export const siteConfig: SiteConfig = {
|
||||
name: "Mkdirs",
|
||||
title: "Mkdirs - The Best Directory Boilerplate with AI",
|
||||
tagline:
|
||||
"Launch AI-powered directory 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",
|
||||
keywords: [
|
||||
"Directory",
|
||||
"Directory Website",
|
||||
"Directory Website Template",
|
||||
"Directory Website Boilerplate",
|
||||
"Directory Website Builder",
|
||||
],
|
||||
author: "Mkdirs",
|
||||
url: SITE_URL ?? "",
|
||||
image: `${SITE_URL}/og.png?v=20250119`,
|
||||
mail: "support@mkdirs.com",
|
||||
utm: {
|
||||
source: "mkdirs.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",
|
||||
},
|
||||
};
|
168
src/types/index.d.ts
vendored
Normal file
168
src/types/index.d.ts
vendored
Normal file
@ -0,0 +1,168 @@
|
||||
import type { Icons } from "@/components/icons/icons";
|
||||
|
||||
/**
|
||||
* utm parameters
|
||||
* https://utmbuilder.com/
|
||||
*/
|
||||
export type SiteConfig = {
|
||||
name: string;
|
||||
title: string;
|
||||
tagline: string;
|
||||
description: string;
|
||||
keywords: string[];
|
||||
author: string;
|
||||
url: string;
|
||||
image: string;
|
||||
mail: string;
|
||||
utm: {
|
||||
source: string;
|
||||
medium: string;
|
||||
campaign: string;
|
||||
};
|
||||
links: {
|
||||
github?: string;
|
||||
twitter?: string;
|
||||
twitter_cn?: string;
|
||||
bluesky?: string;
|
||||
youtube?: string;
|
||||
docs?: string;
|
||||
demo?: string;
|
||||
studio?: string;
|
||||
showcase?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type HeroConfig = {
|
||||
title: {
|
||||
first: string;
|
||||
second: string;
|
||||
};
|
||||
subtitle: string;
|
||||
label: {
|
||||
text: string;
|
||||
icon: keyof typeof Icons;
|
||||
href: string;
|
||||
external: boolean;
|
||||
};
|
||||
primaryButton: {
|
||||
text: string;
|
||||
icon: keyof typeof Icons;
|
||||
href: string;
|
||||
variant: "default" | "outline";
|
||||
};
|
||||
secondaryButton: {
|
||||
text: string;
|
||||
icon: keyof typeof Icons;
|
||||
href: string;
|
||||
variant: "default" | "outline";
|
||||
};
|
||||
};
|
||||
|
||||
export type MarketingConfig = {
|
||||
menus: NavItem[];
|
||||
};
|
||||
|
||||
export type DashboardConfig = {
|
||||
menus: NavItem[];
|
||||
};
|
||||
|
||||
export type UserButtonConfig = {
|
||||
menus: NavItem[];
|
||||
};
|
||||
|
||||
export type FooterConfig = {
|
||||
links: NestedNavItem[];
|
||||
};
|
||||
|
||||
export type NavItem = {
|
||||
title: string;
|
||||
href: string;
|
||||
badge?: number;
|
||||
disabled?: boolean;
|
||||
external?: boolean;
|
||||
authorizeOnly?: UserRole;
|
||||
icon?: keyof typeof Icons;
|
||||
};
|
||||
|
||||
export type NestedNavItem = {
|
||||
title: string;
|
||||
items: NavItem[];
|
||||
authorizeOnly?: UserRole;
|
||||
icon?: keyof typeof Icons;
|
||||
};
|
||||
|
||||
export type PriceConfig = {
|
||||
plans: PricePlan[];
|
||||
};
|
||||
|
||||
export type PricePlan = {
|
||||
title: string;
|
||||
description: string;
|
||||
benefits: string[];
|
||||
limitations: string[];
|
||||
price: number;
|
||||
originalPrice?: number;
|
||||
stripePriceId: string | null;
|
||||
};
|
||||
|
||||
export type FAQConfig = {
|
||||
items: FAQItem[];
|
||||
};
|
||||
|
||||
export type FAQItem = {
|
||||
id: string;
|
||||
question: string;
|
||||
answer: string;
|
||||
};
|
||||
|
||||
export type TemplateConfig = {
|
||||
plans: PricePlan[];
|
||||
};
|
||||
|
||||
// landing sections
|
||||
export type MkdirsInfoLdg = {
|
||||
title: string;
|
||||
images: string[];
|
||||
description: string;
|
||||
list: InfoList[];
|
||||
button?: {
|
||||
text: string;
|
||||
icon: keyof typeof Icons;
|
||||
href: string;
|
||||
variant: "default" | "outline";
|
||||
};
|
||||
};
|
||||
|
||||
export type InfoList = {
|
||||
icon: keyof typeof Icons;
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type InfoLdg = {
|
||||
title: string;
|
||||
image: string;
|
||||
description: string;
|
||||
list: InfoList[];
|
||||
};
|
||||
|
||||
export type PoweredLdg = {
|
||||
title: string;
|
||||
description: string;
|
||||
link: string;
|
||||
icon: keyof typeof Icons;
|
||||
};
|
||||
|
||||
export type FeatureLdg = {
|
||||
title: string;
|
||||
description: string;
|
||||
link: string;
|
||||
icon: keyof typeof Icons;
|
||||
};
|
||||
|
||||
export type TestimonialType = {
|
||||
name: string;
|
||||
job: string;
|
||||
image: string;
|
||||
review: string;
|
||||
};
|
Loading…
Reference in New Issue
Block a user