├── .github
└── workflows
│ ├── deploy_backend.yml
│ └── deploy_frontend.yml
├── .gitignore
├── .npmrc
├── README.md
├── apps
├── backend
│ ├── .env.example
│ ├── .gitignore
│ ├── README.md
│ ├── index.ts
│ ├── middleware.ts
│ ├── models
│ │ ├── BaseModel.ts
│ │ └── FalAIModel.ts
│ ├── package.json
│ ├── routes
│ │ ├── payment.routes.ts
│ │ └── webhook.routes.ts
│ ├── services
│ │ └── payment.ts
│ ├── tsconfig.json
│ └── types.d.ts
└── web
│ ├── .gitignore
│ ├── README.md
│ ├── app
│ ├── config.ts
│ ├── dashboard
│ │ └── page.tsx
│ ├── favicon.ico
│ ├── fonts
│ │ ├── GeistMonoVF.woff
│ │ └── GeistVF.woff
│ ├── globals.css
│ ├── layout.tsx
│ ├── page.module.css
│ ├── page.tsx
│ ├── payment
│ │ ├── cancel
│ │ │ └── page.tsx
│ │ ├── success
│ │ │ └── page.tsx
│ │ └── verify
│ │ │ └── page.tsx
│ ├── pricing
│ │ └── page.tsx
│ ├── purchases
│ │ └── page.tsx
│ └── train
│ │ └── page.tsx
│ ├── components.json
│ ├── components
│ ├── Appbar.tsx
│ ├── Camera.tsx
│ ├── Footer.tsx
│ ├── GenerateImage.tsx
│ ├── GlowEffect.tsx
│ ├── ImageCard.tsx
│ ├── Models.tsx
│ ├── PackCard.tsx
│ ├── Packs.tsx
│ ├── PacksClient.tsx
│ ├── ThemeToggle.tsx
│ ├── Train.tsx
│ ├── home
│ │ ├── BackgroundEffects.tsx
│ │ ├── Features.tsx
│ │ ├── Hero.tsx
│ │ ├── HeroHeader.tsx
│ │ ├── HowItWorks.tsx
│ │ ├── ImageCarousel.tsx
│ │ ├── PricingSection.tsx
│ │ ├── ScrollIndicator.tsx
│ │ ├── StatsSection.tsx
│ │ ├── Testimonials.tsx
│ │ ├── TrustedBy.tsx
│ │ └── data.ts
│ ├── navbar
│ │ └── Credits.tsx
│ ├── payment
│ │ ├── PaymentCancelContent.tsx
│ │ ├── PaymentSuccessContent.tsx
│ │ ├── PurchasesPage.tsx
│ │ └── VerifyContent.tsx
│ ├── providers
│ │ └── Providers.tsx
│ ├── subscription
│ │ └── PlanCard.tsx
│ ├── theme-provider.tsx
│ └── ui
│ │ ├── badge.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── carousel.tsx
│ │ ├── customLabel.tsx
│ │ ├── dialog.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── progress.tsx
│ │ ├── scroll-area.tsx
│ │ ├── select.tsx
│ │ ├── sheet.tsx
│ │ ├── skeleton.tsx
│ │ ├── switch.tsx
│ │ ├── tabs.tsx
│ │ ├── textarea.tsx
│ │ ├── toast.tsx
│ │ ├── toaster.tsx
│ │ ├── tooltip.tsx
│ │ └── upload.tsx
│ ├── eslint.config.js
│ ├── hooks
│ ├── use-credits.ts
│ ├── use-toast.ts
│ ├── useAuth.ts
│ ├── usePayment.ts
│ └── useTransactions.ts
│ ├── lib
│ └── utils.ts
│ ├── middleware.ts
│ ├── next.config.js
│ ├── package.json
│ ├── postcss.config.mjs
│ ├── public
│ ├── file-text.svg
│ ├── globe.svg
│ ├── next.svg
│ ├── turborepo-dark.svg
│ ├── turborepo-light.svg
│ ├── vercel.svg
│ └── window.svg
│ ├── tsconfig.json
│ └── types
│ └── index.ts
├── bun.lockb
├── docker-compose.yml
├── docker
├── Dockerfile.backend
└── Dockerfile.frontend
├── package-lock.json
├── package.json
├── packages
├── common
│ ├── .gitignore
│ ├── README.md
│ ├── index.ts
│ ├── inferred-types.ts
│ ├── package.json
│ ├── tsconfig.json
│ └── types.ts
├── db
│ ├── .gitignore
│ ├── README.md
│ ├── index.ts
│ ├── package.json
│ ├── prisma
│ │ ├── migrations
│ │ │ ├── 20250211165544_init
│ │ │ │ └── migration.sql
│ │ │ ├── 20250211172909_
│ │ │ │ └── migration.sql
│ │ │ ├── 20250211173124_change_color_enunm
│ │ │ │ └── migration.sql
│ │ │ ├── 20250211173427_added_user_id_field
│ │ │ │ └── migration.sql
│ │ │ ├── 20250211173843_added_status
│ │ │ │ └── migration.sql
│ │ │ ├── 20250211205852_added_fal_ai
│ │ │ │ └── migration.sql
│ │ │ ├── 20250211210635_added_index
│ │ │ │ └── migration.sql
│ │ │ ├── 20250211214338_init_db
│ │ │ │ └── migration.sql
│ │ │ ├── 20250212020444_
│ │ │ │ └── migration.sql
│ │ │ ├── 20250212020828_added_image
│ │ │ │ └── migration.sql
│ │ │ ├── 20250212025528_thumbnail
│ │ │ │ └── migration.sql
│ │ │ ├── 20250212031625_added_open_models
│ │ │ │ └── migration.sql
│ │ │ ├── 20250213231325_subscription
│ │ │ │ └── migration.sql
│ │ │ ├── 20250213231834_usercredit
│ │ │ │ └── migration.sql
│ │ │ ├── 20250216194913_user_table_updated
│ │ │ │ └── migration.sql
│ │ │ ├── 20250216195632_user_table_updated
│ │ │ │ └── migration.sql
│ │ │ ├── 20250224171635_transaction
│ │ │ │ └── migration.sql
│ │ │ ├── 20250226214307_removed_annual
│ │ │ │ └── migration.sql
│ │ │ └── migration_lock.toml
│ │ └── schema.prisma
│ └── tsconfig.json
├── eslint-config
│ ├── README.md
│ ├── base.js
│ ├── next.js
│ ├── package.json
│ └── react-internal.js
├── typescript-config
│ ├── base.json
│ ├── nextjs.json
│ ├── package.json
│ └── react-library.json
└── ui
│ ├── eslint.config.mjs
│ ├── package.json
│ ├── src
│ ├── button.tsx
│ ├── card.tsx
│ └── code.tsx
│ ├── tsconfig.json
│ └── turbo
│ └── generators
│ ├── config.ts
│ └── templates
│ └── component.hbs
└── turbo.json
/.github/workflows/deploy_backend.yml:
--------------------------------------------------------------------------------
1 | name: Continuous Deployment (Prod) (Backend)
2 | on:
3 | push:
4 | branches: [ main ]
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v2
10 | with:
11 | fetch-depth: 0
12 |
13 | - name: Docker login
14 | uses: docker/login-action@v2
15 | with:
16 | username: ${{ secrets.DOCKERHUB_USERNAME }}
17 | password: ${{ secrets.DOCKERHUB_TOKEN }}
18 |
19 | - name: Set up Docker Buildx
20 | uses: docker/setup-buildx-action@v2
21 |
22 | - name: Build and push
23 | uses: docker/build-push-action@v4
24 | with:
25 | context: .
26 | file: ./docker/Dockerfile.backend
27 | push: true
28 | tags: 100xdevs/photo-ai-backend:${{ github.sha }}
29 |
30 | - name: Clone staging-ops repo, update, and push
31 | env:
32 | PAT: ${{ secrets.PAT }}
33 | run: |
34 | git clone https://github.com/code100x/staging-ops.git
35 | cd staging-ops
36 | sed -i 's|image: 100xdevs/photo-ai-backend:.*|image: 100xdevs/photo-ai-backend:${{ github.sha }}|' prod/photo-ai/deployment.yml
37 | git config user.name "GitHub Actions Bot"
38 | git config user.email "actions@github.com"
39 | git add prod/photo-ai/deployment.yml
40 | git commit -m "Update dailycode image to ${{ github.sha }}"
41 | git push https://${PAT}@github.com/code100x/staging-ops.git main
--------------------------------------------------------------------------------
/.github/workflows/deploy_frontend.yml:
--------------------------------------------------------------------------------
1 | name: Continuous Deployment (Prod) (Frontend)
2 | on:
3 | push:
4 | branches: [ main ]
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v2
10 | with:
11 | fetch-depth: 0
12 |
13 | - name: Docker login
14 | uses: docker/login-action@v2
15 | with:
16 | username: ${{ secrets.DOCKERHUB_USERNAME }}
17 | password: ${{ secrets.DOCKERHUB_TOKEN }}
18 |
19 | - name: Set up Docker Buildx
20 | uses: docker/setup-buildx-action@v2
21 |
22 | - name: Build and push
23 | uses: docker/build-push-action@v4
24 | with:
25 | context: .
26 | file: ./docker/Dockerfile.frontend
27 | push: true
28 | tags: 100xdevs/photo-ai-frontend:${{ github.sha }}
29 | build-args: |
30 | CLERK_PUBLISHABLE_KEY=${{ secrets.CLERK_PUBLISHABLE_KEY }}
31 |
32 | - name: Clone staging-ops repo, update, and push
33 | env:
34 | PAT: ${{ secrets.PAT }}
35 | run: |
36 | git clone https://github.com/code100x/staging-ops.git
37 | cd staging-ops
38 | sed -i 's|image: 100xdevs/photo-ai-frontend:.*|image: 100xdevs/photo-ai-frontend:${{ github.sha }}|' prod/photo-ai/deployment.yml
39 | git config user.name "GitHub Actions Bot"
40 | git config user.email "actions@github.com"
41 | git add prod/photo-ai/deployment.yml
42 | git commit -m "Update dailycode image to ${{ github.sha }}"
43 | git push https://${PAT}@github.com/code100x/staging-ops.git main
--------------------------------------------------------------------------------
/.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 | # Local env files
9 | .env
10 | .env.local
11 | .env.development.local
12 | .env.test.local
13 | .env.production.local
14 |
15 | # Testing
16 | coverage
17 |
18 | # Turbo
19 | .turbo
20 |
21 | # Vercel
22 | .vercel
23 |
24 | # Build Outputs
25 | .next/
26 | out/
27 | build
28 | dist
29 |
30 |
31 | # Debug
32 | npm-debug.log*
33 | yarn-debug.log*
34 | yarn-error.log*
35 |
36 | # Misc
37 | .DS_Store
38 | *.pem
39 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/photo-ai/752707e61d8ad2e1abd0189ebc30668232c82a1d/.npmrc
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 100xPhoto - AI Image Generation Platform
2 |
3 | 100xPhoto is a powerful AI image platform that lets you generate stunning images and train custom AI models. Built with cutting-edge technology, it enables users to create unique AI-generated artwork and train personalized models on their own image datasets. Whether you're an artist looking to expand your creative possibilities or a developer building AI-powered image applications, 100xPhoto provides an intuitive interface and robust capabilities for AI image generation and model training.
4 |
5 | ## Tech Stack
6 |
7 | - **Frontend**: Next.js 14 (App Router), TypeScript, Tailwind CSS, Shadcn/UI
8 | - **Backend**: Node.js with TypeScript
9 | - **Authentication**: Clerk
10 | - **Containerization**: Docker
11 | - **Package Management**: Bun
12 | - **Monorepo Management**: Turborepo
13 |
14 | ## Project Structure
15 |
16 | ### Apps and Packages
17 |
18 | - `web`: Next.js frontend application
19 | - `backend`: Node.js backend service
20 | - `@repo/ui`: Shared React component library
21 | - `@repo/typescript-config`: Shared TypeScript configurations
22 | - `@repo/eslint-config`: Shared ESLint configurations
23 |
24 | ## Getting Started
25 |
26 | ### Prerequisites
27 |
28 | - Docker
29 | - Bun (for local development)
30 | - Clerk Account (for authentication)
31 |
32 | ### Environment Setup
33 |
34 | 1. Create `.env` files:
35 |
36 | ```bash
37 | # apps/web/.env.local
38 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_publishable_key
39 | CLERK_SECRET_KEY=your_secret_key
40 | NEXT_PUBLIC_BACKEND_URL=http://localhost:8080
41 | ```
42 |
43 |
44 | ### Local Development
45 |
46 | ```bash
47 | # Install dependencies
48 | bun install
49 |
50 | # Run development servers
51 | bun run dev
52 |
53 | # Build all packages
54 | bun run build
55 | ```
56 |
57 | ## Features
58 |
59 | - AI-powered image generation
60 | - User authentication and authorization
61 | - Image gallery with preview
62 | - Download generated images
63 | - Responsive design
64 |
65 | ## Development Commands
66 |
67 | ```bash
68 | # Run frontend only
69 | bun run start:web
70 |
71 | # Run backend only
72 | bun run start:backend
73 |
74 | # Run both frontend and backend
75 | bun run dev
76 | ```
77 |
78 | ## Docker Setup
79 |
80 | ### Environment Variables Required
81 |
82 | ```bash
83 | # Frontend Environment Variables
84 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_Y2xlcmsuMTAweGRldnMuY29tJA
85 | NEXT_PUBLIC_BACKEND_URL=https://api.photoaiv2.100xdevs.com
86 | NEXT_PUBLIC_STRIPE_KEY=pk_test_51QsCmFEI53oUr5PHZw5ErO4Xy2lNh9LkH9vXDb8wc7BOvfSPc0i4xt6I5Qy3jaBLnvg9wPenPoeW0LvQ1x3GtfUm00eNFHdBDd
87 | CLERK_SECRET_KEY=your_clerk_secret_key
88 |
89 | # Backend Environment Variables
90 | DATABASE_URL=your_database_url
91 | ```
92 |
93 | ### Docker Commands
94 |
95 | ```bash
96 | # Navigate to docker directory
97 | cd docker
98 |
99 | # Build images
100 | docker build -f Dockerfile.frontend -t photoai-frontend ..
101 | docker build -f Dockerfile.backend -t photoai-backend ..
102 |
103 | # Run frontend container
104 | docker run -p 3000:3000 \
105 | -e NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_Y2xlcmsuMTAweGRldnMuY29tJA \
106 | -e NEXT_PUBLIC_BACKEND_URL=https://api.photoaiv2.100xdevs.com \
107 | -e NEXT_PUBLIC_STRIPE_KEY=pk_test_51QsCmFEI53oUr5PHZw5ErO4Xy2lNh9LkH9vXDb8wc7BOvfSPc0i4xt6I5Qy3jaBLnvg9wPenPoeW0LvQ1x3GtfUm00eNFHdBDd \
108 | -e CLERK_SECRET_KEY=your_clerk_secret_key \
109 | photoai-frontend
110 |
111 | # Run backend container
112 | docker run -p 8080:8080 \
113 | -e DATABASE_URL=your_database_url \
114 | photoai-backend
115 |
116 | ```
117 |
118 |
119 | ## Project Structure
120 |
121 | ```
122 | .
123 | ├── apps
124 | │ ├── web/ # Next.js frontend
125 | │ └── backend/ # Node.js backend
126 | ├── packages
127 | │ ├── ui/ # Shared UI components
128 | │ ├── typescript-config/ # Shared TS config
129 | │ └── eslint-config/ # Shared ESLint config
130 | ├── docker/ # Docker configuration
131 | │ ├── Dockerfile.frontend
132 | │ └── Dockerfile.backend
133 | └── package.json
134 | ```
135 |
136 | ## Contributing
137 |
138 | 1. Fork the repository
139 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
140 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
141 | 4. Push to the branch (`git push origin feature/amazing-feature`)
142 | 5. Open a Pull Request
143 |
144 | ## License
145 |
146 | This project is licensed under the MIT License - see the LICENSE file for details
147 |
--------------------------------------------------------------------------------
/apps/backend/.env.example:
--------------------------------------------------------------------------------
1 | FAL_KEY=""
2 | S3_ACCESS_KEY=""
3 | S3_SECRET_KEY=""
4 | BUCKET_NAME=""
5 | ENDPOINT=""
6 | AUTH_JWT_KEY=""
7 | RAZORPAY_KEY_ID=""
8 | RAZORPAY_KEY_SECRET=""
9 | SIGNING_SECRET=""
10 | CLERK_JWT_PUBLIC_KEY=""
11 | SIGNING_SECRET=""
12 | WEBHOOK_BASE_URL=""
13 | FRONTEND_URL=""
--------------------------------------------------------------------------------
/apps/backend/.gitignore:
--------------------------------------------------------------------------------
1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
2 |
3 | # Logs
4 |
5 | logs
6 | _.log
7 | npm-debug.log_
8 | yarn-debug.log*
9 | yarn-error.log*
10 | lerna-debug.log*
11 | .pnpm-debug.log*
12 |
13 | # Caches
14 |
15 | .cache
16 |
17 | # Diagnostic reports (https://nodejs.org/api/report.html)
18 |
19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
20 |
21 | # Runtime data
22 |
23 | pids
24 | _.pid
25 | _.seed
26 | *.pid.lock
27 |
28 | # Directory for instrumented libs generated by jscoverage/JSCover
29 |
30 | lib-cov
31 |
32 | # Coverage directory used by tools like istanbul
33 |
34 | coverage
35 | *.lcov
36 |
37 | # nyc test coverage
38 |
39 | .nyc_output
40 |
41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
42 |
43 | .grunt
44 |
45 | # Bower dependency directory (https://bower.io/)
46 |
47 | bower_components
48 |
49 | # node-waf configuration
50 |
51 | .lock-wscript
52 |
53 | # Compiled binary addons (https://nodejs.org/api/addons.html)
54 |
55 | build/Release
56 |
57 | # Dependency directories
58 |
59 | node_modules/
60 | jspm_packages/
61 |
62 | # Snowpack dependency directory (https://snowpack.dev/)
63 |
64 | web_modules/
65 |
66 | # TypeScript cache
67 |
68 | *.tsbuildinfo
69 |
70 | # Optional npm cache directory
71 |
72 | .npm
73 |
74 | # Optional eslint cache
75 |
76 | .eslintcache
77 |
78 | # Optional stylelint cache
79 |
80 | .stylelintcache
81 |
82 | # Microbundle cache
83 |
84 | .rpt2_cache/
85 | .rts2_cache_cjs/
86 | .rts2_cache_es/
87 | .rts2_cache_umd/
88 |
89 | # Optional REPL history
90 |
91 | .node_repl_history
92 |
93 | # Output of 'npm pack'
94 |
95 | *.tgz
96 |
97 | # Yarn Integrity file
98 |
99 | .yarn-integrity
100 |
101 | # dotenv environment variable files
102 |
103 | .env
104 | .env.development.local
105 | .env.test.local
106 | .env.production.local
107 | .env.local
108 |
109 | # parcel-bundler cache (https://parceljs.org/)
110 |
111 | .parcel-cache
112 |
113 | # Next.js build output
114 |
115 | .next
116 | out
117 |
118 | # Nuxt.js build / generate output
119 |
120 | .nuxt
121 | dist
122 |
123 | # Gatsby files
124 |
125 | # Comment in the public line in if your project uses Gatsby and not Next.js
126 |
127 | # https://nextjs.org/blog/next-9-1#public-directory-support
128 |
129 | # public
130 |
131 | # vuepress build output
132 |
133 | .vuepress/dist
134 |
135 | # vuepress v2.x temp and cache directory
136 |
137 | .temp
138 |
139 | # Docusaurus cache and generated files
140 |
141 | .docusaurus
142 |
143 | # Serverless directories
144 |
145 | .serverless/
146 |
147 | # FuseBox cache
148 |
149 | .fusebox/
150 |
151 | # DynamoDB Local files
152 |
153 | .dynamodb/
154 |
155 | # TernJS port file
156 |
157 | .tern-port
158 |
159 | # Stores VSCode versions used for testing VSCode extensions
160 |
161 | .vscode-test
162 |
163 | # yarn v2
164 |
165 | .yarn/cache
166 | .yarn/unplugged
167 | .yarn/build-state.yml
168 | .yarn/install-state.gz
169 | .pnp.*
170 |
171 | # IntelliJ based IDEs
172 | .idea
173 |
174 | # Finder (MacOS) folder config
175 | .DS_Store
176 |
--------------------------------------------------------------------------------
/apps/backend/README.md:
--------------------------------------------------------------------------------
1 | # PhotoAI Backend Service
2 |
3 | The Node.js backend service for PhotoAI - an AI-powered image generation platform.
4 |
5 | ## Features
6 |
7 | - AI Image Generation with FalAI
8 | - Model Training Integration
9 | - S3 Image Storage
10 | - Payment Processing (Stripe & Razorpay)
11 | - User Credit Management
12 | - Clerk Authentication
13 | - Webhook Handlers
14 |
15 | ## Tech Stack
16 |
17 | - Node.js with TypeScript
18 | - Express.js
19 | - Prisma ORM
20 | - FalAI Client
21 | - S3 Storage
22 | - Stripe/Razorpay Integration
23 | - Clerk Authentication
24 |
25 | ## Environment Variables
26 |
27 | Create a `.env` file:
28 |
29 | ```bash
30 | # AI Service
31 | FAL_KEY=your_fal_ai_key
32 |
33 | # Storage
34 | S3_ACCESS_KEY=your_s3_access_key
35 | S3_SECRET_KEY=your_s3_secret_key
36 | BUCKET_NAME=your_bucket_name
37 | ENDPOINT=your_s3_endpoint
38 |
39 | # Authentication
40 | AUTH_JWT_KEY=your_jwt_key
41 | CLERK_JWT_PUBLIC_KEY=your_clerk_public_key
42 | SIGNING_SECRET=your_clerk_webhook_signing_secret
43 |
44 | # Payments
45 | STRIPE_SECRET_KEY=your_stripe_secret_key
46 | STRIPE_WEBHOOK_SECRET=your_stripe_webhook_secret
47 | RAZORPAY_KEY_ID=your_razorpay_key_id
48 | RAZORPAY_KEY_SECRET=your_razorpay_secret_key
49 |
50 | # URLs
51 | WEBHOOK_BASE_URL=your_webhook_base_url
52 | FRONTEND_URL=your_frontend_url
53 | ```
54 |
55 | ## Development
56 |
57 | ```bash
58 | # Install dependencies
59 | bun install
60 |
61 | # Run development server
62 | bun dev
63 |
64 | # Start production server
65 | bun start
66 | ```
67 |
68 | The server will be available at `http://localhost:8080`.
69 |
70 | ## API Endpoints
71 |
72 | ### Authentication
73 |
74 | - `POST /api/webhook/clerk` - Clerk webhook handler
75 |
76 | ### Image Generation
77 |
78 | - `POST /ai/training` - Train new AI model
79 | - `POST /ai/generate` - Generate images
80 | - `POST /pack/generate` - Generate images from pack
81 | - `GET /image/bulk` - Get generated images
82 |
83 | ### Models
84 |
85 | - `GET /models` - Get available models
86 | - `GET /pre-signed-url` - Get S3 upload URL
87 |
88 | ### Payments
89 |
90 | - `POST /payment/create` - Create payment session
91 | - `POST /payment/razorpay/verify` - Verify Razorpay payment
92 | - `GET /payment/subscription/:userId` - Get user subscription
93 | - `GET /payment/credits/:userId` - Get user credits
94 | - `POST /payment/webhook` - Payment webhook handler
95 |
96 | ## Project Structure
97 |
98 | ```
99 | apps/backend/
100 | ├── routes/ # API route handlers
101 | ├── services/ # Business logic
102 | ├── models/ # AI model integrations
103 | ├── middleware/ # Express middleware
104 | └── types/ # TypeScript types
105 | ```
106 |
107 | ## Key Components
108 |
109 | - `index.ts` - Main application entry
110 | - `middleware.ts` - Authentication middleware
111 | - `models/FalAIModel.ts` - FalAI integration
112 | - `services/payment.ts` - Payment processing
113 | - `routes/payment.routes.ts` - Payment endpoints
114 | - `routes/webhook.routes.ts` - Webhook handlers
115 |
116 | ## Docker Support
117 |
118 | Build the backend container:
119 |
120 | ```bash
121 | docker build -f docker/Dockerfile.backend -t photoai-backend ..
122 | ```
123 |
124 | Run the container:
125 |
126 | ```bash
127 | docker run -p 8080:8080 \
128 | --env-file .env \
129 | photoai-backend
130 | ```
131 |
132 | ## Contributing
133 |
134 | 1. Fork the repository
135 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
136 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
137 | 4. Push to the branch (`git push origin feature/amazing-feature`)
138 | 5. Open a Pull Request
139 |
140 | ## License
141 |
142 | This project is licensed under the MIT License - see the LICENSE file for details.
143 |
--------------------------------------------------------------------------------
/apps/backend/middleware.ts:
--------------------------------------------------------------------------------
1 | import type { NextFunction, Request, Response } from "express";
2 | import jwt from "jsonwebtoken";
3 | import { clerkClient } from "@clerk/clerk-sdk-node";
4 |
5 | declare global {
6 | namespace Express {
7 | interface Request {
8 | userId?: string;
9 | user?: {
10 | email: string;
11 | };
12 | }
13 | }
14 | }
15 |
16 | export async function authMiddleware(
17 | req: Request,
18 | res: Response,
19 | next: NextFunction
20 | ) {
21 | try {
22 | const authHeader = req.headers["authorization"];
23 | const token = authHeader?.split(" ")[1];
24 |
25 | if (!token) {
26 | res.status(401).json({ message: "No token provided" });
27 | return;
28 | }
29 |
30 | // Debug logs
31 | console.log("Received token:", token);
32 |
33 | // Get the JWT verification key from environment variable
34 | const publicKey = process.env.CLERK_JWT_PUBLIC_KEY!;
35 |
36 | if (!publicKey) {
37 | console.error("Missing CLERK_JWT_PUBLIC_KEY in environment variables");
38 | res.status(500).json({ message: "Server configuration error" });
39 | return;
40 | }
41 |
42 | // Format the public key properly
43 | const formattedKey = publicKey.replace(/\\n/g, "\n");
44 |
45 | const decoded = jwt.verify(token, formattedKey, {
46 | algorithms: ["RS256"],
47 | issuer:
48 | process.env.CLERK_ISSUER || "https://clerk.100xdevs.com",
49 | complete: true,
50 | });
51 |
52 | console.log("Decoded token:", decoded);
53 |
54 | // Extract user ID from the decoded token
55 | const userId = (decoded as any).payload.sub;
56 |
57 | if (!userId) {
58 | console.error("No user ID in token payload");
59 | res.status(403).json({ message: "Invalid token payload" });
60 | return;
61 | }
62 |
63 | // Fetch user details from Clerk
64 | const user = await clerkClient.users.getUser(userId);
65 | const primaryEmail = user.emailAddresses.find(
66 | (email) => email.id === user.primaryEmailAddressId
67 | );
68 |
69 | if (!primaryEmail) {
70 | console.error("No email found for user");
71 | res.status(400).json({ message: "User email not found" });
72 | return;
73 | }
74 |
75 | // Attach the user ID and email to the request
76 | req.userId = userId;
77 | req.user = {
78 | email: primaryEmail.emailAddress,
79 | };
80 |
81 | next();
82 | } catch (error) {
83 | console.error("Auth error:", error);
84 | if (error instanceof jwt.JsonWebTokenError) {
85 | res.status(403).json({
86 | message: "Invalid token",
87 | details:
88 | process.env.NODE_ENV === "development" ? error.message : undefined,
89 | });
90 | return;
91 | }
92 | res.status(500).json({
93 | message: "Error processing authentication",
94 | details:
95 | process.env.NODE_ENV === "development"
96 | ? (error as Error).message
97 | : undefined,
98 | });
99 | return;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/apps/backend/models/BaseModel.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 | export class BaseModel {
4 | constructor() {}
5 |
6 | private async generateImage(prompt: string, tensorPath: string) {
7 | }
8 |
9 | private async trainModel(inputImages: string[], triggerWord: string) {
10 | }
11 |
12 | }
--------------------------------------------------------------------------------
/apps/backend/models/FalAIModel.ts:
--------------------------------------------------------------------------------
1 | import { fal } from "@fal-ai/client";
2 | import { BaseModel } from "./BaseModel";
3 |
4 | export class FalAIModel {
5 | constructor() {}
6 |
7 | public async generateImage(prompt: string, tensorPath: string) {
8 | const { request_id, response_url } = await fal.queue.submit(
9 | "fal-ai/flux-lora",
10 | {
11 | input: {
12 | prompt: prompt,
13 | loras: [{ path: tensorPath, scale: 1 }],
14 | },
15 | webhookUrl: `${process.env.WEBHOOK_BASE_URL}/fal-ai/webhook/image`,
16 | }
17 | );
18 |
19 | return { request_id, response_url };
20 | }
21 |
22 | public async trainModel(zipUrl: string, triggerWord: string) {
23 | console.log("Training model with URL:", zipUrl);
24 |
25 | try {
26 | const response = await fetch(zipUrl, { method: "HEAD" });
27 | if (!response.ok) {
28 | console.error(
29 | `ZIP URL not accessible: ${zipUrl}, status: ${response.status}`
30 | );
31 | throw new Error(`ZIP URL not accessible: ${response.status}`);
32 | }
33 | } catch (error) {
34 | console.error("Error checking ZIP URL:", error);
35 | throw new Error(`ZIP URL validation failed: ${error as any}.message}`);
36 | }
37 |
38 | const { request_id, response_url } = await fal.queue.submit(
39 | "fal-ai/flux-lora-fast-training",
40 | {
41 | input: {
42 | images_data_url: zipUrl,
43 | trigger_word: triggerWord,
44 | },
45 | webhookUrl: `${process.env.WEBHOOK_BASE_URL}/fal-ai/webhook/train`,
46 | }
47 | );
48 |
49 | console.log("Model training submitted:", request_id);
50 | return { request_id, response_url };
51 | }
52 |
53 | public async generateImageSync(tensorPath: string) {
54 | const response = await fal.subscribe("fal-ai/flux-lora", {
55 | input: {
56 | prompt:
57 | "Generate a head shot for this user in front of a white background",
58 | loras: [{ path: tensorPath, scale: 1 }],
59 | },
60 | });
61 | return {
62 | imageUrl: response.data.images[0].url,
63 | };
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/apps/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "backend",
3 | "module": "index.ts",
4 | "type": "module",
5 | "scripts": {
6 | "start": "bun run index.ts",
7 | "dev": "bun run index.ts"
8 | },
9 | "devDependencies": {
10 | "@types/bun": "latest"
11 | },
12 | "peerDependencies": {
13 | "typescript": "^5.0.0"
14 | },
15 | "dependencies": {
16 | "@clerk/clerk-sdk-node": "^5.1.6",
17 | "@clerk/nextjs": "^6.11.2",
18 | "@fal-ai/client": "^1.2.3",
19 | "@neondatabase/serverless": "^0.10.4",
20 | "@types/cors": "^2.8.17",
21 | "@types/express": "^5.0.0",
22 | "common": "*",
23 | "cors": "^2.8.5",
24 | "db": "*",
25 | "express": "^4.21.2",
26 | "jsonwebtoken": "^9.0.2",
27 | "razorpay": "^2.9.5",
28 | "stripe": "^17.6.0",
29 | "svix": "^1.57.0"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/apps/backend/routes/webhook.routes.ts:
--------------------------------------------------------------------------------
1 | import { prismaClient } from "db";
2 | import { Router } from "express";
3 | import { Webhook } from "svix";
4 |
5 | export const router = Router();
6 |
7 | /**
8 | * POST api/webhook/clerk
9 | * Clerk webhook endpoint
10 | */
11 | router.post("/clerk", async (req, res) => {
12 | const SIGNING_SECRET = process.env.SIGNING_SECRET;
13 |
14 | if (!SIGNING_SECRET) {
15 | throw new Error(
16 | "Error: Please add SIGNING_SECRET from Clerk Dashboard to .env"
17 | );
18 | }
19 |
20 | const wh = new Webhook(SIGNING_SECRET);
21 | const headers = req.headers;
22 | const payload = req.body;
23 |
24 | const svix_id = headers["svix-id"];
25 | const svix_timestamp = headers["svix-timestamp"];
26 | const svix_signature = headers["svix-signature"];
27 |
28 | if (!svix_id || !svix_timestamp || !svix_signature) {
29 | res.status(400).json({
30 | success: false,
31 | message: "Error: Missing svix headers",
32 | });
33 | return;
34 | }
35 |
36 | let evt: any;
37 |
38 | try {
39 | evt = wh.verify(JSON.stringify(payload), {
40 | "svix-id": svix_id as string,
41 | "svix-timestamp": svix_timestamp as string,
42 | "svix-signature": svix_signature as string,
43 | });
44 | } catch (err) {
45 | console.log("Error: Could not verify webhook:", (err as Error).message);
46 | res.status(400).json({
47 | success: false,
48 | message: (err as Error).message,
49 | });
50 | return;
51 | }
52 |
53 | const { id } = evt.data;
54 | const eventType = evt.type;
55 |
56 | try {
57 | switch (eventType) {
58 | case "user.created":
59 | case "user.updated": {
60 | await prismaClient.user.upsert({
61 | where: { clerkId: id },
62 | update: {
63 | name: `${evt.data.first_name ?? ""} ${evt.data.last_name ?? ""}`.trim(),
64 | email: evt.data.email_addresses[0].email_address,
65 | profilePicture: evt.data.profile_image_url,
66 | },
67 | create: {
68 | clerkId: id,
69 | name: `${evt.data.first_name ?? ""} ${evt.data.last_name ?? ""}`.trim(),
70 | email: evt.data.email_addresses[0].email_address,
71 | profilePicture: evt.data.profile_image_url,
72 | },
73 | });
74 | break;
75 | }
76 |
77 | case "user.deleted": {
78 | await prismaClient.user.delete({
79 | where: { clerkId: id },
80 | });
81 | break;
82 | }
83 |
84 | default:
85 | console.log(`Unhandled event type: ${eventType}`);
86 | break;
87 | }
88 | } catch (error) {
89 | console.error("Error handling webhook:", error);
90 | res.status(500).json({ success: false, message: "Internal Server Error" });
91 | return;
92 | }
93 |
94 | res.status(200).json({ success: true, message: "Webhook received" });
95 | return;
96 | });
97 |
--------------------------------------------------------------------------------
/apps/backend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // Enable latest features
4 | "lib": ["ESNext", "DOM"],
5 | "target": "ESNext",
6 | "module": "ESNext",
7 | "moduleDetection": "force",
8 | "jsx": "react-jsx",
9 | "allowJs": true,
10 |
11 | // Bundler mode
12 | "moduleResolution": "bundler",
13 | "allowImportingTsExtensions": true,
14 | "verbatimModuleSyntax": true,
15 | "noEmit": true,
16 |
17 | // Best practices
18 | "strict": true,
19 | "skipLibCheck": true,
20 | "noFallthroughCasesInSwitch": true,
21 |
22 | // Some stricter flags (disabled by default)
23 | "noUnusedLocals": false,
24 | "noUnusedParameters": false,
25 | "noPropertyAccessFromIndexSignature": false
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/apps/backend/types.d.ts:
--------------------------------------------------------------------------------
1 |
2 | declare namespace Express {
3 | interface Request {
4 | userId?: string;
5 | }
6 | }
--------------------------------------------------------------------------------
/apps/web/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # env files (can opt-in for commiting if needed)
29 | .env*
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
38 | # clerk configuration (can include secrets)
39 | /.clerk/
40 |
--------------------------------------------------------------------------------
/apps/web/README.md:
--------------------------------------------------------------------------------
1 | # PhotoAI Web Frontend
2 |
3 | The Next.js frontend application for PhotoAI - an AI-powered image generation platform.
4 |
5 | ## Features
6 |
7 | - AI Image Generation
8 | - Real-time Image Preview
9 | - Beautiful Image Gallery
10 | - Responsive Design
11 | - Authentication with Clerk
12 | - Secure Payment Integration with Stripe
13 |
14 | ## Tech Stack
15 |
16 | - Next.js 14 (App Router)
17 | - TypeScript
18 | - Tailwind CSS
19 | - Shadcn/UI
20 | - Clerk Authentication
21 | - Stripe/Razorpay Payments
22 |
23 | ## Getting Started
24 |
25 | ### Prerequisites
26 |
27 | - Node.js 18+ or Bun
28 | - Clerk Account
29 | - Stripe Account (for payments)
30 |
31 | ### Environment Variables
32 |
33 | Create a `.env.local` file:
34 |
35 | ```bash
36 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_publishable_key
37 | CLERK_SECRET_KEY=your_secret_key
38 | NEXT_PUBLIC_BACKEND_URL=http://localhost:8080
39 | NEXT_PUBLIC_STRIPE_KEY=your_stripe_key
40 | ```
41 |
42 | ### Development
43 |
44 | ```bash
45 | # Install dependencies
46 | bun install
47 |
48 | # Run development server
49 | bun dev
50 |
51 | # Build for production
52 | bun run build
53 |
54 | # Start production server
55 | bun start
56 | ```
57 |
58 | The application will be available at [http://localhost:3000](http://localhost:3000).
59 |
60 | ## Project Structure
61 |
62 | ```
63 | apps/web/
64 | ├── app/ # App Router pages
65 | ├── components/ # React components
66 | ├── lib/ # Utility functions
67 | ├── styles/ # Global styles
68 | ├── types/ # TypeScript types
69 | └── public/ # Static assets
70 | ```
71 |
72 | ## Key Components
73 |
74 | - `app/page.tsx` - Homepage
75 | - `app/dashboard/page.tsx` - User Dashboard
76 | - `components/Camera.tsx` - Image Generation UI
77 | - `components/Gallery.tsx` - Image Gallery
78 | - `components/ui/` - Shared UI Components
79 |
80 | ## Docker Support
81 |
82 | Build the frontend container:
83 |
84 | ```bash
85 | docker build -f docker/Dockerfile.frontend -t photoai-frontend ..
86 | ```
87 |
88 | Run the container:
89 |
90 | ```bash
91 | docker run -p 3000:3000 \
92 | -e NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_key \
93 | -e CLERK_SECRET_KEY=your_secret_key \
94 | -e NEXT_PUBLIC_BACKEND_URL=http://localhost:8080 \
95 | -e NEXT_PUBLIC_STRIPE_KEY=your_stripe_key \
96 | photoai-frontend
97 | ```
98 |
99 | ## API Integration
100 |
101 | The frontend communicates with the backend API at `NEXT_PUBLIC_BACKEND_URL`. Key endpoints:
102 |
103 | - `/api/generate` - Generate new images
104 | - `/api/images` - Fetch user's images
105 | - `/api/pack` - Manage credit packs
106 |
107 | ## Contributing
108 |
109 | 1. Fork the repository
110 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
111 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
112 | 4. Push to the branch (`git push origin feature/amazing-feature`)
113 | 5. Open a Pull Request
114 |
115 | ## License
116 |
117 | This project is licensed under the MIT License - see the LICENSE file for details.
118 |
--------------------------------------------------------------------------------
/apps/web/app/config.ts:
--------------------------------------------------------------------------------
1 | export const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8080";
2 | export const CLOUDFLARE_URL = "https://pub-b2acac8ef6a84c39b35165219b664570.r2.dev";
3 |
--------------------------------------------------------------------------------
/apps/web/app/dashboard/page.tsx:
--------------------------------------------------------------------------------
1 | import { GenerateImage } from "@/components/GenerateImage";
2 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
3 | import { Train } from "@/components/Train";
4 | import { Packs } from "@/components/Packs";
5 | import { Camera } from "@/components/Camera";
6 | import { redirect } from "next/navigation";
7 | import { auth } from "@clerk/nextjs/server";
8 | export const dynamic = "force-dynamic";
9 |
10 | export default async function DashboardPage() {
11 | const { userId } = await auth();
12 |
13 | if (!userId) {
14 | redirect("/");
15 | }
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
26 | Camera
27 |
28 |
32 | GenerateImages
33 |
34 |
38 | Packs
39 |
40 |
44 | TrainModel
45 |
46 |
47 |
48 |
49 |
53 |
54 |
55 |
59 |
60 |
61 |
65 |
66 |
67 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/apps/web/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/photo-ai/752707e61d8ad2e1abd0189ebc30668232c82a1d/apps/web/app/favicon.ico
--------------------------------------------------------------------------------
/apps/web/app/fonts/GeistMonoVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/photo-ai/752707e61d8ad2e1abd0189ebc30668232c82a1d/apps/web/app/fonts/GeistMonoVF.woff
--------------------------------------------------------------------------------
/apps/web/app/fonts/GeistVF.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/photo-ai/752707e61d8ad2e1abd0189ebc30668232c82a1d/apps/web/app/fonts/GeistVF.woff
--------------------------------------------------------------------------------
/apps/web/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import localFont from "next/font/local";
3 | import "./globals.css";
4 | import { Appbar } from "@/components/Appbar";
5 | import { Providers } from "@/components/providers/Providers";
6 | import { Footer } from "@/components/Footer";
7 | import Script from "next/script";
8 |
9 | const geistSans = localFont({
10 | src: "./fonts/GeistVF.woff",
11 | variable: "--font-geist-sans",
12 | });
13 |
14 | const geistMono = localFont({
15 | src: "./fonts/GeistMonoVF.woff",
16 | variable: "--font-geist-mono",
17 | });
18 |
19 | export const metadata: Metadata = {
20 | title: "100xPhoto - AI-Powered Photo Enhancement",
21 | description:
22 | "Transform your photos with AI-powered enhancement and editing tools.",
23 | };
24 |
25 | export default function RootLayout({
26 | children,
27 | }: {
28 | children: React.ReactNode;
29 | }) {
30 | return (
31 |
32 |
33 |
38 |
39 |
42 |
43 |
44 |
45 |
{children}
46 |
47 |
48 |
49 |
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/apps/web/app/page.module.css:
--------------------------------------------------------------------------------
1 | .page {
2 | --gray-rgb: 0, 0, 0;
3 | --gray-alpha-200: rgba(var(--gray-rgb), 0.08);
4 | --gray-alpha-100: rgba(var(--gray-rgb), 0.05);
5 |
6 | --button-primary-hover: #383838;
7 | --button-secondary-hover: #f2f2f2;
8 |
9 | display: grid;
10 | grid-template-rows: 20px 1fr 20px;
11 | align-items: center;
12 | justify-items: center;
13 | min-height: 100svh;
14 | padding: 80px;
15 | gap: 64px;
16 | font-synthesis: none;
17 | }
18 |
19 | @media (prefers-color-scheme: dark) {
20 | .page {
21 | --gray-rgb: 255, 255, 255;
22 | --gray-alpha-200: rgba(var(--gray-rgb), 0.145);
23 | --gray-alpha-100: rgba(var(--gray-rgb), 0.06);
24 |
25 | --button-primary-hover: #ccc;
26 | --button-secondary-hover: #1a1a1a;
27 | }
28 | }
29 |
30 | .main {
31 | display: flex;
32 | flex-direction: column;
33 | gap: 32px;
34 | grid-row-start: 2;
35 | }
36 |
37 | .main ol {
38 | font-family: var(--font-geist-mono);
39 | padding-left: 0;
40 | margin: 0;
41 | font-size: 14px;
42 | line-height: 24px;
43 | letter-spacing: -0.01em;
44 | list-style-position: inside;
45 | }
46 |
47 | .main li:not(:last-of-type) {
48 | margin-bottom: 8px;
49 | }
50 |
51 | .main code {
52 | font-family: inherit;
53 | background: var(--gray-alpha-100);
54 | padding: 2px 4px;
55 | border-radius: 4px;
56 | font-weight: 600;
57 | }
58 |
59 | .ctas {
60 | display: flex;
61 | gap: 16px;
62 | }
63 |
64 | .ctas a {
65 | appearance: none;
66 | border-radius: 128px;
67 | height: 48px;
68 | padding: 0 20px;
69 | border: none;
70 | font-family: var(--font-geist-sans);
71 | border: 1px solid transparent;
72 | transition: background 0.2s, color 0.2s, border-color 0.2s;
73 | cursor: pointer;
74 | display: flex;
75 | align-items: center;
76 | justify-content: center;
77 | font-size: 16px;
78 | line-height: 20px;
79 | font-weight: 500;
80 | }
81 |
82 | a.primary {
83 | background: var(--foreground);
84 | color: var(--background);
85 | gap: 8px;
86 | }
87 |
88 | a.secondary {
89 | border-color: var(--gray-alpha-200);
90 | min-width: 180px;
91 | }
92 |
93 | button.secondary {
94 | appearance: none;
95 | border-radius: 128px;
96 | height: 48px;
97 | padding: 0 20px;
98 | border: none;
99 | font-family: var(--font-geist-sans);
100 | border: 1px solid transparent;
101 | transition: background 0.2s, color 0.2s, border-color 0.2s;
102 | cursor: pointer;
103 | display: flex;
104 | align-items: center;
105 | justify-content: center;
106 | font-size: 16px;
107 | line-height: 20px;
108 | font-weight: 500;
109 | background: transparent;
110 | border-color: var(--gray-alpha-200);
111 | min-width: 180px;
112 | }
113 |
114 | .footer {
115 | font-family: var(--font-geist-sans);
116 | grid-row-start: 3;
117 | display: flex;
118 | gap: 24px;
119 | }
120 |
121 | .footer a {
122 | display: flex;
123 | align-items: center;
124 | gap: 8px;
125 | }
126 |
127 | .footer img {
128 | flex-shrink: 0;
129 | }
130 |
131 | /* Enable hover only on non-touch devices */
132 | @media (hover: hover) and (pointer: fine) {
133 | a.primary:hover {
134 | background: var(--button-primary-hover);
135 | border-color: transparent;
136 | }
137 |
138 | a.secondary:hover {
139 | background: var(--button-secondary-hover);
140 | border-color: transparent;
141 | }
142 |
143 | .footer a:hover {
144 | text-decoration: underline;
145 | text-underline-offset: 4px;
146 | }
147 | }
148 |
149 | @media (max-width: 600px) {
150 | .page {
151 | padding: 32px;
152 | padding-bottom: 80px;
153 | }
154 |
155 | .main {
156 | align-items: center;
157 | }
158 |
159 | .main ol {
160 | text-align: center;
161 | }
162 |
163 | .ctas {
164 | flex-direction: column;
165 | }
166 |
167 | .ctas a {
168 | font-size: 14px;
169 | height: 40px;
170 | padding: 0 16px;
171 | }
172 |
173 | a.secondary {
174 | min-width: auto;
175 | }
176 |
177 | .footer {
178 | flex-wrap: wrap;
179 | align-items: center;
180 | justify-content: center;
181 | }
182 | }
183 |
184 | @media (prefers-color-scheme: dark) {
185 | .logo {
186 | filter: invert();
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/apps/web/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { Hero } from "@/components/home/Hero";
3 | import { useAuth } from "@/hooks/useAuth";
4 | import { redirect } from "next/navigation";
5 |
6 |
7 | export default function Home() {
8 | const { user } = useAuth();
9 | if (user) {
10 | redirect("/dashboard");
11 | }
12 | return (
13 |
14 |
15 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/apps/web/app/payment/cancel/page.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react";
2 | import { PaymentCancelContent } from "@/components/payment/PaymentCancelContent";
3 |
4 | export default function PaymentCancelPage() {
5 | return (
6 |
9 |
10 |
Loading...
11 |
12 |
13 | }
14 | >
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/apps/web/app/payment/success/page.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react";
2 | import { PaymentSuccessContent } from "@/components/payment/PaymentSuccessContent";
3 |
4 | export default function PaymentSuccessPage() {
5 | return (
6 |
9 |
10 |
Loading...
11 |
12 |
13 | }
14 | >
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/apps/web/app/payment/verify/page.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react";
2 | import { VerifyContent } from "@/components/payment/VerifyContent";
3 |
4 | export default function VerifyPage() {
5 | return (
6 |
9 |
10 |
Loading...
11 |
12 |
13 | }
14 | >
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/apps/web/app/pricing/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { PlanCard } from "@/components/subscription/PlanCard";
5 | import { PlanType } from "@/types";
6 | import { usePayment } from "@/hooks/usePayment";
7 | import { motion } from "framer-motion";
8 | import { useAuth } from "@/hooks/useAuth";
9 |
10 | export default function SubscriptionPage() {
11 | const [, setSelectedPlan] = useState(null);
12 | const { handlePayment } = usePayment();
13 | const { isAuthenticated } = useAuth();
14 |
15 | const handlePlanSelect = async (plan: PlanType) => {
16 | if (!isAuthenticated) return;
17 |
18 | setSelectedPlan(plan);
19 | await handlePayment(plan, false, "razorpay");
20 | setSelectedPlan(null);
21 | };
22 |
23 | const plans = [
24 | {
25 | type: PlanType.basic,
26 | name: "Basic Plan",
27 | price: 50,
28 | credits: 500,
29 | features: [
30 | "500 Credits",
31 | "Basic Support",
32 | "Standard Processing",
33 | "Flux Lora",
34 | "24/7 Email Support",
35 | ],
36 | },
37 | {
38 | type: PlanType.premium,
39 | name: "Premium Plan",
40 | price: 100,
41 | credits: 1000,
42 | features: [
43 | "1000 Credits",
44 | "Priority Support",
45 | "Fast Processing",
46 | "Advanced Features",
47 | "Flux Lora",
48 | "Custom Solutions",
49 | ],
50 | },
51 | ] as const;
52 |
53 | return (
54 |
55 |
61 |
62 | Choose Your Plan
63 |
64 |
65 | Find the perfect plan for your needs. Every plan includes access to
66 | our core features.
67 |
68 |
69 |
70 |
76 | {plans.map((plan) => (
77 | handlePlanSelect(plan.type)}
87 | />
88 | ))}
89 |
90 |
91 | );
92 | }
93 |
--------------------------------------------------------------------------------
/apps/web/app/purchases/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation";
2 | import TransactionsPage from "@/components/payment/PurchasesPage";
3 | import React from "react";
4 | import { auth } from "@clerk/nextjs/server";
5 |
6 | export default async function PurchasesPage() {
7 | const { userId } = await auth();
8 |
9 | if (!userId) {
10 | redirect("/");
11 | }
12 |
13 | return ;
14 | }
15 |
--------------------------------------------------------------------------------
/apps/web/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": "",
8 | "css": "app/globals.css",
9 | "baseColor": "neutral",
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 | }
--------------------------------------------------------------------------------
/apps/web/components/Appbar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { SignInButton, SignedIn, SignedOut, UserButton } from "@clerk/nextjs";
4 | import { Button } from "./ui/button";
5 | import { Credits } from "./navbar/Credits";
6 | import Link from "next/link";
7 | import { motion } from "framer-motion";
8 | import { ThemeToggle } from "./ThemeToggle";
9 |
10 | export function Appbar() {
11 | return (
12 |
13 |
19 |
25 |
26 | {/* Logo */}
27 |
28 |
32 |
44 |
45 | 100xPhotos
46 |
47 |
48 |
49 |
50 | {/* Auth & Pricing */}
51 |
52 |
53 |
61 |
69 |
70 |
80 |
81 |
82 |
90 |
94 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | );
113 | }
114 |
--------------------------------------------------------------------------------
/apps/web/components/GenerateImage.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useAuth } from "@clerk/nextjs";
4 | import { useState } from "react";
5 | import { Button } from "./ui/button";
6 | import { Textarea } from "@/components/ui/textarea";
7 | import axios from "axios";
8 | import { BACKEND_URL } from "@/app/config";
9 | import { SelectModel } from "./Models";
10 | import toast from "react-hot-toast";
11 | import { motion } from "framer-motion";
12 | import { Sparkles } from "lucide-react";
13 | import { useCredits } from "@/hooks/use-credits";
14 | import { useRouter } from "next/navigation";
15 | import CustomLabel from "./ui/customLabel";
16 | import { GlowEffect } from "./GlowEffect";
17 |
18 | export function GenerateImage() {
19 | const [prompt, setPrompt] = useState("");
20 | const [selectedModel, setSelectedModel] = useState();
21 | const [isGenerating, setIsGenerating] = useState(false);
22 | const { getToken } = useAuth();
23 | const { credits } = useCredits();
24 | const router = useRouter();
25 |
26 | const handleGenerate = async () => {
27 | if (!prompt || !selectedModel) return;
28 |
29 | if (credits <= 0) {
30 | router.push("/pricing");
31 | return;
32 | }
33 |
34 | setIsGenerating(true);
35 | try {
36 | const token = await getToken();
37 | await axios.post(
38 | `${BACKEND_URL}/ai/generate`,
39 | {
40 | prompt,
41 | modelId: selectedModel,
42 | num: 1,
43 | },
44 | {
45 | headers: { Authorization: `Bearer ${token}` },
46 | }
47 | );
48 | toast.success("Image generation started!");
49 | setPrompt("");
50 | } catch (error) {
51 | toast.error("Failed to generate image");
52 | } finally {
53 | setIsGenerating(false);
54 | }
55 | };
56 |
57 | return (
58 |
64 |
65 |
69 |
70 |
76 |
77 |
82 |
83 |
84 |
85 |
93 | {(prompt && selectedModel) && (
94 |
101 | )}
102 |
103 |
104 |
105 |
106 | );
107 | }
108 |
--------------------------------------------------------------------------------
/apps/web/components/GlowEffect.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { cn } from "@/lib/utils";
3 | import { motion, Transition } from "framer-motion";
4 |
5 | export type GlowEffectProps = {
6 | className?: string;
7 | style?: React.CSSProperties;
8 | colors?: string[];
9 | mode?:
10 | | "rotate"
11 | | "pulse"
12 | | "breathe"
13 | | "colorShift"
14 | | "flowHorizontal"
15 | | "static";
16 | blur?:
17 | | number
18 | | "softest"
19 | | "soft"
20 | | "medium"
21 | | "strong"
22 | | "stronger"
23 | | "strongest"
24 | | "none";
25 | transition?: Transition;
26 | scale?: number;
27 | duration?: number;
28 | };
29 |
30 | export function GlowEffect({
31 | className,
32 | style,
33 | colors = ["#FF5733", "#33FF57", "#3357FF", "#F1C40F"],
34 | mode = "rotate",
35 | blur = "medium",
36 | transition,
37 | scale = 1,
38 | duration = 5,
39 | }: GlowEffectProps) {
40 | const BASE_TRANSITION = {
41 | repeat: Infinity,
42 | duration: duration,
43 | ease: "linear",
44 | };
45 |
46 | const animations = {
47 | rotate: {
48 | background: [
49 | `conic-gradient(from 0deg at 50% 50%, ${colors.join(", ")})`,
50 | `conic-gradient(from 360deg at 50% 50%, ${colors.join(", ")})`,
51 | ],
52 | transition: {
53 | ...(transition ?? BASE_TRANSITION),
54 | },
55 | },
56 | pulse: {
57 | background: colors.map(
58 | (color) =>
59 | `radial-gradient(circle at 50% 50%, ${color} 0%, transparent 100%)`
60 | ),
61 | scale: [1 * scale, 1.1 * scale, 1 * scale],
62 | opacity: [0.5, 0.8, 0.5],
63 | transition: {
64 | ...(transition ?? {
65 | ...BASE_TRANSITION,
66 | repeatType: "mirror",
67 | }),
68 | },
69 | },
70 | breathe: {
71 | background: [
72 | ...colors.map(
73 | (color) =>
74 | `radial-gradient(circle at 50% 50%, ${color} 0%, transparent 100%)`
75 | ),
76 | ],
77 | scale: [1 * scale, 1.05 * scale, 1 * scale],
78 | transition: {
79 | ...(transition ?? {
80 | ...BASE_TRANSITION,
81 | repeatType: "mirror",
82 | }),
83 | },
84 | },
85 | colorShift: {
86 | background: colors.map((color, index) => {
87 | const nextColor = colors[(index + 1) % colors.length];
88 | return `conic-gradient(from 0deg at 50% 50%, ${color} 0%, ${nextColor} 50%, ${color} 100%)`;
89 | }),
90 | transition: {
91 | ...(transition ?? {
92 | ...BASE_TRANSITION,
93 | repeatType: "mirror",
94 | }),
95 | },
96 | },
97 | flowHorizontal: {
98 | background: colors.map((color) => {
99 | const nextColor = colors[(colors.indexOf(color) + 1) % colors.length];
100 | return `linear-gradient(to right, ${color}, ${nextColor})`;
101 | }),
102 | transition: {
103 | ...(transition ?? {
104 | ...BASE_TRANSITION,
105 | repeatType: "mirror",
106 | }),
107 | },
108 | },
109 | static: {
110 | background: `linear-gradient(to right, ${colors.join(", ")})`,
111 | },
112 | };
113 |
114 | const getBlurClass = (blur: GlowEffectProps["blur"]) => {
115 | if (typeof blur === "number") {
116 | return `blur-[${blur}px]`;
117 | }
118 |
119 | const presets = {
120 | softest: "blur-sm",
121 | soft: "blur",
122 | medium: "blur-md",
123 | strong: "blur-lg",
124 | stronger: "blur-xl",
125 | strongest: "blur-xl",
126 | none: "blur-none",
127 | };
128 |
129 | return presets[blur as keyof typeof presets];
130 | };
131 |
132 | return (
133 |
150 | );
151 | }
152 |
--------------------------------------------------------------------------------
/apps/web/components/ImageCard.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowDown } from "lucide-react";
2 | import { Skeleton } from "./ui/skeleton";
3 | import Image from "next/image";
4 | import { TImage } from "./Camera";
5 |
6 | interface ImageCardProps extends TImage {
7 | onClick: () => void;
8 | }
9 | export function ImageCard({ id, status, imageUrl, onClick ,prompt}: ImageCardProps) {
10 | if (!imageUrl) return null;
11 |
12 | return (
13 |
14 |
15 |
24 |
25 |
26 |
{prompt}
27 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 |
35 | export function ImageCardSkeleton() {
36 | return (
37 |
42 | );
43 | }
--------------------------------------------------------------------------------
/apps/web/components/Models.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { useEffect, useState } from "react";
3 | import { useAuth } from "@clerk/nextjs";
4 | import axios from "axios";
5 | import { BACKEND_URL } from "@/app/config";
6 | import Image from "next/image";
7 | import { motion } from "framer-motion";
8 | import { Card } from "./ui/card";
9 | import { Badge } from "./ui/badge";
10 | import { Sparkles, Loader2 } from "lucide-react";
11 | import { cn } from "@/lib/utils";
12 |
13 | interface TModel {
14 | id: string;
15 | thumbnail: string;
16 | name: string;
17 | trainingStatus: "Generated" | "Pending";
18 | }
19 |
20 | export function SelectModel({
21 | setSelectedModel,
22 | selectedModel,
23 | }: {
24 | setSelectedModel: (model: string) => void;
25 | selectedModel?: string;
26 | }) {
27 | const { getToken } = useAuth();
28 | const [modelLoading, setModalLoading] = useState(false);
29 | const [models, setModels] = useState([]);
30 |
31 | useEffect(() => {
32 | (async () => {
33 | const token = await getToken();
34 | const response = await axios.get(`${BACKEND_URL}/models`, {
35 | headers: {
36 | Authorization: `Bearer ${token}`,
37 | },
38 | });
39 | setModels(response.data.models);
40 | setSelectedModel(response.data.models[0]?.id);
41 | setModalLoading(false);
42 | })();
43 | }, []);
44 |
45 | const container = {
46 | hidden: { opacity: 0 },
47 | show: {
48 | opacity: 1,
49 | transition: {
50 | staggerChildren: 0.1,
51 | },
52 | },
53 | };
54 |
55 | const item = {
56 | hidden: { opacity: 0, y: 20 },
57 | show: { opacity: 1, y: 0 },
58 | };
59 |
60 | return (
61 |
62 |
63 |
64 |
65 | Select Model
66 |
67 |
68 | Choose an AI model to generate your images
69 |
70 |
71 | {models.find((x) => x.trainingStatus !== "Generated") && (
72 |
73 |
74 | Training in progress
75 |
76 | )}
77 |
78 |
79 | {modelLoading ? (
80 |
81 | {[1, 2, 3].map((_, i) => (
82 |
83 | ))}
84 |
85 | ) : (
86 |
92 | {models
93 | .filter((model) => model.trainingStatus === "Generated")
94 | .map((model) => (
95 |
96 | setSelectedModel(model.id)}
102 | >
103 |
104 |
110 |
111 |
112 |
113 |
114 | {model.name}
115 |
116 |
117 |
118 |
119 |
120 |
121 | ))}
122 |
123 | )}
124 |
125 | {!modelLoading && models.length === 0 && (
126 |
131 |
132 | No models available
133 |
134 | Start by training a new model
135 |
136 |
137 | )}
138 |
139 | );
140 | }
141 |
--------------------------------------------------------------------------------
/apps/web/components/Packs.tsx:
--------------------------------------------------------------------------------
1 | import { BACKEND_URL } from "@/app/config";
2 | import { TPack } from "./PackCard";
3 | import axios from "axios";
4 | import { PacksClient } from "./PacksClient";
5 |
6 | async function getPacks(): Promise {
7 | const res = await axios.get(`${BACKEND_URL}/pack/bulk`);
8 | return res.data.packs ?? [];
9 | }
10 |
11 | export async function Packs() {
12 | const packs = await getPacks();
13 |
14 | return ;
15 | }
16 |
--------------------------------------------------------------------------------
/apps/web/components/PacksClient.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { SelectModel } from "./Models";
5 | import { PackCard, TPack } from "./PackCard";
6 | import { motion } from "framer-motion";
7 | import { Package, Search, Sparkles, Filter } from "lucide-react";
8 | import { Input } from "./ui/input";
9 | import { Button } from "./ui/button";
10 | import { Badge } from "./ui/badge";
11 | import { cn } from "@/lib/utils";
12 |
13 | export function PacksClient({ packs }: { packs: TPack[] }) {
14 | const [selectedModelId, setSelectedModelId] = useState();
15 | const [searchQuery, setSearchQuery] = useState("");
16 |
17 | const filteredPacks = packs.filter(pack =>
18 | pack.name.toLowerCase().includes(searchQuery.toLowerCase())
19 | );
20 |
21 | return (
22 |
23 |
24 | {/* Filters Section */}
25 |
31 |
39 |
40 |
41 |
42 |
43 | Select Pack
44 |
45 |
46 | Chose a pack to generate images with
47 |
48 |
49 |
69 | {packs.length > 0 ? (
70 | packs.map((pack, index) => (
71 |
79 |
83 |
84 | ))
85 | ) : (
86 |
93 |
94 |
95 |
96 | {searchQuery ? "No matching packs found" : "No packs available"}
97 |
98 |
99 | {searchQuery
100 | ? "Try adjusting your search terms or clear the filter"
101 | : "Select a model above to view compatible packs"
102 | }
103 |
104 | {searchQuery && (
105 |
112 | )}
113 |
114 |
115 | )}
116 |
117 |
118 |
119 | );
120 | }
121 |
--------------------------------------------------------------------------------
/apps/web/components/ThemeToggle.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useTheme } from "next-themes";
4 | import { Switch } from "@/components/ui/switch";
5 | import { Sun, Moon } from "lucide-react";
6 |
7 | export function ThemeToggle() {
8 | const { theme, setTheme } = useTheme();
9 | const isDark = theme === "dark";
10 |
11 | return (
12 |
13 |
14 | setTheme(isDark ? "light" : "dark")}
18 | />
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/apps/web/components/home/BackgroundEffects.tsx:
--------------------------------------------------------------------------------
1 | export function BackgroundEffects() {
2 | return (
3 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/apps/web/components/home/Features.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { motion } from "framer-motion";
4 | import { Camera, Wand2, Users, Clock } from "lucide-react";
5 |
6 | const features = [
7 | {
8 | icon: ,
9 | title: "Professional Quality",
10 | description: "Studio-grade portraits generated in seconds",
11 | gradient: "from-blue-500 to-purple-500",
12 | },
13 | {
14 | icon: ,
15 | title: "Magic Editing",
16 | description: "Advanced AI tools to perfect every detail",
17 | gradient: "from-purple-500 to-pink-500",
18 | },
19 | {
20 | icon: ,
21 | title: "Family Collections",
22 | description: "Create stunning portraits for the whole family",
23 | gradient: "from-pink-500 to-red-500",
24 | },
25 | {
26 | icon: ,
27 | title: "Instant Delivery",
28 | description: "Get your photos in minutes, not days",
29 | gradient: "from-red-500 to-orange-500",
30 | },
31 | ];
32 |
33 | export function Features() {
34 | return (
35 |
41 | {features.map((feature, index) => (
42 |
47 |
50 |
51 | {feature.icon}
52 |
53 |
54 |
55 | {feature.title}
56 |
57 | {feature.description}
58 |
59 | ))}
60 |
61 | );
62 | }
--------------------------------------------------------------------------------
/apps/web/components/home/HeroHeader.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { SignInButton, SignedIn, SignedOut } from "@clerk/nextjs";
4 | import { Button } from "@/components/ui/button";
5 | import { useRouter } from "next/navigation";
6 | import { motion } from "framer-motion";
7 | import { ArrowRight, Sparkles, Zap } from "lucide-react";
8 |
9 | export function HeroHeader() {
10 | const router = useRouter();
11 |
12 | return (
13 |
19 |
20 |
26 |
27 | Next-Gen AI Portrait Generation
28 |
29 |
35 |
36 | Powered by 100xDevs
37 |
38 |
39 |
40 |
41 | Transform Your Photos with{" "}
42 |
43 | AI Magic
44 |
45 |
46 |
47 |
48 |
49 |
60 |
61 |
62 |
69 |
70 |
71 |
72 | );
73 | }
74 |
--------------------------------------------------------------------------------
/apps/web/components/home/HowItWorks.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { motion } from "framer-motion";
4 | import { useInView } from "framer-motion";
5 | import { useRef } from "react";
6 | import { Upload, Wand2, Download } from "lucide-react";
7 |
8 | const steps = [
9 | {
10 | icon: ,
11 | title: "Upload Your Photo",
12 | description: "Start by uploading any portrait photo you'd like to enhance",
13 | },
14 | {
15 | icon: ,
16 | title: "AI Magic",
17 | description:
18 | "Our advanced AI transforms your photo into stunning portraits",
19 | },
20 | {
21 | icon: ,
22 | title: "Download & Share",
23 | description: "Get your enhanced portraits in multiple styles instantly",
24 | },
25 | ];
26 |
27 | export function HowItWorks() {
28 | const ref = useRef(null);
29 | const isInView = useInView(ref, { once: true });
30 |
31 | return (
32 |
39 |
40 |
41 | How It{" "}
42 |
43 | Works
44 |
45 |
46 |
47 | Transform your photos into stunning AI-powered portraits in three
48 | simple steps
49 |
50 |
51 |
52 |
53 | {steps.map((step, index) => (
54 |
61 |
62 |
63 |
64 | {step.icon}
65 |
66 |
67 | {step.title}
68 |
69 |
{step.description}
70 |
71 |
72 | ))}
73 |
74 |
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/apps/web/components/home/PricingSection.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { motion } from "framer-motion";
4 | import { useInView } from "framer-motion";
5 | import { useRef } from "react";
6 | import { Button } from "@/components/ui/button";
7 | import { Check } from "lucide-react";
8 | import { plans } from "./data";
9 |
10 | export function PricingSection() {
11 | const ref = useRef(null);
12 | const isInView = useInView(ref, { once: true });
13 |
14 | return (
15 |
22 |
23 |
24 | Simple,{" "}
25 |
26 | Transparent
27 | {" "}
28 | Pricing
29 |
30 |
31 | Choose the perfect plan for your needs. No hidden fees.
32 |
33 |
34 |
35 |
36 | {plans.map((plan, index) => (
37 |
48 |
55 |
56 |
{plan.name}
57 |
{plan.price}
58 |
59 | {plan.features.map((feature) => (
60 | -
61 |
62 | {feature}
63 |
64 | ))}
65 |
66 |
75 |
76 |
77 |
78 | ))}
79 |
80 |
81 | );
82 | }
83 |
--------------------------------------------------------------------------------
/apps/web/components/home/ScrollIndicator.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { motion, useScroll } from "framer-motion";
4 |
5 | export function ScrollIndicator() {
6 | return (
7 |
13 |
19 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/apps/web/components/home/StatsSection.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { motion } from "framer-motion";
4 | import { useInView } from "framer-motion";
5 | import { useRef } from "react";
6 | import { stats } from "./data";
7 | export function StatsSection() {
8 | const ref = useRef(null);
9 | const isInView = useInView(ref, { once: true });
10 |
11 | return (
12 |
19 |
20 |
21 | {stats.map((stat, index) => (
22 |
31 |
32 | {stat.value}
33 |
34 | {stat.label}
35 |
36 | ))}
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/apps/web/components/home/Testimonials.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { motion } from "framer-motion";
4 | import { Star } from "lucide-react";
5 | import { testimonials } from "./data";
6 |
7 | export function Testimonials() {
8 | return (
9 |
15 |
16 | Loved by Creators
17 |
18 |
19 | Join thousands of satisfied users who have transformed their portraits
20 |
21 |
22 | {testimonials.map((testimonial, index) => (
23 |
28 |
29 |
30 |

35 |
36 |
37 |
38 | {testimonial.text}
39 |
40 |
41 | {testimonial.author}
42 |
43 |
{testimonial.role}
44 |
45 |
46 | ))}
47 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/apps/web/components/home/TrustedBy.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { motion } from "framer-motion";
4 | import Image from "next/image";
5 | import { brands } from "./data";
6 |
7 | export function TrustedBy() {
8 | return (
9 |
15 |
16 | Trusted by leading brands
17 |
18 |
19 | {brands.map((brand, index) => (
20 |
27 |
28 |
29 |
36 |
37 |
38 | ))}
39 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/apps/web/components/home/data.ts:
--------------------------------------------------------------------------------
1 | export const features = [
2 | {
3 | icon: "camera",
4 | title: "Professional Quality",
5 | description: "Studio-grade portraits generated in seconds",
6 | gradient: "from-blue-500 to-purple-500",
7 | },
8 | {
9 | icon: "wand",
10 | title: "Magic Editing",
11 | description: "Advanced AI tools to perfect every detail",
12 | gradient: "from-purple-500 to-pink-500",
13 | },
14 | {
15 | icon: "users",
16 | title: "Family Collections",
17 | description: "Create stunning portraits for the whole family",
18 | gradient: "from-pink-500 to-red-500",
19 | },
20 | {
21 | icon: "clock",
22 | title: "Instant Delivery",
23 | description: "Get your photos in minutes, not days",
24 | gradient: "from-red-500 to-orange-500",
25 | },
26 | ];
27 |
28 | export const testimonials = [
29 | {
30 | text: "The quality of these AI portraits is absolutely incredible. They look better than my professional headshots!",
31 | author: "Harkirat Singh",
32 | role: "Founder",
33 | avatar:
34 | "https://pbs.twimg.com/profile_images/1599003507415166977/pRYwiTo3_400x400.jpg",
35 | },
36 | {
37 | text: "We used this for our family portraits and the results were stunning. So much easier than a traditional photoshoot.",
38 | author: "Yash Makhija",
39 | role: "Developer",
40 | avatar:
41 | "https://i.ibb.co/ZpvLpgf8/Whats-App-Image-2024-12-08-at-01-17-05.jpg",
42 | },
43 | {
44 | text: "Game-changer for my professional brand. The variety of styles and quick delivery is unmatched.",
45 | author: "Sargam Poduel",
46 | role: "Founder of WebCraft",
47 | avatar:
48 | "https://media.licdn.com/dms/image/v2/D5603AQH9LnII_HXrHQ/profile-displayphoto-shrink_200_200/profile-displayphoto-shrink_200_200/0/1698821079941?e=2147483647&v=beta&t=1XGvRit2_LVRAtb-8y_e9mbtqXF102Ia_fX88-OvEI0",
49 | },
50 | ];
51 |
52 | export const carouselImages = [
53 | {
54 | url: "https://r2-us-west.photoai.com/1739277231-0b2465581e9551abecd467b163d0d48a-1.png",
55 | title: "Professional Portrait",
56 | description: "Perfect for LinkedIn and business profiles",
57 | style: "Corporate",
58 | },
59 | {
60 | url: "https://r2-us-west.photoai.com/1739273789-920e7410ef180855f9a5718d1e37eb3a-1.png",
61 | title: "Casual Lifestyle",
62 | description: "Natural and relaxed everyday portraits",
63 | style: "Casual",
64 | },
65 | {
66 | url: "https://r2-us-west.photoai.com/1739273783-9effbeb7239423cba9629e7dd06f3565-1.png",
67 | title: "Creative Portrait",
68 | description: "Artistic shots with unique lighting",
69 | style: "Creative",
70 | },
71 | {
72 | url: "https://r2-us-west.photoai.com/1738861046-1175c64ebe0ecfe10b857e205b3b4a1e-3.png",
73 | title: "Fashion Portrait",
74 | description: "High-end fashion inspired photography",
75 | style: "Fashion",
76 | },
77 | ];
78 |
79 | export const brands = [
80 | {
81 | name: "Company 1",
82 | logo: "https://media.licdn.com/dms/image/v2/D563DAQFdWbNq8YmjeA/image-scale_191_1128/image-scale_191_1128/0/1721141166811/100xdevs_cover?e=2147483647&v=beta&t=a1Ox9U8BLucp5rHxhTXRmUhMoMAKTDrs-IyjT547lPQ",
83 | },
84 | { name: "Company 2", logo: "/logos/logo2.svg" },
85 | { name: "Company 3", logo: "/logos/logo3.svg" },
86 | { name: "Company 4", logo: "/logos/logo4.svg" },
87 | ];
88 |
89 | export const stats = [
90 | { value: "100K+", label: "AI Portraits Generated" },
91 | { value: "50K+", label: "Happy Users" },
92 | { value: "98%", label: "Satisfaction Rate" },
93 | { value: "24/7", label: "AI Support" },
94 | ];
95 |
96 | export const plans = [
97 | {
98 | name: "Starter",
99 | price: "Free",
100 | features: [
101 | "10 AI Portraits",
102 | "Basic Styles",
103 | "24h Support",
104 | "Basic Export",
105 | ],
106 | highlighted: false,
107 | },
108 | {
109 | name: "Pro",
110 | price: "$9.99",
111 | features: [
112 | "100 AI Portraits",
113 | "Premium Styles",
114 | "Priority Support",
115 | "HD Export",
116 | "Advanced Editing",
117 | ],
118 | highlighted: true,
119 | },
120 | {
121 | name: "Enterprise",
122 | price: "Custom",
123 | features: [
124 | "Unlimited Portraits",
125 | "Custom Styles",
126 | "Dedicated Support",
127 | "API Access",
128 | "Custom Integration",
129 | ],
130 | highlighted: false,
131 | },
132 | ];
133 |
--------------------------------------------------------------------------------
/apps/web/components/navbar/Credits.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { Loader2, Plus } from "lucide-react";
3 | import { useRouter } from "next/navigation";
4 | import {
5 | DropdownMenu,
6 | DropdownMenuContent,
7 | DropdownMenuItem,
8 | DropdownMenuTrigger,
9 | } from "@/components/ui/dropdown-menu";
10 | import { Button } from "@/components/ui/button";
11 | import { useCredits } from "@/hooks/use-credits";
12 |
13 | export function Credits() {
14 | const { credits, loading } = useCredits();
15 | const router = useRouter();
16 |
17 | return (
18 |
19 |
20 |
49 |
50 |
51 | router.push("/pricing")}
54 | >
55 | Add Credits
56 |
57 |
58 |
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/apps/web/components/payment/PaymentCancelContent.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 | import { XCircle, AlertCircle, ArrowLeft, RefreshCcw } from "lucide-react";
5 | import { Button } from "@/components/ui/button";
6 | import { motion } from "framer-motion";
7 |
8 | export function PaymentCancelContent() {
9 | const router = useRouter();
10 |
11 | return (
12 |
13 |
18 |
19 |
20 |
26 |
27 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
44 | Payment Cancelled
45 |
46 |
52 | Your payment was not completed. No charges were made.
53 |
54 |
55 |
56 |
62 |
63 |
64 |
65 | Payment Not Processed
66 |
67 |
68 |
69 |
70 |
No Charges Made
71 |
72 |
73 |
74 |
80 |
88 |
95 |
96 |
97 |
98 |
99 |
100 | );
101 | }
102 |
--------------------------------------------------------------------------------
/apps/web/components/providers/Providers.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { dark } from '@clerk/themes'
3 | import { ClerkProvider } from "@clerk/nextjs";
4 | import { ThemeProvider } from "@/components/theme-provider";
5 | import { Toaster } from "react-hot-toast";
6 |
7 | interface ProvidersProps {
8 | children: React.ReactNode;
9 | }
10 |
11 | export function Providers({ children }: ProvidersProps) {
12 | return (
13 | // dark mode
14 |
15 |
21 | {children}
22 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/apps/web/components/subscription/PlanCard.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { Card } from "@/components/ui/card";
3 | import { CheckIcon, Sparkles } from "lucide-react";
4 | import { useState } from "react";
5 | import { useAuth } from "@/hooks/useAuth";
6 | import { SignInButton } from "@clerk/nextjs";
7 |
8 | interface PlanCardProps {
9 | plan: {
10 | type: string;
11 | name: string;
12 | price: number;
13 | credits: number;
14 | features: string[];
15 | };
16 | onSelect: () => void;
17 | }
18 |
19 | export function PlanCard({ plan, onSelect }: PlanCardProps) {
20 | const [isLoading, setIsLoading] = useState(false);
21 | const { isAuthenticated } = useAuth();
22 | const isPremium = plan.type === "premium";
23 |
24 | const handleClick = async () => {
25 | if (isLoading) return;
26 | setIsLoading(true);
27 | try {
28 | await onSelect();
29 | } finally {
30 | setIsLoading(false);
31 | }
32 | };
33 |
34 | return (
35 |
43 | {isPremium && (
44 |
45 |
46 | Popular
47 |
48 |
49 | )}
50 |
51 |
52 |
53 |
54 | {plan.name}
55 |
56 |
57 | One-time payment for {plan.credits} credits
58 |
59 |
60 |
61 |
62 |
63 | ${plan.price}
64 |
65 | one-time
66 |
67 |
68 |
69 | {plan.features.map((feature) => (
70 |
74 |
81 | {feature}
82 |
83 | ))}
84 |
85 |
86 | {isAuthenticated ? (
87 |
106 | ) : (
107 |
108 |
118 |
119 | )}
120 |
121 |
122 | );
123 | }
124 |
--------------------------------------------------------------------------------
/apps/web/components/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { ThemeProvider as NextThemesProvider } from "next-themes";
5 |
6 | export function ThemeProvider({
7 | children,
8 | ...props
9 | }: React.ComponentProps) {
10 | return {children};
11 | }
12 |
--------------------------------------------------------------------------------
/apps/web/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 |
--------------------------------------------------------------------------------
/apps/web/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 | custom: "bg-green-500 text-primary-foreground hover:bg-green-600",
23 | },
24 | size: {
25 | default: "h-9 px-4 py-2",
26 | sm: "h-8 rounded-md px-3 text-xs",
27 | lg: "h-10 rounded-md px-8",
28 | icon: "h-9 w-9",
29 | },
30 | },
31 | defaultVariants: {
32 | variant: "default",
33 | size: "default",
34 | },
35 | }
36 | )
37 |
38 | export interface ButtonProps
39 | extends React.ButtonHTMLAttributes,
40 | VariantProps {
41 | asChild?: boolean
42 | }
43 |
44 | const Button = React.forwardRef(
45 | ({ className, variant, size, asChild = false, ...props }, ref) => {
46 | const Comp = asChild ? Slot : "button"
47 | return (
48 |
53 | )
54 | }
55 | )
56 | Button.displayName = "Button"
57 |
58 | export { Button, buttonVariants }
59 |
--------------------------------------------------------------------------------
/apps/web/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 |
--------------------------------------------------------------------------------
/apps/web/components/ui/customLabel.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default function CustomLabel({ label }: { label: string }) {
4 | return (
5 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/apps/web/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogTrigger,
116 | DialogClose,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/apps/web/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 |
--------------------------------------------------------------------------------
/apps/web/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 | HTMLLabelElement,
15 | React.LabelHTMLAttributes
16 | >(({ className, ...props }, ref) => (
17 |
25 | ));
26 | Label.displayName = "Label";
27 |
28 | export { Label }
29 |
--------------------------------------------------------------------------------
/apps/web/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ProgressPrimitive from "@radix-ui/react-progress"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | function Progress({
9 | className,
10 | value,
11 | ...props
12 | }: React.ComponentProps) {
13 | return (
14 |
22 |
27 |
28 | )
29 | }
30 |
31 | export { Progress }
32 |
--------------------------------------------------------------------------------
/apps/web/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ))
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = "vertical", ...props }, ref) => (
30 |
43 |
44 |
45 | ))
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
47 |
48 | export { ScrollArea, ScrollBar }
49 |
--------------------------------------------------------------------------------
/apps/web/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 |
--------------------------------------------------------------------------------
/apps/web/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 |
--------------------------------------------------------------------------------
/apps/web/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 |
--------------------------------------------------------------------------------
/apps/web/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Tabs = TabsPrimitive.Root
9 |
10 | const TabsList = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | TabsList.displayName = TabsPrimitive.List.displayName
24 |
25 | const TabsTrigger = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
37 | ))
38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
39 |
40 | const TabsContent = React.forwardRef<
41 | React.ElementRef,
42 | React.ComponentPropsWithoutRef
43 | >(({ className, ...props }, ref) => (
44 |
52 | ))
53 | TabsContent.displayName = TabsPrimitive.Content.displayName
54 |
55 | export { Tabs, TabsList, TabsTrigger, TabsContent }
56 |
--------------------------------------------------------------------------------
/apps/web/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Textarea = React.forwardRef<
6 | HTMLTextAreaElement,
7 | React.ComponentProps<"textarea">
8 | >(({ className, ...props }, ref) => {
9 | return (
10 |
18 | )
19 | })
20 | Textarea.displayName = "Textarea"
21 |
22 | export { Textarea }
23 |
--------------------------------------------------------------------------------
/apps/web/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useToast } from "@/hooks/use-toast"
4 | import {
5 | Toast,
6 | ToastClose,
7 | ToastDescription,
8 | ToastProvider,
9 | ToastTitle,
10 | ToastViewport,
11 | } from "@/components/ui/toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/apps/web/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 |
--------------------------------------------------------------------------------
/apps/web/components/ui/upload.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import JSZip from "jszip";
4 | import axios from "axios";
5 | import { useState, useCallback } from "react";
6 | import {
7 | Card,
8 | CardHeader,
9 | CardTitle,
10 | CardDescription,
11 | CardContent,
12 | } from "@/components/ui/card";
13 | import { Button } from "@/components/ui/button";
14 | import { Progress } from "@/components/ui/progress";
15 | import { BACKEND_URL, CLOUDFLARE_URL } from "@/app/config";
16 | import { cn } from "@/lib/utils";
17 |
18 | export function UploadModal({
19 | handleUpload,
20 | uploadProgress,
21 | isUploading,
22 | }: {
23 | handleUpload: (files: File[]) => void;
24 | uploadProgress: number;
25 | isUploading: boolean;
26 | }) {
27 | const [isDragging, setIsDragging] = useState(false);
28 | const handleDrag = useCallback((e: React.DragEvent) => {
29 | e.preventDefault();
30 | e.stopPropagation();
31 | setIsDragging(e.type === "dragenter" || e.type === "dragover");
32 | }, []);
33 |
34 | const handleDrop = useCallback(async (e: React.DragEvent) => {
35 | e.preventDefault();
36 | e.stopPropagation();
37 | setIsDragging(false);
38 |
39 | const files = Array.from(e.dataTransfer.files);
40 | if (files.length) await handleUpload(files);
41 | }, []);
42 |
43 | return (
44 |
45 |
46 |
47 | Upload Modal Images
48 |
49 |
50 | Supports multiple images upload
51 |
52 |
53 |
54 |
67 |
68 |
69 | {isUploading ? (
70 |
71 |
72 |
73 | {uploadProgress < 50 ? "Preparing files..." : "Uploading..."}
74 |
75 |
76 | ) : (
77 |
78 |
79 | Drag and drop files here or
80 |
81 |
98 |
99 | Supported formats: PNG, JPG, GIF
100 |
101 |
102 | )}
103 |
104 |
105 |
106 | );
107 | }
108 |
109 | function CloudUploadIcon(props: React.SVGProps) {
110 | return (
111 |
125 | );
126 | }
127 |
--------------------------------------------------------------------------------
/apps/web/eslint.config.js:
--------------------------------------------------------------------------------
1 | import { nextJsConfig } from "@repo/eslint-config/next-js";
2 |
3 | /** @type {import("eslint").Linter.Config} */
4 | export default nextJsConfig;
5 |
--------------------------------------------------------------------------------
/apps/web/hooks/use-credits.ts:
--------------------------------------------------------------------------------
1 | import { BACKEND_URL } from "@/app/config";
2 | import { useEffect, useState } from "react";
3 | import { useAuth } from "./useAuth";
4 | import { creditUpdateEvent } from "@/hooks/usePayment";
5 |
6 | export function useCredits() {
7 | const { getToken } = useAuth();
8 | const baseurl = BACKEND_URL;
9 | const [credits, setCredits] = useState(0);
10 | const [loading, setLoading] = useState(true);
11 |
12 | const fetchCredits = async () => {
13 | try {
14 | setLoading(true);
15 | const token = await getToken();
16 | if (!token) return;
17 |
18 | const response = await fetch(`${baseurl}/payment/credits`, {
19 | headers: {
20 | Authorization: `Bearer ${token}`,
21 | },
22 | cache: 'no-store', // Disable caching
23 | });
24 |
25 | if (response.ok) {
26 | const data = await response.json();
27 | setCredits(data.credits);
28 | }
29 | } catch (error) {
30 | console.error("Error fetching credits:", error);
31 | } finally {
32 | setLoading(false);
33 | }
34 | };
35 |
36 | useEffect(() => {
37 | fetchCredits();
38 | const handleCreditUpdate = (event: Event) => {
39 | console.log("Credit update event received");
40 | if (event instanceof CustomEvent) {
41 | // Immediately update credits if available in event
42 | if (event.detail) {
43 | setCredits(event.detail);
44 | }
45 | }
46 | // Fetch latest credits from server
47 | fetchCredits();
48 | };
49 |
50 | // Use the creditUpdateEvent instead of window
51 | creditUpdateEvent.addEventListener("creditUpdate", handleCreditUpdate);
52 |
53 | // Refresh credits every minute
54 | const interval = setInterval(fetchCredits, 60 * 1000);
55 |
56 | return () => {
57 | creditUpdateEvent.removeEventListener("creditUpdate", handleCreditUpdate);
58 | clearInterval(interval);
59 | };
60 | }, []);
61 |
62 | return { credits, loading };
63 | }
64 |
65 |
--------------------------------------------------------------------------------
/apps/web/hooks/use-toast.ts:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from "react"
5 |
6 | import type {
7 | ToastActionElement,
8 | ToastProps,
9 | } from "@/components/ui/toast"
10 |
11 | const TOAST_LIMIT = 1
12 | const TOAST_REMOVE_DELAY = 1000000
13 |
14 | type ToasterToast = ToastProps & {
15 | id: string
16 | title?: React.ReactNode
17 | description?: React.ReactNode
18 | action?: ToastActionElement
19 | }
20 |
21 | const actionTypes = {
22 | ADD_TOAST: "ADD_TOAST",
23 | UPDATE_TOAST: "UPDATE_TOAST",
24 | DISMISS_TOAST: "DISMISS_TOAST",
25 | REMOVE_TOAST: "REMOVE_TOAST",
26 | } as const
27 |
28 | let count = 0
29 |
30 | function genId() {
31 | count = (count + 1) % Number.MAX_SAFE_INTEGER
32 | return count.toString()
33 | }
34 |
35 | type ActionType = typeof actionTypes
36 |
37 | type Action =
38 | | {
39 | type: ActionType["ADD_TOAST"]
40 | toast: ToasterToast
41 | }
42 | | {
43 | type: ActionType["UPDATE_TOAST"]
44 | toast: Partial
45 | }
46 | | {
47 | type: ActionType["DISMISS_TOAST"]
48 | toastId?: ToasterToast["id"]
49 | }
50 | | {
51 | type: ActionType["REMOVE_TOAST"]
52 | toastId?: ToasterToast["id"]
53 | }
54 |
55 | interface State {
56 | toasts: ToasterToast[]
57 | }
58 |
59 | const toastTimeouts = new Map>()
60 |
61 | const addToRemoveQueue = (toastId: string) => {
62 | if (toastTimeouts.has(toastId)) {
63 | return
64 | }
65 |
66 | const timeout = setTimeout(() => {
67 | toastTimeouts.delete(toastId)
68 | dispatch({
69 | type: "REMOVE_TOAST",
70 | toastId: toastId,
71 | })
72 | }, TOAST_REMOVE_DELAY)
73 |
74 | toastTimeouts.set(toastId, timeout)
75 | }
76 |
77 | export const reducer = (state: State, action: Action): State => {
78 | switch (action.type) {
79 | case "ADD_TOAST":
80 | return {
81 | ...state,
82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
83 | }
84 |
85 | case "UPDATE_TOAST":
86 | return {
87 | ...state,
88 | toasts: state.toasts.map((t) =>
89 | t.id === action.toast.id ? { ...t, ...action.toast } : t
90 | ),
91 | }
92 |
93 | case "DISMISS_TOAST": {
94 | const { toastId } = action
95 |
96 | // ! Side effects ! - This could be extracted into a dismissToast() action,
97 | // but I'll keep it here for simplicity
98 | if (toastId) {
99 | addToRemoveQueue(toastId)
100 | } else {
101 | state.toasts.forEach((toast) => {
102 | addToRemoveQueue(toast.id)
103 | })
104 | }
105 |
106 | return {
107 | ...state,
108 | toasts: state.toasts.map((t) =>
109 | t.id === toastId || toastId === undefined
110 | ? {
111 | ...t,
112 | open: false,
113 | }
114 | : t
115 | ),
116 | }
117 | }
118 | case "REMOVE_TOAST":
119 | if (action.toastId === undefined) {
120 | return {
121 | ...state,
122 | toasts: [],
123 | }
124 | }
125 | return {
126 | ...state,
127 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
128 | }
129 | }
130 | }
131 |
132 | const listeners: Array<(state: State) => void> = []
133 |
134 | let memoryState: State = { toasts: [] }
135 |
136 | function dispatch(action: Action) {
137 | memoryState = reducer(memoryState, action)
138 | listeners.forEach((listener) => {
139 | listener(memoryState)
140 | })
141 | }
142 |
143 | type Toast = Omit
144 |
145 | function toast({ ...props }: Toast) {
146 | const id = genId()
147 |
148 | const update = (props: ToasterToast) =>
149 | dispatch({
150 | type: "UPDATE_TOAST",
151 | toast: { ...props, id },
152 | })
153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
154 |
155 | dispatch({
156 | type: "ADD_TOAST",
157 | toast: {
158 | ...props,
159 | id,
160 | open: true,
161 | onOpenChange: (open) => {
162 | if (!open) dismiss()
163 | },
164 | },
165 | })
166 |
167 | return {
168 | id: id,
169 | dismiss,
170 | update,
171 | }
172 | }
173 |
174 | function useToast() {
175 | const [state, setState] = React.useState(memoryState)
176 |
177 | React.useEffect(() => {
178 | listeners.push(setState)
179 | return () => {
180 | const index = listeners.indexOf(setState)
181 | if (index > -1) {
182 | listeners.splice(index, 1)
183 | }
184 | }
185 | }, [state])
186 |
187 | return {
188 | ...state,
189 | toast,
190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
191 | }
192 | }
193 |
194 | export { useToast, toast }
195 |
--------------------------------------------------------------------------------
/apps/web/hooks/useAuth.ts:
--------------------------------------------------------------------------------
1 | import { useAuth as useClerkAuth, useUser } from "@clerk/nextjs";
2 |
3 | export function useAuth() {
4 | const { getToken, isSignedIn } = useClerkAuth();
5 | const { user } = useUser();
6 |
7 | return {
8 | getToken,
9 | isAuthenticated: !!isSignedIn,
10 | user,
11 | };
12 | }
13 |
--------------------------------------------------------------------------------
/apps/web/hooks/usePayment.ts:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { loadStripe } from "@stripe/stripe-js";
3 | import { useToast } from "@/hooks/use-toast";
4 | import { useAuth } from "@clerk/nextjs";
5 | import { useRouter } from "next/navigation";
6 | import { BACKEND_URL } from "@/app/config";
7 | import { RazorpayResponse } from "@/types";
8 |
9 | const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_KEY!);
10 | const apiUrl = BACKEND_URL;
11 |
12 | // Create an event bus for credit updates
13 | export const creditUpdateEvent = new EventTarget();
14 |
15 | export function usePayment() {
16 | const [isLoading, setIsLoading] = useState(false);
17 | const { toast } = useToast();
18 | const { getToken } = useAuth();
19 |
20 | const handlePayment = async (plan: "basic" | "premium", p0: boolean, p1: string) => {
21 | try {
22 | setIsLoading(true);
23 | const token = await getToken();
24 | if (!token) throw new Error("Not authenticated");
25 |
26 | const response = await fetch(`${apiUrl}/payment/create`, {
27 | method: "POST",
28 | headers: {
29 | "Content-Type": "application/json",
30 | Authorization: `Bearer ${token}`,
31 | },
32 | body: JSON.stringify({ plan, method: "razorpay" }),
33 | });
34 |
35 | const data = await response.json();
36 | if (!response.ok) throw new Error(data.message || "Payment failed");
37 |
38 | await loadRazorpayScript();
39 |
40 | const options = {
41 | key: data.key,
42 | amount: String(data.amount),
43 | currency: data.currency,
44 | name: data.name,
45 | description: data.description,
46 | order_id: data.order_id,
47 | handler: function (response: RazorpayResponse) {
48 | // Redirect to verify page with all necessary parameters
49 | const params = new URLSearchParams({
50 | razorpay_payment_id: response.razorpay_payment_id,
51 | razorpay_order_id: response.razorpay_order_id,
52 | razorpay_signature: response.razorpay_signature,
53 | plan: plan,
54 | amount: String(data.amount),
55 | });
56 | window.location.href = `/payment/verify?${params.toString()}`;
57 | },
58 | modal: {
59 | ondismiss: function () {
60 | window.location.href = "/payment/cancel";
61 | },
62 | },
63 | theme: {
64 | color: "#000000",
65 | },
66 | };
67 |
68 | const razorpay = new (window as any).Razorpay(options);
69 | razorpay.open();
70 | } catch (error) {
71 | toast({
72 | title: "Payment Error",
73 | description: "Failed to initialize payment",
74 | variant: "destructive",
75 | });
76 | window.location.href = "/payment/cancel";
77 | } finally {
78 | setIsLoading(false);
79 | }
80 | };
81 |
82 | return {
83 | handlePayment,
84 | isLoading,
85 | };
86 | }
87 |
88 | // Helper function to load Razorpay SDK
89 | function loadRazorpayScript(): Promise {
90 | return new Promise((resolve) => {
91 | if (document.getElementById("razorpay-sdk")) {
92 | resolve();
93 | return;
94 | }
95 | const script = document.createElement("script");
96 | script.id = "razorpay-sdk";
97 | script.src = "https://checkout.razorpay.com/v1/checkout.js";
98 | script.async = true;
99 | script.onload = () => resolve();
100 | document.body.appendChild(script);
101 | });
102 | }
103 |
--------------------------------------------------------------------------------
/apps/web/hooks/useTransactions.ts:
--------------------------------------------------------------------------------
1 | import { BACKEND_URL } from "@/app/config";
2 | import { Transaction } from "@/types";
3 | import axios, { AxiosError } from "axios";
4 | import { useEffect, useState } from "react";
5 | import { useAuth } from "./useAuth";
6 |
7 | export const useTransactions = () => {
8 | const { getToken } = useAuth();
9 | const [transactions, setTransactions] = useState([]);
10 | const [isLoading, setIsLoading] = useState(false);
11 | const [error, setError] = useState(null);
12 |
13 | const fetchTransactions = async () => {
14 | setIsLoading(true);
15 | const token = await getToken();
16 | if (!token) return;
17 |
18 | try {
19 | const response = await axios.get(`${BACKEND_URL}/payment/transactions`,{
20 | headers: {
21 | Authorization: `Bearer ${token}`,
22 | },
23 | });
24 | const data = response.data;
25 | setTransactions(data.transactions);
26 | } catch (error: any) {
27 | if (error instanceof AxiosError) {
28 | setError(error.response?.data.message || error.message);
29 | } else {
30 | setError(error.message || "An error occurred");
31 | }
32 | } finally {
33 | setIsLoading(false);
34 | }
35 | };
36 |
37 | useEffect(() => {
38 | fetchTransactions();
39 | }, []);
40 |
41 | return { transactions, isLoading, error };
42 | };
43 |
--------------------------------------------------------------------------------
/apps/web/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 |
--------------------------------------------------------------------------------
/apps/web/middleware.ts:
--------------------------------------------------------------------------------
1 | import { clerkMiddleware } from "@clerk/nextjs/server";
2 |
3 | export default clerkMiddleware();
4 |
5 | export const config = {
6 | matcher: [
7 | // Skip Next.js internals and all static files, unless found in search params
8 | '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
9 | // Always run for API routes
10 | '/(api|trpc)(.*)',
11 | ],
12 | };
--------------------------------------------------------------------------------
/apps/web/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | remotePatterns: [
5 | {
6 | protocol: "https",
7 | hostname: "r2-us-west.photoai.com",
8 | },
9 | {
10 | protocol: "https",
11 | hostname: "r2-us-east.photoai.com",
12 | },
13 | {
14 | protocol: "https",
15 | hostname: "i0.wp.com",
16 | },
17 | {
18 | protocol: "https",
19 | hostname: "encrypted-tbn1.gstatic.com",
20 | },
21 | {
22 | protocol: "https",
23 | hostname: "v3.fal.media",
24 | },
25 | {
26 | protocol: "https",
27 | hostname: "avatars.githubusercontent.com",
28 | },
29 | {
30 | protocol: "https",
31 | hostname: "cloudflare-ipfs.com",
32 | },
33 | {
34 | protocol: "https",
35 | hostname: "images.unsplash.com",
36 | },
37 | ],
38 | },
39 | };
40 |
41 | export default nextConfig;
42 |
--------------------------------------------------------------------------------
/apps/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web",
3 | "version": "0.1.0",
4 | "type": "module",
5 | "private": true,
6 | "scripts": {
7 | "dev": "next dev --turbopack --port 3000",
8 | "build": "next build",
9 | "start": "next start",
10 | "lint": "next lint --max-warnings 0",
11 | "check-types": "tsc --noEmit"
12 | },
13 | "dependencies": {
14 | "@clerk/themes": "^2.2.19",
15 | "@radix-ui/react-dialog": "^1.1.6",
16 | "@radix-ui/react-dropdown-menu": "^2.1.6",
17 | "@radix-ui/react-label": "^2.1.2",
18 | "@radix-ui/react-progress": "^1.1.2",
19 | "@radix-ui/react-scroll-area": "^1.2.3",
20 | "@radix-ui/react-select": "^2.1.6",
21 | "@radix-ui/react-slot": "^1.1.2",
22 | "@radix-ui/react-switch": "^1.1.3",
23 | "@radix-ui/react-tabs": "^1.1.3",
24 | "@radix-ui/react-toast": "^1.2.6",
25 | "@radix-ui/react-tooltip": "^1.1.8",
26 | "@repo/ui": "*",
27 | "@stripe/stripe-js": "^5.6.0",
28 | "@tailwindcss/postcss": "^4.0.6",
29 | "@types/jsonwebtoken": "^9.0.8",
30 | "axios": "^1.7.9",
31 | "class-variance-authority": "^0.7.1",
32 | "clsx": "^2.1.1",
33 | "common": "*",
34 | "date-fns": "^4.1.0",
35 | "embla-carousel-autoplay": "^8.5.2",
36 | "embla-carousel-react": "^8.5.2",
37 | "framer-motion": "^12.4.2",
38 | "jszip": "^3.10.1",
39 | "lucide-react": "^0.475.0",
40 | "next": "^15.1.6",
41 | "next-themes": "^0.4.4",
42 | "postcss": "^8.5.2",
43 | "react": "^19.0.0",
44 | "react-dom": "^19.0.0",
45 | "react-hot-toast": "^2.5.1",
46 | "tailwind-merge": "^3.0.1",
47 | "tailwindcss": "^4.0.6",
48 | "tailwindcss-animate": "^1.0.7"
49 | },
50 | "devDependencies": {
51 | "@repo/eslint-config": "*",
52 | "@repo/typescript-config": "*",
53 | "@types/node": "^22",
54 | "@types/react": "19.0.8",
55 | "@types/react-dom": "19.0.3",
56 | "eslint": "^9.20.0",
57 | "typescript": "5.7.3"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/apps/web/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | plugins: {
3 | "@tailwindcss/postcss": {},
4 | },
5 | };
6 | export default config;
--------------------------------------------------------------------------------
/apps/web/public/file-text.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/apps/web/public/globe.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/apps/web/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/web/public/turborepo-dark.svg:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------
/apps/web/public/turborepo-light.svg:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------
/apps/web/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/apps/web/public/window.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/apps/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/nextjs.json",
3 | "compilerOptions": {
4 | "plugins": [
5 | {
6 | "name": "next"
7 | }
8 | ],
9 |
10 | "baseUrl": ".",
11 | "paths": {
12 | "@/*": [
13 | "./*"
14 | ]
15 | }
16 | },
17 | "include": [
18 | "**/*.ts",
19 | "**/*.tsx",
20 | "next-env.d.ts",
21 | "next.config.js",
22 | ".next/types/**/*.ts"
23 | ],
24 | "exclude": [
25 | "node_modules"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/apps/web/types/index.ts:
--------------------------------------------------------------------------------
1 | export enum PlanType {
2 | basic = "basic",
3 | premium = "premium",
4 | }
5 |
6 | export interface PaymentResponse {
7 | sessionId?: string;
8 | url?: string;
9 | id?: string;
10 | amount?: number;
11 | currency?: string;
12 | success?: boolean;
13 | message?: string;
14 | }
15 |
16 | export interface RazorpayResponse {
17 | razorpay_payment_id: string;
18 | razorpay_order_id: string;
19 | razorpay_signature: string;
20 | }
21 |
22 | export interface SubscriptionStatus {
23 | plan: PlanType;
24 | createdAt: string;
25 | credits: number;
26 | }
27 |
28 | export enum TransactionStatus {
29 | SUCCESS = "SUCCESS",
30 | FAILED = "FAILED",
31 | PENDING = "PENDING",
32 | }
33 |
34 | export interface Transaction {
35 | id: string;
36 | userId: string;
37 | amount: number;
38 | currency: string;
39 | paymentId: string;
40 | orderId: string;
41 | plan: PlanType;
42 | isAnnual: boolean;
43 | status: TransactionStatus;
44 | createdAt: string;
45 | updatedAt: string;
46 | }
47 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/photo-ai/752707e61d8ad2e1abd0189ebc30668232c82a1d/bun.lockb
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | postgres:
3 | image: postgres
4 | ports:
5 | - 5432:5432
6 | restart: always
7 | environment:
8 | - POSTGRES_PASSWORD = mypassword
9 | - POSTGRES_USER = myuser
10 | - POSTGRES_HOST_AUTH_METHOD=trust
11 | volumes:
12 | - pgdata:/var/lib/postgresql
13 |
14 | backend:
15 | build:
16 | context: .
17 | dockerfile: docker/Dockerfile.backend
18 | restart: always
19 | container_name: backend-service
20 | ports:
21 | - 8080:8080
22 | environment:
23 | - FAL_KEY=""
24 | - S3_ACCESS_KEY=""
25 | - S3_SECRET_KEY=""
26 | - BUCKET_NAME=""
27 | - ENDPOINT=""
28 | - RAZORPAY_KEY_ID=""
29 | - RAZORPAY_KEY_SECRET=""
30 | - SIGNING_SECRET=""
31 | - CLERK_JWT_PUBLIC_KEY=""
32 | - WEBHOOK_BASE_URL=""
33 | - FRONTEND_URL=""
34 | - CLERK_SECRET_KEY=
35 | - CLERK_ISSUER=
36 | - STRIPE_SECRET_KEY=""
37 | depends_on:
38 | - postgres
39 |
40 | web:
41 | build:
42 | context: .
43 | dockerfile: docker/Dockerfile.frontend
44 | restart: always
45 | container_name: frontend-service
46 | ports:
47 | - 3000:3000
48 | environment:
49 | - NEXT_PUBLIC_BACKEND_URL=http://backend:8080
50 | - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
51 | - CLERK_SECRET_KEY=
52 | depends_on:
53 | - backend
54 |
55 | volumes:
56 | pgdata:
57 |
--------------------------------------------------------------------------------
/docker/Dockerfile.backend:
--------------------------------------------------------------------------------
1 | FROM oven/bun:1
2 |
3 | WORKDIR /usr/src/app
4 |
5 | COPY package.json bun.lockb ./
6 |
7 | RUN bun install
8 |
9 | COPY . .
10 |
11 |
12 | RUN bunx turbo build --filter=backend...
13 | RUN bun run generate:db
14 |
15 | EXPOSE 8080
16 |
17 | # Start the frontend application
18 | CMD ["bun", "start:backend"]
19 |
--------------------------------------------------------------------------------
/docker/Dockerfile.frontend:
--------------------------------------------------------------------------------
1 | FROM oven/bun:1
2 |
3 | ARG CLERK_PUBLISHABLE_KEY
4 |
5 | WORKDIR /usr/src/app
6 |
7 | COPY . .
8 |
9 | RUN bun install
10 |
11 | RUN NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_Y2xlcmsuMTAweGRldnMuY29tJA NEXT_PUBLIC_BACKEND_URL=https://api.photoaiv2.100xdevs.com NEXT_PUBLIC_STRIPE_KEY=pk_test_51QsCmFEI53oUr5PHZw5ErO4Xy2lNh9LkH9vXDb8wc7BOvfSPc0i4xt6I5Qy3jaBLnvg9wPenPoeW0LvQ1x3GtfUm00eNFHdBDd NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_Y2xlcmsuMTAweGRldnMuY29tJA bunx turbo build --filter=web...
12 |
13 | ENV NODE_ENV production
14 |
15 | EXPOSE 3000
16 |
17 | CMD ["bun", "start:web"]
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "image-generation-platform",
3 | "private": true,
4 | "scripts": {
5 | "build": "turbo build",
6 | "dev": "turbo dev",
7 | "lint": "turbo lint",
8 | "format": "prettier --write \"**/*.{ts,tsx,md}\"",
9 | "start:web": "cd ./apps/web && npm run start",
10 | "start:backend": "cd ./apps/backend && npm run start",
11 | "generate:db": "cd ./packages/db && npx prisma generate && cd ../.."
12 | },
13 | "devDependencies": {
14 | "prettier": "^3.5.0",
15 | "turbo": "^2.4.1",
16 | "typescript": "5.7.3"
17 | },
18 | "engines": {
19 | "node": ">=18"
20 | },
21 | "packageManager": "bun@1.1.26",
22 | "workspaces": [
23 | "apps/*",
24 | "packages/*"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/packages/common/.gitignore:
--------------------------------------------------------------------------------
1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
2 |
3 | # Logs
4 |
5 | logs
6 | _.log
7 | npm-debug.log_
8 | yarn-debug.log*
9 | yarn-error.log*
10 | lerna-debug.log*
11 | .pnpm-debug.log*
12 |
13 | # Caches
14 |
15 | .cache
16 |
17 | # Diagnostic reports (https://nodejs.org/api/report.html)
18 |
19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
20 |
21 | # Runtime data
22 |
23 | pids
24 | _.pid
25 | _.seed
26 | *.pid.lock
27 |
28 | # Directory for instrumented libs generated by jscoverage/JSCover
29 |
30 | lib-cov
31 |
32 | # Coverage directory used by tools like istanbul
33 |
34 | coverage
35 | *.lcov
36 |
37 | # nyc test coverage
38 |
39 | .nyc_output
40 |
41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
42 |
43 | .grunt
44 |
45 | # Bower dependency directory (https://bower.io/)
46 |
47 | bower_components
48 |
49 | # node-waf configuration
50 |
51 | .lock-wscript
52 |
53 | # Compiled binary addons (https://nodejs.org/api/addons.html)
54 |
55 | build/Release
56 |
57 | # Dependency directories
58 |
59 | node_modules/
60 | jspm_packages/
61 |
62 | # Snowpack dependency directory (https://snowpack.dev/)
63 |
64 | web_modules/
65 |
66 | # TypeScript cache
67 |
68 | *.tsbuildinfo
69 |
70 | # Optional npm cache directory
71 |
72 | .npm
73 |
74 | # Optional eslint cache
75 |
76 | .eslintcache
77 |
78 | # Optional stylelint cache
79 |
80 | .stylelintcache
81 |
82 | # Microbundle cache
83 |
84 | .rpt2_cache/
85 | .rts2_cache_cjs/
86 | .rts2_cache_es/
87 | .rts2_cache_umd/
88 |
89 | # Optional REPL history
90 |
91 | .node_repl_history
92 |
93 | # Output of 'npm pack'
94 |
95 | *.tgz
96 |
97 | # Yarn Integrity file
98 |
99 | .yarn-integrity
100 |
101 | # dotenv environment variable files
102 |
103 | .env
104 | .env.development.local
105 | .env.test.local
106 | .env.production.local
107 | .env.local
108 |
109 | # parcel-bundler cache (https://parceljs.org/)
110 |
111 | .parcel-cache
112 |
113 | # Next.js build output
114 |
115 | .next
116 | out
117 |
118 | # Nuxt.js build / generate output
119 |
120 | .nuxt
121 | dist
122 |
123 | # Gatsby files
124 |
125 | # Comment in the public line in if your project uses Gatsby and not Next.js
126 |
127 | # https://nextjs.org/blog/next-9-1#public-directory-support
128 |
129 | # public
130 |
131 | # vuepress build output
132 |
133 | .vuepress/dist
134 |
135 | # vuepress v2.x temp and cache directory
136 |
137 | .temp
138 |
139 | # Docusaurus cache and generated files
140 |
141 | .docusaurus
142 |
143 | # Serverless directories
144 |
145 | .serverless/
146 |
147 | # FuseBox cache
148 |
149 | .fusebox/
150 |
151 | # DynamoDB Local files
152 |
153 | .dynamodb/
154 |
155 | # TernJS port file
156 |
157 | .tern-port
158 |
159 | # Stores VSCode versions used for testing VSCode extensions
160 |
161 | .vscode-test
162 |
163 | # yarn v2
164 |
165 | .yarn/cache
166 | .yarn/unplugged
167 | .yarn/build-state.yml
168 | .yarn/install-state.gz
169 | .pnp.*
170 |
171 | # IntelliJ based IDEs
172 | .idea
173 |
174 | # Finder (MacOS) folder config
175 | .DS_Store
176 |
--------------------------------------------------------------------------------
/packages/common/README.md:
--------------------------------------------------------------------------------
1 | # common
2 |
3 | To install dependencies:
4 |
5 | ```bash
6 | bun install
7 | ```
8 |
9 | To run:
10 |
11 | ```bash
12 | bun run index.ts
13 | ```
14 |
15 | This project was created using `bun init` in bun v1.1.26. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
16 |
--------------------------------------------------------------------------------
/packages/common/index.ts:
--------------------------------------------------------------------------------
1 | console.log("Hello via Bun!");
--------------------------------------------------------------------------------
/packages/common/inferred-types.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import {TrainModel, GenerateImage, GenerateImagesFromPack} from "./types";
3 |
4 | export type TrainModelInput = z.infer;
5 | export type GenerateImageInput = z.infer;
6 | export type GenerateImagesFromPackInput = z.infer;
7 |
--------------------------------------------------------------------------------
/packages/common/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "common",
3 | "module": "index.ts",
4 | "type": "module",
5 | "exports": {
6 | "./types": "./types.ts",
7 | "./inferred": "./inferred-types.ts"
8 | },
9 | "devDependencies": {
10 | "@types/bun": "latest"
11 | },
12 | "peerDependencies": {
13 | "typescript": "^5.0.0"
14 | },
15 | "dependencies": {
16 | "zod": "^3.24.1"
17 | }
18 | }
--------------------------------------------------------------------------------
/packages/common/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // Enable latest features
4 | "lib": ["ESNext", "DOM"],
5 | "target": "ESNext",
6 | "module": "ESNext",
7 | "moduleDetection": "force",
8 | "jsx": "react-jsx",
9 | "allowJs": true,
10 |
11 | // Bundler mode
12 | "moduleResolution": "bundler",
13 | "allowImportingTsExtensions": true,
14 | "verbatimModuleSyntax": true,
15 | "noEmit": true,
16 |
17 | // Best practices
18 | "strict": true,
19 | "skipLibCheck": true,
20 | "noFallthroughCasesInSwitch": true,
21 |
22 | // Some stricter flags (disabled by default)
23 | "noUnusedLocals": false,
24 | "noUnusedParameters": false,
25 | "noPropertyAccessFromIndexSignature": false
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/common/types.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const TrainModel = z.object({
4 | name: z.string(),
5 | type: z.enum(["Man", "Woman", "Others"]),
6 | age: z.number(),
7 | ethinicity: z.enum(["White",
8 | "Black",
9 | "Asian_American",
10 | "East_Asian",
11 | "South_East_Asian",
12 | "South_Asian",
13 | "Middle_Eastern",
14 | "Pacific",
15 | "Hispanic"
16 | ]),
17 | eyeColor: z.enum(["Brown", "Blue", "Hazel", "Gray"]),
18 | bald: z.boolean(),
19 | zipUrl: z.string()
20 | })
21 |
22 | export const GenerateImage = z.object({
23 | prompt: z.string(),
24 | modelId: z.string(),
25 | num: z.number()
26 | })
27 |
28 | export const GenerateImagesFromPack = z.object({
29 | modelId: z.string(),
30 | packId: z.string()
31 | })
--------------------------------------------------------------------------------
/packages/db/.gitignore:
--------------------------------------------------------------------------------
1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
2 |
3 | # Logs
4 |
5 | logs
6 | _.log
7 | npm-debug.log_
8 | yarn-debug.log*
9 | yarn-error.log*
10 | lerna-debug.log*
11 | .pnpm-debug.log*
12 |
13 | # Caches
14 |
15 | .cache
16 |
17 | # Diagnostic reports (https://nodejs.org/api/report.html)
18 |
19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
20 |
21 | # Runtime data
22 |
23 | pids
24 | _.pid
25 | _.seed
26 | *.pid.lock
27 |
28 | # Directory for instrumented libs generated by jscoverage/JSCover
29 |
30 | lib-cov
31 |
32 | # Coverage directory used by tools like istanbul
33 |
34 | coverage
35 | *.lcov
36 |
37 | # nyc test coverage
38 |
39 | .nyc_output
40 |
41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
42 |
43 | .grunt
44 |
45 | # Bower dependency directory (https://bower.io/)
46 |
47 | bower_components
48 |
49 | # node-waf configuration
50 |
51 | .lock-wscript
52 |
53 | # Compiled binary addons (https://nodejs.org/api/addons.html)
54 |
55 | build/Release
56 |
57 | # Dependency directories
58 |
59 | node_modules/
60 | jspm_packages/
61 |
62 | # Snowpack dependency directory (https://snowpack.dev/)
63 |
64 | web_modules/
65 |
66 | # TypeScript cache
67 |
68 | *.tsbuildinfo
69 |
70 | # Optional npm cache directory
71 |
72 | .npm
73 |
74 | # Optional eslint cache
75 |
76 | .eslintcache
77 |
78 | # Optional stylelint cache
79 |
80 | .stylelintcache
81 |
82 | # Microbundle cache
83 |
84 | .rpt2_cache/
85 | .rts2_cache_cjs/
86 | .rts2_cache_es/
87 | .rts2_cache_umd/
88 |
89 | # Optional REPL history
90 |
91 | .node_repl_history
92 |
93 | # Output of 'npm pack'
94 |
95 | *.tgz
96 |
97 | # Yarn Integrity file
98 |
99 | .yarn-integrity
100 |
101 | # dotenv environment variable files
102 |
103 | .env
104 | .env.development.local
105 | .env.test.local
106 | .env.production.local
107 | .env.local
108 |
109 | # parcel-bundler cache (https://parceljs.org/)
110 |
111 | .parcel-cache
112 |
113 | # Next.js build output
114 |
115 | .next
116 | out
117 |
118 | # Nuxt.js build / generate output
119 |
120 | .nuxt
121 | dist
122 |
123 | # Gatsby files
124 |
125 | # Comment in the public line in if your project uses Gatsby and not Next.js
126 |
127 | # https://nextjs.org/blog/next-9-1#public-directory-support
128 |
129 | # public
130 |
131 | # vuepress build output
132 |
133 | .vuepress/dist
134 |
135 | # vuepress v2.x temp and cache directory
136 |
137 | .temp
138 |
139 | # Docusaurus cache and generated files
140 |
141 | .docusaurus
142 |
143 | # Serverless directories
144 |
145 | .serverless/
146 |
147 | # FuseBox cache
148 |
149 | .fusebox/
150 |
151 | # DynamoDB Local files
152 |
153 | .dynamodb/
154 |
155 | # TernJS port file
156 |
157 | .tern-port
158 |
159 | # Stores VSCode versions used for testing VSCode extensions
160 |
161 | .vscode-test
162 |
163 | # yarn v2
164 |
165 | .yarn/cache
166 | .yarn/unplugged
167 | .yarn/build-state.yml
168 | .yarn/install-state.gz
169 | .pnp.*
170 |
171 | # IntelliJ based IDEs
172 | .idea
173 |
174 | # Finder (MacOS) folder config
175 | .DS_Store
176 |
--------------------------------------------------------------------------------
/packages/db/README.md:
--------------------------------------------------------------------------------
1 | # db
2 |
3 | To install dependencies:
4 |
5 | ```bash
6 | bun install
7 | ```
8 |
9 | To run:
10 |
11 | ```bash
12 | bun run index.ts
13 | ```
14 |
15 | This project was created using `bun init` in bun v1.1.26. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
16 |
--------------------------------------------------------------------------------
/packages/db/index.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 | // convert this to a singleton for nextjs
3 | export const prismaClient = new PrismaClient();
4 |
--------------------------------------------------------------------------------
/packages/db/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "db",
3 | "module": "index.ts",
4 | "type": "module",
5 | "exports": {
6 | ".": "./index.ts"
7 | },
8 | "devDependencies": {
9 | "@types/bun": "latest",
10 | "prisma": "^6.3.1"
11 | },
12 | "peerDependencies": {
13 | "typescript": "^5.0.0"
14 | },
15 | "dependencies": {
16 | "@prisma/client": "6.3.1"
17 | }
18 | }
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20250211165544_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateEnum
2 | CREATE TYPE "ModelTypeEnum" AS ENUM ('Man', 'Woman', 'Others');
3 |
4 | -- CreateEnum
5 | CREATE TYPE "EthenecityEnum" AS ENUM ('White', 'Black', 'AsianAmerican', 'EastAsian', 'SouthEastAsian', 'SouthAsian', 'MiddleEastern', 'Pacific', 'Hispanic');
6 |
7 | -- CreateEnum
8 | CREATE TYPE "EyeColorEnum" AS ENUM ('Brown', 'lue', 'azel', 'Gray');
9 |
10 | -- CreateTable
11 | CREATE TABLE "User" (
12 | "id" TEXT NOT NULL,
13 | "username" TEXT NOT NULL,
14 | "profilePicture" TEXT,
15 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
16 | "updatedAt" TIMESTAMP(3) NOT NULL,
17 |
18 | CONSTRAINT "User_pkey" PRIMARY KEY ("id")
19 | );
20 |
21 | -- CreateTable
22 | CREATE TABLE "Model" (
23 | "id" TEXT NOT NULL,
24 | "name" TEXT NOT NULL,
25 | "type" "ModelTypeEnum" NOT NULL,
26 | "age" INTEGER NOT NULL,
27 | "ethinicity" "EthenecityEnum" NOT NULL,
28 | "eyeColor" "EyeColorEnum" NOT NULL,
29 | "bald" BOOLEAN NOT NULL,
30 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
31 | "updatedAt" TIMESTAMP(3) NOT NULL,
32 |
33 | CONSTRAINT "Model_pkey" PRIMARY KEY ("id")
34 | );
35 |
36 | -- CreateTable
37 | CREATE TABLE "TrainingImages" (
38 | "id" TEXT NOT NULL,
39 | "imageUrl" TEXT NOT NULL,
40 | "modelId" TEXT NOT NULL,
41 |
42 | CONSTRAINT "TrainingImages_pkey" PRIMARY KEY ("id")
43 | );
44 |
45 | -- CreateTable
46 | CREATE TABLE "OutputImages" (
47 | "id" TEXT NOT NULL,
48 | "imageUrl" TEXT NOT NULL,
49 | "modelId" TEXT NOT NULL,
50 | "userId" TEXT NOT NULL,
51 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
52 | "updatedAt" TIMESTAMP(3) NOT NULL,
53 |
54 | CONSTRAINT "OutputImages_pkey" PRIMARY KEY ("id")
55 | );
56 |
57 | -- CreateTable
58 | CREATE TABLE "Packs" (
59 | "id" TEXT NOT NULL,
60 | "name" TEXT NOT NULL,
61 |
62 | CONSTRAINT "Packs_pkey" PRIMARY KEY ("id")
63 | );
64 |
65 | -- CreateTable
66 | CREATE TABLE "PackPrompts" (
67 | "id" TEXT NOT NULL,
68 | "prompt" TEXT NOT NULL,
69 | "packId" TEXT NOT NULL,
70 |
71 | CONSTRAINT "PackPrompts_pkey" PRIMARY KEY ("id")
72 | );
73 |
74 | -- AddForeignKey
75 | ALTER TABLE "TrainingImages" ADD CONSTRAINT "TrainingImages_modelId_fkey" FOREIGN KEY ("modelId") REFERENCES "Model"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
76 |
77 | -- AddForeignKey
78 | ALTER TABLE "OutputImages" ADD CONSTRAINT "OutputImages_modelId_fkey" FOREIGN KEY ("modelId") REFERENCES "Model"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
79 |
80 | -- AddForeignKey
81 | ALTER TABLE "PackPrompts" ADD CONSTRAINT "PackPrompts_packId_fkey" FOREIGN KEY ("packId") REFERENCES "Packs"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
82 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20250211172909_/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - The values [AsianAmerican,EastAsian,SouthEastAsian,SouthAsian,MiddleEastern] on the enum `EthenecityEnum` will be removed. If these variants are still used in the database, this will fail.
5 |
6 | */
7 | -- AlterEnum
8 | BEGIN;
9 | CREATE TYPE "EthenecityEnum_new" AS ENUM ('White', 'Black', 'Asian American', 'East Asian', 'South East Asian', 'South Asian', 'Middle Eastern', 'Pacific', 'Hispanic');
10 | ALTER TABLE "Model" ALTER COLUMN "ethinicity" TYPE "EthenecityEnum_new" USING ("ethinicity"::text::"EthenecityEnum_new");
11 | ALTER TYPE "EthenecityEnum" RENAME TO "EthenecityEnum_old";
12 | ALTER TYPE "EthenecityEnum_new" RENAME TO "EthenecityEnum";
13 | DROP TYPE "EthenecityEnum_old";
14 | COMMIT;
15 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20250211173124_change_color_enunm/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - The values [lue,azel] on the enum `EyeColorEnum` will be removed. If these variants are still used in the database, this will fail.
5 |
6 | */
7 | -- AlterEnum
8 | BEGIN;
9 | CREATE TYPE "EyeColorEnum_new" AS ENUM ('Brown', 'Blue', 'Hazel', 'Gray');
10 | ALTER TABLE "Model" ALTER COLUMN "eyeColor" TYPE "EyeColorEnum_new" USING ("eyeColor"::text::"EyeColorEnum_new");
11 | ALTER TYPE "EyeColorEnum" RENAME TO "EyeColorEnum_old";
12 | ALTER TYPE "EyeColorEnum_new" RENAME TO "EyeColorEnum";
13 | DROP TYPE "EyeColorEnum_old";
14 | COMMIT;
15 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20250211173427_added_user_id_field/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - Added the required column `userId` to the `Model` table without a default value. This is not possible if the table is not empty.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE "Model" ADD COLUMN "userId" TEXT NOT NULL;
9 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20250211173843_added_status/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - Added the required column `prompt` to the `OutputImages` table without a default value. This is not possible if the table is not empty.
5 |
6 | */
7 | -- CreateEnum
8 | CREATE TYPE "OutputImageStatusEnum" AS ENUM ('Pending', 'Generated', 'Failed');
9 |
10 | -- AlterTable
11 | ALTER TABLE "OutputImages" ADD COLUMN "prompt" TEXT NOT NULL,
12 | ADD COLUMN "status" "OutputImageStatusEnum" NOT NULL DEFAULT 'Pending';
13 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20250211205852_added_fal_ai/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateEnum
2 | CREATE TYPE "ModelTrainingStatusEnum" AS ENUM ('Pending', 'Generated', 'Failed');
3 |
4 | -- AlterTable
5 | ALTER TABLE "Model" ADD COLUMN "falAiRequestId" TEXT,
6 | ADD COLUMN "tensorPath" TEXT,
7 | ADD COLUMN "trainingStatus" "ModelTrainingStatusEnum" NOT NULL DEFAULT 'Pending',
8 | ADD COLUMN "triggerWord" TEXT;
9 |
10 | -- AlterTable
11 | ALTER TABLE "OutputImages" ADD COLUMN "falAiRequestId" TEXT,
12 | ALTER COLUMN "imageUrl" SET DEFAULT '';
13 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20250211210635_added_index/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateIndex
2 | CREATE INDEX "Model_falAiRequestId_idx" ON "Model"("falAiRequestId");
3 |
4 | -- CreateIndex
5 | CREATE INDEX "OutputImages_falAiRequestId_idx" ON "OutputImages"("falAiRequestId");
6 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20250211214338_init_db/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the `TrainingImages` table. If the table is not empty, all the data it contains will be lost.
5 | - Added the required column `zipUrl` to the `Model` table without a default value. This is not possible if the table is not empty.
6 |
7 | */
8 | -- DropForeignKey
9 | ALTER TABLE "TrainingImages" DROP CONSTRAINT "TrainingImages_modelId_fkey";
10 |
11 | -- AlterTable
12 | ALTER TABLE "Model" ADD COLUMN "zipUrl" TEXT NOT NULL;
13 |
14 | -- DropTable
15 | DROP TABLE "TrainingImages";
16 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20250212020444_/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Packs" ADD COLUMN "description" TEXT NOT NULL DEFAULT '',
3 | ADD COLUMN "imageUrl" TEXT NOT NULL DEFAULT '';
4 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20250212020828_added_image/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `imageUrl` on the `Packs` table. All the data in the column will be lost.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE "Packs" DROP COLUMN "imageUrl",
9 | ADD COLUMN "imageUrl1" TEXT NOT NULL DEFAULT '',
10 | ADD COLUMN "imageUrl2" TEXT NOT NULL DEFAULT '';
11 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20250212025528_thumbnail/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Model" ADD COLUMN "thumbnail" TEXT;
3 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20250212031625_added_open_models/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Model" ADD COLUMN "open" BOOLEAN NOT NULL DEFAULT false;
3 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20250213231325_subscription/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateEnum
2 | CREATE TYPE "PlanType" AS ENUM ('basic', 'premium');
3 |
4 | -- CreateTable
5 | CREATE TABLE "Subscription" (
6 | "id" TEXT NOT NULL,
7 | "userId" TEXT NOT NULL,
8 | "plan" "PlanType" NOT NULL,
9 | "paymentId" TEXT NOT NULL,
10 | "orderId" TEXT NOT NULL,
11 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
12 | "updatedAt" TIMESTAMP(3) NOT NULL,
13 |
14 | CONSTRAINT "Subscription_pkey" PRIMARY KEY ("id")
15 | );
16 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20250213231834_usercredit/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "UserCredit" (
3 | "id" TEXT NOT NULL,
4 | "userId" TEXT NOT NULL,
5 | "amount" INTEGER NOT NULL DEFAULT 0,
6 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
7 | "updatedAt" TIMESTAMP(3) NOT NULL,
8 |
9 | CONSTRAINT "UserCredit_pkey" PRIMARY KEY ("id")
10 | );
11 |
12 | -- CreateIndex
13 | CREATE UNIQUE INDEX "UserCredit_userId_key" ON "UserCredit"("userId");
14 |
15 | -- CreateIndex
16 | CREATE INDEX "UserCredit_userId_idx" ON "UserCredit"("userId");
17 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20250216194913_user_table_updated/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `username` on the `User` table. All the data in the column will be lost.
5 | - Added the required column `email` to the `User` table without a default value. This is not possible if the table is not empty.
6 |
7 | */
8 | -- AlterTable
9 | ALTER TABLE "User" DROP COLUMN "username",
10 | ADD COLUMN "clerkId" TEXT,
11 | ADD COLUMN "email" TEXT NOT NULL,
12 | ADD COLUMN "name" TEXT;
13 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20250216195632_user_table_updated/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - A unique constraint covering the columns `[clerkId]` on the table `User` will be added. If there are existing duplicate values, this will fail.
5 | - A unique constraint covering the columns `[email]` on the table `User` will be added. If there are existing duplicate values, this will fail.
6 | - Made the column `clerkId` on table `User` required. This step will fail if there are existing NULL values in that column.
7 |
8 | */
9 | -- AlterTable
10 | ALTER TABLE "User" ALTER COLUMN "clerkId" SET NOT NULL;
11 |
12 | -- CreateIndex
13 | CREATE UNIQUE INDEX "User_clerkId_key" ON "User"("clerkId");
14 |
15 | -- CreateIndex
16 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
17 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20250224171635_transaction/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateEnum
2 | CREATE TYPE "TransactionStatus" AS ENUM ('PENDING', 'SUCCESS', 'FAILED');
3 |
4 | -- CreateTable
5 | CREATE TABLE "Transaction" (
6 | "id" TEXT NOT NULL,
7 | "userId" TEXT NOT NULL,
8 | "amount" INTEGER NOT NULL,
9 | "currency" TEXT NOT NULL,
10 | "paymentId" TEXT NOT NULL,
11 | "orderId" TEXT NOT NULL,
12 | "plan" "PlanType" NOT NULL,
13 | "isAnnual" BOOLEAN NOT NULL,
14 | "status" "TransactionStatus" NOT NULL DEFAULT 'PENDING',
15 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
16 | "updatedAt" TIMESTAMP(3) NOT NULL,
17 |
18 | CONSTRAINT "Transaction_pkey" PRIMARY KEY ("id")
19 | );
20 |
21 | -- CreateIndex
22 | CREATE INDEX "Transaction_userId_idx" ON "Transaction"("userId");
23 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/20250226214307_removed_annual/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `isAnnual` on the `Transaction` table. All the data in the column will be lost.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE "Transaction" DROP COLUMN "isAnnual";
9 |
--------------------------------------------------------------------------------
/packages/db/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (e.g., Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/packages/db/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 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
6 |
7 | generator client {
8 | provider = "prisma-client-js"
9 | }
10 |
11 | datasource db {
12 | provider = "postgresql"
13 | url = env("DATABASE_URL")
14 | }
15 |
16 | model User {
17 | id String @id @default(uuid())
18 | clerkId String @unique
19 | email String @unique
20 | name String?
21 | profilePicture String?
22 | createdAt DateTime @default(now())
23 | updatedAt DateTime @updatedAt
24 | }
25 |
26 | enum ModelTrainingStatusEnum {
27 | Pending
28 | Generated
29 | Failed
30 | }
31 |
32 | model Model {
33 | id String @id @default(uuid())
34 | name String
35 | type ModelTypeEnum
36 | age Int
37 | ethinicity EthenecityEnum
38 | eyeColor EyeColorEnum
39 | bald Boolean
40 | userId String
41 | triggerWord String?
42 | tensorPath String?
43 | thumbnail String?
44 | trainingStatus ModelTrainingStatusEnum @default(Pending)
45 | outputImages OutputImages[]
46 | createdAt DateTime @default(now())
47 | updatedAt DateTime @updatedAt
48 | falAiRequestId String?
49 | zipUrl String
50 | open Boolean @default(false)
51 | @@index([falAiRequestId])
52 | }
53 |
54 | enum OutputImageStatusEnum {
55 | Pending
56 | Generated
57 | Failed
58 | }
59 |
60 | model OutputImages {
61 | id String @id @default(uuid())
62 | imageUrl String @default("")
63 | modelId String
64 | userId String
65 | prompt String
66 | falAiRequestId String?
67 | status OutputImageStatusEnum @default(Pending)
68 | model Model @relation(fields: [modelId], references: [id])
69 | createdAt DateTime @default(now())
70 | updatedAt DateTime @updatedAt
71 | @@index([falAiRequestId])
72 | }
73 |
74 | model Packs {
75 | id String @id @default(uuid())
76 | name String
77 | description String @default("")
78 | imageUrl1 String @default("")
79 | imageUrl2 String @default("")
80 | prompts PackPrompts[]
81 | }
82 |
83 | model PackPrompts {
84 | id String @id @default(uuid())
85 | prompt String
86 | packId String
87 | pack Packs @relation(fields: [packId], references: [id])
88 | }
89 |
90 | model Subscription {
91 | id String @id @default(cuid())
92 | userId String
93 | plan PlanType
94 | paymentId String
95 | orderId String
96 | createdAt DateTime @default(now())
97 | updatedAt DateTime @updatedAt
98 | }
99 |
100 | enum PlanType {
101 | basic
102 | premium
103 | }
104 |
105 | enum ModelTypeEnum {
106 | Man
107 | Woman
108 | Others
109 | }
110 |
111 | enum EthenecityEnum {
112 | White
113 | Black
114 | Asian_American @map("Asian American")
115 | East_Asian @map("East Asian")
116 | South_East_Asian @map("South East Asian")
117 | South_Asian @map("South Asian")
118 | Middle_Eastern @map("Middle Eastern")
119 | Pacific
120 | Hispanic
121 | }
122 |
123 | enum EyeColorEnum {
124 | Brown
125 | Blue
126 | Hazel
127 | Gray
128 | }
129 |
130 | model UserCredit {
131 | id String @id @default(cuid())
132 | userId String @unique
133 | amount Int @default(0)
134 | createdAt DateTime @default(now())
135 | updatedAt DateTime @updatedAt
136 |
137 | @@index([userId])
138 | }
139 |
140 | enum TransactionStatus {
141 | PENDING
142 | SUCCESS
143 | FAILED
144 | }
145 |
146 | model Transaction {
147 | id String @id @default(cuid())
148 | userId String
149 | amount Int
150 | currency String
151 | paymentId String
152 | orderId String
153 | plan PlanType
154 | status TransactionStatus @default(PENDING)
155 | createdAt DateTime @default(now())
156 | updatedAt DateTime @updatedAt
157 |
158 | @@index([userId])
159 | }
--------------------------------------------------------------------------------
/packages/db/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // Enable latest features
4 | "lib": ["ESNext", "DOM"],
5 | "target": "ESNext",
6 | "module": "ESNext",
7 | "moduleDetection": "force",
8 | "jsx": "react-jsx",
9 | "allowJs": true,
10 |
11 | // Bundler mode
12 | "moduleResolution": "bundler",
13 | "allowImportingTsExtensions": true,
14 | "verbatimModuleSyntax": true,
15 | "noEmit": true,
16 |
17 | // Best practices
18 | "strict": true,
19 | "skipLibCheck": true,
20 | "noFallthroughCasesInSwitch": true,
21 |
22 | // Some stricter flags (disabled by default)
23 | "noUnusedLocals": false,
24 | "noUnusedParameters": false,
25 | "noPropertyAccessFromIndexSignature": false
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/packages/eslint-config/README.md:
--------------------------------------------------------------------------------
1 | # `@turbo/eslint-config`
2 |
3 | Collection of internal eslint configurations.
4 |
--------------------------------------------------------------------------------
/packages/eslint-config/base.js:
--------------------------------------------------------------------------------
1 | import js from "@eslint/js";
2 | import eslintConfigPrettier from "eslint-config-prettier";
3 | import turboPlugin from "eslint-plugin-turbo";
4 | import tseslint from "typescript-eslint";
5 | import onlyWarn from "eslint-plugin-only-warn";
6 |
7 | /**
8 | * A shared ESLint configuration for the repository.
9 | *
10 | * @type {import("eslint").Linter.Config}
11 | * */
12 | export const config = [
13 | js.configs.recommended,
14 | eslintConfigPrettier,
15 | ...tseslint.configs.recommended,
16 | {
17 | plugins: {
18 | turbo: turboPlugin,
19 | },
20 | rules: {
21 | "turbo/no-undeclared-env-vars": "warn",
22 | },
23 | },
24 | {
25 | plugins: {
26 | onlyWarn,
27 | },
28 | },
29 | {
30 | ignores: ["dist/**"],
31 | },
32 | ];
33 |
--------------------------------------------------------------------------------
/packages/eslint-config/next.js:
--------------------------------------------------------------------------------
1 | import js from "@eslint/js";
2 | import eslintConfigPrettier from "eslint-config-prettier";
3 | import tseslint from "typescript-eslint";
4 | import pluginReactHooks from "eslint-plugin-react-hooks";
5 | import pluginReact from "eslint-plugin-react";
6 | import globals from "globals";
7 | import pluginNext from "@next/eslint-plugin-next";
8 | import { config as baseConfig } from "./base.js";
9 |
10 | /**
11 | * A custom ESLint configuration for libraries that use Next.js.
12 | *
13 | * @type {import("eslint").Linter.Config}
14 | * */
15 | export const nextJsConfig = [
16 | ...baseConfig,
17 | js.configs.recommended,
18 | eslintConfigPrettier,
19 | ...tseslint.configs.recommended,
20 | {
21 | ...pluginReact.configs.flat.recommended,
22 | languageOptions: {
23 | ...pluginReact.configs.flat.recommended.languageOptions,
24 | globals: {
25 | ...globals.serviceworker,
26 | },
27 | },
28 | },
29 | {
30 | plugins: {
31 | "@next/next": pluginNext,
32 | },
33 | rules: {
34 | ...pluginNext.configs.recommended.rules,
35 | ...pluginNext.configs["core-web-vitals"].rules,
36 | },
37 | },
38 | {
39 | plugins: {
40 | "react-hooks": pluginReactHooks,
41 | },
42 | settings: { react: { version: "detect" } },
43 | rules: {
44 | ...pluginReactHooks.configs.recommended.rules,
45 | // React scope no longer necessary with new JSX transform.
46 | "react/react-in-jsx-scope": "off",
47 | },
48 | },
49 | ];
50 |
--------------------------------------------------------------------------------
/packages/eslint-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/eslint-config",
3 | "version": "0.0.0",
4 | "type": "module",
5 | "private": true,
6 | "exports": {
7 | "./base": "./base.js",
8 | "./next-js": "./next.js",
9 | "./react-internal": "./react-internal.js"
10 | },
11 | "devDependencies": {
12 | "@eslint/js": "^9.20.0",
13 | "@next/eslint-plugin-next": "^15.1.6",
14 | "eslint": "^9.20.0",
15 | "eslint-config-prettier": "^10.0.1",
16 | "eslint-plugin-only-warn": "^1.1.0",
17 | "eslint-plugin-react": "^7.37.4",
18 | "eslint-plugin-react-hooks": "^5.1.0",
19 | "eslint-plugin-turbo": "^2.4.0",
20 | "globals": "^15.14.0",
21 | "typescript": "^5.7.3",
22 | "typescript-eslint": "^8.23.0"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/eslint-config/react-internal.js:
--------------------------------------------------------------------------------
1 | import js from "@eslint/js";
2 | import eslintConfigPrettier from "eslint-config-prettier";
3 | import tseslint from "typescript-eslint";
4 | import pluginReactHooks from "eslint-plugin-react-hooks";
5 | import pluginReact from "eslint-plugin-react";
6 | import globals from "globals";
7 | import { config as baseConfig } from "./base.js";
8 |
9 | /**
10 | * A custom ESLint configuration for libraries that use React.
11 | *
12 | * @type {import("eslint").Linter.Config} */
13 | export const config = [
14 | ...baseConfig,
15 | js.configs.recommended,
16 | eslintConfigPrettier,
17 | ...tseslint.configs.recommended,
18 | pluginReact.configs.flat.recommended,
19 | {
20 | languageOptions: {
21 | ...pluginReact.configs.flat.recommended.languageOptions,
22 | globals: {
23 | ...globals.serviceworker,
24 | ...globals.browser,
25 | },
26 | },
27 | },
28 | {
29 | plugins: {
30 | "react-hooks": pluginReactHooks,
31 | },
32 | settings: { react: { version: "detect" } },
33 | rules: {
34 | ...pluginReactHooks.configs.recommended.rules,
35 | // React scope no longer necessary with new JSX transform.
36 | "react/react-in-jsx-scope": "off",
37 | },
38 | },
39 | ];
40 |
--------------------------------------------------------------------------------
/packages/typescript-config/base.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "declaration": true,
5 | "declarationMap": true,
6 | "esModuleInterop": true,
7 | "incremental": false,
8 | "isolatedModules": true,
9 | "lib": ["es2022", "DOM", "DOM.Iterable"],
10 | "module": "NodeNext",
11 | "moduleDetection": "force",
12 | "moduleResolution": "NodeNext",
13 | "noUncheckedIndexedAccess": true,
14 | "resolveJsonModule": true,
15 | "skipLibCheck": true,
16 | "strict": true,
17 | "target": "ES2022"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/packages/typescript-config/nextjs.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "./base.json",
4 | "compilerOptions": {
5 | "plugins": [{ "name": "next" }],
6 | "module": "ESNext",
7 | "moduleResolution": "Bundler",
8 | "allowJs": true,
9 | "jsx": "preserve",
10 | "noEmit": true
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/packages/typescript-config/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/typescript-config",
3 | "version": "0.0.0",
4 | "private": true,
5 | "license": "MIT",
6 | "publishConfig": {
7 | "access": "public"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/typescript-config/react-library.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": "./base.json",
4 | "compilerOptions": {
5 | "jsx": "react-jsx"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/packages/ui/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { config } from "@repo/eslint-config/react-internal";
2 |
3 | /** @type {import("eslint").Linter.Config} */
4 | export default config;
5 |
--------------------------------------------------------------------------------
/packages/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@repo/ui",
3 | "version": "0.0.0",
4 | "private": true,
5 | "exports": {
6 | "./button": "./src/button.tsx",
7 | "./card": "./src/card.tsx",
8 | "./code": "./src/code.tsx"
9 | },
10 | "scripts": {
11 | "lint": "eslint . --max-warnings 0",
12 | "generate:component": "turbo gen react-component",
13 | "check-types": "tsc --noEmit"
14 | },
15 | "devDependencies": {
16 | "@repo/eslint-config": "*",
17 | "@repo/typescript-config": "*",
18 | "@turbo/gen": "^2.4.0",
19 | "@types/node": "^22.13.0",
20 | "@types/react": "19.0.8",
21 | "@types/react-dom": "19.0.3",
22 | "eslint": "^9.20.0",
23 | "typescript": "5.7.3"
24 | },
25 | "dependencies": {
26 | "react": "^19.0.0",
27 | "react-dom": "^19.0.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/packages/ui/src/button.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ReactNode } from "react";
4 |
5 | interface ButtonProps {
6 | children: ReactNode;
7 | className?: string;
8 | appName: string;
9 | }
10 |
11 | export const Button = ({ children, className, appName }: ButtonProps) => {
12 | return (
13 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/packages/ui/src/card.tsx:
--------------------------------------------------------------------------------
1 | import { type JSX } from "react";
2 |
3 | export function Card({
4 | className,
5 | title,
6 | children,
7 | href,
8 | }: {
9 | className?: string;
10 | title: string;
11 | children: React.ReactNode;
12 | href: string;
13 | }): JSX.Element {
14 | return (
15 |
21 |
22 | {title} ->
23 |
24 | {children}
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/packages/ui/src/code.tsx:
--------------------------------------------------------------------------------
1 | import { type JSX } from "react";
2 |
3 | export function Code({
4 | children,
5 | className,
6 | }: {
7 | children: React.ReactNode;
8 | className?: string;
9 | }): JSX.Element {
10 | return {children}
;
11 | }
12 |
--------------------------------------------------------------------------------
/packages/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@repo/typescript-config/react-library.json",
3 | "compilerOptions": {
4 | "outDir": "dist"
5 | },
6 | "include": ["src"],
7 | "exclude": ["node_modules", "dist"]
8 | }
9 |
--------------------------------------------------------------------------------
/packages/ui/turbo/generators/config.ts:
--------------------------------------------------------------------------------
1 | import type { PlopTypes } from "@turbo/gen";
2 |
3 | // Learn more about Turborepo Generators at https://turbo.build/repo/docs/core-concepts/monorepos/code-generation
4 |
5 | export default function generator(plop: PlopTypes.NodePlopAPI): void {
6 | // A simple generator to add a new React component to the internal UI library
7 | plop.setGenerator("react-component", {
8 | description: "Adds a new react component",
9 | prompts: [
10 | {
11 | type: "input",
12 | name: "name",
13 | message: "What is the name of the component?",
14 | },
15 | ],
16 | actions: [
17 | {
18 | type: "add",
19 | path: "src/{{kebabCase name}}.tsx",
20 | templateFile: "templates/component.hbs",
21 | },
22 | {
23 | type: "append",
24 | path: "package.json",
25 | pattern: /"exports": {(?)/g,
26 | template: ' "./{{kebabCase name}}": "./src/{{kebabCase name}}.tsx",',
27 | },
28 | ],
29 | });
30 | }
31 |
--------------------------------------------------------------------------------
/packages/ui/turbo/generators/templates/component.hbs:
--------------------------------------------------------------------------------
1 | export const {{ pascalCase name }} = ({ children }: { children: React.ReactNode }) => {
2 | return (
3 |
4 |
{{ pascalCase name }} Component
5 | {children}
6 |
7 | );
8 | };
9 |
--------------------------------------------------------------------------------
/turbo.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://turbo.build/schema.json",
3 | "ui": "tui",
4 | "tasks": {
5 | "build": {
6 | "dependsOn": ["^build"],
7 | "inputs": ["$TURBO_DEFAULT$", ".env*"],
8 | "outputs": [".next/**", "!.next/cache/**"]
9 | },
10 | "lint": {
11 | "dependsOn": ["^lint"]
12 | },
13 | "check-types": {
14 | "dependsOn": ["^check-types"]
15 | },
16 | "dev": {
17 | "cache": false,
18 | "persistent": true
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------