feat: restructure app layout and add marketing pages

This commit is contained in:
javayhu 2025-02-20 01:10:35 +08:00
parent f6b029a843
commit 0842c3c5db
15 changed files with 362 additions and 34 deletions

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

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

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

View 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" />;
}

View 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

View File

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

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