├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── app ├── favicon.ico ├── globals.css ├── layout.tsx └── page.tsx ├── components.json ├── components ├── auth │ └── loading.tsx └── ui │ ├── button.tsx │ ├── dialog.tsx │ ├── input.tsx │ ├── popover.tsx │ └── sonner.tsx ├── convex ├── README.md ├── _generated │ ├── api.d.ts │ ├── api.js │ ├── dataModel.d.ts │ ├── server.d.ts │ └── server.js ├── auth.config.ts ├── http.ts ├── schema.ts ├── stripe.ts ├── tsconfig.json └── users.ts ├── lib └── utils.ts ├── middleware.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── providers └── convex-client-provider.tsx ├── public ├── logo.svg ├── next.svg ├── thumbnail.png └── vercel.svg ├── tailwind.config.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Vuk Rosić 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 | This is code for the following tutorial: 2 | 3 | ![ChatGPT Clone](https://github.com/vukrosic/setup-nextjs-convex-clerk-typescript-stripe-shadcn-tailwindcss-react/blob/main/public/thumbnail.png?raw=true) 4 | 5 | [VIDEO TUTORIAL](https://youtu.be/bTY0fa8p8D0?si=hPgOec1M7xjJ4rvB) 6 | 7 | 8 | If you want to run this I recommend following the tutorial. You can also try the following: 9 | 10 | ## Required: 11 | Node version 14.x 12 | 13 | ## Clone this repo 14 | ```bash 15 | git clone https://github.com/vukrosic/setup-nextjs-convex-clerk-typescript-stripe-shadcn-tailwindcss-react 16 | ``` 17 | 18 | ## Install packages 19 | ```bash 20 | cd nextjs14-chatgpt & npm install 21 | ``` 22 | 23 | ## Run Convex 24 | ```bash 25 | npx convex dev 26 | ``` 27 | 28 | You will need to setup Clerk, Convex and other things. Don't forget to add your variables to convex website as well. Here is example .env.local file: 29 | 30 | ```env 31 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 32 | CLERK_SECRET_KEY= 33 | 34 | NEXT_PUBLIC_HOSTING_URL=http://localhost:3000 35 | 36 | NEXT_STRIPE_PUBLISHABLE_KEY= 37 | NEXT_STRIPE_SECRET_KEY= 38 | STRIPE_ONE_TIME_PRICE_ID= 39 | STRIPE_WEBHOOK_SECRET= 40 | ``` 41 | 42 | ## Run the app 43 | ```bash 44 | npm run dev 45 | ``` 46 | 47 | At this point, you application should work, but it probably doesn't. You may check timesteps in the video description or try to read and solve errors by yourself. You can also ask in comments. 48 | 49 | 50 | 51 | 52 | 53 | # Next JS documentation below 54 | 55 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 56 | 57 | ## Getting Started 58 | 59 | First, run the development server: 60 | 61 | ```bash 62 | npm run dev 63 | # or 64 | yarn dev 65 | # or 66 | pnpm dev 67 | # or 68 | bun dev 69 | ``` 70 | 71 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 72 | 73 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 74 | 75 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 76 | 77 | ## Learn More 78 | 79 | To learn more about Next.js, take a look at the following resources: 80 | 81 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 82 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 83 | 84 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 85 | 86 | ## Deploy on Vercel 87 | 88 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 89 | 90 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 91 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vukrosic/setup-nextjs-convex-clerk-typescript-stripe-shadcn-tailwindcss-react/3f71a1086fd851f47f07bb29167125cbf955efb4/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root, 6 | html, 7 | body { 8 | height: 100%; 9 | } 10 | 11 | @layer base { 12 | :root { 13 | --background: 0 0% 100%; 14 | --foreground: 222.2 84% 4.9%; 15 | 16 | --card: 0 0% 100%; 17 | --card-foreground: 222.2 84% 4.9%; 18 | 19 | --popover: 0 0% 100%; 20 | --popover-foreground: 222.2 84% 4.9%; 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 | --muted: 210 40% 96.1%; 29 | --muted-foreground: 215.4 16.3% 46.9%; 30 | 31 | --accent: 210 40% 96.1%; 32 | --accent-foreground: 222.2 47.4% 11.2%; 33 | 34 | --destructive: 0 84.2% 60.2%; 35 | --destructive-foreground: 210 40% 98%; 36 | 37 | --border: 214.3 31.8% 91.4%; 38 | --input: 214.3 31.8% 91.4%; 39 | --ring: 222.2 84% 4.9%; 40 | 41 | --radius: 0.5rem; 42 | } 43 | 44 | .dark { 45 | --background: 222.2 84% 4.9%; 46 | --foreground: 210 40% 98%; 47 | 48 | --card: 222.2 84% 4.9%; 49 | --card-foreground: 210 40% 98%; 50 | 51 | --popover: 222.2 84% 4.9%; 52 | --popover-foreground: 210 40% 98%; 53 | 54 | --primary: 210 40% 98%; 55 | --primary-foreground: 222.2 47.4% 11.2%; 56 | 57 | --secondary: 217.2 32.6% 17.5%; 58 | --secondary-foreground: 210 40% 98%; 59 | 60 | --muted: 217.2 32.6% 17.5%; 61 | --muted-foreground: 215 20.2% 65.1%; 62 | 63 | --accent: 217.2 32.6% 17.5%; 64 | --accent-foreground: 210 40% 98%; 65 | 66 | --destructive: 0 62.8% 30.6%; 67 | --destructive-foreground: 210 40% 98%; 68 | 69 | --border: 217.2 32.6% 17.5%; 70 | --input: 217.2 32.6% 17.5%; 71 | --ring: 212.7 26.8% 83.9%; 72 | } 73 | } 74 | 75 | @layer base { 76 | * { 77 | @apply border-border; 78 | } 79 | 80 | body { 81 | @apply bg-background text-foreground; 82 | } 83 | } -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import { ConvexClientProvider } from "@/providers/convex-client-provider"; 5 | import { Toaster } from "sonner"; 6 | 7 | const inter = Inter({ subsets: ["latin"] }); 8 | 9 | export const metadata: Metadata = { 10 | title: "Create Next App", 11 | description: "Generated by create next app", 12 | }; 13 | 14 | export default function RootLayout({ 15 | children, 16 | }: Readonly<{ 17 | children: React.ReactNode; 18 | }>) { 19 | return ( 20 | 21 | 22 | 23 | 24 | {children} 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { api } from "@/convex/_generated/api"; 5 | import { useAction, useMutation } from "convex/react"; 6 | import Image from "next/image"; 7 | import { useRouter } from "next/navigation"; 8 | import { useEffect } from "react"; 9 | import { toast } from "sonner"; 10 | 11 | export default function Home() { 12 | const storeUser = useMutation(api.users.store); 13 | 14 | const upgrade = useAction(api.stripe.pay); 15 | const router = useRouter(); 16 | 17 | useEffect(() => { 18 | storeUser({}); 19 | }); 20 | 21 | 22 | const handleBuy = async () => { 23 | const url = await upgrade({}); 24 | if (!url) return; 25 | router.push(url); 26 | } 27 | 28 | 29 | return ( 30 |
31 | Hello world 32 | 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /components/auth/loading.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image" 2 | 3 | export const Loading = () => { 4 | return ( 5 |
6 | logo 13 |
14 | ) 15 | } -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /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 = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogClose, 116 | DialogTrigger, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /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/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Popover = PopoverPrimitive.Root 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )) 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 30 | 31 | export { Popover, PopoverTrigger, PopoverContent } 32 | -------------------------------------------------------------------------------- /components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTheme } from "next-themes" 4 | import { Toaster as Sonner } from "sonner" 5 | 6 | type ToasterProps = React.ComponentProps 7 | 8 | const Toaster = ({ ...props }: ToasterProps) => { 9 | const { theme = "system" } = useTheme() 10 | 11 | return ( 12 | 28 | ) 29 | } 30 | 31 | export { Toaster } 32 | -------------------------------------------------------------------------------- /convex/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your Convex functions directory! 2 | 3 | Write your Convex functions here. 4 | See https://docs.convex.dev/functions for more. 5 | 6 | A query function that takes two arguments looks like: 7 | 8 | ```ts 9 | // functions.js 10 | import { query } from "./_generated/server"; 11 | import { v } from "convex/values"; 12 | 13 | export const myQueryFunction = query({ 14 | // Validators for arguments. 15 | args: { 16 | first: v.number(), 17 | second: v.string(), 18 | }, 19 | 20 | // Function implementation. 21 | handler: async (ctx, args) => { 22 | // Read the database as many times as you need here. 23 | // See https://docs.convex.dev/database/reading-data. 24 | const documents = await ctx.db.query("tablename").collect(); 25 | 26 | // Arguments passed from the client are properties of the args object. 27 | console.log(args.first, args.second); 28 | 29 | // Write arbitrary JavaScript here: filter, aggregate, build derived data, 30 | // remove non-public properties, or create new objects. 31 | return documents; 32 | }, 33 | }); 34 | ``` 35 | 36 | Using this query function in a React component looks like: 37 | 38 | ```ts 39 | const data = useQuery(api.functions.myQueryFunction, { 40 | first: 10, 41 | second: "hello", 42 | }); 43 | ``` 44 | 45 | A mutation function looks like: 46 | 47 | ```ts 48 | // functions.js 49 | import { mutation } from "./_generated/server"; 50 | import { v } from "convex/values"; 51 | 52 | export const myMutationFunction = mutation({ 53 | // Validators for arguments. 54 | args: { 55 | first: v.string(), 56 | second: v.string(), 57 | }, 58 | 59 | // Function implementation. 60 | handler: async (ctx, args) => { 61 | // Insert or modify documents in the database here. 62 | // Mutations can also read from the database like queries. 63 | // See https://docs.convex.dev/database/writing-data. 64 | const message = { body: args.first, author: args.second }; 65 | const id = await ctx.db.insert("messages", message); 66 | 67 | // Optionally, return a value from your mutation. 68 | return await ctx.db.get(id); 69 | }, 70 | }); 71 | ``` 72 | 73 | Using this mutation function in a React component looks like: 74 | 75 | ```ts 76 | const mutation = useMutation(api.functions.myMutationFunction); 77 | function handleButtonPress() { 78 | // fire and forget, the most common way to use mutations 79 | mutation({ first: "Hello!", second: "me" }); 80 | // OR 81 | // use the result once the mutation has completed 82 | mutation({ first: "Hello!", second: "me" }).then((result) => 83 | console.log(result) 84 | ); 85 | } 86 | ``` 87 | 88 | Use the Convex CLI to push your functions to a deployment. See everything 89 | the Convex CLI can do by running `npx convex -h` in your project root 90 | directory. To learn more, launch the docs with `npx convex docs`. 91 | -------------------------------------------------------------------------------- /convex/_generated/api.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.10.0. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import type { 13 | ApiFromModules, 14 | FilterApi, 15 | FunctionReference, 16 | } from "convex/server"; 17 | import type * as http from "../http.js"; 18 | import type * as stripe from "../stripe.js"; 19 | import type * as users from "../users.js"; 20 | 21 | /** 22 | * A utility for referencing Convex functions in your app's API. 23 | * 24 | * Usage: 25 | * ```js 26 | * const myFunctionReference = api.myModule.myFunction; 27 | * ``` 28 | */ 29 | declare const fullApi: ApiFromModules<{ 30 | http: typeof http; 31 | stripe: typeof stripe; 32 | users: typeof users; 33 | }>; 34 | export declare const api: FilterApi< 35 | typeof fullApi, 36 | FunctionReference 37 | >; 38 | export declare const internal: FilterApi< 39 | typeof fullApi, 40 | FunctionReference 41 | >; 42 | -------------------------------------------------------------------------------- /convex/_generated/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.10.0. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { anyApi } from "convex/server"; 13 | 14 | /** 15 | * A utility for referencing Convex functions in your app's API. 16 | * 17 | * Usage: 18 | * ```js 19 | * const myFunctionReference = api.myModule.myFunction; 20 | * ``` 21 | */ 22 | export const api = anyApi; 23 | export const internal = anyApi; 24 | -------------------------------------------------------------------------------- /convex/_generated/dataModel.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated data model types. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.10.0. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import type { 13 | DataModelFromSchemaDefinition, 14 | DocumentByName, 15 | TableNamesInDataModel, 16 | SystemTableNames, 17 | } from "convex/server"; 18 | import type { GenericId } from "convex/values"; 19 | import schema from "../schema.js"; 20 | 21 | /** 22 | * The names of all of your Convex tables. 23 | */ 24 | export type TableNames = TableNamesInDataModel; 25 | 26 | /** 27 | * The type of a document stored in Convex. 28 | * 29 | * @typeParam TableName - A string literal type of the table name (like "users"). 30 | */ 31 | export type Doc = DocumentByName< 32 | DataModel, 33 | TableName 34 | >; 35 | 36 | /** 37 | * An identifier for a document in Convex. 38 | * 39 | * Convex documents are uniquely identified by their `Id`, which is accessible 40 | * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). 41 | * 42 | * Documents can be loaded using `db.get(id)` in query and mutation functions. 43 | * 44 | * IDs are just strings at runtime, but this type can be used to distinguish them from other 45 | * strings when type checking. 46 | * 47 | * @typeParam TableName - A string literal type of the table name (like "users"). 48 | */ 49 | export type Id = 50 | GenericId; 51 | 52 | /** 53 | * A type describing your Convex data model. 54 | * 55 | * This type includes information about what tables you have, the type of 56 | * documents stored in those tables, and the indexes defined on them. 57 | * 58 | * This type is used to parameterize methods like `queryGeneric` and 59 | * `mutationGeneric` to make them type-safe. 60 | */ 61 | export type DataModel = DataModelFromSchemaDefinition; 62 | -------------------------------------------------------------------------------- /convex/_generated/server.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.10.0. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { 13 | ActionBuilder, 14 | HttpActionBuilder, 15 | MutationBuilder, 16 | QueryBuilder, 17 | GenericActionCtx, 18 | GenericMutationCtx, 19 | GenericQueryCtx, 20 | GenericDatabaseReader, 21 | GenericDatabaseWriter, 22 | } from "convex/server"; 23 | import type { DataModel } from "./dataModel.js"; 24 | 25 | /** 26 | * Define a query in this Convex app's public API. 27 | * 28 | * This function will be allowed to read your Convex database and will be accessible from the client. 29 | * 30 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 31 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 32 | */ 33 | export declare const query: QueryBuilder; 34 | 35 | /** 36 | * Define a query that is only accessible from other Convex functions (but not from the client). 37 | * 38 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 39 | * 40 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 41 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 42 | */ 43 | export declare const internalQuery: QueryBuilder; 44 | 45 | /** 46 | * Define a mutation in this Convex app's public API. 47 | * 48 | * This function will be allowed to modify your Convex database and will be accessible from the client. 49 | * 50 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 51 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 52 | */ 53 | export declare const mutation: MutationBuilder; 54 | 55 | /** 56 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 57 | * 58 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 59 | * 60 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 61 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 62 | */ 63 | export declare const internalMutation: MutationBuilder; 64 | 65 | /** 66 | * Define an action in this Convex app's public API. 67 | * 68 | * An action is a function which can execute any JavaScript code, including non-deterministic 69 | * code and code with side-effects, like calling third-party services. 70 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 71 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 72 | * 73 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 74 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 75 | */ 76 | export declare const action: ActionBuilder; 77 | 78 | /** 79 | * Define an action that is only accessible from other Convex functions (but not from the client). 80 | * 81 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 82 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 83 | */ 84 | export declare const internalAction: ActionBuilder; 85 | 86 | /** 87 | * Define an HTTP action. 88 | * 89 | * This function will be used to respond to HTTP requests received by a Convex 90 | * deployment if the requests matches the path and method where this action 91 | * is routed. Be sure to route your action in `convex/http.js`. 92 | * 93 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 94 | * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. 95 | */ 96 | export declare const httpAction: HttpActionBuilder; 97 | 98 | /** 99 | * A set of services for use within Convex query functions. 100 | * 101 | * The query context is passed as the first argument to any Convex query 102 | * function run on the server. 103 | * 104 | * This differs from the {@link MutationCtx} because all of the services are 105 | * read-only. 106 | */ 107 | export type QueryCtx = GenericQueryCtx; 108 | 109 | /** 110 | * A set of services for use within Convex mutation functions. 111 | * 112 | * The mutation context is passed as the first argument to any Convex mutation 113 | * function run on the server. 114 | */ 115 | export type MutationCtx = GenericMutationCtx; 116 | 117 | /** 118 | * A set of services for use within Convex action functions. 119 | * 120 | * The action context is passed as the first argument to any Convex action 121 | * function run on the server. 122 | */ 123 | export type ActionCtx = GenericActionCtx; 124 | 125 | /** 126 | * An interface to read from the database within Convex query functions. 127 | * 128 | * The two entry points are {@link DatabaseReader.get}, which fetches a single 129 | * document by its {@link Id}, or {@link DatabaseReader.query}, which starts 130 | * building a query. 131 | */ 132 | export type DatabaseReader = GenericDatabaseReader; 133 | 134 | /** 135 | * An interface to read from and write to the database within Convex mutation 136 | * functions. 137 | * 138 | * Convex guarantees that all writes within a single mutation are 139 | * executed atomically, so you never have to worry about partial writes leaving 140 | * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) 141 | * for the guarantees Convex provides your functions. 142 | */ 143 | export type DatabaseWriter = GenericDatabaseWriter; 144 | -------------------------------------------------------------------------------- /convex/_generated/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * Generated by convex@1.10.0. 8 | * To regenerate, run `npx convex dev`. 9 | * @module 10 | */ 11 | 12 | import { 13 | actionGeneric, 14 | httpActionGeneric, 15 | queryGeneric, 16 | mutationGeneric, 17 | internalActionGeneric, 18 | internalMutationGeneric, 19 | internalQueryGeneric, 20 | } from "convex/server"; 21 | 22 | /** 23 | * Define a query in this Convex app's public API. 24 | * 25 | * This function will be allowed to read your Convex database and will be accessible from the client. 26 | * 27 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 28 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 29 | */ 30 | export const query = queryGeneric; 31 | 32 | /** 33 | * Define a query that is only accessible from other Convex functions (but not from the client). 34 | * 35 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 36 | * 37 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 38 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 39 | */ 40 | export const internalQuery = internalQueryGeneric; 41 | 42 | /** 43 | * Define a mutation in this Convex app's public API. 44 | * 45 | * This function will be allowed to modify your Convex database and will be accessible from the client. 46 | * 47 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 48 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 49 | */ 50 | export const mutation = mutationGeneric; 51 | 52 | /** 53 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 54 | * 55 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 56 | * 57 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 58 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 59 | */ 60 | export const internalMutation = internalMutationGeneric; 61 | 62 | /** 63 | * Define an action in this Convex app's public API. 64 | * 65 | * An action is a function which can execute any JavaScript code, including non-deterministic 66 | * code and code with side-effects, like calling third-party services. 67 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 68 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 69 | * 70 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 71 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 72 | */ 73 | export const action = actionGeneric; 74 | 75 | /** 76 | * Define an action that is only accessible from other Convex functions (but not from the client). 77 | * 78 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 79 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 80 | */ 81 | export const internalAction = internalActionGeneric; 82 | 83 | /** 84 | * Define a Convex HTTP action. 85 | * 86 | * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object 87 | * as its second. 88 | * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`. 89 | */ 90 | export const httpAction = httpActionGeneric; 91 | -------------------------------------------------------------------------------- /convex/auth.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | providers: [ 3 | { 4 | domain: "https://oriented-whippet-31.clerk.accounts.dev/", 5 | applicationID: "convex", 6 | }, 7 | ] 8 | }; -------------------------------------------------------------------------------- /convex/http.ts: -------------------------------------------------------------------------------- 1 | import { httpRouter } from "convex/server"; 2 | import { httpAction } from "./_generated/server"; 3 | import { internal } from "./_generated/api"; 4 | 5 | const http = httpRouter(); 6 | 7 | http.route({ 8 | path: "/stripe", 9 | method: "POST", 10 | handler: httpAction(async (ctx, request) => { 11 | const signature: string = request.headers.get("stripe-signature") as string; 12 | const result = await ctx.runAction(internal.stripe.fulfill, { 13 | signature, 14 | payload: await request.text(), 15 | }); 16 | if (result.success) { 17 | return new Response(null, { 18 | status: 200, 19 | }); 20 | } else { 21 | return new Response("Webhook Error", { 22 | status: 400, 23 | }); 24 | } 25 | }), 26 | }); 27 | 28 | export default http; -------------------------------------------------------------------------------- /convex/schema.ts: -------------------------------------------------------------------------------- 1 | import { defineSchema, defineTable } from "convex/server"; 2 | import { v } from "convex/values"; 3 | 4 | export default defineSchema({ 5 | users: defineTable({ 6 | tokenIdentifier: v.string(), 7 | email: v.string(), 8 | endsOn: v.optional(v.number()), 9 | subscriptionId: v.optional(v.string()), 10 | }) 11 | .index("by_token", ["tokenIdentifier"]) 12 | .index("by_subscriptionId", ["subscriptionId"]), 13 | }); -------------------------------------------------------------------------------- /convex/stripe.ts: -------------------------------------------------------------------------------- 1 | import { action, internalAction } from "./_generated/server"; 2 | import { api, internal } from "./_generated/api"; 3 | import Stripe from 'stripe'; 4 | import { Id } from "./_generated/dataModel"; 5 | import { v } from "convex/values"; 6 | 7 | export const pay = action({ 8 | args: {}, 9 | handler: async (ctx) => { 10 | const clerkUser = await ctx.auth.getUserIdentity(); 11 | const user = await ctx.runQuery(api.users.currentUser, {}); 12 | 13 | if (!user || !clerkUser) { 14 | throw new Error("User not authenticated!"); 15 | } 16 | 17 | if (!clerkUser.emailVerified) { 18 | throw new Error("User email not verified!"); 19 | } 20 | 21 | const stripe = new Stripe(process.env.NEXT_STRIPE_SECRET_KEY!, { 22 | apiVersion: "2023-10-16" 23 | }); 24 | 25 | const domain = process.env.NEXT_PUBLIC_HOSTING_URL!; 26 | 27 | const session: Stripe.Response = await stripe.checkout.sessions.create( 28 | { 29 | mode: "subscription", 30 | line_items: [ 31 | { 32 | price: process.env.STRIPE_ONE_TIME_PRICE_ID!, 33 | quantity: 1, 34 | }, 35 | ], 36 | customer_email: clerkUser.email, 37 | metadata: { 38 | userId: user._id, 39 | }, 40 | success_url: `${domain}`, 41 | cancel_url: `${domain}`, 42 | } 43 | ); 44 | return session.url; 45 | }, 46 | }); 47 | 48 | 49 | type Metadata = { 50 | userId: Id<"users">; 51 | } 52 | 53 | export const fulfill = internalAction({ 54 | args: { signature: v.string(), payload: v.string() }, 55 | handler: async ({ runQuery, runMutation }, { signature, payload }) => { 56 | const stripe = new Stripe(process.env.NEXT_STRIPE_SECRET_KEY!, { 57 | apiVersion: "2023-10-16", 58 | }); 59 | 60 | const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET as string; 61 | try { 62 | const event = await stripe.webhooks.constructEventAsync( 63 | payload, 64 | signature, 65 | webhookSecret 66 | ); 67 | const completedEvent = event.data.object as Stripe.Checkout.Session & { 68 | metadata: Metadata; 69 | } 70 | 71 | if (event.type === "checkout.session.completed") { 72 | const subscription = await stripe.subscriptions.retrieve( 73 | completedEvent.subscription as string 74 | ) 75 | 76 | const userId = completedEvent.metadata.userId; 77 | 78 | await runMutation(internal.users.updateSubscription, { 79 | userId, 80 | subscriptionId: subscription.id, 81 | endsOn: subscription.current_period_end * 1000, 82 | }); 83 | } 84 | 85 | if (event.type === "invoice.payment_succeeded") { 86 | const subscription = await stripe.subscriptions.retrieve( 87 | completedEvent.subscription as string 88 | ); 89 | 90 | await runMutation(internal.users.updateSubscriptionById, { 91 | subscriptionId: subscription.id, 92 | endsOn: subscription.current_period_end * 1000, 93 | }); 94 | } 95 | 96 | return { success: true }; 97 | } catch (error) { 98 | console.log(error); 99 | return { success: false, error: (error as { message: string }).message }; 100 | } 101 | }, 102 | }); -------------------------------------------------------------------------------- /convex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* This TypeScript project config describes the environment that 3 | * Convex functions run in and is used to typecheck them. 4 | * You can modify it, but some settings required to use Convex. 5 | */ 6 | "compilerOptions": { 7 | /* These settings are not required by Convex and can be modified. */ 8 | "allowJs": true, 9 | "strict": true, 10 | 11 | /* These compiler options are required by Convex */ 12 | "target": "ESNext", 13 | "lib": ["ES2021", "dom"], 14 | "forceConsistentCasingInFileNames": true, 15 | "allowSyntheticDefaultImports": true, 16 | "module": "ESNext", 17 | "moduleResolution": "Node", 18 | "isolatedModules": true, 19 | "skipLibCheck": true, 20 | "noEmit": true 21 | }, 22 | "include": ["./**/*"], 23 | "exclude": ["./_generated"] 24 | } 25 | -------------------------------------------------------------------------------- /convex/users.ts: -------------------------------------------------------------------------------- 1 | import { v } from "convex/values"; 2 | import { internalMutation, mutation, query } from "./_generated/server"; 3 | 4 | export const store = mutation({ 5 | args: {}, 6 | handler: async (ctx) => { 7 | const identity = await ctx.auth.getUserIdentity(); 8 | if (!identity) { 9 | throw new Error("Called storeUser without authenticated user"); 10 | } 11 | 12 | // check if user is already stored 13 | const user = await ctx.db 14 | .query("users") 15 | .withIndex("by_token", (q) => 16 | q.eq("tokenIdentifier", identity.tokenIdentifier)) 17 | .unique(); 18 | 19 | if (user !== null) { 20 | return user._id; 21 | } 22 | 23 | const userId = await ctx.db.insert("users", { 24 | tokenIdentifier: identity.tokenIdentifier, 25 | email: identity.email!, 26 | }); 27 | 28 | return userId; 29 | } 30 | }); 31 | 32 | export const currentUser = query({ 33 | args: {}, 34 | handler: async (ctx) => { 35 | const identity = await ctx.auth.getUserIdentity(); 36 | if (!identity) { 37 | throw new Error("Called selectGPT without authenticated user"); 38 | } 39 | 40 | return await ctx.db 41 | .query("users") 42 | .withIndex("by_token", (q) => 43 | q.eq("tokenIdentifier", identity.tokenIdentifier)) 44 | .unique(); 45 | } 46 | }) 47 | 48 | 49 | //update subscription 50 | export const updateSubscription = internalMutation({ 51 | args: { subscriptionId: v.string(), userId: v.id("users"), endsOn: v.number() }, 52 | handler: async (ctx, { subscriptionId, userId, endsOn }) => { 53 | await ctx.db.patch(userId, { 54 | subscriptionId: subscriptionId, 55 | endsOn: endsOn 56 | }); 57 | }, 58 | }); 59 | 60 | //update subscription by id 61 | export const updateSubscriptionById = internalMutation({ 62 | args: { subscriptionId: v.string(), endsOn: v.number() }, 63 | handler: async (ctx, { subscriptionId, endsOn }) => { 64 | const user = await ctx.db.query("users") 65 | .withIndex("by_subscriptionId", (q) => q.eq("subscriptionId", subscriptionId)) 66 | .unique(); 67 | 68 | if (!user) { 69 | throw new Error("User not found!"); 70 | } 71 | 72 | await ctx.db.patch(user._id, { 73 | endsOn: endsOn 74 | }); 75 | }, 76 | }); -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { authMiddleware } from "@clerk/nextjs"; 2 | 3 | export default authMiddleware({ 4 | // Routes that can be accessed while signed out 5 | publicRoutes: [], 6 | // Routes that can always be accessed, and have 7 | // no authentication information 8 | ignoredRoutes: [], 9 | }); 10 | 11 | export const config = { 12 | // Protects all routes, including api/trpc. 13 | // See https://clerk.com/docs/references/nextjs/auth-middleware 14 | // for more information about configuring your Middleware 15 | matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"], 16 | }; -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tutorial-setup", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@clerk/nextjs": "^4.29.9", 13 | "@radix-ui/react-dialog": "^1.0.5", 14 | "@radix-ui/react-popover": "^1.0.7", 15 | "@radix-ui/react-slot": "^1.0.2", 16 | "class-variance-authority": "^0.7.0", 17 | "clsx": "^2.1.0", 18 | "convex": "^1.10.0", 19 | "lucide-react": "^0.356.0", 20 | "next": "14.1.3", 21 | "next-themes": "^0.2.1", 22 | "react": "^18", 23 | "react-dom": "^18", 24 | "sonner": "^1.4.3", 25 | "stripe": "^14.20.0", 26 | "tailwind-merge": "^2.2.1", 27 | "tailwindcss-animate": "^1.0.7" 28 | }, 29 | "devDependencies": { 30 | "@types/node": "^20", 31 | "@types/react": "^18", 32 | "@types/react-dom": "^18", 33 | "autoprefixer": "^10.0.1", 34 | "eslint": "^8", 35 | "eslint-config-next": "14.1.3", 36 | "postcss": "^8", 37 | "tailwindcss": "^3.3.0", 38 | "typescript": "^5" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /providers/convex-client-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | 4 | import { Loading } from "@/components/auth/loading"; 5 | import { ClerkProvider, useAuth } from "@clerk/nextjs"; 6 | import { AuthLoading, Authenticated, ConvexReactClient, Unauthenticated } from "convex/react"; 7 | import { ConvexProviderWithClerk } from "convex/react-clerk"; 8 | 9 | interface ConvexClientProviderProps { 10 | children: React.ReactNode; 11 | }; 12 | 13 | const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL!; 14 | 15 | const convex = new ConvexReactClient(convexUrl); 16 | 17 | export const ConvexClientProvider = ({ 18 | children 19 | }: ConvexClientProviderProps) => { 20 | return ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | {children} 28 | 29 | 30 | {children} 31 | 32 | 33 | 34 | ) 35 | } -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vukrosic/setup-nextjs-convex-clerk-typescript-stripe-shadcn-tailwindcss-react/3f71a1086fd851f47f07bb29167125cbf955efb4/public/thumbnail.png -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss" 2 | 3 | const config = { 4 | darkMode: ["class"], 5 | content: [ 6 | './pages/**/*.{ts,tsx}', 7 | './components/**/*.{ts,tsx}', 8 | './app/**/*.{ts,tsx}', 9 | './src/**/*.{ts,tsx}', 10 | ], 11 | prefix: "", 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 | keyframes: { 62 | "accordion-down": { 63 | from: { height: "0" }, 64 | to: { height: "var(--radix-accordion-content-height)" }, 65 | }, 66 | "accordion-up": { 67 | from: { height: "var(--radix-accordion-content-height)" }, 68 | to: { height: "0" }, 69 | }, 70 | }, 71 | animation: { 72 | "accordion-down": "accordion-down 0.2s ease-out", 73 | "accordion-up": "accordion-up 0.2s ease-out", 74 | }, 75 | }, 76 | }, 77 | plugins: [require("tailwindcss-animate")], 78 | } satisfies Config 79 | 80 | export default config -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | --------------------------------------------------------------------------------