Repository: shadcn-ui/taxonomy Branch: main Commit: 651f984e52ed Files: 166 Total size: 337.3 KB Directory structure: gitextract_6s0f8trj/ ├── .commitlintrc.json ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .husky/ │ ├── commit-msg │ └── pre-commit ├── .nvmrc ├── .prettierignore ├── LICENSE.md ├── README.md ├── app/ │ ├── (auth)/ │ │ ├── layout.tsx │ │ ├── login/ │ │ │ └── page.tsx │ │ └── register/ │ │ └── page.tsx │ ├── (dashboard)/ │ │ └── dashboard/ │ │ ├── billing/ │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── loading.tsx │ │ ├── page.tsx │ │ └── settings/ │ │ ├── loading.tsx │ │ └── page.tsx │ ├── (docs)/ │ │ ├── docs/ │ │ │ ├── [[...slug]]/ │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── guides/ │ │ │ ├── [...slug]/ │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── (editor)/ │ │ └── editor/ │ │ ├── [postId]/ │ │ │ ├── loading.tsx │ │ │ ├── not-found.tsx │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── (marketing)/ │ │ ├── [...slug]/ │ │ │ └── page.tsx │ │ ├── blog/ │ │ │ ├── [...slug]/ │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── pricing/ │ │ └── page.tsx │ ├── api/ │ │ ├── auth/ │ │ │ └── [...nextauth]/ │ │ │ └── _route.ts │ │ ├── og/ │ │ │ └── route.tsx │ │ ├── posts/ │ │ │ ├── [postId]/ │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── users/ │ │ │ ├── [userId]/ │ │ │ │ └── route.ts │ │ │ └── stripe/ │ │ │ └── route.ts │ │ └── webhooks/ │ │ └── stripe/ │ │ └── route.ts │ ├── layout.tsx │ └── robots.ts ├── components/ │ ├── analytics.tsx │ ├── billing-form.tsx │ ├── callout.tsx │ ├── card-skeleton.tsx │ ├── editor.tsx │ ├── empty-placeholder.tsx │ ├── header.tsx │ ├── icons.tsx │ ├── main-nav.tsx │ ├── mdx-card.tsx │ ├── mdx-components.tsx │ ├── mobile-nav.tsx │ ├── mode-toggle.tsx │ ├── nav.tsx │ ├── page-header.tsx │ ├── pager.tsx │ ├── post-create-button.tsx │ ├── post-item.tsx │ ├── post-operations.tsx │ ├── search.tsx │ ├── shell.tsx │ ├── sidebar-nav.tsx │ ├── site-footer.tsx │ ├── tailwind-indicator.tsx │ ├── theme-provider.tsx │ ├── toc.tsx │ ├── ui/ │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── hover-card.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── switch.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ ├── toggle.tsx │ │ ├── tooltip.tsx │ │ └── use-toast.ts │ ├── user-account-nav.tsx │ ├── user-auth-form.tsx │ ├── user-avatar.tsx │ └── user-name-form.tsx ├── config/ │ ├── dashboard.ts │ ├── docs.ts │ ├── marketing.ts │ ├── site.ts │ └── subscriptions.ts ├── content/ │ ├── authors/ │ │ └── shadcn.mdx │ ├── blog/ │ │ ├── deploying-next-apps.mdx │ │ ├── dynamic-routing-static-regeneration.mdx │ │ ├── preview-mode-headless-cms.mdx │ │ └── server-client-components.mdx │ ├── docs/ │ │ ├── documentation/ │ │ │ ├── code-blocks.mdx │ │ │ ├── components.mdx │ │ │ ├── index.mdx │ │ │ └── style-guide.mdx │ │ ├── in-progress.mdx │ │ └── index.mdx │ ├── guides/ │ │ ├── build-blog-using-contentlayer-mdx.mdx │ │ └── using-next-auth-next-13.mdx │ └── pages/ │ ├── privacy.mdx │ └── terms.mdx ├── contentlayer.config.js ├── env.mjs ├── hooks/ │ ├── use-lock-body.ts │ └── use-mounted.ts ├── lib/ │ ├── auth.ts │ ├── db.ts │ ├── exceptions.ts │ ├── session.ts │ ├── stripe.ts │ ├── subscription.ts │ ├── toc.ts │ ├── utils.ts │ └── validations/ │ ├── auth.ts │ ├── og.ts │ ├── post.ts │ └── user.ts ├── middleware.ts ├── next.config.mjs ├── package.json ├── pages/ │ └── api/ │ └── auth/ │ └── [...nextauth].ts ├── postcss.config.js ├── prettier.config.js ├── prisma/ │ ├── migrations/ │ │ ├── 20221021182747_init/ │ │ │ └── migration.sql │ │ ├── 20221118173244_add_stripe_columns/ │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── public/ │ └── site.webmanifest ├── styles/ │ ├── editor.css │ ├── globals.css │ └── mdx.css ├── tailwind.config.js ├── tsconfig.json └── types/ ├── index.d.ts └── next-auth.d.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .commitlintrc.json ================================================ { "extends": ["@commitlint/config-conventional"] } ================================================ FILE: .editorconfig ================================================ # editorconfig.org root = true [*] charset = utf-8 end_of_line = lf indent_size = 2 indent_style = space insert_final_newline = true trim_trailing_whitespace = true ================================================ FILE: .eslintrc.json ================================================ { "$schema": "https://json.schemastore.org/eslintrc", "root": true, "extends": [ "next/core-web-vitals", "prettier", "plugin:tailwindcss/recommended" ], "plugins": ["tailwindcss"], "rules": { "@next/next/no-html-link-for-pages": "off", "react/jsx-key": "off", "tailwindcss/no-custom-classname": "off", "tailwindcss/classnames-order": "error" }, "settings": { "tailwindcss": { "callees": ["cn"], "config": "tailwind.config.js" }, "next": { "rootDir": true } }, "overrides": [ { "files": ["*.ts", "*.tsx"], "parser": "@typescript-eslint/parser" } ] } ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # next.js /.next/ /out/ # production /build # misc .DS_Store *.pem # debug npm-debug.log* yarn-debug.log* yarn-error.log* .pnpm-debug.log* # local env files .env*.local .env # vercel .vercel # typescript *.tsbuildinfo next-env.d.ts .vscode .contentlayer ================================================ FILE: .husky/commit-msg ================================================ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" npx commitlint --edit $1 ================================================ FILE: .husky/pre-commit ================================================ #!/usr/bin/env sh . "$(dirname -- "$0")/_/husky.sh" npx pretty-quick --staged ================================================ FILE: .nvmrc ================================================ v16.18.0 ================================================ FILE: .prettierignore ================================================ dist node_modules .next build .contentlayer ================================================ FILE: LICENSE.md ================================================ MIT License Copyright (c) 2022 shadcn Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Taxonomy An open source application built using the new router, server components and everything new in Next.js 13. > **Warning** > This app is a work in progress. I'm building this in public. You can follow the progress on Twitter [@shadcn](https://twitter.com/shadcn). > See the roadmap below. ## About this project This project as an experiment to see how a modern app (with features like authentication, subscriptions, API routes, static pages for docs ...etc) would work in Next.js 13 and server components. **This is not a starter template.** A few people have asked me to turn this into a starter. I think we could do that once the new features are out of beta. ## Note on Performance > **Warning** > This app is using the unstable releases for Next.js 13 and React 18. The new router and app dir is still in beta and not production-ready. > **Expect some performance hits when testing the dashboard**. > If you see something broken, you can ping me [@shadcn](https://twitter.com/shadcn). ## Features - New `/app` dir, - Routing, Layouts, Nested Layouts and Layout Groups - Data Fetching, Caching and Mutation - Loading UI - Route handlers - Metadata files - Server and Client Components - API Routes and Middlewares - Authentication using **NextAuth.js** - ORM using **Prisma** - Database on **PlanetScale** - UI Components built using **Radix UI** - Documentation and blog using **MDX** and **Contentlayer** - Subscriptions using **Stripe** - Styled using **Tailwind CSS** - Validations using **Zod** - Written in **TypeScript** ## Roadmap - [x] ~Add MDX support for basic pages~ - [x] ~Build marketing pages~ - [x] ~Subscriptions using Stripe~ - [x] ~Responsive styles~ - [x] ~Add OG image for blog using @vercel/og~ - [x] Dark mode ## Known Issues A list of things not working right now: 1. ~GitHub authentication (use email)~ 2. ~[Prisma: Error: ENOENT: no such file or directory, open '/var/task/.next/server/chunks/schema.prisma'](https://github.com/prisma/prisma/issues/16117)~ 3. ~[Next.js 13: Client side navigation does not update head](https://github.com/vercel/next.js/issues/42414)~ 4. [Cannot use opengraph-image.tsx inside catch-all routes](https://github.com/vercel/next.js/issues/48162) ## Why not tRPC, Turborepo or X? I might add this later. For now, I want to see how far we can get using Next.js only. If you have some suggestions, feel free to create an issue. ## Running Locally 1. Install dependencies using pnpm: ```sh pnpm install ``` 2. Copy `.env.example` to `.env.local` and update the variables. ```sh cp .env.example .env.local ``` 3. Start the development server: ```sh pnpm dev ``` ## License Licensed under the [MIT license](https://github.com/shadcn/taxonomy/blob/main/LICENSE.md). ================================================ FILE: app/(auth)/layout.tsx ================================================ interface AuthLayoutProps { children: React.ReactNode } export default function AuthLayout({ children }: AuthLayoutProps) { return
{children}
} ================================================ FILE: app/(auth)/login/page.tsx ================================================ import { Metadata } from "next" import Link from "next/link" import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" import { Icons } from "@/components/icons" import { UserAuthForm } from "@/components/user-auth-form" export const metadata: Metadata = { title: "Login", description: "Login to your account", } export default function LoginPage() { return (
<> Back

