├── .env.example
├── .eslintrc.cjs
├── .gitignore
├── LICENSE
├── README.md
├── components.json
├── cursorrules
├── next-sitemap.config.cjs
├── next.config.js
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── prettier.config.js
├── prisma
    └── schema.prisma
├── public
    ├── favicon.ico
    └── sitemap.xml
├── sentry.client.config.ts
├── sentry.edge.config.ts
├── sentry.server.config.ts
├── src
    ├── app
    │   ├── (marketing)
    │   │   ├── (home)
    │   │   │   └── page.tsx
    │   │   └── layout.tsx
    │   ├── (saas)
    │   │   ├── app
    │   │   │   └── page.tsx
    │   │   └── layout.tsx
    │   ├── api
    │   │   ├── sentry-example-api
    │   │   │   └── route.ts
    │   │   └── trpc
    │   │   │   └── [trpc]
    │   │   │       └── route.ts
    │   ├── dashboard
    │   │   └── page.tsx
    │   ├── global-error.tsx
    │   ├── handler
    │   │   └── [...stack]
    │   │   │   └── page.tsx
    │   ├── layout.tsx
    │   ├── loading.tsx
    │   └── sentry-example-page
    │   │   └── page.tsx
    ├── components
    │   ├── emails
    │   │   └── email-template.tsx
    │   ├── marketing
    │   │   ├── features
    │   │   │   ├── animated-beam-multiple-outputs.tsx
    │   │   │   └── animated-list-demo.tsx
    │   │   ├── hero
    │   │   │   ├── assurance-items.tsx
    │   │   │   ├── hero-banner.tsx
    │   │   │   ├── hero-title.tsx
    │   │   │   └── testimonials-avatars.tsx
    │   │   ├── pricing
    │   │   │   └── plan-card.tsx
    │   │   ├── sections
    │   │   │   ├── feature-section.tsx
    │   │   │   ├── hero-section.tsx
    │   │   │   └── pricing-section.tsx
    │   │   ├── shared
    │   │   │   ├── footer.tsx
    │   │   │   ├── navbar.tsx
    │   │   │   └── section-title.tsx
    │   │   └── testimonials
    │   │   │   └── testimonials.tsx
    │   ├── saas
    │   │   ├── app-sidebar.tsx
    │   │   ├── nav-main.tsx
    │   │   ├── nav-projects.tsx
    │   │   ├── nav-secondary.tsx
    │   │   ├── nav-user.tsx
    │   │   └── post.tsx
    │   └── ui
    │   │   ├── animated-beam.tsx
    │   │   ├── animated-grid-pattern.tsx
    │   │   ├── animated-list.tsx
    │   │   ├── animated-shiny-text.tsx
    │   │   ├── avatar-circles.tsx
    │   │   ├── avatar.tsx
    │   │   ├── badge.tsx
    │   │   ├── bento-grid.tsx
    │   │   ├── breadcrumb.tsx
    │   │   ├── button.tsx
    │   │   ├── card.tsx
    │   │   ├── collapsible.tsx
    │   │   ├── container.tsx
    │   │   ├── dropdown-menu.tsx
    │   │   ├── gradient-text.tsx
    │   │   ├── hero-video-dialog.tsx
    │   │   ├── input.tsx
    │   │   ├── label.tsx
    │   │   ├── marquee.tsx
    │   │   ├── rainbow-button.tsx
    │   │   ├── separator.tsx
    │   │   ├── sheet.tsx
    │   │   ├── sidebar.tsx
    │   │   ├── skeleton.tsx
    │   │   ├── switch.tsx
    │   │   ├── tooltip.tsx
    │   │   └── tracing-beam.tsx
    ├── env.js
    ├── hooks
    │   └── use-mobile.tsx
    ├── instrumentation.ts
    ├── lib
    │   ├── mail.ts
    │   ├── posthog.ts
    │   └── utils.ts
    ├── provider
    │   └── posthog-provider.tsx
    ├── server
    │   ├── api
    │   │   ├── root.ts
    │   │   ├── routers
    │   │   │   ├── email.ts
    │   │   │   └── post.ts
    │   │   └── trpc.ts
    │   └── db.ts
    ├── stack.tsx
    ├── styles
    │   └── globals.css
    ├── trpc
    │   ├── query-client.ts
    │   ├── react.tsx
    │   └── server.ts
    └── types
    │   └── pricing.ts
├── start-database.sh
├── tailwind.config.ts
└── tsconfig.json
/.env.example:
--------------------------------------------------------------------------------
 1 | # Since the ".env" file is gitignored, you can use the ".env.example" file to
 2 | # build a new ".env" file when you clone the repo. Keep this file up-to-date
 3 | # when you add new variables to `.env`.
 4 | 
 5 | # This file will be committed to version control, so make sure not to have any
 6 | # secrets in it. If you are cloning this repo, create a copy of this file named
 7 | # ".env" and populate it with your secrets.
 8 | 
 9 | # When adding additional environment variables, the schema in "/src/env.js"
10 | # should be updated accordingly.
11 | 
12 | # Prisma (Required)
13 | # https://www.prisma.io/docs/reference/database-reference/connection-urls#env
14 | DATABASE_URL="postgresql://postgres:password@localhost:5432/saas-boilerplate"
15 | 
16 | # Stack Auth (Required)
17 | # https://docs.stack-auth.com/overview
18 | NEXT_PUBLIC_STACK_PROJECT_ID=""
19 | NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=""
20 | STACK_SECRET_SERVER_KEY=""
21 | 
22 | # Sentry (Optional)
23 | SENTRY_ORG=""
24 | SENTRY_PROJECT=""
25 | 
26 | # Resend (Optional)
27 | RESEND_API_KEY=""
28 | RESEND_AUDIENCE_ID=""
29 | 
30 | # PostHog
31 | NEXT_PUBLIC_POSTHOG_HOST=""
32 | NEXT_PUBLIC_POSTHOG_KEY=""
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
 1 | /** @type {import("eslint").Linter.Config} */
 2 | const config = {
 3 |   "parser": "@typescript-eslint/parser",
 4 |   "parserOptions": {
 5 |     "project": true
 6 |   },
 7 |   "plugins": [
 8 |     "@typescript-eslint"
 9 |   ],
10 |   "extends": [
11 |     "next/core-web-vitals",
12 |     "plugin:@typescript-eslint/recommended-type-checked",
13 |     "plugin:@typescript-eslint/stylistic-type-checked"
14 |   ],
15 |   "rules": {
16 |     "@typescript-eslint/array-type": "off",
17 |     "@typescript-eslint/consistent-type-definitions": "off",
18 |     "@typescript-eslint/consistent-type-imports": [
19 |       "warn",
20 |       {
21 |         "prefer": "type-imports",
22 |         "fixStyle": "inline-type-imports"
23 |       }
24 |     ],
25 |     "@typescript-eslint/no-unused-vars": [
26 |       "warn",
27 |       {
28 |         "argsIgnorePattern": "^_"
29 |       }
30 |     ],
31 |     "@typescript-eslint/require-await": "off",
32 |     "@typescript-eslint/no-misused-promises": [
33 |       "error",
34 |       {
35 |         "checksVoidReturn": {
36 |           "attributes": false
37 |         }
38 |       }
39 |     ],
40 |     "@typescript-eslint/ban-ts-comment": "off"
41 |   }
42 | }
43 | module.exports = config;
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 2 | 
 3 | # dependencies
 4 | /node_modules
 5 | /.pnp
 6 | .pnp.js
 7 | 
 8 | # testing
 9 | /coverage
10 | 
11 | # database
12 | /prisma/db.sqlite
13 | /prisma/db.sqlite-journal
14 | db.sqlite
15 | 
16 | # next.js
17 | /.next/
18 | /out/
19 | next-env.d.ts
20 | 
21 | # production
22 | /build
23 | 
24 | # misc
25 | .DS_Store
26 | *.pem
27 | 
28 | # debug
29 | npm-debug.log*
30 | yarn-debug.log*
31 | yarn-error.log*
32 | .pnpm-debug.log*
33 | 
34 | # local env files
35 | # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
36 | .env
37 | .env*.local
38 | 
39 | # vercel
40 | .vercel
41 | 
42 | # typescript
43 | *.tsbuildinfo
44 | 
45 | # idea files
46 | .idea
47 | # Sentry Config File
48 | .env.sentry-build-plugin
49 | 
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright (c) 2023 Robin Faraj
 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. 
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | 
  2 |    
  3 |   SaaS Boilerplate
  4 |    
  5 |  
  6 | 
  7 | A modern, type-safe SaaS boilerplate built with Next.js 15, tRPC, and Prisma. 
  8 | 
  9 | 
 10 |   Key Features  •
 11 |   Tech Stack  •
 12 |   Quick Start  •
 13 |   Project Structure 
 14 | 
 15 | 
 16 | 
 17 |   
 18 |      
 19 |    
 20 | 
 21 | 
 22 | 
 23 |   Looking for a mobile app boilerplate? Check out NativeExpress  - The ultimate React Native & Expo boilerplate! 
 24 | 
 25 | 
 26 | ## Key Features
 27 | 
 28 | * Full-stack type safety with TypeScript and tRPC
 29 | * Modern file-based routing with Next.js 15 App Router
 30 | * Beautiful UI components from ShadcN and MagicUI
 31 | * Secure database management with Prisma and PostgreSQL
 32 | * Real-time analytics with PostHog
 33 | * Responsive design with TailwindCSS
 34 | * Dark/Light mode support
 35 | * End-to-end type safety from database to frontend
 36 | * Production-ready authentication system
 37 | * Optimized data fetching with React Query
 38 | 
 39 | ## Tech Stack
 40 | 
 41 | * **Framework**: Next.js 15 with App Router
 42 | * **Language**: TypeScript
 43 | * **API**: tRPC for end-to-end type-safe APIs
 44 | * **Database**: PostgreSQL with Prisma ORM
 45 | * **UI Components**: ShadcN UI & MagicUI
 46 | * **Styling**: TailwindCSS
 47 | * **Analytics**: PostHog
 48 | * **Data Fetching**: React Query (through tRPC)
 49 | * **Validation**: Zod
 50 | 
 51 | ## Quick Start
 52 | 
 53 | 1. Clone the repository:
 54 |    ```bash
 55 |    git clone https://github.com/robinsadeghpour/saas-boilerplate.git
 56 |    cd saas-boilerplate
 57 |    ```
 58 | 
 59 | 2. Install dependencies:
 60 |    ```bash
 61 |    pnpm install
 62 |    ```
 63 | 
 64 | 3. Copy the environment variables:
 65 |    ```bash
 66 |    cp .env.example .env
 67 |    ```
 68 | 
 69 | 4. Start the PostgreSQL database:
 70 |    ```bash
 71 |    ./start-database.sh
 72 |    ```
 73 | 
 74 | 5. Run database migrations:
 75 |    ```bash
 76 |    pnpm prisma migrate dev
 77 |    ```
 78 | 
 79 | 6. Start the development server:
 80 |    ```bash
 81 |    pnpm dev
 82 |    ```
 83 | 
 84 | ## Project Structure
 85 | 
 86 | ```
 87 | src/
 88 | ├── app/          # Next.js App Router pages and layouts
 89 | ├── components/   # Reusable UI components
 90 | ├── hooks/        # Custom React hooks
 91 | ├── lib/          # Core utilities and services
 92 | ├── provider/     # Context providers and wrappers
 93 | ├── server/       # Server-side logic
 94 | ├── styles/       # Global styles and Tailwind config
 95 | ├── trpc/         # tRPC router and procedures
 96 | └── types/        # TypeScript types and interfaces
 97 | ```
 98 | 
 99 | ## Development Guidelines
