├── .env.example ├── .eslintrc.json ├── .github └── images │ ├── features.png │ └── homepage.png ├── .gitignore ├── README.md ├── components.json ├── drizzle.config.ts ├── drizzle ├── 0000_sweet_zeigeist.sql ├── 0001_volatile_gressill.sql ├── 0002_lovely_jocasta.sql └── meta │ ├── 0000_snapshot.json │ ├── 0001_snapshot.json │ ├── 0002_snapshot.json │ └── _journal.json ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── public ├── bg.jpg └── logo.svg ├── src ├── app │ ├── (auth) │ │ ├── layout.tsx │ │ ├── sign-in │ │ │ └── page.tsx │ │ └── sign-up │ │ │ └── page.tsx │ ├── (dashboard) │ │ ├── banner.tsx │ │ ├── layout.tsx │ │ ├── logo.tsx │ │ ├── navbar.tsx │ │ ├── page.tsx │ │ ├── projects-section.tsx │ │ ├── sidebar-item.tsx │ │ ├── sidebar-routes.tsx │ │ ├── sidebar.tsx │ │ ├── template-card.tsx │ │ └── templates-section.tsx │ ├── api │ │ ├── [[...route]] │ │ │ ├── ai.ts │ │ │ ├── images.ts │ │ │ ├── projects.ts │ │ │ ├── route.ts │ │ │ └── users.ts │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ └── uploadthing │ │ │ ├── core.ts │ │ │ └── route.ts │ ├── editor │ │ └── [projectId] │ │ │ └── page.tsx │ ├── favicon.ico │ ├── fonts │ │ ├── GeistMonoVF.woff │ │ └── GeistVF.woff │ ├── globals.css │ └── layout.tsx ├── auth.config.ts ├── auth.ts ├── components │ ├── hint.tsx │ ├── modals.tsx │ ├── provides.tsx │ ├── query-provider.tsx │ └── ui │ │ ├── avatar.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── scroll-area.tsx │ │ ├── separator.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── table.tsx │ │ ├── textarea.tsx │ │ └── tooltip.tsx ├── db │ ├── drizzle.ts │ └── schema.ts ├── features │ ├── ai │ │ └── api │ │ │ ├── use-generate-image.ts │ │ │ └── use-remove-background.ts │ ├── auth │ │ ├── components │ │ │ ├── sign-in-card.tsx │ │ │ ├── sign-up-card.tsx │ │ │ └── user-button.tsx │ │ ├── hooks │ │ │ └── use-sign-up.ts │ │ └── utils.ts │ ├── editor │ │ ├── components │ │ │ ├── ai-sidebar.tsx │ │ │ ├── color-picker.tsx │ │ │ ├── draw-sidebar.tsx │ │ │ ├── editor.tsx │ │ │ ├── fill-color-sidebar.tsx │ │ │ ├── filter-sidebar.tsx │ │ │ ├── font-sidebar.tsx │ │ │ ├── font-size-input.tsx │ │ │ ├── footer.tsx │ │ │ ├── image-sidebar.tsx │ │ │ ├── logo.tsx │ │ │ ├── navbar.tsx │ │ │ ├── opacity-sidebar.tsx │ │ │ ├── remove-bg-sidebar.tsx │ │ │ ├── settings-sidebar.tsx │ │ │ ├── shape-sidebar.tsx │ │ │ ├── shape-tool.tsx │ │ │ ├── sidebar-item.tsx │ │ │ ├── sidebar.tsx │ │ │ ├── stroke-color-sidebar.tsx │ │ │ ├── stroke-width-sidebar.tsx │ │ │ ├── template-sidebar.tsx │ │ │ ├── text-sidebar.tsx │ │ │ ├── tool-sidebar-close.tsx │ │ │ ├── tool-sidebar-header.tsx │ │ │ └── toolbar.tsx │ │ ├── hooks │ │ │ ├── use-auto-resize.ts │ │ │ ├── use-canvas-events.ts │ │ │ ├── use-clipboard.ts │ │ │ ├── use-editor.ts │ │ │ ├── use-history.ts │ │ │ ├── use-hotkeys.ts │ │ │ ├── use-load-state.ts │ │ │ └── use-window-events.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── images │ │ └── api │ │ │ └── use-get-imgaes.ts │ ├── projects │ │ └── api │ │ │ ├── use-create-project.ts │ │ │ ├── use-delete-project.ts │ │ │ ├── use-duplicate-project.ts │ │ │ ├── use-get-project.ts │ │ │ ├── use-get-projects.ts │ │ │ ├── use-get-templates.ts │ │ │ └── use-update-project.ts │ └── subscriptions │ │ ├── components │ │ └── subscription-modal.tsx │ │ ├── hooks │ │ └── use-paywall.ts │ │ └── store │ │ └── use-subscription-modal.ts ├── hooks │ └── use-confirm.tsx ├── lib │ ├── hono.ts │ ├── replicate.ts │ ├── unsplash.ts │ ├── uploadthing.ts │ └── utils.ts └── middleware.ts ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_APP_URL= 2 | 3 | NEXT_PUBLIC_UNSPLASH_ACCESS_KEY= 4 | 5 | UPLOADTHING_TOKEN= 6 | 7 | UPLOADTHING_SECRET= 8 | UPLOADTHING_APP_ID= 9 | 10 | REPLICATE_API_TOKEN= 11 | AUTH_SECRET= 12 | 13 | AUTH_GOOGLE_ID= 14 | AUTH_GOOGLE_SECRET= 15 | 16 | AUTH_GITHUB_ID= 17 | AUTH_GITHUB_SECRET= 18 | 19 | DATABASE_URL= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"], 3 | "rules": { 4 | "no-unused-vars": "off", 5 | "@typescript-eslint/no-explicit-any": "off", 6 | "@typescript-eslint/ban-ts-comment": "off", 7 | "@typescript-eslint/no-unused-vars": "off", 8 | "@typescript-eslint/no-non-null-asserted-optional-chain": "off" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.github/images/features.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhengchchen/image-ai/26305374f6b9d912d478700e3f6827bf8c27c5a4/.github/images/features.png -------------------------------------------------------------------------------- /.github/images/homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhengchchen/image-ai/26305374f6b9d912d478700e3f6827bf8c27c5a4/.github/images/homepage.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Image AI - AI 驱动的图片编辑器 2 | 3 | Image AI 是一个功能强大的在线图片编辑器,集成了 AI 图片生成、背景移除等功能。它使用 Next.js 构建,提供直观的用户界面和丰富的编辑工具。 4 | 5 | ![主页](.github/images/homepage.png) 6 | 7 | ## ✨ 主要功能 8 | 9 | - 🎨 完整的图片编辑功能 10 | - 添加文字、形状 11 | - 调整颜色、大小 12 | - 图层管理 13 | - 绘图工具 14 | - 🤖 AI 功能 15 | - AI 图片生成 16 | - 智能背景移除 17 | - 📦 模板系统 18 | - 内置多种设计模板 19 | - 支持自定义模板 20 | - 💾 多种导出格式 21 | - PNG 22 | - JPG 23 | - SVG 24 | - JSON(用于后续编辑) 25 | 26 | ![功能](.github/images/features.png) 27 | 28 | ## 🚀 快速开始 29 | 30 | ### 在线使用 31 | 32 | 访问 [Image AI](https://www.imagegicai.com/) 即可开始使用。 33 | 34 | ### 本地开发 35 | 36 | 1. 克隆项目: 37 | ```bash 38 | git clone https://github.com/zhengchchen/image-ai.git 39 | cd image-ai 40 | ``` 41 | 2. 安装依赖: 42 | ```bash 43 | pnpm install 44 | ``` 45 | 46 | 3. 配置环境变量: 47 | 48 | 复制 `.env.example` 为 `.env.local` 并填写必要的环境变量: 49 | ```bash 50 | cp .env.example .env.local 51 | ``` 52 | 需要配置的环境变量包括: 53 | 54 | - `DATABASE_URL`: 数据库连接地址 55 | - `NEXTAUTH_SECRET`: NextAuth 密钥 56 | - `UPLOADTHING_SECRET`: UploadThing 密钥 57 | - `UPLOADTHING_APP_ID`: UploadThing 应用 ID 58 | - `REPLICATE_API_TOKEN`: Replicate API 密钥 59 | 60 | 4. 启动开发服务器: 61 | ```bash 62 | pnpm dev 63 | ``` 64 | 访问 [http://localhost:3000](http://localhost:3000) 查看结果。 65 | 66 | ## 🛠 技术栈 67 | 68 | - [Next.js 14](https://nextjs.org/) - React 框架 69 | - [TypeScript](https://www.typescriptlang.org/) - 类型检查 70 | - [Tailwind CSS](https://tailwindcss.com/) - 样式 71 | - [Fabric.js](http://fabricjs.com/) - 画布操作 72 | - [Drizzle ORM](https://orm.drizzle.team/) - 数据库 ORM 73 | - [NextAuth.js](https://next-auth.js.org/) - 身份认证 74 | - [Replicate](https://replicate.com/) - AI 功能 75 | - [UploadThing](https://uploadthing.com/) - 文件上传 76 | - [Neon Database](https://neon.tech/) - PostgreSQL 数据库 77 | 78 | ## 📦 项目结构 79 | src/ 80 | ├── app/ # Next.js 应用路由 81 | ├── components/ # 通用组件 82 | ├── db/ # 数据库 schema 定义 83 | ├── hooks/ # 自定义 hooks 84 | ├── features/ # 功能模块 85 | │ ├── editor/ # 编辑器核心功能 86 | │ ├── ai/ # AI 相关功能 87 | │ ├── auth/ # 认证相关 88 | │ └── projects/ # 项目管理 89 | └── lib/ # 工具函数和配置 90 | 91 | ## 📄 License 92 | 93 | MIT License - 查看 [LICENSE](LICENSE) 文件了解详情 94 | 95 | ## 🤝 贡献指南 96 | 97 | 欢迎提交 Issue 和 Pull Request! 98 | 99 | 1. Fork 项目 100 | 2. 创建功能分支 (`git checkout -b feature/AmazingFeature`) 101 | 3. 提交改动 (`git commit -m 'Add some AmazingFeature'`) 102 | 4. 推送到分支 (`git push origin feature/AmazingFeature`) 103 | 5. 提交 Pull Request 104 | 105 | ## 📧 联系方式 106 | 107 | [275781239@qq.com](mailto:275781239@qq.com) 108 | 109 | ## 🙏 致谢 110 | 111 | 感谢以下开源项目: 112 | 113 | - [Next.js](https://nextjs.org/) 114 | - [Fabric.js](http://fabricjs.com/) 115 | - [shadcn/ui](https://ui.shadcn.com/) 116 | - [Replicate](https://replicate.com/) -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 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 | } -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit"; 2 | import { config } from "dotenv"; 3 | 4 | config({path:".env.local"}) 5 | 6 | export default defineConfig({ 7 | dialect: "postgresql", 8 | schema: "./src/db/schema.ts", 9 | dbCredentials:{ 10 | url:process.env.DATABASE_URL! 11 | }, 12 | verbose:true, 13 | strict:true 14 | }); 15 | -------------------------------------------------------------------------------- /drizzle/0000_sweet_zeigeist.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "account" ( 2 | "userId" text NOT NULL, 3 | "type" text NOT NULL, 4 | "provider" text NOT NULL, 5 | "providerAccountId" text NOT NULL, 6 | "refresh_token" text, 7 | "access_token" text, 8 | "expires_at" integer, 9 | "token_type" text, 10 | "scope" text, 11 | "id_token" text, 12 | "session_state" text, 13 | CONSTRAINT "account_provider_providerAccountId_pk" PRIMARY KEY("provider","providerAccountId") 14 | ); 15 | --> statement-breakpoint 16 | CREATE TABLE IF NOT EXISTS "authenticator" ( 17 | "credentialID" text NOT NULL, 18 | "userId" text NOT NULL, 19 | "providerAccountId" text NOT NULL, 20 | "credentialPublicKey" text NOT NULL, 21 | "counter" integer NOT NULL, 22 | "credentialDeviceType" text NOT NULL, 23 | "credentialBackedUp" boolean NOT NULL, 24 | "transports" text, 25 | CONSTRAINT "authenticator_userId_credentialID_pk" PRIMARY KEY("userId","credentialID"), 26 | CONSTRAINT "authenticator_credentialID_unique" UNIQUE("credentialID") 27 | ); 28 | --> statement-breakpoint 29 | CREATE TABLE IF NOT EXISTS "session" ( 30 | "sessionToken" text PRIMARY KEY NOT NULL, 31 | "userId" text NOT NULL, 32 | "expires" timestamp NOT NULL 33 | ); 34 | --> statement-breakpoint 35 | CREATE TABLE IF NOT EXISTS "user" ( 36 | "id" text PRIMARY KEY NOT NULL, 37 | "name" text, 38 | "email" text, 39 | "emailVerified" timestamp, 40 | "image" text, 41 | CONSTRAINT "user_email_unique" UNIQUE("email") 42 | ); 43 | --> statement-breakpoint 44 | CREATE TABLE IF NOT EXISTS "verificationToken" ( 45 | "identifier" text NOT NULL, 46 | "token" text NOT NULL, 47 | "expires" timestamp NOT NULL, 48 | CONSTRAINT "verificationToken_identifier_token_pk" PRIMARY KEY("identifier","token") 49 | ); 50 | --> statement-breakpoint 51 | DO $$ BEGIN 52 | ALTER TABLE "account" ADD CONSTRAINT "account_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; 53 | EXCEPTION 54 | WHEN duplicate_object THEN null; 55 | END $$; 56 | --> statement-breakpoint 57 | DO $$ BEGIN 58 | ALTER TABLE "authenticator" ADD CONSTRAINT "authenticator_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; 59 | EXCEPTION 60 | WHEN duplicate_object THEN null; 61 | END $$; 62 | --> statement-breakpoint 63 | DO $$ BEGIN 64 | ALTER TABLE "session" ADD CONSTRAINT "session_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; 65 | EXCEPTION 66 | WHEN duplicate_object THEN null; 67 | END $$; 68 | -------------------------------------------------------------------------------- /drizzle/0001_volatile_gressill.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "user" ADD COLUMN "password" text; -------------------------------------------------------------------------------- /drizzle/0002_lovely_jocasta.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "project" ( 2 | "id" text PRIMARY KEY NOT NULL, 3 | "name" text NOT NULL, 4 | "userId" text NOT NULL, 5 | "json" text NOT NULL, 6 | "height" integer NOT NULL, 7 | "width" integer NOT NULL, 8 | "thumbnailUrl" text, 9 | "isTemplate" boolean, 10 | "isPro" boolean, 11 | "createdAt" timestamp NOT NULL, 12 | "updatedAt" timestamp NOT NULL 13 | ); 14 | --> statement-breakpoint 15 | DO $$ BEGIN 16 | ALTER TABLE "project" ADD CONSTRAINT "project_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; 17 | EXCEPTION 18 | WHEN duplicate_object THEN null; 19 | END $$; 20 | -------------------------------------------------------------------------------- /drizzle/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "72be38dc-461c-4537-a8f9-a0e01f5bc469", 3 | "prevId": "00000000-0000-0000-0000-000000000000", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.account": { 8 | "name": "account", 9 | "schema": "", 10 | "columns": { 11 | "userId": { 12 | "name": "userId", 13 | "type": "text", 14 | "primaryKey": false, 15 | "notNull": true 16 | }, 17 | "type": { 18 | "name": "type", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "provider": { 24 | "name": "provider", 25 | "type": "text", 26 | "primaryKey": false, 27 | "notNull": true 28 | }, 29 | "providerAccountId": { 30 | "name": "providerAccountId", 31 | "type": "text", 32 | "primaryKey": false, 33 | "notNull": true 34 | }, 35 | "refresh_token": { 36 | "name": "refresh_token", 37 | "type": "text", 38 | "primaryKey": false, 39 | "notNull": false 40 | }, 41 | "access_token": { 42 | "name": "access_token", 43 | "type": "text", 44 | "primaryKey": false, 45 | "notNull": false 46 | }, 47 | "expires_at": { 48 | "name": "expires_at", 49 | "type": "integer", 50 | "primaryKey": false, 51 | "notNull": false 52 | }, 53 | "token_type": { 54 | "name": "token_type", 55 | "type": "text", 56 | "primaryKey": false, 57 | "notNull": false 58 | }, 59 | "scope": { 60 | "name": "scope", 61 | "type": "text", 62 | "primaryKey": false, 63 | "notNull": false 64 | }, 65 | "id_token": { 66 | "name": "id_token", 67 | "type": "text", 68 | "primaryKey": false, 69 | "notNull": false 70 | }, 71 | "session_state": { 72 | "name": "session_state", 73 | "type": "text", 74 | "primaryKey": false, 75 | "notNull": false 76 | } 77 | }, 78 | "indexes": {}, 79 | "foreignKeys": { 80 | "account_userId_user_id_fk": { 81 | "name": "account_userId_user_id_fk", 82 | "tableFrom": "account", 83 | "tableTo": "user", 84 | "columnsFrom": [ 85 | "userId" 86 | ], 87 | "columnsTo": [ 88 | "id" 89 | ], 90 | "onDelete": "cascade", 91 | "onUpdate": "no action" 92 | } 93 | }, 94 | "compositePrimaryKeys": { 95 | "account_provider_providerAccountId_pk": { 96 | "name": "account_provider_providerAccountId_pk", 97 | "columns": [ 98 | "provider", 99 | "providerAccountId" 100 | ] 101 | } 102 | }, 103 | "uniqueConstraints": {}, 104 | "policies": {}, 105 | "checkConstraints": {}, 106 | "isRLSEnabled": false 107 | }, 108 | "public.authenticator": { 109 | "name": "authenticator", 110 | "schema": "", 111 | "columns": { 112 | "credentialID": { 113 | "name": "credentialID", 114 | "type": "text", 115 | "primaryKey": false, 116 | "notNull": true 117 | }, 118 | "userId": { 119 | "name": "userId", 120 | "type": "text", 121 | "primaryKey": false, 122 | "notNull": true 123 | }, 124 | "providerAccountId": { 125 | "name": "providerAccountId", 126 | "type": "text", 127 | "primaryKey": false, 128 | "notNull": true 129 | }, 130 | "credentialPublicKey": { 131 | "name": "credentialPublicKey", 132 | "type": "text", 133 | "primaryKey": false, 134 | "notNull": true 135 | }, 136 | "counter": { 137 | "name": "counter", 138 | "type": "integer", 139 | "primaryKey": false, 140 | "notNull": true 141 | }, 142 | "credentialDeviceType": { 143 | "name": "credentialDeviceType", 144 | "type": "text", 145 | "primaryKey": false, 146 | "notNull": true 147 | }, 148 | "credentialBackedUp": { 149 | "name": "credentialBackedUp", 150 | "type": "boolean", 151 | "primaryKey": false, 152 | "notNull": true 153 | }, 154 | "transports": { 155 | "name": "transports", 156 | "type": "text", 157 | "primaryKey": false, 158 | "notNull": false 159 | } 160 | }, 161 | "indexes": {}, 162 | "foreignKeys": { 163 | "authenticator_userId_user_id_fk": { 164 | "name": "authenticator_userId_user_id_fk", 165 | "tableFrom": "authenticator", 166 | "tableTo": "user", 167 | "columnsFrom": [ 168 | "userId" 169 | ], 170 | "columnsTo": [ 171 | "id" 172 | ], 173 | "onDelete": "cascade", 174 | "onUpdate": "no action" 175 | } 176 | }, 177 | "compositePrimaryKeys": { 178 | "authenticator_userId_credentialID_pk": { 179 | "name": "authenticator_userId_credentialID_pk", 180 | "columns": [ 181 | "userId", 182 | "credentialID" 183 | ] 184 | } 185 | }, 186 | "uniqueConstraints": { 187 | "authenticator_credentialID_unique": { 188 | "name": "authenticator_credentialID_unique", 189 | "nullsNotDistinct": false, 190 | "columns": [ 191 | "credentialID" 192 | ] 193 | } 194 | }, 195 | "policies": {}, 196 | "checkConstraints": {}, 197 | "isRLSEnabled": false 198 | }, 199 | "public.session": { 200 | "name": "session", 201 | "schema": "", 202 | "columns": { 203 | "sessionToken": { 204 | "name": "sessionToken", 205 | "type": "text", 206 | "primaryKey": true, 207 | "notNull": true 208 | }, 209 | "userId": { 210 | "name": "userId", 211 | "type": "text", 212 | "primaryKey": false, 213 | "notNull": true 214 | }, 215 | "expires": { 216 | "name": "expires", 217 | "type": "timestamp", 218 | "primaryKey": false, 219 | "notNull": true 220 | } 221 | }, 222 | "indexes": {}, 223 | "foreignKeys": { 224 | "session_userId_user_id_fk": { 225 | "name": "session_userId_user_id_fk", 226 | "tableFrom": "session", 227 | "tableTo": "user", 228 | "columnsFrom": [ 229 | "userId" 230 | ], 231 | "columnsTo": [ 232 | "id" 233 | ], 234 | "onDelete": "cascade", 235 | "onUpdate": "no action" 236 | } 237 | }, 238 | "compositePrimaryKeys": {}, 239 | "uniqueConstraints": {}, 240 | "policies": {}, 241 | "checkConstraints": {}, 242 | "isRLSEnabled": false 243 | }, 244 | "public.user": { 245 | "name": "user", 246 | "schema": "", 247 | "columns": { 248 | "id": { 249 | "name": "id", 250 | "type": "text", 251 | "primaryKey": true, 252 | "notNull": true 253 | }, 254 | "name": { 255 | "name": "name", 256 | "type": "text", 257 | "primaryKey": false, 258 | "notNull": false 259 | }, 260 | "email": { 261 | "name": "email", 262 | "type": "text", 263 | "primaryKey": false, 264 | "notNull": false 265 | }, 266 | "emailVerified": { 267 | "name": "emailVerified", 268 | "type": "timestamp", 269 | "primaryKey": false, 270 | "notNull": false 271 | }, 272 | "image": { 273 | "name": "image", 274 | "type": "text", 275 | "primaryKey": false, 276 | "notNull": false 277 | } 278 | }, 279 | "indexes": {}, 280 | "foreignKeys": {}, 281 | "compositePrimaryKeys": {}, 282 | "uniqueConstraints": { 283 | "user_email_unique": { 284 | "name": "user_email_unique", 285 | "nullsNotDistinct": false, 286 | "columns": [ 287 | "email" 288 | ] 289 | } 290 | }, 291 | "policies": {}, 292 | "checkConstraints": {}, 293 | "isRLSEnabled": false 294 | }, 295 | "public.verificationToken": { 296 | "name": "verificationToken", 297 | "schema": "", 298 | "columns": { 299 | "identifier": { 300 | "name": "identifier", 301 | "type": "text", 302 | "primaryKey": false, 303 | "notNull": true 304 | }, 305 | "token": { 306 | "name": "token", 307 | "type": "text", 308 | "primaryKey": false, 309 | "notNull": true 310 | }, 311 | "expires": { 312 | "name": "expires", 313 | "type": "timestamp", 314 | "primaryKey": false, 315 | "notNull": true 316 | } 317 | }, 318 | "indexes": {}, 319 | "foreignKeys": {}, 320 | "compositePrimaryKeys": { 321 | "verificationToken_identifier_token_pk": { 322 | "name": "verificationToken_identifier_token_pk", 323 | "columns": [ 324 | "identifier", 325 | "token" 326 | ] 327 | } 328 | }, 329 | "uniqueConstraints": {}, 330 | "policies": {}, 331 | "checkConstraints": {}, 332 | "isRLSEnabled": false 333 | } 334 | }, 335 | "enums": {}, 336 | "schemas": {}, 337 | "sequences": {}, 338 | "roles": {}, 339 | "policies": {}, 340 | "views": {}, 341 | "_meta": { 342 | "columns": {}, 343 | "schemas": {}, 344 | "tables": {} 345 | } 346 | } -------------------------------------------------------------------------------- /drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "postgresql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "7", 8 | "when": 1732175650722, 9 | "tag": "0000_sweet_zeigeist", 10 | "breakpoints": true 11 | }, 12 | { 13 | "idx": 1, 14 | "version": "7", 15 | "when": 1732242076926, 16 | "tag": "0001_volatile_gressill", 17 | "breakpoints": true 18 | }, 19 | { 20 | "idx": 2, 21 | "version": "7", 22 | "when": 1732449402899, 23 | "tag": "0002_lovely_jocasta", 24 | "breakpoints": true 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "images.unsplash.com", 8 | }, 9 | { 10 | protocol: "https", 11 | hostname: "utfs.io", 12 | }, 13 | { 14 | protocol: "https", 15 | hostname: "replicate.delivery", 16 | }, 17 | ], 18 | }, 19 | }; 20 | 21 | export default nextConfig; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "image-ai", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "db:generate": "npx drizzle-kit generate", 11 | "db:migrate": "npx drizzle-kit migrate", 12 | "db:studio": "npx drizzle-kit studio" 13 | }, 14 | "dependencies": { 15 | "@auth/core": "^0.37.4", 16 | "@auth/drizzle-adapter": "^1.7.4", 17 | "@hono/auth-js": "^1.0.15", 18 | "@hono/zod-validator": "^0.4.1", 19 | "@neondatabase/serverless": "^0.10.3", 20 | "@radix-ui/react-avatar": "^1.1.1", 21 | "@radix-ui/react-dialog": "^1.1.2", 22 | "@radix-ui/react-dropdown-menu": "^2.1.2", 23 | "@radix-ui/react-label": "^2.1.0", 24 | "@radix-ui/react-scroll-area": "^1.2.0", 25 | "@radix-ui/react-separator": "^1.1.0", 26 | "@radix-ui/react-slider": "^1.2.1", 27 | "@radix-ui/react-slot": "^1.1.0", 28 | "@radix-ui/react-tooltip": "^1.1.3", 29 | "@tanstack/react-query": "^5.60.2", 30 | "@types/material-colors": "^1.2.3", 31 | "@types/react-color": "^3.0.12", 32 | "@uploadthing/react": "^7.1.1", 33 | "@vercel/analytics": "^1.4.1", 34 | "bcryptjs": "^2.4.3", 35 | "class-variance-authority": "^0.7.0", 36 | "clsx": "^2.1.1", 37 | "date-fns": "^4.1.0", 38 | "drizzle-orm": "^0.36.3", 39 | "drizzle-zod": "^0.5.1", 40 | "fabric": "5.3.0-browser", 41 | "hono": "^4.6.11", 42 | "jsdom": "^25.0.0", 43 | "lodash.debounce": "^4.0.8", 44 | "lucide-react": "^0.440.0", 45 | "material-colors": "^1.2.6", 46 | "next": "14.2.10", 47 | "next-auth": "5.0.0-beta.25", 48 | "next-themes": "^0.4.3", 49 | "react": "^18", 50 | "react-color": "^2.19.3", 51 | "react-dom": "^18", 52 | "react-icons": "^5.3.0", 53 | "react-use": "^17.5.1", 54 | "replicate": "^1.0.1", 55 | "sonner": "^1.7.0", 56 | "tailwind-merge": "^2.5.2", 57 | "tailwindcss-animate": "^1.0.7", 58 | "unsplash-js": "^7.0.19", 59 | "uploadthing": "^7.3.0", 60 | "use-file-picker": "^2.1.2", 61 | "uuidv4": "^6.2.13", 62 | "zod": "^3.23.8", 63 | "zustand": "^5.0.1" 64 | }, 65 | "devDependencies": { 66 | "@types/bcryptjs": "^2.4.6", 67 | "@types/fabric": "^5.3.0", 68 | "@types/lodash.debounce": "^4.0.9", 69 | "@types/node": "^20", 70 | "@types/react": "^18", 71 | "@types/react-dom": "^18", 72 | "dotenv": "^16.4.5", 73 | "drizzle-kit": "^0.28.1", 74 | "eslint": "^8", 75 | "eslint-config-next": "14.2.10", 76 | "postcss": "^8", 77 | "tailwindcss": "^3.4.1", 78 | "typescript": "^5" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhengchchen/image-ai/26305374f6b9d912d478700e3f6827bf8c27c5a4/public/bg.jpg -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | interface AuthLayoutProps { 2 | children: React.ReactNode; 3 | } 4 | 5 | const AuthLayout = ({ children }: AuthLayoutProps) => { 6 | return ( 7 |
8 |
9 |
{children}
10 |
11 |
12 |
13 | ); 14 | }; 15 | 16 | export default AuthLayout; 17 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import { SignInCard } from "@/features/auth/components/sign-in-card"; 3 | 4 | import { auth } from "@/auth"; 5 | 6 | const SignInPage = async () => { 7 | const session = await auth(); 8 | if (session) { 9 | redirect("/"); 10 | } 11 | return ; 12 | }; 13 | 14 | export default SignInPage; 15 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-up/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | import { auth } from "@/auth"; 4 | import { SignUpCard } from "@/features/auth/components/sign-up-card"; 5 | 6 | const SignUpPage = async () => { 7 | const session = await auth(); 8 | if (session) { 9 | redirect("/"); 10 | } 11 | return ; 12 | }; 13 | 14 | export default SignUpPage; 15 | -------------------------------------------------------------------------------- /src/app/(dashboard)/banner.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { ArrowRight, Sparkles } from "lucide-react"; 5 | import { useCreateProject } from "@/features/projects/api/use-create-project"; 6 | import { useRouter } from "next/navigation"; 7 | 8 | export const Banner = () => { 9 | const router = useRouter(); 10 | 11 | const mutation = useCreateProject(); 12 | 13 | const onClick = () => { 14 | mutation.mutate( 15 | { 16 | name: "Untitled project", 17 | json: "", 18 | height: 900, 19 | width: 1200, 20 | }, 21 | { 22 | onSuccess: ({ data }) => { 23 | router.push(`/editor/${data.id}`); 24 | }, 25 | } 26 | ); 27 | }; 28 | 29 | return ( 30 |
31 |
32 |
33 | 34 |
35 |
36 |
37 |

Visualize your ideas with Image AI

38 |

39 | Turn inspiration into design in no time. Simply upload a image and let AI do the rest. 40 |

41 | 45 |
46 |
47 | ); 48 | }; 49 | -------------------------------------------------------------------------------- /src/app/(dashboard)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Navbar } from "./navbar"; 2 | import { Sidebar } from "./sidebar"; 3 | 4 | interface DashboardLayoutProps { 5 | children: React.ReactNode; 6 | } 7 | 8 | const DashboardLayout = ({ children }: DashboardLayoutProps) => { 9 | return ( 10 |
11 | 12 |
13 | 14 |
{children}
15 |
16 |
17 | ); 18 | }; 19 | 20 | export default DashboardLayout; 21 | -------------------------------------------------------------------------------- /src/app/(dashboard)/logo.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Image from "next/image"; 3 | import { Space_Grotesk } from "next/font/google"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const font = Space_Grotesk({ 8 | subsets: ["latin"], 9 | weight: ["700"], 10 | }); 11 | 12 | export const Logo = () => { 13 | return ( 14 | 15 |
16 |
17 | Image AI 18 |
19 |

Image AI

20 |
21 | 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/app/(dashboard)/navbar.tsx: -------------------------------------------------------------------------------- 1 | import { UserButton } from "@/features/auth/components/user-button"; 2 | 3 | export const Navbar = () => { 4 | return ( 5 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/(dashboard)/page.tsx: -------------------------------------------------------------------------------- 1 | import { protectServer } from "@/features/auth/utils"; 2 | import { Banner } from "./banner"; 3 | import { ProjectsSection } from "./projects-section"; 4 | import { TemplatesSection } from "./templates-section"; 5 | 6 | export default async function Home() { 7 | await protectServer(); 8 | 9 | return ( 10 |
11 | 12 | 13 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/app/(dashboard)/projects-section.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { useRouter } from "next/navigation"; 5 | import { formatDistanceToNow } from "date-fns"; 6 | 7 | import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table"; 8 | 9 | import { 10 | DropdownMenu, 11 | DropdownMenuContent, 12 | DropdownMenuItem, 13 | DropdownMenuTrigger, 14 | } from "@/components/ui/dropdown-menu"; 15 | 16 | import { useGetProjects } from "@/features/projects/api/use-get-projects"; 17 | import { Search, AlertTriangle, Loader, FileIcon, MoreHorizontal, CopyIcon, Trash, TrashIcon } from "lucide-react"; 18 | import { Button } from "@/components/ui/button"; 19 | import { useDuplicateProject } from "@/features/projects/api/use-duplicate-project"; 20 | import { useDeleteProject } from "@/features/projects/api/use-delete-project"; 21 | import { useConfirm } from "@/hooks/use-confirm"; 22 | 23 | export const ProjectsSection = () => { 24 | const [ConfirmationDialog, confirm] = useConfirm("Are you sure?", "You are about to delete this project."); 25 | const router = useRouter(); 26 | 27 | const duplicateMutation = useDuplicateProject(); 28 | const removeMutation = useDeleteProject(); 29 | 30 | const onCopy = (id: string) => { 31 | duplicateMutation.mutate({ id }); 32 | }; 33 | 34 | const onDelete = async (id: string) => { 35 | const ok = await confirm(); 36 | if (ok) removeMutation.mutate({ id }); 37 | }; 38 | 39 | const { data, status, fetchNextPage, isFetchingNextPage, hasNextPage } = useGetProjects(); 40 | 41 | if (status === "pending") { 42 | return ( 43 |
44 |

Recent Projects

45 |
46 | 47 |
48 |
49 | ); 50 | } 51 | 52 | if (status === "error") { 53 | return ( 54 |
55 |

Recent Projects

56 |
57 | 58 |

Failed to load projects

59 |
60 |
61 | ); 62 | } 63 | 64 | if (!data.pages.length || !data.pages[0].data.length) { 65 | return ( 66 |
67 |

Recent Projects

68 |
69 | 70 |

No projects found

71 |
72 |
73 | ); 74 | } 75 | 76 | return ( 77 |
78 | 79 |

Recent Projects

80 | 81 | 82 | {data.pages.map((group, i) => ( 83 | 84 | {group.data.map((project) => ( 85 | 86 | router.push(`/editor/${project.id}`)} 88 | className="font-medium flex items-center gap-x-2 cursor-pointer" 89 | > 90 | 91 | {project.name} 92 | 93 | router.push(`/editor/${project.id}`)} 95 | className="hidden md:table-cell cursor-pointer" 96 | > 97 | {project.width} x {project.height} px 98 | 99 | router.push(`/editor/${project.id}`)} 101 | className="hidden md:table-cell cursor-pointer" 102 | > 103 | {formatDistanceToNow(new Date(project.updatedAt), { addSuffix: true })} 104 | 105 | 106 | 107 | 108 | 111 | 112 | 113 | onCopy(project.id)} 117 | > 118 | 119 | Make a copy 120 | 121 | 122 | onDelete(project.id)} 126 | > 127 | 128 | Delete 129 | 130 | 131 | 132 | 133 | 134 | ))} 135 | 136 | ))} 137 | 138 |
139 | {hasNextPage && ( 140 |
141 | 144 |
145 | )} 146 |
147 | ); 148 | }; 149 | -------------------------------------------------------------------------------- /src/app/(dashboard)/sidebar-item.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import type { LucideIcon } from "lucide-react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | interface SidebarItemProps { 7 | href: string; 8 | icon: LucideIcon; 9 | label: string; 10 | isActive?: boolean; 11 | onClick?: () => void; 12 | } 13 | 14 | export const SidebarItem = ({ href, icon: Icon, label, isActive, onClick }: SidebarItemProps) => { 15 | return ( 16 | 17 |
23 | 24 | {label} 25 |
26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /src/app/(dashboard)/sidebar-routes.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { CreditCard, Crown, Home, MessageCircleQuestion } from "lucide-react"; 4 | import { usePathname } from "next/navigation"; 5 | 6 | import { Button } from "@/components/ui/button"; 7 | import { Separator } from "@/components/ui/separator"; 8 | 9 | import { SidebarItem } from "./sidebar-item"; 10 | import { usePaywall } from "@/features/subscriptions/hooks/use-paywall"; 11 | 12 | export const SidebarRoutes = () => { 13 | const pathname = usePathname(); 14 | 15 | const { shouldBlock, triggerPaywall } = usePaywall(); 16 | 17 | const onClick = () => { 18 | if (shouldBlock) { 19 | triggerPaywall(); 20 | return; 21 | } 22 | }; 23 | 24 | return ( 25 |
26 |
27 | 36 |
37 |
38 | 39 |
40 |
    41 | 42 |
43 |
44 | 45 |
46 |
    47 | {}} /> 48 | 49 |
50 |
51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /src/app/(dashboard)/sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { Logo } from "./logo"; 2 | import { SidebarRoutes } from "./sidebar-routes"; 3 | 4 | export const Sidebar = () => { 5 | return ( 6 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /src/app/(dashboard)/template-card.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { Crown } from "lucide-react"; 5 | 6 | interface TemplateCardProps { 7 | imageSrc: string; 8 | title: string; 9 | onClick: (title: string) => void; 10 | description: string; 11 | width: number; 12 | height: number; 13 | isPro: boolean | null; 14 | disabled?: boolean; 15 | } 16 | 17 | export const TemplateCard = ({ 18 | imageSrc, 19 | title, 20 | onClick, 21 | description, 22 | width, 23 | height, 24 | isPro, 25 | disabled, 26 | }: TemplateCardProps) => { 27 | return ( 28 | 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /src/app/(dashboard)/templates-section.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ResponseType, useGetTemplates } from "@/features/projects/api/use-get-templates"; 4 | import { Loader, TriangleAlert } from "lucide-react"; 5 | import { TemplateCard } from "./template-card"; 6 | import { useCreateProject } from "@/features/projects/api/use-create-project"; 7 | import { useRouter } from "next/navigation"; 8 | import { usePaywall } from "@/features/subscriptions/hooks/use-paywall"; 9 | 10 | export const TemplatesSection = () => { 11 | const { shouldBlock, triggerPaywall } = usePaywall(); 12 | const router = useRouter(); 13 | const mutation = useCreateProject(); 14 | const { data, isLoading, isError } = useGetTemplates({ page: "1", limit: "4" }); 15 | 16 | const onClick = (template: ResponseType["data"][0]) => { 17 | if (template.isPro && shouldBlock) { 18 | triggerPaywall(); 19 | return; 20 | } 21 | mutation.mutate( 22 | { 23 | name: `${template.name} project`, 24 | json: template.json, 25 | height: template.height, 26 | width: template.width, 27 | }, 28 | { 29 | onSuccess: ({ data }) => { 30 | router.push(`/editor/${data.id}`); 31 | }, 32 | } 33 | ); 34 | }; 35 | 36 | if (isLoading) { 37 | return ( 38 |
39 |

Start from a template

40 |
41 | 42 |
43 |
44 | ); 45 | } 46 | 47 | if (isError) { 48 | return ( 49 |
50 |

Start from a template

51 |
52 | 53 |

Failed to load templates

54 |
55 |
56 | ); 57 | } 58 | 59 | if (!data?.length) { 60 | return null; 61 | } 62 | 63 | return ( 64 |
65 |

Start from a template

66 |
67 | {data.map((template) => ( 68 | onClick(template)} 77 | description={`${template.width} x ${template.height} px`} 78 | /> 79 | ))} 80 |
81 |
82 | ); 83 | }; 84 | -------------------------------------------------------------------------------- /src/app/api/[[...route]]/ai.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { Hono } from "hono"; 3 | import { zValidator } from "@hono/zod-validator"; 4 | import { verifyAuth } from "@hono/auth-js"; 5 | 6 | const app = new Hono() 7 | .post( 8 | "/remove-bg", 9 | verifyAuth(), 10 | zValidator( 11 | "json", 12 | z.object({ 13 | image: z.string(), 14 | }) 15 | ), 16 | async (c) => { 17 | const { image } = c.req.valid("json"); 18 | 19 | try { 20 | const input = { 21 | image: image, 22 | }; 23 | 24 | const response = await fetch("https://api.replicate.com/v1/predictions", { 25 | method: "POST", 26 | headers: { 27 | Authorization: `Token ${process.env.REPLICATE_API_TOKEN}`, 28 | "Content-Type": "application/json", 29 | }, 30 | body: JSON.stringify({ 31 | version: "e89200fbc08c5c5e9314e246db83a79d43f16c552dc4005e46cd7896800a989e", 32 | input: input, 33 | }), 34 | }); 35 | 36 | const prediction = await response.json(); 37 | 38 | const getResult = async () => { 39 | const resultResponse = await fetch(prediction.urls.get, { 40 | headers: { 41 | Authorization: `Token ${process.env.REPLICATE_API_TOKEN}`, 42 | }, 43 | }); 44 | return await resultResponse.json(); 45 | }; 46 | 47 | let result; 48 | while (true) { 49 | result = await getResult(); 50 | if (result.status === "succeeded") { 51 | return c.json({ data: result.output }); 52 | } 53 | if (result.status === "failed") { 54 | throw new Error("Background removal failed"); 55 | } 56 | await new Promise((resolve) => setTimeout(resolve, 1000)); 57 | } 58 | } catch (error) { 59 | return c.json({ error: "Failed to remove background" }, 500); 60 | } 61 | } 62 | ) 63 | .post( 64 | "/generate-image", 65 | verifyAuth(), 66 | zValidator( 67 | "json", 68 | z.object({ 69 | prompt: z.string(), 70 | }) 71 | ), 72 | async (c) => { 73 | const { prompt } = c.req.valid("json"); 74 | 75 | try { 76 | const input = { 77 | cfg: 3.5, 78 | steps: 28, 79 | prompt, 80 | aspect_ratio: "3:2", 81 | output_format: "webp", 82 | output_quality: 90, 83 | negative_prompt: "", 84 | prompt_strength: 0.85, 85 | }; 86 | 87 | const response = await fetch("https://api.replicate.com/v1/predictions", { 88 | method: "POST", 89 | headers: { 90 | Authorization: `Token ${process.env.REPLICATE_API_TOKEN}`, 91 | "Content-Type": "application/json", 92 | }, 93 | body: JSON.stringify({ 94 | version: "72c05df2daf615fb5cc07c28b662a2a58feb6a4d0a652e67e5a9959d914a9ed2", 95 | input: input, 96 | }), 97 | }); 98 | 99 | const prediction = await response.json(); 100 | 101 | const getResult = async () => { 102 | const resultResponse = await fetch(prediction.urls.get, { 103 | headers: { 104 | Authorization: `Token ${process.env.REPLICATE_API_TOKEN}`, 105 | }, 106 | }); 107 | return await resultResponse.json(); 108 | }; 109 | 110 | let result; 111 | while (true) { 112 | result = await getResult(); 113 | if (result.status === "succeeded") { 114 | return c.json({ data: result.output[0] }); 115 | } 116 | if (result.status === "failed") { 117 | throw new Error("Image generation failed"); 118 | } 119 | await new Promise((resolve) => setTimeout(resolve, 1000)); 120 | } 121 | } catch (error) { 122 | console.error("Error:", error); 123 | return c.json({ error: "Failed to generate image" }, 500); 124 | } 125 | } 126 | ); 127 | 128 | export default app; 129 | -------------------------------------------------------------------------------- /src/app/api/[[...route]]/images.ts: -------------------------------------------------------------------------------- 1 | import { unsplash } from "@/lib/unsplash"; 2 | import { verifyAuth } from "@hono/auth-js"; 3 | import { Hono } from "hono"; 4 | 5 | const DEFAULT_COUNT = 50; 6 | const DEFAULT_COLLECTION_IDS = ["317099"]; 7 | 8 | const app = new Hono().get("/", verifyAuth(), async (c) => { 9 | const images = await unsplash.photos.getRandom({ 10 | count: DEFAULT_COUNT, 11 | collectionIds: DEFAULT_COLLECTION_IDS, 12 | }); 13 | 14 | if (images.errors) { 15 | return c.json( 16 | { 17 | error: "Something went wrong", 18 | }, 19 | 401 20 | ); 21 | } 22 | 23 | let response = images.response; 24 | 25 | if (!Array.isArray(response)) { 26 | response = [response]; 27 | } 28 | 29 | return c.json({ 30 | data: response, 31 | }); 32 | }); 33 | 34 | export default app; 35 | -------------------------------------------------------------------------------- /src/app/api/[[...route]]/projects.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/db/drizzle"; 2 | import { eq, and, desc, asc } from "drizzle-orm"; 3 | import { projectInsertSchema, projects } from "@/db/schema"; 4 | import { verifyAuth } from "@hono/auth-js"; 5 | import { zValidator } from "@hono/zod-validator"; 6 | import { Hono } from "hono"; 7 | import { z } from "zod"; 8 | 9 | const app = new Hono() 10 | .get( 11 | "/templates", 12 | verifyAuth(), 13 | zValidator( 14 | "query", 15 | z.object({ 16 | page: z.coerce.number(), 17 | limit: z.coerce.number() 18 | }) 19 | ), 20 | async (c)=>{ 21 | const { page, limit } = c.req.valid("query") 22 | 23 | const data = await db 24 | .select() 25 | .from(projects) 26 | .where(eq(projects.isTemplate, true)) 27 | .limit(limit) 28 | .offset((page - 1) * limit) 29 | .orderBy( 30 | asc(projects.isPro), 31 | desc(projects.updatedAt) 32 | ); 33 | 34 | return c.json({ data }); 35 | } 36 | ) 37 | .delete( 38 | "/:id", 39 | verifyAuth(), 40 | zValidator("param", z.object({ id: z.string() })), 41 | async (c) => { 42 | const auth = c.get("authUser"); 43 | const { id } = c.req.valid("param"); 44 | 45 | if (!auth.token?.id) { 46 | return c.json({ error: "Unauthorized" }, 401); 47 | } 48 | 49 | const data = await db 50 | .delete(projects) 51 | .where(and(eq(projects.id, id), eq(projects.userId, auth.token.id.toString()))) 52 | .returning(); 53 | 54 | if(!data[0]){ 55 | return c.json({ error: "Not found" }, 404); 56 | } 57 | 58 | return c.json({ data: { id } }); 59 | } 60 | ) 61 | .post( 62 | "/:id/duplicate", 63 | verifyAuth(), 64 | zValidator( 65 | "param", 66 | z.object({ 67 | id: z.string(), 68 | }) 69 | ), 70 | async (c) => { 71 | const auth = c.get("authUser"); 72 | const { id } = c.req.valid("param"); 73 | 74 | if (!auth.token?.id) { 75 | return c.json({ error: "Unauthorized" }, 401); 76 | } 77 | 78 | const data = await db 79 | .select() 80 | .from(projects) 81 | .where( 82 | and( 83 | eq(projects.id, id), 84 | eq(projects.userId, auth.token.id.toString()) 85 | ) 86 | ); 87 | 88 | if(!data[0]){ 89 | return c.json({ error: "Not found" }, 404); 90 | } 91 | 92 | const project = data[0]; 93 | 94 | const duplicateData = await db.insert(projects).values({ 95 | name: `Copy of ${project.name}`, 96 | json: project.json, 97 | height: project.height, 98 | width: project.width, 99 | userId: project.userId, 100 | createdAt: new Date(), 101 | updatedAt: new Date(), 102 | }).returning(); 103 | 104 | return c.json({ data: duplicateData[0] }); 105 | } 106 | ) 107 | .get( 108 | "/", 109 | verifyAuth(), 110 | zValidator( 111 | "query", 112 | z.object({ 113 | limit: z.coerce.number(), 114 | page: z.coerce.number(), 115 | }) 116 | ), 117 | async (c) => { 118 | const auth = c.get("authUser"); 119 | const { limit, page } = c.req.valid("query"); 120 | 121 | if (!auth.token?.id) { 122 | return c.json({ error: "Unauthorized" }, 401); 123 | } 124 | 125 | const data = await db 126 | .select() 127 | .from(projects) 128 | .where(eq(projects.userId, auth.token.id.toString())) 129 | .limit(limit) 130 | .offset((page - 1) * limit) 131 | .orderBy(desc(projects.updatedAt)); 132 | 133 | return c.json({ data, nextPage: data.length === limit ? page + 1 : null }); 134 | } 135 | ) 136 | .patch( 137 | "/:id", 138 | verifyAuth(), 139 | zValidator( 140 | "param", 141 | z.object({ 142 | id: z.string(), 143 | }) 144 | ), 145 | zValidator( 146 | "json", 147 | projectInsertSchema 148 | .omit({ 149 | id:true, 150 | userId: true, 151 | createdAt: true, 152 | updatedAt: true, 153 | }) 154 | .partial() 155 | ), 156 | async (c) => { 157 | const auth = c.get("authUser"); 158 | 159 | const { id } = c.req.valid("param"); 160 | const values = c.req.valid("json"); 161 | 162 | if(!auth.token?.id){ 163 | return c.json({ error: "Unauthorized" }, 401); 164 | } 165 | 166 | const data = await db 167 | .update(projects) 168 | .set({ 169 | ...values, 170 | updatedAt: new Date(), 171 | }) 172 | .where(and(eq(projects.id, id), eq(projects.userId, auth.token.id.toString()))) 173 | .returning(); 174 | 175 | if (!data[0]) { 176 | return c.json({ error: "Unauthorized" }, 401); 177 | } 178 | return c.json({ data: data[0] }); 179 | } 180 | ) 181 | .get( 182 | "/:id", 183 | verifyAuth(), 184 | zValidator( 185 | "param", 186 | z.object({ 187 | id: z.string(), 188 | }) 189 | ), 190 | async (c) => { 191 | const auth = c.get("authUser"); 192 | const { id } = c.req.valid("param"); 193 | 194 | if (!auth.token?.id) { 195 | return c.json({ error: "Unauthorized" }, 401); 196 | } 197 | 198 | const data = await db 199 | .select() 200 | .from(projects) 201 | // @ts-ignore 202 | .where(and(eq(projects.id, id), eq(projects.userId, auth.token.id))); 203 | 204 | if (!data[0]) { 205 | return c.json({ error: "Not found" }, 404); 206 | } 207 | return c.json({ data: data[0] }); 208 | } 209 | ) 210 | .post( 211 | "/", 212 | verifyAuth(), 213 | zValidator( 214 | "json", 215 | projectInsertSchema.pick({ 216 | name: true, 217 | json: true, 218 | height: true, 219 | width: true, 220 | }) 221 | ), 222 | async (c) => { 223 | const auth = c.get("authUser"); 224 | const { name, json, height, width } = c.req.valid("json"); 225 | if (!auth.token?.id) { 226 | return c.json({ error: "Unauthorized" }, 401); 227 | } 228 | const data = await db 229 | .insert(projects) 230 | // @ts-ignore 231 | .values({ 232 | name, 233 | json, 234 | height, 235 | width, 236 | userId: auth.token.id, 237 | createdAt: new Date(), 238 | updatedAt: new Date(), 239 | }) 240 | .returning(); 241 | 242 | if (!data[0]) { 243 | return c.json({ error: "Something went wrong" }, 400); 244 | } 245 | return c.json({ data: data[0] }); 246 | } 247 | ); 248 | 249 | export default app; 250 | -------------------------------------------------------------------------------- /src/app/api/[[...route]]/route.ts: -------------------------------------------------------------------------------- 1 | import { Context, Hono } from "hono"; 2 | import { handle } from "hono/vercel"; 3 | 4 | import { AuthConfig, initAuthConfig } from "@hono/auth-js"; 5 | 6 | import ai from "./ai"; 7 | import users from "./users"; 8 | import images from "./images"; 9 | import projects from "./projects"; 10 | import authConfig from "@/auth.config"; 11 | // Revert to "edge" if planning on running on the edge 12 | export const runtime = "nodejs"; 13 | 14 | function getAuthConfig(c: Context): AuthConfig { 15 | return { 16 | secret: process.env.AUTH_SECRET, 17 | ...(authConfig as any), 18 | }; 19 | } 20 | 21 | const app = new Hono().basePath("/api"); 22 | 23 | app.use("*", initAuthConfig(getAuthConfig)); 24 | 25 | const routes = app.route("/ai", ai).route("/users", users).route("/images", images).route("/projects", projects); 26 | 27 | export const GET = handle(app); 28 | export const POST = handle(app); 29 | export const PATCH = handle(app); 30 | export const DELETE = handle(app); 31 | 32 | export type AppType = typeof routes; 33 | -------------------------------------------------------------------------------- /src/app/api/[[...route]]/users.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import bcrypt from "bcryptjs"; 3 | import { Hono } from "hono"; 4 | import { zValidator } from "@hono/zod-validator"; 5 | import { db } from "@/db/drizzle"; 6 | import { users } from "@/db/schema"; 7 | import { eq } from "drizzle-orm"; 8 | 9 | const app = new Hono().post( 10 | "/", 11 | zValidator( 12 | "json", 13 | z.object({ 14 | name: z.string(), 15 | email: z.string().email(), 16 | password: z.string().min(3).max(20), 17 | }) 18 | ), 19 | async (c) => { 20 | const { name, email, password } = c.req.valid("json"); 21 | 22 | const hashedPassword = await bcrypt.hash(password, 12); 23 | 24 | const query = await db.select().from(users).where(eq(users.email, email)); 25 | 26 | if (query.length > 0) { 27 | return c.json({ 28 | error: "Email already in use", 29 | }, 400); 30 | } 31 | 32 | await db.insert(users).values({ 33 | name, 34 | email, 35 | password: hashedPassword, 36 | }); 37 | 38 | return c.json(null, 200); 39 | } 40 | ); 41 | 42 | export default app; 43 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from "@/auth" 2 | export const { GET, POST } = handlers -------------------------------------------------------------------------------- /src/app/api/uploadthing/core.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@/auth"; 2 | import { createUploadthing, type FileRouter } from "uploadthing/next"; 3 | import { UploadThingError } from "uploadthing/server"; 4 | 5 | const f = createUploadthing(); 6 | 7 | export const ourFileRouter = { 8 | imageUploader: f({ image: { maxFileSize: "4MB" } }) 9 | .middleware(async ({ req }) => { 10 | // TODO: replace with next auth 11 | const session = await auth() 12 | 13 | if (!session) throw new UploadThingError("Unauthorized"); 14 | 15 | return { userId: session.user?.id }; 16 | }) 17 | .onUploadComplete(async ({ metadata, file }) => { 18 | return { url: file.url }; 19 | }), 20 | } satisfies FileRouter; 21 | 22 | export type OurFileRouter = typeof ourFileRouter; 23 | -------------------------------------------------------------------------------- /src/app/api/uploadthing/route.ts: -------------------------------------------------------------------------------- 1 | import { createRouteHandler } from "uploadthing/next"; 2 | 3 | import { ourFileRouter } from "./core"; 4 | 5 | // Export routes for Next App Router 6 | export const { GET, POST } = createRouteHandler({ 7 | router: ourFileRouter, 8 | 9 | // Apply an (optional) custom config: 10 | // config: { ... }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/app/editor/[projectId]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useGetProject } from "@/features/projects/api/use-get-project"; 4 | import { Editor } from "@/features/editor/components/editor"; 5 | import { Loader, TriangleAlert } from "lucide-react"; 6 | import { Button } from "@/components/ui/button"; 7 | import Link from "next/link"; 8 | 9 | interface EditorProjectIdPageProps { 10 | params: { 11 | projectId: string; 12 | }; 13 | } 14 | 15 | const EditorProjectIdPage = ({ params }: EditorProjectIdPageProps) => { 16 | const { data, isLoading, isError } = useGetProject(params.projectId); 17 | 18 | if (isLoading || !data) 19 | return ( 20 |
21 | 22 |
23 | ); 24 | if (isError) 25 | return ( 26 |
27 | 28 |

Failed to fetch project

29 | 32 |
33 | ); 34 | 35 | return ; 36 | }; 37 | 38 | export default EditorProjectIdPage; 39 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhengchchen/image-ai/26305374f6b9d912d478700e3f6827bf8c27c5a4/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhengchchen/image-ai/26305374f6b9d912d478700e3f6827bf8c27c5a4/src/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /src/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhengchchen/image-ai/26305374f6b9d912d478700e3f6827bf8c27c5a4/src/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body { 7 | height: 100%; 8 | } 9 | 10 | :root { 11 | --background: #ffffff; 12 | --foreground: #171717; 13 | } 14 | 15 | @media (prefers-color-scheme: dark) { 16 | :root { 17 | --background: #0a0a0a; 18 | --foreground: #ededed; 19 | } 20 | } 21 | 22 | body { 23 | color: var(--foreground); 24 | background: var(--background); 25 | font-family: Arial, Helvetica, sans-serif; 26 | } 27 | 28 | @layer utilities { 29 | .text-balance { 30 | text-wrap: balance; 31 | } 32 | } 33 | 34 | @layer base { 35 | :root { 36 | --background: 0 0% 100%; 37 | --foreground: 222.2 84% 4.9%; 38 | --card: 0 0% 100%; 39 | --card-foreground: 222.2 84% 4.9%; 40 | --popover: 0 0% 100%; 41 | --popover-foreground: 222.2 84% 4.9%; 42 | --primary: 222.2 47.4% 11.2%; 43 | --primary-foreground: 210 40% 98%; 44 | --secondary: 210 40% 96.1%; 45 | --secondary-foreground: 222.2 47.4% 11.2%; 46 | --muted: 210 40% 96.1%; 47 | --muted-foreground: 215.4 16.3% 46.9%; 48 | --accent: 210 40% 96.1%; 49 | --accent-foreground: 222.2 47.4% 11.2%; 50 | --destructive: 0 84.2% 60.2%; 51 | --destructive-foreground: 210 40% 98%; 52 | --border: 214.3 31.8% 91.4%; 53 | --input: 214.3 31.8% 91.4%; 54 | --ring: 222.2 84% 4.9%; 55 | --chart-1: 12 76% 61%; 56 | --chart-2: 173 58% 39%; 57 | --chart-3: 197 37% 24%; 58 | --chart-4: 43 74% 66%; 59 | --chart-5: 27 87% 67%; 60 | --radius: 0.5rem; 61 | } 62 | .dark { 63 | --background: 222.2 84% 4.9%; 64 | --foreground: 210 40% 98%; 65 | --card: 222.2 84% 4.9%; 66 | --card-foreground: 210 40% 98%; 67 | --popover: 222.2 84% 4.9%; 68 | --popover-foreground: 210 40% 98%; 69 | --primary: 210 40% 98%; 70 | --primary-foreground: 222.2 47.4% 11.2%; 71 | --secondary: 217.2 32.6% 17.5%; 72 | --secondary-foreground: 210 40% 98%; 73 | --muted: 217.2 32.6% 17.5%; 74 | --muted-foreground: 215 20.2% 65.1%; 75 | --accent: 217.2 32.6% 17.5%; 76 | --accent-foreground: 210 40% 98%; 77 | --destructive: 0 62.8% 30.6%; 78 | --destructive-foreground: 210 40% 98%; 79 | --border: 217.2 32.6% 17.5%; 80 | --input: 217.2 32.6% 17.5%; 81 | --ring: 212.7 26.8% 83.9%; 82 | --chart-1: 220 70% 50%; 83 | --chart-2: 160 60% 45%; 84 | --chart-3: 30 80% 55%; 85 | --chart-4: 280 65% 60%; 86 | --chart-5: 340 75% 55%; 87 | } 88 | } 89 | 90 | @layer base { 91 | * { 92 | @apply border-border; 93 | } 94 | body { 95 | @apply bg-background text-foreground; 96 | } 97 | } 98 | 99 | .circle-picker { 100 | width: auto !important; 101 | } 102 | 103 | .chrome-picker { 104 | width: auto !important; 105 | box-shadow: none !important; 106 | border-radius: 0.5rem !important; 107 | overflow: hidden !important; 108 | } 109 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Toaster } from "sonner"; 3 | import "./globals.css"; 4 | 5 | import { Inter } from "next/font/google"; 6 | 7 | import { Providers } from "@/components/provides"; 8 | import { auth } from "@/auth"; 9 | import { SessionProvider } from "next-auth/react"; 10 | import { Modals } from "@/components/modals"; 11 | import { Analytics } from "@vercel/analytics/react" 12 | 13 | const inter = Inter({ 14 | subsets: ["latin"], 15 | }); 16 | 17 | export const metadata: Metadata = { 18 | title: "Image AI - AI-Powered Image Editor & Generator", 19 | description: 20 | "Transform your images with AI magic. Create, edit, and enhance photos with advanced AI filters, one-click beautification, and AI image generation.", 21 | keywords: "AI image editor, online photo editor, AI filters, image generation, photo enhancement", 22 | openGraph: { 23 | title: "ImageGicAI - AI-Powered Image Editor & Generator", 24 | description: "Transform your images with AI magic", 25 | images: ["/og-image.png"], 26 | }, 27 | twitter: { 28 | card: "summary_large_image", 29 | title: "ImageGicAI - AI-Powered Image Editor", 30 | description: "Transform your images with AI magic", 31 | }, 32 | }; 33 | 34 | export default async function RootLayout({ 35 | children, 36 | }: Readonly<{ 37 | children: React.ReactNode; 38 | }>) { 39 | const session = await auth(); 40 | return ( 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | {children} 49 | 50 | 51 | 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/auth.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextAuthConfig } from "next-auth"; 2 | 3 | import { JWT } from "next-auth/jwt"; 4 | 5 | import Credentials from "next-auth/providers/credentials"; 6 | import Github from "next-auth/providers/github"; 7 | import Google from "next-auth/providers/google"; 8 | import { DrizzleAdapter } from "@auth/drizzle-adapter"; 9 | import { db } from "@/db/drizzle"; 10 | import bcrypt from "bcryptjs"; 11 | import { eq } from "drizzle-orm"; 12 | 13 | import { users, accounts } from "@/db/schema"; 14 | import { z } from "zod"; 15 | 16 | declare module "next-auth/jwt" { 17 | interface JWT { 18 | id: string | undefined; 19 | } 20 | } 21 | 22 | const CredentialsSchema = z.object({ 23 | email: z.string().email(), 24 | password: z.string(), 25 | }); 26 | 27 | export default { 28 | providers: [ 29 | Credentials({ 30 | credentials: { 31 | email: { label: "Email", type: "email" }, 32 | password: { label: "Password", type: "password" }, 33 | }, 34 | async authorize(credentials) { 35 | const validatedFields = CredentialsSchema.safeParse(credentials); 36 | if (!validatedFields.success) { 37 | return null; 38 | } 39 | 40 | const { email, password } = validatedFields.data 41 | 42 | const query = await db.select().from(users).where(eq(users.email,email)) 43 | 44 | const user = query[0] 45 | 46 | if(!user || !user.password){ 47 | return null 48 | } 49 | 50 | const passwordsMatch = await bcrypt.compare(password, user.password) 51 | 52 | if(!passwordsMatch){ 53 | return null 54 | } 55 | 56 | return user 57 | }, 58 | }), 59 | Github, 60 | Google], 61 | adapter: DrizzleAdapter(db, { 62 | usersTable: users, 63 | accountsTable: accounts, 64 | }), 65 | pages: { 66 | signIn: "/sign-in", 67 | error: "/sign-in", 68 | }, 69 | session: { 70 | strategy: "jwt", 71 | }, 72 | callbacks:{ 73 | jwt({ token, user }) { 74 | if (user) { 75 | token.id = user.id; 76 | } 77 | return token; 78 | }, 79 | session({ session, token }) { 80 | if (token.id) { 81 | session.user.id = token.id; 82 | } 83 | return session; 84 | }, 85 | }, 86 | } satisfies NextAuthConfig; 87 | -------------------------------------------------------------------------------- /src/auth.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | 3 | import authConfig from "@/auth.config"; 4 | 5 | export const { handlers, signIn, signOut, auth } = NextAuth(authConfig); 6 | -------------------------------------------------------------------------------- /src/components/hint.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; 2 | 3 | export interface HintProps { 4 | label: string; 5 | children: React.ReactNode; 6 | side?: "top" | "bottom" | "left" | "right"; 7 | align?: "start" | "center" | "end"; 8 | sideOffset?: number; 9 | alignOffset?: number; 10 | } 11 | 12 | export const Hint = ({ label, children, side, align, sideOffset, alignOffset }: HintProps) => { 13 | return ( 14 | 15 | 16 | {children} 17 | 24 |

{label}

25 |
26 |
27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/modals.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | 5 | import { SubscriptionModal } from "@/features/subscriptions/components/subscription-modal"; 6 | 7 | export const Modals = () => { 8 | const [isMounted, setIsMounted] = useState(false); 9 | 10 | useEffect(() => { 11 | setIsMounted(true); 12 | }, []); 13 | 14 | if (!isMounted) return null; 15 | 16 | return ( 17 | <> 18 | 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/components/provides.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { QueryProviders } from "./query-provider"; 4 | 5 | interface ProvidersProps { 6 | children: React.ReactNode; 7 | } 8 | 9 | export function Providers({ children }: ProvidersProps) { 10 | return {children}; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/query-provider.tsx: -------------------------------------------------------------------------------- 1 | // In Next.js, this file would be called: app/providers.tsx 2 | "use client"; 3 | 4 | // Since QueryClientProvider relies on useContext under the hood, we have to put 'use client' on top 5 | import { isServer, QueryClient, QueryClientProvider } from "@tanstack/react-query"; 6 | 7 | function makeQueryClient() { 8 | return new QueryClient({ 9 | defaultOptions: { 10 | queries: { 11 | // With SSR, we usually want to set some default staleTime 12 | // above 0 to avoid refetching immediately on the client 13 | staleTime: 60 * 1000, 14 | }, 15 | }, 16 | }); 17 | } 18 | 19 | let browserQueryClient: QueryClient | undefined = undefined; 20 | 21 | function getQueryClient() { 22 | if (isServer) { 23 | // Server: always make a new query client 24 | return makeQueryClient(); 25 | } else { 26 | // Browser: make a new query client if we don't already have one 27 | // This is very important, so we don't re-make a new client if React 28 | // suspends during the initial render. This may not be needed if we 29 | // have a suspense boundary BELOW the creation of the query client 30 | if (!browserQueryClient) browserQueryClient = makeQueryClient(); 31 | return browserQueryClient; 32 | } 33 | } 34 | 35 | interface QueryProvidersProps { 36 | children: React.ReactNode; 37 | } 38 | 39 | export function QueryProviders({ children }: QueryProvidersProps) { 40 | // NOTE: Avoid useState when initializing the query client if you don't 41 | // have a suspense boundary between this and the code that may 42 | // suspend because React will throw away the client on the initial 43 | // render if it suspends and there is no boundary 44 | const queryClient = getQueryClient(); 45 | 46 | return {children}; 47 | } 48 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", 14 | outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 15 | secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", 16 | ghost: "hover:bg-accent hover:text-accent-foreground", 17 | link: "text-primary underline-offset-4 hover:underline", 18 | }, 19 | size: { 20 | default: "h-10 px-4 py-2", 21 | sm: "h-9 rounded-md px-3", 22 | lg: "h-11 rounded-md px-8", 23 | icon: "h-8 w-8", 24 | }, 25 | }, 26 | defaultVariants: { 27 | variant: "default", 28 | size: "default", 29 | }, 30 | } 31 | ); 32 | 33 | export interface ButtonProps 34 | extends React.ButtonHTMLAttributes, 35 | VariantProps { 36 | asChild?: boolean; 37 | } 38 | 39 | const Button = React.forwardRef( 40 | ({ className, variant, size, asChild = false, ...props }, ref) => { 41 | const Comp = asChild ? Slot : "button"; 42 | return ; 43 | } 44 | ); 45 | Button.displayName = "Button"; 46 | 47 | export { Button, buttonVariants }; 48 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLDivElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |
64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogClose, 116 | DialogTrigger, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /src/components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; 5 | import { Check, ChevronRight, Circle } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const DropdownMenu = DropdownMenuPrimitive.Root; 10 | 11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; 12 | 13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group; 14 | 15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal; 16 | 17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub; 18 | 19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; 20 | 21 | const DropdownMenuSubTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef & { 24 | inset?: boolean; 25 | } 26 | >(({ className, inset, children, ...props }, ref) => ( 27 | 36 | {children} 37 | 38 | 39 | )); 40 | DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName; 41 | 42 | const DropdownMenuSubContent = React.forwardRef< 43 | React.ElementRef, 44 | React.ComponentPropsWithoutRef 45 | >(({ className, ...props }, ref) => ( 46 | 54 | )); 55 | DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName; 56 | 57 | const DropdownMenuContent = React.forwardRef< 58 | React.ElementRef, 59 | React.ComponentPropsWithoutRef 60 | >(({ className, sideOffset = 4, ...props }, ref) => ( 61 | 62 | 71 | 72 | )); 73 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; 74 | 75 | const DropdownMenuItem = React.forwardRef< 76 | React.ElementRef, 77 | React.ComponentPropsWithoutRef & { 78 | inset?: boolean; 79 | } 80 | >(({ className, inset, ...props }, ref) => ( 81 | 90 | )); 91 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; 92 | 93 | const DropdownMenuCheckboxItem = React.forwardRef< 94 | React.ElementRef, 95 | React.ComponentPropsWithoutRef 96 | >(({ className, children, checked, ...props }, ref) => ( 97 | 106 | 107 | 108 | 109 | 110 | 111 | {children} 112 | 113 | )); 114 | DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName; 115 | 116 | const DropdownMenuRadioItem = React.forwardRef< 117 | React.ElementRef, 118 | React.ComponentPropsWithoutRef 119 | >(({ className, children, ...props }, ref) => ( 120 | 128 | 129 | 130 | 131 | 132 | 133 | {children} 134 | 135 | )); 136 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; 137 | 138 | const DropdownMenuLabel = React.forwardRef< 139 | React.ElementRef, 140 | React.ComponentPropsWithoutRef & { 141 | inset?: boolean; 142 | } 143 | >(({ className, inset, ...props }, ref) => ( 144 | 149 | )); 150 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; 151 | 152 | const DropdownMenuSeparator = React.forwardRef< 153 | React.ElementRef, 154 | React.ComponentPropsWithoutRef 155 | >(({ className, ...props }, ref) => ( 156 | 157 | )); 158 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; 159 | 160 | const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { 161 | return ; 162 | }; 163 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; 164 | 165 | export { 166 | DropdownMenu, 167 | DropdownMenuTrigger, 168 | DropdownMenuContent, 169 | DropdownMenuItem, 170 | DropdownMenuCheckboxItem, 171 | DropdownMenuRadioItem, 172 | DropdownMenuLabel, 173 | DropdownMenuSeparator, 174 | DropdownMenuShortcut, 175 | DropdownMenuGroup, 176 | DropdownMenuPortal, 177 | DropdownMenuSub, 178 | DropdownMenuSubContent, 179 | DropdownMenuSubTrigger, 180 | DropdownMenuRadioGroup, 181 | }; 182 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ) 18 | } 19 | ) 20 | Input.displayName = "Input" 21 | 22 | export { Input } 23 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /src/components/ui/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 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | 27 | ) 28 | ) 29 | Separator.displayName = SeparatorPrimitive.Root.displayName 30 | 31 | export { Separator } 32 | -------------------------------------------------------------------------------- /src/components/ui/slider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SliderPrimitive from "@radix-ui/react-slider" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Slider = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 21 | 22 | 23 | 24 | 25 | )) 26 | Slider.displayName = SliderPrimitive.Root.displayName 27 | 28 | export { Slider } 29 | -------------------------------------------------------------------------------- /src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTheme } from "next-themes" 4 | import { Toaster as Sonner } from "sonner" 5 | 6 | type ToasterProps = React.ComponentProps 7 | 8 | const Toaster = ({ ...props }: ToasterProps) => { 9 | const { theme = "system" } = useTheme() 10 | 11 | return ( 12 | 28 | ) 29 | } 30 | 31 | export { Toaster } 32 | -------------------------------------------------------------------------------- /src/components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
10 | 15 | 16 | )) 17 | Table.displayName = "Table" 18 | 19 | const TableHeader = React.forwardRef< 20 | HTMLTableSectionElement, 21 | React.HTMLAttributes 22 | >(({ className, ...props }, ref) => ( 23 | 24 | )) 25 | TableHeader.displayName = "TableHeader" 26 | 27 | const TableBody = React.forwardRef< 28 | HTMLTableSectionElement, 29 | React.HTMLAttributes 30 | >(({ className, ...props }, ref) => ( 31 | 36 | )) 37 | TableBody.displayName = "TableBody" 38 | 39 | const TableFooter = React.forwardRef< 40 | HTMLTableSectionElement, 41 | React.HTMLAttributes 42 | >(({ className, ...props }, ref) => ( 43 | tr]:last:border-b-0", 47 | className 48 | )} 49 | {...props} 50 | /> 51 | )) 52 | TableFooter.displayName = "TableFooter" 53 | 54 | const TableRow = React.forwardRef< 55 | HTMLTableRowElement, 56 | React.HTMLAttributes 57 | >(({ className, ...props }, ref) => ( 58 | 66 | )) 67 | TableRow.displayName = "TableRow" 68 | 69 | const TableHead = React.forwardRef< 70 | HTMLTableCellElement, 71 | React.ThHTMLAttributes 72 | >(({ className, ...props }, ref) => ( 73 |
81 | )) 82 | TableHead.displayName = "TableHead" 83 | 84 | const TableCell = React.forwardRef< 85 | HTMLTableCellElement, 86 | React.TdHTMLAttributes 87 | >(({ className, ...props }, ref) => ( 88 | 93 | )) 94 | TableCell.displayName = "TableCell" 95 | 96 | const TableCaption = React.forwardRef< 97 | HTMLTableCaptionElement, 98 | React.HTMLAttributes 99 | >(({ className, ...props }, ref) => ( 100 |
105 | )) 106 | TableCaption.displayName = "TableCaption" 107 | 108 | export { 109 | Table, 110 | TableHeader, 111 | TableBody, 112 | TableFooter, 113 | TableHead, 114 | TableRow, 115 | TableCell, 116 | TableCaption, 117 | } 118 | -------------------------------------------------------------------------------- /src/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 |