Welcome back

Enter your email to sign in to your account

Don't have an account? Sign Up

) } ================================================ FILE: app/(auth)/register/page.tsx ================================================ import Link from "next/link" import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" import { Icons } from "@/components/icons" import { UserAuthForm } from "@/components/user-auth-form" export const metadata = { title: "Create an account", description: "Create an account to get started.", } export default function RegisterPage() { return (
Login

Create an account

Enter your email below to create your account

By clicking continue, you agree to our{" "} Terms of Service {" "} and{" "} Privacy Policy .

) } ================================================ FILE: app/(dashboard)/dashboard/billing/loading.tsx ================================================ import { CardSkeleton } from "@/components/card-skeleton" import { DashboardHeader } from "@/components/header" import { DashboardShell } from "@/components/shell" export default function DashboardBillingLoading() { return (
) } ================================================ FILE: app/(dashboard)/dashboard/billing/page.tsx ================================================ import { redirect } from "next/navigation" import { authOptions } from "@/lib/auth" import { getCurrentUser } from "@/lib/session" import { stripe } from "@/lib/stripe" import { getUserSubscriptionPlan } from "@/lib/subscription" import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card" import { BillingForm } from "@/components/billing-form" import { DashboardHeader } from "@/components/header" import { Icons } from "@/components/icons" import { DashboardShell } from "@/components/shell" export const metadata = { title: "Billing", description: "Manage billing and your subscription plan.", } export default async function BillingPage() { const user = await getCurrentUser() if (!user) { redirect(authOptions?.pages?.signIn || "/login") } const subscriptionPlan = await getUserSubscriptionPlan(user.id) // If user has a pro plan, check cancel status on Stripe. let isCanceled = false if (subscriptionPlan.isPro && subscriptionPlan.stripeSubscriptionId) { const stripePlan = await stripe.subscriptions.retrieve( subscriptionPlan.stripeSubscriptionId ) isCanceled = stripePlan.cancel_at_period_end } return (
This is a demo app. Taxonomy app is a demo app using a Stripe test environment. You can find a list of test card numbers on the{" "} Stripe docs .
) } ================================================ FILE: app/(dashboard)/dashboard/layout.tsx ================================================ import { notFound } from "next/navigation" import { dashboardConfig } from "@/config/dashboard" import { getCurrentUser } from "@/lib/session" import { MainNav } from "@/components/main-nav" import { DashboardNav } from "@/components/nav" import { SiteFooter } from "@/components/site-footer" import { UserAccountNav } from "@/components/user-account-nav" interface DashboardLayoutProps { children?: React.ReactNode } export default async function DashboardLayout({ children, }: DashboardLayoutProps) { const user = await getCurrentUser() if (!user) { return notFound() } return (
{children}
) } ================================================ FILE: app/(dashboard)/dashboard/loading.tsx ================================================ import { DashboardHeader } from "@/components/header" import { PostCreateButton } from "@/components/post-create-button" import { PostItem } from "@/components/post-item" import { DashboardShell } from "@/components/shell" export default function DashboardLoading() { return (
) } ================================================ FILE: app/(dashboard)/dashboard/page.tsx ================================================ import { redirect } from "next/navigation" import { authOptions } from "@/lib/auth" import { db } from "@/lib/db" import { getCurrentUser } from "@/lib/session" import { EmptyPlaceholder } from "@/components/empty-placeholder" import { DashboardHeader } from "@/components/header" import { PostCreateButton } from "@/components/post-create-button" import { PostItem } from "@/components/post-item" import { DashboardShell } from "@/components/shell" export const metadata = { title: "Dashboard", } export default async function DashboardPage() { const user = await getCurrentUser() if (!user) { redirect(authOptions?.pages?.signIn || "/login") } const posts = await db.post.findMany({ where: { authorId: user.id, }, select: { id: true, title: true, published: true, createdAt: true, }, orderBy: { updatedAt: "desc", }, }) return (
{posts?.length ? (
{posts.map((post) => ( ))}
) : ( No posts created You don't have any posts yet. Start creating content. )}
) } ================================================ FILE: app/(dashboard)/dashboard/settings/loading.tsx ================================================ import { Card } from "@/components/ui/card" import { CardSkeleton } from "@/components/card-skeleton" import { DashboardHeader } from "@/components/header" import { DashboardShell } from "@/components/shell" export default function DashboardSettingsLoading() { return (
) } ================================================ FILE: app/(dashboard)/dashboard/settings/page.tsx ================================================ import { redirect } from "next/navigation" import { authOptions } from "@/lib/auth" import { getCurrentUser } from "@/lib/session" import { DashboardHeader } from "@/components/header" import { DashboardShell } from "@/components/shell" import { UserNameForm } from "@/components/user-name-form" export const metadata = { title: "Settings", description: "Manage account and website settings.", } export default async function SettingsPage() { const user = await getCurrentUser() if (!user) { redirect(authOptions?.pages?.signIn || "/login") } return (
) } ================================================ FILE: app/(docs)/docs/[[...slug]]/page.tsx ================================================ import { notFound } from "next/navigation" import { allDocs } from "contentlayer/generated" import { getTableOfContents } from "@/lib/toc" import { Mdx } from "@/components/mdx-components" import { DocsPageHeader } from "@/components/page-header" import { DocsPager } from "@/components/pager" import { DashboardTableOfContents } from "@/components/toc" import "@/styles/mdx.css" import { Metadata } from "next" import { env } from "@/env.mjs" import { absoluteUrl } from "@/lib/utils" interface DocPageProps { params: { slug: string[] } } async function getDocFromParams(params) { const slug = params.slug?.join("/") || "" const doc = allDocs.find((doc) => doc.slugAsParams === slug) if (!doc) { null } return doc } export async function generateMetadata({ params, }: DocPageProps): Promise { const doc = await getDocFromParams(params) if (!doc) { return {} } const url = env.NEXT_PUBLIC_APP_URL const ogUrl = new URL(`${url}/api/og`) ogUrl.searchParams.set("heading", doc.description ?? doc.title) ogUrl.searchParams.set("type", "Documentation") ogUrl.searchParams.set("mode", "dark") return { title: doc.title, description: doc.description, openGraph: { title: doc.title, description: doc.description, type: "article", url: absoluteUrl(doc.slug), images: [ { url: ogUrl.toString(), width: 1200, height: 630, alt: doc.title, }, ], }, twitter: { card: "summary_large_image", title: doc.title, description: doc.description, images: [ogUrl.toString()], }, } } export async function generateStaticParams(): Promise< DocPageProps["params"][] > { return allDocs.map((doc) => ({ slug: doc.slugAsParams.split("/"), })) } export default async function DocPage({ params }: DocPageProps) { const doc = await getDocFromParams(params) if (!doc) { notFound() } const toc = await getTableOfContents(doc.body.raw) return (

) } ================================================ FILE: app/(docs)/docs/layout.tsx ================================================ import { docsConfig } from "@/config/docs" import { DocsSidebarNav } from "@/components/sidebar-nav" interface DocsLayoutProps { children: React.ReactNode } export default function DocsLayout({ children }: DocsLayoutProps) { return (
{children}
) } ================================================ FILE: app/(docs)/guides/[...slug]/page.tsx ================================================ import Link from "next/link" import { notFound } from "next/navigation" import { allGuides } from "contentlayer/generated" import { getTableOfContents } from "@/lib/toc" import { Icons } from "@/components/icons" import { Mdx } from "@/components/mdx-components" import { DocsPageHeader } from "@/components/page-header" import { DashboardTableOfContents } from "@/components/toc" import "@/styles/mdx.css" import { Metadata } from "next" import { env } from "@/env.mjs" import { absoluteUrl, cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" interface GuidePageProps { params: { slug: string[] } } async function getGuideFromParams(params) { const slug = params?.slug?.join("/") const guide = allGuides.find((guide) => guide.slugAsParams === slug) if (!guide) { null } return guide } export async function generateMetadata({ params, }: GuidePageProps): Promise { const guide = await getGuideFromParams(params) if (!guide) { return {} } const url = env.NEXT_PUBLIC_APP_URL const ogUrl = new URL(`${url}/api/og`) ogUrl.searchParams.set("heading", guide.title) ogUrl.searchParams.set("type", "Guide") ogUrl.searchParams.set("mode", "dark") return { title: guide.title, description: guide.description, openGraph: { title: guide.title, description: guide.description, type: "article", url: absoluteUrl(guide.slug), images: [ { url: ogUrl.toString(), width: 1200, height: 630, alt: guide.title, }, ], }, twitter: { card: "summary_large_image", title: guide.title, description: guide.description, images: [ogUrl.toString()], }, } } export async function generateStaticParams(): Promise< GuidePageProps["params"][] > { return allGuides.map((guide) => ({ slug: guide.slugAsParams.split("/"), })) } export default async function GuidePage({ params }: GuidePageProps) { const guide = await getGuideFromParams(params) if (!guide) { notFound() } const toc = await getTableOfContents(guide.body.raw) return (

See all guides
) } ================================================ FILE: app/(docs)/guides/layout.tsx ================================================ interface GuidesLayoutProps { children: React.ReactNode } export default function GuidesLayout({ children }: GuidesLayoutProps) { return
{children}
} ================================================ FILE: app/(docs)/guides/page.tsx ================================================ import Link from "next/link" import { allGuides } from "contentlayer/generated" import { compareDesc } from "date-fns" import { formatDate } from "@/lib/utils" import { DocsPageHeader } from "@/components/page-header" export const metadata = { title: "Guides", description: "This section includes end-to-end guides for developing Next.js 13 apps.", } export default function GuidesPage() { const guides = allGuides .filter((guide) => guide.published) .sort((a, b) => { return compareDesc(new Date(a.date), new Date(b.date)) }) return (
{guides?.length ? (
{guides.map((guide) => (
{guide.featured && ( Featured )}

{guide.title}

{guide.description && (

{guide.description}

)}
{guide.date && (

{formatDate(guide.date)}

)}
View
))}
) : (

No guides published.

)}
) } ================================================ FILE: app/(docs)/layout.tsx ================================================ import Link from "next/link" import { docsConfig } from "@/config/docs" import { siteConfig } from "@/config/site" import { Icons } from "@/components/icons" import { MainNav } from "@/components/main-nav" import { DocsSearch } from "@/components/search" import { DocsSidebarNav } from "@/components/sidebar-nav" import { SiteFooter } from "@/components/site-footer" interface DocsLayoutProps { children: React.ReactNode } export default function DocsLayout({ children }: DocsLayoutProps) { return (
{children}
) } ================================================ FILE: app/(editor)/editor/[postId]/loading.tsx ================================================ import { Skeleton } from "@/components/ui/skeleton" export default function Loading() { return (
) } ================================================ FILE: app/(editor)/editor/[postId]/not-found.tsx ================================================ import Link from "next/link" import { buttonVariants } from "@/components/ui/button" import { EmptyPlaceholder } from "@/components/empty-placeholder" export default function NotFound() { return ( Uh oh! Not Found This post cound not be found. Please try again. Go to Dashboard ) } ================================================ FILE: app/(editor)/editor/[postId]/page.tsx ================================================ import { notFound, redirect } from "next/navigation" import { Post, User } from "@prisma/client" import { authOptions } from "@/lib/auth" import { db } from "@/lib/db" import { getCurrentUser } from "@/lib/session" import { Editor } from "@/components/editor" async function getPostForUser(postId: Post["id"], userId: User["id"]) { return await db.post.findFirst({ where: { id: postId, authorId: userId, }, }) } interface EditorPageProps { params: { postId: string } } export default async function EditorPage({ params }: EditorPageProps) { const user = await getCurrentUser() if (!user) { redirect(authOptions?.pages?.signIn || "/login") } const post = await getPostForUser(params.postId, user.id) if (!post) { notFound() } return ( ) } ================================================ FILE: app/(editor)/editor/layout.tsx ================================================ interface EditorProps { children?: React.ReactNode } export default function EditorLayout({ children }: EditorProps) { return (
{children}
) } ================================================ FILE: app/(marketing)/[...slug]/page.tsx ================================================ import { notFound } from "next/navigation" import { allPages } from "contentlayer/generated" import { Mdx } from "@/components/mdx-components" import "@/styles/mdx.css" import { Metadata } from "next" import { env } from "@/env.mjs" import { siteConfig } from "@/config/site" import { absoluteUrl } from "@/lib/utils" interface PageProps { params: { slug: string[] } } async function getPageFromParams(params) { const slug = params?.slug?.join("/") const page = allPages.find((page) => page.slugAsParams === slug) if (!page) { null } return page } export async function generateMetadata({ params, }: PageProps): Promise { const page = await getPageFromParams(params) if (!page) { return {} } const url = env.NEXT_PUBLIC_APP_URL const ogUrl = new URL(`${url}/api/og`) ogUrl.searchParams.set("heading", page.title) ogUrl.searchParams.set("type", siteConfig.name) ogUrl.searchParams.set("mode", "light") return { title: page.title, description: page.description, openGraph: { title: page.title, description: page.description, type: "article", url: absoluteUrl(page.slug), images: [ { url: ogUrl.toString(), width: 1200, height: 630, alt: page.title, }, ], }, twitter: { card: "summary_large_image", title: page.title, description: page.description, images: [ogUrl.toString()], }, } } export async function generateStaticParams(): Promise { return allPages.map((page) => ({ slug: page.slugAsParams.split("/"), })) } export default async function PagePage({ params }: PageProps) { const page = await getPageFromParams(params) if (!page) { notFound() } return (

{page.title}

{page.description && (

{page.description}

)}

) } ================================================ FILE: app/(marketing)/blog/[...slug]/page.tsx ================================================ import { notFound } from "next/navigation" import { allAuthors, allPosts } from "contentlayer/generated" import { Mdx } from "@/components/mdx-components" import "@/styles/mdx.css" import { Metadata } from "next" import Image from "next/image" import Link from "next/link" import { env } from "@/env.mjs" import { absoluteUrl, cn, formatDate } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" import { Icons } from "@/components/icons" interface PostPageProps { params: { slug: string[] } } async function getPostFromParams(params) { const slug = params?.slug?.join("/") const post = allPosts.find((post) => post.slugAsParams === slug) if (!post) { null } return post } export async function generateMetadata({ params, }: PostPageProps): Promise { const post = await getPostFromParams(params) if (!post) { return {} } const url = env.NEXT_PUBLIC_APP_URL const ogUrl = new URL(`${url}/api/og`) ogUrl.searchParams.set("heading", post.title) ogUrl.searchParams.set("type", "Blog Post") ogUrl.searchParams.set("mode", "dark") return { title: post.title, description: post.description, authors: post.authors.map((author) => ({ name: author, })), openGraph: { title: post.title, description: post.description, type: "article", url: absoluteUrl(post.slug), images: [ { url: ogUrl.toString(), width: 1200, height: 630, alt: post.title, }, ], }, twitter: { card: "summary_large_image", title: post.title, description: post.description, images: [ogUrl.toString()], }, } } export async function generateStaticParams(): Promise< PostPageProps["params"][] > { return allPosts.map((post) => ({ slug: post.slugAsParams.split("/"), })) } export default async function PostPage({ params }: PostPageProps) { const post = await getPostFromParams(params) if (!post) { notFound() } const authors = post.authors.map((author) => allAuthors.find(({ slug }) => slug === `/authors/${author}`) ) return (
See all posts
{post.date && ( )}

{post.title}

{authors?.length ? (
{authors.map((author) => author ? ( {author.title}

{author.title}

@{author.twitter}

) : null )}
) : null}
{post.image && ( {post.title} )}
See all posts
) } ================================================ FILE: app/(marketing)/blog/page.tsx ================================================ import Image from "next/image" import Link from "next/link" import { allPosts } from "contentlayer/generated" import { compareDesc } from "date-fns" import { formatDate } from "@/lib/utils" export const metadata = { title: "Blog", } export default async function BlogPage() { const posts = allPosts .filter((post) => post.published) .sort((a, b) => { return compareDesc(new Date(a.date), new Date(b.date)) }) return (

Blog

A blog built using Contentlayer. Posts are written in MDX.


{posts?.length ? (
{posts.map((post, index) => (
{post.image && ( {post.title} )}

{post.title}

{post.description && (

{post.description}

)} {post.date && (

{formatDate(post.date)}

)} View Article
))}
) : (

No posts published.

)}
) } ================================================ FILE: app/(marketing)/layout.tsx ================================================ import Link from "next/link" import { marketingConfig } from "@/config/marketing" import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" import { MainNav } from "@/components/main-nav" import { SiteFooter } from "@/components/site-footer" interface MarketingLayoutProps { children: React.ReactNode } export default async function MarketingLayout({ children, }: MarketingLayoutProps) { return (
{children}
) } ================================================ FILE: app/(marketing)/page.tsx ================================================ import Link from "next/link" import { env } from "@/env.mjs" import { siteConfig } from "@/config/site" import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" async function getGitHubStars(): Promise { try { const response = await fetch( "https://api.github.com/repos/shadcn/taxonomy", { headers: { Accept: "application/vnd.github+json", Authorization: `Bearer ${env.GITHUB_ACCESS_TOKEN}`, }, next: { revalidate: 60, }, } ) if (!response?.ok) { return null } const json = await response.json() return parseInt(json["stargazers_count"]).toLocaleString() } catch (error) { return null } } export default async function IndexPage() { const stars = await getGitHubStars() return ( <>
Follow along on Twitter