100 | 
101 | * Use TypeScript for all files except configurations
102 | * Follow functional component patterns with hooks
103 | * Keep components focused and small
104 | * Use Tailwind for styling with consistent design tokens
105 | * Follow mobile-first responsive design
106 | * Implement proper error handling with tRPC
107 | * Use React Query features through tRPC for data management
108 | 
109 | ## Related Projects
110 | 
111 | 🚀 **[NativeExpress](https://native.express?ref=saas-boilerplate)** - The ultimate React Native & Expo boilerplate with all the features you need to build production-ready mobile apps.
112 | 
113 | ---
114 | 
115 | 
116 |   Follow on X  •
117 |   Subscribe on YouTube 
118 | 
119 | 
120 | > This project is maintained by [Robin Faraj](https://x.com/robin_faraj?ref=saas-boilerplate)
121 | 
122 | ## Environment Variables
123 | 
124 | ### Required Environment Variables
125 | The following environment variables are required for the application to function properly:
126 | 
127 | - `DATABASE_URL`: PostgreSQL connection URL
128 | - `NEXT_PUBLIC_STACK_PROJECT_ID`: Your Stack Auth project ID
129 | - `NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY`: Your Stack Auth publishable client key
130 | - `STACK_SECRET_SERVER_KEY`: Your Stack Auth secret server key
131 | 
132 | ### Optional Environment Variables
133 | These environment variables are optional but enable additional features:
134 | 
135 | - `RESEND_API_KEY`: For email functionality
136 | - `RESEND_AUDIENCE_ID`: For newsletter management
137 | - `SENTRY_ORG` and `SENTRY_PROJECT`: For error tracking
138 | - `NEXT_PUBLIC_POSTHOG_HOST` and `NEXT_PUBLIC_POSTHOG_KEY`: For analytics
139 | 
140 | ### Setup Instructions
141 | 
142 | 1. Copy `.env.example` to `.env`
143 | 
144 | 2. Set up the database:
145 |    - Make sure Docker is installed and running
146 |    - Start the local database using the provided script:
147 |      ```bash
148 |      ./start-database.sh
149 |      ```
150 |    - Push the database schema:
151 |      ```bash
152 |      pnpm db:push
153 |      ```
154 | 
155 | 3. For Stack Auth credentials:
156 |    - Visit [Stack Auth Documentation](https://docs.stack-auth.com/overview)
157 |    - Follow their setup guide to obtain your project credentials
158 |    - Fill in the corresponding environment variables
159 | 
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "$schema": "https://ui.shadcn.com/schema.json",
 3 |   "style": "new-york",
 4 |   "rsc": true,
 5 |   "tsx": true,
 6 |   "tailwind": {
 7 |     "config": "tailwind.config.ts",
 8 |     "css": "src/styles/globals.css",
 9 |     "baseColor": "zinc",
10 |     "cssVariables": true,
11 |     "prefix": ""
12 |   },
13 |   "aliases": {
14 |     "components": "@/components",
15 |     "utils": "@/lib/utils",
16 |     "ui": "@/components/ui",
17 |     "lib": "@/lib",
18 |     "hooks": "@/hooks"
19 |   },
20 |   "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/cursorrules:
--------------------------------------------------------------------------------
  1 | You are an expert in TypeScript, Next.js, and full-stack development, specializing in creating clean, modular code with modern best practices.
  2 | 
  3 | # Tech Stack
  4 | - Next.js 15 with App Router for file-based routing
  5 | - TypeScript for type-safe development
  6 | - tRPC for end-to-end typesafe APIs
  7 | - Prisma with PostgreSQL for database management
  8 | - ShadcN UI for component library
  9 | - MagicUI for enhanced UI components
 10 | - TailwindCSS for styling
 11 | - PostHog for analytics
 12 | - React Query (through tRPC) for data fetching
 13 | - Zod for schema validation
 14 | 
 15 | # Project Structure
 16 | /src/app/** -> Next.js App Router pages and layouts
 17 | /src/components/** -> Reusable UI components
 18 | /src/hooks/** -> Custom React hooks
 19 | /src/lib/** -> Core utilities and services
 20 | /src/provider/** -> Context providers and wrappers
 21 | /src/server/** -> Server-side logic
 22 | /src/styles/** -> Global styles and Tailwind configurations
 23 | /src/trpc/** -> tRPC router and procedures
 24 | /src/types/** -> TypeScript types and interfaces
 25 | /prisma/** -> Database schema and migrations
 26 | 
 27 | # Coding Standards
 28 | - Use TypeScript for all files except configurations
 29 | - Implement functional components with hooks
 30 | - Keep components focused and small (< 200 lines)
 31 | - Use Tailwind for styling with consistent design tokens
 32 | - Follow mobile-first responsive design
 33 | - Implement proper error handling with tRPC
 34 | - Use React Query features through tRPC for data management
 35 | 
 36 | # Import Patterns
 37 | - Use absolute imports with @ prefix
 38 | - Import UI components from @/components/ui
 39 | - Import server procedures from @/server/api
 40 | - Import types from @/types
 41 | - Import hooks from @/hooks
 42 | 
 43 | # Style Guidelines
 44 | - Use ShadcN UI components from @/components/ui
 45 | - Implement className="..." for Tailwind styling
 46 | - Support dark mode with next-themes
 47 | - Follow mobile-first responsive design
 48 | - Use semantic class names and design tokens
 49 | 
 50 | # Type Safety
 51 | - Use interfaces for complex objects, type for simple ones
 52 | - Use Zod for runtime validation
 53 | - Leverage tRPC's type inference
 54 | - Avoid any type unless absolutely necessary
 55 | - Use strict TypeScript configurations
 56 | 
 57 | # Component Pattern
 58 | ```typescript
 59 | import { type ComponentProps } from 'react';
 60 | import { cn } from '@/lib/utils';
 61 | 
 62 | interface Props extends ComponentProps<'div'> {
 63 |   title: string;
 64 | }
 65 | 
 66 | export function Component({ 
 67 |   title, 
 68 |   className,
 69 |   ...props 
 70 | }: Props) {
 71 |   return (
 72 |     
 79 |       {/* Component content */}
 80 |     
 81 |   );
 82 | }
 83 | ```
 84 | 
 85 | # tRPC Router Pattern
 86 | ```typescript
 87 | import { z } from 'zod';
 88 | import { createTRPCRouter, publicProcedure, protectedProcedure } from '../trpc';
 89 | 
 90 | export const router = createTRPCRouter({
 91 |   create: protectedProcedure
 92 |     .input(z.object({
 93 |       // validation schema
 94 |     }))
 95 |     .mutation(async ({ ctx, input }) => {
 96 |       // implementation
 97 |     }),
 98 | });
 99 | ```
100 | 
101 | # Prisma Pattern
102 | ```typescript
103 | // prisma/schema.prisma
104 | model User {
105 |   id        String   @id @default(cuid())
106 |   email     String   @unique
107 |   name      String?
108 |   createdAt DateTime @default(now())
109 |   updatedAt DateTime @updatedAt
110 | }
111 | ``` 
--------------------------------------------------------------------------------
/next-sitemap.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next-sitemap').IConfig} */
2 | module.exports = {
3 | 	siteUrl: process.env.SITE_URL ?? "https://example.com",
4 | 	generateRobotsTxt: false,
5 | };
6 | 
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful
 3 |  * for Docker builds.
 4 |  */
 5 | import "./src/env.js";
 6 | import { withSentryConfig } from "@sentry/nextjs";
 7 | 
 8 | /** @type {import("next").NextConfig} */
 9 | const config = {
10 |   images: {
11 |     remotePatterns: [
12 |       {
13 |         // google profile images
14 |         protocol: "https",
15 |         hostname: "lh3.googleusercontent.com",
16 |       },
17 |       {
18 |         // github profile images
19 |         protocol: "https",
20 |         hostname: "avatars.githubusercontent.com",
21 |       },
22 |       {
23 |         // vercel profile images
24 |         protocol: "https",
25 |         hostname: "avatar.vercel.sh",
26 |       },
27 |       {
28 |         // vercel profile images
29 |         protocol: "https",
30 |         hostname: "startup-template-sage.vercel.app",
31 |       },
32 |       {
33 |         // supabase profile images
34 |         protocol: "https",
35 |         hostname: "qpdwualqgmqaxfgoapyc.supabase.co",
36 |       },
37 |     ],
38 |   },
39 | };
40 | 
41 | // Sentry configuration
42 | const sentryConfig = {
43 |   org: process.env.SENTRY_ORG,
44 |   project: process.env.SENTRY_PROJECT,
45 |   
46 |   // Only print logs for uploading source maps in CI
47 |   silent: !process.env.CI,
48 | 
49 | 
50 |   // For all available options, see:
51 |   // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
52 | 
53 |   
54 |   // For all available options, see:
55 |   // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
56 | 
57 |   // Upload a larger set of source maps for prettier stack traces (increases build time)
58 |   widenClientFileUpload: true,
59 |   
60 |   // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers
61 |   tunnelRoute: "/monitoring",
62 |   
63 |   // Hides source maps from generated client bundles
64 |   hideSourceMaps: true,
65 |   
66 |   // Automatically tree-shake Sentry logger statements to reduce bundle size
67 |   disableLogger: true,
68 |   
69 |   // Enables automatic instrumentation of Vercel Cron Monitors
70 |   automaticVercelMonitors: true,
71 | };
72 | 
73 | export default withSentryConfig(config, sentryConfig);
74 | 
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "saas-boilerplate",
 3 |   "version": "0.1.0",
 4 |   "private": true,
 5 |   "type": "module",
 6 |   "scripts": {
 7 |     "build": "next build",
 8 |     "postbuild": "next-sitemap --config next-sitemap.config.cjs",
 9 |     "check": "next lint && tsc --noEmit",
10 |     "db:generate": "prisma migrate dev",
11 |     "db:migrate": "prisma migrate deploy",
12 |     "db:push": "prisma db push",
13 |     "db:studio": "prisma studio",
14 |     "dev": "next dev --turbo",
15 |     "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
16 |     "format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache",
17 |     "postinstall": "prisma generate",
18 |     "lint": "next lint",
19 |     "lint:fix": "next lint --fix",
20 |     "preview": "next build && next start",
21 |     "start": "next start",
22 |     "typecheck": "tsc --noEmit"
23 |   },
24 |   "dependencies": {
25 |     "@prisma/client": "^6.1.0",
26 |     "@radix-ui/react-avatar": "^1.1.2",
27 |     "@radix-ui/react-collapsible": "^1.1.2",
28 |     "@radix-ui/react-dialog": "^1.1.4",
29 |     "@radix-ui/react-dropdown-menu": "^2.1.4",
30 |     "@radix-ui/react-icons": "^1.3.2",
31 |     "@radix-ui/react-label": "^2.1.1",
32 |     "@radix-ui/react-separator": "^1.1.1",
33 |     "@radix-ui/react-slot": "^1.1.1",
34 |     "@radix-ui/react-switch": "^1.1.2",
35 |     "@radix-ui/react-tooltip": "^1.1.6",
36 |     "@react-email/components": "^0.0.31",
37 |     "@sentry/nextjs": "^8",
38 |     "@stackframe/stack": "^2.6.39",
39 |     "@t3-oss/env-nextjs": "^0.10.1",
40 |     "@tanstack/react-query": "^5.50.0",
41 |     "@trpc/client": "^11.0.0-rc.446",
42 |     "@trpc/react-query": "^11.0.0-rc.446",
43 |     "@trpc/server": "^11.0.0-rc.446",
44 |     "class-variance-authority": "^0.7.1",
45 |     "clsx": "^2.1.1",
46 |     "framer-motion": "^11.15.0",
47 |     "geist": "^1.3.0",
48 |     "lucide-react": "^0.469.0",
49 |     "next": "^15.0.1",
50 |     "next-sitemap": "^4.2.3",
51 |     "posthog-js": "^1.203.1",
52 |     "posthog-node": "^4.3.2",
53 |     "react": "^18.3.1",
54 |     "react-dom": "^18.3.1",
55 |     "react-email": "^3.0.4",
56 |     "resend": "^4.0.1",
57 |     "server-only": "^0.0.1",
58 |     "superjson": "^2.2.1",
59 |     "tailwind-merge": "^2.5.5",
60 |     "tailwindcss-animate": "^1.0.7",
61 |     "zod": "^3.23.3"
62 |   },
63 |   "devDependencies": {
64 |     "@types/eslint": "^8.56.10",
65 |     "@types/node": "^20.14.10",
66 |     "@types/react": "^18.3.3",
67 |     "@types/react-dom": "^18.3.0",
68 |     "@typescript-eslint/eslint-plugin": "^8.1.0",
69 |     "@typescript-eslint/parser": "^8.1.0",
70 |     "eslint": "^8.57.0",
71 |     "eslint-config-next": "^15.0.1",
72 |     "postcss": "^8.4.39",
73 |     "prettier": "^3.3.2",
74 |     "prettier-plugin-tailwindcss": "^0.6.5",
75 |     "prisma": "^5.14.0",
76 |     "tailwindcss": "^3.4.3",
77 |     "typescript": "^5.5.3"
78 |   },
79 |   "ct3aMetadata": {
80 |     "initVersion": "7.38.1"
81 |   },
82 |   "packageManager": "pnpm@9.9.0"
83 | }
84 | 
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 |   plugins: {
3 |     tailwindcss: {},
4 |   },
5 | };
6 | 
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').PluginOptions} */
2 | export default {
3 |   plugins: ["prettier-plugin-tailwindcss"],
4 | };
5 | 
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
 1 | // This is your Prisma schema file,
 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
 3 | 
 4 | generator client {
 5 |     provider = "prisma-client-js"
 6 | }
 7 | 
 8 | datasource db {
 9 |     provider = "postgresql"
10 |     url      = env("DATABASE_URL")
11 | }
12 | 
13 | model Post {
14 |     id        Int      @id @default(autoincrement())
15 |     name      String
16 |     createdAt DateTime @default(now())
17 |     updatedAt DateTime @updatedAt
18 | 
19 |     @@index([name])
20 | }
21 | 
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robinfaraj/saas-boilerplate/4eab0ba953fa1d4d2de58d28c371b075142e069e/public/favicon.ico
--------------------------------------------------------------------------------
/public/sitemap.xml:
--------------------------------------------------------------------------------
1 | 
2 | 
3 |  
--------------------------------------------------------------------------------
/sentry.client.config.ts:
--------------------------------------------------------------------------------
 1 | // This file configures the initialization of Sentry on the client.
 2 | // The config you add here will be used whenever a users loads a page in their browser.
 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/
 4 | 
 5 | import * as Sentry from "@sentry/nextjs";
 6 | 
 7 | Sentry.init({
 8 |   dsn: "https://52fc7cd5d5f21b308f7460a3ac02b676@o4507856717938688.ingest.de.sentry.io/4507856718659664",
 9 | 
10 |   // Add optional integrations for additional features
11 |   integrations: [Sentry.replayIntegration()],
12 | 
13 |   // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
14 |   tracesSampleRate: 1,
15 | 
16 |   // Define how likely Replay events are sampled.
17 |   // This sets the sample rate to be 10%. You may want this to be 100% while
18 |   // in development and sample at a lower rate in production
19 |   replaysSessionSampleRate: 0.1,
20 | 
21 |   // Define how likely Replay events are sampled when an error occurs.
22 |   replaysOnErrorSampleRate: 1.0,
23 | 
24 |   // Setting this option to true will print useful information to the console while you're setting up Sentry.
25 |   debug: false,
26 | });
27 | 
--------------------------------------------------------------------------------
/sentry.edge.config.ts:
--------------------------------------------------------------------------------
 1 | // This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
 2 | // The config you add here will be used whenever one of the edge features is loaded.
 3 | // Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
 4 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/
 5 | 
 6 | import * as Sentry from "@sentry/nextjs";
 7 | 
 8 | Sentry.init({
 9 |   dsn: "https://52fc7cd5d5f21b308f7460a3ac02b676@o4507856717938688.ingest.de.sentry.io/4507856718659664",
10 | 
11 |   // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
12 |   tracesSampleRate: 1,
13 | 
14 |   // Setting this option to true will print useful information to the console while you're setting up Sentry.
15 |   debug: false,
16 | });
17 | 
--------------------------------------------------------------------------------
/sentry.server.config.ts:
--------------------------------------------------------------------------------
 1 | // This file configures the initialization of Sentry on the server.
 2 | // The config you add here will be used whenever the server handles a request.
 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/
 4 | 
 5 | import * as Sentry from "@sentry/nextjs";
 6 | 
 7 | Sentry.init({
 8 |   dsn: "https://52fc7cd5d5f21b308f7460a3ac02b676@o4507856717938688.ingest.de.sentry.io/4507856718659664",
 9 | 
10 |   // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
11 |   tracesSampleRate: 1,
12 | 
13 |   // Setting this option to true will print useful information to the console while you're setting up Sentry.
14 |   debug: false,
15 | });
16 | 
--------------------------------------------------------------------------------
/src/app/(marketing)/(home)/page.tsx:
--------------------------------------------------------------------------------
 1 | import { HeroSection } from "@/components/marketing/sections/hero-section";
 2 | import { PricingSection } from "@/components/marketing/sections/pricing-section";
 3 | import { Testimonials } from "@/components/marketing/testimonials/testimonials";
 4 | import { FeatureSection } from "@/components/marketing/sections/feature-section";
 5 | 
 6 | 
 7 | export default function MarketingPage() {
 8 |   return (
 9 |     <>
10 |        
11 |        
12 |        
13 |        
14 |     >
15 |   )
16 | }
17 | 
--------------------------------------------------------------------------------
/src/app/(marketing)/layout.tsx:
--------------------------------------------------------------------------------
 1 | import { Footer } from "@/components/marketing/shared/footer";
 2 | import { NavBar } from "@/components/marketing/shared/navbar";
 3 | import type { PropsWithChildren } from "react";
 4 | 
 5 | export default async function MarketingLayout({ children }: PropsWithChildren) {
 6 | 	return (
 7 | 		<>
 8 | 			 
 9 | 			{children} 
10 | 			
11 | 		>
12 | 	);
13 | }
14 | 
--------------------------------------------------------------------------------
/src/app/(saas)/app/page.tsx:
--------------------------------------------------------------------------------
 1 | import { LatestPost } from "@/components/saas/post";
 2 | import { api } from "@/trpc/server";
 3 | import { stackServerApp } from "@/stack";
 4 | import { Breadcrumb, BreadcrumbList, BreadcrumbItem, BreadcrumbLink, BreadcrumbSeparator, BreadcrumbPage } from "@/components/ui/breadcrumb";
 5 | import { SidebarTrigger } from "@/components/ui/sidebar";
 6 | import { Separator } from "@radix-ui/react-separator";
 7 | 
 8 | export default async function Home() {
 9 |   await stackServerApp.getUser({ or: "redirect" }); // prevent unauthenticated users from accessing the page
10 | 
11 |   const hello = await api.post.hello({ text: "from tRPC" });
12 | 
13 |   void api.post.getLatest.prefetch();
14 | 
15 |   return (
16 |     <>
17 |     
36 |           
37 |             
42 |             
43 |                    
44 |              
45 |                {hello ? hello.greeting : "Loading tRPC query..."}
46 |              
47 |            
48 | 
49 |           
50 |          
51 |           
 
52 |           >
53 |     // 
54 |     //   
55 |     //     
56 |     //       
57 |     //         Create T3  App
58 |     //        
59 |     //       
60 |     //         
65 |     //           
First Steps → 
66 |     //           
67 |     //             Just the basics - Everything you need to know to set up your
68 |     //             database and authentication.
69 |     //           
70 |     //         
71 |     //         
76 |     //           
Documentation → 
77 |     //           
78 |     //             Learn more about Create T3 App, the libraries it uses, and how
79 |     //             to deploy it.
80 |     //           
81 |     //         
82 |     //       
83 |     //       
84 |     //         
85 |     //           {hello ? hello.greeting : "Loading tRPC query..."}
86 |     //         
87 |     //       
88 | 
89 |     //       
90 |     //     
 
91 |     //    
92 |     //  
93 |   );
94 | }
95 | 
--------------------------------------------------------------------------------
/src/app/(saas)/layout.tsx:
--------------------------------------------------------------------------------
 1 | import { AppSidebar } from "@/components/saas/app-sidebar";
 2 | import { SidebarProvider, SidebarInset } from "@/components/ui/sidebar";
 3 | import type { PropsWithChildren } from "react";
 4 | 
 5 | export default async function MarketingLayout({ children }: PropsWithChildren) {
 6 | 	return (
 7 |         
 8 |          
 9 |         
10 |           {children}
11 |          
12 |        
13 | 	);
14 | }
15 | 
--------------------------------------------------------------------------------
/src/app/api/sentry-example-api/route.ts:
--------------------------------------------------------------------------------
 1 | import { NextResponse } from "next/server";
 2 | 
 3 | export const dynamic = "force-dynamic";
 4 | 
 5 | // A faulty API route to test Sentry's error monitoring
 6 | export function GET() {
 7 |   throw new Error("Sentry Example API Route Error");
 8 |   return NextResponse.json({ data: "Testing Sentry Error..." });
 9 | }
10 | 
--------------------------------------------------------------------------------
/src/app/api/trpc/[trpc]/route.ts:
--------------------------------------------------------------------------------
 1 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
 2 | import { type NextRequest } from "next/server";
 3 | 
 4 | import { env } from "@/env";
 5 | import { appRouter } from "@/server/api/root";
 6 | import { createTRPCContext } from "@/server/api/trpc";
 7 | 
 8 | /**
 9 |  * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when
10 |  * handling a HTTP request (e.g. when you make requests from Client Components).
11 |  */
12 | const createContext = async (req: NextRequest) => {
13 |   return createTRPCContext({
14 |     headers: req.headers,
15 |   });
16 | };
17 | 
18 | const handler = (req: NextRequest) =>
19 |   fetchRequestHandler({
20 |     endpoint: "/api/trpc",
21 |     req,
22 |     router: appRouter,
23 |     createContext: () => createContext(req),
24 |     onError:
25 |       env.NODE_ENV === "development"
26 |         ? ({ path, error }) => {
27 |             console.error(
28 |               `❌ tRPC failed on ${path ?? ""}: ${error.message}`
29 |             );
30 |           }
31 |         : undefined,
32 |   });
33 | 
34 | export { handler as GET, handler as POST };
35 | 
--------------------------------------------------------------------------------
/src/app/dashboard/page.tsx:
--------------------------------------------------------------------------------
 1 | import { AppSidebar } from "@/components/saas/app-sidebar"
 2 | import {
 3 |   Breadcrumb,
 4 |   BreadcrumbItem,
 5 |   BreadcrumbLink,
 6 |   BreadcrumbList,
 7 |   BreadcrumbPage,
 8 |   BreadcrumbSeparator,
 9 | } from "@/components/ui/breadcrumb"
10 | import { Separator } from "@/components/ui/separator"
11 | import {
12 |   SidebarInset,
13 |   SidebarProvider,
14 |   SidebarTrigger,
15 | } from "@/components/ui/sidebar"
16 | 
17 | export default function Page() {
18 |   return (
19 |     
20 |        
21 |       
22 |         
41 |         
49 |        
50 |      
51 |   )
52 | }
53 | 
--------------------------------------------------------------------------------
/src/app/global-error.tsx:
--------------------------------------------------------------------------------
 1 | "use client";
 2 | 
 3 | import * as Sentry from "@sentry/nextjs";
 4 | import NextError from "next/error";
 5 | import { useEffect } from "react";
 6 | 
 7 | export default function GlobalError({
 8 |   error,
 9 | }: {
10 |   error: Error & { digest?: string };
11 | }) {
12 |   useEffect(() => {
13 |     Sentry.captureException(error);
14 |   }, [error]);
15 | 
16 |   return (
17 |     
18 |       
19 |         {/* `NextError` is the default Next.js error page component. Its type
20 |         definition requires a `statusCode` prop. However, since the App Router
21 |         does not expose status codes for errors, we simply pass 0 to render a
22 |         generic error message. */}
23 |          
24 |       
25 |     
26 |   );
27 | }
28 | 
--------------------------------------------------------------------------------
/src/app/handler/[...stack]/page.tsx:
--------------------------------------------------------------------------------
1 | import { StackHandler } from "@stackframe/stack";
2 | import { stackServerApp } from "@/stack";
3 | 
4 | export default function Handler(props: unknown) {
5 |   return  ;
6 | }
7 | 
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
 1 | import "@/styles/globals.css";
 2 | 
 3 | import { StackProvider, StackTheme } from "@stackframe/stack";
 4 | import { stackServerApp } from "@/stack";
 5 | import { GeistSans } from "geist/font/sans";
 6 | import { type Metadata } from "next";
 7 | 
 8 | import { TRPCReactProvider } from "@/trpc/react";
 9 | import { PosthogProvider } from "@/provider/posthog-provider";
10 | 
11 | export const metadata: Metadata = {
12 |   title: "SaaS Template",
13 |   description: "Created by Robin Faraj",
14 |   icons: [{ rel: "icon", url: "/favicon.ico" }],
15 | };
16 | 
17 | export default function RootLayout({
18 |   children,
19 | }: Readonly<{ children: React.ReactNode }>) {
20 |   return (
21 |     
22 |       
27 |       
28 | 
29 |       
30 |           
31 |             {children} 
32 |            
33 |          
34 |        
35 |       
36 |     
37 |   );
38 | }
39 | 
--------------------------------------------------------------------------------
/src/app/loading.tsx:
--------------------------------------------------------------------------------
1 | export default function Loading() {
2 |   // Stack uses React Suspense, which will render this page while user data is being fetched.
3 |   // See: https://nextjs.org/docs/app/api-reference/file-conventions/loading
4 |   return <>>;
5 | }
6 | 
--------------------------------------------------------------------------------
/src/app/sentry-example-page/page.tsx:
--------------------------------------------------------------------------------
 1 | "use client";
 2 | 
 3 | import Head from "next/head";
 4 | import * as Sentry from "@sentry/nextjs";
 5 | 
 6 | export default function Page() {
 7 |   return (
 8 |     
 9 |       
10 |         
Sentry Onboarding 
11 |         
12 |       
13 | 
14 |       
23 |         
24 |           
31 |              
35 |            
36 |          
37 | 
38 |         Get started by sending us a sample error:
39 |          {
52 |             await Sentry.startSpan(
53 |               {
54 |                 name: "Example Frontend Span",
55 |                 op: "test",
56 |               },
57 |               async () => {
58 |                 const res = await fetch("/api/sentry-example-api");
59 |                 if (!res.ok) {
60 |                   throw new Error("Sentry Example Frontend Error");
61 |                 }
62 |               },
63 |             );
64 |           }}
65 |         >
66 |           Throw error!
67 |          
68 | 
69 |         
70 |           Next, look for the error on the{" "}
71 |           
72 |             Issues Page
73 |            
74 |           .
75 |         
76 |         
77 |           For more information, see{" "}
78 |           
79 |             https://docs.sentry.io/platforms/javascript/guides/nextjs/
80 |            
81 |         
82 |        
83 |     
 
84 |   );
85 | }
86 | 
--------------------------------------------------------------------------------
/src/components/emails/email-template.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react";
 2 | 
 3 | interface EmailTemplateProps {
 4 |   firstName: string;
 5 | }
 6 | 
 7 | // this is an example email template
 8 | export const EmailTemplate: React.FC> = ({
 9 |   firstName,
10 | }) => (
11 |   
12 |     
Welcome, {firstName}! 
13 |   
14 | );
15 | 
--------------------------------------------------------------------------------
/src/components/marketing/features/animated-list-demo.tsx:
--------------------------------------------------------------------------------
  1 | "use client";
  2 | 
  3 | import { cn } from "@/lib/utils";
  4 | import { AnimatedList } from "../../ui/animated-list";
  5 | 
  6 | interface Item {
  7 |   name: string;
  8 |   description: string;
  9 |   icon: string;
 10 |   color: string;
 11 |   time: string;
 12 | }
 13 | 
 14 | let notifications = [
 15 |   {
 16 |     name: "Payment received",
 17 |     description: "Magic UI",
 18 |     time: "15m ago",
 19 | 
 20 |     icon: "💸",
 21 |     color: "#00C9A7",
 22 |   },
 23 |   {
 24 |     name: "User signed up",
 25 |     description: "Magic UI",
 26 |     time: "10m ago",
 27 |     icon: "👤",
 28 |     color: "#FFB800",
 29 |   },
 30 |   {
 31 |     name: "New message",
 32 |     description: "Magic UI",
 33 |     time: "5m ago",
 34 |     icon: "💬",
 35 |     color: "#FF3D71",
 36 |   },
 37 |   {
 38 |     name: "New event",
 39 |     description: "Magic UI",
 40 |     time: "2m ago",
 41 |     icon: "🗞️",
 42 |     color: "#1E86FF",
 43 |   },
 44 | ];
 45 | 
 46 | notifications = Array.from({ length: 10 }, () => notifications).flat();
 47 | 
 48 | const Notification = ({ name, description, icon, color, time }: Item) => {
 49 |   return (
 50 |     
 61 |       
 62 |         
 68 |           {icon} 
 69 |         
 70 |         
 71 |           
 72 |             {name} 
 73 |             · 
 74 |             {time} 
 75 |            
 76 |           
 77 |             {description}
 78 |           
 79 |         
 80 |       
 
 81 |      
 82 |   );
 83 | };
 84 | 
 85 | export function AnimatedListDemo({
 86 |                                    className,
 87 |                                  }: {
 88 |   className?: string;
 89 | }) {
 90 |   return (
 91 |     
 97 |       
 98 |         {notifications.map((item, idx) => (
 99 |            
100 |         ))}
101 |        
102 |     
 
103 |   );
104 | }
105 | 
--------------------------------------------------------------------------------
/src/components/marketing/hero/assurance-items.tsx:
--------------------------------------------------------------------------------
 1 | import { Check } from "lucide-react";
 2 | 
 3 | interface AssuranceItemProps {
 4 |   text: string;
 5 | }
 6 | 
 7 | export function AssuranceItem({ text }: AssuranceItemProps) {
 8 |   return (
 9 |     
10 |       
11 |          
12 |       
13 |       
{text} 
14 |     
 
15 |   );
16 | }
--------------------------------------------------------------------------------
/src/components/marketing/hero/hero-banner.tsx:
--------------------------------------------------------------------------------
 1 | import { LucideGift } from "lucide-react";
 2 | import AnimatedShinyText from "@/components/ui/animated-shiny-text";
 3 | 
 4 | export default function HeroBanner() {
 5 |   return (
 6 |     
 7 |       
 8 |         
 9 |            
10 |           50$ off
11 |          
12 |         
13 |           with our Launch Deal!
14 |          
15 |        
16 |     
 
17 |   );
18 | }
19 | 
--------------------------------------------------------------------------------
/src/components/marketing/hero/hero-title.tsx:
--------------------------------------------------------------------------------
 1 | import { GradientText } from '@/components/ui/gradient-text'
 2 | 
 3 | export function HeroTitle() {
 4 |   return (
 5 |     
 6 |       Open Source{' '}
 7 |       LLM Engineering 
 8 |        
 9 |       Platform
10 |      
11 |   )
12 | } 
--------------------------------------------------------------------------------
/src/components/marketing/hero/testimonials-avatars.tsx:
--------------------------------------------------------------------------------
 1 | import { Star } from "lucide-react";
 2 | import AvatarCircles from "../../ui/avatar-circles";
 3 | 
 4 | const avatars = [
 5 | 	{
 6 | 	  imageUrl: "https://qpdwualqgmqaxfgoapyc.supabase.co/storage/v1/object/public/appboilerplate/avatars/Profile%20Facelift.jpg?t=2024-12-23T11%3A27%3A14.031Z",
 7 | 	  profileUrl: "https://x.com/robin_faraj",
 8 | 	},
 9 | 	{
10 | 	  imageUrl: "https://qpdwualqgmqaxfgoapyc.supabase.co/storage/v1/object/public/appboilerplate/landingpage/andrei.jpg",
11 | 	  profileUrl: "https://x.com/AndreiHudovich",
12 | 	},
13 | 	{
14 | 	  imageUrl: "https://qpdwualqgmqaxfgoapyc.supabase.co/storage/v1/object/public/appboilerplate/landingpage/miguel_new.jpg?t=2024-12-23T11%3A31%3A29.278Z",
15 | 	  profileUrl: "https://x.com/fullstackmiguel",
16 | 	},
17 | 	{
18 | 	  imageUrl: "https://qpdwualqgmqaxfgoapyc.supabase.co/storage/v1/object/public/appboilerplate/landingpage/adam.jpg?t=2024-12-23T11%3A31%3A19.946Z",
19 | 	  profileUrl: "https://x.com/AdamBartas",
20 | 	},
21 |   ];
22 |   
23 | 
24 | export function TestimonialsAvatars() {
25 | 	return (
26 | 		
27 | 			
28 | 			
29 | 				
Loved by Founders
30 | 				
31 | 					{Array.from({ length: 5 }, (_, i) => (
32 | 						 
36 | 					))}
37 | 				
38 | 			
39 | 		
 
40 | 	);
41 | }
42 | 
--------------------------------------------------------------------------------
/src/components/marketing/pricing/plan-card.tsx:
--------------------------------------------------------------------------------
 1 | import { Button } from "@/components/ui/button";
 2 | import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card";
 3 | import { CheckCircle } from "lucide-react";
 4 | import { Badge } from "@/components/ui/badge";
 5 | import { type PricingPlan } from "@/types/pricing";
 6 | 
 7 | interface PlanCardProps {
 8 |   plan: PricingPlan,
 9 |   isYearly: boolean
10 | }
11 | 
12 | export const PlanCard: React.FC = ({
13 |     plan,
14 |     isYearly
15 |   }) => {
16 |     return (
17 |       
23 |         
24 |           {plan.popular && (
25 |             
26 |               Most popular
27 |              
28 |           )}
29 |           {plan.title} 
30 |           
31 |             {isYearly ? plan.price.yearly : plan.price.monthly}
32 |            
33 |          
34 |         
35 |           {plan.description}
36 |          
37 |         
38 |           
39 |             {plan.features.map((feature, idx) => (
40 |               
41 |                  
42 |                 {feature} 
43 |                
44 |             ))}
45 |            
46 |          
47 |         
48 |           
53 |             Sign up
54 |            
55 |          
56 |        
57 |     );
58 |   };
--------------------------------------------------------------------------------
/src/components/marketing/sections/feature-section.tsx:
--------------------------------------------------------------------------------
  1 | import { CalendarIcon, FileTextIcon } from "@radix-ui/react-icons";
  2 | import { BellIcon, Share2Icon } from "lucide-react";
  3 | 
  4 | import { cn } from "@/lib/utils";
  5 | import Marquee from "../../ui/marquee";
  6 | import { BentoCard, BentoGrid } from "@/components/ui/bento-grid";
  7 | import { AnimatedListDemo } from "@/components/marketing/features/animated-list-demo";
  8 | import { AnimatedBeamMultipleOutputDemo } from "@/components/marketing/features/animated-beam-multiple-outputs";
  9 | import type React from "react";
 10 | import { SectionTitle } from "../shared/section-title";
 11 | 
 12 | 
 13 | const files = [
 14 |   {
 15 |     name: "bitcoin.pdf",
 16 |     body: "Bitcoin is a cryptocurrency invented in 2008 by an unknown person or group of people using the name Satoshi Nakamoto.",
 17 |   },
 18 |   {
 19 |     name: "finances.xlsx",
 20 |     body: "A spreadsheet or worksheet is a file made of rows and columns that help sort data, arrange data easily, and calculate numerical data.",
 21 |   },
 22 |   {
 23 |     name: "logo.svg",
 24 |     body: "Scalable Vector Graphics is an Extensible Markup Language-based vector image format for two-dimensional graphics with support for interactivity and animation.",
 25 |   },
 26 |   {
 27 |     name: "keys.gpg",
 28 |     body: "GPG keys are used to encrypt and decrypt email, files, directories, and whole disk partitions and to authenticate messages.",
 29 |   },
 30 |   {
 31 |     name: "seed.txt",
 32 |     body: "A seed phrase, seed recovery phrase or backup seed phrase is a list of words which store all the information needed to recover Bitcoin funds on-chain.",
 33 |   },
 34 | ];
 35 | 
 36 | const featureSection = [
 37 |   {
 38 |     Icon: FileTextIcon,
 39 |     name: "Save your files",
 40 |     description: "We automatically save your files as you type.",
 41 |     href: "#",
 42 |     cta: "Learn more",
 43 |     className: "col-span-3 lg:col-span-1",
 44 |     background: (
 45 |       
 49 |         {files.map((f, idx) => (
 50 |           
 59 |             
 60 |               
 61 |                 
 62 |                   {f.name}
 63 |                  
 64 |               
 65 |             
 
 66 |             {f.body} 
 67 |            
 68 |         ))}
 69 |        
 70 |     ),
 71 |   },
 72 |   {
 73 |     Icon: BellIcon,
 74 |     name: "Notifications",
 75 |     description: "Get notified when something happens.",
 76 |     href: "#",
 77 |     cta: "Learn more",
 78 |     className: "col-span-3 lg:col-span-2",
 79 |     background: (
 80 |        
 81 |     ),
 82 |   },
 83 |   {
 84 |     Icon: Share2Icon,
 85 |     name: "Integrations",
 86 |     description: "Supports 100+ integrations and counting.",
 87 |     href: "#",
 88 |     cta: "Learn more",
 89 |     className: "col-span-3 lg:col-span-2",
 90 |     background: (
 91 |        
 92 |     ),
 93 |   },
 94 |   {
 95 |     Icon: CalendarIcon,
 96 |     name: "Calendar",
 97 |     description: "Use the calendar to filter your files by date.",
 98 |     className: "col-span-3 lg:col-span-1",
 99 |     href: "#",
100 |     cta: "Learn more",
101 |     background: (
102 |       <>>
103 |     ),
104 |   },
105 | ];
106 | 
107 | export function FeatureSection() {
108 |   return (
109 |     
110 |        
111 |       
112 |         {featureSection.map((feature, idx) => (
113 |            
114 |         ))}
115 |        
116 |     
117 | 
118 |   );
119 | }
120 | 
--------------------------------------------------------------------------------
/src/components/marketing/sections/hero-section.tsx:
--------------------------------------------------------------------------------
  1 | "use client";
  2 | 
  3 | import { motion } from "framer-motion";
  4 | import HeroBanner from "../hero/hero-banner";
  5 | import { TestimonialsAvatars } from "../hero/testimonials-avatars";
  6 | import { RainbowButton } from "../../ui/rainbow-button";
  7 | import { cn } from "@/lib/utils";
  8 | import AnimatedGridPattern from "../../ui/animated-grid-pattern";
  9 | import HeroVideoDialog from "../../ui/hero-video-dialog";
 10 | import { AssuranceItem } from "../hero/assurance-items";
 11 | 
 12 | const ease = [0.16, 1, 0.3, 1];
 13 | 
 14 | export function HeroSection() {
 15 | 	return (
 16 | 		
 17 | 			
 27 | 			
 28 | 				
 34 | 					 
 35 | 				 
 36 | 				
 37 | 					
 43 | 						{["The", "Ultimate", "SaaS", "Techstack"].map(
 44 | 							(text, index) => (
 45 | 								
 56 | 									{text}
 57 | 								 
 58 | 							),
 59 | 						)}
 60 | 					 
 61 | 
 62 | 					
 68 | 						Your product is ready to go. Start selling it today. This is the best way to get your product out there and start generating revenue.
 69 | 					 
 70 | 				
 71 | 				
 77 | 					 
 78 | 					 
 79 | 					 
 80 | 				 
 81 | 				
 87 | 					Get Started Now 
 88 | 				 
 89 | 				
 95 | 					 
 96 | 				 
 97 | 
 98 | 			
 99 | 			
105 | 
106 | 				 
113 | 				 
120 | 			 
121 | 		
 
122 | 	);
123 | }
124 | 
--------------------------------------------------------------------------------
/src/components/marketing/sections/pricing-section.tsx:
--------------------------------------------------------------------------------
 1 | "use client";
 2 | 
 3 | import { Badge } from "@/components/ui/badge";
 4 | import { Switch } from "../../ui/switch";
 5 | import { Label } from "../../ui/label";
 6 | import React, { useState } from "react";
 7 | import { PlanCard } from "../pricing/plan-card";
 8 | import { type PricingPlan } from "@/types/pricing";
 9 | import { SectionTitle } from "@/components/marketing/shared/section-title";
10 | 
11 | 
12 | const pricingPlans: PricingPlan[] = [
13 |   {
14 |     title: "Free",
15 |     description: "Forever free",
16 |     price: {
17 |       monthly: "Free",
18 |       yearly: "Free"
19 |     },
20 |     popular: false,
21 |     features: [
22 |       "1 user",
23 |       "Plan features",
24 |       "Product support",
25 |     ],
26 |   },
27 |   {
28 |     title: "Startup",
29 |     description: "All the basics for starting a new business",
30 |     price: {
31 |       monthly: "$39",
32 |       yearly: "$390"
33 |     },
34 |     popular: true,
35 |     features: [
36 |       "2 users",
37 |       "Plan features",
38 |       "Product support",
39 |     ],
40 |   },
41 | ];
42 | 
43 | export function PricingSection() {
44 |   const [isYearly, setIsYearly] = useState(false);
45 | 
46 |   return (
47 |     
48 |       
49 |         
50 | 
51 |        
52 |           
53 |             Monthly
54 |            
55 |           
56 |           
57 |             Annual
58 |             
59 |               
60 |                 
68 |                    
73 |                  
74 |                 Save up to 10% 
75 |                
76 |              
77 |            
78 |         
79 |         
80 |           {pricingPlans.map((plan, index) => (
81 |             
82 |           ))}
83 |         
84 |       
 
85 |      
86 |   );
87 | }
88 | 
89 | 
90 | 
--------------------------------------------------------------------------------
/src/components/marketing/shared/footer.tsx:
--------------------------------------------------------------------------------
 1 | import Link from "next/link"
 2 | import { Container } from "@/components/ui/container"
 3 | 
 4 | export function Footer() {
 5 |   return (
 6 |     
 7 |       
 8 |         
 9 |           {/* Company Info */}
10 |           
11 |             
Acme 
12 |             
13 |               © {new Date().getFullYear()} Acme. All rights reserved.
14 |             
15 |           
16 | 
17 |           {/* Product Links */}
18 |           
19 |             
Product 
20 |             
21 |                
22 |                 Documentation
23 |               
24 |                
25 |                 Blog
26 |               
27 |                
28 |                 Pricing
29 |               
30 |              
31 |           
32 | 
33 |           {/* Legal Links */}
34 |           
35 |             
Legal 
36 |             
37 |                
38 |                 Privacy Policy
39 |               
40 |                
41 |                 Terms of Service
42 |               
43 |              
44 |           
45 |         
 
46 |        
47 |      
48 |   )
49 | }
50 | 
--------------------------------------------------------------------------------
/src/components/marketing/shared/navbar.tsx:
--------------------------------------------------------------------------------
  1 | "use client"
  2 | 
  3 | import Link from "next/link"
  4 | import { usePathname } from "next/navigation"
  5 | import { Button } from "@/components/ui/button"
  6 | import { Container } from "@/components/ui/container"
  7 | import { Sheet, SheetContent, SheetTitle, SheetTrigger } from "@/components/ui/sheet"
  8 | import { Menu } from "lucide-react"
  9 | import { useState, useEffect } from "react"
 10 | import { cn } from "@/lib/utils"
 11 | import { useUser } from "@stackframe/stack"
 12 | 
 13 | const menuItems = [
 14 |   { label: "Docs", href: "/docs" },
 15 |   { label: "Blog", href: "/blog" },
 16 |   { label: "Pricing", href: "/pricing" },
 17 | ]
 18 | 
 19 | export function NavBar() {
 20 |   const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
 21 |   const pathname = usePathname()
 22 |   const user = useUser();
 23 | 
 24 |   // Close mobile menu on route change
 25 |   useEffect(() => {
 26 |     setMobileMenuOpen(false)
 27 |   }, [pathname])
 28 | 
 29 |   return (
 30 |     
 31 |       
 32 |         
 33 |           {/* Logo - Left */}
 34 |           
 35 |              
 36 |               Acme 
 37 |             
 38 |           
 39 | 
 40 |           {/* Navigation Links - Center */}
 41 |           
 42 |             {menuItems.map((item) => (
 43 |                
 48 |                 {item.label}
 49 |               
 50 |             ))}
 51 |            
 52 | 
 53 |           {/* Auth Buttons - Right */}
 54 |           
 55 |             {user ? (
 56 |               
 57 |                  Continue
 58 |                
 59 |             ) : (
 60 |               <>
 61 |                 
 62 |                    Sign in
 63 |                  
 64 |                 
 65 |                    Sign up
 66 |                  
 67 |               >
 68 |             )}
 69 | 
 70 |             {/* Mobile Menu */}
 71 |             
 72 |               
 73 |                 
 74 |                    
 75 |                  
 76 |                
 77 |               
 78 |                 
 79 |                 Menu 
 80 |                 
 81 |                   {/* Top section with menu items */}
 82 |                   
 83 |                     {menuItems.map((item) => (
 84 |                        
 92 |                         {item.label}
 93 |                       
 94 |                     ))}
 95 |                   
 96 | 
 97 |                   {/* Bottom section with auth buttons */}
 98 |                   
 99 |                     {user ? (
100 |                       
101 |                          Continue
102 |                        
103 |                     ) : (
104 |                       <>
105 |                         
106 |                            Sign in
107 |                          
108 |                         
109 |                            Sign up
110 |                          
111 |                       >
112 |                     )}
113 |                   
114 |                 
 
115 |                
116 |              
117 |           
118 |         
 
119 |        
120 |      
121 |   )
122 | }
123 | 
--------------------------------------------------------------------------------
/src/components/marketing/shared/section-title.tsx:
--------------------------------------------------------------------------------
 1 | import React from "react";
 2 | 
 3 | interface SectionTitleProps {
 4 |   name: string;
 5 |   title: string;
 6 | }
 7 | 
 8 | export const SectionTitle = ({name, title}: SectionTitleProps) => {
 9 |   return (
10 |     
11 |       
12 |         {name}
13 |        
14 |       
15 |         {title}
16 |        
17 |     
18 |   );
19 | };
--------------------------------------------------------------------------------
/src/components/marketing/testimonials/testimonials.tsx:
--------------------------------------------------------------------------------
  1 | import Image from "next/image";
  2 | import type React from "react";
  3 | import { cn } from "@/lib/utils";
  4 | import { Card } from "@/components/ui/card";
  5 | import Marquee from "../../ui/marquee";
  6 | import { SectionTitle } from "@/components/marketing/shared/section-title";
  7 | 
  8 | const Highlight = ({
  9 | 	children,
 10 | 	className,
 11 | }: {
 12 | 	children: React.ReactNode;
 13 | 	className?: string;
 14 | }) => {
 15 | 	return (
 16 | 		
 22 | 			{children}
 23 | 		 
 24 | 	);
 25 | };
 26 | 
 27 | export interface TestimonialCardProps {
 28 | 	name: string;
 29 | 	role: string;
 30 | 	img?: string;
 31 | 	description: React.ReactNode;
 32 | 	className?: string;
 33 | }
 34 | 
 35 | export const TestimonialCard = ({
 36 | 	description,
 37 | 	name,
 38 | 	img,
 39 | 	role,
 40 | 	className,
 41 | }: TestimonialCardProps) => (
 42 | 	
 50 | 		
 51 | 			{description}
 52 | 		
 53 | 
 54 | 		
 55 | 			{img && (
 56 | 				
 63 | 			)}
 64 | 
 65 | 			
 66 | 				
 67 | 					{name}
 68 | 				
 69 | 				
 70 | 					{role}
 71 | 				
 72 | 			
 73 | 		
 
 74 | 	 
 75 | );
 76 | 
 77 | const testimonials = [
 78 | 	{
 79 | 		name: "Alex Johnson",
 80 | 		role: "Senior Developer at TechCorp",
 81 | 		img: "https://avatar.vercel.sh/alex",
 82 | 		description: (
 83 | 			
 84 | 				Lorem ipsum dolor sit amet.{" "}
 85 | 				
 86 | 					Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
 87 | 					et dolore magna aliqua
 88 | 				 
 89 | 				, ut enim ad minim veniam.
 90 | 			
 91 | 		),
 92 | 	},
 93 | 	{
 94 | 		name: "Sarah Smith",
 95 | 		role: "CTO at StartupX",
 96 | 		img: "https://avatar.vercel.sh/sarah",
 97 | 		description: (
 98 | 			
 99 | 				Ut enim ad minim veniam,{" "}
100 | 				
101 | 					quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
102 | 					consequat
103 | 				 {" "}
104 | 				duis aute irure dolor.
105 | 			
106 | 		),
107 | 	},
108 | 	{
109 | 		name: "Michael Chen",
110 | 		role: "Product Lead @TechStart",
111 | 		img: "https://avatar.vercel.sh/michael",
112 | 		description: (
113 | 			
114 | 				Duis aute irure dolor in reprehenderit.{" "}
115 | 				
116 | 					In voluptate velit esse cillum dolore eu fugiat nulla pariatur
117 | 				 {" "}
118 | 				excepteur sint occaecat cupidatat.
119 | 			
120 | 		),
121 | 	},
122 | 	{
123 | 		name: "Emma Wilson",
124 | 		role: "Frontend Developer @WebCo",
125 | 		img: "https://avatar.vercel.sh/emma",
126 | 		description: (
127 | 			
128 | 				
129 | 					Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
130 | 					officia
131 | 				 {" "}
132 | 				deserunt mollit anim id est laborum.
133 | 			
134 | 		),
135 | 	},
136 | 	{
137 | 		name: "David Lee",
138 | 		role: "Software Architect @CodeCo",
139 | 		img: "https://avatar.vercel.sh/david",
140 | 		description: (
141 | 			
142 | 				Sed ut perspiciatis unde omnis.{" "}
143 | 				
144 | 					Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit
145 | 				 
146 | 				aut fugit, sed quia consequuntur.
147 | 			
148 | 		),
149 | 	},
150 | 	{
151 | 		name: "Lisa Brown",
152 | 		role: "Tech Lead @DevInc",
153 | 		img: "https://avatar.vercel.sh/lisa",
154 | 		description: (
155 | 			
156 | 				At vero eos et accusamus.{" "}
157 | 				
158 | 					Et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum
159 | 				 
160 | 				deleniti atque corrupti.
161 | 			
162 | 		),
163 | 	},
164 | 	{
165 | 		name: "Rachel Green",
166 | 		role: "UX Designer @DesignHub",
167 | 		img: "https://avatar.vercel.sh/rachel",
168 | 		description: (
169 | 			
170 | 				Neque porro quisquam est.{" "}
171 | 				
172 | 					Qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit
173 | 				 {" "}
174 | 				sed quia non numquam eius modi.
175 | 			
176 | 		),
177 | 	},
178 | 	{
179 | 		name: "James Wilson",
180 | 		role: "DevOps Engineer @CloudTech",
181 | 		img: "https://avatar.vercel.sh/james",
182 | 		description: (
183 | 			
184 | 				Temporibus autem quibusdam.{" "}
185 | 				
186 | 					Et aut officiis debitis aut rerum necessitatibus saepe eveniet
187 | 				 {" "}
188 | 				ut et voluptates repudiandae sint.
189 | 			
190 | 		),
191 | 	},
192 | 	{
193 | 		name: "Sofia Rodriguez",
194 | 		role: "Mobile Dev @AppWorks",
195 | 		img: "https://avatar.vercel.sh/sofia",
196 | 		description: (
197 | 			
198 | 				Nam libero tempore.{" "}
199 | 				
200 | 					Cum soluta nobis est eligendi optio cumque nihil impedit quo minus
201 | 				 {" "}
202 | 				id quod maxime placeat facere.
203 | 			
204 | 		),
205 | 	},
206 | 	{
207 | 		name: "Ryan Park",
208 | 		role: "Backend Lead @ServerPro",
209 | 		img: "https://avatar.vercel.sh/ryan",
210 | 		description: (
211 | 			
212 | 				Itaque earum rerum.{" "}
213 | 				
214 | 					Hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores
215 | 				 {" "}
216 | 				alias consequatur aut perferendis.
217 | 			
218 | 		),
219 | 	},
220 | ];
221 | const firstRow = testimonials.slice(0, testimonials.length / 2);
222 | const secondRow = testimonials.slice(testimonials.length / 2);
223 | 
224 | export function Testimonials({ className }: { className?: string }) {
225 | 	return (
226 | 		
227 | 			 
228 | 
229 | 			
230 | 				
231 | 					{firstRow.map((review) => (
232 | 						 
233 | 					))}
234 | 				 
235 | 				
236 | 					{secondRow.map((review) => (
237 | 						 
238 | 					))}
239 | 				 
240 | 				
248 | 					{/* Empty placeholder  */}
249 | 					
250 | 					{testimonials.map((card, idx) => (
251 | 						
256 | 					))}
257 | 					
258 | 					{/* Empty placeholder  */}
259 | 					
260 | 				
261 | 
262 | 				
263 | 				
264 | 			
 
265 | 		 
266 | 	);
267 | }
268 | 
--------------------------------------------------------------------------------
/src/components/saas/app-sidebar.tsx:
--------------------------------------------------------------------------------
  1 | "use client"
  2 | 
  3 | import * as React from "react"
  4 | import {
  5 |   BookOpen,
  6 |   Bot,
  7 |   Command,
  8 |   Frame,
  9 |   LifeBuoy,
 10 |   Map,
 11 |   PieChart,
 12 |   Send,
 13 |   Settings2,
 14 |   SquareTerminal,
 15 | } from "lucide-react"
 16 | 
 17 | import { NavMain } from "@/components/saas/nav-main"
 18 | import { NavProjects } from "@/components/saas/nav-projects"
 19 | import { NavSecondary } from "@/components/saas/nav-secondary"
 20 | import { NavUser } from "@/components/saas/nav-user"
 21 | import {
 22 |   Sidebar,
 23 |   SidebarContent,
 24 |   SidebarFooter,
 25 |   SidebarHeader,
 26 |   SidebarMenu,
 27 |   SidebarMenuButton,
 28 |   SidebarMenuItem,
 29 | } from "@/components/ui/sidebar"
 30 | import { useUser } from "@stackframe/stack"
 31 | 
 32 | const data = {
 33 |   navMain: [
 34 |     {
 35 |       title: "Playground",
 36 |       url: "#",
 37 |       icon: SquareTerminal,
 38 |       isActive: true,
 39 |       items: [
 40 |         {
 41 |           title: "History",
 42 |           url: "#",
 43 |         },
 44 |         {
 45 |           title: "Starred",
 46 |           url: "#",
 47 |         },
 48 |         {
 49 |           title: "Settings",
 50 |           url: "#",
 51 |         },
 52 |       ],
 53 |     },
 54 |     {
 55 |       title: "Models",
 56 |       url: "#",
 57 |       icon: Bot,
 58 |       items: [
 59 |         {
 60 |           title: "Genesis",
 61 |           url: "#",
 62 |         },
 63 |         {
 64 |           title: "Explorer",
 65 |           url: "#",
 66 |         },
 67 |         {
 68 |           title: "Quantum",
 69 |           url: "#",
 70 |         },
 71 |       ],
 72 |     },
 73 |     {
 74 |       title: "Documentation",
 75 |       url: "#",
 76 |       icon: BookOpen,
 77 |       items: [
 78 |         {
 79 |           title: "Introduction",
 80 |           url: "#",
 81 |         },
 82 |         {
 83 |           title: "Get Started",
 84 |           url: "#",
 85 |         },
 86 |         {
 87 |           title: "Tutorials",
 88 |           url: "#",
 89 |         },
 90 |         {
 91 |           title: "Changelog",
 92 |           url: "#",
 93 |         },
 94 |       ],
 95 |     },
 96 |     {
 97 |       title: "Settings",
 98 |       url: "#",
 99 |       icon: Settings2,
100 |       items: [
101 |         {
102 |           title: "General",
103 |           url: "#",
104 |         },
105 |         {
106 |           title: "Team",
107 |           url: "#",
108 |         },
109 |         {
110 |           title: "Billing",
111 |           url: "#",
112 |         },
113 |         {
114 |           title: "Limits",
115 |           url: "#",
116 |         },
117 |       ],
118 |     },
119 |   ],
120 |   navSecondary: [
121 |     {
122 |       title: "Support",
123 |       url: "#",
124 |       icon: LifeBuoy,
125 |     },
126 |     {
127 |       title: "Feedback",
128 |       url: "#",
129 |       icon: Send,
130 |     },
131 |   ],
132 |   projects: [
133 |     {
134 |       name: "Design Engineering",
135 |       url: "#",
136 |       icon: Frame,
137 |     },
138 |     {
139 |       name: "Sales & Marketing",
140 |       url: "#",
141 |       icon: PieChart,
142 |     },
143 |     {
144 |       name: "Travel",
145 |       url: "#",
146 |       icon: Map,
147 |     },
148 |   ],
149 | }
150 | 
151 | export function AppSidebar({ ...props }: React.ComponentProps) {
152 |   const user = useUser({ or: "redirect" });
153 | 
154 |   return (
155 |     
156 |       
157 |         
158 |           
159 |             
160 |               
161 |                 
162 |                    
163 |                 
164 |                 
165 |                   Acme Inc 
166 |                   Enterprise 
167 |                 
168 |                
169 |              
170 |            
171 |          
172 |        
173 |       
174 |          
175 |          
176 |          
177 |        
178 |       
179 |          
180 |        
181 |      
182 |   )
183 | }
184 | 
--------------------------------------------------------------------------------
/src/components/saas/nav-main.tsx:
--------------------------------------------------------------------------------
 1 | "use client"
 2 | 
 3 | import { ChevronRight, type LucideIcon } from "lucide-react"
 4 | 
 5 | import {
 6 |   Collapsible,
 7 |   CollapsibleContent,
 8 |   CollapsibleTrigger,
 9 | } from "@/components/ui/collapsible"
10 | import {
11 |   SidebarGroup,
12 |   SidebarGroupLabel,
13 |   SidebarMenu,
14 |   SidebarMenuAction,
15 |   SidebarMenuButton,
16 |   SidebarMenuItem,
17 |   SidebarMenuSub,
18 |   SidebarMenuSubButton,
19 |   SidebarMenuSubItem,
20 | } from "@/components/ui/sidebar"
21 | 
22 | export function NavMain({
23 |   items,
24 | }: {
25 |   items: {
26 |     title: string
27 |     url: string
28 |     icon: LucideIcon
29 |     isActive?: boolean
30 |     items?: {
31 |       title: string
32 |       url: string
33 |     }[]
34 |   }[]
35 | }) {
36 |   return (
37 |     
38 |       Platform 
39 |       
40 |         {items.map((item) => (
41 |           
42 |             
43 |               
44 |                 
45 |                    
46 |                   {item.title} 
47 |                  
48 |                
49 |               {item.items?.length ? (
50 |                 <>
51 |                   
52 |                     
53 |                        
54 |                       Toggle 
55 |                      
56 |                    
57 |                   
58 |                     
59 |                       {item.items?.map((subItem) => (
60 |                         
61 |                           
62 |                             
63 |                               {subItem.title} 
64 |                              
65 |                            
66 |                          
67 |                       ))}
68 |                      
69 |                    
70 |                 >
71 |               ) : null}
72 |              
73 |            
74 |         ))}
75 |        
76 |      
77 |   )
78 | }
79 | 
--------------------------------------------------------------------------------
/src/components/saas/nav-projects.tsx:
--------------------------------------------------------------------------------
 1 | "use client"
 2 | 
 3 | import {
 4 |   Folder,
 5 |   MoreHorizontal,
 6 |   Share,
 7 |   Trash2,
 8 |   type LucideIcon,
 9 | } from "lucide-react"
10 | 
11 | import {
12 |   DropdownMenu,
13 |   DropdownMenuContent,
14 |   DropdownMenuItem,
15 |   DropdownMenuSeparator,
16 |   DropdownMenuTrigger,
17 | } from "@/components/ui/dropdown-menu"
18 | import {
19 |   SidebarGroup,
20 |   SidebarGroupLabel,
21 |   SidebarMenu,
22 |   SidebarMenuAction,
23 |   SidebarMenuButton,
24 |   SidebarMenuItem,
25 |   useSidebar,
26 | } from "@/components/ui/sidebar"
27 | 
28 | export function NavProjects({
29 |   projects,
30 | }: {
31 |   projects: {
32 |     name: string
33 |     url: string
34 |     icon: LucideIcon
35 |   }[]
36 | }) {
37 |   const { isMobile } = useSidebar()
38 | 
39 |   return (
40 |     
41 |       Projects 
42 |       
43 |         {projects.map((item) => (
44 |           
45 |             
46 |               
47 |                  
48 |                 {item.name} 
49 |                
50 |              
51 |             
52 |               
53 |                 
54 |                    
55 |                   More 
56 |                  
57 |                
58 |               
63 |                 
64 |                    
65 |                   View Project 
66 |                  
67 |                 
68 |                    
69 |                   Share Project 
70 |                  
71 |                  
72 |                 
73 |                    
74 |                   Delete Project 
75 |                  
76 |                
77 |              
78 |            
79 |         ))}
80 |         
81 |           
82 |              
83 |             More 
84 |            
85 |          
86 |        
87 |      
88 |   )
89 | }
90 | 
--------------------------------------------------------------------------------
/src/components/saas/nav-secondary.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react"
 2 | import { type LucideIcon } from "lucide-react"
 3 | 
 4 | import {
 5 |   SidebarGroup,
 6 |   SidebarGroupContent,
 7 |   SidebarMenu,
 8 |   SidebarMenuButton,
 9 |   SidebarMenuItem,
10 | } from "@/components/ui/sidebar"
11 | 
12 | export function NavSecondary({
13 |   items,
14 |   ...props
15 | }: {
16 |   items: {
17 |     title: string
18 |     url: string
19 |     icon: LucideIcon
20 |   }[]
21 | } & React.ComponentPropsWithoutRef) {
22 |   return (
23 |     
24 |       
25 |         
26 |           {items.map((item) => (
27 |             
28 |               
29 |                 
30 |                    
31 |                   {item.title} 
32 |                  
33 |                
34 |              
35 |           ))}
36 |          
37 |        
38 |      
39 |   )
40 | }
41 | 
--------------------------------------------------------------------------------
/src/components/saas/nav-user.tsx:
--------------------------------------------------------------------------------
  1 | "use client"
  2 | 
  3 | import {
  4 |   BadgeCheck,
  5 |   Bell,
  6 |   ChevronsUpDown,
  7 |   CreditCard,
  8 |   LogOut,
  9 |   Sparkles,
 10 | } from "lucide-react"
 11 | 
 12 | import {
 13 |   Avatar,
 14 |   AvatarFallback,
 15 |   AvatarImage,
 16 | } from "@/components/ui/avatar"
 17 | import {
 18 |   DropdownMenu,
 19 |   DropdownMenuContent,
 20 |   DropdownMenuGroup,
 21 |   DropdownMenuItem,
 22 |   DropdownMenuLabel,
 23 |   DropdownMenuSeparator,
 24 |   DropdownMenuTrigger,
 25 | } from "@/components/ui/dropdown-menu"
 26 | import {
 27 |   SidebarMenu,
 28 |   SidebarMenuButton,
 29 |   SidebarMenuItem,
 30 |   useSidebar,
 31 | } from "@/components/ui/sidebar"
 32 | import Link from "next/link"
 33 | 
 34 | export function NavUser({
 35 |   name,
 36 |   email,
 37 |   avatar,
 38 | }: {
 39 |   name: string
 40 |     email: string
 41 |     avatar: string
 42 |   }
 43 | ) {
 44 |   const { isMobile } = useSidebar()
 45 | 
 46 |   return (
 47 |     
 48 |       
 49 |         
 50 |           
 51 |             
 55 |               
 56 |                  
 57 |                 
 58 |                   {name
 59 |                     .split(' ')
 60 |                     .map(word => word[0])
 61 |                     .join('')
 62 |                     .toUpperCase()
 63 |                     .slice(0, 2)}
 64 |                  
 65 |                
 66 |               
 67 |                 {name} 
 68 |                 {email} 
 69 |               
 70 |                
 71 |              
 72 |            
 73 |           
 79 |             
 80 |               
 81 |                 
 82 |                    
 83 |                   
 84 |                     {name
 85 |                       .split(' ')
 86 |                       .map(word => word[0])
 87 |                       .join('')
 88 |                       .toUpperCase()
 89 |                       .slice(0, 2)}
 90 |                    
 91 |                  
 92 |                 
 93 |                   {name} 
 94 |                   {email} 
 95 |                 
 96 |               
 
 97 |              
 98 |              
 99 |             
100 |               
101 |                  
102 |                 Upgrade to Pro
103 |                
104 |              
105 |              
106 |             
107 |               
108 |                  
109 |                    
110 |                   Account
111 |                 
112 |                
113 |               
114 |                  
115 |                 Billing
116 |                
117 |               
118 |                  
119 |                 Notifications
120 |                
121 |              
122 |              
123 |             
124 |                
125 |                  
126 |                 Sign out
127 |               
128 |              
129 |            
130 |          
131 |        
132 |      
133 |   )
134 | }
135 | 
--------------------------------------------------------------------------------
/src/components/saas/post.tsx:
--------------------------------------------------------------------------------
 1 | "use client";
 2 | 
 3 | import { useState } from "react";
 4 | 
 5 | import { api } from "@/trpc/react";
 6 | 
 7 | export function LatestPost() {
 8 |   const [latestPost] = api.post.getLatest.useSuspenseQuery();
 9 | 
10 |   const utils = api.useUtils();
11 |   const [name, setName] = useState("");
12 |   const createPost = api.post.create.useMutation({
13 |     onSuccess: async () => {
14 |       await utils.post.invalidate();
15 |       setName("");
16 |     },
17 |   });
18 | 
19 |   return (
20 |     
21 |       {latestPost ? (
22 |         
Your most recent post: {latestPost.name}
23 |       ) : (
24 |         
You have no posts yet.
25 |       )}
26 |       
48 |     
 
49 |   );
50 | }
51 | 
--------------------------------------------------------------------------------
/src/components/ui/animated-beam.tsx:
--------------------------------------------------------------------------------
  1 | "use client";
  2 | 
  3 | import { type RefObject, useEffect, useId, useState } from "react";
  4 | import { motion } from "framer-motion";
  5 | 
  6 | import { cn } from "@/lib/utils";
  7 | 
  8 | export interface AnimatedBeamProps {
  9 |   className?: string;
 10 |   containerRef: RefObject; // Container ref
 11 |   fromRef: RefObject;
 12 |   toRef: RefObject;
 13 |   curvature?: number;
 14 |   reverse?: boolean;
 15 |   pathColor?: string;
 16 |   pathWidth?: number;
 17 |   pathOpacity?: number;
 18 |   gradientStartColor?: string;
 19 |   gradientStopColor?: string;
 20 |   delay?: number;
 21 |   duration?: number;
 22 |   startXOffset?: number;
 23 |   startYOffset?: number;
 24 |   endXOffset?: number;
 25 |   endYOffset?: number;
 26 | }
 27 | 
 28 | export const AnimatedBeam: React.FC = ({
 29 |   className,
 30 |   containerRef,
 31 |   fromRef,
 32 |   toRef,
 33 |   curvature = 0,
 34 |   reverse = false, // Include the reverse prop
 35 |   duration = Math.random() * 3 + 4,
 36 |   delay = 0,
 37 |   pathColor = "gray",
 38 |   pathWidth = 2,
 39 |   pathOpacity = 0.2,
 40 |   gradientStartColor = "#ffaa40",
 41 |   gradientStopColor = "#9c40ff",
 42 |   startXOffset = 0,
 43 |   startYOffset = 0,
 44 |   endXOffset = 0,
 45 |   endYOffset = 0,
 46 | }) => {
 47 |   const id = useId();
 48 |   const [pathD, setPathD] = useState("");
 49 |   const [svgDimensions, setSvgDimensions] = useState({ width: 0, height: 0 });
 50 | 
 51 |   // Calculate the gradient coordinates based on the reverse prop
 52 |   const gradientCoordinates = reverse
 53 |     ? {
 54 |         x1: ["90%", "-10%"],
 55 |         x2: ["100%", "0%"],
 56 |         y1: ["0%", "0%"],
 57 |         y2: ["0%", "0%"],
 58 |       }
 59 |     : {
 60 |         x1: ["10%", "110%"],
 61 |         x2: ["0%", "100%"],
 62 |         y1: ["0%", "0%"],
 63 |         y2: ["0%", "0%"],
 64 |       };
 65 | 
 66 |   useEffect(() => {
 67 |     const updatePath = () => {
 68 |       if (containerRef.current && fromRef.current && toRef.current) {
 69 |         const containerRect = containerRef.current.getBoundingClientRect();
 70 |         const rectA = fromRef.current.getBoundingClientRect();
 71 |         const rectB = toRef.current.getBoundingClientRect();
 72 | 
 73 |         const svgWidth = containerRect.width;
 74 |         const svgHeight = containerRect.height;
 75 |         setSvgDimensions({ width: svgWidth, height: svgHeight });
 76 | 
 77 |         const startX =
 78 |           rectA.left - containerRect.left + rectA.width / 2 + startXOffset;
 79 |         const startY =
 80 |           rectA.top - containerRect.top + rectA.height / 2 + startYOffset;
 81 |         const endX =
 82 |           rectB.left - containerRect.left + rectB.width / 2 + endXOffset;
 83 |         const endY =
 84 |           rectB.top - containerRect.top + rectB.height / 2 + endYOffset;
 85 | 
 86 |         const controlY = startY - curvature;
 87 |         const d = `M ${startX},${startY} Q ${
 88 |           (startX + endX) / 2
 89 |         },${controlY} ${endX},${endY}`;
 90 |         setPathD(d);
 91 |       }
 92 |     };
 93 | 
 94 |     // Initialize ResizeObserver
 95 |     const resizeObserver = new ResizeObserver((entries) => {
 96 |       // For all entries, recalculate the path
 97 |       for (const entry of entries) {
 98 |         updatePath();
 99 |       }
100 |     });
101 | 
102 |     // Observe the container element
103 |     if (containerRef.current) {
104 |       resizeObserver.observe(containerRef.current);
105 |     }
106 | 
107 |     // Call the updatePath initially to set the initial path
108 |     updatePath();
109 | 
110 |     // Clean up the observer on component unmount
111 |     return () => {
112 |       resizeObserver.disconnect();
113 |     };
114 |   }, [
115 |     containerRef,
116 |     fromRef,
117 |     toRef,
118 |     curvature,
119 |     startXOffset,
120 |     startYOffset,
121 |     endXOffset,
122 |     endYOffset,
123 |   ]);
124 | 
125 |   return (
126 |     
137 |        
144 |        
151 |       
152 |         
176 |            
177 |            
178 |            
179 |            
184 |          
185 |        
186 |      
187 |   );
188 | };
189 | 
--------------------------------------------------------------------------------
/src/components/ui/animated-grid-pattern.tsx:
--------------------------------------------------------------------------------
  1 | "use client";
  2 | 
  3 | import { useEffect, useId, useRef, useState } from "react";
  4 | import { motion } from "framer-motion";
  5 | 
  6 | import { cn } from "@/lib/utils";
  7 | 
  8 | interface AnimatedGridPatternProps {
  9 |   width?: number;
 10 |   height?: number;
 11 |   x?: number;
 12 |   y?: number;
 13 |   strokeDasharray?:  string | number;
 14 |   numSquares?: number;
 15 |   className?: string;
 16 |   maxOpacity?: number;
 17 |   duration?: number;
 18 |   repeatDelay?: number;
 19 | }
 20 | 
 21 | export default function AnimatedGridPattern({
 22 |   width = 40,
 23 |   height = 40,
 24 |   x = -1,
 25 |   y = -1,
 26 |   strokeDasharray = 0,
 27 |   numSquares = 50,
 28 |   className,
 29 |   maxOpacity = 0.5,
 30 |   duration = 4,
 31 |   repeatDelay = 0.5,
 32 |   ...props
 33 | }: AnimatedGridPatternProps) {
 34 |   const id = useId();
 35 |   const containerRef = useRef(null);
 36 |   const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
 37 |   const [squares, setSquares] = useState(() => generateSquares(numSquares));
 38 | 
 39 |   function getPos() {
 40 |     return [
 41 |       Math.floor((Math.random() * dimensions.width) / width),
 42 |       Math.floor((Math.random() * dimensions.height) / height),
 43 |     ];
 44 |   }
 45 | 
 46 |   // Adjust the generateSquares function to return objects with an id, x, and y
 47 |   function generateSquares(count: number) {
 48 |     return Array.from({ length: count }, (_, i) => ({
 49 |       id: i,
 50 |       pos: getPos(),
 51 |     }));
 52 |   }
 53 | 
 54 |   // Function to update a single square's position
 55 |   const updateSquarePosition = (id: number) => {
 56 |     setSquares((currentSquares) =>
 57 |       currentSquares.map((sq) =>
 58 |         sq.id === id
 59 |           ? {
 60 |               ...sq,
 61 |               pos: getPos(),
 62 |             }
 63 |           : sq,
 64 |       ),
 65 |     );
 66 |   };
 67 | 
 68 |   // Update squares to animate in
 69 |   useEffect(() => {
 70 |     if (dimensions.width && dimensions.height) {
 71 |       setSquares(generateSquares(numSquares));
 72 |     }
 73 |   }, [dimensions, numSquares]);
 74 | 
 75 |   // Resize observer to update container dimensions
 76 |   useEffect(() => {
 77 |     const resizeObserver = new ResizeObserver((entries) => {
 78 |       for (const entry of entries) {
 79 |         setDimensions({
 80 |           width: entry.contentRect.width,
 81 |           height: entry.contentRect.height,
 82 |         });
 83 |       }
 84 |     });
 85 | 
 86 |     if (containerRef.current) {
 87 |       resizeObserver.observe(containerRef.current);
 88 |     }
 89 | 
 90 |     return () => {
 91 |       if (containerRef.current) {
 92 |         resizeObserver.unobserve(containerRef.current);
 93 |       }
 94 |     };
 95 |   }, [containerRef]);
 96 | 
 97 |   return (
 98 |     
107 |       
108 |         
116 |            
121 |          
122 |        
123 |        
124 |       
125 |         {squares.map(({ pos: [x, y], id }, index) => (
126 |            updateSquarePosition(id)}
136 |             key={`${x}-${y}-${index}`}
137 |             width={width - 1}
138 |             height={height - 1}
139 |             x={x! * width + 1}
140 |             y={y! * height + 1}
141 |             fill="currentColor"
142 |             strokeWidth="0"
143 |           />
144 |         ))}
145 |         
146 |      
147 |   );
148 | }
149 | 
--------------------------------------------------------------------------------
/src/components/ui/animated-list.tsx:
--------------------------------------------------------------------------------
 1 | "use client";
 2 | 
 3 | import React, { useEffect, useMemo, useState } from "react";
 4 | import { AnimatePresence, motion } from "framer-motion";
 5 | 
 6 | export interface AnimatedListProps {
 7 |   className?: string;
 8 |   children: React.ReactNode;
 9 |   delay?: number;
10 | }
11 | 
12 | export const AnimatedList = React.memo(
13 |   ({ className, children, delay = 1000 }: AnimatedListProps) => {
14 |     const [index, setIndex] = useState(0);
15 |     const childrenArray = useMemo(
16 |       () => React.Children.toArray(children),
17 |       [children],
18 |     );
19 | 
20 |     useEffect(() => {
21 |       if (index < childrenArray.length - 1) {
22 |         const timeout = setTimeout(() => {
23 |           setIndex((prevIndex) => prevIndex + 1);
24 |         }, delay);
25 | 
26 |         return () => clearTimeout(timeout);
27 |       }
28 |     }, [index, delay, childrenArray.length]);
29 | 
30 |     const itemsToShow = useMemo(() => {
31 |       return childrenArray.slice(0, index + 1).reverse();
32 |     }, [index, childrenArray]);
33 | 
34 |     return (
35 |       
36 |         
37 |           {itemsToShow.map((item) => (
38 |             
39 |               {item}
40 |              
41 |           ))}
42 |          
43 |       
 
44 |     );
45 |   },
46 | );
47 | 
48 | AnimatedList.displayName = "AnimatedList";
49 | 
50 | export function AnimatedListItem({ children }: { children: React.ReactNode }) {
51 |   const animations = {
52 |     initial: { scale: 0, opacity: 0 },
53 |     animate: { scale: 1, opacity: 1, originY: 0 },
54 |     exit: { scale: 0, opacity: 0 },
55 |     transition: { type: "spring", stiffness: 350, damping: 40 },
56 |   };
57 | 
58 |   return (
59 |     
60 |       {children}
61 |      
62 |   );
63 | }
64 | 
--------------------------------------------------------------------------------
/src/components/ui/animated-shiny-text.tsx:
--------------------------------------------------------------------------------
 1 | import { type CSSProperties, type FC, type ReactNode } from "react";
 2 | 
 3 | import { cn } from "@/lib/utils";
 4 | 
 5 | interface AnimatedShinyTextProps {
 6 |   children: ReactNode;
 7 |   className?: string;
 8 |   shimmerWidth?: number;
 9 | }
10 | 
11 | const AnimatedShinyText: FC = ({
12 |   children,
13 |   className,
14 |   shimmerWidth = 100,
15 | }) => {
16 |   return (
17 |     
35 |       {children}
36 |     
37 |   );
38 | };
39 | 
40 | export default AnimatedShinyText;
41 | 
--------------------------------------------------------------------------------
/src/components/ui/avatar-circles.tsx:
--------------------------------------------------------------------------------
 1 | "use client";
 2 | 
 3 | import React from "react";
 4 | import Image from "next/image";
 5 | import { cn } from "@/lib/utils";
 6 | 
 7 | interface Avatar {
 8 |   imageUrl: string;
 9 |   profileUrl: string;
10 | }
11 | interface AvatarCirclesProps {
12 |   className?: string;
13 |   numPeople?: number;
14 |   avatarUrls: Avatar[];
15 | }
16 | 
17 | const AvatarCircles = ({
18 |   numPeople,
19 |   className,
20 |   avatarUrls,
21 | }: AvatarCirclesProps) => {
22 |   return (
23 |     
50 |   );
51 | };
52 | 
53 | export default AvatarCircles;
54 | 
--------------------------------------------------------------------------------
/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
 1 | "use client"
 2 | 
 3 | import * as React from "react"
 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
 5 | 
 6 | import { cn } from "@/lib/utils"
 7 | 
 8 | const Avatar = React.forwardRef<
 9 |   React.ElementRef,
10 |   React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |    
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 | 
23 | const AvatarImage = React.forwardRef<
24 |   React.ElementRef,
25 |   React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |    
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 | 
35 | const AvatarFallback = React.forwardRef<
36 |   React.ElementRef,
37 |   React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |    
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 | 
50 | export { Avatar, AvatarImage, AvatarFallback }
51 | 
--------------------------------------------------------------------------------
/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react"
 2 | import { cva, type VariantProps } from "class-variance-authority"
 3 | 
 4 | import { cn } from "@/lib/utils"
 5 | 
 6 | const badgeVariants = cva(
 7 |   "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
 8 |   {
 9 |     variants: {
10 |       variant: {
11 |         default:
12 |           "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
13 |         secondary:
14 |           "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 |         destructive:
16 |           "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
17 |         outline: "text-foreground",
18 |       },
19 |     },
20 |     defaultVariants: {
21 |       variant: "default",
22 |     },
23 |   }
24 | )
25 | 
26 | export interface BadgeProps
27 |   extends React.HTMLAttributes,
28 |     VariantProps {}
29 | 
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 |   return (
32 |     
33 |   )
34 | }
35 | 
36 | export { Badge, badgeVariants }
37 | 
--------------------------------------------------------------------------------
/src/components/ui/bento-grid.tsx:
--------------------------------------------------------------------------------
 1 | import { type ReactNode, type ElementType } from "react";
 2 | import { ArrowRightIcon } from "@radix-ui/react-icons";
 3 | 
 4 | import { cn } from "@/lib/utils";
 5 | import { Button } from "@/components/ui/button";
 6 | 
 7 | type BentoGridProps = {
 8 |   children: ReactNode;
 9 |   className?: string;
10 | };
11 | 
12 | type BentoCardProps = {
13 |   name: string;
14 |   className: string;
15 |   background: ReactNode;
16 |   Icon: ElementType;
17 |   description: string;
18 |   href: string;
19 |   cta: string;
20 | };
21 | 
22 | const BentoGrid = ({ children, className }: BentoGridProps) => {
23 |   return (
24 |     
30 |       {children}
31 |     
32 |   );
33 | };
34 | 
35 | const BentoCard = ({
36 |   name,
37 |   className,
38 |   background,
39 |   Icon,
40 |   description,
41 |   href,
42 |   cta,
43 | }: BentoCardProps) => (
44 |   
55 |     
{background}
56 |     
57 |       
58 |       
59 |         {name}
60 |        
61 |       
{description}
62 |     
63 | 
64 |     
76 |     
77 |   
 
78 | );
79 | 
80 | export { BentoCard, BentoGrid };
81 | 
--------------------------------------------------------------------------------
/src/components/ui/breadcrumb.tsx:
--------------------------------------------------------------------------------
  1 | import * as React from "react"
  2 | import { Slot } from "@radix-ui/react-slot"
  3 | import { ChevronRight, MoreHorizontal } from "lucide-react"
  4 | 
  5 | import { cn } from "@/lib/utils"
  6 | 
  7 | const Breadcrumb = React.forwardRef<
  8 |   HTMLElement,
  9 |   React.ComponentPropsWithoutRef<"nav"> & {
 10 |     separator?: React.ReactNode
 11 |   }
 12 | >(({ ...props }, ref) =>  )
 13 | Breadcrumb.displayName = "Breadcrumb"
 14 | 
 15 | const BreadcrumbList = React.forwardRef<
 16 |   HTMLOListElement,
 17 |   React.ComponentPropsWithoutRef<"ol">
 18 | >(({ className, ...props }, ref) => (
 19 |    
 27 | ))
 28 | BreadcrumbList.displayName = "BreadcrumbList"
 29 | 
 30 | const BreadcrumbItem = React.forwardRef<
 31 |   HTMLLIElement,
 32 |   React.ComponentPropsWithoutRef<"li">
 33 | >(({ className, ...props }, ref) => (
 34 |    
 39 | ))
 40 | BreadcrumbItem.displayName = "BreadcrumbItem"
 41 | 
 42 | const BreadcrumbLink = React.forwardRef<
 43 |   HTMLAnchorElement,
 44 |   React.ComponentPropsWithoutRef<"a"> & {
 45 |     asChild?: boolean
 46 |   }
 47 | >(({ asChild, className, ...props }, ref) => {
 48 |   const Comp = asChild ? Slot : "a"
 49 | 
 50 |   return (
 51 |      
 56 |   )
 57 | })
 58 | BreadcrumbLink.displayName = "BreadcrumbLink"
 59 | 
 60 | const BreadcrumbPage = React.forwardRef<
 61 |   HTMLSpanElement,
 62 |   React.ComponentPropsWithoutRef<"span">
 63 | >(({ className, ...props }, ref) => (
 64 |    
 72 | ))
 73 | BreadcrumbPage.displayName = "BreadcrumbPage"
 74 | 
 75 | const BreadcrumbSeparator = ({
 76 |   children,
 77 |   className,
 78 |   ...props
 79 | }: React.ComponentProps<"li">) => (
 80 |   svg]:w-3.5 [&>svg]:h-3.5", className)}
 84 |     {...props}
 85 |   >
 86 |     {children ??  }
 87 |    
 88 | )
 89 | BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
 90 | 
 91 | const BreadcrumbEllipsis = ({
 92 |   className,
 93 |   ...props
 94 | }: React.ComponentProps<"span">) => (
 95 |   
101 |      
102 |     More 
103 |    
104 | )
105 | BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
106 | 
107 | export {
108 |   Breadcrumb,
109 |   BreadcrumbList,
110 |   BreadcrumbItem,
111 |   BreadcrumbLink,
112 |   BreadcrumbPage,
113 |   BreadcrumbSeparator,
114 |   BreadcrumbEllipsis,
115 | }
116 | 
--------------------------------------------------------------------------------
/src/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 gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
 9 |   {
10 |     variants: {
11 |       variant: {
12 |         default:
13 |           "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 |         destructive:
15 |           "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 |         outline:
17 |           "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 |         secondary:
19 |           "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 |         ghost: "hover:bg-accent hover:text-accent-foreground",
21 |         link: "text-primary underline-offset-4 hover:underline",
22 |       },
23 |       size: {
24 |         default: "h-9 px-4 py-2",
25 |         sm: "h-8 rounded-md px-3 text-xs",
26 |         lg: "h-10 rounded-md px-8",
27 |         icon: "h-9 w-9",
28 |       },
29 |     },
30 |     defaultVariants: {
31 |       variant: "default",
32 |       size: "default",
33 |     },
34 |   }
35 | )
36 | 
37 | export interface ButtonProps
38 |   extends React.ButtonHTMLAttributes,
39 |     VariantProps {
40 |   asChild?: boolean
41 | }
42 | 
43 | const Button = React.forwardRef(
44 |   ({ className, variant, size, asChild = false, ...props }, ref) => {
45 |     const Comp = asChild ? Slot : "button"
46 |     return (
47 |        
52 |     )
53 |   }
54 | )
55 | Button.displayName = "Button"
56 | 
57 | export { Button, buttonVariants }
58 | 
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react"
 2 | 
 3 | import { cn } from "@/lib/utils"
 4 | 
 5 | const Card = React.forwardRef<
 6 |   HTMLDivElement,
 7 |   React.HTMLAttributes
 8 | >(({ className, ...props }, ref) => (
 9 |   
17 | ))
18 | Card.displayName = "Card"
19 | 
20 | const CardHeader = React.forwardRef<
21 |   HTMLDivElement,
22 |   React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |   
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 | 
32 | const CardTitle = React.forwardRef<
33 |   HTMLDivElement,
34 |   React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |   
41 | ))
42 | CardTitle.displayName = "CardTitle"
43 | 
44 | const CardDescription = React.forwardRef<
45 |   HTMLDivElement,
46 |   React.HTMLAttributes
47 | >(({ className, ...props }, ref) => (
48 |   
53 | ))
54 | CardDescription.displayName = "CardDescription"
55 | 
56 | const CardContent = React.forwardRef<
57 |   HTMLDivElement,
58 |   React.HTMLAttributes
59 | >(({ className, ...props }, ref) => (
60 |   
61 | ))
62 | CardContent.displayName = "CardContent"
63 | 
64 | const CardFooter = React.forwardRef<
65 |   HTMLDivElement,
66 |   React.HTMLAttributes
67 | >(({ className, ...props }, ref) => (
68 |   
73 | ))
74 | CardFooter.displayName = "CardFooter"
75 | 
76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
77 | 
--------------------------------------------------------------------------------
/src/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
 1 | "use client"
 2 | 
 3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
 4 | 
 5 | const Collapsible = CollapsiblePrimitive.Root
 6 | 
 7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
 8 | 
 9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
10 | 
11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
12 | 
--------------------------------------------------------------------------------
/src/components/ui/container.tsx:
--------------------------------------------------------------------------------
 1 | import { cn } from '@/lib/utils'
 2 | 
 3 | type ContainerProps = React.HTMLAttributes
 4 | 
 5 | export function Container({ className, ...props }: ContainerProps) {
 6 |   return (
 7 |     
11 |   )
12 | } 
--------------------------------------------------------------------------------
/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
  1 | "use client"
  2 | 
  3 | import * as React from "react"
  4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
  5 | import { Check, ChevronRight, Circle } from "lucide-react"
  6 | 
  7 | import { cn } from "@/lib/utils"
  8 | 
  9 | const DropdownMenu = DropdownMenuPrimitive.Root
 10 | 
 11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
 12 | 
 13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
 14 | 
 15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
 16 | 
 17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
 18 | 
 19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
 20 | 
 21 | const DropdownMenuSubTrigger = React.forwardRef<
 22 |   React.ElementRef,
 23 |   React.ComponentPropsWithoutRef & {
 24 |     inset?: boolean
 25 |   }
 26 | >(({ className, inset, children, ...props }, ref) => (
 27 |   
 36 |     {children}
 37 |      
 38 |    
 39 | ))
 40 | DropdownMenuSubTrigger.displayName =
 41 |   DropdownMenuPrimitive.SubTrigger.displayName
 42 | 
 43 | const DropdownMenuSubContent = React.forwardRef<
 44 |   React.ElementRef,
 45 |   React.ComponentPropsWithoutRef
 46 | >(({ className, ...props }, ref) => (
 47 |    
 55 | ))
 56 | DropdownMenuSubContent.displayName =
 57 |   DropdownMenuPrimitive.SubContent.displayName
 58 | 
 59 | const DropdownMenuContent = React.forwardRef<
 60 |   React.ElementRef,
 61 |   React.ComponentPropsWithoutRef
 62 | >(({ className, sideOffset = 4, ...props }, ref) => (
 63 |   
 64 |      
 74 |    
 75 | ))
 76 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
 77 | 
 78 | const DropdownMenuItem = React.forwardRef<
 79 |   React.ElementRef,
 80 |   React.ComponentPropsWithoutRef & {
 81 |     inset?: boolean
 82 |   }
 83 | >(({ className, inset, ...props }, ref) => (
 84 |   svg]:size-4 [&>svg]:shrink-0",
 88 |       inset && "pl-8",
 89 |       className
 90 |     )}
 91 |     {...props}
 92 |   />
 93 | ))
 94 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
 95 | 
 96 | const DropdownMenuCheckboxItem = React.forwardRef<
 97 |   React.ElementRef,
 98 |   React.ComponentPropsWithoutRef
 99 | >(({ className, children, checked, ...props }, ref) => (
100 |   
109 |     
110 |       
111 |          
112 |        
113 |      
114 |     {children}
115 |    
116 | ))
117 | DropdownMenuCheckboxItem.displayName =
118 |   DropdownMenuPrimitive.CheckboxItem.displayName
119 | 
120 | const DropdownMenuRadioItem = React.forwardRef<
121 |   React.ElementRef,
122 |   React.ComponentPropsWithoutRef
123 | >(({ className, children, ...props }, ref) => (
124 |   
132 |     
133 |       
134 |          
135 |        
136 |      
137 |     {children}
138 |    
139 | ))
140 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
141 | 
142 | const DropdownMenuLabel = React.forwardRef<
143 |   React.ElementRef,
144 |   React.ComponentPropsWithoutRef & {
145 |     inset?: boolean
146 |   }
147 | >(({ className, inset, ...props }, ref) => (
148 |    
157 | ))
158 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
159 | 
160 | const DropdownMenuSeparator = React.forwardRef<
161 |   React.ElementRef,
162 |   React.ComponentPropsWithoutRef
163 | >(({ className, ...props }, ref) => (
164 |    
169 | ))
170 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
171 | 
172 | const DropdownMenuShortcut = ({
173 |   className,
174 |   ...props
175 | }: React.HTMLAttributes) => {
176 |   return (
177 |      
181 |   )
182 | }
183 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
184 | 
185 | export {
186 |   DropdownMenu,
187 |   DropdownMenuTrigger,
188 |   DropdownMenuContent,
189 |   DropdownMenuItem,
190 |   DropdownMenuCheckboxItem,
191 |   DropdownMenuRadioItem,
192 |   DropdownMenuLabel,
193 |   DropdownMenuSeparator,
194 |   DropdownMenuShortcut,
195 |   DropdownMenuGroup,
196 |   DropdownMenuPortal,
197 |   DropdownMenuSub,
198 |   DropdownMenuSubContent,
199 |   DropdownMenuSubTrigger,
200 |   DropdownMenuRadioGroup,
201 | }
202 | 
--------------------------------------------------------------------------------
/src/components/ui/gradient-text.tsx:
--------------------------------------------------------------------------------
 1 | import { cn } from '@/lib/utils'
 2 | 
 3 | type GradientTextProps = React.HTMLAttributes
 4 | 
 5 | export function GradientText({ className, ...props }: GradientTextProps) {
 6 |   return (
 7 |      
14 |   )
15 | } 
--------------------------------------------------------------------------------
/src/components/ui/hero-video-dialog.tsx:
--------------------------------------------------------------------------------
  1 | "use client";
  2 | 
  3 | import { useState } from "react";
  4 | import { AnimatePresence, motion } from "framer-motion";
  5 | import { Play, XIcon } from "lucide-react";
  6 | import Image from "next/image";
  7 | import { cn } from "@/lib/utils";
  8 | 
  9 | type AnimationStyle =
 10 |   | "from-bottom"
 11 |   | "from-center"
 12 |   | "from-top"
 13 |   | "from-left"
 14 |   | "from-right"
 15 |   | "fade"
 16 |   | "top-in-bottom-out"
 17 |   | "left-in-right-out";
 18 | 
 19 | interface HeroVideoProps {
 20 |   animationStyle?: AnimationStyle;
 21 |   videoSrc: string;
 22 |   thumbnailSrc: string;
 23 |   thumbnailAlt?: string;
 24 |   className?: string;
 25 | }
 26 | 
 27 | const animationVariants = {
 28 |   "from-bottom": {
 29 |     initial: { y: "100%", opacity: 0 },
 30 |     animate: { y: 0, opacity: 1 },
 31 |     exit: { y: "100%", opacity: 0 },
 32 |   },
 33 |   "from-center": {
 34 |     initial: { scale: 0.5, opacity: 0 },
 35 |     animate: { scale: 1, opacity: 1 },
 36 |     exit: { scale: 0.5, opacity: 0 },
 37 |   },
 38 |   "from-top": {
 39 |     initial: { y: "-100%", opacity: 0 },
 40 |     animate: { y: 0, opacity: 1 },
 41 |     exit: { y: "-100%", opacity: 0 },
 42 |   },
 43 |   "from-left": {
 44 |     initial: { x: "-100%", opacity: 0 },
 45 |     animate: { x: 0, opacity: 1 },
 46 |     exit: { x: "-100%", opacity: 0 },
 47 |   },
 48 |   "from-right": {
 49 |     initial: { x: "100%", opacity: 0 },
 50 |     animate: { x: 0, opacity: 1 },
 51 |     exit: { x: "100%", opacity: 0 },
 52 |   },
 53 |   fade: {
 54 |     initial: { opacity: 0 },
 55 |     animate: { opacity: 1 },
 56 |     exit: { opacity: 0 },
 57 |   },
 58 |   "top-in-bottom-out": {
 59 |     initial: { y: "-100%", opacity: 0 },
 60 |     animate: { y: 0, opacity: 1 },
 61 |     exit: { y: "100%", opacity: 0 },
 62 |   },
 63 |   "left-in-right-out": {
 64 |     initial: { x: "-100%", opacity: 0 },
 65 |     animate: { x: 0, opacity: 1 },
 66 |     exit: { x: "100%", opacity: 0 },
 67 |   },
 68 | };
 69 | 
 70 | export default function HeroVideoDialog({
 71 |   animationStyle = "from-center",
 72 |   videoSrc,
 73 |   thumbnailSrc,
 74 |   thumbnailAlt = "Video thumbnail",
 75 |   className,
 76 | }: HeroVideoProps) {
 77 |   const [isVideoOpen, setIsVideoOpen] = useState(false);
 78 |   const selectedAnimation = animationVariants[animationStyle];
 79 | 
 80 |   return (
 81 |     
 82 |       
 setIsVideoOpen(true)}
 85 |       >
 86 |         
 93 |         
108 |       
109 |       
110 |         {isVideoOpen && (
111 |            setIsVideoOpen(false)}
115 |             exit={{ opacity: 0 }}
116 |             className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-md"
117 |           >
118 |             
123 |               
124 |                  
125 |                
126 |               
127 |                 
133 |               
134 |              
135 |            
136 |         )}
137 |        
138 |     
 
139 |   );
140 | }
141 | 
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react"
 2 | 
 3 | import { cn } from "@/lib/utils"
 4 | 
 5 | const Input = React.forwardRef>(
 6 |   ({ className, type, ...props }, ref) => {
 7 |     return (
 8 |        
17 |     )
18 |   }
19 | )
20 | Input.displayName = "Input"
21 | 
22 | export { Input }
23 | 
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
 1 | "use client"
 2 | 
 3 | import * as React from "react"
 4 | import * as LabelPrimitive from "@radix-ui/react-label"
 5 | import { cva, type VariantProps } from "class-variance-authority"
 6 | 
 7 | import { cn } from "@/lib/utils"
 8 | 
 9 | const labelVariants = cva(
10 |   "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 | 
13 | const Label = React.forwardRef<
14 |   React.ElementRef,
15 |   React.ComponentPropsWithoutRef &
16 |     VariantProps
17 | >(({ className, ...props }, ref) => (
18 |    
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 | 
26 | export { Label }
27 | 
--------------------------------------------------------------------------------
/src/components/ui/marquee.tsx:
--------------------------------------------------------------------------------
 1 | import { cn } from "@/lib/utils";
 2 | import { type ComponentPropsWithoutRef } from "react";
 3 | 
 4 | interface MarqueeProps extends ComponentPropsWithoutRef<"div"> {
 5 |   /**
 6 |    * Optional CSS class name to apply custom styles
 7 |    */
 8 |   className?: string;
 9 |   /**
10 |    * Whether to reverse the animation direction
11 |    * @default false
12 |    */
13 |   reverse?: boolean;
14 |   /**
15 |    * Whether to pause the animation on hover
16 |    * @default false
17 |    */
18 |   pauseOnHover?: boolean;
19 |   /**
20 |    * Content to be displayed in the marquee
21 |    */
22 |   children: React.ReactNode;
23 |   /**
24 |    * Whether to animate vertically instead of horizontally
25 |    * @default false
26 |    */
27 |   vertical?: boolean;
28 |   /**
29 |    * Number of times to repeat the content
30 |    * @default 4
31 |    */
32 |   repeat?: number;
33 | }
34 | 
35 | export default function Marquee({
36 |   className,
37 |   reverse = false,
38 |   pauseOnHover = false,
39 |   children,
40 |   vertical = false,
41 |   repeat = 4,
42 |   ...props
43 | }: MarqueeProps) {
44 |   return (
45 |     
56 |       {Array(repeat)
57 |         .fill(0)
58 |         .map((_, i) => (
59 |           
68 |             {children}
69 |           
70 |         ))}
71 |     
 
72 |   );
73 | }
74 | 
--------------------------------------------------------------------------------
/src/components/ui/rainbow-button.tsx:
--------------------------------------------------------------------------------
 1 | import React from "react";
 2 | 
 3 | import { cn } from "@/lib/utils";
 4 | type RainbowButtonProps = React.ButtonHTMLAttributes
 5 | 
 6 | export function RainbowButton({
 7 |   children,
 8 |   className,
 9 |   ...props
10 | }: RainbowButtonProps) {
11 |   return (
12 |     
29 |       {children}
30 |      
31 |   );
32 | }
33 | 
--------------------------------------------------------------------------------
/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
 1 | "use client"
 2 | 
 3 | import * as React from "react"
 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
 5 | 
 6 | import { cn } from "@/lib/utils"
 7 | 
 8 | const Separator = React.forwardRef<
 9 |   React.ElementRef,
10 |   React.ComponentPropsWithoutRef
11 | >(
12 |   (
13 |     { className, orientation = "horizontal", decorative = true, ...props },
14 |     ref
15 |   ) => (
16 |      
27 |   )
28 | )
29 | Separator.displayName = SeparatorPrimitive.Root.displayName
30 | 
31 | export { Separator }
32 | 
--------------------------------------------------------------------------------
/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
  1 | "use client"
  2 | 
  3 | import * as React from "react"
  4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
  5 | import { cva, type VariantProps } from "class-variance-authority"
  6 | import { X } from "lucide-react"
  7 | 
  8 | import { cn } from "@/lib/utils"
  9 | 
 10 | const Sheet = SheetPrimitive.Root
 11 | 
 12 | const SheetTrigger = SheetPrimitive.Trigger
 13 | 
 14 | const SheetClose = SheetPrimitive.Close
 15 | 
 16 | const SheetPortal = SheetPrimitive.Portal
 17 | 
 18 | const SheetOverlay = React.forwardRef<
 19 |   React.ElementRef,
 20 |   React.ComponentPropsWithoutRef
 21 | >(({ className, ...props }, ref) => (
 22 |    
 30 | ))
 31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
 32 | 
 33 | const sheetVariants = cva(
 34 |   "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out",
 35 |   {
 36 |     variants: {
 37 |       side: {
 38 |         top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
 39 |         bottom:
 40 |           "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
 41 |         left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
 42 |         right:
 43 |           "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
 44 |       },
 45 |     },
 46 |     defaultVariants: {
 47 |       side: "right",
 48 |     },
 49 |   }
 50 | )
 51 | 
 52 | interface SheetContentProps
 53 |   extends React.ComponentPropsWithoutRef,
 54 |     VariantProps {}
 55 | 
 56 | const SheetContent = React.forwardRef<
 57 |   React.ElementRef,
 58 |   SheetContentProps
 59 | >(({ side = "right", className, children, ...props }, ref) => (
 60 |   
 61 |      
 62 |     
 67 |       
 68 |          
 69 |         Close 
 70 |        
 71 |       {children}
 72 |      
 73 |    
 74 | ))
 75 | SheetContent.displayName = SheetPrimitive.Content.displayName
 76 | 
 77 | const SheetHeader = ({
 78 |   className,
 79 |   ...props
 80 | }: React.HTMLAttributes) => (
 81 |   
 88 | )
 89 | SheetHeader.displayName = "SheetHeader"
 90 | 
 91 | const SheetFooter = ({
 92 |   className,
 93 |   ...props
 94 | }: React.HTMLAttributes) => (
 95 |   
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 | 
105 | const SheetTitle = React.forwardRef<
106 |   React.ElementRef,
107 |   React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |    
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 | 
117 | const SheetDescription = React.forwardRef<
118 |   React.ElementRef,
119 |   React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |    
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 | 
129 | export {
130 |   Sheet,
131 |   SheetPortal,
132 |   SheetOverlay,
133 |   SheetTrigger,
134 |   SheetClose,
135 |   SheetContent,
136 |   SheetHeader,
137 |   SheetFooter,
138 |   SheetTitle,
139 |   SheetDescription,
140 | }
141 | 
--------------------------------------------------------------------------------
/src/components/ui/sidebar.tsx:
--------------------------------------------------------------------------------
  1 | "use client"
  2 | 
  3 | import * as React from "react"
  4 | import { Slot } from "@radix-ui/react-slot"
  5 | import { type VariantProps, cva } from "class-variance-authority"
  6 | import { PanelLeft } from "lucide-react"
  7 | 
  8 | import { useIsMobile } from "@/hooks/use-mobile"
  9 | import { cn } from "@/lib/utils"
 10 | import { Button } from "@/components/ui/button"
 11 | import { Input } from "@/components/ui/input"
 12 | import { Separator } from "@/components/ui/separator"
 13 | import { Sheet, SheetContent } from "@/components/ui/sheet"
 14 | import { Skeleton } from "@/components/ui/skeleton"
 15 | import {
 16 |   Tooltip,
 17 |   TooltipContent,
 18 |   TooltipProvider,
 19 |   TooltipTrigger,
 20 | } from "@/components/ui/tooltip"
 21 | 
 22 | const SIDEBAR_COOKIE_NAME = "sidebar:state"
 23 | const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
 24 | const SIDEBAR_WIDTH = "16rem"
 25 | const SIDEBAR_WIDTH_MOBILE = "18rem"
 26 | const SIDEBAR_WIDTH_ICON = "3rem"
 27 | const SIDEBAR_KEYBOARD_SHORTCUT = "b"
 28 | 
 29 | type SidebarContext = {
 30 |   state: "expanded" | "collapsed"
 31 |   open: boolean
 32 |   setOpen: (open: boolean) => void
 33 |   openMobile: boolean
 34 |   setOpenMobile: (open: boolean) => void
 35 |   isMobile: boolean
 36 |   toggleSidebar: () => void
 37 | }
 38 | 
 39 | const SidebarContext = React.createContext(null)
 40 | 
 41 | function useSidebar() {
 42 |   const context = React.useContext(SidebarContext)
 43 |   if (!context) {
 44 |     throw new Error("useSidebar must be used within a SidebarProvider.")
 45 |   }
 46 | 
 47 |   return context
 48 | }
 49 | 
 50 | const SidebarProvider = React.forwardRef<
 51 |   HTMLDivElement,
 52 |   React.ComponentProps<"div"> & {
 53 |     defaultOpen?: boolean
 54 |     open?: boolean
 55 |     onOpenChange?: (open: boolean) => void
 56 |   }
 57 | >(
 58 |   (
 59 |     {
 60 |       defaultOpen = true,
 61 |       open: openProp,
 62 |       onOpenChange: setOpenProp,
 63 |       className,
 64 |       style,
 65 |       children,
 66 |       ...props
 67 |     },
 68 |     ref
 69 |   ) => {
 70 |     const isMobile = useIsMobile()
 71 |     const [openMobile, setOpenMobile] = React.useState(false)
 72 | 
 73 |     // This is the internal state of the sidebar.
 74 |     // We use openProp and setOpenProp for control from outside the component.
 75 |     const [_open, _setOpen] = React.useState(defaultOpen)
 76 |     const open = openProp ?? _open
 77 |     const setOpen = React.useCallback(
 78 |       (value: boolean | ((value: boolean) => boolean)) => {
 79 |         const openState = typeof value === "function" ? value(open) : value
 80 |         if (setOpenProp) {
 81 |           setOpenProp(openState)
 82 |         } else {
 83 |           _setOpen(openState)
 84 |         }
 85 | 
 86 |         // This sets the cookie to keep the sidebar state.
 87 |         document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
 88 |       },
 89 |       [setOpenProp, open]
 90 |     )
 91 | 
 92 |     // Helper to toggle the sidebar.
 93 |     const toggleSidebar = React.useCallback(() => {
 94 |       return isMobile
 95 |         ? setOpenMobile((open) => !open)
 96 |         : setOpen((open) => !open)
 97 |     }, [isMobile, setOpen, setOpenMobile])
 98 | 
 99 |     // Adds a keyboard shortcut to toggle the sidebar.
100 |     React.useEffect(() => {
101 |       const handleKeyDown = (event: KeyboardEvent) => {
102 |         if (
103 |           event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
104 |           (event.metaKey || event.ctrlKey)
105 |         ) {
106 |           event.preventDefault()
107 |           toggleSidebar()
108 |         }
109 |       }
110 | 
111 |       window.addEventListener("keydown", handleKeyDown)
112 |       return () => window.removeEventListener("keydown", handleKeyDown)
113 |     }, [toggleSidebar])
114 | 
115 |     // We add a state so that we can do data-state="expanded" or "collapsed".
116 |     // This makes it easier to style the sidebar with Tailwind classes.
117 |     const state = open ? "expanded" : "collapsed"
118 | 
119 |     const contextValue = React.useMemo(
120 |       () => ({
121 |         state,
122 |         open,
123 |         setOpen,
124 |         isMobile,
125 |         openMobile,
126 |         setOpenMobile,
127 |         toggleSidebar,
128 |       }),
129 |       [state, open, setOpen, isMobile, openMobile, setOpenMobile, toggleSidebar]
130 |     )
131 | 
132 |     return (
133 |       
134 |         
135 |           
150 |             {children}
151 |           
152 |          
153 |        
154 |     )
155 |   }
156 | )
157 | SidebarProvider.displayName = "SidebarProvider"
158 | 
159 | const Sidebar = React.forwardRef<
160 |   HTMLDivElement,
161 |   React.ComponentProps<"div"> & {
162 |     side?: "left" | "right"
163 |     variant?: "sidebar" | "floating" | "inset"
164 |     collapsible?: "offcanvas" | "icon" | "none"
165 |   }
166 | >(
167 |   (
168 |     {
169 |       side = "left",
170 |       variant = "sidebar",
171 |       collapsible = "offcanvas",
172 |       className,
173 |       children,
174 |       ...props
175 |     },
176 |     ref
177 |   ) => {
178 |     const { isMobile, state, openMobile, setOpenMobile } = useSidebar()
179 | 
180 |     if (collapsible === "none") {
181 |       return (
182 |         
190 |           {children}
191 |         
192 |       )
193 |     }
194 | 
195 |     if (isMobile) {
196 |       return (
197 |         
198 |           
209 |             {children}
210 |            
211 |          
212 |       )
213 |     }
214 | 
215 |     return (
216 |       
224 |         {/* This is what handles the sidebar gap on desktop */}
225 |         
235 |         
249 |           
253 |             {children}
254 |           
255 |         
256 |       
 
257 |     )
258 |   }
259 | )
260 | Sidebar.displayName = "Sidebar"
261 | 
262 | const SidebarTrigger = React.forwardRef<
263 |   React.ElementRef,
264 |   React.ComponentProps
265 | >(({ className, onClick, ...props }, ref) => {
266 |   const { toggleSidebar } = useSidebar()
267 | 
268 |   return (
269 |      {
276 |         onClick?.(event)
277 |         toggleSidebar()
278 |       }}
279 |       {...props}
280 |     >
281 |        
282 |       Toggle Sidebar 
283 |      
284 |   )
285 | })
286 | SidebarTrigger.displayName = "SidebarTrigger"
287 | 
288 | const SidebarRail = React.forwardRef<
289 |   HTMLButtonElement,
290 |   React.ComponentProps<"button">
291 | >(({ className, ...props }, ref) => {
292 |   const { toggleSidebar } = useSidebar()
293 | 
294 |   return (
295 |      
313 |   )
314 | })
315 | SidebarRail.displayName = "SidebarRail"
316 | 
317 | const SidebarInset = React.forwardRef<
318 |   HTMLDivElement,
319 |   React.ComponentProps<"main">
320 | >(({ className, ...props }, ref) => {
321 |   return (
322 |      
331 |   )
332 | })
333 | SidebarInset.displayName = "SidebarInset"
334 | 
335 | const SidebarInput = React.forwardRef<
336 |   React.ElementRef,
337 |   React.ComponentProps
338 | >(({ className, ...props }, ref) => {
339 |   return (
340 |      
349 |   )
350 | })
351 | SidebarInput.displayName = "SidebarInput"
352 | 
353 | const SidebarHeader = React.forwardRef<
354 |   HTMLDivElement,
355 |   React.ComponentProps<"div">
356 | >(({ className, ...props }, ref) => {
357 |   return (
358 |     
364 |   )
365 | })
366 | SidebarHeader.displayName = "SidebarHeader"
367 | 
368 | const SidebarFooter = React.forwardRef<
369 |   HTMLDivElement,
370 |   React.ComponentProps<"div">
371 | >(({ className, ...props }, ref) => {
372 |   return (
373 |     
379 |   )
380 | })
381 | SidebarFooter.displayName = "SidebarFooter"
382 | 
383 | const SidebarSeparator = React.forwardRef<
384 |   React.ElementRef,
385 |   React.ComponentProps
386 | >(({ className, ...props }, ref) => {
387 |   return (
388 |      
394 |   )
395 | })
396 | SidebarSeparator.displayName = "SidebarSeparator"
397 | 
398 | const SidebarContent = React.forwardRef<
399 |   HTMLDivElement,
400 |   React.ComponentProps<"div">
401 | >(({ className, ...props }, ref) => {
402 |   return (
403 |     
412 |   )
413 | })
414 | SidebarContent.displayName = "SidebarContent"
415 | 
416 | const SidebarGroup = React.forwardRef<
417 |   HTMLDivElement,
418 |   React.ComponentProps<"div">
419 | >(({ className, ...props }, ref) => {
420 |   return (
421 |     
427 |   )
428 | })
429 | SidebarGroup.displayName = "SidebarGroup"
430 | 
431 | const SidebarGroupLabel = React.forwardRef<
432 |   HTMLDivElement,
433 |   React.ComponentProps<"div"> & { asChild?: boolean }
434 | >(({ className, asChild = false, ...props }, ref) => {
435 |   const Comp = asChild ? Slot : "div"
436 | 
437 |   return (
438 |     svg]:size-4 [&>svg]:shrink-0",
443 |         "group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
444 |         className
445 |       )}
446 |       {...props}
447 |     />
448 |   )
449 | })
450 | SidebarGroupLabel.displayName = "SidebarGroupLabel"
451 | 
452 | const SidebarGroupAction = React.forwardRef<
453 |   HTMLButtonElement,
454 |   React.ComponentProps<"button"> & { asChild?: boolean }
455 | >(({ className, asChild = false, ...props }, ref) => {
456 |   const Comp = asChild ? Slot : "button"
457 | 
458 |   return (
459 |     svg]:size-4 [&>svg]:shrink-0",
464 |         // Increases the hit area of the button on mobile.
465 |         "after:absolute after:-inset-2 after:md:hidden",
466 |         "group-data-[collapsible=icon]:hidden",
467 |         className
468 |       )}
469 |       {...props}
470 |     />
471 |   )
472 | })
473 | SidebarGroupAction.displayName = "SidebarGroupAction"
474 | 
475 | const SidebarGroupContent = React.forwardRef<
476 |   HTMLDivElement,
477 |   React.ComponentProps<"div">
478 | >(({ className, ...props }, ref) => (
479 |   
485 | ))
486 | SidebarGroupContent.displayName = "SidebarGroupContent"
487 | 
488 | const SidebarMenu = React.forwardRef<
489 |   HTMLUListElement,
490 |   React.ComponentProps<"ul">
491 | >(({ className, ...props }, ref) => (
492 |   
498 | ))
499 | SidebarMenu.displayName = "SidebarMenu"
500 | 
501 | const SidebarMenuItem = React.forwardRef<
502 |   HTMLLIElement,
503 |   React.ComponentProps<"li">
504 | >(({ className, ...props }, ref) => (
505 |    
511 | ))
512 | SidebarMenuItem.displayName = "SidebarMenuItem"
513 | 
514 | const sidebarMenuButtonVariants = cva(
515 |   "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
516 |   {
517 |     variants: {
518 |       variant: {
519 |         default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
520 |         outline:
521 |           "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
522 |       },
523 |       size: {
524 |         default: "h-8 text-sm",
525 |         sm: "h-7 text-xs",
526 |         lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
527 |       },
528 |     },
529 |     defaultVariants: {
530 |       variant: "default",
531 |       size: "default",
532 |     },
533 |   }
534 | )
535 | 
536 | const SidebarMenuButton = React.forwardRef<
537 |   HTMLButtonElement,
538 |   React.ComponentProps<"button"> & {
539 |     asChild?: boolean
540 |     isActive?: boolean
541 |     tooltip?: string | React.ComponentProps
542 |   } & VariantProps
543 | >(
544 |   (
545 |     {
546 |       asChild = false,
547 |       isActive = false,
548 |       variant = "default",
549 |       size = "default",
550 |       tooltip,
551 |       className,
552 |       ...props
553 |     },
554 |     ref
555 |   ) => {
556 |     const Comp = asChild ? Slot : "button"
557 |     const { isMobile, state } = useSidebar()
558 | 
559 |     const button = (
560 |        
568 |     )
569 | 
570 |     if (!tooltip) {
571 |       return button
572 |     }
573 | 
574 |     if (typeof tooltip === "string") {
575 |       tooltip = {
576 |         children: tooltip,
577 |       }
578 |     }
579 | 
580 |     return (
581 |       
582 |         {button} 
583 |          
589 |        
590 |     )
591 |   }
592 | )
593 | SidebarMenuButton.displayName = "SidebarMenuButton"
594 | 
595 | const SidebarMenuAction = React.forwardRef<
596 |   HTMLButtonElement,
597 |   React.ComponentProps<"button"> & {
598 |     asChild?: boolean
599 |     showOnHover?: boolean
600 |   }
601 | >(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
602 |   const Comp = asChild ? Slot : "button"
603 | 
604 |   return (
605 |     svg]:size-4 [&>svg]:shrink-0",
610 |         // Increases the hit area of the button on mobile.
611 |         "after:absolute after:-inset-2 after:md:hidden",
612 |         "peer-data-[size=sm]/menu-button:top-1",
613 |         "peer-data-[size=default]/menu-button:top-1.5",
614 |         "peer-data-[size=lg]/menu-button:top-2.5",
615 |         "group-data-[collapsible=icon]:hidden",
616 |         showOnHover &&
617 |           "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
618 |         className
619 |       )}
620 |       {...props}
621 |     />
622 |   )
623 | })
624 | SidebarMenuAction.displayName = "SidebarMenuAction"
625 | 
626 | const SidebarMenuBadge = React.forwardRef<
627 |   HTMLDivElement,
628 |   React.ComponentProps<"div">
629 | >(({ className, ...props }, ref) => (
630 |   
644 | ))
645 | SidebarMenuBadge.displayName = "SidebarMenuBadge"
646 | 
647 | const SidebarMenuSkeleton = React.forwardRef<
648 |   HTMLDivElement,
649 |   React.ComponentProps<"div"> & {
650 |     showIcon?: boolean
651 |   }
652 | >(({ className, showIcon = false, ...props }, ref) => {
653 |   // Random width between 50 to 90%.
654 |   const width = React.useMemo(() => {
655 |     return `${Math.floor(Math.random() * 40) + 50}%`
656 |   }, [])
657 | 
658 |   return (
659 |     
665 |       {showIcon && (
666 |          
670 |       )}
671 |        
680 |     
681 |   )
682 | })
683 | SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton"
684 | 
685 | const SidebarMenuSub = React.forwardRef<
686 |   HTMLUListElement,
687 |   React.ComponentProps<"ul">
688 | >(({ className, ...props }, ref) => (
689 |   
699 | ))
700 | SidebarMenuSub.displayName = "SidebarMenuSub"
701 | 
702 | const SidebarMenuSubItem = React.forwardRef<
703 |   HTMLLIElement,
704 |   React.ComponentProps<"li">
705 | >(({ ...props }, ref) =>  )
706 | SidebarMenuSubItem.displayName = "SidebarMenuSubItem"
707 | 
708 | const SidebarMenuSubButton = React.forwardRef<
709 |   HTMLAnchorElement,
710 |   React.ComponentProps<"a"> & {
711 |     asChild?: boolean
712 |     size?: "sm" | "md"
713 |     isActive?: boolean
714 |   }
715 | >(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
716 |   const Comp = asChild ? Slot : "a"
717 | 
718 |   return (
719 |     span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
726 |         "data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
727 |         size === "sm" && "text-xs",
728 |         size === "md" && "text-sm",
729 |         "group-data-[collapsible=icon]:hidden",
730 |         className
731 |       )}
732 |       {...props}
733 |     />
734 |   )
735 | })
736 | SidebarMenuSubButton.displayName = "SidebarMenuSubButton"
737 | 
738 | export {
739 |   Sidebar,
740 |   SidebarContent,
741 |   SidebarFooter,
742 |   SidebarGroup,
743 |   SidebarGroupAction,
744 |   SidebarGroupContent,
745 |   SidebarGroupLabel,
746 |   SidebarHeader,
747 |   SidebarInput,
748 |   SidebarInset,
749 |   SidebarMenu,
750 |   SidebarMenuAction,
751 |   SidebarMenuBadge,
752 |   SidebarMenuButton,
753 |   SidebarMenuItem,
754 |   SidebarMenuSkeleton,
755 |   SidebarMenuSub,
756 |   SidebarMenuSubButton,
757 |   SidebarMenuSubItem,
758 |   SidebarProvider,
759 |   SidebarRail,
760 |   SidebarSeparator,
761 |   SidebarTrigger,
762 |   useSidebar,
763 | }
764 | 
--------------------------------------------------------------------------------
/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
 1 | import { cn } from "@/lib/utils"
 2 | 
 3 | function Skeleton({
 4 |   className,
 5 |   ...props
 6 | }: React.HTMLAttributes) {
 7 |   return (
 8 |     
12 |   )
13 | }
14 | 
15 | export { Skeleton }
16 | 
--------------------------------------------------------------------------------
/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
 1 | "use client"
 2 | 
 3 | import * as React from "react"
 4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
 5 | 
 6 | import { cn } from "@/lib/utils"
 7 | 
 8 | const Switch = React.forwardRef<
 9 |   React.ElementRef,
10 |   React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |   
20 |      
25 |    
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 | 
29 | export { Switch }
30 | 
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
 1 | "use client"
 2 | 
 3 | import * as React from "react"
 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
 5 | 
 6 | import { cn } from "@/lib/utils"
 7 | 
 8 | const TooltipProvider = TooltipPrimitive.Provider
 9 | 
10 | const Tooltip = TooltipPrimitive.Root
11 | 
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 | 
14 | const TooltipContent = React.forwardRef<
15 |   React.ElementRef,
16 |   React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |   
19 |      
28 |    
29 | ))
30 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
31 | 
32 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
33 | 
--------------------------------------------------------------------------------
/src/components/ui/tracing-beam.tsx:
--------------------------------------------------------------------------------
1 |  
--------------------------------------------------------------------------------
/src/env.js:
--------------------------------------------------------------------------------
 1 | import { createEnv } from "@t3-oss/env-nextjs";
 2 | import { z } from "zod";
 3 | 
 4 | export const env = createEnv({
 5 |   /**
 6 |    * Specify your server-side environment variables schema here. This way you can ensure the app
 7 |    * isn't built with invalid env vars.
 8 |    */
 9 |   server: {
10 |     DATABASE_URL: z.string().url(),
11 |     NODE_ENV: z
12 |       .enum(["development", "test", "production"])
13 |       .default("development"),
14 |     STACK_SECRET_SERVER_KEY: z.string().min(1),
15 |     RESEND_API_KEY: z.string().optional(),
16 |     RESEND_AUDIENCE_ID: z.string().optional(),
17 |     SENTRY_ORG: z.string().optional(),
18 |     SENTRY_PROJECT: z.string().optional(),
19 |   },
20 | 
21 |   /**
22 |    * Specify your client-side environment variables schema here. This way you can ensure the app
23 |    * isn't built with invalid env vars. To expose them to the client, prefix them with
24 |    * `NEXT_PUBLIC_`.
25 |    */
26 |   client: {
27 |     // NEXT_PUBLIC_CLIENTVAR: z.string(),
28 |     NEXT_PUBLIC_STACK_PROJECT_ID: z.string().min(1),
29 |     NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY: z.string().min(1),
30 |     NEXT_PUBLIC_POSTHOG_HOST: z.string().optional(),
31 |     NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
32 |   },
33 | 
34 |   /**
35 |    * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g.
36 |    * middlewares) or client-side so we need to destruct manually.
37 |    */
38 |   runtimeEnv: {
39 |     DATABASE_URL: process.env.DATABASE_URL,
40 |     NODE_ENV: process.env.NODE_ENV,
41 |     STACK_SECRET_SERVER_KEY: process.env.STACK_SECRET_SERVER_KEY,
42 |     NEXT_PUBLIC_STACK_PROJECT_ID: process.env.NEXT_PUBLIC_STACK_PROJECT_ID,
43 |     NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY:
44 |       process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY,
45 |     NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
46 |     NEXT_PUBLIC_POSTHOG_KEY: process.env.NEXT_PUBLIC_POSTHOG_KEY,
47 |     // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
48 |     RESEND_API_KEY: process.env.RESEND_API_KEY,
49 |     RESEND_AUDIENCE_ID: process.env.RESEND_AUDIENCE_ID,
50 |     SENTRY_ORG: process.env.SENTRY_ORG,
51 |     SENTRY_PROJECT: process.env.SENTRY_PROJECT,
52 |   },
53 |   /**
54 |    * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
55 |    * useful for Docker builds.
56 |    */
57 |   skipValidation: !!process.env.SKIP_ENV_VALIDATION,
58 |   /**
59 |    * Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and
60 |    * `SOME_VAR=''` will throw an error.
61 |    */
62 |   emptyStringAsUndefined: true,
63 | });
64 | 
--------------------------------------------------------------------------------
/src/hooks/use-mobile.tsx:
--------------------------------------------------------------------------------
 1 | import * as React from "react"
 2 | 
 3 | const MOBILE_BREAKPOINT = 768
 4 | 
 5 | export function useIsMobile() {
 6 |   const [isMobile, setIsMobile] = React.useState(undefined)
 7 | 
 8 |   React.useEffect(() => {
 9 |     const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 |     const onChange = () => {
11 |       setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 |     }
13 |     mql.addEventListener("change", onChange)
14 |     setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 |     return () => mql.removeEventListener("change", onChange)
16 |   }, [])
17 | 
18 |   return !!isMobile
19 | }
20 | 
--------------------------------------------------------------------------------
/src/instrumentation.ts:
--------------------------------------------------------------------------------
 1 | import * as Sentry from "@sentry/nextjs";
 2 | 
 3 | export async function register() {
 4 |   if (process.env.NEXT_RUNTIME === "nodejs") {
 5 |     await import("../sentry.server.config");
 6 |   }
 7 | 
 8 |   if (process.env.NEXT_RUNTIME === "edge") {
 9 |     await import("../sentry.edge.config");
10 |   }
11 | }
12 | 
13 | export const onRequestError = Sentry.captureRequestError;
14 | 
--------------------------------------------------------------------------------
/src/lib/mail.ts:
--------------------------------------------------------------------------------
 1 | import { Resend } from "resend";
 2 | import { env } from "@/env";
 3 | 
 4 | const resend = env.RESEND_API_KEY ? new Resend(env.RESEND_API_KEY) : null;
 5 | const AUDIENCE_ID = env.RESEND_AUDIENCE_ID;
 6 | 
 7 | export type SendEmailParams = {
 8 |   to: string;
 9 |   subject: string;
10 |   html: string;
11 |   text?: string;
12 | };
13 | 
14 | export type NewsletterContact = {
15 |   email: string;
16 |   firstName?: string;
17 |   lastName?: string;
18 |   data?: Record;
19 | };
20 | 
21 | export type SendEmailResult = {
22 |   success: boolean;
23 |   data?: unknown;
24 |   error?: unknown;
25 | };
26 | 
27 | export async function sendEmail(
28 |   params: SendEmailParams,
29 | ): Promise {
30 |   if (!resend) {
31 |     console.warn('Email service not configured: Missing RESEND_API_KEY');
32 |     return { success: false, error: 'Email service not configured' };
33 |   }
34 | 
35 |   const { to, subject, html, text } = params;
36 | 
37 |   try {
38 |     const data = await resend.emails.send({
39 |       from: "SaaS Boilerplate ",
40 |       to,
41 |       subject,
42 |       html,
43 |       text: text ?? html.replace(/<[^>]*>/g, ""),
44 |     });
45 | 
46 |     return { success: true, data };
47 |   } catch (error) {
48 |     console.error("Failed to send email:", error);
49 |     return { success: false, error };
50 |   }
51 | }
52 | 
53 | export async function addContactToNewsletter(
54 |   contact: NewsletterContact,
55 | ): Promise {
56 |   if (!resend) {
57 |     console.warn('Email service not configured: Missing RESEND_API_KEY');
58 |     return { success: false, error: 'Email service not configured' };
59 |   }
60 | 
61 |   if (!AUDIENCE_ID) {
62 |     console.warn('Newsletter service not configured: Missing RESEND_AUDIENCE_ID');
63 |     return { success: false, error: 'Newsletter service not configured' };
64 |   }
65 | 
66 |   try {
67 |     const data = await resend.contacts.create({
68 |       email: contact.email,
69 |       firstName: contact.firstName,
70 |       lastName: contact.lastName,
71 |       unsubscribed: false,
72 |       audienceId: AUDIENCE_ID,
73 |       ...(contact.data ?? {}),
74 |     });
75 | 
76 |     return { success: true, data };
77 |   } catch (error) {
78 |     console.error("Failed to add contact:", error);
79 |     return { success: false, error };
80 |   }
81 | }
82 | 
--------------------------------------------------------------------------------
/src/lib/posthog.ts:
--------------------------------------------------------------------------------
 1 | import { PostHog } from "posthog-node";
 2 | import { env } from "@/env";
 3 | 
 4 | // use this client for server side
 5 | export default function PostHogClient(): PostHog | undefined {
 6 |   if (!env.NEXT_PUBLIC_POSTHOG_KEY) {
 7 |     return undefined;
 8 |   }
 9 | 
10 |   try {
11 |     return new PostHog(env.NEXT_PUBLIC_POSTHOG_KEY, {
12 |       host: env.NEXT_PUBLIC_POSTHOG_HOST,
13 |       flushAt: 1,
14 |       flushInterval: 0,
15 |     });
16 |   } catch (error) {
17 |     console.error("Failed to initialize PostHog client:", error);
18 |     return undefined;
19 |   }
20 | }
21 | 
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 | 
4 | export function cn(...inputs: ClassValue[]) {
5 |   return twMerge(clsx(inputs))
6 | }
7 | 
--------------------------------------------------------------------------------
/src/provider/posthog-provider.tsx:
--------------------------------------------------------------------------------
 1 | "use client";
 2 | 
 3 | import posthog from "posthog-js";
 4 | import { PostHogProvider } from "posthog-js/react";
 5 | import { useEffect } from "react";
 6 | import { env } from "@/env";
 7 | 
 8 | export function PosthogProvider({ children }: { children: React.ReactNode }) {
 9 |   useEffect(() => {
10 |     if (!env.NEXT_PUBLIC_POSTHOG_KEY || typeof window === 'undefined') {
11 |       return;
12 |     }
13 | 
14 |     try {
15 |       posthog.init(env.NEXT_PUBLIC_POSTHOG_KEY, {
16 |         api_host: env.NEXT_PUBLIC_POSTHOG_HOST ?? 'https://app.posthog.com',
17 |         person_profiles: "identified_only",
18 |         capture_pageview: false,
19 |         capture_pageleave: true,
20 |       });
21 |     } catch (error) {
22 |       console.error('Failed to initialize PostHog:', error);
23 |     }
24 |   }, []);
25 | 
26 |   if (!env.NEXT_PUBLIC_POSTHOG_KEY || typeof window === 'undefined') {
27 |     return <>{children}>;
28 |   }
29 | 
30 |   return {children} ;
31 | }
32 | 
--------------------------------------------------------------------------------
/src/server/api/root.ts:
--------------------------------------------------------------------------------
 1 | import { postRouter } from "@/server/api/routers/post";
 2 | import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc";
 3 | import { emailRouter } from "./routers/email";
 4 | 
 5 | /**
 6 |  * This is the primary router for your server.
 7 |  *
 8 |  * All routers added in /api/routers should be manually added here.
 9 |  */
10 | export const appRouter = createTRPCRouter({
11 |   post: postRouter,
12 |   email: emailRouter,
13 | });
14 | 
15 | // export type definition of API
16 | export type AppRouter = typeof appRouter;
17 | 
18 | /**
19 |  * Create a server-side caller for the tRPC API.
20 |  * @example
21 |  * const trpc = createCaller(createContext);
22 |  * const res = await trpc.post.all();
23 |  *       ^? Post[]
24 |  */
25 | export const createCaller = createCallerFactory(appRouter);
26 | 
--------------------------------------------------------------------------------
/src/server/api/routers/email.ts:
--------------------------------------------------------------------------------
 1 | import { z } from "zod";
 2 | import {
 3 |   createTRPCRouter,
 4 |   protectedProcedure,
 5 |   publicProcedure,
 6 | } from "@/server/api/trpc";
 7 | import { sendEmail, addContactToNewsletter } from "@/lib/mail";
 8 | 
 9 | export const emailRouter = createTRPCRouter({
10 |   // Regular email sending
11 |   send: protectedProcedure
12 |     .input(
13 |       z.object({
14 |         to: z.string().email(),
15 |         subject: z.string(),
16 |         html: z.string(),
17 |         text: z.string().optional(),
18 |       }),
19 |     )
20 |     .mutation(async ({ input }) => {
21 |       return sendEmail(input);
22 |     }),
23 | 
24 |   // Newsletter subscription
25 |   subscribe: publicProcedure
26 |     .input(
27 |       z.object({
28 |         email: z.string().email(),
29 |         firstName: z.string().optional(),
30 |         lastName: z.string().optional(),
31 |         data: z.record(z.unknown()).optional(),
32 |       }),
33 |     )
34 |     .mutation(async ({ input }) => {
35 |       return addContactToNewsletter(input);
36 |     }),
37 | });
38 | 
--------------------------------------------------------------------------------
/src/server/api/routers/post.ts:
--------------------------------------------------------------------------------
 1 | import { z } from "zod";
 2 | 
 3 | import {
 4 |   createTRPCRouter,
 5 |   protectedProcedure,
 6 |   publicProcedure,
 7 | } from "@/server/api/trpc";
 8 | 
 9 | export const postRouter = createTRPCRouter({
10 |   hello: publicProcedure
11 |     .input(z.object({ text: z.string() }))
12 |     .query(({ input }) => {
13 |       return {
14 |         greeting: `Hello ${input.text}`,
15 |       };
16 |     }),
17 | 
18 |   create: protectedProcedure
19 |     .input(z.object({ name: z.string().min(1) }))
20 |     .mutation(async ({ ctx, input }) => {
21 |       return ctx.db.post.create({
22 |         data: {
23 |           name: input.name,
24 |         },
25 |       });
26 |     }),
27 | 
28 |   getLatest: publicProcedure.query(async ({ ctx }) => {
29 |     const post = await ctx.db.post.findFirst({
30 |       orderBy: { createdAt: "desc" },
31 |     });
32 | 
33 |     return post ?? null;
34 |   }),
35 | });
36 | 
--------------------------------------------------------------------------------
/src/server/api/trpc.ts:
--------------------------------------------------------------------------------
  1 | /**
  2 |  * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
  3 |  * 1. You want to modify request context (see Part 1).
  4 |  * 2. You want to create a new middleware or type of procedure (see Part 3).
  5 |  *
  6 |  * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
  7 |  * need to use are documented accordingly near the end.
  8 |  */
  9 | import { initTRPC, TRPCError } from "@trpc/server";
 10 | import superjson from "superjson";
 11 | import { ZodError } from "zod";
 12 | 
 13 | import { db } from "@/server/db";
 14 | import { stackServerApp } from "@/stack";
 15 | 
 16 | /**
 17 |  * 1. CONTEXT
 18 |  *
 19 |  * This section defines the "contexts" that are available in the backend API.
 20 |  *
 21 |  * These allow you to access things when processing a request, like the database, the session, etc.
 22 |  *
 23 |  * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each
 24 |  * wrap this and provides the required context.
 25 |  *
 26 |  * @see https://trpc.io/docs/server/context
 27 |  */
 28 | export const createTRPCContext = async (opts: { headers: Headers }) => {
 29 |   const user = await stackServerApp.getUser();
 30 |   
 31 |   return {
 32 |     db,
 33 |     user,
 34 |     ...opts,
 35 |   };
 36 | };
 37 | 
 38 | /**
 39 |  * 2. INITIALIZATION
 40 |  *
 41 |  * This is where the tRPC API is initialized, connecting the context and transformer. We also parse
 42 |  * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
 43 |  * errors on the backend.
 44 |  */
 45 | const t = initTRPC.context().create({
 46 |   transformer: superjson,
 47 |   errorFormatter({ shape, error }) {
 48 |     return {
 49 |       ...shape,
 50 |       data: {
 51 |         ...shape.data,
 52 |         zodError:
 53 |           error.cause instanceof ZodError ? error.cause.flatten() : null,
 54 |       },
 55 |     };
 56 |   },
 57 | });
 58 | 
 59 | /**
 60 |  * Create a server-side caller.
 61 |  *
 62 |  * @see https://trpc.io/docs/server/server-side-calls
 63 |  */
 64 | export const createCallerFactory = t.createCallerFactory;
 65 | 
 66 | /**
 67 |  * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
 68 |  *
 69 |  * These are the pieces you use to build your tRPC API. You should import these a lot in the
 70 |  * "/src/server/api/routers" directory.
 71 |  */
 72 | 
 73 | /**
 74 |  * This is how you create new routers and sub-routers in your tRPC API.
 75 |  *
 76 |  * @see https://trpc.io/docs/router
 77 |  */
 78 | export const createTRPCRouter = t.router;
 79 | 
 80 | /**
 81 |  * Middleware for timing procedure execution and adding an artificial delay in development.
 82 |  *
 83 |  * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating
 84 |  * network latency that would occur in production but not in local development.
 85 |  */
 86 | const timingMiddleware = t.middleware(async ({ next, path }) => {
 87 |   const start = Date.now();
 88 | 
 89 |   if (t._config.isDev) {
 90 |     // artificial delay in dev
 91 |     const waitMs = Math.floor(Math.random() * 400) + 100;
 92 |     await new Promise((resolve) => setTimeout(resolve, waitMs));
 93 |   }
 94 | 
 95 |   const result = await next();
 96 | 
 97 |   const end = Date.now();
 98 |   console.log(`[TRPC] ${path} took ${end - start}ms to execute`);
 99 | 
100 |   return result;
101 | });
102 | 
103 | const authMiddleware = t.middleware(async ({ ctx, next }) => {
104 |   if (!ctx.user) {
105 |     throw new TRPCError({ code: "UNAUTHORIZED" });
106 |   }
107 |   
108 |   return next({
109 |     ctx: {
110 |       ...ctx,
111 |       user: ctx.user,
112 |     },
113 |   });
114 | });
115 | 
116 | /**
117 |  * Public (unauthenticated) procedure
118 |  *
119 |  * This is the base piece you use to build new queries and mutations on your tRPC API. It does not
120 |  * guarantee that a user querying is authorized, but you can still access user session data if they
121 |  * are logged in.
122 |  */
123 | export const publicProcedure = t.procedure.use(timingMiddleware);
124 | export const protectedProcedure = t.procedure
125 |   .use(timingMiddleware)
126 |   .use(authMiddleware);
--------------------------------------------------------------------------------
/src/server/db.ts:
--------------------------------------------------------------------------------
 1 | import { PrismaClient } from "@prisma/client";
 2 | 
 3 | import { env } from "@/env";
 4 | 
 5 | const createPrismaClient = () =>
 6 |   new PrismaClient({
 7 |     log:
 8 |       env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
 9 |   });
10 | 
11 | const globalForPrisma = globalThis as unknown as {
12 |   prisma: ReturnType | undefined;
13 | };
14 | 
15 | export const db = globalForPrisma.prisma ?? createPrismaClient();
16 | 
17 | if (env.NODE_ENV !== "production") globalForPrisma.prisma = db;
18 | 
--------------------------------------------------------------------------------
/src/stack.tsx:
--------------------------------------------------------------------------------
 1 | import "server-only";
 2 | 
 3 | import { StackServerApp } from "@stackframe/stack";
 4 | 
 5 | export const stackServerApp = new StackServerApp({
 6 |   tokenStore: "nextjs-cookie",
 7 |   urls: {
 8 |   afterSignIn: "/app",
 9 |   afterSignUp: "/app",
10 |     },
11 | });
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
  1 | @tailwind base;
  2 | @tailwind components;
  3 | @tailwind utilities;
  4 | @layer base {
  5 |   :root {
  6 |     --background: 0 0% 100%;
  7 |     --foreground: 240 10% 3.9%;
  8 |     --card: 0 0% 100%;
  9 |     --card-foreground: 240 10% 3.9%;
 10 |     --popover: 0 0% 100%;
 11 |     --popover-foreground: 240 10% 3.9%;
 12 |     --primary: 240 5.9% 10%;
 13 |     --primary-foreground: 0 0% 98%;
 14 |     --secondary: 240 4.8% 95.9%;
 15 |     --secondary-foreground: 240 5.9% 10%;
 16 |     --muted: 240 4.8% 95.9%;
 17 |     --muted-foreground: 240 3.8% 46.1%;
 18 |     --accent: 240 4.8% 95.9%;
 19 |     --accent-foreground: 240 5.9% 10%;
 20 |     --destructive: 0 84.2% 60.2%;
 21 |     --destructive-foreground: 0 0% 98%;
 22 |     --border: 240 5.9% 90%;
 23 |     --input: 240 5.9% 90%;
 24 |     --ring: 240 10% 3.9%;
 25 |     --chart-1: 12 76% 61%;
 26 |     --chart-2: 173 58% 39%;
 27 |     --chart-3: 197 37% 24%;
 28 |     --chart-4: 43 74% 66%;
 29 |     --chart-5: 27 87% 67%;
 30 |     --radius: 0.5rem
 31 |   ;
 32 |     --color-1: 0 100% 63%;
 33 |     --color-2: 270 100% 63%;
 34 |     --color-3: 210 100% 63%;
 35 |     --color-4: 195 100% 63%;
 36 |     --color-5: 90 100% 63%;
 37 |     --sidebar-background: 0 0% 98%;
 38 |     --sidebar-foreground: 240 5.3% 26.1%;
 39 |     --sidebar-primary: 240 5.9% 10%;
 40 |     --sidebar-primary-foreground: 0 0% 98%;
 41 |     --sidebar-accent: 240 4.8% 95.9%;
 42 |     --sidebar-accent-foreground: 240 5.9% 10%;
 43 |     --sidebar-border: 220 13% 91%;
 44 |     --sidebar-ring: 217.2 91.2% 59.8%}
 45 |   .dark {
 46 |     --background: 240 10% 3.9%;
 47 |     --foreground: 0 0% 98%;
 48 |     --card: 240 10% 3.9%;
 49 |     --card-foreground: 0 0% 98%;
 50 |     --popover: 240 10% 3.9%;
 51 |     --popover-foreground: 0 0% 98%;
 52 |     --primary: 0 0% 98%;
 53 |     --primary-foreground: 240 5.9% 10%;
 54 |     --secondary: 240 3.7% 15.9%;
 55 |     --secondary-foreground: 0 0% 98%;
 56 |     --muted: 240 3.7% 15.9%;
 57 |     --muted-foreground: 240 5% 64.9%;
 58 |     --accent: 240 3.7% 15.9%;
 59 |     --accent-foreground: 0 0% 98%;
 60 |     --destructive: 0 62.8% 30.6%;
 61 |     --destructive-foreground: 0 0% 98%;
 62 |     --border: 240 3.7% 15.9%;
 63 |     --input: 240 3.7% 15.9%;
 64 |     --ring: 240 4.9% 83.9%;
 65 |     --chart-1: 220 70% 50%;
 66 |     --chart-2: 160 60% 45%;
 67 |     --chart-3: 30 80% 55%;
 68 |     --chart-4: 280 65% 60%;
 69 |     --chart-5: 340 75% 55%
 70 |   ;
 71 |     --color-1: 0 100% 63%;
 72 |     --color-2: 270 100% 63%;
 73 |     --color-3: 210 100% 63%;
 74 |     --color-4: 195 100% 63%;
 75 |     --color-5: 90 100% 63%;
 76 |     --sidebar-background: 240 5.9% 10%;
 77 |     --sidebar-foreground: 240 4.8% 95.9%;
 78 |     --sidebar-primary: 224.3 76.3% 48%;
 79 |     --sidebar-primary-foreground: 0 0% 100%;
 80 |     --sidebar-accent: 240 3.7% 15.9%;
 81 |     --sidebar-accent-foreground: 240 4.8% 95.9%;
 82 |     --sidebar-border: 240 3.7% 15.9%;
 83 |     --sidebar-ring: 217.2 91.2% 59.8%}
 84 | }
 85 | @layer base {
 86 |   * {
 87 |     @apply border-border;
 88 |   }
 89 |   body {
 90 |     @apply bg-background text-foreground;
 91 |   }
 92 | }
 93 | 
 94 | @layer base {
 95 |   :root {
 96 |     --sidebar-background: 0 0% 98%;
 97 |     --sidebar-foreground: 240 5.3% 26.1%;
 98 |     --sidebar-primary: 240 5.9% 10%;
 99 |     --sidebar-primary-foreground: 0 0% 98%;
100 |     --sidebar-accent: 240 4.8% 95.9%;
101 |     --sidebar-accent-foreground: 240 5.9% 10%;
102 |     --sidebar-border: 220 13% 91%;
103 |     --sidebar-ring: 217.2 91.2% 59.8%;
104 |   }
105 | 
106 |   .dark {
107 |     --sidebar-background: 240 5.9% 10%;
108 |     --sidebar-foreground: 240 4.8% 95.9%;
109 |     --sidebar-primary: 224.3 76.3% 48%;
110 |     --sidebar-primary-foreground: 0 0% 100%;
111 |     --sidebar-accent: 240 3.7% 15.9%;
112 |     --sidebar-accent-foreground: 240 4.8% 95.9%;
113 |     --sidebar-border: 240 3.7% 15.9%;
114 |     --sidebar-ring: 217.2 91.2% 59.8%;
115 |   }
116 | }
117 | 
--------------------------------------------------------------------------------
/src/trpc/query-client.ts:
--------------------------------------------------------------------------------
 1 | import {
 2 |   defaultShouldDehydrateQuery,
 3 |   QueryClient,
 4 | } from "@tanstack/react-query";
 5 | import SuperJSON from "superjson";
 6 | 
 7 | export const createQueryClient = () =>
 8 |   new QueryClient({
 9 |     defaultOptions: {
10 |       queries: {
11 |         // With SSR, we usually want to set some default staleTime
12 |         // above 0 to avoid refetching immediately on the client
13 |         staleTime: 30 * 1000,
14 |       },
15 |       dehydrate: {
16 |         serializeData: SuperJSON.serialize,
17 |         shouldDehydrateQuery: (query) =>
18 |           defaultShouldDehydrateQuery(query) ||
19 |           query.state.status === "pending",
20 |       },
21 |       hydrate: {
22 |         deserializeData: SuperJSON.deserialize,
23 |       },
24 |     },
25 |   });
26 | 
--------------------------------------------------------------------------------
/src/trpc/react.tsx:
--------------------------------------------------------------------------------
 1 | "use client";
 2 | 
 3 | import { QueryClientProvider, type QueryClient } from "@tanstack/react-query";
 4 | import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client";
 5 | import { createTRPCReact } from "@trpc/react-query";
 6 | import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server";
 7 | import { useState } from "react";
 8 | import SuperJSON from "superjson";
 9 | 
10 | import { type AppRouter } from "@/server/api/root";
11 | import { createQueryClient } from "./query-client";
12 | 
13 | let clientQueryClientSingleton: QueryClient | undefined = undefined;
14 | const getQueryClient = () => {
15 |   if (typeof window === "undefined") {
16 |     // Server: always make a new query client
17 |     return createQueryClient();
18 |   }
19 |   // Browser: use singleton pattern to keep the same query client
20 |   return (clientQueryClientSingleton ??= createQueryClient());
21 | };
22 | 
23 | export const api = createTRPCReact();
24 | 
25 | /**
26 |  * Inference helper for inputs.
27 |  *
28 |  * @example type HelloInput = RouterInputs['example']['hello']
29 |  */
30 | export type RouterInputs = inferRouterInputs;
31 | 
32 | /**
33 |  * Inference helper for outputs.
34 |  *
35 |  * @example type HelloOutput = RouterOutputs['example']['hello']
36 |  */
37 | export type RouterOutputs = inferRouterOutputs;
38 | 
39 | export function TRPCReactProvider(props: { children: React.ReactNode }) {
40 |   const queryClient = getQueryClient();
41 | 
42 |   const [trpcClient] = useState(() =>
43 |     api.createClient({
44 |       links: [
45 |         loggerLink({
46 |           enabled: (op) =>
47 |             process.env.NODE_ENV === "development" ||
48 |             (op.direction === "down" && op.result instanceof Error),
49 |         }),
50 |         unstable_httpBatchStreamLink({
51 |           transformer: SuperJSON,
52 |           url: getBaseUrl() + "/api/trpc",
53 |           headers: () => {
54 |             const headers = new Headers();
55 |             headers.set("x-trpc-source", "nextjs-react");
56 |             return headers;
57 |           },
58 |         }),
59 |       ],
60 |     })
61 |   );
62 | 
63 |   return (
64 |     
65 |       
66 |         {props.children}
67 |        
68 |      
69 |   );
70 | }
71 | 
72 | function getBaseUrl() {
73 |   if (typeof window !== "undefined") return window.location.origin;
74 |   if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
75 |   return `http://localhost:${process.env.PORT ?? 3000}`;
76 | }
77 | 
--------------------------------------------------------------------------------
/src/trpc/server.ts:
--------------------------------------------------------------------------------
 1 | import "server-only";
 2 | 
 3 | import { createHydrationHelpers } from "@trpc/react-query/rsc";
 4 | import { headers } from "next/headers";
 5 | import { cache } from "react";
 6 | 
 7 | import { createCaller, type AppRouter } from "@/server/api/root";
 8 | import { createTRPCContext } from "@/server/api/trpc";
 9 | import { createQueryClient } from "./query-client";
