├── .commitlintrc.json
├── .editorconfig
├── .env.example
├── .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
├── opengraph-image.jpg
└── robots.ts
├── assets
└── fonts
│ ├── CalSans-SemiBold.ttf
│ ├── CalSans-SemiBold.woff
│ ├── CalSans-SemiBold.woff2
│ ├── Inter-Bold.ttf
│ └── Inter-Regular.ttf
├── 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
├── pnpm-lock.yaml
├── postcss.config.js
├── prettier.config.js
├── prisma
├── migrations
│ ├── 20221021182747_init
│ │ └── migration.sql
│ ├── 20221118173244_add_stripe_columns
│ │ └── migration.sql
│ └── migration_lock.toml
└── schema.prisma
├── public
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── images
│ ├── avatars
│ │ └── shadcn.png
│ ├── blog
│ │ ├── blog-post-1.jpg
│ │ ├── blog-post-2.jpg
│ │ ├── blog-post-3.jpg
│ │ └── blog-post-4.jpg
│ └── hero.png
├── og.jpg
├── site.webmanifest
└── vercel.svg
├── styles
├── editor.css
├── globals.css
└── mdx.css
├── tailwind.config.js
├── tsconfig.json
└── types
├── index.d.ts
└── next-auth.d.ts
/.commitlintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@commitlint/config-conventional"]
3 | }
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_size = 2
8 | indent_style = space
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # -----------------------------------------------------------------------------
2 | # App
3 | # -----------------------------------------------------------------------------
4 | NEXT_PUBLIC_APP_URL=http://localhost:3000
5 |
6 | # -----------------------------------------------------------------------------
7 | # Authentication (NextAuth.js)
8 | # -----------------------------------------------------------------------------
9 | NEXTAUTH_URL=http://localhost:3000
10 | NEXTAUTH_SECRET=
11 |
12 | GITHUB_CLIENT_ID=
13 | GITHUB_CLIENT_SECRET=
14 | GITHUB_ACCESS_TOKEN=
15 |
16 | # -----------------------------------------------------------------------------
17 | # Database (MySQL - PlanetScale)
18 | # -----------------------------------------------------------------------------
19 | DATABASE_URL="mysql://root:root@localhost:3306/taxonomy?schema=public"
20 |
21 | # -----------------------------------------------------------------------------
22 | # Email (Postmark)
23 | # -----------------------------------------------------------------------------
24 | SMTP_FROM=
25 | POSTMARK_API_TOKEN=
26 | POSTMARK_SIGN_IN_TEMPLATE=
27 | POSTMARK_ACTIVATION_TEMPLATE=
28 |
29 | # -----------------------------------------------------------------------------
30 | # Subscriptions (Stripe)
31 | # -----------------------------------------------------------------------------
32 | STRIPE_API_KEY=
33 | STRIPE_WEBHOOK_SECRET=
34 | STRIPE_PRO_MONTHLY_PLAN_ID=
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/eslintrc",
3 | "root": true,
4 | "extends": [
5 | "next/core-web-vitals",
6 | "prettier",
7 | "plugin:tailwindcss/recommended"
8 | ],
9 | "plugins": ["tailwindcss"],
10 | "rules": {
11 | "@next/next/no-html-link-for-pages": "off",
12 | "react/jsx-key": "off",
13 | "tailwindcss/no-custom-classname": "off",
14 | "tailwindcss/classnames-order": "error"
15 | },
16 | "settings": {
17 | "tailwindcss": {
18 | "callees": ["cn"],
19 | "config": "tailwind.config.js"
20 | },
21 | "next": {
22 | "rootDir": true
23 | }
24 | },
25 | "overrides": [
26 | {
27 | "files": ["*.ts", "*.tsx"],
28 | "parser": "@typescript-eslint/parser"
29 | }
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
39 | .vscode
40 | .contentlayer
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx commitlint --edit $1
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx pretty-quick --staged
5 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v16.18.0
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | .next
4 | build
5 | .contentlayer
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 shadcn
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Taxonomy
2 |
3 | An open source application built using the new router, server components and everything new in Next.js 13.
4 |
5 | > **Warning**
6 | > 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).
7 | > See the roadmap below.
8 |
9 | ## About this project
10 |
11 | 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.
12 |
13 | **This is not a starter template.**
14 |
15 | 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.
16 |
17 | ## Note on Performance
18 |
19 | > **Warning**
20 | > 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.
21 | > **Expect some performance hits when testing the dashboard**.
22 | > If you see something broken, you can ping me [@shadcn](https://twitter.com/shadcn).
23 |
24 | ## Features
25 |
26 | - New `/app` dir,
27 | - Routing, Layouts, Nested Layouts and Layout Groups
28 | - Data Fetching, Caching and Mutation
29 | - Loading UI
30 | - Route handlers
31 | - Metadata files
32 | - Server and Client Components
33 | - API Routes and Middlewares
34 | - Authentication using **NextAuth.js**
35 | - ORM using **Prisma**
36 | - Database on **PlanetScale**
37 | - UI Components built using **Radix UI**
38 | - Documentation and blog using **MDX** and **Contentlayer**
39 | - Subscriptions using **Stripe**
40 | - Styled using **Tailwind CSS**
41 | - Validations using **Zod**
42 | - Written in **TypeScript**
43 |
44 | ## Roadmap
45 |
46 | - [x] ~Add MDX support for basic pages~
47 | - [x] ~Build marketing pages~
48 | - [x] ~Subscriptions using Stripe~
49 | - [x] ~Responsive styles~
50 | - [x] ~Add OG image for blog using @vercel/og~
51 | - [x] Dark mode
52 |
53 | ## Known Issues
54 |
55 | A list of things not working right now:
56 |
57 | 1. ~GitHub authentication (use email)~
58 | 2. ~[Prisma: Error: ENOENT: no such file or directory, open '/var/task/.next/server/chunks/schema.prisma'](https://github.com/prisma/prisma/issues/16117)~
59 | 3. ~[Next.js 13: Client side navigation does not update head](https://github.com/vercel/next.js/issues/42414)~
60 | 4. [Cannot use opengraph-image.tsx inside catch-all routes](https://github.com/vercel/next.js/issues/48162)
61 |
62 | ## Why not tRPC, Turborepo or X?
63 |
64 | I might add this later. For now, I want to see how far we can get using Next.js only.
65 |
66 | If you have some suggestions, feel free to create an issue.
67 |
68 | ## Running Locally
69 |
70 | 1. Install dependencies using pnpm:
71 |
72 | ```sh
73 | pnpm install
74 | ```
75 |
76 | 2. Copy `.env.example` to `.env.local` and update the variables.
77 |
78 | ```sh
79 | cp .env.example .env.local
80 | ```
81 |
82 | 3. Start the development server:
83 |
84 | ```sh
85 | pnpm dev
86 | ```
87 |
88 | ## License
89 |
90 | Licensed under the [MIT license](https://github.com/shadcn/taxonomy/blob/main/LICENSE.md).
91 |
--------------------------------------------------------------------------------
/app/(auth)/layout.tsx:
--------------------------------------------------------------------------------
1 | interface AuthLayoutProps {
2 | children: React.ReactNode
3 | }
4 |
5 | export default function AuthLayout({ children }: AuthLayoutProps) {
6 | return
{children}
7 | }
8 |
--------------------------------------------------------------------------------
/app/(auth)/login/page.tsx:
--------------------------------------------------------------------------------
1 | import { Metadata } from "next"
2 | import Link from "next/link"
3 |
4 | import { cn } from "@/lib/utils"
5 | import { buttonVariants } from "@/components/ui/button"
6 | import { Icons } from "@/components/icons"
7 | import { UserAuthForm } from "@/components/user-auth-form"
8 |
9 | export const metadata: Metadata = {
10 | title: "Login",
11 | description: "Login to your account",
12 | }
13 |
14 | export default function LoginPage() {
15 | return (
16 |
17 |
24 | <>
25 |
26 | Back
27 | >
28 |
29 |
30 |
31 |
32 |
33 | Welcome back
34 |
35 |
36 | Enter your email to sign in to your account
37 |
38 |
39 |
40 |
41 |
45 | Don't have an account? Sign Up
46 |
47 |
48 |
49 |
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/app/(auth)/register/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 |
3 | import { cn } from "@/lib/utils"
4 | import { buttonVariants } from "@/components/ui/button"
5 | import { Icons } from "@/components/icons"
6 | import { UserAuthForm } from "@/components/user-auth-form"
7 |
8 | export const metadata = {
9 | title: "Create an account",
10 | description: "Create an account to get started.",
11 | }
12 |
13 | export default function RegisterPage() {
14 | return (
15 |
16 |
23 | Login
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | Create an account
32 |
33 |
34 | Enter your email below to create your account
35 |
36 |
37 |
38 |
39 | By clicking continue, you agree to our{" "}
40 |
44 | Terms of Service
45 | {" "}
46 | and{" "}
47 |
51 | Privacy Policy
52 |
53 | .
54 |
55 |
56 |
57 |
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/billing/loading.tsx:
--------------------------------------------------------------------------------
1 | import { CardSkeleton } from "@/components/card-skeleton"
2 | import { DashboardHeader } from "@/components/header"
3 | import { DashboardShell } from "@/components/shell"
4 |
5 | export default function DashboardBillingLoading() {
6 | return (
7 |
8 |
12 |
13 |
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/billing/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation"
2 |
3 | import { authOptions } from "@/lib/auth"
4 | import { getCurrentUser } from "@/lib/session"
5 | import { stripe } from "@/lib/stripe"
6 | import { getUserSubscriptionPlan } from "@/lib/subscription"
7 | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
8 | import {
9 | Card,
10 | CardContent,
11 | CardDescription,
12 | CardHeader,
13 | CardTitle,
14 | } from "@/components/ui/card"
15 | import { BillingForm } from "@/components/billing-form"
16 | import { DashboardHeader } from "@/components/header"
17 | import { Icons } from "@/components/icons"
18 | import { DashboardShell } from "@/components/shell"
19 |
20 | export const metadata = {
21 | title: "Billing",
22 | description: "Manage billing and your subscription plan.",
23 | }
24 |
25 | export default async function BillingPage() {
26 | const user = await getCurrentUser()
27 |
28 | if (!user) {
29 | redirect(authOptions?.pages?.signIn || "/login")
30 | }
31 |
32 | const subscriptionPlan = await getUserSubscriptionPlan(user.id)
33 |
34 | // If user has a pro plan, check cancel status on Stripe.
35 | let isCanceled = false
36 | if (subscriptionPlan.isPro && subscriptionPlan.stripeSubscriptionId) {
37 | const stripePlan = await stripe.subscriptions.retrieve(
38 | subscriptionPlan.stripeSubscriptionId
39 | )
40 | isCanceled = stripePlan.cancel_at_period_end
41 | }
42 |
43 | return (
44 |
45 |
49 |
50 |
51 |
52 | This is a demo app.
53 |
54 | Taxonomy app is a demo app using a Stripe test environment. You can
55 | find a list of test card numbers on the{" "}
56 |
62 | Stripe docs
63 |
64 | .
65 |
66 |
67 |
73 |
74 |
75 | )
76 | }
77 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/layout.tsx:
--------------------------------------------------------------------------------
1 | import { notFound } from "next/navigation"
2 |
3 | import { dashboardConfig } from "@/config/dashboard"
4 | import { getCurrentUser } from "@/lib/session"
5 | import { MainNav } from "@/components/main-nav"
6 | import { DashboardNav } from "@/components/nav"
7 | import { SiteFooter } from "@/components/site-footer"
8 | import { UserAccountNav } from "@/components/user-account-nav"
9 |
10 | interface DashboardLayoutProps {
11 | children?: React.ReactNode
12 | }
13 |
14 | export default async function DashboardLayout({
15 | children,
16 | }: DashboardLayoutProps) {
17 | const user = await getCurrentUser()
18 |
19 | if (!user) {
20 | return notFound()
21 | }
22 |
23 | return (
24 |
25 |
37 |
38 |
41 |
42 | {children}
43 |
44 |
45 |
46 |
47 | )
48 | }
49 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/loading.tsx:
--------------------------------------------------------------------------------
1 | import { DashboardHeader } from "@/components/header"
2 | import { PostCreateButton } from "@/components/post-create-button"
3 | import { PostItem } from "@/components/post-item"
4 | import { DashboardShell } from "@/components/shell"
5 |
6 | export default function DashboardLoading() {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation"
2 |
3 | import { authOptions } from "@/lib/auth"
4 | import { db } from "@/lib/db"
5 | import { getCurrentUser } from "@/lib/session"
6 | import { EmptyPlaceholder } from "@/components/empty-placeholder"
7 | import { DashboardHeader } from "@/components/header"
8 | import { PostCreateButton } from "@/components/post-create-button"
9 | import { PostItem } from "@/components/post-item"
10 | import { DashboardShell } from "@/components/shell"
11 |
12 | export const metadata = {
13 | title: "Dashboard",
14 | }
15 |
16 | export default async function DashboardPage() {
17 | const user = await getCurrentUser()
18 |
19 | if (!user) {
20 | redirect(authOptions?.pages?.signIn || "/login")
21 | }
22 |
23 | const posts = await db.post.findMany({
24 | where: {
25 | authorId: user.id,
26 | },
27 | select: {
28 | id: true,
29 | title: true,
30 | published: true,
31 | createdAt: true,
32 | },
33 | orderBy: {
34 | updatedAt: "desc",
35 | },
36 | })
37 |
38 | return (
39 |
40 |
41 |
42 |
43 |
44 | {posts?.length ? (
45 |
46 | {posts.map((post) => (
47 |
48 | ))}
49 |
50 | ) : (
51 |
52 |
53 | No posts created
54 |
55 | You don't have any posts yet. Start creating content.
56 |
57 |
58 |
59 | )}
60 |
61 |
62 | )
63 | }
64 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/settings/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Card } from "@/components/ui/card"
2 | import { CardSkeleton } from "@/components/card-skeleton"
3 | import { DashboardHeader } from "@/components/header"
4 | import { DashboardShell } from "@/components/shell"
5 |
6 | export default function DashboardSettingsLoading() {
7 | return (
8 |
9 |
13 |
14 |
15 |
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/app/(dashboard)/dashboard/settings/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation"
2 |
3 | import { authOptions } from "@/lib/auth"
4 | import { getCurrentUser } from "@/lib/session"
5 | import { DashboardHeader } from "@/components/header"
6 | import { DashboardShell } from "@/components/shell"
7 | import { UserNameForm } from "@/components/user-name-form"
8 |
9 | export const metadata = {
10 | title: "Settings",
11 | description: "Manage account and website settings.",
12 | }
13 |
14 | export default async function SettingsPage() {
15 | const user = await getCurrentUser()
16 |
17 | if (!user) {
18 | redirect(authOptions?.pages?.signIn || "/login")
19 | }
20 |
21 | return (
22 |
23 |
27 |
28 |
29 |
30 |
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/app/(docs)/docs/[[...slug]]/page.tsx:
--------------------------------------------------------------------------------
1 | import { notFound } from "next/navigation"
2 | import { allDocs } from "contentlayer/generated"
3 |
4 | import { getTableOfContents } from "@/lib/toc"
5 | import { Mdx } from "@/components/mdx-components"
6 | import { DocsPageHeader } from "@/components/page-header"
7 | import { DocsPager } from "@/components/pager"
8 | import { DashboardTableOfContents } from "@/components/toc"
9 |
10 | import "@/styles/mdx.css"
11 | import { Metadata } from "next"
12 |
13 | import { env } from "@/env.mjs"
14 | import { absoluteUrl } from "@/lib/utils"
15 |
16 | interface DocPageProps {
17 | params: {
18 | slug: string[]
19 | }
20 | }
21 |
22 | async function getDocFromParams(params) {
23 | const slug = params.slug?.join("/") || ""
24 | const doc = allDocs.find((doc) => doc.slugAsParams === slug)
25 |
26 | if (!doc) {
27 | null
28 | }
29 |
30 | return doc
31 | }
32 |
33 | export async function generateMetadata({
34 | params,
35 | }: DocPageProps): Promise {
36 | const doc = await getDocFromParams(params)
37 |
38 | if (!doc) {
39 | return {}
40 | }
41 |
42 | const url = env.NEXT_PUBLIC_APP_URL
43 |
44 | const ogUrl = new URL(`${url}/api/og`)
45 | ogUrl.searchParams.set("heading", doc.description ?? doc.title)
46 | ogUrl.searchParams.set("type", "Documentation")
47 | ogUrl.searchParams.set("mode", "dark")
48 |
49 | return {
50 | title: doc.title,
51 | description: doc.description,
52 | openGraph: {
53 | title: doc.title,
54 | description: doc.description,
55 | type: "article",
56 | url: absoluteUrl(doc.slug),
57 | images: [
58 | {
59 | url: ogUrl.toString(),
60 | width: 1200,
61 | height: 630,
62 | alt: doc.title,
63 | },
64 | ],
65 | },
66 | twitter: {
67 | card: "summary_large_image",
68 | title: doc.title,
69 | description: doc.description,
70 | images: [ogUrl.toString()],
71 | },
72 | }
73 | }
74 |
75 | export async function generateStaticParams(): Promise<
76 | DocPageProps["params"][]
77 | > {
78 | return allDocs.map((doc) => ({
79 | slug: doc.slugAsParams.split("/"),
80 | }))
81 | }
82 |
83 | export default async function DocPage({ params }: DocPageProps) {
84 | const doc = await getDocFromParams(params)
85 |
86 | if (!doc) {
87 | notFound()
88 | }
89 |
90 | const toc = await getTableOfContents(doc.body.raw)
91 |
92 | return (
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
105 |
106 | )
107 | }
108 |
--------------------------------------------------------------------------------
/app/(docs)/docs/layout.tsx:
--------------------------------------------------------------------------------
1 | import { docsConfig } from "@/config/docs"
2 | import { DocsSidebarNav } from "@/components/sidebar-nav"
3 |
4 | interface DocsLayoutProps {
5 | children: React.ReactNode
6 | }
7 |
8 | export default function DocsLayout({ children }: DocsLayoutProps) {
9 | return (
10 |
11 |
14 | {children}
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/app/(docs)/guides/[...slug]/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 | import { notFound } from "next/navigation"
3 | import { allGuides } from "contentlayer/generated"
4 |
5 | import { getTableOfContents } from "@/lib/toc"
6 | import { Icons } from "@/components/icons"
7 | import { Mdx } from "@/components/mdx-components"
8 | import { DocsPageHeader } from "@/components/page-header"
9 | import { DashboardTableOfContents } from "@/components/toc"
10 |
11 | import "@/styles/mdx.css"
12 | import { Metadata } from "next"
13 |
14 | import { env } from "@/env.mjs"
15 | import { absoluteUrl, cn } from "@/lib/utils"
16 | import { buttonVariants } from "@/components/ui/button"
17 |
18 | interface GuidePageProps {
19 | params: {
20 | slug: string[]
21 | }
22 | }
23 |
24 | async function getGuideFromParams(params) {
25 | const slug = params?.slug?.join("/")
26 | const guide = allGuides.find((guide) => guide.slugAsParams === slug)
27 |
28 | if (!guide) {
29 | null
30 | }
31 |
32 | return guide
33 | }
34 |
35 | export async function generateMetadata({
36 | params,
37 | }: GuidePageProps): Promise {
38 | const guide = await getGuideFromParams(params)
39 |
40 | if (!guide) {
41 | return {}
42 | }
43 |
44 | const url = env.NEXT_PUBLIC_APP_URL
45 |
46 | const ogUrl = new URL(`${url}/api/og`)
47 | ogUrl.searchParams.set("heading", guide.title)
48 | ogUrl.searchParams.set("type", "Guide")
49 | ogUrl.searchParams.set("mode", "dark")
50 |
51 | return {
52 | title: guide.title,
53 | description: guide.description,
54 | openGraph: {
55 | title: guide.title,
56 | description: guide.description,
57 | type: "article",
58 | url: absoluteUrl(guide.slug),
59 | images: [
60 | {
61 | url: ogUrl.toString(),
62 | width: 1200,
63 | height: 630,
64 | alt: guide.title,
65 | },
66 | ],
67 | },
68 | twitter: {
69 | card: "summary_large_image",
70 | title: guide.title,
71 | description: guide.description,
72 | images: [ogUrl.toString()],
73 | },
74 | }
75 | }
76 |
77 | export async function generateStaticParams(): Promise<
78 | GuidePageProps["params"][]
79 | > {
80 | return allGuides.map((guide) => ({
81 | slug: guide.slugAsParams.split("/"),
82 | }))
83 | }
84 |
85 | export default async function GuidePage({ params }: GuidePageProps) {
86 | const guide = await getGuideFromParams(params)
87 |
88 | if (!guide) {
89 | notFound()
90 | }
91 |
92 | const toc = await getTableOfContents(guide.body.raw)
93 |
94 | return (
95 |
96 |
97 |
98 |
99 |
100 |
101 |
105 |
106 | See all guides
107 |
108 |
109 |
110 |
115 |
116 | )
117 | }
118 |
--------------------------------------------------------------------------------
/app/(docs)/guides/layout.tsx:
--------------------------------------------------------------------------------
1 | interface GuidesLayoutProps {
2 | children: React.ReactNode
3 | }
4 |
5 | export default function GuidesLayout({ children }: GuidesLayoutProps) {
6 | return {children}
7 | }
8 |
--------------------------------------------------------------------------------
/app/(docs)/guides/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 | import { allGuides } from "contentlayer/generated"
3 | import { compareDesc } from "date-fns"
4 |
5 | import { formatDate } from "@/lib/utils"
6 | import { DocsPageHeader } from "@/components/page-header"
7 |
8 | export const metadata = {
9 | title: "Guides",
10 | description:
11 | "This section includes end-to-end guides for developing Next.js 13 apps.",
12 | }
13 |
14 | export default function GuidesPage() {
15 | const guides = allGuides
16 | .filter((guide) => guide.published)
17 | .sort((a, b) => {
18 | return compareDesc(new Date(a.date), new Date(b.date))
19 | })
20 |
21 | return (
22 |
23 |
27 | {guides?.length ? (
28 |
29 | {guides.map((guide) => (
30 |
34 | {guide.featured && (
35 |
36 | Featured
37 |
38 | )}
39 |
40 |
41 |
42 | {guide.title}
43 |
44 | {guide.description && (
45 |
{guide.description}
46 | )}
47 |
48 | {guide.date && (
49 |
50 | {formatDate(guide.date)}
51 |
52 | )}
53 |
54 |
55 | View
56 |
57 |
58 | ))}
59 |
60 | ) : (
61 |
No guides published.
62 | )}
63 |
64 | )
65 | }
66 |
--------------------------------------------------------------------------------
/app/(docs)/layout.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 |
3 | import { docsConfig } from "@/config/docs"
4 | import { siteConfig } from "@/config/site"
5 | import { Icons } from "@/components/icons"
6 | import { MainNav } from "@/components/main-nav"
7 | import { DocsSearch } from "@/components/search"
8 | import { DocsSidebarNav } from "@/components/sidebar-nav"
9 | import { SiteFooter } from "@/components/site-footer"
10 |
11 | interface DocsLayoutProps {
12 | children: React.ReactNode
13 | }
14 |
15 | export default function DocsLayout({ children }: DocsLayoutProps) {
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
33 |
34 | GitHub
35 |
36 |
37 |
38 |
39 |
40 |
{children}
41 |
42 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/app/(editor)/editor/[postId]/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Skeleton } from "@/components/ui/skeleton"
2 |
3 | export default function Loading() {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | )
18 | }
19 |
--------------------------------------------------------------------------------
/app/(editor)/editor/[postId]/not-found.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 |
3 | import { buttonVariants } from "@/components/ui/button"
4 | import { EmptyPlaceholder } from "@/components/empty-placeholder"
5 |
6 | export default function NotFound() {
7 | return (
8 |
9 |
10 | Uh oh! Not Found
11 |
12 | This post cound not be found. Please try again.
13 |
14 |
15 | Go to Dashboard
16 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/app/(editor)/editor/[postId]/page.tsx:
--------------------------------------------------------------------------------
1 | import { notFound, redirect } from "next/navigation"
2 | import { Post, User } from "@prisma/client"
3 |
4 | import { authOptions } from "@/lib/auth"
5 | import { db } from "@/lib/db"
6 | import { getCurrentUser } from "@/lib/session"
7 | import { Editor } from "@/components/editor"
8 |
9 | async function getPostForUser(postId: Post["id"], userId: User["id"]) {
10 | return await db.post.findFirst({
11 | where: {
12 | id: postId,
13 | authorId: userId,
14 | },
15 | })
16 | }
17 |
18 | interface EditorPageProps {
19 | params: { postId: string }
20 | }
21 |
22 | export default async function EditorPage({ params }: EditorPageProps) {
23 | const user = await getCurrentUser()
24 |
25 | if (!user) {
26 | redirect(authOptions?.pages?.signIn || "/login")
27 | }
28 |
29 | const post = await getPostForUser(params.postId, user.id)
30 |
31 | if (!post) {
32 | notFound()
33 | }
34 |
35 | return (
36 |
44 | )
45 | }
46 |
--------------------------------------------------------------------------------
/app/(editor)/editor/layout.tsx:
--------------------------------------------------------------------------------
1 | interface EditorProps {
2 | children?: React.ReactNode
3 | }
4 |
5 | export default function EditorLayout({ children }: EditorProps) {
6 | return (
7 |
8 | {children}
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/app/(marketing)/[...slug]/page.tsx:
--------------------------------------------------------------------------------
1 | import { notFound } from "next/navigation"
2 | import { allPages } from "contentlayer/generated"
3 |
4 | import { Mdx } from "@/components/mdx-components"
5 |
6 | import "@/styles/mdx.css"
7 | import { Metadata } from "next"
8 |
9 | import { env } from "@/env.mjs"
10 | import { siteConfig } from "@/config/site"
11 | import { absoluteUrl } from "@/lib/utils"
12 |
13 | interface PageProps {
14 | params: {
15 | slug: string[]
16 | }
17 | }
18 |
19 | async function getPageFromParams(params) {
20 | const slug = params?.slug?.join("/")
21 | const page = allPages.find((page) => page.slugAsParams === slug)
22 |
23 | if (!page) {
24 | null
25 | }
26 |
27 | return page
28 | }
29 |
30 | export async function generateMetadata({
31 | params,
32 | }: PageProps): Promise {
33 | const page = await getPageFromParams(params)
34 |
35 | if (!page) {
36 | return {}
37 | }
38 |
39 | const url = env.NEXT_PUBLIC_APP_URL
40 |
41 | const ogUrl = new URL(`${url}/api/og`)
42 | ogUrl.searchParams.set("heading", page.title)
43 | ogUrl.searchParams.set("type", siteConfig.name)
44 | ogUrl.searchParams.set("mode", "light")
45 |
46 | return {
47 | title: page.title,
48 | description: page.description,
49 | openGraph: {
50 | title: page.title,
51 | description: page.description,
52 | type: "article",
53 | url: absoluteUrl(page.slug),
54 | images: [
55 | {
56 | url: ogUrl.toString(),
57 | width: 1200,
58 | height: 630,
59 | alt: page.title,
60 | },
61 | ],
62 | },
63 | twitter: {
64 | card: "summary_large_image",
65 | title: page.title,
66 | description: page.description,
67 | images: [ogUrl.toString()],
68 | },
69 | }
70 | }
71 |
72 | export async function generateStaticParams(): Promise {
73 | return allPages.map((page) => ({
74 | slug: page.slugAsParams.split("/"),
75 | }))
76 | }
77 |
78 | export default async function PagePage({ params }: PageProps) {
79 | const page = await getPageFromParams(params)
80 |
81 | if (!page) {
82 | notFound()
83 | }
84 |
85 | return (
86 |
87 |
88 |
89 | {page.title}
90 |
91 | {page.description && (
92 |
{page.description}
93 | )}
94 |
95 |
96 |
97 |
98 | )
99 | }
100 |
--------------------------------------------------------------------------------
/app/(marketing)/blog/page.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image"
2 | import Link from "next/link"
3 | import { allPosts } from "contentlayer/generated"
4 | import { compareDesc } from "date-fns"
5 |
6 | import { formatDate } from "@/lib/utils"
7 |
8 | export const metadata = {
9 | title: "Blog",
10 | }
11 |
12 | export default async function BlogPage() {
13 | const posts = allPosts
14 | .filter((post) => post.published)
15 | .sort((a, b) => {
16 | return compareDesc(new Date(a.date), new Date(b.date))
17 | })
18 |
19 | return (
20 |
21 |
22 |
23 |
24 | Blog
25 |
26 |
27 | A blog built using Contentlayer. Posts are written in MDX.
28 |
29 |
30 |
31 |
32 | {posts?.length ? (
33 |
34 | {posts.map((post, index) => (
35 |
39 | {post.image && (
40 |
48 | )}
49 | {post.title}
50 | {post.description && (
51 | {post.description}
52 | )}
53 | {post.date && (
54 |
55 | {formatDate(post.date)}
56 |
57 | )}
58 |
59 | View Article
60 |
61 |
62 | ))}
63 |
64 | ) : (
65 |
No posts published.
66 | )}
67 |
68 | )
69 | }
70 |
--------------------------------------------------------------------------------
/app/(marketing)/layout.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 |
3 | import { marketingConfig } from "@/config/marketing"
4 | import { cn } from "@/lib/utils"
5 | import { buttonVariants } from "@/components/ui/button"
6 | import { MainNav } from "@/components/main-nav"
7 | import { SiteFooter } from "@/components/site-footer"
8 |
9 | interface MarketingLayoutProps {
10 | children: React.ReactNode
11 | }
12 |
13 | export default async function MarketingLayout({
14 | children,
15 | }: MarketingLayoutProps) {
16 | return (
17 |
18 |
34 |
{children}
35 |
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/app/(marketing)/pricing/page.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 |
3 | import { cn } from "@/lib/utils"
4 | import { buttonVariants } from "@/components/ui/button"
5 | import { Icons } from "@/components/icons"
6 |
7 | export const metadata = {
8 | title: "Pricing",
9 | }
10 |
11 | export default function PricingPage() {
12 | return (
13 |
14 |
15 |
16 | Simple, transparent pricing
17 |
18 |
19 | Unlock all features including unlimited posts for your blog.
20 |
21 |
22 |
23 |
24 |
25 | What's included in the PRO plan
26 |
27 |
28 |
29 | Unlimited Posts
30 |
31 |
32 | Unlimited Users
33 |
34 |
35 |
36 | Custom domain
37 |
38 |
39 | Dashboard Analytics
40 |
41 |
42 | Access to Discord
43 |
44 |
45 | Premium Support
46 |
47 |
48 |
49 |
50 |
51 |
$19
52 |
53 | Billed Monthly
54 |
55 |
56 |
57 | Get Started
58 |
59 |
60 |
61 |
62 |
63 | Taxonomy is a demo app.{" "}
64 | You can test the upgrade and won't be charged.
65 |
66 |
67 |
68 | )
69 | }
70 |
--------------------------------------------------------------------------------
/app/api/auth/[...nextauth]/_route.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth"
2 |
3 | import { authOptions } from "@/lib/auth"
4 |
5 | const handler = NextAuth(authOptions)
6 |
7 | export { handler as GET, handler as POST }
8 |
--------------------------------------------------------------------------------
/app/api/posts/[postId]/route.ts:
--------------------------------------------------------------------------------
1 | import { getServerSession } from "next-auth"
2 | import * as z from "zod"
3 |
4 | import { authOptions } from "@/lib/auth"
5 | import { db } from "@/lib/db"
6 | import { postPatchSchema } from "@/lib/validations/post"
7 |
8 | const routeContextSchema = z.object({
9 | params: z.object({
10 | postId: z.string(),
11 | }),
12 | })
13 |
14 | export async function DELETE(
15 | req: Request,
16 | context: z.infer
17 | ) {
18 | try {
19 | // Validate the route params.
20 | const { params } = routeContextSchema.parse(context)
21 |
22 | // Check if the user has access to this post.
23 | if (!(await verifyCurrentUserHasAccessToPost(params.postId))) {
24 | return new Response(null, { status: 403 })
25 | }
26 |
27 | // Delete the post.
28 | await db.post.delete({
29 | where: {
30 | id: params.postId as string,
31 | },
32 | })
33 |
34 | return new Response(null, { status: 204 })
35 | } catch (error) {
36 | if (error instanceof z.ZodError) {
37 | return new Response(JSON.stringify(error.issues), { status: 422 })
38 | }
39 |
40 | return new Response(null, { status: 500 })
41 | }
42 | }
43 |
44 | export async function PATCH(
45 | req: Request,
46 | context: z.infer
47 | ) {
48 | try {
49 | // Validate route params.
50 | const { params } = routeContextSchema.parse(context)
51 |
52 | // Check if the user has access to this post.
53 | if (!(await verifyCurrentUserHasAccessToPost(params.postId))) {
54 | return new Response(null, { status: 403 })
55 | }
56 |
57 | // Get the request body and validate it.
58 | const json = await req.json()
59 | const body = postPatchSchema.parse(json)
60 |
61 | // Update the post.
62 | // TODO: Implement sanitization for content.
63 | await db.post.update({
64 | where: {
65 | id: params.postId,
66 | },
67 | data: {
68 | title: body.title,
69 | content: body.content,
70 | },
71 | })
72 |
73 | return new Response(null, { status: 200 })
74 | } catch (error) {
75 | if (error instanceof z.ZodError) {
76 | return new Response(JSON.stringify(error.issues), { status: 422 })
77 | }
78 |
79 | return new Response(null, { status: 500 })
80 | }
81 | }
82 |
83 | async function verifyCurrentUserHasAccessToPost(postId: string) {
84 | const session = await getServerSession(authOptions)
85 | const count = await db.post.count({
86 | where: {
87 | id: postId,
88 | authorId: session?.user.id,
89 | },
90 | })
91 |
92 | return count > 0
93 | }
94 |
--------------------------------------------------------------------------------
/app/api/posts/route.ts:
--------------------------------------------------------------------------------
1 | import { getServerSession } from "next-auth/next"
2 | import * as z from "zod"
3 |
4 | import { authOptions } from "@/lib/auth"
5 | import { db } from "@/lib/db"
6 | import { RequiresProPlanError } from "@/lib/exceptions"
7 | import { getUserSubscriptionPlan } from "@/lib/subscription"
8 |
9 | const postCreateSchema = z.object({
10 | title: z.string(),
11 | content: z.string().optional(),
12 | })
13 |
14 | export async function GET() {
15 | try {
16 | const session = await getServerSession(authOptions)
17 |
18 | if (!session) {
19 | return new Response("Unauthorized", { status: 403 })
20 | }
21 |
22 | const { user } = session
23 | const posts = await db.post.findMany({
24 | select: {
25 | id: true,
26 | title: true,
27 | published: true,
28 | createdAt: true,
29 | },
30 | where: {
31 | authorId: user.id,
32 | },
33 | })
34 |
35 | return new Response(JSON.stringify(posts))
36 | } catch (error) {
37 | return new Response(null, { status: 500 })
38 | }
39 | }
40 |
41 | export async function POST(req: Request) {
42 | try {
43 | const session = await getServerSession(authOptions)
44 |
45 | if (!session) {
46 | return new Response("Unauthorized", { status: 403 })
47 | }
48 |
49 | const { user } = session
50 | const subscriptionPlan = await getUserSubscriptionPlan(user.id)
51 |
52 | // If user is on a free plan.
53 | // Check if user has reached limit of 3 posts.
54 | if (!subscriptionPlan?.isPro) {
55 | const count = await db.post.count({
56 | where: {
57 | authorId: user.id,
58 | },
59 | })
60 |
61 | if (count >= 3) {
62 | throw new RequiresProPlanError()
63 | }
64 | }
65 |
66 | const json = await req.json()
67 | const body = postCreateSchema.parse(json)
68 |
69 | const post = await db.post.create({
70 | data: {
71 | title: body.title,
72 | content: body.content,
73 | authorId: session.user.id,
74 | },
75 | select: {
76 | id: true,
77 | },
78 | })
79 |
80 | return new Response(JSON.stringify(post))
81 | } catch (error) {
82 | if (error instanceof z.ZodError) {
83 | return new Response(JSON.stringify(error.issues), { status: 422 })
84 | }
85 |
86 | if (error instanceof RequiresProPlanError) {
87 | return new Response("Requires Pro Plan", { status: 402 })
88 | }
89 |
90 | return new Response(null, { status: 500 })
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/app/api/users/[userId]/route.ts:
--------------------------------------------------------------------------------
1 | import { getServerSession } from "next-auth/next"
2 | import { z } from "zod"
3 |
4 | import { authOptions } from "@/lib/auth"
5 | import { db } from "@/lib/db"
6 | import { userNameSchema } from "@/lib/validations/user"
7 |
8 | const routeContextSchema = z.object({
9 | params: z.object({
10 | userId: z.string(),
11 | }),
12 | })
13 |
14 | export async function PATCH(
15 | req: Request,
16 | context: z.infer
17 | ) {
18 | try {
19 | // Validate the route context.
20 | const { params } = routeContextSchema.parse(context)
21 |
22 | // Ensure user is authentication and has access to this user.
23 | const session = await getServerSession(authOptions)
24 | if (!session?.user || params.userId !== session?.user.id) {
25 | return new Response(null, { status: 403 })
26 | }
27 |
28 | // Get the request body and validate it.
29 | const body = await req.json()
30 | const payload = userNameSchema.parse(body)
31 |
32 | // Update the user.
33 | await db.user.update({
34 | where: {
35 | id: session.user.id,
36 | },
37 | data: {
38 | name: payload.name,
39 | },
40 | })
41 |
42 | return new Response(null, { status: 200 })
43 | } catch (error) {
44 | if (error instanceof z.ZodError) {
45 | return new Response(JSON.stringify(error.issues), { status: 422 })
46 | }
47 |
48 | return new Response(null, { status: 500 })
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/api/users/stripe/route.ts:
--------------------------------------------------------------------------------
1 | import { getServerSession } from "next-auth/next"
2 | import { z } from "zod"
3 |
4 | import { proPlan } from "@/config/subscriptions"
5 | import { authOptions } from "@/lib/auth"
6 | import { stripe } from "@/lib/stripe"
7 | import { getUserSubscriptionPlan } from "@/lib/subscription"
8 | import { absoluteUrl } from "@/lib/utils"
9 |
10 | const billingUrl = absoluteUrl("/dashboard/billing")
11 |
12 | export async function GET(req: Request) {
13 | try {
14 | const session = await getServerSession(authOptions)
15 |
16 | if (!session?.user || !session?.user.email) {
17 | return new Response(null, { status: 403 })
18 | }
19 |
20 | const subscriptionPlan = await getUserSubscriptionPlan(session.user.id)
21 |
22 | // The user is on the pro plan.
23 | // Create a portal session to manage subscription.
24 | if (subscriptionPlan.isPro && subscriptionPlan.stripeCustomerId) {
25 | const stripeSession = await stripe.billingPortal.sessions.create({
26 | customer: subscriptionPlan.stripeCustomerId,
27 | return_url: billingUrl,
28 | })
29 |
30 | return new Response(JSON.stringify({ url: stripeSession.url }))
31 | }
32 |
33 | // The user is on the free plan.
34 | // Create a checkout session to upgrade.
35 | const stripeSession = await stripe.checkout.sessions.create({
36 | success_url: billingUrl,
37 | cancel_url: billingUrl,
38 | payment_method_types: ["card"],
39 | mode: "subscription",
40 | billing_address_collection: "auto",
41 | customer_email: session.user.email,
42 | line_items: [
43 | {
44 | price: proPlan.stripePriceId,
45 | quantity: 1,
46 | },
47 | ],
48 | metadata: {
49 | userId: session.user.id,
50 | },
51 | })
52 |
53 | return new Response(JSON.stringify({ url: stripeSession.url }))
54 | } catch (error) {
55 | if (error instanceof z.ZodError) {
56 | return new Response(JSON.stringify(error.issues), { status: 422 })
57 | }
58 |
59 | return new Response(null, { status: 500 })
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/app/api/webhooks/stripe/route.ts:
--------------------------------------------------------------------------------
1 | import { headers } from "next/headers"
2 | import Stripe from "stripe"
3 |
4 | import { env } from "@/env.mjs"
5 | import { db } from "@/lib/db"
6 | import { stripe } from "@/lib/stripe"
7 |
8 | export async function POST(req: Request) {
9 | const body = await req.text()
10 | const signature = headers().get("Stripe-Signature") as string
11 |
12 | let event: Stripe.Event
13 |
14 | try {
15 | event = stripe.webhooks.constructEvent(
16 | body,
17 | signature,
18 | env.STRIPE_WEBHOOK_SECRET
19 | )
20 | } catch (error) {
21 | return new Response(`Webhook Error: ${error.message}`, { status: 400 })
22 | }
23 |
24 | const session = event.data.object as Stripe.Checkout.Session
25 |
26 | if (event.type === "checkout.session.completed") {
27 | // Retrieve the subscription details from Stripe.
28 | const subscription = await stripe.subscriptions.retrieve(
29 | session.subscription as string
30 | )
31 |
32 | // Update the user stripe into in our database.
33 | // Since this is the initial subscription, we need to update
34 | // the subscription id and customer id.
35 | await db.user.update({
36 | where: {
37 | id: session?.metadata?.userId,
38 | },
39 | data: {
40 | stripeSubscriptionId: subscription.id,
41 | stripeCustomerId: subscription.customer as string,
42 | stripePriceId: subscription.items.data[0].price.id,
43 | stripeCurrentPeriodEnd: new Date(
44 | subscription.current_period_end * 1000
45 | ),
46 | },
47 | })
48 | }
49 |
50 | if (event.type === "invoice.payment_succeeded") {
51 | // Retrieve the subscription details from Stripe.
52 | const subscription = await stripe.subscriptions.retrieve(
53 | session.subscription as string
54 | )
55 |
56 | // Update the price id and set the new period end.
57 | await db.user.update({
58 | where: {
59 | stripeSubscriptionId: subscription.id,
60 | },
61 | data: {
62 | stripePriceId: subscription.items.data[0].price.id,
63 | stripeCurrentPeriodEnd: new Date(
64 | subscription.current_period_end * 1000
65 | ),
66 | },
67 | })
68 | }
69 |
70 | return new Response(null, { status: 200 })
71 | }
72 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import { Inter as FontSans } from "next/font/google"
2 | import localFont from "next/font/local"
3 |
4 | import "@/styles/globals.css"
5 | import { siteConfig } from "@/config/site"
6 | import { absoluteUrl, cn } from "@/lib/utils"
7 | import { Toaster } from "@/components/ui/toaster"
8 | import { Analytics } from "@/components/analytics"
9 | import { TailwindIndicator } from "@/components/tailwind-indicator"
10 | import { ThemeProvider } from "@/components/theme-provider"
11 |
12 | const fontSans = FontSans({
13 | subsets: ["latin"],
14 | variable: "--font-sans",
15 | })
16 |
17 | // Font files can be colocated inside of `pages`
18 | const fontHeading = localFont({
19 | src: "../assets/fonts/CalSans-SemiBold.woff2",
20 | variable: "--font-heading",
21 | })
22 |
23 | interface RootLayoutProps {
24 | children: React.ReactNode
25 | }
26 |
27 | export const metadata = {
28 | title: {
29 | default: siteConfig.name,
30 | template: `%s | ${siteConfig.name}`,
31 | },
32 | description: siteConfig.description,
33 | keywords: [
34 | "Next.js",
35 | "React",
36 | "Tailwind CSS",
37 | "Server Components",
38 | "Radix UI",
39 | ],
40 | authors: [
41 | {
42 | name: "shadcn",
43 | url: "https://shadcn.com",
44 | },
45 | ],
46 | creator: "shadcn",
47 | themeColor: [
48 | { media: "(prefers-color-scheme: light)", color: "white" },
49 | { media: "(prefers-color-scheme: dark)", color: "black" },
50 | ],
51 | openGraph: {
52 | type: "website",
53 | locale: "en_US",
54 | url: siteConfig.url,
55 | title: siteConfig.name,
56 | description: siteConfig.description,
57 | siteName: siteConfig.name,
58 | },
59 | twitter: {
60 | card: "summary_large_image",
61 | title: siteConfig.name,
62 | description: siteConfig.description,
63 | images: [`${siteConfig.url}/og.jpg`],
64 | creator: "@shadcn",
65 | },
66 | icons: {
67 | icon: "/favicon.ico",
68 | shortcut: "/favicon-16x16.png",
69 | apple: "/apple-touch-icon.png",
70 | },
71 | manifest: `${siteConfig.url}/site.webmanifest`,
72 | }
73 |
74 | export default function RootLayout({ children }: RootLayoutProps) {
75 | return (
76 |
77 |
78 |
85 |
86 | {children}
87 |
88 |
89 |
90 |
91 |
92 |
93 | )
94 | }
95 |
--------------------------------------------------------------------------------
/app/opengraph-image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn-ui/taxonomy/651f984e52edd65d40ccd55e299c1baeea3ff017/app/opengraph-image.jpg
--------------------------------------------------------------------------------
/app/robots.ts:
--------------------------------------------------------------------------------
1 | import { MetadataRoute } from "next"
2 |
3 | export default function robots(): MetadataRoute.Robots {
4 | return {
5 | rules: {
6 | userAgent: "*",
7 | allow: "/",
8 | },
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/assets/fonts/CalSans-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn-ui/taxonomy/651f984e52edd65d40ccd55e299c1baeea3ff017/assets/fonts/CalSans-SemiBold.ttf
--------------------------------------------------------------------------------
/assets/fonts/CalSans-SemiBold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn-ui/taxonomy/651f984e52edd65d40ccd55e299c1baeea3ff017/assets/fonts/CalSans-SemiBold.woff
--------------------------------------------------------------------------------
/assets/fonts/CalSans-SemiBold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn-ui/taxonomy/651f984e52edd65d40ccd55e299c1baeea3ff017/assets/fonts/CalSans-SemiBold.woff2
--------------------------------------------------------------------------------
/assets/fonts/Inter-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn-ui/taxonomy/651f984e52edd65d40ccd55e299c1baeea3ff017/assets/fonts/Inter-Bold.ttf
--------------------------------------------------------------------------------
/assets/fonts/Inter-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn-ui/taxonomy/651f984e52edd65d40ccd55e299c1baeea3ff017/assets/fonts/Inter-Regular.ttf
--------------------------------------------------------------------------------
/components/analytics.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Analytics as VercelAnalytics } from "@vercel/analytics/react"
4 |
5 | export function Analytics() {
6 | return
7 | }
8 |
--------------------------------------------------------------------------------
/components/billing-form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 |
5 | import { UserSubscriptionPlan } from "types"
6 | import { cn, formatDate } from "@/lib/utils"
7 | import { buttonVariants } from "@/components/ui/button"
8 | import {
9 | Card,
10 | CardContent,
11 | CardDescription,
12 | CardFooter,
13 | CardHeader,
14 | CardTitle,
15 | } from "@/components/ui/card"
16 | import { toast } from "@/components/ui/use-toast"
17 | import { Icons } from "@/components/icons"
18 |
19 | interface BillingFormProps extends React.HTMLAttributes {
20 | subscriptionPlan: UserSubscriptionPlan & {
21 | isCanceled: boolean
22 | }
23 | }
24 |
25 | export function BillingForm({
26 | subscriptionPlan,
27 | className,
28 | ...props
29 | }: BillingFormProps) {
30 | const [isLoading, setIsLoading] = React.useState(false)
31 |
32 | async function onSubmit(event) {
33 | event.preventDefault()
34 | setIsLoading(!isLoading)
35 |
36 | // Get a Stripe session URL.
37 | const response = await fetch("/api/users/stripe")
38 |
39 | if (!response?.ok) {
40 | return toast({
41 | title: "Something went wrong.",
42 | description: "Please refresh the page and try again.",
43 | variant: "destructive",
44 | })
45 | }
46 |
47 | // Redirect to the Stripe session.
48 | // This could be a checkout page for initial upgrade.
49 | // Or portal to manage existing subscription.
50 | const session = await response.json()
51 | if (session) {
52 | window.location.href = session.url
53 | }
54 | }
55 |
56 | return (
57 |
89 | )
90 | }
91 |
--------------------------------------------------------------------------------
/components/callout.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | interface CalloutProps {
4 | icon?: string
5 | children?: React.ReactNode
6 | type?: "default" | "warning" | "danger"
7 | }
8 |
9 | export function Callout({
10 | children,
11 | icon,
12 | type = "default",
13 | ...props
14 | }: CalloutProps) {
15 | return (
16 |
23 | {icon &&
{icon} }
24 |
{children}
25 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/components/card-skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"
2 | import { Skeleton } from "@/components/ui/skeleton"
3 |
4 | export function CardSkeleton() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/components/empty-placeholder.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 | import { Icons } from "@/components/icons"
5 |
6 | interface EmptyPlaceholderProps extends React.HTMLAttributes {}
7 |
8 | export function EmptyPlaceholder({
9 | className,
10 | children,
11 | ...props
12 | }: EmptyPlaceholderProps) {
13 | return (
14 |
21 |
22 | {children}
23 |
24 |
25 | )
26 | }
27 |
28 | interface EmptyPlaceholderIconProps
29 | extends Partial> {
30 | name: keyof typeof Icons
31 | }
32 |
33 | EmptyPlaceholder.Icon = function EmptyPlaceHolderIcon({
34 | name,
35 | className,
36 | ...props
37 | }: EmptyPlaceholderIconProps) {
38 | const Icon = Icons[name]
39 |
40 | if (!Icon) {
41 | return null
42 | }
43 |
44 | return (
45 |
46 |
47 |
48 | )
49 | }
50 |
51 | interface EmptyPlacholderTitleProps
52 | extends React.HTMLAttributes {}
53 |
54 | EmptyPlaceholder.Title = function EmptyPlaceholderTitle({
55 | className,
56 | ...props
57 | }: EmptyPlacholderTitleProps) {
58 | return (
59 |
60 | )
61 | }
62 |
63 | interface EmptyPlacholderDescriptionProps
64 | extends React.HTMLAttributes {}
65 |
66 | EmptyPlaceholder.Description = function EmptyPlaceholderDescription({
67 | className,
68 | ...props
69 | }: EmptyPlacholderDescriptionProps) {
70 | return (
71 |
78 | )
79 | }
80 |
--------------------------------------------------------------------------------
/components/header.tsx:
--------------------------------------------------------------------------------
1 | interface DashboardHeaderProps {
2 | heading: string
3 | text?: string
4 | children?: React.ReactNode
5 | }
6 |
7 | export function DashboardHeader({
8 | heading,
9 | text,
10 | children,
11 | }: DashboardHeaderProps) {
12 | return (
13 |
14 |
15 |
{heading}
16 | {text &&
{text}
}
17 |
18 | {children}
19 |
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/components/icons.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AlertTriangle,
3 | ArrowRight,
4 | Check,
5 | ChevronLeft,
6 | ChevronRight,
7 | Command,
8 | CreditCard,
9 | File,
10 | FileText,
11 | HelpCircle,
12 | Image,
13 | Laptop,
14 | Loader2,
15 | LucideProps,
16 | Moon,
17 | MoreVertical,
18 | Pizza,
19 | Plus,
20 | Settings,
21 | SunMedium,
22 | Trash,
23 | Twitter,
24 | User,
25 | X,
26 | type Icon as LucideIcon,
27 | } from "lucide-react"
28 |
29 | export type Icon = LucideIcon
30 |
31 | export const Icons = {
32 | logo: Command,
33 | close: X,
34 | spinner: Loader2,
35 | chevronLeft: ChevronLeft,
36 | chevronRight: ChevronRight,
37 | trash: Trash,
38 | post: FileText,
39 | page: File,
40 | media: Image,
41 | settings: Settings,
42 | billing: CreditCard,
43 | ellipsis: MoreVertical,
44 | add: Plus,
45 | warning: AlertTriangle,
46 | user: User,
47 | arrowRight: ArrowRight,
48 | help: HelpCircle,
49 | pizza: Pizza,
50 | sun: SunMedium,
51 | moon: Moon,
52 | laptop: Laptop,
53 | gitHub: ({ ...props }: LucideProps) => (
54 |
64 |
68 |
69 | ),
70 | twitter: Twitter,
71 | check: Check,
72 | }
73 |
--------------------------------------------------------------------------------
/components/main-nav.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import Link from "next/link"
5 | import { useSelectedLayoutSegment } from "next/navigation"
6 |
7 | import { MainNavItem } from "types"
8 | import { siteConfig } from "@/config/site"
9 | import { cn } from "@/lib/utils"
10 | import { Icons } from "@/components/icons"
11 | import { MobileNav } from "@/components/mobile-nav"
12 |
13 | interface MainNavProps {
14 | items?: MainNavItem[]
15 | children?: React.ReactNode
16 | }
17 |
18 | export function MainNav({ items, children }: MainNavProps) {
19 | const segment = useSelectedLayoutSegment()
20 | const [showMobileMenu, setShowMobileMenu] = React.useState(false)
21 |
22 | return (
23 |
24 |
25 |
26 |
27 | {siteConfig.name}
28 |
29 |
30 | {items?.length ? (
31 |
32 | {items?.map((item, index) => (
33 |
44 | {item.title}
45 |
46 | ))}
47 |
48 | ) : null}
49 | setShowMobileMenu(!showMobileMenu)}
52 | >
53 | {showMobileMenu ? : }
54 | Menu
55 |
56 | {showMobileMenu && items && (
57 | {children}
58 | )}
59 |
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/components/mdx-card.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | interface CardProps extends React.HTMLAttributes {
6 | href?: string
7 | disabled?: boolean
8 | }
9 |
10 | export function MdxCard({
11 | href,
12 | className,
13 | children,
14 | disabled,
15 | ...props
16 | }: CardProps) {
17 | return (
18 |
26 |
27 |
28 | {children}
29 |
30 |
31 | {href && (
32 |
33 |
View
34 |
35 | )}
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/components/mobile-nav.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import Link from "next/link"
3 |
4 | import { MainNavItem } from "types"
5 | import { siteConfig } from "@/config/site"
6 | import { cn } from "@/lib/utils"
7 | import { useLockBody } from "@/hooks/use-lock-body"
8 | import { Icons } from "@/components/icons"
9 |
10 | interface MobileNavProps {
11 | items: MainNavItem[]
12 | children?: React.ReactNode
13 | }
14 |
15 | export function MobileNav({ items, children }: MobileNavProps) {
16 | useLockBody()
17 |
18 | return (
19 |
24 |
25 |
26 |
27 | {siteConfig.name}
28 |
29 |
30 | {items.map((item, index) => (
31 |
39 | {item.title}
40 |
41 | ))}
42 |
43 | {children}
44 |
45 |
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/components/mode-toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { useTheme } from "next-themes"
5 |
6 | import { Button } from "@/components/ui/button"
7 | import {
8 | DropdownMenu,
9 | DropdownMenuContent,
10 | DropdownMenuItem,
11 | DropdownMenuTrigger,
12 | } from "@/components/ui/dropdown-menu"
13 | import { Icons } from "@/components/icons"
14 |
15 | export function ModeToggle() {
16 | const { setTheme } = useTheme()
17 |
18 | return (
19 |
20 |
21 |
22 |
23 |
24 | Toggle theme
25 |
26 |
27 |
28 | setTheme("light")}>
29 |
30 | Light
31 |
32 | setTheme("dark")}>
33 |
34 | Dark
35 |
36 | setTheme("system")}>
37 |
38 | System
39 |
40 |
41 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/components/nav.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Link from "next/link"
4 | import { usePathname } from "next/navigation"
5 |
6 | import { SidebarNavItem } from "types"
7 | import { cn } from "@/lib/utils"
8 | import { Icons } from "@/components/icons"
9 |
10 | interface DashboardNavProps {
11 | items: SidebarNavItem[]
12 | }
13 |
14 | export function DashboardNav({ items }: DashboardNavProps) {
15 | const path = usePathname()
16 |
17 | if (!items?.length) {
18 | return null
19 | }
20 |
21 | return (
22 |
23 | {items.map((item, index) => {
24 | const Icon = Icons[item.icon || "arrowRight"]
25 | return (
26 | item.href && (
27 |
28 |
35 |
36 | {item.title}
37 |
38 |
39 | )
40 | )
41 | })}
42 |
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/components/page-header.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | interface DocsPageHeaderProps extends React.HTMLAttributes {
4 | heading: string
5 | text?: string
6 | }
7 |
8 | export function DocsPageHeader({
9 | heading,
10 | text,
11 | className,
12 | ...props
13 | }: DocsPageHeaderProps) {
14 | return (
15 | <>
16 |
17 |
18 | {heading}
19 |
20 | {text &&
{text}
}
21 |
22 |
23 | >
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/components/pager.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 | import { Doc } from "contentlayer/generated"
3 |
4 | import { docsConfig } from "@/config/docs"
5 | import { cn } from "@/lib/utils"
6 | import { buttonVariants } from "@/components/ui/button"
7 | import { Icons } from "@/components/icons"
8 |
9 | interface DocsPagerProps {
10 | doc: Doc
11 | }
12 |
13 | export function DocsPager({ doc }: DocsPagerProps) {
14 | const pager = getPagerForDoc(doc)
15 |
16 | if (!pager) {
17 | return null
18 | }
19 |
20 | return (
21 |
22 | {pager?.prev && (
23 |
27 |
28 | {pager.prev.title}
29 |
30 | )}
31 | {pager?.next && (
32 |
36 | {pager.next.title}
37 |
38 |
39 | )}
40 |
41 | )
42 | }
43 |
44 | export function getPagerForDoc(doc: Doc) {
45 | const flattenedLinks = [null, ...flatten(docsConfig.sidebarNav), null]
46 | const activeIndex = flattenedLinks.findIndex(
47 | (link) => doc.slug === link?.href
48 | )
49 | const prev = activeIndex !== 0 ? flattenedLinks[activeIndex - 1] : null
50 | const next =
51 | activeIndex !== flattenedLinks.length - 1
52 | ? flattenedLinks[activeIndex + 1]
53 | : null
54 | return {
55 | prev,
56 | next,
57 | }
58 | }
59 |
60 | export function flatten(links: { items? }[]) {
61 | return links.reduce((flat, link) => {
62 | return flat.concat(link.items ? flatten(link.items) : link)
63 | }, [])
64 | }
65 |
--------------------------------------------------------------------------------
/components/post-create-button.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { useRouter } from "next/navigation"
5 |
6 | import { cn } from "@/lib/utils"
7 | import { ButtonProps, buttonVariants } from "@/components/ui/button"
8 | import { toast } from "@/components/ui/use-toast"
9 | import { Icons } from "@/components/icons"
10 |
11 | interface PostCreateButtonProps extends ButtonProps {}
12 |
13 | export function PostCreateButton({
14 | className,
15 | variant,
16 | ...props
17 | }: PostCreateButtonProps) {
18 | const router = useRouter()
19 | const [isLoading, setIsLoading] = React.useState(false)
20 |
21 | async function onClick() {
22 | setIsLoading(true)
23 |
24 | const response = await fetch("/api/posts", {
25 | method: "POST",
26 | headers: {
27 | "Content-Type": "application/json",
28 | },
29 | body: JSON.stringify({
30 | title: "Untitled Post",
31 | }),
32 | })
33 |
34 | setIsLoading(false)
35 |
36 | if (!response?.ok) {
37 | if (response.status === 402) {
38 | return toast({
39 | title: "Limit of 3 posts reached.",
40 | description: "Please upgrade to the PRO plan.",
41 | variant: "destructive",
42 | })
43 | }
44 |
45 | return toast({
46 | title: "Something went wrong.",
47 | description: "Your post was not created. Please try again.",
48 | variant: "destructive",
49 | })
50 | }
51 |
52 | const post = await response.json()
53 |
54 | // This forces a cache invalidation.
55 | router.refresh()
56 |
57 | router.push(`/editor/${post.id}`)
58 | }
59 |
60 | return (
61 |
73 | {isLoading ? (
74 |
75 | ) : (
76 |
77 | )}
78 | New post
79 |
80 | )
81 | }
82 |
--------------------------------------------------------------------------------
/components/post-item.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 | import { Post } from "@prisma/client"
3 |
4 | import { formatDate } from "@/lib/utils"
5 | import { Skeleton } from "@/components/ui/skeleton"
6 | import { PostOperations } from "@/components/post-operations"
7 |
8 | interface PostItemProps {
9 | post: Pick
10 | }
11 |
12 | export function PostItem({ post }: PostItemProps) {
13 | return (
14 |
15 |
16 |
20 | {post.title}
21 |
22 |
23 |
24 | {formatDate(post.createdAt?.toDateString())}
25 |
26 |
27 |
28 |
29 |
30 | )
31 | }
32 |
33 | PostItem.Skeleton = function PostItemSkeleton() {
34 | return (
35 |
41 | )
42 | }
43 |
--------------------------------------------------------------------------------
/components/post-operations.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import Link from "next/link"
5 | import { useRouter } from "next/navigation"
6 | import { Post } from "@prisma/client"
7 |
8 | import {
9 | AlertDialog,
10 | AlertDialogAction,
11 | AlertDialogCancel,
12 | AlertDialogContent,
13 | AlertDialogDescription,
14 | AlertDialogFooter,
15 | AlertDialogHeader,
16 | AlertDialogTitle,
17 | } from "@/components/ui/alert-dialog"
18 | import {
19 | DropdownMenu,
20 | DropdownMenuContent,
21 | DropdownMenuItem,
22 | DropdownMenuSeparator,
23 | DropdownMenuTrigger,
24 | } from "@/components/ui/dropdown-menu"
25 | import { toast } from "@/components/ui/use-toast"
26 | import { Icons } from "@/components/icons"
27 |
28 | async function deletePost(postId: string) {
29 | const response = await fetch(`/api/posts/${postId}`, {
30 | method: "DELETE",
31 | })
32 |
33 | if (!response?.ok) {
34 | toast({
35 | title: "Something went wrong.",
36 | description: "Your post was not deleted. Please try again.",
37 | variant: "destructive",
38 | })
39 | }
40 |
41 | return true
42 | }
43 |
44 | interface PostOperationsProps {
45 | post: Pick
46 | }
47 |
48 | export function PostOperations({ post }: PostOperationsProps) {
49 | const router = useRouter()
50 | const [showDeleteAlert, setShowDeleteAlert] = React.useState(false)
51 | const [isDeleteLoading, setIsDeleteLoading] = React.useState(false)
52 |
53 | return (
54 | <>
55 |
56 |
57 |
58 | Open
59 |
60 |
61 |
62 |
63 | Edit
64 |
65 |
66 |
67 | setShowDeleteAlert(true)}
70 | >
71 | Delete
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | Are you sure you want to delete this post?
80 |
81 |
82 | This action cannot be undone.
83 |
84 |
85 |
86 | Cancel
87 | {
89 | event.preventDefault()
90 | setIsDeleteLoading(true)
91 |
92 | const deleted = await deletePost(post.id)
93 |
94 | if (deleted) {
95 | setIsDeleteLoading(false)
96 | setShowDeleteAlert(false)
97 | router.refresh()
98 | }
99 | }}
100 | className="bg-red-600 focus:ring-red-600"
101 | >
102 | {isDeleteLoading ? (
103 |
104 | ) : (
105 |
106 | )}
107 | Delete
108 |
109 |
110 |
111 |
112 | >
113 | )
114 | }
115 |
--------------------------------------------------------------------------------
/components/search.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 |
5 | import { cn } from "@/lib/utils"
6 | import { Input } from "@/components/ui/input"
7 | import { toast } from "@/components/ui/use-toast"
8 |
9 | interface DocsSearchProps extends React.HTMLAttributes {}
10 |
11 | export function DocsSearch({ className, ...props }: DocsSearchProps) {
12 | function onSubmit(event: React.SyntheticEvent) {
13 | event.preventDefault()
14 |
15 | return toast({
16 | title: "Not implemented",
17 | description: "We're still working on the search.",
18 | })
19 | }
20 |
21 | return (
22 |
36 | )
37 | }
38 |
--------------------------------------------------------------------------------
/components/shell.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | interface DashboardShellProps extends React.HTMLAttributes {}
6 |
7 | export function DashboardShell({
8 | children,
9 | className,
10 | ...props
11 | }: DashboardShellProps) {
12 | return (
13 |
14 | {children}
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/components/sidebar-nav.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Link from "next/link"
4 | import { usePathname } from "next/navigation"
5 |
6 | import { SidebarNavItem } from "types"
7 | import { cn } from "@/lib/utils"
8 |
9 | export interface DocsSidebarNavProps {
10 | items: SidebarNavItem[]
11 | }
12 |
13 | export function DocsSidebarNav({ items }: DocsSidebarNavProps) {
14 | const pathname = usePathname()
15 |
16 | return items.length ? (
17 |
18 | {items.map((item, index) => (
19 |
20 |
21 | {item.title}
22 |
23 | {item.items ? (
24 |
25 | ) : null}
26 |
27 | ))}
28 |
29 | ) : null
30 | }
31 |
32 | interface DocsSidebarNavItemsProps {
33 | items: SidebarNavItem[]
34 | pathname: string | null
35 | }
36 |
37 | export function DocsSidebarNavItems({
38 | items,
39 | pathname,
40 | }: DocsSidebarNavItemsProps) {
41 | return items?.length ? (
42 |
43 | {items.map((item, index) =>
44 | !item.disabled && item.href ? (
45 |
57 | {item.title}
58 |
59 | ) : (
60 |
61 | {item.title}
62 |
63 | )
64 | )}
65 |
66 | ) : null
67 | }
68 |
--------------------------------------------------------------------------------
/components/site-footer.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { siteConfig } from "@/config/site"
4 | import { cn } from "@/lib/utils"
5 | import { Icons } from "@/components/icons"
6 | import { ModeToggle } from "@/components/mode-toggle"
7 |
8 | export function SiteFooter({ className }: React.HTMLAttributes) {
9 | return (
10 |
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/components/tailwind-indicator.tsx:
--------------------------------------------------------------------------------
1 | export function TailwindIndicator() {
2 | if (process.env.NODE_ENV === "production") return null
3 |
4 | return (
5 |
6 |
xs
7 |
8 | sm
9 |
10 |
md
11 |
lg
12 |
xl
13 |
2xl
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/components/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { ThemeProvider as NextThemesProvider } from "next-themes"
5 | import { ThemeProviderProps } from "next-themes/dist/types"
6 |
7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8 | return {children}
9 | }
10 |
--------------------------------------------------------------------------------
/components/toc.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 |
5 | import { TableOfContents } from "@/lib/toc"
6 | import { cn } from "@/lib/utils"
7 | import { useMounted } from "@/hooks/use-mounted"
8 |
9 | interface TocProps {
10 | toc: TableOfContents
11 | }
12 |
13 | export function DashboardTableOfContents({ toc }: TocProps) {
14 | const itemIds = React.useMemo(
15 | () =>
16 | toc.items
17 | ? toc.items
18 | .flatMap((item) => [item.url, item?.items?.map((item) => item.url)])
19 | .flat()
20 | .filter(Boolean)
21 | .map((id) => id?.split("#")[1])
22 | : [],
23 | [toc]
24 | )
25 | const activeHeading = useActiveItem(itemIds)
26 | const mounted = useMounted()
27 |
28 | if (!toc?.items) {
29 | return null
30 | }
31 |
32 | return mounted ? (
33 |
34 |
On This Page
35 |
36 |
37 | ) : null
38 | }
39 |
40 | function useActiveItem(itemIds: (string | undefined)[]) {
41 | const [activeId, setActiveId] = React.useState("")
42 |
43 | React.useEffect(() => {
44 | const observer = new IntersectionObserver(
45 | (entries) => {
46 | entries.forEach((entry) => {
47 | if (entry.isIntersecting) {
48 | setActiveId(entry.target.id)
49 | }
50 | })
51 | },
52 | { rootMargin: `0% 0% -80% 0%` }
53 | )
54 |
55 | itemIds?.forEach((id) => {
56 | if (!id) {
57 | return
58 | }
59 |
60 | const element = document.getElementById(id)
61 | if (element) {
62 | observer.observe(element)
63 | }
64 | })
65 |
66 | return () => {
67 | itemIds?.forEach((id) => {
68 | if (!id) {
69 | return
70 | }
71 |
72 | const element = document.getElementById(id)
73 | if (element) {
74 | observer.unobserve(element)
75 | }
76 | })
77 | }
78 | }, [itemIds])
79 |
80 | return activeId
81 | }
82 |
83 | interface TreeProps {
84 | tree: TableOfContents
85 | level?: number
86 | activeItem?: string | null
87 | }
88 |
89 | function Tree({ tree, level = 1, activeItem }: TreeProps) {
90 | return tree?.items?.length && level < 3 ? (
91 |
92 | {tree.items.map((item, index) => {
93 | return (
94 |
95 |
104 | {item.title}
105 |
106 | {item.items?.length ? (
107 |
108 | ) : null}
109 |
110 | )
111 | })}
112 |
113 | ) : null
114 | }
115 |
--------------------------------------------------------------------------------
/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
5 | import { ChevronDown } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Accordion = AccordionPrimitive.Root
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
20 | ))
21 | AccordionItem.displayName = "AccordionItem"
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => (
27 |
28 | svg]:rotate-180",
32 | className
33 | )}
34 | {...props}
35 | >
36 | {children}
37 |
38 |
39 |
40 | ))
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
55 | {children}
56 |
57 | ))
58 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
59 |
60 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
61 |
--------------------------------------------------------------------------------
/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { VariantProps, cva } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const alertVariants = cva(
7 | "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",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "text-destructive border-destructive/50 dark:border-destructive [&>svg]:text-destructive text-destructive",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | )
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ))
33 | Alert.displayName = "Alert"
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ))
45 | AlertTitle.displayName = "AlertTitle"
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | AlertDescription.displayName = "AlertDescription"
58 |
59 | export { Alert, AlertTitle, AlertDescription }
60 |
--------------------------------------------------------------------------------
/components/ui/aspect-ratio.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
4 |
5 | const AspectRatio = AspectRatioPrimitive.Root
6 |
7 | export { AspectRatio }
8 |
--------------------------------------------------------------------------------
/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { VariantProps, cva } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "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",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
13 | secondary:
14 | "bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
15 | destructive:
16 | "bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { VariantProps, cva } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const buttonVariants = cva(
7 | "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",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
12 | destructive:
13 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
14 | outline:
15 | "border border-input hover:bg-accent hover:text-accent-foreground",
16 | secondary:
17 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
18 | ghost: "hover:bg-accent hover:text-accent-foreground",
19 | link: "underline-offset-4 hover:underline text-primary",
20 | },
21 | size: {
22 | default: "h-10 py-2 px-4",
23 | sm: "h-9 px-3 rounded-md",
24 | lg: "h-11 px-8 rounded-md",
25 | },
26 | },
27 | defaultVariants: {
28 | variant: "default",
29 | size: "default",
30 | },
31 | }
32 | )
33 |
34 | export interface ButtonProps
35 | extends React.ButtonHTMLAttributes,
36 | VariantProps {}
37 |
38 | const Button = React.forwardRef(
39 | ({ className, variant, size, ...props }, ref) => {
40 | return (
41 |
46 | )
47 | }
48 | )
49 | Button.displayName = "Button"
50 |
51 | export { Button, buttonVariants }
52 |
--------------------------------------------------------------------------------
/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { ChevronLeft, ChevronRight } from "lucide-react"
5 | import { DayPicker } from "react-day-picker"
6 |
7 | import { cn } from "@/lib/utils"
8 | import { buttonVariants } from "@/components/ui/button"
9 |
10 | export type CalendarProps = React.ComponentProps
11 |
12 | function Calendar({
13 | className,
14 | classNames,
15 | showOutsideDays = true,
16 | ...props
17 | }: CalendarProps) {
18 | return (
19 | ,
56 | IconRight: ({ ...props }) => ,
57 | }}
58 | {...props}
59 | />
60 | )
61 | }
62 | Calendar.displayName = "Calendar"
63 |
64 | export { Calendar }
65 |
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5 | import { Check } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ))
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4 |
5 | const Collapsible = CollapsiblePrimitive.Root
6 |
7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
8 |
9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
10 |
11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
12 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = ({
14 | className,
15 | children,
16 | ...props
17 | }: DialogPrimitive.DialogPortalProps) => (
18 |
19 |
20 | {children}
21 |
22 |
23 | )
24 | DialogPortal.displayName = DialogPrimitive.Portal.displayName
25 |
26 | const DialogOverlay = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, ...props }, ref) => (
30 |
38 | ))
39 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
40 |
41 | const DialogContent = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef
44 | >(({ className, children, ...props }, ref) => (
45 |
46 |
47 |
55 | {children}
56 |
57 |
58 | Close
59 |
60 |
61 |
62 | ))
63 | DialogContent.displayName = DialogPrimitive.Content.displayName
64 |
65 | const DialogHeader = ({
66 | className,
67 | ...props
68 | }: React.HTMLAttributes) => (
69 |
76 | )
77 | DialogHeader.displayName = "DialogHeader"
78 |
79 | const DialogFooter = ({
80 | className,
81 | ...props
82 | }: React.HTMLAttributes) => (
83 |
90 | )
91 | DialogFooter.displayName = "DialogFooter"
92 |
93 | const DialogTitle = React.forwardRef<
94 | React.ElementRef,
95 | React.ComponentPropsWithoutRef
96 | >(({ className, ...props }, ref) => (
97 |
105 | ))
106 | DialogTitle.displayName = DialogPrimitive.Title.displayName
107 |
108 | const DialogDescription = React.forwardRef<
109 | React.ElementRef,
110 | React.ComponentPropsWithoutRef
111 | >(({ className, ...props }, ref) => (
112 |
117 | ))
118 | DialogDescription.displayName = DialogPrimitive.Description.displayName
119 |
120 | export {
121 | Dialog,
122 | DialogTrigger,
123 | DialogContent,
124 | DialogHeader,
125 | DialogFooter,
126 | DialogTitle,
127 | DialogDescription,
128 | }
129 |
--------------------------------------------------------------------------------
/components/ui/hover-card.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const HoverCard = HoverCardPrimitive.Root
9 |
10 | const HoverCardTrigger = HoverCardPrimitive.Trigger
11 |
12 | const HoverCardContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
26 | ))
27 | HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
28 |
29 | export { HoverCard, HoverCardTrigger, HoverCardContent }
30 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { VariantProps, cva } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent }
32 |
--------------------------------------------------------------------------------
/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ProgressPrimitive from "@radix-ui/react-progress"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Progress = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, value, ...props }, ref) => (
12 |
20 |
24 |
25 | ))
26 | Progress.displayName = ProgressPrimitive.Root.displayName
27 |
28 | export { Progress }
29 |
--------------------------------------------------------------------------------
/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
5 | import { Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const RadioGroup = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => {
13 | return (
14 |
19 | )
20 | })
21 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
22 |
23 | const RadioGroupItem = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => {
27 | return (
28 |
36 |
37 |
38 |
39 |
40 | )
41 | })
42 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
43 |
44 | export { RadioGroup, RadioGroupItem }
45 |
--------------------------------------------------------------------------------
/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ))
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
47 |
48 | export { ScrollArea, ScrollBar }
49 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 |
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectContent = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, children, position = "popper", ...props }, ref) => (
39 |
40 |
50 |
57 | {children}
58 |
59 |
60 |
61 | ))
62 | SelectContent.displayName = SelectPrimitive.Content.displayName
63 |
64 | const SelectLabel = React.forwardRef<
65 | React.ElementRef,
66 | React.ComponentPropsWithoutRef
67 | >(({ className, ...props }, ref) => (
68 |
73 | ))
74 | SelectLabel.displayName = SelectPrimitive.Label.displayName
75 |
76 | const SelectItem = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, children, ...props }, ref) => (
80 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | {children}
95 |
96 | ))
97 | SelectItem.displayName = SelectPrimitive.Item.displayName
98 |
99 | const SelectSeparator = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
110 |
111 | export {
112 | Select,
113 | SelectGroup,
114 | SelectValue,
115 | SelectTrigger,
116 | SelectContent,
117 | SelectLabel,
118 | SelectItem,
119 | SelectSeparator,
120 | }
121 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = "horizontal", decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 |
31 | export { Separator }
32 |
--------------------------------------------------------------------------------
/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SliderPrimitive from "@radix-ui/react-slider"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Slider = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
21 |
22 |
23 |
24 |
25 | ))
26 | Slider.displayName = SliderPrimitive.Root.displayName
27 |
28 | export { Slider }
29 |
--------------------------------------------------------------------------------
/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | TabsList.displayName = TabsPrimitive.List.displayName
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | TabsContent.displayName = TabsPrimitive.Content.displayName
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent }
56 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "@/components/ui/toast"
11 | import { useToast } from "@/components/ui/use-toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title} }
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TogglePrimitive from "@radix-ui/react-toggle"
5 | import { VariantProps, cva } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const toggleVariants = cva(
10 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors data-[state=on]:bg-accent data-[state=on]:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 ring-offset-background hover:bg-muted hover:text-muted-foreground",
11 | {
12 | variants: {
13 | variant: {
14 | default: "bg-transparent",
15 | outline:
16 | "bg-transparent border border-input hover:bg-accent hover:text-accent-foreground",
17 | },
18 | size: {
19 | default: "h-10 px-3",
20 | sm: "h-9 px-2.5",
21 | lg: "h-11 px-5",
22 | },
23 | },
24 | defaultVariants: {
25 | variant: "default",
26 | size: "default",
27 | },
28 | }
29 | )
30 |
31 | const Toggle = React.forwardRef<
32 | React.ElementRef,
33 | React.ComponentPropsWithoutRef &
34 | VariantProps
35 | >(({ className, variant, size, ...props }, ref) => (
36 |
41 | ))
42 |
43 | Toggle.displayName = TogglePrimitive.Root.displayName
44 |
45 | export { Toggle, toggleVariants }
46 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/components/user-account-nav.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Link from "next/link"
4 | import { User } from "next-auth"
5 | import { signOut } from "next-auth/react"
6 |
7 | import {
8 | DropdownMenu,
9 | DropdownMenuContent,
10 | DropdownMenuItem,
11 | DropdownMenuSeparator,
12 | DropdownMenuTrigger,
13 | } from "@/components/ui/dropdown-menu"
14 | import { UserAvatar } from "@/components/user-avatar"
15 |
16 | interface UserAccountNavProps extends React.HTMLAttributes {
17 | user: Pick
18 | }
19 |
20 | export function UserAccountNav({ user }: UserAccountNavProps) {
21 | return (
22 |
23 |
24 |
28 |
29 |
30 |
31 |
32 | {user.name &&
{user.name}
}
33 | {user.email && (
34 |
35 | {user.email}
36 |
37 | )}
38 |
39 |
40 |
41 |
42 | Dashboard
43 |
44 |
45 | Billing
46 |
47 |
48 | Settings
49 |
50 |
51 | {
54 | event.preventDefault()
55 | signOut({
56 | callbackUrl: `${window.location.origin}/login`,
57 | })
58 | }}
59 | >
60 | Sign out
61 |
62 |
63 |
64 | )
65 | }
66 |
--------------------------------------------------------------------------------
/components/user-auth-form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { useSearchParams } from "next/navigation"
5 | import { zodResolver } from "@hookform/resolvers/zod"
6 | import { signIn } from "next-auth/react"
7 | import { useForm } from "react-hook-form"
8 | import * as z from "zod"
9 |
10 | import { cn } from "@/lib/utils"
11 | import { userAuthSchema } from "@/lib/validations/auth"
12 | import { buttonVariants } from "@/components/ui/button"
13 | import { Input } from "@/components/ui/input"
14 | import { Label } from "@/components/ui/label"
15 | import { toast } from "@/components/ui/use-toast"
16 | import { Icons } from "@/components/icons"
17 |
18 | interface UserAuthFormProps extends React.HTMLAttributes {}
19 |
20 | type FormData = z.infer
21 |
22 | export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
23 | const {
24 | register,
25 | handleSubmit,
26 | formState: { errors },
27 | } = useForm({
28 | resolver: zodResolver(userAuthSchema),
29 | })
30 | const [isLoading, setIsLoading] = React.useState(false)
31 | const [isGitHubLoading, setIsGitHubLoading] = React.useState(false)
32 | const searchParams = useSearchParams()
33 |
34 | async function onSubmit(data: FormData) {
35 | setIsLoading(true)
36 |
37 | const signInResult = await signIn("email", {
38 | email: data.email.toLowerCase(),
39 | redirect: false,
40 | callbackUrl: searchParams?.get("from") || "/dashboard",
41 | })
42 |
43 | setIsLoading(false)
44 |
45 | if (!signInResult?.ok) {
46 | return toast({
47 | title: "Something went wrong.",
48 | description: "Your sign in request failed. Please try again.",
49 | variant: "destructive",
50 | })
51 | }
52 |
53 | return toast({
54 | title: "Check your email",
55 | description: "We sent you a login link. Be sure to check your spam too.",
56 | })
57 | }
58 |
59 | return (
60 |
61 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | Or continue with
98 |
99 |
100 |
101 |
{
105 | setIsGitHubLoading(true)
106 | signIn("github")
107 | }}
108 | disabled={isLoading || isGitHubLoading}
109 | >
110 | {isGitHubLoading ? (
111 |
112 | ) : (
113 |
114 | )}{" "}
115 | Github
116 |
117 |
118 | )
119 | }
120 |
--------------------------------------------------------------------------------
/components/user-avatar.tsx:
--------------------------------------------------------------------------------
1 | import { User } from "@prisma/client"
2 | import { AvatarProps } from "@radix-ui/react-avatar"
3 |
4 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
5 | import { Icons } from "@/components/icons"
6 |
7 | interface UserAvatarProps extends AvatarProps {
8 | user: Pick
9 | }
10 |
11 | export function UserAvatar({ user, ...props }: UserAvatarProps) {
12 | return (
13 |
14 | {user.image ? (
15 |
16 | ) : (
17 |
18 | {user.name}
19 |
20 |
21 | )}
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/components/user-name-form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { useRouter } from "next/navigation"
5 | import { zodResolver } from "@hookform/resolvers/zod"
6 | import { User } from "@prisma/client"
7 | import { useForm } from "react-hook-form"
8 | import * as z from "zod"
9 |
10 | import { cn } from "@/lib/utils"
11 | import { userNameSchema } from "@/lib/validations/user"
12 | import { buttonVariants } from "@/components/ui/button"
13 | import {
14 | Card,
15 | CardContent,
16 | CardDescription,
17 | CardFooter,
18 | CardHeader,
19 | CardTitle,
20 | } from "@/components/ui/card"
21 | import { Input } from "@/components/ui/input"
22 | import { Label } from "@/components/ui/label"
23 | import { toast } from "@/components/ui/use-toast"
24 | import { Icons } from "@/components/icons"
25 |
26 | interface UserNameFormProps extends React.HTMLAttributes {
27 | user: Pick
28 | }
29 |
30 | type FormData = z.infer
31 |
32 | export function UserNameForm({ user, className, ...props }: UserNameFormProps) {
33 | const router = useRouter()
34 | const {
35 | handleSubmit,
36 | register,
37 | formState: { errors },
38 | } = useForm({
39 | resolver: zodResolver(userNameSchema),
40 | defaultValues: {
41 | name: user?.name || "",
42 | },
43 | })
44 | const [isSaving, setIsSaving] = React.useState(false)
45 |
46 | async function onSubmit(data: FormData) {
47 | setIsSaving(true)
48 |
49 | const response = await fetch(`/api/users/${user.id}`, {
50 | method: "PATCH",
51 | headers: {
52 | "Content-Type": "application/json",
53 | },
54 | body: JSON.stringify({
55 | name: data.name,
56 | }),
57 | })
58 |
59 | setIsSaving(false)
60 |
61 | if (!response?.ok) {
62 | return toast({
63 | title: "Something went wrong.",
64 | description: "Your name was not updated. Please try again.",
65 | variant: "destructive",
66 | })
67 | }
68 |
69 | toast({
70 | description: "Your name has been updated.",
71 | })
72 |
73 | router.refresh()
74 | }
75 |
76 | return (
77 |
120 | )
121 | }
122 |
--------------------------------------------------------------------------------
/config/dashboard.ts:
--------------------------------------------------------------------------------
1 | import { DashboardConfig } from "types"
2 |
3 | export const dashboardConfig: DashboardConfig = {
4 | mainNav: [
5 | {
6 | title: "Documentation",
7 | href: "/docs",
8 | },
9 | {
10 | title: "Support",
11 | href: "/support",
12 | disabled: true,
13 | },
14 | ],
15 | sidebarNav: [
16 | {
17 | title: "Posts",
18 | href: "/dashboard",
19 | icon: "post",
20 | },
21 | {
22 | title: "Billing",
23 | href: "/dashboard/billing",
24 | icon: "billing",
25 | },
26 | {
27 | title: "Settings",
28 | href: "/dashboard/settings",
29 | icon: "settings",
30 | },
31 | ],
32 | }
33 |
--------------------------------------------------------------------------------
/config/docs.ts:
--------------------------------------------------------------------------------
1 | import { DocsConfig } from "types"
2 |
3 | export const docsConfig: DocsConfig = {
4 | mainNav: [
5 | {
6 | title: "Documentation",
7 | href: "/docs",
8 | },
9 | {
10 | title: "Guides",
11 | href: "/guides",
12 | },
13 | ],
14 | sidebarNav: [
15 | {
16 | title: "Getting Started",
17 | items: [
18 | {
19 | title: "Introduction",
20 | href: "/docs",
21 | },
22 | ],
23 | },
24 | {
25 | title: "Documentation",
26 | items: [
27 | {
28 | title: "Introduction",
29 | href: "/docs/documentation",
30 | },
31 | {
32 | title: "Contentlayer",
33 | href: "/docs/in-progress",
34 | disabled: true,
35 | },
36 | {
37 | title: "Components",
38 | href: "/docs/documentation/components",
39 | },
40 | {
41 | title: "Code Blocks",
42 | href: "/docs/documentation/code-blocks",
43 | },
44 | {
45 | title: "Style Guide",
46 | href: "/docs/documentation/style-guide",
47 | },
48 | {
49 | title: "Search",
50 | href: "/docs/in-progress",
51 | disabled: true,
52 | },
53 | ],
54 | },
55 | {
56 | title: "Blog",
57 | items: [
58 | {
59 | title: "Introduction",
60 | href: "/docs/in-progress",
61 | disabled: true,
62 | },
63 | {
64 | title: "Build your own",
65 | href: "/docs/in-progress",
66 | disabled: true,
67 | },
68 | {
69 | title: "Writing Posts",
70 | href: "/docs/in-progress",
71 | disabled: true,
72 | },
73 | ],
74 | },
75 | {
76 | title: "Dashboard",
77 | items: [
78 | {
79 | title: "Introduction",
80 | href: "/docs/in-progress",
81 | disabled: true,
82 | },
83 | {
84 | title: "Layouts",
85 | href: "/docs/in-progress",
86 | disabled: true,
87 | },
88 | {
89 | title: "Server Components",
90 | href: "/docs/in-progress",
91 | disabled: true,
92 | },
93 | {
94 | title: "Authentication",
95 | href: "/docs/in-progress",
96 | disabled: true,
97 | },
98 | {
99 | title: "Database with Prisma",
100 | href: "/docs/in-progress",
101 | disabled: true,
102 | },
103 | {
104 | title: "API Routes",
105 | href: "/docs/in-progress",
106 | disabled: true,
107 | },
108 | ],
109 | },
110 | {
111 | title: "Marketing Site",
112 | items: [
113 | {
114 | title: "Introduction",
115 | href: "/docs/in-progress",
116 | disabled: true,
117 | },
118 | {
119 | title: "File Structure",
120 | href: "/docs/in-progress",
121 | disabled: true,
122 | },
123 | {
124 | title: "Tailwind CSS",
125 | href: "/docs/in-progress",
126 | disabled: true,
127 | },
128 | {
129 | title: "Typography",
130 | href: "/docs/in-progress",
131 | disabled: true,
132 | },
133 | ],
134 | },
135 | ],
136 | }
137 |
--------------------------------------------------------------------------------
/config/marketing.ts:
--------------------------------------------------------------------------------
1 | import { MarketingConfig } from "types"
2 |
3 | export const marketingConfig: MarketingConfig = {
4 | mainNav: [
5 | {
6 | title: "Features",
7 | href: "/#features",
8 | },
9 | {
10 | title: "Pricing",
11 | href: "/pricing",
12 | },
13 | {
14 | title: "Blog",
15 | href: "/blog",
16 | },
17 | {
18 | title: "Documentation",
19 | href: "/docs",
20 | },
21 | ],
22 | }
23 |
--------------------------------------------------------------------------------
/config/site.ts:
--------------------------------------------------------------------------------
1 | import { SiteConfig } from "types"
2 |
3 | export const siteConfig: SiteConfig = {
4 | name: "Taxonomy",
5 | description:
6 | "An open source application built using the new router, server components and everything new in Next.js 13.",
7 | url: "https://tx.shadcn.com",
8 | ogImage: "https://tx.shadcn.com/og.jpg",
9 | links: {
10 | twitter: "https://twitter.com/shadcn",
11 | github: "https://github.com/shadcn/taxonomy",
12 | },
13 | }
14 |
--------------------------------------------------------------------------------
/config/subscriptions.ts:
--------------------------------------------------------------------------------
1 | import { SubscriptionPlan } from "types"
2 | import { env } from "@/env.mjs"
3 |
4 | export const freePlan: SubscriptionPlan = {
5 | name: "Free",
6 | description:
7 | "The free plan is limited to 3 posts. Upgrade to the PRO plan for unlimited posts.",
8 | stripePriceId: "",
9 | }
10 |
11 | export const proPlan: SubscriptionPlan = {
12 | name: "PRO",
13 | description: "The PRO plan has unlimited posts.",
14 | stripePriceId: env.STRIPE_PRO_MONTHLY_PLAN_ID || "",
15 | }
16 |
--------------------------------------------------------------------------------
/content/authors/shadcn.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: shadcn
3 | avatar: /images/avatars/shadcn.png
4 | twitter: shadcn
5 | ---
6 |
--------------------------------------------------------------------------------
/content/docs/documentation/code-blocks.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Code Blocks
3 | description: Advanced code blocks with highlighting, file names and more.
4 | ---
5 |
6 | The code blocks on the documentation site and the blog are powered by [rehype-pretty-code](https://github.com/atomiks/rehype-pretty-code). The syntax highlighting is done using [shiki](https://github.com/shikijs/shiki).
7 |
8 | It has the following features:
9 |
10 | 1. Beautiful code blocks with syntax highlighting.
11 | 2. Support for VS Code themes.
12 | 3. Works with hundreds of languages.
13 | 4. Line and word highlighting.
14 | 5. Support for line numbers.
15 | 6. Show code block titles using meta strings.
16 |
17 |
18 |
19 | Thanks to Shiki, highlighting is done at build time. No JavaScript is sent to the client for runtime highlighting.
20 |
21 |
22 |
23 | ## Example
24 |
25 | ```ts showLineNumbers title="next.config.js" {3} /appDir: true/
26 | import { withContentlayer } from "next-contentlayer"
27 |
28 | /** @type {import('next').NextConfig} */
29 | const nextConfig = {
30 | reactStrictMode: true,
31 | images: {
32 | domains: ["avatars.githubusercontent.com"],
33 | },
34 | experimental: {
35 | appDir: true,
36 | serverComponentsExternalPackages: ["@prisma/client"],
37 | },
38 | }
39 |
40 | export default withContentlayer(nextConfig)
41 | ```
42 |
43 | ## Title
44 |
45 | ````mdx
46 | ```ts title="path/to/file.ts"
47 | // Code here
48 | ```
49 | ````
50 |
51 | ## Line Highlight
52 |
53 | ````mdx
54 | ```ts {1,3-6}
55 | // Highlight line 1 and line 3 to 6
56 | ```
57 | ````
58 |
59 | ## Word Highlight
60 |
61 | ````mdx
62 | ```ts /shadcn/
63 | // Highlight the word shadcn.
64 | ```
65 | ````
66 |
67 | ## Line Numbers
68 |
69 | ````mdx
70 | ```ts showLineNumbers
71 | // This will show line numbers.
72 | ```
73 | ````
74 |
--------------------------------------------------------------------------------
/content/docs/documentation/components.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Components
3 | description: Use React components in Markdown using MDX.
4 | ---
5 |
6 | The following components are available out of the box for use in Markdown.
7 |
8 | If you'd like to build and add your own custom components, see the [Custom Components](#custom-components) section below.
9 |
10 | ## Built-in Components
11 |
12 | ### 1. Callout
13 |
14 | ```mdx
15 |
16 |
17 | This is a default callout. You can embed **Markdown** inside a `callout`.
18 |
19 |
20 | ```
21 |
22 |
23 |
24 | This is a default callout. You can embed **Markdown** inside a `callout`.
25 |
26 |
27 |
28 |
29 |
30 | This is a warning callout. It uses the props `type="warning"`.
31 |
32 |
33 |
34 |
35 |
36 | This is a danger callout. It uses the props `type="danger"`.
37 |
38 |
39 |
40 | ### 2. Card
41 |
42 | ```mdx
43 |
44 |
45 | #### Heading
46 |
47 | You can use **markdown** inside cards.
48 |
49 |
50 | ```
51 |
52 |
53 |
54 | #### Heading
55 |
56 | You can use **markdown** inside cards.
57 |
58 |
59 |
60 | You can also use HTML to embed cards in a grid.
61 |
62 | ```mdx
63 |
64 |
65 | #### Card One
66 | You can use **markdown** inside cards.
67 |
68 |
69 |
70 | #### Card Two
71 | You can also use `inline code` and code blocks.
72 |
73 |
74 | ```
75 |
76 |
77 |
78 | #### Card One
79 | You can use **markdown** inside cards.
80 |
81 |
82 |
83 | #### Card Two
84 | You can also use `inline code` and code blocks.
85 |
86 |
87 |
88 | ---
89 |
90 | ## Custom Components
91 |
92 | You can add your own custom components using the `components` props from `useMDXComponent`.
93 |
94 | ```ts title="components/mdx.tsx" {2,6}
95 | import { Callout } from "@/components/callout"
96 | import { CustomComponent } from "@/components/custom"
97 |
98 | const components = {
99 | Callout,
100 | CustomComponent,
101 | }
102 |
103 | export function Mdx({ code }) {
104 | const Component = useMDXComponent(code)
105 |
106 | return (
107 |
108 |
109 |
110 | )
111 | }
112 | ```
113 |
114 | Once you've added your custom component, you can use it in MDX as follows:
115 |
116 | ```js
117 |
118 | ```
119 |
120 | ---
121 |
122 | ## HTML Elements
123 |
124 | You can overwrite HTML elements using the same technique above.
125 |
126 | ```ts {4}
127 | const components = {
128 | Callout,
129 | CustomComponent,
130 | hr: ({ ...props }) => ,
131 | }
132 | ```
133 |
134 | This will overwrite the ` ` tag or `---` in Mardown with the HTML output above.
135 |
136 | ---
137 |
138 | ## Styling
139 |
140 | Tailwind CSS classes can be used inside MDX for styling.
141 |
142 | ```mdx
143 | This text will be red.
144 | ```
145 |
146 | Make sure you have configured the path to your content in your `tailwind.config.js` file:
147 |
148 | ```js title="tailwind.config.js" {6}
149 | /** @type {import('tailwindcss').Config} */
150 | module.exports = {
151 | content: [
152 | "./app/**/*.{ts,tsx}",
153 | "./components/**/*.{ts,tsx}",
154 | "./content/**/*.{md,mdx}",
155 | ],
156 | }
157 | ```
158 |
--------------------------------------------------------------------------------
/content/docs/documentation/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Documentation
3 | description: Build your documentation site using Contentlayer and MDX.
4 | ---
5 |
6 | Taxonomy includes a documentation site built using [Contentlayer](https://contentlayer.dev) and [MDX](http://mdxjs.com).
7 |
8 | ## Features
9 |
10 | It comes with the following features out of the box:
11 |
12 | 1. Write content using MDX.
13 | 2. Transform and validate content using Contentlayer.
14 | 3. MDX components such as ` ` and ` `.
15 | 4. Support for table of contents.
16 | 5. Custom navigation with prev and next pager.
17 | 6. Beautiful code blocks using `rehype-pretty-code`.
18 | 7. Syntax highlighting using `shiki`.
19 | 8. Built-in search (_in progress_).
20 | 9. Dark mode (_in progress_).
21 |
22 | ## How is it built
23 |
24 | Click on a section below to learn how the documentation site built.
25 |
26 |
27 |
28 |
29 |
30 | ### Contentlayer
31 |
32 | Learn how to use MDX with Contentlayer.
33 |
34 |
35 |
36 |
37 |
38 | ### Components
39 |
40 | Using React components in Mardown.
41 |
42 |
43 |
44 |
45 |
46 | ### Code Blocks
47 |
48 | Beautiful code blocks with syntax highlighting.
49 |
50 |
51 |
52 |
53 |
54 | ### Style Guide
55 |
56 | View a sample page with all the styles.
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/content/docs/in-progress.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Not Implemented
3 | description: This page is in progress.
4 | ---
5 |
6 |
7 |
8 | This site is a work in progress. If you see dummy text on a page, it means I'm still working on it. You can follow updates on Twitter [@shadcn](https://twitter.com/shadcn).
9 |
10 |
11 |
--------------------------------------------------------------------------------
/content/docs/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Documentation
3 | description: Welcome to the Taxonomy documentation.
4 | ---
5 |
6 | This is the documentation for the Taxonomy site.
7 |
8 | This is an example of a doc site built using [ContentLayer](/docs/documentation/contentlayer) and MDX.
9 |
10 |
11 |
12 | This site is a work in progress. If you see dummy text on a page, it means I'm still working on it. You can follow updates on Twitter [@shadcn](https://twitter.com/shadcn).
13 |
14 |
15 |
16 | ## Features
17 |
18 | Select a feature below to learn more about it.
19 |
20 |
21 |
22 |
23 |
24 | ### Documentation
25 |
26 | This documentation site built using Contentlayer.
27 |
28 |
29 |
30 |
31 |
32 | ### Marketing
33 |
34 | The marketing site with landing pages.
35 |
36 |
37 |
38 |
39 |
40 | ### App
41 |
42 | The dashboard with auth and subscriptions.
43 |
44 |
45 |
46 |
47 |
48 | ### Blog
49 |
50 | The blog built using Contentlayer and MDX.
51 |
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/content/pages/privacy.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Privacy
3 | description: The Privacy Policy for Taxonomy App.
4 | ---
5 |
6 | Blandit libero volutpat sed cras ornare arcu. Cursus sit amet dictum sit amet. Nunc vel risus commodo viverra maecenas accumsan. Libero id faucibus nisl tincidunt eget nullam non nisi est. Varius quam quisque id diam vel quam. Id donec ultrices tincidunt arcu non.
7 |
8 | ## Consent
9 |
10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Volutpat sed cras ornare arcu. Nibh ipsum consequat nisl vel pretium lectus quam id leo. A arcu cursus vitae congue. Amet justo donec enim diam. Vel pharetra vel turpis nunc eget lorem. Gravida quis blandit turpis cursus in. Semper auctor neque vitae tempus. Elementum facilisis leo vel fringilla est ullamcorper eget nulla. Imperdiet nulla malesuada pellentesque elit eget.
11 |
12 | Felis donec et odio pellentesque diam volutpat commodo sed.
13 |
14 | Tortor consequat id porta nibh. Fames ac turpis egestas maecenas pharetra convallis posuere morbi leo. Scelerisque fermentum dui faucibus in. Tortor posuere ac ut consequat semper viverra.
15 |
16 | ## Information we collect
17 |
18 | Amet justo donec enim diam. In hendrerit gravida rutrum quisque non. Hac habitasse platea dictumst quisque sagittis purus sit.
19 |
20 | ## How we use your Information
21 |
22 | Ut sem nulla pharetra diam sit amet nisl suscipit adipiscing. Consectetur adipiscing elit pellentesque habitant. Ut tristique et egestas quis ipsum suspendisse ultrices gravida.
23 |
--------------------------------------------------------------------------------
/content/pages/terms.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Terms & Conditions
3 | description: Read our terms and conditions.
4 | ---
5 |
6 | Lorem ipsumMagna fermentum iaculis eu non diam. Vitae purus faucibus ornare suspendisse sed nisi lacus sed. In nibh mauris cursus mattis molestie a iaculis at. Enim sit amet venenatis urna. Eget sit amet tellus cras adipiscing.
7 |
8 | ## Legal Notices
9 |
10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Volutpat sed cras ornare arcu. Nibh ipsum consequat nisl vel pretium lectus quam id leo. A arcu cursus vitae congue. Amet justo donec enim diam. Vel pharetra vel turpis nunc eget lorem. Gravida quis blandit turpis cursus in. Semper auctor neque vitae tempus. Elementum facilisis leo vel fringilla est ullamcorper eget nulla. Imperdiet nulla malesuada pellentesque elit eget.
11 |
12 | Felis donec et odio pellentesque diam volutpat commodo sed.
13 |
14 | Tortor consequat id porta nibh. Fames ac turpis egestas maecenas pharetra convallis posuere morbi leo. Scelerisque fermentum dui faucibus in. Tortor posuere ac ut consequat semper viverra.
15 |
16 | ## Warranty Disclaimer
17 |
18 | Tellus in hac habitasse platea dictumst vestibulum. Faucibus in ornare quam viverra. Viverra aliquet eget sit amet tellus cras adipiscing. Erat nam at lectus urna duis convallis convallis tellus. Bibendum est ultricies integer quis auctor elit sed vulputate.
19 |
20 | Nisl condimentum id venenatis a condimentum vitae. Ac auctor augue mauris augue neque gravida in fermentum. Arcu felis bibendum ut tristique. Tempor commodo ullamcorper a lacus vestibulum sed arcu non.
21 |
22 | ## General
23 |
24 | Magna fermentum iaculis eu non diam. Vitae purus faucibus ornare suspendisse sed nisi lacus sed. In nibh mauris cursus mattis molestie a iaculis at. Enim sit amet venenatis urna. Eget sit amet tellus cras adipiscing.
25 |
26 | Sed lectus vestibulum mattis ullamcorper velit. Id diam vel quam elementum pulvinar. In iaculis nunc sed augue lacus viverra. In hendrerit gravida rutrum quisque non tellus. Nisl purus in mollis nunc.
27 |
28 | ## Disclaimer
29 |
30 | Amet justo donec enim diam. In hendrerit gravida rutrum quisque non. Hac habitasse platea dictumst quisque sagittis purus sit. Faucibus ornare suspendisse sed nisi lacus. Nulla porttitor massa id neque aliquam vestibulum. Ante in nibh mauris cursus mattis molestie a. Mi tempus imperdiet nulla malesuada.
31 |
--------------------------------------------------------------------------------
/env.mjs:
--------------------------------------------------------------------------------
1 | import { createEnv } from "@t3-oss/env-nextjs"
2 | import { z } from "zod"
3 |
4 | export const env = createEnv({
5 | server: {
6 | // This is optional because it's only used in development.
7 | // See https://next-auth.js.org/deployment.
8 | NEXTAUTH_URL: z.string().url().optional(),
9 | NEXTAUTH_SECRET: z.string().min(1),
10 | GITHUB_CLIENT_ID: z.string().min(1),
11 | GITHUB_CLIENT_SECRET: z.string().min(1),
12 | GITHUB_ACCESS_TOKEN: z.string().min(1),
13 | DATABASE_URL: z.string().min(1),
14 | SMTP_FROM: z.string().min(1),
15 | POSTMARK_API_TOKEN: z.string().min(1),
16 | POSTMARK_SIGN_IN_TEMPLATE: z.string().min(1),
17 | POSTMARK_ACTIVATION_TEMPLATE: z.string().min(1),
18 | STRIPE_API_KEY: z.string().min(1),
19 | STRIPE_WEBHOOK_SECRET: z.string().min(1),
20 | STRIPE_PRO_MONTHLY_PLAN_ID: z.string().min(1),
21 | },
22 | client: {
23 | NEXT_PUBLIC_APP_URL: z.string().min(1),
24 | },
25 | runtimeEnv: {
26 | NEXTAUTH_URL: process.env.NEXTAUTH_URL,
27 | NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
28 | GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID,
29 | GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET,
30 | GITHUB_ACCESS_TOKEN: process.env.GITHUB_ACCESS_TOKEN,
31 | DATABASE_URL: process.env.DATABASE_URL,
32 | SMTP_FROM: process.env.SMTP_FROM,
33 | POSTMARK_API_TOKEN: process.env.POSTMARK_API_TOKEN,
34 | POSTMARK_SIGN_IN_TEMPLATE: process.env.POSTMARK_SIGN_IN_TEMPLATE,
35 | POSTMARK_ACTIVATION_TEMPLATE: process.env.POSTMARK_ACTIVATION_TEMPLATE,
36 | STRIPE_API_KEY: process.env.STRIPE_API_KEY,
37 | STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET,
38 | STRIPE_PRO_MONTHLY_PLAN_ID: process.env.STRIPE_PRO_MONTHLY_PLAN_ID,
39 | NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
40 | },
41 | })
42 |
--------------------------------------------------------------------------------
/hooks/use-lock-body.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | // @see https://usehooks.com/useLockBodyScroll.
4 | export function useLockBody() {
5 | React.useLayoutEffect((): (() => void) => {
6 | const originalStyle: string = window.getComputedStyle(
7 | document.body
8 | ).overflow
9 | document.body.style.overflow = "hidden"
10 | return () => (document.body.style.overflow = originalStyle)
11 | }, [])
12 | }
13 |
--------------------------------------------------------------------------------
/hooks/use-mounted.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | export function useMounted() {
4 | const [mounted, setMounted] = React.useState(false)
5 |
6 | React.useEffect(() => {
7 | setMounted(true)
8 | }, [])
9 |
10 | return mounted
11 | }
12 |
--------------------------------------------------------------------------------
/lib/auth.ts:
--------------------------------------------------------------------------------
1 | import { PrismaAdapter } from "@next-auth/prisma-adapter"
2 | import { NextAuthOptions } from "next-auth"
3 | import EmailProvider from "next-auth/providers/email"
4 | import GitHubProvider from "next-auth/providers/github"
5 | import { Client } from "postmark"
6 |
7 | import { env } from "@/env.mjs"
8 | import { siteConfig } from "@/config/site"
9 | import { db } from "@/lib/db"
10 |
11 | const postmarkClient = new Client(env.POSTMARK_API_TOKEN)
12 |
13 | export const authOptions: NextAuthOptions = {
14 | // huh any! I know.
15 | // This is a temporary fix for prisma client.
16 | // @see https://github.com/prisma/prisma/issues/16117
17 | adapter: PrismaAdapter(db as any),
18 | session: {
19 | strategy: "jwt",
20 | },
21 | pages: {
22 | signIn: "/login",
23 | },
24 | providers: [
25 | GitHubProvider({
26 | clientId: env.GITHUB_CLIENT_ID,
27 | clientSecret: env.GITHUB_CLIENT_SECRET,
28 | }),
29 | EmailProvider({
30 | from: env.SMTP_FROM,
31 | sendVerificationRequest: async ({ identifier, url, provider }) => {
32 | const user = await db.user.findUnique({
33 | where: {
34 | email: identifier,
35 | },
36 | select: {
37 | emailVerified: true,
38 | },
39 | })
40 |
41 | const templateId = user?.emailVerified
42 | ? env.POSTMARK_SIGN_IN_TEMPLATE
43 | : env.POSTMARK_ACTIVATION_TEMPLATE
44 | if (!templateId) {
45 | throw new Error("Missing template id")
46 | }
47 |
48 | const result = await postmarkClient.sendEmailWithTemplate({
49 | TemplateId: parseInt(templateId),
50 | To: identifier,
51 | From: provider.from as string,
52 | TemplateModel: {
53 | action_url: url,
54 | product_name: siteConfig.name,
55 | },
56 | Headers: [
57 | {
58 | // Set this to prevent Gmail from threading emails.
59 | // See https://stackoverflow.com/questions/23434110/force-emails-not-to-be-grouped-into-conversations/25435722.
60 | Name: "X-Entity-Ref-ID",
61 | Value: new Date().getTime() + "",
62 | },
63 | ],
64 | })
65 |
66 | if (result.ErrorCode) {
67 | throw new Error(result.Message)
68 | }
69 | },
70 | }),
71 | ],
72 | callbacks: {
73 | async session({ token, session }) {
74 | if (token) {
75 | session.user.id = token.id
76 | session.user.name = token.name
77 | session.user.email = token.email
78 | session.user.image = token.picture
79 | }
80 |
81 | return session
82 | },
83 | async jwt({ token, user }) {
84 | const dbUser = await db.user.findFirst({
85 | where: {
86 | email: token.email,
87 | },
88 | })
89 |
90 | if (!dbUser) {
91 | if (user) {
92 | token.id = user?.id
93 | }
94 | return token
95 | }
96 |
97 | return {
98 | id: dbUser.id,
99 | name: dbUser.name,
100 | email: dbUser.email,
101 | picture: dbUser.image,
102 | }
103 | },
104 | },
105 | }
106 |
--------------------------------------------------------------------------------
/lib/db.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client"
2 |
3 | declare global {
4 | // eslint-disable-next-line no-var
5 | var cachedPrisma: PrismaClient
6 | }
7 |
8 | let prisma: PrismaClient
9 | if (process.env.NODE_ENV === "production") {
10 | prisma = new PrismaClient()
11 | } else {
12 | if (!global.cachedPrisma) {
13 | global.cachedPrisma = new PrismaClient()
14 | }
15 | prisma = global.cachedPrisma
16 | }
17 |
18 | export const db = prisma
19 |
--------------------------------------------------------------------------------
/lib/exceptions.ts:
--------------------------------------------------------------------------------
1 | export class RequiresProPlanError extends Error {
2 | constructor(message = "This action requires a pro plan") {
3 | super(message)
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/lib/session.ts:
--------------------------------------------------------------------------------
1 | import { getServerSession } from "next-auth/next"
2 |
3 | import { authOptions } from "@/lib/auth"
4 |
5 | export async function getCurrentUser() {
6 | const session = await getServerSession(authOptions)
7 |
8 | return session?.user
9 | }
10 |
--------------------------------------------------------------------------------
/lib/stripe.ts:
--------------------------------------------------------------------------------
1 | import Stripe from "stripe"
2 |
3 | import { env } from "@/env.mjs"
4 |
5 | export const stripe = new Stripe(env.STRIPE_API_KEY, {
6 | apiVersion: "2022-11-15",
7 | typescript: true,
8 | })
9 |
--------------------------------------------------------------------------------
/lib/subscription.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | // TODO: Fix this when we turn strict mode on.
3 | import { UserSubscriptionPlan } from "types"
4 | import { freePlan, proPlan } from "@/config/subscriptions"
5 | import { db } from "@/lib/db"
6 |
7 | export async function getUserSubscriptionPlan(
8 | userId: string
9 | ): Promise {
10 | const user = await db.user.findFirst({
11 | where: {
12 | id: userId,
13 | },
14 | select: {
15 | stripeSubscriptionId: true,
16 | stripeCurrentPeriodEnd: true,
17 | stripeCustomerId: true,
18 | stripePriceId: true,
19 | },
20 | })
21 |
22 | if (!user) {
23 | throw new Error("User not found")
24 | }
25 |
26 | // Check if user is on a pro plan.
27 | const isPro =
28 | user.stripePriceId &&
29 | user.stripeCurrentPeriodEnd?.getTime() + 86_400_000 > Date.now()
30 |
31 | const plan = isPro ? proPlan : freePlan
32 |
33 | return {
34 | ...plan,
35 | ...user,
36 | stripeCurrentPeriodEnd: user.stripeCurrentPeriodEnd?.getTime(),
37 | isPro,
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/lib/toc.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | // TODO: Fix this when we turn strict mode on.
3 |
4 | import { toc } from "mdast-util-toc"
5 | import { remark } from "remark"
6 | import { visit } from "unist-util-visit"
7 |
8 | const textTypes = ["text", "emphasis", "strong", "inlineCode"]
9 |
10 | function flattenNode(node) {
11 | const p = []
12 | visit(node, (node) => {
13 | if (!textTypes.includes(node.type)) return
14 | p.push(node.value)
15 | })
16 | return p.join(``)
17 | }
18 |
19 | interface Item {
20 | title: string
21 | url: string
22 | items?: Item[]
23 | }
24 |
25 | interface Items {
26 | items?: Item[]
27 | }
28 |
29 | function getItems(node, current): Items {
30 | if (!node) {
31 | return {}
32 | }
33 |
34 | if (node.type === "paragraph") {
35 | visit(node, (item) => {
36 | if (item.type === "link") {
37 | current.url = item.url
38 | current.title = flattenNode(node)
39 | }
40 |
41 | if (item.type === "text") {
42 | current.title = flattenNode(node)
43 | }
44 | })
45 |
46 | return current
47 | }
48 |
49 | if (node.type === "list") {
50 | current.items = node.children.map((i) => getItems(i, {}))
51 |
52 | return current
53 | } else if (node.type === "listItem") {
54 | const heading = getItems(node.children[0], {})
55 |
56 | if (node.children.length > 1) {
57 | getItems(node.children[1], heading)
58 | }
59 |
60 | return heading
61 | }
62 |
63 | return {}
64 | }
65 |
66 | const getToc = () => (node, file) => {
67 | const table = toc(node)
68 | file.data = getItems(table.map, {})
69 | }
70 |
71 | export type TableOfContents = Items
72 |
73 | export async function getTableOfContents(
74 | content: string
75 | ): Promise {
76 | const result = await remark().use(getToc).process(content)
77 |
78 | return result.data
79 | }
80 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | import { env } from "@/env.mjs"
5 |
6 | export function cn(...inputs: ClassValue[]) {
7 | return twMerge(clsx(inputs))
8 | }
9 |
10 | export function formatDate(input: string | number): string {
11 | const date = new Date(input)
12 | return date.toLocaleDateString("en-US", {
13 | month: "long",
14 | day: "numeric",
15 | year: "numeric",
16 | })
17 | }
18 |
19 | export function absoluteUrl(path: string) {
20 | return `${env.NEXT_PUBLIC_APP_URL}${path}`
21 | }
22 |
--------------------------------------------------------------------------------
/lib/validations/auth.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod"
2 |
3 | export const userAuthSchema = z.object({
4 | email: z.string().email(),
5 | })
6 |
--------------------------------------------------------------------------------
/lib/validations/og.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod"
2 |
3 | export const ogImageSchema = z.object({
4 | heading: z.string(),
5 | type: z.string(),
6 | mode: z.enum(["light", "dark"]).default("dark"),
7 | })
8 |
--------------------------------------------------------------------------------
/lib/validations/post.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod"
2 |
3 | export const postPatchSchema = z.object({
4 | title: z.string().min(3).max(128).optional(),
5 |
6 | // TODO: Type this properly from editorjs block types?
7 | content: z.any().optional(),
8 | })
9 |
--------------------------------------------------------------------------------
/lib/validations/user.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod"
2 |
3 | export const userNameSchema = z.object({
4 | name: z.string().min(3).max(32),
5 | })
6 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { getToken } from "next-auth/jwt"
2 | import { withAuth } from "next-auth/middleware"
3 | import { NextResponse } from "next/server"
4 |
5 | export default withAuth(
6 | async function middleware(req) {
7 | const token = await getToken({ req })
8 | const isAuth = !!token
9 | const isAuthPage =
10 | req.nextUrl.pathname.startsWith("/login") ||
11 | req.nextUrl.pathname.startsWith("/register")
12 |
13 | if (isAuthPage) {
14 | if (isAuth) {
15 | return NextResponse.redirect(new URL("/dashboard", req.url))
16 | }
17 |
18 | return null
19 | }
20 |
21 | if (!isAuth) {
22 | let from = req.nextUrl.pathname;
23 | if (req.nextUrl.search) {
24 | from += req.nextUrl.search;
25 | }
26 |
27 | return NextResponse.redirect(
28 | new URL(`/login?from=${encodeURIComponent(from)}`, req.url)
29 | );
30 | }
31 | },
32 | {
33 | callbacks: {
34 | async authorized() {
35 | // This is a work-around for handling redirect on auth pages.
36 | // We return true here so that the middleware function above
37 | // is always called.
38 | return true
39 | },
40 | },
41 | }
42 | )
43 |
44 | export const config = {
45 | matcher: ["/dashboard/:path*", "/editor/:path*", "/login", "/register"],
46 | }
47 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | import { withContentlayer } from "next-contentlayer"
2 |
3 | import "./env.mjs"
4 |
5 | /** @type {import('next').NextConfig} */
6 | const nextConfig = {
7 | reactStrictMode: true,
8 | images: {
9 | domains: ["avatars.githubusercontent.com"],
10 | },
11 | experimental: {
12 | appDir: true,
13 | serverComponentsExternalPackages: ["@prisma/client"],
14 | },
15 | }
16 |
17 | export default withContentlayer(nextConfig)
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "taxonomy",
3 | "version": "0.2.0",
4 | "private": true,
5 | "author": {
6 | "name": "shadcn",
7 | "url": "https://twitter.com/shadcn"
8 | },
9 | "scripts": {
10 | "dev": "concurrently \"contentlayer dev\" \"next dev\"",
11 | "build": "contentlayer build && next build",
12 | "turbo": "next dev --turbo",
13 | "start": "next start",
14 | "lint": "next lint",
15 | "preview": "next build && next start",
16 | "postinstall": "prisma generate"
17 | },
18 | "dependencies": {
19 | "@editorjs/code": "^2.8.0",
20 | "@editorjs/editorjs": "^2.26.5",
21 | "@editorjs/embed": "^2.5.3",
22 | "@editorjs/header": "^2.7.0",
23 | "@editorjs/inline-code": "^1.4.0",
24 | "@editorjs/link": "^2.5.0",
25 | "@editorjs/list": "^1.8.0",
26 | "@editorjs/paragraph": "^2.9.0",
27 | "@editorjs/table": "^2.2.1",
28 | "@hookform/resolvers": "^3.1.0",
29 | "@next-auth/prisma-adapter": "^1.0.6",
30 | "@prisma/client": "^4.13.0",
31 | "@radix-ui/react-accessible-icon": "^1.0.2",
32 | "@radix-ui/react-accordion": "^1.1.1",
33 | "@radix-ui/react-alert-dialog": "^1.0.3",
34 | "@radix-ui/react-aspect-ratio": "^1.0.2",
35 | "@radix-ui/react-avatar": "^1.0.2",
36 | "@radix-ui/react-checkbox": "^1.0.3",
37 | "@radix-ui/react-collapsible": "^1.0.2",
38 | "@radix-ui/react-context-menu": "^2.1.3",
39 | "@radix-ui/react-dialog": "^1.0.3",
40 | "@radix-ui/react-dropdown-menu": "^2.0.4",
41 | "@radix-ui/react-hover-card": "^1.0.5",
42 | "@radix-ui/react-label": "^2.0.1",
43 | "@radix-ui/react-menubar": "^1.0.2",
44 | "@radix-ui/react-navigation-menu": "^1.1.2",
45 | "@radix-ui/react-popover": "^1.0.5",
46 | "@radix-ui/react-progress": "^1.0.2",
47 | "@radix-ui/react-radio-group": "^1.1.2",
48 | "@radix-ui/react-scroll-area": "^1.0.3",
49 | "@radix-ui/react-select": "^1.2.1",
50 | "@radix-ui/react-separator": "^1.0.2",
51 | "@radix-ui/react-slider": "^1.1.1",
52 | "@radix-ui/react-slot": "^1.0.1",
53 | "@radix-ui/react-switch": "^1.0.2",
54 | "@radix-ui/react-tabs": "^1.0.3",
55 | "@radix-ui/react-toast": "^1.1.3",
56 | "@radix-ui/react-toggle": "^1.0.2",
57 | "@radix-ui/react-toggle-group": "^1.0.3",
58 | "@radix-ui/react-tooltip": "^1.0.5",
59 | "@t3-oss/env-nextjs": "^0.2.2",
60 | "@typescript-eslint/parser": "^5.59.0",
61 | "@vercel/analytics": "^1.0.0",
62 | "@vercel/og": "^0.0.21",
63 | "class-variance-authority": "^0.4.0",
64 | "clsx": "^1.2.1",
65 | "cmdk": "^0.1.22",
66 | "concurrently": "^8.0.1",
67 | "contentlayer": "^0.3.1",
68 | "date-fns": "^2.29.3",
69 | "lucide-react": "^0.92.0",
70 | "next": "13.3.2-canary.13",
71 | "next-auth": "4.22.1",
72 | "next-contentlayer": "^0.3.1",
73 | "next-themes": "^0.2.1",
74 | "nodemailer": "^6.9.1",
75 | "postmark": "^3.0.15",
76 | "prop-types": "^15.8.1",
77 | "react": "^18.2.0",
78 | "react-day-picker": "^8.7.1",
79 | "react-dom": "^18.2.0",
80 | "react-editor-js": "^2.1.0",
81 | "react-hook-form": "^7.43.9",
82 | "react-textarea-autosize": "^8.4.1",
83 | "sharp": "^0.31.3",
84 | "shiki": "^0.11.1",
85 | "stripe": "^11.18.0",
86 | "tailwind-merge": "^1.12.0",
87 | "tailwindcss-animate": "^1.0.5",
88 | "zod": "^3.21.4"
89 | },
90 | "devDependencies": {
91 | "@commitlint/cli": "^17.6.1",
92 | "@commitlint/config-conventional": "^17.6.1",
93 | "@ianvs/prettier-plugin-sort-imports": "^3.7.2",
94 | "@tailwindcss/line-clamp": "^0.4.4",
95 | "@tailwindcss/typography": "^0.5.9",
96 | "@types/node": "^18.16.0",
97 | "@types/react": "18.0.15",
98 | "@types/react-dom": "18.0.6",
99 | "autoprefixer": "^10.4.14",
100 | "eslint": "^8.39.0",
101 | "eslint-config-next": "13.0.0",
102 | "eslint-config-prettier": "^8.8.0",
103 | "eslint-plugin-react": "^7.32.2",
104 | "eslint-plugin-tailwindcss": "^3.11.0",
105 | "husky": "^8.0.3",
106 | "mdast-util-toc": "^6.1.1",
107 | "postcss": "^8.4.23",
108 | "prettier": "^2.8.8",
109 | "prettier-plugin-tailwindcss": "^0.1.13",
110 | "pretty-quick": "^3.1.3",
111 | "prisma": "^4.13.0",
112 | "rehype": "^12.0.1",
113 | "rehype-autolink-headings": "^6.1.1",
114 | "rehype-pretty-code": "^0.9.5",
115 | "rehype-slug": "^5.1.0",
116 | "remark": "^14.0.2",
117 | "remark-gfm": "^3.0.1",
118 | "tailwindcss": "^3.3.1",
119 | "typescript": "4.7.4",
120 | "unist-util-visit": "^4.1.2"
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/pages/api/auth/[...nextauth].ts:
--------------------------------------------------------------------------------
1 | import NextAuth from "next-auth"
2 |
3 | import { authOptions } from "@/lib/auth"
4 |
5 | // @see ./lib/auth
6 | export default NextAuth(authOptions)
7 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('prettier').Config} */
2 | module.exports = {
3 | endOfLine: "lf",
4 | semi: false,
5 | singleQuote: false,
6 | tabWidth: 2,
7 | trailingComma: "es5",
8 | importOrder: [
9 | "^(react/(.*)$)|^(react$)",
10 | "^(next/(.*)$)|^(next$)",
11 | "",
12 | "",
13 | "^types$",
14 | "^@/env(.*)$",
15 | "^@/types/(.*)$",
16 | "^@/config/(.*)$",
17 | "^@/lib/(.*)$",
18 | "^@/hooks/(.*)$",
19 | "^@/components/ui/(.*)$",
20 | "^@/components/(.*)$",
21 | "^@/styles/(.*)$",
22 | "^@/app/(.*)$",
23 | "",
24 | "^[./]",
25 | ],
26 | importOrderSeparation: false,
27 | importOrderSortSpecifiers: true,
28 | importOrderBuiltinModulesToTop: true,
29 | importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"],
30 | importOrderMergeDuplicateImports: true,
31 | importOrderCombineTypeAndValueImports: true,
32 | plugins: ["@ianvs/prettier-plugin-sort-imports"],
33 | }
34 |
--------------------------------------------------------------------------------
/prisma/migrations/20221021182747_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE `accounts` (
3 | `id` VARCHAR(191) NOT NULL,
4 | `userId` VARCHAR(191) NOT NULL,
5 | `type` VARCHAR(191) NOT NULL,
6 | `provider` VARCHAR(191) NOT NULL,
7 | `providerAccountId` VARCHAR(191) NOT NULL,
8 | `refresh_token` TEXT NULL,
9 | `access_token` TEXT NULL,
10 | `expires_at` INTEGER NULL,
11 | `token_type` VARCHAR(191) NULL,
12 | `scope` VARCHAR(191) NULL,
13 | `id_token` TEXT NULL,
14 | `session_state` VARCHAR(191) NULL,
15 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
16 | `updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
17 |
18 | UNIQUE INDEX `accounts_provider_providerAccountId_key`(`provider`, `providerAccountId`),
19 | PRIMARY KEY (`id`)
20 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
21 |
22 | -- CreateTable
23 | CREATE TABLE `sessions` (
24 | `id` VARCHAR(191) NOT NULL,
25 | `sessionToken` VARCHAR(191) NOT NULL,
26 | `userId` VARCHAR(191) NOT NULL,
27 | `expires` DATETIME(3) NOT NULL,
28 |
29 | UNIQUE INDEX `sessions_sessionToken_key`(`sessionToken`),
30 | PRIMARY KEY (`id`)
31 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
32 |
33 | -- CreateTable
34 | CREATE TABLE `users` (
35 | `id` VARCHAR(191) NOT NULL,
36 | `name` VARCHAR(191) NULL,
37 | `email` VARCHAR(191) NULL,
38 | `emailVerified` DATETIME(3) NULL,
39 | `image` VARCHAR(191) NULL,
40 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
41 | `updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
42 |
43 | UNIQUE INDEX `users_email_key`(`email`),
44 | PRIMARY KEY (`id`)
45 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
46 |
47 | -- CreateTable
48 | CREATE TABLE `verification_tokens` (
49 | `identifier` VARCHAR(191) NOT NULL,
50 | `token` VARCHAR(191) NOT NULL,
51 | `expires` DATETIME(3) NOT NULL,
52 |
53 | UNIQUE INDEX `verification_tokens_token_key`(`token`),
54 | UNIQUE INDEX `verification_tokens_identifier_token_key`(`identifier`, `token`)
55 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
56 |
57 | -- CreateTable
58 | CREATE TABLE `posts` (
59 | `id` VARCHAR(191) NOT NULL,
60 | `title` VARCHAR(191) NOT NULL,
61 | `content` JSON NULL,
62 | `published` BOOLEAN NOT NULL DEFAULT false,
63 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
64 | `updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
65 | `authorId` VARCHAR(191) NOT NULL,
66 |
67 | PRIMARY KEY (`id`)
68 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
69 |
70 | -- AddForeignKey
71 | ALTER TABLE `accounts` ADD CONSTRAINT `accounts_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
72 |
73 | -- AddForeignKey
74 | ALTER TABLE `sessions` ADD CONSTRAINT `sessions_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
75 |
76 | -- AddForeignKey
77 | ALTER TABLE `posts` ADD CONSTRAINT `posts_authorId_fkey` FOREIGN KEY (`authorId`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
78 |
--------------------------------------------------------------------------------
/prisma/migrations/20221118173244_add_stripe_columns/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - A unique constraint covering the columns `[stripe_customer_id]` on the table `users` will be added. If there are existing duplicate values, this will fail.
5 | - A unique constraint covering the columns `[stripe_subscription_id]` on the table `users` will be added. If there are existing duplicate values, this will fail.
6 |
7 | */
8 | -- DropForeignKey
9 | ALTER TABLE `accounts` DROP FOREIGN KEY `accounts_userId_fkey`;
10 |
11 | -- DropForeignKey
12 | ALTER TABLE `posts` DROP FOREIGN KEY `posts_authorId_fkey`;
13 |
14 | -- DropForeignKey
15 | ALTER TABLE `sessions` DROP FOREIGN KEY `sessions_userId_fkey`;
16 |
17 | -- AlterTable
18 | ALTER TABLE `users` ADD COLUMN `stripe_current_period_end` DATETIME(3) NULL,
19 | ADD COLUMN `stripe_customer_id` VARCHAR(191) NULL,
20 | ADD COLUMN `stripe_price_id` VARCHAR(191) NULL,
21 | ADD COLUMN `stripe_subscription_id` VARCHAR(191) NULL;
22 |
23 | -- CreateIndex
24 | CREATE UNIQUE INDEX `users_stripe_customer_id_key` ON `users`(`stripe_customer_id`);
25 |
26 | -- CreateIndex
27 | CREATE UNIQUE INDEX `users_stripe_subscription_id_key` ON `users`(`stripe_subscription_id`);
28 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "mysql"
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | generator client {
5 | provider = "prisma-client-js"
6 | }
7 |
8 | datasource db {
9 | provider = "mysql"
10 | url = env("DATABASE_URL")
11 | }
12 |
13 | model Account {
14 | id String @id @default(cuid())
15 | userId String
16 | type String
17 | provider String
18 | providerAccountId String
19 | refresh_token String? @db.Text
20 | access_token String? @db.Text
21 | expires_at Int?
22 | token_type String?
23 | scope String?
24 | id_token String? @db.Text
25 | session_state String?
26 | createdAt DateTime @default(now()) @map(name: "created_at")
27 | updatedAt DateTime @default(now()) @map(name: "updated_at")
28 |
29 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
30 |
31 | @@unique([provider, providerAccountId])
32 | @@map(name: "accounts")
33 | }
34 |
35 | model Session {
36 | id String @id @default(cuid())
37 | sessionToken String @unique
38 | userId String
39 | expires DateTime
40 | user User @relation(fields: [userId], references: [id], onDelete: Cascade)
41 |
42 | @@map(name: "sessions")
43 | }
44 |
45 | model User {
46 | id String @id @default(cuid())
47 | name String?
48 | email String? @unique
49 | emailVerified DateTime?
50 | image String?
51 | createdAt DateTime @default(now()) @map(name: "created_at")
52 | updatedAt DateTime @default(now()) @map(name: "updated_at")
53 |
54 | accounts Account[]
55 | sessions Session[]
56 | Post Post[]
57 |
58 | stripeCustomerId String? @unique @map(name: "stripe_customer_id")
59 | stripeSubscriptionId String? @unique @map(name: "stripe_subscription_id")
60 | stripePriceId String? @map(name: "stripe_price_id")
61 | stripeCurrentPeriodEnd DateTime? @map(name: "stripe_current_period_end")
62 |
63 | @@map(name: "users")
64 | }
65 |
66 | model VerificationToken {
67 | identifier String
68 | token String @unique
69 | expires DateTime
70 |
71 | @@unique([identifier, token])
72 | @@map(name: "verification_tokens")
73 | }
74 |
75 | model Post {
76 | id String @id @default(cuid())
77 | title String
78 | content Json?
79 | published Boolean @default(false)
80 | createdAt DateTime @default(now()) @map(name: "created_at")
81 | updatedAt DateTime @default(now()) @map(name: "updated_at")
82 | authorId String
83 |
84 | author User @relation(fields: [authorId], references: [id])
85 |
86 | @@map(name: "posts")
87 | }
88 |
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn-ui/taxonomy/651f984e52edd65d40ccd55e299c1baeea3ff017/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn-ui/taxonomy/651f984e52edd65d40ccd55e299c1baeea3ff017/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn-ui/taxonomy/651f984e52edd65d40ccd55e299c1baeea3ff017/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn-ui/taxonomy/651f984e52edd65d40ccd55e299c1baeea3ff017/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn-ui/taxonomy/651f984e52edd65d40ccd55e299c1baeea3ff017/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn-ui/taxonomy/651f984e52edd65d40ccd55e299c1baeea3ff017/public/favicon.ico
--------------------------------------------------------------------------------
/public/images/avatars/shadcn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn-ui/taxonomy/651f984e52edd65d40ccd55e299c1baeea3ff017/public/images/avatars/shadcn.png
--------------------------------------------------------------------------------
/public/images/blog/blog-post-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn-ui/taxonomy/651f984e52edd65d40ccd55e299c1baeea3ff017/public/images/blog/blog-post-1.jpg
--------------------------------------------------------------------------------
/public/images/blog/blog-post-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn-ui/taxonomy/651f984e52edd65d40ccd55e299c1baeea3ff017/public/images/blog/blog-post-2.jpg
--------------------------------------------------------------------------------
/public/images/blog/blog-post-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn-ui/taxonomy/651f984e52edd65d40ccd55e299c1baeea3ff017/public/images/blog/blog-post-3.jpg
--------------------------------------------------------------------------------
/public/images/blog/blog-post-4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn-ui/taxonomy/651f984e52edd65d40ccd55e299c1baeea3ff017/public/images/blog/blog-post-4.jpg
--------------------------------------------------------------------------------
/public/images/hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn-ui/taxonomy/651f984e52edd65d40ccd55e299c1baeea3ff017/public/images/hero.png
--------------------------------------------------------------------------------
/public/og.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shadcn-ui/taxonomy/651f984e52edd65d40ccd55e299c1baeea3ff017/public/og.jpg
--------------------------------------------------------------------------------
/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Taxonomy",
3 | "short_name": "Taxonomy",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/styles/editor.css:
--------------------------------------------------------------------------------
1 | .dark .ce-block--selected .ce-block__content,
2 | .dark .ce-inline-toolbar,
3 | .dark .codex-editor--narrow .ce-toolbox,
4 | .dark .ce-conversion-toolbar,
5 | .dark .ce-settings,
6 | .dark .ce-settings__button,
7 | .dark .ce-toolbar__settings-btn,
8 | .dark .cdx-button,
9 | .dark .ce-popover,
10 | .dark .ce-toolbar__plus:hover {
11 | background: theme('colors.popover.DEFAULT');
12 | color: inherit;
13 | border-color: theme('colors.border');
14 | }
15 |
16 | .dark .ce-inline-tool,
17 | .dark .ce-conversion-toolbar__label,
18 | .dark .ce-toolbox__button,
19 | .dark .cdx-settings-button,
20 | .dark .ce-toolbar__plus {
21 | color: inherit;
22 | }
23 |
24 | .dark .ce-popover__item-icon,
25 | .dark .ce-conversion-tool__icon {
26 | background-color: theme('colors.muted.DEFAULT');
27 | box-shadow: none;
28 | }
29 |
30 | .dark .cdx-search-field {
31 | border-color: theme('colors.border');
32 | background: theme('colors.input');
33 | color: inherit;
34 | }
35 |
36 | .dark ::selection {
37 | background: theme('colors.accent.DEFAULT');
38 | }
39 |
40 | .dark .cdx-settings-button:hover,
41 | .dark .ce-settings__button:hover,
42 | .dark .ce-toolbox__button--active,
43 | .dark .ce-toolbox__button:hover,
44 | .dark .cdx-button:hover,
45 | .dark .ce-inline-toolbar__dropdown:hover,
46 | .dark .ce-inline-tool:hover,
47 | .dark .ce-popover__item:hover,
48 | .dark .ce-conversion-tool:hover,
49 | .dark .ce-toolbar__settings-btn:hover {
50 | background-color:theme('colors.accent.DEFAULT');
51 | color: theme('colors.accent.foreground');;
52 | }
53 |
54 | .dark .cdx-notify--error {
55 | background: theme('colors.destructive.DEFAULT') !important;
56 | }
57 |
58 | .dark .cdx-notify__cross::after,
59 | .dark .cdx-notify__cross::before {
60 | background: white;
61 | }
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 222.2 47.4% 11.2%;
9 |
10 | --muted: 210 40% 96.1%;
11 | --muted-foreground: 215.4 16.3% 46.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 222.2 47.4% 11.2%;
15 |
16 | --border: 214.3 31.8% 91.4%;
17 | --input: 214.3 31.8% 91.4%;
18 |
19 | --card: 0 0% 100%;
20 | --card-foreground: 222.2 47.4% 11.2%;
21 |
22 | --primary: 222.2 47.4% 11.2%;
23 | --primary-foreground: 210 40% 98%;
24 |
25 | --secondary: 210 40% 96.1%;
26 | --secondary-foreground: 222.2 47.4% 11.2%;
27 |
28 | --accent: 210 40% 96.1%;
29 | --accent-foreground: 222.2 47.4% 11.2%;
30 |
31 | --destructive: 0 100% 50%;
32 | --destructive-foreground: 210 40% 98%;
33 |
34 | --ring: 215 20.2% 65.1%;
35 |
36 | --radius: 0.5rem;
37 | }
38 |
39 | .dark {
40 | --background: 224 71% 4%;
41 | --foreground: 213 31% 91%;
42 |
43 | --muted: 223 47% 11%;
44 | --muted-foreground: 215.4 16.3% 56.9%;
45 |
46 | --accent: 216 34% 17%;
47 | --accent-foreground: 210 40% 98%;
48 |
49 | --popover: 224 71% 4%;
50 | --popover-foreground: 215 20.2% 65.1%;
51 |
52 | --border: 216 34% 17%;
53 | --input: 216 34% 17%;
54 |
55 | --card: 224 71% 4%;
56 | --card-foreground: 213 31% 91%;
57 |
58 | --primary: 210 40% 98%;
59 | --primary-foreground: 222.2 47.4% 1.2%;
60 |
61 | --secondary: 222.2 47.4% 11.2%;
62 | --secondary-foreground: 210 40% 98%;
63 |
64 | --destructive: 0 63% 31%;
65 | --destructive-foreground: 210 40% 98%;
66 |
67 | --ring: 216 34% 17%;
68 |
69 | --radius: 0.5rem;
70 | }
71 | }
72 |
73 | @layer base {
74 | * {
75 | @apply border-border;
76 | }
77 | body {
78 | @apply bg-background text-foreground;
79 | font-feature-settings: "rlig" 1, "calt" 1;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/styles/mdx.css:
--------------------------------------------------------------------------------
1 | [data-rehype-pretty-code-fragment] code {
2 | @apply grid min-w-full break-words rounded-none border-0 bg-transparent p-0 text-sm text-black;
3 | counter-reset: line;
4 | box-decoration-break: clone;
5 | }
6 | [data-rehype-pretty-code-fragment] .line {
7 | @apply px-4 py-1;
8 | }
9 | [data-rehype-pretty-code-fragment] [data-line-numbers] > .line::before {
10 | counter-increment: line;
11 | content: counter(line);
12 | display: inline-block;
13 | width: 1rem;
14 | margin-right: 1rem;
15 | text-align: right;
16 | color: gray;
17 | }
18 | [data-rehype-pretty-code-fragment] .line--highlighted {
19 | @apply bg-slate-300 bg-opacity-10;
20 | }
21 | [data-rehype-pretty-code-fragment] .line-highlighted span {
22 | @apply relative;
23 | }
24 | [data-rehype-pretty-code-fragment] .word--highlighted {
25 | @apply rounded-md bg-slate-300 bg-opacity-10 p-1;
26 | }
27 | [data-rehype-pretty-code-title] {
28 | @apply mt-4 py-2 px-4 text-sm font-medium;
29 | }
30 | [data-rehype-pretty-code-title] + pre {
31 | @apply mt-0;
32 | }
33 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const { fontFamily } = require("tailwindcss/defaultTheme")
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | module.exports = {
5 | content: [
6 | "./app/**/*.{ts,tsx}",
7 | "./components/**/*.{ts,tsx}",
8 | "./ui/**/*.{ts,tsx}",
9 | "./content/**/*.{md,mdx}",
10 | ],
11 | darkMode: ["class"],
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px",
18 | },
19 | },
20 | extend: {
21 | colors: {
22 | border: "hsl(var(--border))",
23 | input: "hsl(var(--input))",
24 | ring: "hsl(var(--ring))",
25 | background: "hsl(var(--background))",
26 | foreground: "hsl(var(--foreground))",
27 | primary: {
28 | DEFAULT: "hsl(var(--primary))",
29 | foreground: "hsl(var(--primary-foreground))",
30 | },
31 | secondary: {
32 | DEFAULT: "hsl(var(--secondary))",
33 | foreground: "hsl(var(--secondary-foreground))",
34 | },
35 | destructive: {
36 | DEFAULT: "hsl(var(--destructive))",
37 | foreground: "hsl(var(--destructive-foreground))",
38 | },
39 | muted: {
40 | DEFAULT: "hsl(var(--muted))",
41 | foreground: "hsl(var(--muted-foreground))",
42 | },
43 | accent: {
44 | DEFAULT: "hsl(var(--accent))",
45 | foreground: "hsl(var(--accent-foreground))",
46 | },
47 | popover: {
48 | DEFAULT: "hsl(var(--popover))",
49 | foreground: "hsl(var(--popover-foreground))",
50 | },
51 | card: {
52 | DEFAULT: "hsl(var(--card))",
53 | foreground: "hsl(var(--card-foreground))",
54 | },
55 | },
56 | borderRadius: {
57 | lg: `var(--radius)`,
58 | md: `calc(var(--radius) - 2px)`,
59 | sm: "calc(var(--radius) - 4px)",
60 | },
61 | fontFamily: {
62 | sans: ["var(--font-sans)", ...fontFamily.sans],
63 | heading: ["var(--font-heading)", ...fontFamily.sans],
64 | },
65 | keyframes: {
66 | "accordion-down": {
67 | from: { height: 0 },
68 | to: { height: "var(--radix-accordion-content-height)" },
69 | },
70 | "accordion-up": {
71 | from: { height: "var(--radix-accordion-content-height)" },
72 | to: { height: 0 },
73 | },
74 | },
75 | animation: {
76 | "accordion-down": "accordion-down 0.2s ease-out",
77 | "accordion-up": "accordion-up 0.2s ease-out",
78 | },
79 | },
80 | },
81 | plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
82 | }
83 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": false,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "baseUrl": ".",
18 | "paths": {
19 | "@/*": ["./*"],
20 | "contentlayer/generated": ["./.contentlayer/generated"]
21 | },
22 | "plugins": [
23 | {
24 | "name": "next"
25 | }
26 | ],
27 | "strictNullChecks": true
28 | },
29 | "include": [
30 | "next-env.d.ts",
31 | "**/*.ts",
32 | "**/*.tsx",
33 | ".next/types/**/*.ts",
34 | ".contentlayer/generated"
35 | ],
36 | "exclude": ["node_modules"]
37 | }
38 |
--------------------------------------------------------------------------------
/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import { User } from "@prisma/client"
2 | import type { Icon } from "lucide-react"
3 |
4 | import { Icons } from "@/components/icons"
5 |
6 | export type NavItem = {
7 | title: string
8 | href: string
9 | disabled?: boolean
10 | }
11 |
12 | export type MainNavItem = NavItem
13 |
14 | export type SidebarNavItem = {
15 | title: string
16 | disabled?: boolean
17 | external?: boolean
18 | icon?: keyof typeof Icons
19 | } & (
20 | | {
21 | href: string
22 | items?: never
23 | }
24 | | {
25 | href?: string
26 | items: NavLink[]
27 | }
28 | )
29 |
30 | export type SiteConfig = {
31 | name: string
32 | description: string
33 | url: string
34 | ogImage: string
35 | links: {
36 | twitter: string
37 | github: string
38 | }
39 | }
40 |
41 | export type DocsConfig = {
42 | mainNav: MainNavItem[]
43 | sidebarNav: SidebarNavItem[]
44 | }
45 |
46 | export type MarketingConfig = {
47 | mainNav: MainNavItem[]
48 | }
49 |
50 | export type DashboardConfig = {
51 | mainNav: MainNavItem[]
52 | sidebarNav: SidebarNavItem[]
53 | }
54 |
55 | export type SubscriptionPlan = {
56 | name: string
57 | description: string
58 | stripePriceId: string
59 | }
60 |
61 | export type UserSubscriptionPlan = SubscriptionPlan &
62 | Pick & {
63 | stripeCurrentPeriodEnd: number
64 | isPro: boolean
65 | }
66 |
--------------------------------------------------------------------------------
/types/next-auth.d.ts:
--------------------------------------------------------------------------------
1 | import { User } from "next-auth"
2 | import { JWT } from "next-auth/jwt"
3 |
4 | type UserId = string
5 |
6 | declare module "next-auth/jwt" {
7 | interface JWT {
8 | id: UserId
9 | }
10 | }
11 |
12 | declare module "next-auth" {
13 | interface Session {
14 | user: User & {
15 | id: UserId
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------