An example app built using Next.js 13 server components.

I'm building a web app with Next.js 13 and open sourcing everything. Follow along as we figure this out together.

Get Started GitHub

Features

This project is an experiment to see how a modern app, with features like auth, subscriptions, API routes, and static pages would work in Next.js 13 app dir.

Next.js 13

App dir, Routing, Layouts, Loading UI and API routes.

React 18

Server and Client Components. Use hook.

Database

ORM using Prisma and deployed on PlanetScale.

Components

UI components built using Radix UI and styled with Tailwind CSS.

Authentication

Authentication using NextAuth.js and middlewares.

Subscriptions

Free and paid subscriptions using Stripe.

Taxonomy also includes a blog and a full-featured documentation site built using Contentlayer and MDX.

Proudly Open Source

Taxonomy is open source and powered by open source software.
{" "} The code is available on{" "} GitHub .{" "}

{stars && (
{stars} stars on GitHub
)}
) } ================================================ FILE: app/(marketing)/pricing/page.tsx ================================================ import Link from "next/link" import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" import { Icons } from "@/components/icons" export const metadata = { title: "Pricing", } export default function PricingPage() { return (

Simple, transparent pricing

Unlock all features including unlimited posts for your blog.

What's included in the PRO plan

  • Unlimited Posts
  • Unlimited Users
  • Custom domain
  • Dashboard Analytics
  • Access to Discord
  • Premium Support