10 | 
11 | /**
12 |  * This wraps the `createTRPCContext` helper and provides the required context for the tRPC API when
13 |  * handling a tRPC call from a React Server Component.
14 |  */
15 | const createContext = cache(async () => {
16 |   const heads = new Headers(await headers());
17 |   heads.set("x-trpc-source", "rsc");
18 | 
19 |   return createTRPCContext({
20 |     headers: heads,
21 |   });
22 | });
23 | 
24 | const getQueryClient = cache(createQueryClient);
25 | const caller = createCaller(createContext);
26 | 
27 | export const { trpc: api, HydrateClient } = createHydrationHelpers(
28 |   caller,
29 |   getQueryClient
30 | );
31 | 
--------------------------------------------------------------------------------
/src/types/pricing.ts:
--------------------------------------------------------------------------------
1 | 
2 | export interface PricingPlan {
3 |     title: string;
4 |     description: string;
5 |     price: { monthly: string; yearly: string };
6 |     popular: boolean;
7 |     features: string[];
8 |   }
--------------------------------------------------------------------------------
/start-database.sh:
--------------------------------------------------------------------------------
 1 | #!/usr/bin/env bash
 2 | # Use this script to start a docker container for a local development database
 3 | 
 4 | # TO RUN ON WINDOWS:
 5 | # 1. Install WSL (Windows Subsystem for Linux) - https://learn.microsoft.com/en-us/windows/wsl/install
 6 | # 2. Install Docker Desktop for Windows - https://docs.docker.com/docker-for-windows/install/
 7 | # 3. Open WSL - `wsl`
 8 | # 4. Run this script - `./start-database.sh`
 9 | 
