├── .cursor └── rules │ ├── auth.mdc │ ├── backend.mdc │ ├── env.mdc │ ├── frontend.mdc │ ├── general.mdc │ ├── payments.mdc │ ├── storage.mdc │ └── types.mdc ├── .cursorrules ├── .env.example ├── .eslintrc.json ├── .gitignore ├── .husky └── pre-commit ├── .repo_ignore ├── README.md ├── actions ├── db │ └── profiles-actions.ts └── stripe-actions.ts ├── app ├── (auth) │ ├── layout.tsx │ ├── login │ │ └── [[...login]] │ │ │ └── page.tsx │ └── signup │ │ └── [[...signup]] │ │ └── page.tsx ├── (marketing) │ ├── about │ │ └── page.tsx │ ├── contact │ │ ├── _components │ │ │ └── contact-form.tsx │ │ └── page.tsx │ ├── features │ │ └── page.tsx │ ├── layout.tsx │ ├── page.tsx │ └── pricing │ │ └── page.tsx ├── api │ └── stripe │ │ └── webhooks │ │ └── route.ts ├── globals.css └── layout.tsx ├── components.json ├── components ├── landing │ ├── footer.tsx │ ├── header.tsx │ └── hero.tsx ├── magicui │ ├── animated-gradient-text.tsx │ └── hero-video-dialog.tsx ├── ui │ ├── accordion.tsx │ ├── alert-dialog.tsx │ ├── alert.tsx │ ├── aspect-ratio.tsx │ ├── avatar.tsx │ ├── badge.tsx │ ├── breadcrumb.tsx │ ├── button.tsx │ ├── calendar.tsx │ ├── card.tsx │ ├── carousel.tsx │ ├── chart.tsx │ ├── checkbox.tsx │ ├── collapsible.tsx │ ├── command.tsx │ ├── context-menu.tsx │ ├── dialog.tsx │ ├── drawer.tsx │ ├── dropdown-menu.tsx │ ├── form.tsx │ ├── hover-card.tsx │ ├── input-otp.tsx │ ├── input.tsx │ ├── label.tsx │ ├── menubar.tsx │ ├── navigation-menu.tsx │ ├── pagination.tsx │ ├── popover.tsx │ ├── progress.tsx │ ├── radio-group.tsx │ ├── resizable.tsx │ ├── scroll-area.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── sidebar.tsx │ ├── skeleton.tsx │ ├── slider.tsx │ ├── sonner.tsx │ ├── switch.tsx │ ├── table.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ ├── toast.tsx │ ├── toaster.tsx │ ├── toggle-group.tsx │ ├── toggle.tsx │ ├── tooltip.tsx │ └── use-toast.ts └── utilities │ ├── providers.tsx │ ├── tailwind-indicator.tsx │ └── theme-switcher.tsx ├── db ├── db.ts └── schema │ ├── index.ts │ └── profiles-schema.ts ├── drizzle.config.ts ├── lib ├── hooks │ ├── use-copy-to-clipboard.tsx │ ├── use-mobile.tsx │ └── use-toast.ts ├── stripe.ts └── utils.ts ├── license ├── middleware.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── prettier.config.cjs ├── public └── hero.png ├── tailwind.config.ts ├── tsconfig.json └── types ├── index.ts └── server-action-types.ts /.cursor/rules/auth.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Follow these rules when working on auth. 3 | globs: 4 | --- 5 | # Auth Rules 6 | 7 | Follow these rules when working on auth. 8 | 9 | It uses Clerk for authentication. 10 | 11 | ## General Rules 12 | 13 | - Import the auth helper with `import { auth } from "@clerk/nextjs/server"` in server components 14 | - await the auth helper in server action -------------------------------------------------------------------------------- /.cursor/rules/backend.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Follow these rules when working on the backend. 3 | globs: 4 | --- 5 | # Backend Rules 6 | 7 | Follow these rules when working on the backend. 8 | 9 | It uses Postgres, Supabase, Drizzle ORM, and Server Actions. 10 | 11 | ## General Rules 12 | 13 | - Never generate migrations. You do not have to do anything in the `db/migrations` folder inluding migrations and metadata. Ignore it. 14 | 15 | ## Organization 16 | 17 | ## Schemas 18 | 19 | - When importing schemas, use `@/db/schema` 20 | - Name files like `example-schema.ts` 21 | - All schemas should go in `db/schema` 22 | - Make sure to export the schema in `db/schema/index.ts` 23 | - Make sure to add the schema to the `schema` object in `db/db.ts` 24 | - If using a userId, always use `userId: text("user_id").notNull()` 25 | - Always include createdAt and updatedAt columns in all tables 26 | - Make sure to cascade delete when necessary 27 | - Use enums for columns that have a limited set of possible values such as: 28 | 29 | ```ts 30 | import { pgEnum } from "drizzle-orm/pg-core" 31 | 32 | export const membershipEnum = pgEnum("membership", ["free", "pro"]) 33 | 34 | membership: membershipEnum("membership").notNull().default("free") 35 | ``` 36 | 37 | Example of a schema: 38 | 39 | `db/schema/todos-schema.ts` 40 | 41 | ```ts 42 | import { boolean, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core" 43 | 44 | export const todosTable = pgTable("todos", { 45 | id: uuid("id").defaultRandom().primaryKey(), 46 | userId: text("user_id").notNull(), 47 | content: text("content").notNull(), 48 | completed: boolean("completed").default(false).notNull(), 49 | createdAt: timestamp("created_at").defaultNow().notNull(), 50 | updatedAt: timestamp("updated_at") 51 | .defaultNow() 52 | .notNull() 53 | .$onUpdate(() => new Date()) 54 | }) 55 | 56 | export type InsertTodo = typeof todosTable.$inferInsert 57 | export type SelectTodo = typeof todosTable.$inferSelect 58 | ``` 59 | 60 | And exporting it: 61 | 62 | `db/schema/index.ts` 63 | 64 | ```ts 65 | export * from "./todos-schema" 66 | ``` 67 | 68 | And adding it to the schema in `db/db.ts`: 69 | 70 | `db/db.ts` 71 | 72 | ```ts 73 | import { todosTable } from "@/db/schema" 74 | 75 | const schema = { 76 | todos: todosTable 77 | } 78 | ``` 79 | 80 | And a more complex schema: 81 | 82 | ```ts 83 | import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core" 84 | 85 | export const chatsTable = pgTable("chats", { 86 | id: uuid("id").defaultRandom().primaryKey(), 87 | userId: text("user_id").notNull(), 88 | name: text("name").notNull(), 89 | createdAt: timestamp("created_at").defaultNow().notNull(), 90 | updatedAt: timestamp("updated_at") 91 | .defaultNow() 92 | .notNull() 93 | .$onUpdate(() => new Date()) 94 | }) 95 | 96 | export type InsertChat = typeof chatsTable.$inferInsert 97 | export type SelectChat = typeof chatsTable.$inferSelect 98 | ``` 99 | 100 | ```ts 101 | import { pgEnum, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core" 102 | import { chatsTable } from "./chats-schema" 103 | 104 | export const roleEnum = pgEnum("role", ["assistant", "user"]) 105 | 106 | export const messagesTable = pgTable("messages", { 107 | id: uuid("id").defaultRandom().primaryKey(), 108 | chatId: uuid("chat_id") 109 | .references(() => chatsTable.id, { onDelete: "cascade" }) 110 | .notNull(), 111 | content: text("content").notNull(), 112 | role: roleEnum("role").notNull(), 113 | createdAt: timestamp("created_at").defaultNow().notNull(), 114 | updatedAt: timestamp("updated_at") 115 | .defaultNow() 116 | .notNull() 117 | .$onUpdate(() => new Date()) 118 | }) 119 | 120 | export type InsertMessage = typeof messagesTable.$inferInsert 121 | export type SelectMessage = typeof messagesTable.$inferSelect 122 | ``` 123 | 124 | And exporting it: 125 | 126 | `db/schema/index.ts` 127 | 128 | ```ts 129 | export * from "./chats-schema" 130 | export * from "./messages-schema" 131 | ``` 132 | 133 | And adding it to the schema in `db/db.ts`: 134 | 135 | `db/db.ts` 136 | 137 | ```ts 138 | import { chatsTable, messagesTable } from "@/db/schema" 139 | 140 | const schema = { 141 | chats: chatsTable, 142 | messages: messagesTable 143 | } 144 | ``` 145 | 146 | ## Server Actions 147 | 148 | - When importing actions, use `@/actions` or `@/actions/db` if db related 149 | - DB related actions should go in the `actions/db` folder 150 | - Other actions should go in the `actions` folder 151 | - Name files like `example-actions.ts` 152 | - All actions should go in the `actions` folder 153 | - Only write the needed actions 154 | - Return an ActionState with the needed data type from actions 155 | - Include Action at the end of function names `Ex: exampleFunction -> exampleFunctionAction` 156 | - Actions should return a Promise> 157 | - Sort in CRUD order: Create, Read, Update, Delete 158 | - Make sure to return undefined as the data type if the action is not supposed to return any data 159 | - **Date Handling:** For columns defined as `PgDateString` (or any date string type), always convert JavaScript `Date` objects to ISO strings using `.toISOString()` before performing operations (e.g., comparisons or insertions). This ensures value type consistency and prevents type errors. 160 | 161 | ```ts 162 | export type ActionState = 163 | | { isSuccess: true; message: string; data: T } 164 | | { isSuccess: false; message: string; data?: never } 165 | ``` 166 | 167 | Example of an action: 168 | 169 | `actions/db/todos-actions.ts` 170 | 171 | ```ts 172 | "use server" 173 | 174 | import { db } from "@/db/db" 175 | import { InsertTodo, SelectTodo, todosTable } from "@/db/schema/todos-schema" 176 | import { ActionState } from "@/types" 177 | import { eq } from "drizzle-orm" 178 | 179 | export async function createTodoAction( 180 | todo: InsertTodo 181 | ): Promise> { 182 | try { 183 | const [newTodo] = await db.insert(todosTable).values(todo).returning() 184 | return { 185 | isSuccess: true, 186 | message: "Todo created successfully", 187 | data: newTodo 188 | } 189 | } catch (error) { 190 | console.error("Error creating todo:", error) 191 | return { isSuccess: false, message: "Failed to create todo" } 192 | } 193 | } 194 | 195 | export async function getTodosAction( 196 | userId: string 197 | ): Promise> { 198 | try { 199 | const todos = await db.query.todos.findMany({ 200 | where: eq(todosTable.userId, userId) 201 | }) 202 | return { 203 | isSuccess: true, 204 | message: "Todos retrieved successfully", 205 | data: todos 206 | } 207 | } catch (error) { 208 | console.error("Error getting todos:", error) 209 | return { isSuccess: false, message: "Failed to get todos" } 210 | } 211 | } 212 | 213 | export async function updateTodoAction( 214 | id: string, 215 | data: Partial 216 | ): Promise> { 217 | try { 218 | const [updatedTodo] = await db 219 | .update(todosTable) 220 | .set(data) 221 | .where(eq(todosTable.id, id)) 222 | .returning() 223 | 224 | return { 225 | isSuccess: true, 226 | message: "Todo updated successfully", 227 | data: updatedTodo 228 | } 229 | } catch (error) { 230 | console.error("Error updating todo:", error) 231 | return { isSuccess: false, message: "Failed to update todo" } 232 | } 233 | } 234 | 235 | export async function deleteTodoAction(id: string): Promise> { 236 | try { 237 | await db.delete(todosTable).where(eq(todosTable.id, id)) 238 | return { 239 | isSuccess: true, 240 | message: "Todo deleted successfully", 241 | data: undefined 242 | } 243 | } catch (error) { 244 | console.error("Error deleting todo:", error) 245 | return { isSuccess: false, message: "Failed to delete todo" } 246 | } 247 | } 248 | ``` -------------------------------------------------------------------------------- /.cursor/rules/env.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Follow these rules when working with environment variables. 3 | globs: 4 | --- 5 | # Env Rules 6 | 7 | - If you update environment variables, update the `.env.example` file 8 | - All environment variables should go in `.env.local` 9 | - Do not expose environment variables to the frontend 10 | - Use `NEXT_PUBLIC_` prefix for environment variables that need to be accessed from the frontend 11 | - You may import environment variables in server actions and components by using `process.env.VARIABLE_NAME` -------------------------------------------------------------------------------- /.cursor/rules/frontend.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Follow these rules when working on the frontend. 3 | globs: 4 | --- 5 | # Frontend Rules 6 | 7 | Follow these rules when working on the frontend. 8 | 9 | It uses Next.js, Tailwind, Shadcn, and Framer Motion. 10 | 11 | ## General Rules 12 | 13 | - Use `lucide-react` for icons 14 | - useSidebar must be used within a SidebarProvider 15 | 16 | ## Components 17 | 18 | - Use divs instead of other html tags unless otherwise specified 19 | - Separate the main parts of a component's html with an extra blank line for visual spacing 20 | - Always tag a component with either `use server` or `use client` at the top, including layouts and pages 21 | 22 | ### Organization 23 | 24 | - All components be named using kebab case like `example-component.tsx` unless otherwise specified 25 | - Put components in `/_components` in the route if one-off components 26 | - Put components in `/components` from the root if shared components 27 | 28 | ### Data Fetching 29 | 30 | - Fetch data in server components and pass the data down as props to client components. 31 | - Use server actions from `/actions` to mutate data. 32 | 33 | ### Server Components 34 | 35 | - Use `"use server"` at the top of the file. 36 | - Implement Suspense for asynchronous data fetching to show loading states while data is being fetched. 37 | - If no asynchronous logic is required for a given server component, you do not need to wrap the component in ``. You can simply return the final UI directly since there is no async boundary needed. 38 | - If asynchronous fetching is required, you can use a `` boundary and a fallback to indicate a loading state while data is loading. 39 | - Server components cannot be imported into client components. If you want to use a server component in a client component, you must pass the as props using the "children" prop 40 | - params in server pages should be awaited such as `const { courseId } = await params` where the type is `params: Promise<{ courseId: string }>` 41 | 42 | Example of a server layout: 43 | 44 | ```tsx 45 | "use server" 46 | 47 | export default async function ExampleServerLayout({ 48 | children 49 | }: { 50 | children: React.ReactNode 51 | }) { 52 | return children 53 | } 54 | ``` 55 | 56 | Example of a server page (with async logic): 57 | 58 | ```tsx 59 | "use server" 60 | 61 | import { Suspense } from "react" 62 | import { SomeAction } from "@/actions/some-actions" 63 | import SomeComponent from "./_components/some-component" 64 | import SomeSkeleton from "./_components/some-skeleton" 65 | 66 | export default async function ExampleServerPage() { 67 | return ( 68 | }> 69 | 70 | 71 | ) 72 | } 73 | 74 | async function SomeComponentFetcher() { 75 | const { data } = await SomeAction() 76 | return 77 | } 78 | ``` 79 | 80 | Example of a server page (no async logic required): 81 | 82 | ```tsx 83 | "use server" 84 | 85 | import SomeClientComponent from "./_components/some-client-component" 86 | 87 | // In this case, no asynchronous work is being done, so no Suspense or fallback is required. 88 | export default async function ExampleServerPage() { 89 | return 90 | } 91 | ``` 92 | 93 | Example of a server component: 94 | 95 | ```tsx 96 | "use server" 97 | 98 | interface ExampleServerComponentProps { 99 | // Your props here 100 | } 101 | 102 | export async function ExampleServerComponent({ 103 | props 104 | }: ExampleServerComponentProps) { 105 | // Your code here 106 | } 107 | ``` 108 | 109 | ### Client Components 110 | 111 | - Use `"use client"` at the top of the file 112 | - Client components can safely rely on props passed down from server components, or handle UI interactions without needing if there’s no async logic. 113 | - Never use server actions in client components. If you need to create a new server action, create it in `/actions` 114 | 115 | Example of a client page: 116 | 117 | ```tsx 118 | "use client" 119 | 120 | export default function ExampleClientPage() { 121 | // Your code here 122 | } 123 | ``` 124 | 125 | Example of a client component: 126 | 127 | ```tsx 128 | "use client" 129 | 130 | interface ExampleClientComponentProps { 131 | initialData: any[] 132 | } 133 | 134 | export default function ExampleClientComponent({ 135 | initialData 136 | }: ExampleClientComponentProps) { 137 | // Client-side logic here 138 | return
{initialData.length} items
139 | } 140 | ``` -------------------------------------------------------------------------------- /.cursor/rules/general.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Follow these rules for all requests. 3 | globs: 4 | --- 5 | # Project Instructions 6 | 7 | Use specification and guidelines as you build the app. 8 | 9 | Write the complete code for every step. Do not get lazy. 10 | 11 | Your goal is to completely finish whatever I ask for. 12 | 13 | You will see tags in the code. These are context tags that you should use to help you understand the codebase. 14 | 15 | ## Overview 16 | 17 | This is a web app template. 18 | 19 | ## Tech Stack 20 | 21 | - Frontend: Next.js, Tailwind, Shadcn, Framer Motion 22 | - Backend: Postgres, Supabase, Drizzle ORM, Server Actions 23 | - Auth: Clerk 24 | - Payments: Stripe 25 | - Deployment: Vercel 26 | 27 | ## Project Structure 28 | 29 | - `actions` - Server actions 30 | - `db` - Database related actions 31 | - Other actions 32 | - `app` - Next.js app router 33 | - `api` - API routes 34 | - `route` - An example route 35 | - `_components` - One-off components for the route 36 | - `layout.tsx` - Layout for the route 37 | - `page.tsx` - Page for the route 38 | - `components` - Shared components 39 | - `ui` - UI components 40 | - `utilities` - Utility components 41 | - `db` - Database 42 | - `schema` - Database schemas 43 | - `lib` - Library code 44 | - `hooks` - Custom hooks 45 | - `prompts` - Prompt files 46 | - `public` - Static assets 47 | - `types` - Type definitions 48 | 49 | ## Rules 50 | 51 | Follow these rules when building the app. 52 | 53 | ### General Rules 54 | 55 | - All files should have a comment at the very top of the file that consisely explain what it does 56 | - Use `@` to import anything from the app unless otherwise specified 57 | - Use kebab case for all files and folders unless otherwise specified 58 | - Don't update shadcn components unless otherwise specified 59 | -------------------------------------------------------------------------------- /.cursor/rules/payments.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Follow these rules when working on payments. 3 | globs: 4 | --- 5 | # Payments Rules 6 | 7 | Follow these rules when working on payments. 8 | 9 | It uses Stripe for payments. -------------------------------------------------------------------------------- /.cursor/rules/storage.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Follow these rules when working on file storage. 3 | globs: 4 | --- 5 | # Storage Rules 6 | 7 | Follow these rules when working with Supabase Storage. 8 | 9 | It uses Supabase Storage for file uploads, downloads, and management. 10 | 11 | ## General Rules 12 | 13 | - Always use environment variables for bucket names to maintain consistency across environments 14 | - Never hardcode bucket names in the application code 15 | - Always handle file size limits and allowed file types at the application level 16 | - Use the `upsert` method instead of `upload` when you want to replace existing files 17 | - Always implement proper error handling for storage operations 18 | - Use content-type headers when uploading files to ensure proper file handling 19 | 20 | ## Organization 21 | 22 | ### Buckets 23 | 24 | - Name buckets in kebab-case: `user-uploads`, `profile-images` 25 | - Create separate buckets for different types of files (e.g., `profile-images`, `documents`, `attachments`) 26 | - Document bucket purposes in a central location 27 | - Set appropriate bucket policies (public/private) based on access requirements 28 | - Implement RLS (Row Level Security) policies for buckets that need user-specific access 29 | - Make sure to let me know instructions for setting up RLS policies on Supabase since you can't do this yourself, including the SQL scripts I need to run in the editor 30 | 31 | ### File Structure 32 | 33 | - Organize files in folders based on their purpose and ownership 34 | - Use predictable, collision-resistant naming patterns 35 | - Structure: `{bucket}/{userId}/{purpose}/{filename}` 36 | - Example: `profile-images/123e4567-e89b/avatar/profile.jpg` 37 | - Include timestamps in filenames when version history is important 38 | - Example: `documents/123e4567-e89b/contracts/2024-02-13-contract.pdf` 39 | 40 | ## Actions 41 | 42 | - When importing storage actions, use `@/actions/storage` 43 | - Name files like `example-storage-actions.ts` 44 | - Include Storage at the end of function names `Ex: uploadFile -> uploadFileStorage` 45 | - Follow the same ActionState pattern as DB actions 46 | 47 | Example of a storage action: 48 | 49 | ```ts 50 | "use server" 51 | 52 | import { createClientComponentClient } from "@supabase/auth-helpers-nextjs" 53 | import { ActionState } from "@/types" 54 | 55 | export async function uploadFileStorage( 56 | bucket: string, 57 | path: string, 58 | file: File 59 | ): Promise> { 60 | try { 61 | const supabase = createClientComponentClient() 62 | 63 | const { data, error } = await supabase 64 | .storage 65 | .from(bucket) 66 | .upload(path, file, { 67 | upsert: false, 68 | contentType: file.type 69 | }) 70 | 71 | if (error) throw error 72 | 73 | return { 74 | isSuccess: true, 75 | message: "File uploaded successfully", 76 | data: { path: data.path } 77 | } 78 | } catch (error) { 79 | console.error("Error uploading file:", error) 80 | return { isSuccess: false, message: "Failed to upload file" } 81 | } 82 | } 83 | ``` 84 | 85 | ## File Handling 86 | 87 | ### Upload Rules 88 | 89 | - Always validate file size before upload 90 | - Implement file type validation using both extension and MIME type 91 | - Generate unique filenames to prevent collisions 92 | - Set appropriate content-type headers 93 | - Handle existing files appropriately (error or upsert) 94 | 95 | Example validation: 96 | 97 | ```ts 98 | const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10MB 99 | const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"] 100 | 101 | function validateFile(file: File): boolean { 102 | if (file.size > MAX_FILE_SIZE) { 103 | throw new Error("File size exceeds limit") 104 | } 105 | 106 | if (!ALLOWED_TYPES.includes(file.type)) { 107 | throw new Error("File type not allowed") 108 | } 109 | 110 | return true 111 | } 112 | ``` 113 | 114 | ### Download Rules 115 | 116 | - Always handle missing files gracefully 117 | - Implement proper error handling for failed downloads 118 | - Use signed URLs for private files 119 | 120 | ### Delete Rules 121 | 122 | - Implement soft deletes when appropriate 123 | - Clean up related database records when deleting files 124 | - Handle bulk deletions carefully 125 | - Verify ownership before deletion 126 | - Always delete all versions/transforms of a file 127 | 128 | ## Security 129 | 130 | ### Bucket Policies 131 | 132 | - Make buckets private by default 133 | - Only make buckets public when absolutely necessary 134 | - Use RLS policies to restrict access to authorized users 135 | - Example RLS policy: 136 | 137 | ```sql 138 | CREATE POLICY "Users can only access their own files" 139 | ON storage.objects 140 | FOR ALL 141 | USING (auth.uid()::text = (storage.foldername(name))[1]); 142 | ``` 143 | 144 | ### Access Control 145 | 146 | - Generate short-lived signed URLs for private files 147 | - Implement proper CORS policies 148 | - Use separate buckets for public and private files 149 | - Never expose internal file paths 150 | - Validate user permissions before any operation 151 | 152 | ## Error Handling 153 | 154 | - Implement specific error types for common storage issues 155 | - Always provide meaningful error messages 156 | - Implement retry logic for transient failures 157 | - Log storage errors separately for monitoring 158 | 159 | ## Optimization 160 | 161 | - Implement progressive upload for large files 162 | - Clean up temporary files and failed uploads 163 | - Use batch operations when handling multiple files -------------------------------------------------------------------------------- /.cursor/rules/types.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Follow these rules when working with types. 3 | globs: 4 | --- 5 | # Type Rules 6 | 7 | - When importing types, use `@/types` 8 | - Name files like `example-types.ts` 9 | - All types should go in `types` 10 | - Make sure to export the types in `types/index.ts` 11 | - Prefer interfaces over type aliases 12 | - If referring to db types, use `@/db/schema` such as `SelectTodo` from `todos-schema.ts` 13 | 14 | An example of a type: 15 | 16 | `types/actions-types.ts` 17 | 18 | ```ts 19 | export type ActionState = 20 | | { isSuccess: true; message: string; data: T } 21 | | { isSuccess: false; message: string; data?: never } 22 | ``` 23 | 24 | And exporting it: 25 | 26 | `types/index.ts` 27 | 28 | ```ts 29 | export * from "./actions-types" 30 | ``` -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # DB 2 | DATABASE_URL= 3 | 4 | # Supabase 5 | SUPABASE_URL= 6 | SUPABASE_SERVICE_ROLE_KEY= 7 | SUPABASE_BUCKET_RECEIPTS= 8 | 9 | # Auth 10 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 11 | CLERK_SECRET_KEY= 12 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/login 13 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/signup 14 | 15 | # Payments 16 | STRIPE_SECRET_KEY= 17 | STRIPE_WEBHOOK_SECRET= 18 | NEXT_PUBLIC_STRIPE_PAYMENT_LINK_YEARLY= 19 | NEXT_PUBLIC_STRIPE_PAYMENT_LINK_MONTHLY= 20 | 21 | # AI 22 | OPENAI_API_KEY= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | /* 2 | Contains the ESLint configuration for the app. 3 | */ 4 | 5 | { 6 | "$schema": "https://json.schemastore.org/eslintrc", 7 | "root": true, 8 | "extends": [ 9 | "next/core-web-vitals", 10 | "prettier", 11 | "plugin:tailwindcss/recommended" 12 | ], 13 | "plugins": ["tailwindcss"], 14 | "rules": { 15 | "@next/next/no-img-element": "off", 16 | "jsx-a11y/alt-text": "off", 17 | "react-hooks/exhaustive-deps": "off", 18 | "tailwindcss/enforces-negative-arbitrary-values": "off", 19 | "tailwindcss/no-contradicting-classname": "off", 20 | "tailwindcss/no-custom-classname": "off", 21 | "tailwindcss/no-unnecessary-arbitrary-value": "off", 22 | "react/no-unescaped-entities": "off" 23 | }, 24 | "settings": { 25 | "tailwindcss": { "callees": ["cn", "cva"], "config": "tailwind.config.js" } 26 | }, 27 | "overrides": [ 28 | { "files": ["*.ts", "*.tsx"], "parser": "@typescript-eslint/parser" } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.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 | 38 | # clerk configuration (can include secrets) 39 | /.clerk/ 40 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | . "$(dirname -- "$0")/_/husky.sh" 4 | 5 | npm run lint:fix && npm run format:write && git add . -------------------------------------------------------------------------------- /.repo_ignore: -------------------------------------------------------------------------------- 1 | # Package manager caches 2 | **/node_modules/ 3 | **/.npm/ 4 | **/__pycache__/ 5 | **/.pytest_cache/ 6 | **/.mypy_cache/ 7 | 8 | # Build caches 9 | **/.gradle/ 10 | **/.nuget/ 11 | **/.cargo/ 12 | **/.stack-work/ 13 | **/.ccache/ 14 | 15 | # IDE and Editor caches 16 | **/.idea/ 17 | **/.vscode/ 18 | **/*.swp 19 | **/*~ 20 | 21 | # Temp files 22 | **/*.tmp 23 | **/*.temp 24 | **/*.bak 25 | 26 | **/*.meta 27 | **/package-lock.json 28 | 29 | # AI Specific 30 | .repo_ignore 31 | .cursorrules 32 | /.cursor 33 | 34 | # Project Specific 35 | **/.github 36 | **/.husky 37 | **/migrations 38 | **/public 39 | **/.next 40 | README.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Building Apps with the o1 Pro Template System 2 | 3 | This is the repo for a free workshop on how to use [OpenAI's o1-pro](https://chatgpt.com/) to build full-stack web apps with a [starter template](https://github.com/mckaywrigley/mckays-app-template). 4 | 5 | It is part 1 of a 2 part series. This is the beginner workshop. The advanced workshop will be released on February 24th. 6 | 7 | ## Workshop Video 8 | 9 | You can find the video for this workshop on [X](https://x.com/mckaywrigley/status/1891544731496206365) and [YouTube](https://www.youtube.com/watch?v=Y4n_p9w8pGY). 10 | 11 | This workshop is also available in course form on [Takeoff](https://www.jointakeoff.com/) - we will continue to add to it and keep it updated with the latest model releases over time. 12 | 13 | Use code `O1PRO` for 25% off at checkout. 14 | 15 | I get asked all the time for an example of content on Takeoff, so hopefully this workshop gives you a feel for our content and my teaching style. 16 | 17 | ## About Me 18 | 19 | My name is [Mckay](https://www.mckaywrigley.com/). 20 | 21 | I'm currently building [Takeoff](https://www.jointakeoff.com/) - the best place on the internet to learn how to build with AI. 22 | 23 | Follow me on [X](https://x.com/mckaywrigley) and subscribe to my [YouTube](https://www.youtube.com/channel/UCXZFVVCFahewxr3est7aT7Q) for more free AI coding tutorials & guides. 24 | 25 | ## Tech Stack 26 | 27 | - AI Model: [o1-pro](https://chatgpt.com/) 28 | - IDE: [Cursor](https://www.cursor.com/) 29 | - AI Tools: [RepoPrompt](https://repoprompt.com/), [V0](https://v0.dev/), [Perplexity](https://www.perplexity.com/) 30 | - Frontend: [Next.js](https://nextjs.org/docs), [Tailwind](https://tailwindcss.com/docs/guides/nextjs), [Shadcn](https://ui.shadcn.com/docs/installation), [Framer Motion](https://www.framer.com/motion/introduction/) 31 | - Backend: [PostgreSQL](https://www.postgresql.org/about/), [Supabase](https://supabase.com/), [Drizzle](https://orm.drizzle.team/docs/get-started-postgresql), [Server Actions](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations) 32 | - Auth: [Clerk](https://clerk.com/) 33 | - Payments: [Stripe](https://stripe.com/) 34 | 35 | **Note**: While I _highly_ recommend using o1-pro for this workflow, you can also use o3-mini, Claude 3.5 Sonnet, Gemini 2.0 Pro, and DeepSeek r1 for cheaper alternatives. However, you _will_ run into issues with those other models in this particular workflow, so I recommend using o1-pro for this workflow if possible. 36 | 37 | ## Prerequisites 38 | 39 | You will need accounts for the following services. 40 | 41 | They all have free plans that you can use to get started, with the exception of ChatGPT Pro (if you are using o1-pro). 42 | 43 | - Create a [Cursor](https://www.cursor.com/) account 44 | - Create a [GitHub](https://github.com/) account 45 | - Create a [Supabase](https://supabase.com/) account 46 | - Create a [Clerk](https://clerk.com/) account 47 | - Create a [Stripe](https://stripe.com/) account 48 | - Create a [Vercel](https://vercel.com/) account 49 | 50 | You will likely not need paid plans unless you are building a business. 51 | 52 | ## Guide 53 | 54 | ### Clone the repo 55 | 56 | 1. Clone this repo: 57 | 58 | ```bash 59 | git clone https://github.com/mckaywrigley/o1-pro-template-system o1-pro-project 60 | ``` 61 | 62 | 2. Save the original remote as "upstream" before removing it: 63 | 64 | ```bash 65 | git remote rename origin upstream 66 | ``` 67 | 68 | 3. Create a new repository on GitHub 69 | 70 | 4. Add the new repository as "origin": 71 | 72 | ```bash 73 | git remote add origin https://github.com/your-username/your-repo-name.git 74 | ``` 75 | 76 | 5. Push the new repository: 77 | 78 | ``` 79 | git branch -M main 80 | git push -u origin main 81 | ``` 82 | 83 | ### Run the app 84 | 85 | 1. Install dependencies: 86 | 87 | ```bash 88 | npm install 89 | ``` 90 | 91 | 2. Run the app: 92 | 93 | ```bash 94 | npm run dev 95 | ``` 96 | 97 | 3. View the app on http://localhost:3000 98 | 99 | ### Follow the workshop 100 | 101 | View the full workshop on [X](https://x.com/mckaywrigley/status/1891544731496206365) and [YouTube](https://www.youtube.com/watch?v=Y4n_p9w8pGY). 102 | 103 | Or sign up for [Takeoff](https://www.jointakeoff.com/) to get access to the full workshop in course form. 104 | -------------------------------------------------------------------------------- /actions/db/profiles-actions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Contains server actions related to profiles in the DB. 3 | */ 4 | 5 | "use server" 6 | 7 | import { db } from "@/db/db" 8 | import { 9 | InsertProfile, 10 | profilesTable, 11 | SelectProfile 12 | } from "@/db/schema/profiles-schema" 13 | import { ActionState } from "@/types" 14 | import { eq } from "drizzle-orm" 15 | 16 | export async function createProfileAction( 17 | data: InsertProfile 18 | ): Promise> { 19 | try { 20 | const [newProfile] = await db.insert(profilesTable).values(data).returning() 21 | return { 22 | isSuccess: true, 23 | message: "Profile created successfully", 24 | data: newProfile 25 | } 26 | } catch (error) { 27 | console.error("Error creating profile:", error) 28 | return { isSuccess: false, message: "Failed to create profile" } 29 | } 30 | } 31 | 32 | export async function getProfileByUserIdAction( 33 | userId: string 34 | ): Promise> { 35 | try { 36 | const profile = await db.query.profiles.findFirst({ 37 | where: eq(profilesTable.userId, userId) 38 | }) 39 | if (!profile) { 40 | return { isSuccess: false, message: "Profile not found" } 41 | } 42 | 43 | return { 44 | isSuccess: true, 45 | message: "Profile retrieved successfully", 46 | data: profile 47 | } 48 | } catch (error) { 49 | console.error("Error getting profile by user id", error) 50 | return { isSuccess: false, message: "Failed to get profile" } 51 | } 52 | } 53 | 54 | export async function updateProfileAction( 55 | userId: string, 56 | data: Partial 57 | ): Promise> { 58 | try { 59 | const [updatedProfile] = await db 60 | .update(profilesTable) 61 | .set(data) 62 | .where(eq(profilesTable.userId, userId)) 63 | .returning() 64 | 65 | if (!updatedProfile) { 66 | return { isSuccess: false, message: "Profile not found to update" } 67 | } 68 | 69 | return { 70 | isSuccess: true, 71 | message: "Profile updated successfully", 72 | data: updatedProfile 73 | } 74 | } catch (error) { 75 | console.error("Error updating profile:", error) 76 | return { isSuccess: false, message: "Failed to update profile" } 77 | } 78 | } 79 | 80 | export async function updateProfileByStripeCustomerIdAction( 81 | stripeCustomerId: string, 82 | data: Partial 83 | ): Promise> { 84 | try { 85 | const [updatedProfile] = await db 86 | .update(profilesTable) 87 | .set(data) 88 | .where(eq(profilesTable.stripeCustomerId, stripeCustomerId)) 89 | .returning() 90 | 91 | if (!updatedProfile) { 92 | return { 93 | isSuccess: false, 94 | message: "Profile not found by Stripe customer ID" 95 | } 96 | } 97 | 98 | return { 99 | isSuccess: true, 100 | message: "Profile updated by Stripe customer ID successfully", 101 | data: updatedProfile 102 | } 103 | } catch (error) { 104 | console.error("Error updating profile by stripe customer ID:", error) 105 | return { 106 | isSuccess: false, 107 | message: "Failed to update profile by Stripe customer ID" 108 | } 109 | } 110 | } 111 | 112 | export async function deleteProfileAction( 113 | userId: string 114 | ): Promise> { 115 | try { 116 | await db.delete(profilesTable).where(eq(profilesTable.userId, userId)) 117 | return { 118 | isSuccess: true, 119 | message: "Profile deleted successfully", 120 | data: undefined 121 | } 122 | } catch (error) { 123 | console.error("Error deleting profile:", error) 124 | return { isSuccess: false, message: "Failed to delete profile" } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /actions/stripe-actions.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Contains server actions related to Stripe. 3 | */ 4 | 5 | import { 6 | updateProfileAction, 7 | updateProfileByStripeCustomerIdAction 8 | } from "@/actions/db/profiles-actions" 9 | import { SelectProfile } from "@/db/schema" 10 | import { stripe } from "@/lib/stripe" 11 | import Stripe from "stripe" 12 | 13 | type MembershipStatus = SelectProfile["membership"] 14 | 15 | const getMembershipStatus = ( 16 | status: Stripe.Subscription.Status, 17 | membership: MembershipStatus 18 | ): MembershipStatus => { 19 | switch (status) { 20 | case "active": 21 | case "trialing": 22 | return membership 23 | case "canceled": 24 | case "incomplete": 25 | case "incomplete_expired": 26 | case "past_due": 27 | case "paused": 28 | case "unpaid": 29 | return "free" 30 | default: 31 | return "free" 32 | } 33 | } 34 | 35 | const getSubscription = async (subscriptionId: string) => { 36 | return stripe.subscriptions.retrieve(subscriptionId, { 37 | expand: ["default_payment_method"] 38 | }) 39 | } 40 | 41 | export const updateStripeCustomer = async ( 42 | userId: string, 43 | subscriptionId: string, 44 | customerId: string 45 | ) => { 46 | try { 47 | if (!userId || !subscriptionId || !customerId) { 48 | throw new Error("Missing required parameters for updateStripeCustomer") 49 | } 50 | 51 | const subscription = await getSubscription(subscriptionId) 52 | 53 | const result = await updateProfileAction(userId, { 54 | stripeCustomerId: customerId, 55 | stripeSubscriptionId: subscription.id 56 | }) 57 | 58 | if (!result.isSuccess) { 59 | throw new Error("Failed to update customer profile") 60 | } 61 | 62 | return result.data 63 | } catch (error) { 64 | console.error("Error in updateStripeCustomer:", error) 65 | throw error instanceof Error 66 | ? error 67 | : new Error("Failed to update Stripe customer") 68 | } 69 | } 70 | 71 | export const manageSubscriptionStatusChange = async ( 72 | subscriptionId: string, 73 | customerId: string, 74 | productId: string 75 | ): Promise => { 76 | try { 77 | if (!subscriptionId || !customerId || !productId) { 78 | throw new Error( 79 | "Missing required parameters for manageSubscriptionStatusChange" 80 | ) 81 | } 82 | 83 | const subscription = await getSubscription(subscriptionId) 84 | const product = await stripe.products.retrieve(productId) 85 | const membership = product.metadata.membership as MembershipStatus 86 | 87 | if (!["free", "pro"].includes(membership)) { 88 | throw new Error( 89 | `Invalid membership type in product metadata: ${membership}` 90 | ) 91 | } 92 | 93 | const membershipStatus = getMembershipStatus( 94 | subscription.status, 95 | membership 96 | ) 97 | 98 | const updateResult = await updateProfileByStripeCustomerIdAction( 99 | customerId, 100 | { stripeSubscriptionId: subscription.id, membership: membershipStatus } 101 | ) 102 | 103 | if (!updateResult.isSuccess) { 104 | throw new Error("Failed to update subscription status") 105 | } 106 | 107 | return membershipStatus 108 | } catch (error) { 109 | console.error("Error in manageSubscriptionStatusChange:", error) 110 | throw error instanceof Error 111 | ? error 112 | : new Error("Failed to update subscription status") 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | This server layout provides a centered layout for (auth) pages. 3 | */ 4 | 5 | "use server" 6 | 7 | interface AuthLayoutProps { 8 | children: React.ReactNode 9 | } 10 | 11 | export default async function AuthLayout({ children }: AuthLayoutProps) { 12 | return ( 13 |
{children}
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /app/(auth)/login/[[...login]]/page.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | This client page provides the login form from Clerk. 3 | */ 4 | 5 | "use client" 6 | 7 | import { SignIn } from "@clerk/nextjs" 8 | import { dark } from "@clerk/themes" 9 | import { useTheme } from "next-themes" 10 | 11 | export default function LoginPage() { 12 | const { theme } = useTheme() 13 | 14 | return ( 15 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /app/(auth)/signup/[[...signup]]/page.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | This client page provides the signup form from Clerk. 3 | */ 4 | 5 | "use client" 6 | 7 | import { SignUp } from "@clerk/nextjs" 8 | import { dark } from "@clerk/themes" 9 | import { useTheme } from "next-themes" 10 | 11 | export default function SignUpPage() { 12 | const { theme } = useTheme() 13 | 14 | return ( 15 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /app/(marketing)/about/page.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | This server page displays information about the company, mission, and team. 3 | */ 4 | 5 | "use server" 6 | 7 | import { Card, CardContent } from "@/components/ui/card" 8 | 9 | export default async function AboutPage() { 10 | return ( 11 |
12 |

About Us

13 | 14 |
15 | 16 | 17 |

Our Story

18 |

19 | We are passionate about building tools that help people work 20 | smarter and achieve more. Our platform combines cutting-edge 21 | technology with intuitive design to create a seamless experience 22 | for our users. 23 |

24 |
25 |
26 | 27 | 28 | 29 |

Our Mission

30 |

31 | Our mission is to empower individuals and organizations with 32 | innovative solutions that drive productivity and success. We 33 | believe in creating technology that adapts to how people work, not 34 | the other way around. 35 |

36 |
37 |
38 | 39 | 40 | 41 |

Core Values

42 |
    43 |
  • Innovation in everything we do
  • 44 |
  • Customer success is our success
  • 45 |
  • Transparency and trust
  • 46 |
  • Continuous improvement
  • 47 |
48 |
49 |
50 |
51 |
52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /app/(marketing)/contact/_components/contact-form.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Button } from "@/components/ui/button" 4 | import { 5 | Form, 6 | FormControl, 7 | FormField, 8 | FormItem, 9 | FormLabel, 10 | FormMessage 11 | } from "@/components/ui/form" 12 | import { Input } from "@/components/ui/input" 13 | import { Textarea } from "@/components/ui/textarea" 14 | import { zodResolver } from "@hookform/resolvers/zod" 15 | import { useForm } from "react-hook-form" 16 | import * as z from "zod" 17 | 18 | const formSchema = z.object({ 19 | name: z.string().min(2, "Name must be at least 2 characters"), 20 | email: z.string().email("Please enter a valid email address"), 21 | message: z.string().min(10, "Message must be at least 10 characters") 22 | }) 23 | 24 | export default function ContactForm() { 25 | const form = useForm>({ 26 | resolver: zodResolver(formSchema), 27 | defaultValues: { name: "", email: "", message: "" } 28 | }) 29 | 30 | async function onSubmit(values: z.infer) { 31 | // In a real app, you would handle the form submission here 32 | // For example, sending the data to your API route 33 | console.log(values) 34 | } 35 | 36 | return ( 37 |
38 | 39 | ( 43 | 44 | Name 45 | 46 | 47 | 48 | 49 | 50 | )} 51 | /> 52 | 53 | ( 57 | 58 | Email 59 | 60 | 61 | 62 | 63 | 64 | )} 65 | /> 66 | 67 | ( 71 | 72 | Message 73 | 74 |