$19

Billed Monthly

Get Started

Taxonomy is a demo app.{" "} You can test the upgrade and won't be charged.

) } ================================================ FILE: app/api/auth/[...nextauth]/_route.ts ================================================ import NextAuth from "next-auth" import { authOptions } from "@/lib/auth" const handler = NextAuth(authOptions) export { handler as GET, handler as POST } ================================================ FILE: app/api/og/route.tsx ================================================ import { ImageResponse } from "@vercel/og" import { ogImageSchema } from "@/lib/validations/og" export const runtime = "edge" const interRegular = fetch( new URL("../../../assets/fonts/Inter-Regular.ttf", import.meta.url) ).then((res) => res.arrayBuffer()) const interBold = fetch( new URL("../../../assets/fonts/CalSans-SemiBold.ttf", import.meta.url) ).then((res) => res.arrayBuffer()) export async function GET(req: Request) { try { const fontRegular = await interRegular const fontBold = await interBold const url = new URL(req.url) const values = ogImageSchema.parse(Object.fromEntries(url.searchParams)) const heading = values.heading.length > 140 ? `${values.heading.substring(0, 140)}...` : values.heading const { mode } = values const paint = mode === "dark" ? "#fff" : "#000" const fontSize = heading.length > 100 ? "70px" : "100px" return new ImageResponse( (
{values.type}
{heading}
tx.shadcn.com
github.com/shadcn/taxonomy
), { width: 1200, height: 630, fonts: [ { name: "Inter", data: fontRegular, weight: 400, style: "normal", }, { name: "Cal Sans", data: fontBold, weight: 700, style: "normal", }, ], } ) } catch (error) { return new Response(`Failed to generate image`, { status: 500, }) } } ================================================ FILE: app/api/posts/[postId]/route.ts ================================================ import { getServerSession } from "next-auth" import * as z from "zod" import { authOptions } from "@/lib/auth" import { db } from "@/lib/db" import { postPatchSchema } from "@/lib/validations/post" const routeContextSchema = z.object({ params: z.object({ postId: z.string(), }), }) export async function DELETE( req: Request, context: z.infer ) { try { // Validate the route params. const { params } = routeContextSchema.parse(context) // Check if the user has access to this post. if (!(await verifyCurrentUserHasAccessToPost(params.postId))) { return new Response(null, { status: 403 }) } // Delete the post. await db.post.delete({ where: { id: params.postId as string, }, }) return new Response(null, { status: 204 }) } catch (error) { if (error instanceof z.ZodError) { return new Response(JSON.stringify(error.issues), { status: 422 }) } return new Response(null, { status: 500 }) } } export async function PATCH( req: Request, context: z.infer ) { try { // Validate route params. const { params } = routeContextSchema.parse(context) // Check if the user has access to this post. if (!(await verifyCurrentUserHasAccessToPost(params.postId))) { return new Response(null, { status: 403 }) } // Get the request body and validate it. const json = await req.json() const body = postPatchSchema.parse(json) // Update the post. // TODO: Implement sanitization for content. await db.post.update({ where: { id: params.postId, }, data: { title: body.title, content: body.content, }, }) return new Response(null, { status: 200 }) } catch (error) { if (error instanceof z.ZodError) { return new Response(JSON.stringify(error.issues), { status: 422 }) } return new Response(null, { status: 500 }) } } async function verifyCurrentUserHasAccessToPost(postId: string) { const session = await getServerSession(authOptions) const count = await db.post.count({ where: { id: postId, authorId: session?.user.id, }, }) return count > 0 } ================================================ FILE: app/api/posts/route.ts ================================================ import { getServerSession } from "next-auth/next" import * as z from "zod" import { authOptions } from "@/lib/auth" import { db } from "@/lib/db" import { RequiresProPlanError } from "@/lib/exceptions" import { getUserSubscriptionPlan } from "@/lib/subscription" const postCreateSchema = z.object({ title: z.string(), content: z.string().optional(), }) export async function GET() { try { const session = await getServerSession(authOptions) if (!session) { return new Response("Unauthorized", { status: 403 }) } const { user } = session const posts = await db.post.findMany({ select: { id: true, title: true, published: true, createdAt: true, }, where: { authorId: user.id, }, }) return new Response(JSON.stringify(posts)) } catch (error) { return new Response(null, { status: 500 }) } } export async function POST(req: Request) { try { const session = await getServerSession(authOptions) if (!session) { return new Response("Unauthorized", { status: 403 }) } const { user } = session const subscriptionPlan = await getUserSubscriptionPlan(user.id) // If user is on a free plan. // Check if user has reached limit of 3 posts. if (!subscriptionPlan?.isPro) { const count = await db.post.count({ where: { authorId: user.id, }, }) if (count >= 3) { throw new RequiresProPlanError() } } const json = await req.json() const body = postCreateSchema.parse(json) const post = await db.post.create({ data: { title: body.title, content: body.content, authorId: session.user.id, }, select: { id: true, }, }) return new Response(JSON.stringify(post)) } catch (error) { if (error instanceof z.ZodError) { return new Response(JSON.stringify(error.issues), { status: 422 }) } if (error instanceof RequiresProPlanError) { return new Response("Requires Pro Plan", { status: 402 }) } return new Response(null, { status: 500 }) } } ================================================ FILE: app/api/users/[userId]/route.ts ================================================ import { getServerSession } from "next-auth/next" import { z } from "zod" import { authOptions } from "@/lib/auth" import { db } from "@/lib/db" import { userNameSchema } from "@/lib/validations/user" const routeContextSchema = z.object({ params: z.object({ userId: z.string(), }), }) export async function PATCH( req: Request, context: z.infer ) { try { // Validate the route context. const { params } = routeContextSchema.parse(context) // Ensure user is authentication and has access to this user. const session = await getServerSession(authOptions) if (!session?.user || params.userId !== session?.user.id) { return new Response(null, { status: 403 }) } // Get the request body and validate it. const body = await req.json() const payload = userNameSchema.parse(body) // Update the user. await db.user.update({ where: { id: session.user.id, }, data: { name: payload.name, }, }) return new Response(null, { status: 200 }) } catch (error) { if (error instanceof z.ZodError) { return new Response(JSON.stringify(error.issues), { status: 422 }) } return new Response(null, { status: 500 }) } } ================================================ FILE: app/api/users/stripe/route.ts ================================================ import { getServerSession } from "next-auth/next" import { z } from "zod" import { proPlan } from "@/config/subscriptions" import { authOptions } from "@/lib/auth" import { stripe } from "@/lib/stripe" import { getUserSubscriptionPlan } from "@/lib/subscription" import { absoluteUrl } from "@/lib/utils" const billingUrl = absoluteUrl("/dashboard/billing") export async function GET(req: Request) { try { const session = await getServerSession(authOptions) if (!session?.user || !session?.user.email) { return new Response(null, { status: 403 }) } const subscriptionPlan = await getUserSubscriptionPlan(session.user.id) // The user is on the pro plan. // Create a portal session to manage subscription. if (subscriptionPlan.isPro && subscriptionPlan.stripeCustomerId) { const stripeSession = await stripe.billingPortal.sessions.create({ customer: subscriptionPlan.stripeCustomerId, return_url: billingUrl, }) return new Response(JSON.stringify({ url: stripeSession.url })) } // The user is on the free plan. // Create a checkout session to upgrade. const stripeSession = await stripe.checkout.sessions.create({ success_url: billingUrl, cancel_url: billingUrl, payment_method_types: ["card"], mode: "subscription", billing_address_collection: "auto", customer_email: session.user.email, line_items: [ { price: proPlan.stripePriceId, quantity: 1, }, ], metadata: { userId: session.user.id, }, }) return new Response(JSON.stringify({ url: stripeSession.url })) } catch (error) { if (error instanceof z.ZodError) { return new Response(JSON.stringify(error.issues), { status: 422 }) } return new Response(null, { status: 500 }) } } ================================================ FILE: app/api/webhooks/stripe/route.ts ================================================ import { headers } from "next/headers" import Stripe from "stripe" import { env } from "@/env.mjs" import { db } from "@/lib/db" import { stripe } from "@/lib/stripe" export async function POST(req: Request) { const body = await req.text() const signature = headers().get("Stripe-Signature") as string let event: Stripe.Event try { event = stripe.webhooks.constructEvent( body, signature, env.STRIPE_WEBHOOK_SECRET ) } catch (error) { return new Response(`Webhook Error: ${error.message}`, { status: 400 }) } const session = event.data.object as Stripe.Checkout.Session if (event.type === "checkout.session.completed") { // Retrieve the subscription details from Stripe. const subscription = await stripe.subscriptions.retrieve( session.subscription as string ) // Update the user stripe into in our database. // Since this is the initial subscription, we need to update // the subscription id and customer id. await db.user.update({ where: { id: session?.metadata?.userId, }, data: { stripeSubscriptionId: subscription.id, stripeCustomerId: subscription.customer as string, stripePriceId: subscription.items.data[0].price.id, stripeCurrentPeriodEnd: new Date( subscription.current_period_end * 1000 ), }, }) } if (event.type === "invoice.payment_succeeded") { // Retrieve the subscription details from Stripe. const subscription = await stripe.subscriptions.retrieve( session.subscription as string ) // Update the price id and set the new period end. await db.user.update({ where: { stripeSubscriptionId: subscription.id, }, data: { stripePriceId: subscription.items.data[0].price.id, stripeCurrentPeriodEnd: new Date( subscription.current_period_end * 1000 ), }, }) } return new Response(null, { status: 200 }) } ================================================ FILE: app/layout.tsx ================================================ import { Inter as FontSans } from "next/font/google" import localFont from "next/font/local" import "@/styles/globals.css" import { siteConfig } from "@/config/site" import { absoluteUrl, cn } from "@/lib/utils" import { Toaster } from "@/components/ui/toaster" import { Analytics } from "@/components/analytics" import { TailwindIndicator } from "@/components/tailwind-indicator" import { ThemeProvider } from "@/components/theme-provider" const fontSans = FontSans({ subsets: ["latin"], variable: "--font-sans", }) // Font files can be colocated inside of `pages` const fontHeading = localFont({ src: "../assets/fonts/CalSans-SemiBold.woff2", variable: "--font-heading", }) interface RootLayoutProps { children: React.ReactNode } export const metadata = { title: { default: siteConfig.name, template: `%s | ${siteConfig.name}`, }, description: siteConfig.description, keywords: [ "Next.js", "React", "Tailwind CSS", "Server Components", "Radix UI", ], authors: [ { name: "shadcn", url: "https://shadcn.com", }, ], creator: "shadcn", themeColor: [ { media: "(prefers-color-scheme: light)", color: "white" }, { media: "(prefers-color-scheme: dark)", color: "black" }, ], openGraph: { type: "website", locale: "en_US", url: siteConfig.url, title: siteConfig.name, description: siteConfig.description, siteName: siteConfig.name, }, twitter: { card: "summary_large_image", title: siteConfig.name, description: siteConfig.description, images: [`${siteConfig.url}/og.jpg`], creator: "@shadcn", }, icons: { icon: "/favicon.ico", shortcut: "/favicon-16x16.png", apple: "/apple-touch-icon.png", }, manifest: `${siteConfig.url}/site.webmanifest`, } export default function RootLayout({ children }: RootLayoutProps) { return ( {children} ) } ================================================ FILE: app/robots.ts ================================================ import { MetadataRoute } from "next" export default function robots(): MetadataRoute.Robots { return { rules: { userAgent: "*", allow: "/", }, } } ================================================ FILE: components/analytics.tsx ================================================ "use client" import { Analytics as VercelAnalytics } from "@vercel/analytics/react" export function Analytics() { return } ================================================ FILE: components/billing-form.tsx ================================================ "use client" import * as React from "react" import { UserSubscriptionPlan } from "types" import { cn, formatDate } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card" import { toast } from "@/components/ui/use-toast" import { Icons } from "@/components/icons" interface BillingFormProps extends React.HTMLAttributes { subscriptionPlan: UserSubscriptionPlan & { isCanceled: boolean } } export function BillingForm({ subscriptionPlan, className, ...props }: BillingFormProps) { const [isLoading, setIsLoading] = React.useState(false) async function onSubmit(event) { event.preventDefault() setIsLoading(!isLoading) // Get a Stripe session URL. const response = await fetch("/api/users/stripe") if (!response?.ok) { return toast({ title: "Something went wrong.", description: "Please refresh the page and try again.", variant: "destructive", }) } // Redirect to the Stripe session. // This could be a checkout page for initial upgrade. // Or portal to manage existing subscription. const session = await response.json() if (session) { window.location.href = session.url } } return (
Subscription Plan You are currently on the {subscriptionPlan.name}{" "} plan. {subscriptionPlan.description} {subscriptionPlan.isPro ? (

{subscriptionPlan.isCanceled ? "Your plan will be canceled on " : "Your plan renews on "} {formatDate(subscriptionPlan.stripeCurrentPeriodEnd)}.

) : null}
) } ================================================ FILE: components/callout.tsx ================================================ import { cn } from "@/lib/utils" interface CalloutProps { icon?: string children?: React.ReactNode type?: "default" | "warning" | "danger" } export function Callout({ children, icon, type = "default", ...props }: CalloutProps) { return (
{icon && {icon}}
{children}
) } ================================================ FILE: components/card-skeleton.tsx ================================================ import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card" import { Skeleton } from "@/components/ui/skeleton" export function CardSkeleton() { return ( ) } ================================================ FILE: components/editor.tsx ================================================ "use client" import * as React from "react" import Link from "next/link" import { useRouter } from "next/navigation" import EditorJS from "@editorjs/editorjs" import { zodResolver } from "@hookform/resolvers/zod" import { Post } from "@prisma/client" import { useForm } from "react-hook-form" import TextareaAutosize from "react-textarea-autosize" import * as z from "zod" import "@/styles/editor.css" import { cn } from "@/lib/utils" import { postPatchSchema } from "@/lib/validations/post" import { buttonVariants } from "@/components/ui/button" import { toast } from "@/components/ui/use-toast" import { Icons } from "@/components/icons" interface EditorProps { post: Pick } type FormData = z.infer export function Editor({ post }: EditorProps) { const { register, handleSubmit } = useForm({ resolver: zodResolver(postPatchSchema), }) const ref = React.useRef() const router = useRouter() const [isSaving, setIsSaving] = React.useState(false) const [isMounted, setIsMounted] = React.useState(false) const initializeEditor = React.useCallback(async () => { const EditorJS = (await import("@editorjs/editorjs")).default const Header = (await import("@editorjs/header")).default const Embed = (await import("@editorjs/embed")).default const Table = (await import("@editorjs/table")).default const List = (await import("@editorjs/list")).default const Code = (await import("@editorjs/code")).default const LinkTool = (await import("@editorjs/link")).default const InlineCode = (await import("@editorjs/inline-code")).default const body = postPatchSchema.parse(post) if (!ref.current) { const editor = new EditorJS({ holder: "editor", onReady() { ref.current = editor }, placeholder: "Type here to write your post...", inlineToolbar: true, data: body.content, tools: { header: Header, linkTool: LinkTool, list: List, code: Code, inlineCode: InlineCode, table: Table, embed: Embed, }, }) } }, [post]) React.useEffect(() => { if (typeof window !== "undefined") { setIsMounted(true) } }, []) React.useEffect(() => { if (isMounted) { initializeEditor() return () => { ref.current?.destroy() ref.current = undefined } } }, [isMounted, initializeEditor]) async function onSubmit(data: FormData) { setIsSaving(true) const blocks = await ref.current?.save() const response = await fetch(`/api/posts/${post.id}`, { method: "PATCH", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ title: data.title, content: blocks, }), }) setIsSaving(false) if (!response?.ok) { return toast({ title: "Something went wrong.", description: "Your post was not saved. Please try again.", variant: "destructive", }) } router.refresh() return toast({ description: "Your post has been saved.", }) } if (!isMounted) { return null } return (
<> Back