10 | # On Linux and macOS you can run this script directly - `./start-database.sh`
11 | 
12 | DB_CONTAINER_NAME="saas-boilerplate-postgres"
13 | 
14 | if ! [ -x "$(command -v docker)" ]; then
15 |   echo -e "Docker is not installed. Please install docker and try again.\nDocker install guide: https://docs.docker.com/engine/install/"
16 |   exit 1
17 | fi
18 | 
19 | if ! docker info > /dev/null 2>&1; then
20 |   echo "Docker daemon is not running. Please start Docker and try again."
21 |   exit 1
22 | fi
23 | 
24 | if [ "$(docker ps -q -f name=$DB_CONTAINER_NAME)" ]; then
25 |   echo "Database container '$DB_CONTAINER_NAME' already running"
26 |   exit 0
27 | fi
28 | 
29 | if [ "$(docker ps -q -a -f name=$DB_CONTAINER_NAME)" ]; then
30 |   docker start "$DB_CONTAINER_NAME"
31 |   echo "Existing database container '$DB_CONTAINER_NAME' started"
32 |   exit 0
33 | fi
34 | 
35 | # import env variables from .env
36 | set -a
37 | source .env
38 | 
39 | DB_PASSWORD=$(echo "$DATABASE_URL" | awk -F':' '{print $3}' | awk -F'@' '{print $1}')
40 | DB_PORT=$(echo "$DATABASE_URL" | awk -F':' '{print $4}' | awk -F'\/' '{print $1}')
41 | 
42 | if [ "$DB_PASSWORD" = "password" ]; then
43 |   echo "You are using the default database password"
44 |   read -p "Should we generate a random password for you? [y/N]: " -r REPLY
45 |   if ! [[ $REPLY =~ ^[Yy]$ ]]; then
46 |     echo "Please change the default password in the .env file and try again"
47 |     exit 1
48 |   fi
49 |   # Generate a random URL-safe password
50 |   DB_PASSWORD=$(openssl rand -base64 12 | tr '+/' '-_')
51 |   sed -i -e "s#:password@#:$DB_PASSWORD@#" .env
52 | fi
53 | 
54 | docker run -d \
55 |   --name $DB_CONTAINER_NAME \
56 |   -e POSTGRES_USER="postgres" \
57 |   -e POSTGRES_PASSWORD="$DB_PASSWORD" \
58 |   -e POSTGRES_DB=saas-boilerplate \
59 |   -p "$DB_PORT":5432 \
60 |   docker.io/postgres && echo "Database container '$DB_CONTAINER_NAME' was successfully created"
61 | 
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
  1 | import { type Config } from "tailwindcss";
  2 | import { fontFamily } from "tailwindcss/defaultTheme";
  3 | 
  4 | export default {
  5 |     darkMode: ["class"],
  6 |     content: ["./src/**/*.tsx"],
  7 |   theme: {
  8 |   	container: {
  9 |   		center: true,
 10 |   		padding: '2rem',
 11 |   		screens: {
 12 |   			'2xl': '1280px'
 13 |   		}
 14 |   	},
 15 |   	extend: {
 16 |   		fontFamily: {
 17 |   			sans: [
 18 |   				'var(--font-geist-sans)',
 19 |                     ...fontFamily.sans
 20 |                 ]
 21 |   		},
 22 |   		borderRadius: {
 23 |   			lg: 'var(--radius)',
 24 |   			md: 'calc(var(--radius) - 2px)',
 25 |   			sm: 'calc(var(--radius) - 4px)'
 26 |   		},
 27 |   		colors: {
 28 |   			background: 'hsl(var(--background))',
 29 |   			foreground: 'hsl(var(--foreground))',
 30 |   			card: {
 31 |   				DEFAULT: 'hsl(var(--card))',
 32 |   				foreground: 'hsl(var(--card-foreground))'
 33 |   			},
 34 |   			popover: {
 35 |   				DEFAULT: 'hsl(var(--popover))',
 36 |   				foreground: 'hsl(var(--popover-foreground))'
 37 |   			},
 38 |   			primary: {
 39 |   				DEFAULT: 'hsl(var(--primary))',
 40 |   				foreground: 'hsl(var(--primary-foreground))'
 41 |   			},
 42 |   			secondary: {
 43 |   				DEFAULT: 'hsl(var(--secondary))',
 44 |   				foreground: 'hsl(var(--secondary-foreground))'
 45 |   			},
 46 |   			muted: {
 47 |   				DEFAULT: 'hsl(var(--muted))',
 48 |   				foreground: 'hsl(var(--muted-foreground))'
 49 |   			},
 50 |   			accent: {
 51 |   				DEFAULT: 'hsl(var(--accent))',
 52 |   				foreground: 'hsl(var(--accent-foreground))'
 53 |   			},
 54 |   			destructive: {
 55 |   				DEFAULT: 'hsl(var(--destructive))',
 56 |   				foreground: 'hsl(var(--destructive-foreground))'
 57 |   			},
 58 |   			border: 'hsl(var(--border))',
 59 |   			input: 'hsl(var(--input))',
 60 |   			ring: 'hsl(var(--ring))',
 61 |   			chart: {
 62 |   				'1': 'hsl(var(--chart-1))',
 63 |   				'2': 'hsl(var(--chart-2))',
 64 |   				'3': 'hsl(var(--chart-3))',
 65 |   				'4': 'hsl(var(--chart-4))',
 66 |   				'5': 'hsl(var(--chart-5))'
 67 |   			},
 68 |   			'color-1': 'hsl(var(--color-1))',
 69 |   			'color-2': 'hsl(var(--color-2))',
 70 |   			'color-3': 'hsl(var(--color-3))',
 71 |   			'color-4': 'hsl(var(--color-4))',
 72 |   			'color-5': 'hsl(var(--color-5))',
 73 |   			sidebar: {
 74 |   				DEFAULT: 'hsl(var(--sidebar-background))',
 75 |   				foreground: 'hsl(var(--sidebar-foreground))',
 76 |   				primary: 'hsl(var(--sidebar-primary))',
 77 |   				'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
 78 |   				accent: 'hsl(var(--sidebar-accent))',
 79 |   				'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
 80 |   				border: 'hsl(var(--sidebar-border))',
 81 |   				ring: 'hsl(var(--sidebar-ring))'
 82 |   			}
 83 |   		},
 84 |   		animation: {
 85 |   			rainbow: 'rainbow var(--speed, 2s) infinite linear',
 86 |   			'shiny-text': 'shiny-text 8s infinite',
 87 |   			marquee: 'marquee var(--duration) infinite linear',
 88 |   			'marquee-vertical': 'marquee-vertical var(--duration) linear infinite'
 89 |   		},
 90 |   		keyframes: {
 91 |   			rainbow: {
 92 |   				'0%': {
 93 |   					'background-position': '0%'
 94 |   				},
 95 |   				'100%': {
 96 |   					'background-position': '200%'
 97 |   				}
 98 |   			},
 99 |   			'shiny-text': {
100 |   				'0%, 90%, 100%': {
101 |   					'background-position': 'calc(-100% - var(--shiny-width)) 0'
102 |   				},
103 |   				'30%, 60%': {
104 |   					'background-position': 'calc(100% + var(--shiny-width)) 0'
105 |   				}
106 |   			},
107 |   			marquee: {
108 |   				from: {
109 |   					transform: 'translateX(0)'
110 |   				},
111 |   				to: {
112 |   					transform: 'translateX(calc(-100% - var(--gap)))'
113 |   				}
114 |   			},
115 |   			'marquee-vertical': {
116 |   				from: {
117 |   					transform: 'translateY(0)'
118 |   				},
119 |   				to: {
120 |   					transform: 'translateY(calc(-100% - var(--gap)))'
121 |   				}
122 |   			}
123 |   		}
124 |   	}
125 |   },
126 |   plugins: [require("tailwindcss-animate")],
127 | } satisfies Config;
128 | 
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     /* Base Options: */
 4 |     "esModuleInterop": true,
 5 |     "skipLibCheck": true,
 6 |     "target": "es2022",
 7 |     "allowJs": true,
 8 |     "resolveJsonModule": true,
 9 |     "moduleDetection": "force",
10 |     "isolatedModules": true,
11 | 
12 |     /* Strictness */
13 |     "strict": true,
14 |     "noUncheckedIndexedAccess": true,
15 |     "checkJs": true,
16 | 
17 |     /* Bundled projects */
18 |     "lib": ["dom", "dom.iterable", "ES2022"],
19 |     "noEmit": true,
20 |     "module": "ESNext",
21 |     "moduleResolution": "Bundler",
22 |     "jsx": "preserve",
23 |     "plugins": [{ "name": "next" }],
24 |     "incremental": true,
25 | 
26 |     /* Path Aliases */
27 |     "baseUrl": ".",
28 |     "paths": {
29 |       "@/*": ["./src/*"]
30 |     }
31 |   },
32 |   "include": [
33 |     ".eslintrc.cjs",
34 |     "next-env.d.ts",
35 |     "**/*.ts",
36 |     "**/*.tsx",
37 |     "**/*.cjs",
38 |     "**/*.js",
39 |     ".next/types/**/*.ts"
40 |   ],
41 |   "exclude": ["node_modules"]
42 | }
43 | 
--------------------------------------------------------------------------------