42 | {guide.title} 43 |
44 | {guide.description && ( 45 |{guide.description}
46 | )} 47 |50 | {formatDate(guide.date)} 51 |
52 | )} 53 |├── .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 ├── (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 │ ├── og │ │ └── route.tsx │ ├── posts │ │ ├── [postId] │ │ │ └── route.ts │ │ └── route.ts │ ├── users │ │ ├── [userId] │ │ │ └── route.ts │ │ └── stripe │ │ │ └── route.ts │ └── webhooks │ │ └── stripe │ │ └── route.ts ├── auth │ └── callback │ │ └── route.ts ├── layout.tsx ├── opengraph-image.jpg ├── robots.ts └── supabase-server.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 ├── exceptions.ts ├── helpers.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 ├── postcss.config.js ├── prettier.config.js ├── 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 ├── schema.sql ├── styles ├── editor.css ├── globals.css └── mdx.css ├── tailwind.config.js ├── tsconfig.json └── types ├── db.ts ├── index.d.ts └── main.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 | # Database (PostgreSQL - Supabase) 8 | # ----------------------------------------------------------------------------- 9 | NEXT_PUBLIC_SUPABASE_ANON= 10 | NEXT_PUBLIC_SUPABASE_URL= 11 | 12 | # ----------------------------------------------------------------------------- 13 | # Subscriptions (Stripe) 14 | # ----------------------------------------------------------------------------- 15 | STRIPE_API_KEY= 16 | STRIPE_WEBHOOK_SECRET= 17 | STRIPE_PRO_MONTHLY_PLAN_ID= 18 | 19 | # ----------------------------------------------------------------------------- 20 | # Misc (GitHub) 21 | # ----------------------------------------------------------------------------- 22 | GITHUB_ACCESS_TOKEN= -------------------------------------------------------------------------------- /.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 + Supabase 2 | 3 | A clone of @shadcn's open source application built using the new router, server components, and everything new in Next.js 13, but with Supabase as the backend/auth solution. This project will be kept up to date with the main project as an alternative option to PlanetScale/Next Auth. The below is the README from the main project. 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 **Supabase Auth** 35 | - Database on **Supabase** 36 | - UI Components built using **Radix UI** 37 | - Documentation and blog using **MDX** and **Contentlayer** 38 | - Subscriptions using **Stripe** 39 | - Styled using **Tailwind CSS** 40 | - Validations using **Zod** 41 | - Written in **TypeScript** 42 | 43 | ## Roadmap 44 | 45 | - [x] ~Add MDX support for basic pages~ 46 | - [x] ~Build marketing pages~ 47 | - [x] ~Subscriptions using Stripe~ 48 | - [x] ~Responsive styles~ 49 | - [x] ~Add OG image for blog using @vercel/og~ 50 | - [x] Dark mode 51 | 52 | ## Known Issues 53 | 54 | A list of things not working right now: 55 | 56 | 1. ~GitHub authentication (use email)~ 57 | 2. ~[Prisma: Error: ENOENT: no such file or directory, open '/var/task/.next/server/chunks/schema.prisma'](https://github.com/prisma/prisma/issues/16117)~ 58 | 3. ~[Next.js 13: Client side navigation does not update head](https://github.com/vercel/next.js/issues/42414)~ 59 | 4. [Cannot use opengraph-image.tsx inside catch-all routes](https://github.com/vercel/next.js/issues/48162) 60 | 61 | ## Why not tRPC, Turborepo or X? 62 | 63 | I might add this later. For now, I want to see how far we can get using Next.js only. 64 | 65 | If you have some suggestions, feel free to create an issue. 66 | 67 | ## Running Locally 68 | 69 | 1. Install dependencies using pnpm: 70 | 71 | ```sh 72 | pnpm install 73 | ``` 74 | 75 | 2. Copy `.env.example` to `.env.local` and update the variables. 76 | ``` 77 | cp .env.example .env.local 78 | ``` 79 | 80 | 3. Create a Supabase project and copy the environmental variables into `.env.local`. You can follow the [official docs](https://supabase.io/docs/guides/with-nextjs) to get started. 81 | 82 | 4. Copy the `schema.sql` file from the root of this project into your Supabase project's SQL editor and run it to create the tables. 83 | 84 | 5. Start the development server: 85 | 86 | ```sh 87 | pnpm dev 88 | ``` 89 | 90 | ## License 91 | 92 | Licensed under the [MIT license](https://github.com/shadcn/taxonomy/blob/main/LICENSE.md). 93 | -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation" 2 | 3 | import { getUser } from "@/app/supabase-server" 4 | 5 | interface AuthLayoutProps { 6 | children: React.ReactNode 7 | } 8 | 9 | export default async function AuthLayout({ children }: AuthLayoutProps) { 10 | const user = await getUser() 11 | 12 | if (user) { 13 | redirect("/dashboard") 14 | } 15 | return
{guide.description}
46 | )} 47 |50 | {formatDate(guide.date)} 51 |
52 | )} 53 |No guides published.
62 | )} 63 |{page.description}
93 | )} 94 |27 | A blog built using Contentlayer. Posts are written in MDX. 28 |
29 |{post.description}
52 | )} 53 | {post.date && ( 54 |55 | {formatDate(post.date)} 56 |
57 | )} 58 | 59 | View Article 60 | 61 |No posts published.
66 | )} 67 |19 | Unlock all features including unlimited posts for your blog. 20 |
21 |53 | Billed Monthly 54 |
55 |63 | Taxonomy is a demo app.{" "} 64 | You can test the upgrade and won't be charged. 65 |
66 |{text}
} 17 |*]:text-muted-foreground", 89 | className 90 | )} 91 | {...props} 92 | /> 93 | ), 94 | img: ({ 95 | className, 96 | alt, 97 | ...props 98 | }: React.ImgHTMLAttributes) => ( 99 | // eslint-disable-next-line @next/next/no-img-element 100 | 101 | ), 102 | hr: ({ ...props }) =>
, 103 | table: ({ className, ...props }: React.HTMLAttributes) => ( 104 | 105 |107 | ), 108 | tr: ({ className, ...props }: React.HTMLAttributes106 |
) => ( 109 | 113 | ), 114 | th: ({ className, ...props }) => ( 115 | 122 | ), 123 | td: ({ className, ...props }) => ( 124 | 131 | ), 132 | pre: ({ className, ...props }) => ( 133 | 140 | ), 141 | code: ({ className, ...props }) => ( 142 | 149 | ), 150 | Image, 151 | Callout, 152 | Card: MdxCard, 153 | } 154 | 155 | interface MdxProps { 156 | code: string 157 | } 158 | 159 | export function Mdx({ code }: MdxProps) { 160 | const Component = useMDXComponent(code) 161 | 162 | return ( 163 |
164 |166 | ) 167 | } 168 | -------------------------------------------------------------------------------- /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 |165 | 24 |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 |25 | 26 |45 |27 | {siteConfig.name} 28 | 29 | 43 | {children} 44 | 20 | 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 | 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /components/page-header.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | interface DocsPageHeaderProps extends React.HTMLAttributes21 | 26 | 27 |28 | 41 |setTheme("light")}> 29 | 32 |30 | Light 31 | setTheme("dark")}> 33 | 36 |34 | Dark 35 | setTheme("system")}> 37 | 40 |38 | System 39 | { 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 |22 |18 | {heading} 19 |
20 | {text &&{text}
} 21 |
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 |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.useState28 | {pager.prev.title} 29 | 30 | )} 31 | {pager?.next && ( 32 | 36 | {pager.next.title} 37 | 38 | 39 | )} 40 | (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[0].id}`) 58 | } 59 | 60 | return ( 61 | 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /components/post-item.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | 3 | import { Post } from "@/types/main" 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 |30 | ) 31 | } 32 | 33 | PostItem.Skeleton = function PostItemSkeleton() { 34 | return ( 35 |16 | 20 | {post.title} 21 | 22 |28 |23 |27 |24 | {formatDate(new Date(post.created_at).toDateString())} 25 |
26 |29 | 36 |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 "@/types/main" 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: Pick37 |40 |38 | 39 | 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 | 75 |57 | 60 |58 | Open 59 | 61 | 74 |62 | 63 | Edit 64 | 65 | 66 |67 | setShowDeleteAlert(true)} 70 | > 71 | Delete 72 | 73 |76 | 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.HTMLAttributes77 | 111 |78 | 85 |79 | Are you sure you want to delete this post? 80 | 81 |82 | This action cannot be undone. 83 | 84 |86 | 110 |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 | 109 |104 | ) : ( 105 | 106 | )} 107 | Delete 108 | {} 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 |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 |20 |27 | ))} 28 |21 | {item.title} 22 |
23 | {item.items ? ( 24 |25 | ) : null} 26 | 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 |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 | returnxs7 |8 | sm 9 |10 |md11 |lg12 |xl13 |2xl14 |{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 |37 | ) : null 38 | } 39 | 40 | function useActiveItem(itemIds: (string | undefined)[]) { 41 | const [activeId, setActiveId] = React.useStateOn This Page
35 |36 | ("") 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 |
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- 95 | 104 | {item.title} 105 | 106 | {item.items?.length ? ( 107 |
110 | ) 111 | })} 112 |108 | ) : null} 109 | , 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 | 40 | )) 41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName 42 | 43 | const AccordionContent = React.forwardRef< 44 | React.ElementRefsvg]:rotate-180", 32 | className 33 | )} 34 | {...props} 35 | > 36 | {children} 37 | 39 |38 | , 45 | React.ComponentPropsWithoutRef 46 | >(({ className, children, ...props }, ref) => ( 47 | 55 | 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{children}56 |& 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 | 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 |24 | 26 |25 | 19 | 23 | ) 24 | DialogPortal.displayName = DialogPrimitive.Portal.displayName 25 | 26 | const DialogOverlay = React.forwardRef< 27 | React.ElementRef20 | {children} 21 |22 |, 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 | 62 | )) 63 | DialogContent.displayName = DialogPrimitive.Content.displayName 64 | 65 | const DialogHeader = ({ 66 | className, 67 | ...props 68 | }: React.HTMLAttributes47 | 55 | {children} 56 | 61 |57 | 60 |58 | Close 59 | ) => ( 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 | 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.ElementRef27 | , 10 | React.ComponentPropsWithoutRef 11 | >(({ className, value, ...props }, ref) => ( 12 | 20 | 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.ElementRef24 | , 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 | 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.ElementRef37 | 39 |38 | , 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 23 | )) 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef18 | {children} 19 | 20 |21 | 22 | , 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | 43 | 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.ElementRef44 | , 17 | React.ComponentPropsWithoutRef 18 | >(({ className, children, ...props }, ref) => ( 19 | 27 | {children} 28 | 32 | )) 33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName 34 | 35 | const SelectContent = React.forwardRef< 36 | React.ElementRef29 | 31 |30 | , 37 | React.ComponentPropsWithoutRef 38 | >(({ className, children, position = "popper", ...props }, ref) => ( 39 | 40 | 61 | )) 62 | SelectContent.displayName = SelectPrimitive.Content.displayName 63 | 64 | const SelectLabel = React.forwardRef< 65 | React.ElementRef50 | 60 |57 | {children} 58 | 59 |, 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 | 96 | )) 97 | SelectItem.displayName = SelectPrimitive.Item.displayName 98 | 99 | const SelectSeparator = React.forwardRef< 100 | React.ElementRef90 | 92 | 93 | 94 |91 | {children} 95 |, 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 | 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.ElementRef21 | 23 |22 | 24 | , 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 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.ElementRef25 | , 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 | 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.ElementRef21 | 30 | ) 31 | })} 32 |22 | {title &&27 | {action} 28 |{title} } 23 | {description && ( 24 |{description} 25 | )} 26 |29 | 33 | , 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 { useRouter } from "next/navigation" 5 | import { createClientComponentClient } from "@supabase/auth-helpers-nextjs" 6 | 7 | import { Database } from "@/types/db" 8 | import { User } from "@/types/main" 9 | import { 10 | DropdownMenu, 11 | DropdownMenuContent, 12 | DropdownMenuItem, 13 | DropdownMenuSeparator, 14 | DropdownMenuTrigger, 15 | } from "@/components/ui/dropdown-menu" 16 | import { UserAvatar } from "@/components/user-avatar" 17 | 18 | interface UserAccountNavProps extends React.HTMLAttributes { 19 | user: Pick 20 | } 21 | 22 | export function UserAccountNav({ user }: UserAccountNavProps) { 23 | const router = useRouter() 24 | const supabase = createClientComponentClient () 25 | 26 | const handleSignOut = async () => { 27 | await supabase.auth.signOut() 28 | router.refresh() 29 | } 30 | 31 | return ( 32 | 33 | 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /components/user-auth-form.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { createClientComponentClient } from "@supabase/auth-helpers-nextjs" 4 | import { Auth } from "@supabase/auth-ui-react" 5 | 6 | import { Database } from "@/types/db" 7 | import { getURL } from "@/lib/utils" 8 | 9 | export default function UserAuthForm() { 10 | const supabase = createClientComponentClient34 | 39 |38 | 40 | 65 |41 |50 |42 | {user.name &&49 |{user.name}
} 43 | {user.email && ( 44 |45 | {user.email} 46 |
47 | )} 48 |51 | 52 | Dashboard 53 | 54 |55 | Billing 56 | 57 |58 | Settings 59 | 60 |61 | 62 | Sign out 63 | 64 |() 11 | 12 | return ( 13 | 14 |36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /components/user-avatar.tsx: -------------------------------------------------------------------------------- 1 | import { User } from "@/types/main" 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: Pick35 | 9 | } 10 | 11 | export function UserAvatar({ user, ...props }: UserAvatarProps) { 12 | return ( 13 | 14 | {user.image ? ( 15 | 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 "@/types/main" 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.HTMLAttributes16 | ) : ( 17 | 18 | {user.name} 19 | 21 | )} 22 |20 | { 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/dalkommatt/taxonomy-supabase", 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 | stripe_price_id: "", 9 | } 10 | 11 | export const proPlan: SubscriptionPlan = { 12 | name: "PRO", 13 | description: "The PRO plan has unlimited posts.", 14 | stripe_price_id: 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 |74 | ``` 75 | 76 |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 |77 |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 |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 |108 |110 | ) 111 | } 112 | ``` 113 | 114 | Once you've added your custom component, you can use it in MDX as follows: 115 | 116 | ```js 117 |109 | 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 |61 | -------------------------------------------------------------------------------- /content/docs/in-progress.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Not Implemented 3 | description: This page is in progress. 4 | --- 5 | 6 |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 |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 |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 | -------------------------------------------------------------------------------- /contentlayer.config.js: -------------------------------------------------------------------------------- 1 | import { defineDocumentType, makeSource } from "contentlayer/source-files" 2 | import rehypeAutolinkHeadings from "rehype-autolink-headings" 3 | import rehypePrettyCode from "rehype-pretty-code" 4 | import rehypeSlug from "rehype-slug" 5 | import remarkGfm from "remark-gfm" 6 | 7 | /** @type {import('contentlayer/source-files').ComputedFields} */ 8 | const computedFields = { 9 | slug: { 10 | type: "string", 11 | resolve: (doc) => `/${doc._raw.flattenedPath}`, 12 | }, 13 | slugAsParams: { 14 | type: "string", 15 | resolve: (doc) => doc._raw.flattenedPath.split("/").slice(1).join("/"), 16 | }, 17 | } 18 | 19 | export const Doc = defineDocumentType(() => ({ 20 | name: "Doc", 21 | filePathPattern: `docs/**/*.mdx`, 22 | contentType: "mdx", 23 | fields: { 24 | title: { 25 | type: "string", 26 | required: true, 27 | }, 28 | description: { 29 | type: "string", 30 | }, 31 | published: { 32 | type: "boolean", 33 | default: true, 34 | }, 35 | }, 36 | computedFields, 37 | })) 38 | 39 | export const Guide = defineDocumentType(() => ({ 40 | name: "Guide", 41 | filePathPattern: `guides/**/*.mdx`, 42 | contentType: "mdx", 43 | fields: { 44 | title: { 45 | type: "string", 46 | required: true, 47 | }, 48 | description: { 49 | type: "string", 50 | }, 51 | date: { 52 | type: "date", 53 | required: true, 54 | }, 55 | published: { 56 | type: "boolean", 57 | default: true, 58 | }, 59 | featured: { 60 | type: "boolean", 61 | default: false, 62 | }, 63 | }, 64 | computedFields, 65 | })) 66 | 67 | export const Post = defineDocumentType(() => ({ 68 | name: "Post", 69 | filePathPattern: `blog/**/*.mdx`, 70 | contentType: "mdx", 71 | fields: { 72 | title: { 73 | type: "string", 74 | required: true, 75 | }, 76 | description: { 77 | type: "string", 78 | }, 79 | date: { 80 | type: "date", 81 | required: true, 82 | }, 83 | published: { 84 | type: "boolean", 85 | default: true, 86 | }, 87 | image: { 88 | type: "string", 89 | required: true, 90 | }, 91 | authors: { 92 | // Reference types are not embedded. 93 | // Until this is fixed, we can use a simple list. 94 | // type: "reference", 95 | // of: Author, 96 | type: "list", 97 | of: { type: "string" }, 98 | required: true, 99 | }, 100 | }, 101 | computedFields, 102 | })) 103 | 104 | export const Author = defineDocumentType(() => ({ 105 | name: "Author", 106 | filePathPattern: `authors/**/*.mdx`, 107 | contentType: "mdx", 108 | fields: { 109 | title: { 110 | type: "string", 111 | required: true, 112 | }, 113 | description: { 114 | type: "string", 115 | }, 116 | avatar: { 117 | type: "string", 118 | required: true, 119 | }, 120 | twitter: { 121 | type: "string", 122 | required: true, 123 | }, 124 | }, 125 | computedFields, 126 | })) 127 | 128 | export const Page = defineDocumentType(() => ({ 129 | name: "Page", 130 | filePathPattern: `pages/**/*.mdx`, 131 | contentType: "mdx", 132 | fields: { 133 | title: { 134 | type: "string", 135 | required: true, 136 | }, 137 | description: { 138 | type: "string", 139 | }, 140 | }, 141 | computedFields, 142 | })) 143 | 144 | export default makeSource({ 145 | contentDirPath: "./content", 146 | documentTypes: [Page, Doc, Guide, Post, Author], 147 | mdx: { 148 | remarkPlugins: [remarkGfm], 149 | rehypePlugins: [ 150 | rehypeSlug, 151 | [ 152 | rehypePrettyCode, 153 | { 154 | theme: "github-dark", 155 | onVisitLine(node) { 156 | // Prevent lines from collapsing in `display: grid` mode, and allow empty 157 | // lines to be copy/pasted 158 | if (node.children.length === 0) { 159 | node.children = [{ type: "text", value: " " }] 160 | } 161 | }, 162 | onVisitHighlightedLine(node) { 163 | node.properties.className.push("line--highlighted") 164 | }, 165 | onVisitHighlightedWord(node) { 166 | node.properties.className = ["word--highlighted"] 167 | }, 168 | }, 169 | ], 170 | [ 171 | rehypeAutolinkHeadings, 172 | { 173 | properties: { 174 | className: ["subheading-anchor"], 175 | ariaLabel: "Link to section", 176 | }, 177 | }, 178 | ], 179 | ], 180 | }, 181 | }) 182 | -------------------------------------------------------------------------------- /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 | NEXT_PUBLIC_SUPABASE_ANON_KEY: z.string().min(1), 9 | NEXT_PUBLIC_SUPABASE_URL: z.string().url().min(1), 10 | STRIPE_API_KEY: z.string().min(1), 11 | STRIPE_WEBHOOK_SECRET: z.string().min(1), 12 | STRIPE_PRO_MONTHLY_PLAN_ID: z.string().min(1), 13 | GITHUB_ACCESS_TOKEN: z.string().min(1), 14 | }, 15 | client: { 16 | NEXT_PUBLIC_APP_URL: z.string().min(1), 17 | }, 18 | runtimeEnv: { 19 | NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY, 20 | NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL, 21 | STRIPE_API_KEY: process.env.STRIPE_API_KEY, 22 | STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET, 23 | STRIPE_PRO_MONTHLY_PLAN_ID: process.env.STRIPE_PRO_MONTHLY_PLAN_ID, 24 | NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL, 25 | GITHUB_ACCESS_TOKEN: process.env.GITHUB_ACCESS_TOKEN, 26 | }, 27 | }) 28 | -------------------------------------------------------------------------------- /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/exceptions.ts: -------------------------------------------------------------------------------- 1 | export class RequiresProPlanError extends Error { 2 | constructor(message = "This action requires a pro plan") { 3 | super(message) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lib/helpers.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalkommatt/taxonomy-supabase/bfe4dd5de46e250c7c7f1d8cf45c57a7a583bc97/lib/helpers.ts -------------------------------------------------------------------------------- /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 { createServerSupabaseClient } from "@/app/supabase-server" 6 | 7 | export async function getUserSubscriptionPlan( 8 | userId: string 9 | ): Promise23 | 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 |{ 10 | const supabase = createServerSupabaseClient() 11 | const { data: user } = await supabase 12 | .from("users") 13 | .select( 14 | "stripe_subscription_id, stripe_current_period_end, stripe_customer_id, stripe_price_id" 15 | ) 16 | .eq("id", userId) 17 | .single() 18 | if (!user) { 19 | throw new Error("User not found") 20 | } 21 | 22 | // Check if user is on a pro plan. 23 | const isPro = 24 | user.stripe_price_id && 25 | user.stripe_current_period_end?.getTime() + 86_400_000 > Date.now() 26 | 27 | const plan = isPro ? proPlan : freePlan 28 | 29 | return { 30 | ...plan, 31 | ...user, 32 | stripe_current_period_end: user.stripe_current_period_end?.getTime(), 33 | isPro, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /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 | 23 | export const getURL = () => { 24 | let url = 25 | process?.env?.NEXT_PUBLIC_APP_URL ?? // Set this to your site URL in production env. 26 | process?.env?.NEXT_PUBLIC_VERCEL_URL ?? // Automatically set by Vercel. 27 | "http://localhost:3000/" 28 | // Make sure to include `https://` when not localhost. 29 | url = url.includes("http") ? url : `https://${url}` 30 | // Make sure to including trailing `/`. 31 | url = url.charAt(url.length - 1) === "/" ? url : `${url}/` 32 | url = url.concat("auth/callback") 33 | return url 34 | } -------------------------------------------------------------------------------- /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 { NextResponse } from "next/server" 2 | import type { NextRequest } from "next/server" 3 | import { createMiddlewareClient } from "@supabase/auth-helpers-nextjs" 4 | 5 | import type { Database } from "@/types/db" 6 | 7 | export async function middleware(req: NextRequest) { 8 | const res = NextResponse.next() 9 | const supabase = createMiddlewareClient ({ req, res }) 10 | await supabase.auth.getSession() 11 | return res 12 | } 13 | -------------------------------------------------------------------------------- /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", "lh3.googleusercontent.com"], 10 | }, 11 | experimental: { 12 | appDir: true, 13 | }, 14 | } 15 | 16 | export default withContentlayer(nextConfig) 17 | -------------------------------------------------------------------------------- /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 | "generate-types": "npx supabase gen types typescript --project-id [YOUR-PROJECT-REF] --schema public > types/db.ts" 17 | }, 18 | "dependencies": { 19 | "@editorjs/code": "^2.8.0", 20 | "@editorjs/editorjs": "^2.27.0", 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 | "@radix-ui/react-accessible-icon": "^1.0.3", 30 | "@radix-ui/react-accordion": "^1.1.2", 31 | "@radix-ui/react-alert-dialog": "^1.0.4", 32 | "@radix-ui/react-aspect-ratio": "^1.0.3", 33 | "@radix-ui/react-avatar": "^1.0.3", 34 | "@radix-ui/react-checkbox": "^1.0.4", 35 | "@radix-ui/react-collapsible": "^1.0.3", 36 | "@radix-ui/react-context-menu": "^2.1.4", 37 | "@radix-ui/react-dialog": "^1.0.4", 38 | "@radix-ui/react-dropdown-menu": "^2.0.5", 39 | "@radix-ui/react-hover-card": "^1.0.6", 40 | "@radix-ui/react-label": "^2.0.2", 41 | "@radix-ui/react-menubar": "^1.0.3", 42 | "@radix-ui/react-navigation-menu": "^1.1.3", 43 | "@radix-ui/react-popover": "^1.0.6", 44 | "@radix-ui/react-progress": "^1.0.3", 45 | "@radix-ui/react-radio-group": "^1.1.3", 46 | "@radix-ui/react-scroll-area": "^1.0.4", 47 | "@radix-ui/react-select": "^1.2.2", 48 | "@radix-ui/react-separator": "^1.0.3", 49 | "@radix-ui/react-slider": "^1.1.2", 50 | "@radix-ui/react-slot": "^1.0.2", 51 | "@radix-ui/react-switch": "^1.0.3", 52 | "@radix-ui/react-tabs": "^1.0.4", 53 | "@radix-ui/react-toast": "^1.1.4", 54 | "@radix-ui/react-toggle": "^1.0.3", 55 | "@radix-ui/react-toggle-group": "^1.0.4", 56 | "@radix-ui/react-tooltip": "^1.0.6", 57 | "@supabase/auth-helpers-nextjs": "^0.7.1", 58 | "@supabase/auth-ui-react": "^0.4.2", 59 | "@supabase/auth-ui-shared": "^0.1.6", 60 | "@supabase/supabase-js": "^2.24.0", 61 | "@t3-oss/env-nextjs": "^0.4.0", 62 | "@typescript-eslint/parser": "^5.59.8", 63 | "@vercel/analytics": "^1.0.1", 64 | "@vercel/og": "^0.5.6", 65 | "class-variance-authority": "^0.6.0", 66 | "clsx": "^1.2.1", 67 | "cmdk": "^0.2.0", 68 | "concurrently": "^8.0.1", 69 | "contentlayer": "^0.3.3", 70 | "date-fns": "^2.30.0", 71 | "lucide-react": "^0.230.0", 72 | "next": "13.4.4", 73 | "next-contentlayer": "^0.3.3", 74 | "next-themes": "^0.2.1", 75 | "nodemailer": "^6.9.3", 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.44.2", 82 | "react-textarea-autosize": "^8.4.1", 83 | "sharp": "^0.32.1", 84 | "shiki": "^0.14.2", 85 | "stripe": "^12.7.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.5", 92 | "@commitlint/config-conventional": "^17.6.5", 93 | "@ianvs/prettier-plugin-sort-imports": "^4.0.1", 94 | "@tailwindcss/line-clamp": "^0.4.4", 95 | "@tailwindcss/typography": "^0.5.9", 96 | "@types/node": "^20.2.5", 97 | "@types/react": "18.2.7", 98 | "@types/react-dom": "18.2.4", 99 | "autoprefixer": "^10.4.14", 100 | "eslint": "^8.41.0", 101 | "eslint-config-next": "13.4.4", 102 | "eslint-config-prettier": "^8.8.0", 103 | "eslint-plugin-react": "^7.32.2", 104 | "eslint-plugin-tailwindcss": "^3.12.1", 105 | "husky": "^8.0.3", 106 | "mdast-util-toc": "^6.1.1", 107 | "postcss": "^8.4.24", 108 | "prettier": "^2.8.8", 109 | "prettier-plugin-tailwindcss": "^0.3.0", 110 | "pretty-quick": "^3.1.3", 111 | "rehype": "^12.0.1", 112 | "rehype-autolink-headings": "^6.1.1", 113 | "rehype-pretty-code": "^0.9.6", 114 | "rehype-slug": "^5.1.0", 115 | "remark": "^14.0.3", 116 | "remark-gfm": "^3.0.1", 117 | "supabase": "^1.64.8", 118 | "tailwindcss": "^3.3.2", 119 | "typescript": "5.0.4", 120 | "unist-util-visit": "^4.1.2" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalkommatt/taxonomy-supabase/bfe4dd5de46e250c7c7f1d8cf45c57a7a583bc97/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalkommatt/taxonomy-supabase/bfe4dd5de46e250c7c7f1d8cf45c57a7a583bc97/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalkommatt/taxonomy-supabase/bfe4dd5de46e250c7c7f1d8cf45c57a7a583bc97/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalkommatt/taxonomy-supabase/bfe4dd5de46e250c7c7f1d8cf45c57a7a583bc97/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalkommatt/taxonomy-supabase/bfe4dd5de46e250c7c7f1d8cf45c57a7a583bc97/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalkommatt/taxonomy-supabase/bfe4dd5de46e250c7c7f1d8cf45c57a7a583bc97/public/favicon.ico -------------------------------------------------------------------------------- /public/images/avatars/shadcn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalkommatt/taxonomy-supabase/bfe4dd5de46e250c7c7f1d8cf45c57a7a583bc97/public/images/avatars/shadcn.png -------------------------------------------------------------------------------- /public/images/blog/blog-post-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalkommatt/taxonomy-supabase/bfe4dd5de46e250c7c7f1d8cf45c57a7a583bc97/public/images/blog/blog-post-1.jpg -------------------------------------------------------------------------------- /public/images/blog/blog-post-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalkommatt/taxonomy-supabase/bfe4dd5de46e250c7c7f1d8cf45c57a7a583bc97/public/images/blog/blog-post-2.jpg -------------------------------------------------------------------------------- /public/images/blog/blog-post-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalkommatt/taxonomy-supabase/bfe4dd5de46e250c7c7f1d8cf45c57a7a583bc97/public/images/blog/blog-post-3.jpg -------------------------------------------------------------------------------- /public/images/blog/blog-post-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalkommatt/taxonomy-supabase/bfe4dd5de46e250c7c7f1d8cf45c57a7a583bc97/public/images/blog/blog-post-4.jpg -------------------------------------------------------------------------------- /public/images/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalkommatt/taxonomy-supabase/bfe4dd5de46e250c7c7f1d8cf45c57a7a583bc97/public/images/hero.png -------------------------------------------------------------------------------- /public/og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalkommatt/taxonomy-supabase/bfe4dd5de46e250c7c7f1d8cf45c57a7a583bc97/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 | -------------------------------------------------------------------------------- /schema.sql: -------------------------------------------------------------------------------- 1 | /** 2 | * USERS 3 | * Note: This table contains user data. Users should only be able to view and update their own data. 4 | */ 5 | create table users ( 6 | -- UUID from auth.users 7 | id uuid references auth.users(id) not null primary key, 8 | name text, 9 | email text, 10 | email_verified timestamp with time zone, 11 | image text, 12 | created_at timestamp with time zone, 13 | updated_at timestamp with time zone, 14 | stripe_customer_id text, 15 | stripe_subscription_id text, 16 | stripe_price_id text, 17 | stripe_current_period_end timestamp with time zone 18 | ); 19 | alter table users add constraint unique_email unique (email); 20 | alter table users add constraint unique_stripe_customer_id unique (stripe_customer_id); 21 | alter table users add constraint unique_stripe_subscription_id unique (stripe_subscription_id); 22 | alter table users enable row level security; 23 | create policy "Users can view own user data." on users for select using (auth.uid() = id); 24 | create policy "Users can update own user data." on users for update using (auth.uid() = id); 25 | 26 | /** 27 | * This trigger automatically creates a user entry when a new user signs up via Supabase Auth. 28 | */ 29 | create function public.handle_new_user() 30 | returns trigger as $$ 31 | begin 32 | insert into public.users (id, email, name, image, created_at) 33 | values (new.id, new.email, new.raw_user_meta_data->>'full_name', new.raw_user_meta_data->>'avatar_url', new.created_at); 34 | return new; 35 | end; 36 | $$ language plpgsql security definer; 37 | create trigger on_auth_user_created 38 | after insert on auth.users 39 | for each row execute procedure public.handle_new_user(); 40 | 41 | /** 42 | * This trigger automatically creates a user entry when a new user signs up via Supabase Auth. 43 | */ 44 | 45 | create function public.update_public_user_info() 46 | returns trigger as $$ 47 | begin 48 | update public.users 49 | set email_verified = new.email_confirmed_at, updated_at = new.updated_at, name = new.raw_user_meta_data->>'full_name' 50 | where id = new.id; 51 | return new; 52 | end; 53 | $$ language plpgsql security definer; 54 | 55 | create trigger on_auth_user_updated 56 | after update on auth.users 57 | for each row execute procedure public.update_public_user_info(); 58 | 59 | /** 60 | * POSTS 61 | * Note: this is a private table that contains a mapping of user IDs to author IDs. 62 | */ 63 | 64 | create table posts ( 65 | id uuid default uuid_generate_v4() not null primary key, 66 | title text not null, 67 | content jsonb, 68 | published boolean default false not null, 69 | created_at timestamp with time zone default timezone('utc'::text, now()) not null, 70 | updated_at timestamp with time zone default timezone('utc'::text, now()) not null, 71 | author_id uuid references public.users(id) not null 72 | ); 73 | alter table posts enable row level security; 74 | create policy "Anyone can read posts." on posts for select using (true); 75 | create policy "Authorized users can insert posts." on posts for insert to authenticated with check (true); 76 | create policy "Users update own posts." on posts for update using (auth.uid() = author_id); 77 | create policy "Users delete own posts." on posts for delete using (auth.uid() = author_id); 78 | -------------------------------------------------------------------------------- /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/db.ts: -------------------------------------------------------------------------------- 1 | export type Json = 2 | | string 3 | | number 4 | | boolean 5 | | null 6 | | { [key: string]: Json } 7 | | Json[] 8 | 9 | export interface Database { 10 | public: { 11 | Tables: { 12 | posts: { 13 | Row: { 14 | author_id: string 15 | content: Json | null 16 | created_at: string 17 | id: string 18 | published: boolean 19 | title: string 20 | updated_at: string 21 | } 22 | Insert: { 23 | author_id: string 24 | content?: Json | null 25 | created_at?: string 26 | id?: string 27 | published?: boolean 28 | title: string 29 | updated_at?: string 30 | } 31 | Update: { 32 | author_id?: string 33 | content?: Json | null 34 | created_at?: string 35 | id?: string 36 | published?: boolean 37 | title?: string 38 | updated_at?: string 39 | } 40 | } 41 | users: { 42 | Row: { 43 | created_at: string | null 44 | email: string | null 45 | email_verified: string | null 46 | id: string 47 | image: string | null 48 | name: string | null 49 | stripe_current_period_end: string | null 50 | stripe_customer_id: string | null 51 | stripe_price_id: string | null 52 | stripe_subscription_id: string | null 53 | updated_at: string | null 54 | } 55 | Insert: { 56 | created_at?: string | null 57 | email?: string | null 58 | email_verified?: string | null 59 | id: string 60 | image?: string | null 61 | name?: string | null 62 | stripe_current_period_end?: string | null 63 | stripe_customer_id?: string | null 64 | stripe_price_id?: string | null 65 | stripe_subscription_id?: string | null 66 | updated_at?: string | null 67 | } 68 | Update: { 69 | created_at?: string | null 70 | email?: string | null 71 | email_verified?: string | null 72 | id?: string 73 | image?: string | null 74 | name?: string | null 75 | stripe_current_period_end?: string | null 76 | stripe_customer_id?: string | null 77 | stripe_price_id?: string | null 78 | stripe_subscription_id?: string | null 79 | updated_at?: string | null 80 | } 81 | } 82 | } 83 | Views: { 84 | [_ in never]: never 85 | } 86 | Functions: { 87 | [_ in never]: never 88 | } 89 | Enums: { 90 | [_ in never]: never 91 | } 92 | CompositeTypes: { 93 | [_ in never]: never 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { User } from "./main" 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 | stripe_price_id: string 59 | } 60 | 61 | export type UserSubscriptionPlan = SubscriptionPlan & 62 | Pick & { 63 | stripe_current_period_end: number 64 | isPro: boolean 65 | } 66 | -------------------------------------------------------------------------------- /types/main.ts: -------------------------------------------------------------------------------- 1 | export interface PageMeta { 2 | title: string 3 | description: string 4 | cardImage: string 5 | url: string 6 | robots?: string 7 | favicon?: string 8 | type?: string 9 | } 10 | 11 | export interface User { 12 | created_at: string | null 13 | email: string | null | undefined 14 | email_verified: string | null 15 | id: string 16 | image: string | null 17 | name: string | null 18 | stripe_current_period_end: string | null 19 | stripe_customer_id: string | null 20 | stripe_price_id: string | null 21 | stripe_subscription_id: string | null 22 | updated_at: string | null 23 | } 24 | 25 | export interface Post { 26 | id: string /* primary key */ 27 | title: string 28 | content: JSON | null 29 | published: boolean 30 | created_at: string 31 | updated_at: string 32 | author_id: string 33 | } 34 | --------------------------------------------------------------------------------