{post.published ? "Published" : "Draft"}

Use{" "} Tab {" "} to open the command menu.

) } ================================================ FILE: components/empty-placeholder.tsx ================================================ import * as React from "react" import { cn } from "@/lib/utils" import { Icons } from "@/components/icons" interface EmptyPlaceholderProps extends React.HTMLAttributes {} export function EmptyPlaceholder({ className, children, ...props }: EmptyPlaceholderProps) { return (
{children}
) } interface EmptyPlaceholderIconProps extends Partial> { name: keyof typeof Icons } EmptyPlaceholder.Icon = function EmptyPlaceHolderIcon({ name, className, ...props }: EmptyPlaceholderIconProps) { const Icon = Icons[name] if (!Icon) { return null } return (
) } interface EmptyPlacholderTitleProps extends React.HTMLAttributes {} EmptyPlaceholder.Title = function EmptyPlaceholderTitle({ className, ...props }: EmptyPlacholderTitleProps) { return (

) } interface EmptyPlacholderDescriptionProps extends React.HTMLAttributes {} EmptyPlaceholder.Description = function EmptyPlaceholderDescription({ className, ...props }: EmptyPlacholderDescriptionProps) { return (

) } ================================================ FILE: components/header.tsx ================================================ interface DashboardHeaderProps { heading: string text?: string children?: React.ReactNode } export function DashboardHeader({ heading, text, children, }: DashboardHeaderProps) { return (

{heading}

{text &&

{text}

}
{children}
) } ================================================ FILE: components/icons.tsx ================================================ import { AlertTriangle, ArrowRight, Check, ChevronLeft, ChevronRight, Command, CreditCard, File, FileText, HelpCircle, Image, Laptop, Loader2, LucideProps, Moon, MoreVertical, Pizza, Plus, Settings, SunMedium, Trash, Twitter, User, X, type Icon as LucideIcon, } from "lucide-react" export type Icon = LucideIcon export const Icons = { logo: Command, close: X, spinner: Loader2, chevronLeft: ChevronLeft, chevronRight: ChevronRight, trash: Trash, post: FileText, page: File, media: Image, settings: Settings, billing: CreditCard, ellipsis: MoreVertical, add: Plus, warning: AlertTriangle, user: User, arrowRight: ArrowRight, help: HelpCircle, pizza: Pizza, sun: SunMedium, moon: Moon, laptop: Laptop, gitHub: ({ ...props }: LucideProps) => ( ), twitter: Twitter, check: Check, } ================================================ FILE: components/main-nav.tsx ================================================ "use client" import * as React from "react" import Link from "next/link" import { useSelectedLayoutSegment } from "next/navigation" import { MainNavItem } from "types" import { siteConfig } from "@/config/site" import { cn } from "@/lib/utils" import { Icons } from "@/components/icons" import { MobileNav } from "@/components/mobile-nav" interface MainNavProps { items?: MainNavItem[] children?: React.ReactNode } export function MainNav({ items, children }: MainNavProps) { const segment = useSelectedLayoutSegment() const [showMobileMenu, setShowMobileMenu] = React.useState(false) return (
{siteConfig.name} {items?.length ? ( ) : null} {showMobileMenu && items && ( {children} )}
) } ================================================ FILE: components/mdx-card.tsx ================================================ import Link from "next/link" import { cn } from "@/lib/utils" interface CardProps extends React.HTMLAttributes { href?: string disabled?: boolean } export function MdxCard({ href, className, children, disabled, ...props }: CardProps) { return (
{children}
{href && ( View )}
) } ================================================ FILE: components/mdx-components.tsx ================================================ import * as React from "react" import Image from "next/image" import { useMDXComponent } from "next-contentlayer/hooks" import { cn } from "@/lib/utils" import { Callout } from "@/components/callout" import { MdxCard } from "@/components/mdx-card" const components = { h1: ({ className, ...props }) => (

), h2: ({ className, ...props }) => (

), h3: ({ className, ...props }) => (

), h4: ({ className, ...props }) => (

), h5: ({ className, ...props }) => (

), h6: ({ className, ...props }) => (
), a: ({ className, ...props }) => ( ), p: ({ className, ...props }) => (

), ul: ({ className, ...props }) => (

    ), ol: ({ className, ...props }) => (
      ), li: ({ className, ...props }) => (
    1. ), blockquote: ({ className, ...props }) => (
      *]:text-muted-foreground", className )} {...props} /> ), img: ({ className, alt, ...props }: React.ImgHTMLAttributes) => ( // eslint-disable-next-line @next/next/no-img-element {alt} ), hr: ({ ...props }) =>
      , table: ({ className, ...props }: React.HTMLAttributes) => (
      ), tr: ({ className, ...props }: React.HTMLAttributes) => ( ), th: ({ className, ...props }) => (
      ), td: ({ className, ...props }) => ( ), pre: ({ className, ...props }) => (
        ),
        code: ({ className, ...props }) => (
          
        ),
        Image,
        Callout,
        Card: MdxCard,
      }
      
      interface MdxProps {
        code: string
      }
      
      export function Mdx({ code }: MdxProps) {
        const Component = useMDXComponent(code)
      
        return (
          
      ) } ================================================ FILE: components/mobile-nav.tsx ================================================ import * as React from "react" import Link from "next/link" import { MainNavItem } from "types" import { siteConfig } from "@/config/site" import { cn } from "@/lib/utils" import { useLockBody } from "@/hooks/use-lock-body" import { Icons } from "@/components/icons" interface MobileNavProps { items: MainNavItem[] children?: React.ReactNode } export function MobileNav({ items, children }: MobileNavProps) { useLockBody() return (
      {siteConfig.name} {children}
      ) } ================================================ FILE: components/mode-toggle.tsx ================================================ "use client" import * as React from "react" import { useTheme } from "next-themes" import { Button } from "@/components/ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Icons } from "@/components/icons" export function ModeToggle() { const { setTheme } = useTheme() return ( setTheme("light")}> Light setTheme("dark")}> Dark setTheme("system")}> System ) } ================================================ FILE: components/nav.tsx ================================================ "use client" import Link from "next/link" import { usePathname } from "next/navigation" import { SidebarNavItem } from "types" import { cn } from "@/lib/utils" import { Icons } from "@/components/icons" interface DashboardNavProps { items: SidebarNavItem[] } export function DashboardNav({ items }: DashboardNavProps) { const path = usePathname() if (!items?.length) { return null } return ( ) } ================================================ FILE: components/page-header.tsx ================================================ import { cn } from "@/lib/utils" interface DocsPageHeaderProps extends React.HTMLAttributes { heading: string text?: string } export function DocsPageHeader({ heading, text, className, ...props }: DocsPageHeaderProps) { return ( <>

      {heading}

      {text &&

      {text}

      }

      ) } ================================================ FILE: components/pager.tsx ================================================ import Link from "next/link" import { Doc } from "contentlayer/generated" import { docsConfig } from "@/config/docs" import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" import { Icons } from "@/components/icons" interface DocsPagerProps { doc: Doc } export function DocsPager({ doc }: DocsPagerProps) { const pager = getPagerForDoc(doc) if (!pager) { return null } return (
      {pager?.prev && ( {pager.prev.title} )} {pager?.next && ( {pager.next.title} )}
      ) } export function getPagerForDoc(doc: Doc) { const flattenedLinks = [null, ...flatten(docsConfig.sidebarNav), null] const activeIndex = flattenedLinks.findIndex( (link) => doc.slug === link?.href ) const prev = activeIndex !== 0 ? flattenedLinks[activeIndex - 1] : null const next = activeIndex !== flattenedLinks.length - 1 ? flattenedLinks[activeIndex + 1] : null return { prev, next, } } export function flatten(links: { items? }[]) { return links.reduce((flat, link) => { return flat.concat(link.items ? flatten(link.items) : link) }, []) } ================================================ FILE: components/post-create-button.tsx ================================================ "use client" import * as React from "react" import { useRouter } from "next/navigation" import { cn } from "@/lib/utils" import { ButtonProps, buttonVariants } from "@/components/ui/button" import { toast } from "@/components/ui/use-toast" import { Icons } from "@/components/icons" interface PostCreateButtonProps extends ButtonProps {} export function PostCreateButton({ className, variant, ...props }: PostCreateButtonProps) { const router = useRouter() const [isLoading, setIsLoading] = React.useState(false) async function onClick() { setIsLoading(true) const response = await fetch("/api/posts", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ title: "Untitled Post", }), }) setIsLoading(false) if (!response?.ok) { if (response.status === 402) { return toast({ title: "Limit of 3 posts reached.", description: "Please upgrade to the PRO plan.", variant: "destructive", }) } return toast({ title: "Something went wrong.", description: "Your post was not created. Please try again.", variant: "destructive", }) } const post = await response.json() // This forces a cache invalidation. router.refresh() router.push(`/editor/${post.id}`) } return ( ) } ================================================ FILE: components/post-item.tsx ================================================ import Link from "next/link" import { Post } from "@prisma/client" import { formatDate } from "@/lib/utils" import { Skeleton } from "@/components/ui/skeleton" import { PostOperations } from "@/components/post-operations" interface PostItemProps { post: Pick } export function PostItem({ post }: PostItemProps) { return (
      {post.title}

      {formatDate(post.createdAt?.toDateString())}

      ) } PostItem.Skeleton = function PostItemSkeleton() { return (
      ) } ================================================ FILE: components/post-operations.tsx ================================================ "use client" import * as React from "react" import Link from "next/link" import { useRouter } from "next/navigation" import { Post } from "@prisma/client" import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { toast } from "@/components/ui/use-toast" import { Icons } from "@/components/icons" async function deletePost(postId: string) { const response = await fetch(`/api/posts/${postId}`, { method: "DELETE", }) if (!response?.ok) { toast({ title: "Something went wrong.", description: "Your post was not deleted. Please try again.", variant: "destructive", }) } return true } interface PostOperationsProps { post: Pick } export function PostOperations({ post }: PostOperationsProps) { const router = useRouter() const [showDeleteAlert, setShowDeleteAlert] = React.useState(false) const [isDeleteLoading, setIsDeleteLoading] = React.useState(false) return ( <> Open Edit setShowDeleteAlert(true)} > Delete Are you sure you want to delete this post? This action cannot be undone. Cancel { event.preventDefault() setIsDeleteLoading(true) const deleted = await deletePost(post.id) if (deleted) { setIsDeleteLoading(false) setShowDeleteAlert(false) router.refresh() } }} className="bg-red-600 focus:ring-red-600" > {isDeleteLoading ? ( ) : ( )} Delete ) } ================================================ FILE: components/search.tsx ================================================ "use client" import * as React from "react" import { cn } from "@/lib/utils" import { Input } from "@/components/ui/input" import { toast } from "@/components/ui/use-toast" interface DocsSearchProps extends React.HTMLAttributes {} export function DocsSearch({ className, ...props }: DocsSearchProps) { function onSubmit(event: React.SyntheticEvent) { event.preventDefault() return toast({ title: "Not implemented", description: "We're still working on the search.", }) } return (
      K
      ) } ================================================ FILE: components/shell.tsx ================================================ import * as React from "react" import { cn } from "@/lib/utils" interface DashboardShellProps extends React.HTMLAttributes {} export function DashboardShell({ children, className, ...props }: DashboardShellProps) { return (
      {children}
      ) } ================================================ FILE: components/sidebar-nav.tsx ================================================ "use client" import Link from "next/link" import { usePathname } from "next/navigation" import { SidebarNavItem } from "types" import { cn } from "@/lib/utils" export interface DocsSidebarNavProps { items: SidebarNavItem[] } export function DocsSidebarNav({ items }: DocsSidebarNavProps) { const pathname = usePathname() return items.length ? (
      {items.map((item, index) => (

      {item.title}

      {item.items ? ( ) : null}
      ))}
      ) : null } interface DocsSidebarNavItemsProps { items: SidebarNavItem[] pathname: string | null } export function DocsSidebarNavItems({ items, pathname, }: DocsSidebarNavItemsProps) { return items?.length ? (
      {items.map((item, index) => !item.disabled && item.href ? ( {item.title} ) : ( {item.title} ) )}
      ) : null } ================================================ FILE: components/site-footer.tsx ================================================ import * as React from "react" import { siteConfig } from "@/config/site" import { cn } from "@/lib/utils" import { Icons } from "@/components/icons" import { ModeToggle } from "@/components/mode-toggle" export function SiteFooter({ className }: React.HTMLAttributes) { return ( ) } ================================================ FILE: components/tailwind-indicator.tsx ================================================ export function TailwindIndicator() { if (process.env.NODE_ENV === "production") return null return (
      xs
      sm
      md
      lg
      xl
      2xl
      ) } ================================================ FILE: components/theme-provider.tsx ================================================ "use client" import * as React from "react" import { ThemeProvider as NextThemesProvider } from "next-themes" import { ThemeProviderProps } from "next-themes/dist/types" export function ThemeProvider({ children, ...props }: ThemeProviderProps) { return {children} } ================================================ FILE: components/toc.tsx ================================================ "use client" import * as React from "react" import { TableOfContents } from "@/lib/toc" import { cn } from "@/lib/utils" import { useMounted } from "@/hooks/use-mounted" interface TocProps { toc: TableOfContents } export function DashboardTableOfContents({ toc }: TocProps) { const itemIds = React.useMemo( () => toc.items ? toc.items .flatMap((item) => [item.url, item?.items?.map((item) => item.url)]) .flat() .filter(Boolean) .map((id) => id?.split("#")[1]) : [], [toc] ) const activeHeading = useActiveItem(itemIds) const mounted = useMounted() if (!toc?.items) { return null } return mounted ? (

      On This Page

      ) : null } function useActiveItem(itemIds: (string | undefined)[]) { const [activeId, setActiveId] = React.useState("") React.useEffect(() => { const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { setActiveId(entry.target.id) } }) }, { rootMargin: `0% 0% -80% 0%` } ) itemIds?.forEach((id) => { if (!id) { return } const element = document.getElementById(id) if (element) { observer.observe(element) } }) return () => { itemIds?.forEach((id) => { if (!id) { return } const element = document.getElementById(id) if (element) { observer.unobserve(element) } }) } }, [itemIds]) return activeId } interface TreeProps { tree: TableOfContents level?: number activeItem?: string | null } function Tree({ tree, level = 1, activeItem }: TreeProps) { return tree?.items?.length && level < 3 ? (
        {tree.items.map((item, index) => { return (
      • {item.title} {item.items?.length ? ( ) : null}
      • ) })}
      ) : null } ================================================ FILE: components/ui/accordion.tsx ================================================ "use client" import * as React from "react" import * as AccordionPrimitive from "@radix-ui/react-accordion" import { ChevronDown } from "lucide-react" import { cn } from "@/lib/utils" const Accordion = AccordionPrimitive.Root const AccordionItem = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AccordionItem.displayName = "AccordionItem" const AccordionTrigger = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( svg]:rotate-180", className )} {...props} > {children} )) AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName const AccordionContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => (
      {children}
      )) AccordionContent.displayName = AccordionPrimitive.Content.displayName export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } ================================================ FILE: components/ui/alert-dialog.tsx ================================================ "use client" import * as React from "react" import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" const AlertDialog = AlertDialogPrimitive.Root const AlertDialogTrigger = AlertDialogPrimitive.Trigger const AlertDialogPortal = ({ className, children, ...props }: AlertDialogPrimitive.AlertDialogPortalProps) => (
      {children}
      ) AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName const AlertDialogOverlay = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( )) AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName const AlertDialogContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
      ) AlertDialogHeader.displayName = "AlertDialogHeader" const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
      ) AlertDialogFooter.displayName = "AlertDialogFooter" const AlertDialogTitle = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName const AlertDialogDescription = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName const AlertDialogAction = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName const AlertDialogCancel = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName export { AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogFooter, AlertDialogTitle, AlertDialogDescription, AlertDialogAction, AlertDialogCancel, } ================================================ FILE: components/ui/alert.tsx ================================================ import * as React from "react" import { VariantProps, cva } from "class-variance-authority" import { cn } from "@/lib/utils" const alertVariants = cva( "relative w-full rounded-lg border p-4 [&>svg]:absolute [&>svg]:text-foreground [&>svg]:left-4 [&>svg]:top-4 [&>svg+div]:translate-y-[-3px] [&:has(svg)]:pl-11", { variants: { variant: { default: "bg-background text-foreground", destructive: "text-destructive border-destructive/50 dark:border-destructive [&>svg]:text-destructive text-destructive", }, }, defaultVariants: { variant: "default", }, } ) const Alert = React.forwardRef< HTMLDivElement, React.HTMLAttributes & VariantProps >(({ className, variant, ...props }, ref) => (
      )) Alert.displayName = "Alert" const AlertTitle = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
      )) AlertTitle.displayName = "AlertTitle" const AlertDescription = React.forwardRef< HTMLParagraphElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
      )) AlertDescription.displayName = "AlertDescription" export { Alert, AlertTitle, AlertDescription } ================================================ FILE: components/ui/aspect-ratio.tsx ================================================ "use client" import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" const AspectRatio = AspectRatioPrimitive.Root export { AspectRatio } ================================================ FILE: components/ui/avatar.tsx ================================================ "use client" import * as React from "react" import * as AvatarPrimitive from "@radix-ui/react-avatar" import { cn } from "@/lib/utils" const Avatar = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) Avatar.displayName = AvatarPrimitive.Root.displayName const AvatarImage = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AvatarImage.displayName = AvatarPrimitive.Image.displayName const AvatarFallback = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( )) AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName export { Avatar, AvatarImage, AvatarFallback } ================================================ FILE: components/ui/badge.tsx ================================================ import * as React from "react" import { VariantProps, cva } from "class-variance-authority" import { cn } from "@/lib/utils" const badgeVariants = cva( "inline-flex items-center border rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", { variants: { variant: { default: "bg-primary hover:bg-primary/80 border-transparent text-primary-foreground", secondary: "bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground", destructive: "bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground", outline: "text-foreground", }, }, defaultVariants: { variant: "default", }, } ) export interface BadgeProps extends React.HTMLAttributes, VariantProps {} function Badge({ className, variant, ...props }: BadgeProps) { return (
      ) } export { Badge, badgeVariants } ================================================ FILE: components/ui/button.tsx ================================================ import * as React from "react" import { VariantProps, cva } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background", { variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", outline: "border border-input hover:bg-accent hover:text-accent-foreground", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "underline-offset-4 hover:underline text-primary", }, size: { default: "h-10 py-2 px-4", sm: "h-9 px-3 rounded-md", lg: "h-11 px-8 rounded-md", }, }, defaultVariants: { variant: "default", size: "default", }, } ) export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps {} const Button = React.forwardRef( ({ className, variant, size, ...props }, ref) => { return (