{"id":24972928,"url":"https://github.com/aknibircse/rupantor-ai-saas","last_synced_at":"2025-08-30T02:04:19.258Z","repository":{"id":275410748,"uuid":"902566399","full_name":"aknibircse/Rupantor-AI-SaaS","owner":"aknibircse","description":"Here’s your paragraph with emojis added:    A REAL 🌟 Software-as-a-Service (SaaS) App with AI Features 🤖; Payments 💳 \u0026 Credits System 🎟️ that You Might Even Turn Into a Side Income 💰 or Business Idea 💡 Using Next.js 14 🚀, Clerk 🔒, MongoDB 🗂️, Cloudinary AI 🖼️, and Stripe 💵. ","archived":false,"fork":false,"pushed_at":"2025-04-28T23:30:21.000Z","size":550,"stargazers_count":5,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-29T00:31:11.369Z","etag":null,"topics":["clerk-auth","cloudinary-ai","cloudinary-sdk","docker-container","docker-image","image-processing","mongodb","mongodb-atlas","mongoose","newman","nextjs","nginx-server","nodejs","postman","shadcn","stripe-payments","tailwind-css","typescript","vercel-deployment","webhooks-api"],"latest_commit_sha":null,"homepage":"https://rupantor.aknibir.dev","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aknibircse.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-12-12T20:25:19.000Z","updated_at":"2025-04-28T23:30:24.000Z","dependencies_parsed_at":"2025-02-02T23:24:15.179Z","dependency_job_id":null,"html_url":"https://github.com/aknibircse/Rupantor-AI-SaaS","commit_stats":null,"previous_names":["aknibircse/rupantor--an-ai-based-saas","aknibircse/rupantor-ai-saas"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/aknibircse/Rupantor-AI-SaaS","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aknibircse%2FRupantor-AI-SaaS","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aknibircse%2FRupantor-AI-SaaS/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aknibircse%2FRupantor-AI-SaaS/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aknibircse%2FRupantor-AI-SaaS/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aknibircse","download_url":"https://codeload.github.com/aknibircse/Rupantor-AI-SaaS/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aknibircse%2FRupantor-AI-SaaS/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":272793017,"owners_count":24993830,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-30T02:00:09.474Z","response_time":77,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clerk-auth","cloudinary-ai","cloudinary-sdk","docker-container","docker-image","image-processing","mongodb","mongodb-atlas","mongoose","newman","nextjs","nginx-server","nodejs","postman","shadcn","stripe-payments","tailwind-css","typescript","vercel-deployment","webhooks-api"],"created_at":"2025-02-03T18:10:44.106Z","updated_at":"2025-08-30T02:04:19.204Z","avatar_url":"https://github.com/aknibircse.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n    \n  \u003ch1 align=\"center\"\u003eRUPANTOR - AN AI SAAS PLATFORM\u003c/h1\u003e\n\n  \u003cbr/\u003e\n\n  \u003cdiv\u003e\n    \u003cimg src=\"https://img.shields.io/badge/-Next_JS-black?style=for-the-badge\u0026logoColor=white\u0026logo=nextdotjs\u0026color=000000\" alt=\"nextdotjs\" /\u003e\n    \u003cimg src=\"https://img.shields.io/badge/-TypeScript-black?style=for-the-badge\u0026logoColor=white\u0026logo=typescript\u0026color=3178C6\" alt=\"typescript\" /\u003e\n    \u003cimg src=\"https://img.shields.io/badge/-Stripe-black?style=for-the-badge\u0026logoColor=white\u0026logo=stripe\u0026color=008CDD\" alt=\"stripe\" /\u003e\n    \u003cimg src=\"https://img.shields.io/badge/-MongoDB-black?style=for-the-badge\u0026logoColor=white\u0026logo=mongodb\u0026color=47A248\" alt=\"mongodb\" /\u003e\n    \u003cimg src=\"https://img.shields.io/badge/-Tailwind_CSS-black?style=for-the-badge\u0026logoColor=white\u0026logo=tailwindcss\u0026color=06B6D4\" alt=\"tailwindcss\" /\u003e\n  \u003c/div\u003e\u003cbr\u003e\n\n    \n\n   \u003cdiv align=\"center\"\u003e\n     A REAL Software-as-a-Service App with AI Features; Payments \u0026 Credits System that You Might Even Turn Into a Side Income or Business Idea Using Next.js 14, Clerk, MongoDB, Cloudinary AI, and Stripe.\n    \u003c/div\u003e\n\u003c/div\u003e\n\n\u003c/br\u003e\n\n## \u003ca name=\"table\"\u003e📋 TABLE OF CONTENTS\u003c/a\u003e\n\n1. 🤖 [INTRODUCTION](#introduction)\n2. ⚙️ [TECH STACK](#tech-stack)\n3. 🔋 [FEATURES](#features)\n4. 🤸 [QUICK START](#quick-start)\n5. 🐳 [DOCKER SETUP \u0026 USAGE](#docker-setup-and-usage)\n6. 🕸️ [CODE SNIPPETS](#snippets)\n7. 📃 [COPYRIGHTS](#copyright)\n\n\u003c/br\u003e\n\n## \u003ca name=\"introduction\"\u003e🤖 INTRODUCTION\u003c/a\u003e\n\nRUPANTOR is an AI Image SaaS Platform that Excels in Image Processing Capabilities, Integrates a Secure Payment Infrastructure, Offers Advanced Image Search Functionalities, and Supports Multiple AI features, including Image Restoration, Recoloring, Object Removal, Generative Filling, and Background Removal.\n\n\u003c/br\u003e\n\n## \u003ca name=\"tech-stack\"\u003e⚙️ TECH STACK\u003c/a\u003e\n\n- Next JS\n- TypeScript\n- MongoDB\n- Clerk\n- Cloudinary\n- Stripe\n- Shadcn\n- TailwindCSS\n\n\u003c/br\u003e\n\n## \u003ca name=\"features\"\u003e🔋 FEATURES\u003c/a\u003e\n\n👉 **Authentication and Authorization**: Secure User Access with Registration, Login, and Route Protection.\n\n👉 **Community Image Showcase**: Explore User Transformations with Easy Navigation Using Pagination.\n\n👉 **Advanced Image Search**: Find Images by Content or Objects Present Inside the Image Quickly and Accurately.\n\n👉 **Image Restoration**: Revive Old or Damaged Images Effortlessly.\n\n👉 **Image Recoloring**: Customize Images by Replacing Objects with Desired Colors Easily\n\n👉 **Image Generative Fill**: Fill in Missing Areas of Images Seamlessly.\n\n👉 **Object Removal**: Clean Up Images by Removing Unwanted Objects with Precision.\n\n👉 **Background Removal**: Extract Objects From Backgrounds with Ease.\n\n👉 **Download Transformed Images**: Save and Share AI-Transformed Images Conveniently.\n\n👉 **Transformed Image Details**: View Details of Transformations for Each Image.\n\n👉 **Transformation Management**: Control Over Deletion and Updates of Transformations.\n\n👉 **Credits System**: Earn or Purchase Credits for Image Transformations.\n\n👉 **Profile Page**: Access Transformed Images and Credit Information Personally.\n\n👉 **Credits Purchase**: Securely Buy Credits via Stripe for Uninterrupted Use.\n\n👉 **Responsive UI/UX**: A Seamless Experience Across Devices with a User-Friendly Interface.\n\n👉 And Many More, Including Code Architecture and Reusability.\n\n\u003c/br\u003e\n\n## \u003ca name=\"quick-start\"\u003e🤸 QUICK START\u003c/a\u003e\n\nFollow These Steps to Setup The Project Locally on Your Machine.\n\n**Prerequisites**\n\nMake Sure, You have The Following Dependancies Installed on Your Machine:\n\n- [Git](https://git-scm.com/) (Git Version Control)\n- [Node.js](https://nodejs.org/en) (JavaScript Runtime Environment)\n- [nvm](https://github.com/nvm-sh/nvm) (Node Version Manager)\n- [npm](https://www.npmjs.com/) (Node Package Manager)\n- [mongodb](https://www.npmjs.com/) (MongoDB Database)\n\n\u003c/br\u003e\n\n**Cloning The Repository**\n\n```bash\ngit clone https://github.com/aknibircse/Rupantor-AI-SaaS.git\ncd Rupantor-AI-SaaS\n```\n\n\u003c/br\u003e\n\n**Installation**\n\nInstall The Project Dependencies Using NPM: *(* *Reuired NodeJS_V.18.12.0_LTS* *)*\n\n```bash\nnpm install --legacy-peer-deps\n```\n\n\u003c/br\u003e\n\n**Setup Environment Variables**\n\nCreate a New File Named `.env.local` in The Root of Your Project \u0026 Add The Following Variables with Their Values:\n\n```env\n# NEXT.JS CONFIGURATION\n\nNEXT_PUBLIC_CLERK_SIGN_IN_URL = \nNEXT_PUBLIC_CLERK_SIGN_UP_URL = \nNEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL = \nNEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL = \n\n# MONGODB CONFIGURATION\n\nMONGODB_URL = \n\n# CLERK CONFIGURATION\n\nNEXT_PUBLIC_CLERK_PUBLISHABLE_KEY = \nCLERK_SECRET_KEY = \nWEBHOOK_SECRET = \n\n# CLOUDINARY CONFIGURATION\n\nNEXT_PUBLIC_CLOUDINARY_CLOUD_NAME = \nNEXT_PUBLIC_CLOUDINARY_API_KEY = \nNEXT_PUBLIC_CLOUDINARY_API_SECRET = \nNEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET = \nNEXT_PUBLIC_CLOUDINARY_BUCKET_NAME = \n\n# STRIPE CONFIGURATION\n\nNEXT_PUBLIC_STRIPE_WEBHOOK_CHECKOUT_URL = \nNEXT_PUBLIC_STRIPE_SECRET_KEY = \nNEXT_PUBLIC_STRIPE_WEBHOOK_SECRET = \nNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY = \n```\n\u003cbr/\u003e\n\nReplace The Placeholder Values with Your Actual Respective Account Credentials. You can Obtain These Credentials by Signing-up on The ***[Clerk](https://clerk.com/)***, ***[MongoDB](https://www.mongodb.com/)***, ***[Cloudinary](https://cloudinary.com/)*** and ***[Stripe](https://stripe.com)***.\n\n\u003c/br\u003e\n\n**Run This Project In Development Mode**\n\n```bash\nnpm run start:dev\n```\n\n\u003c/br\u003e\n\n**Build Production Version Of This Project**\n\n```bash\nnpm run build:prod\n```\n\n\u003c/br\u003e\n\n**Running The Build Version Of This Project**\n\n```bash\nnpm run start:prod\n```\n\u003cbr/\u003e\n\n**Validate ESLint Rule For Errors \u0026 Warnings**\n\n```bash\nnpm run valid:lint\n```\n\u003c/br\u003e\n\nOpen Your Browser \u0026 View The Project at: ***[http://localhost:3000](http://localhost:3000)***\n\n\u003cbr/\u003e\n\n## 🐳 Docker Setup \u0026 Usage\n\n### Prerequisites\n- Docker Installed On Your System.\n- Docker Compose Installed On Your System.\n- `.env` File With Required Environment Variables.\n\n### Docker Commands\n\n1. **Build \u0026 Run The Application**\n```bash\n# Build \u0026 Start All Services\ndocker compose up --build\n\n# Run In Detached Mode (Background)\ndocker compose up -d --build\n```\n\n2. **Stop The Application**\n```bash\n# Stop All Services\ndocker compose down\n\n# Stop And Remove Volumes\ndocker compose down -v\n```\n\n3. **View Logs**\n```bash\n# View Logs Of All Services\ndocker compose logs\n\n# Follow Logs In Real-Time\ndocker compose logs -f\n\n# View logs Of Specific Service\ndocker compose logs app\ndocker compose logs mongodb\n```\n\n4. **Container Management**\n```bash\n# List Running Containers\ndocker compose ps\n\n# Restart Services\ndocker compose restart\n\n# Restart Specific Service\ndocker compose restart app\n```\n\n### Project Structure\n- `Dockerfile`: Multi-Stage Build Configuration For The Next.js Application.\n- `compose.yaml`: Docker Compose Configuration Defining Services:\n  - `app`: Next.js Application Service.\n  - `mongodb`: MongoDB Database Service.\n  - `nginx`: Reverse Proxy Service. (If Configured)\n\n### Environment Variables\nMake Sure to Create a `.env` File In The Project Root With The Following Variables:\n```plaintext\nNEXT_PUBLIC_CLERK_SIGN_IN_URL=\nNEXT_PUBLIC_CLERK_SIGN_UP_URL=\nNEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=\nNEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=\nMONGODB_URL=\nNEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=\nCLERK_SECRET_KEY=\nWEBHOOK_SECRET=\nNEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=\nNEXT_PUBLIC_CLOUDINARY_API_KEY=\nNEXT_PUBLIC_CLOUDINARY_API_SECRET=\nNEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET=\nNEXT_PUBLIC_CLOUDINARY_BUCKET_NAME=\nNEXT_PUBLIC_STRIPE_WEBHOOK_CHECKOUT_URL=\nNEXT_PUBLIC_STRIPE_SECRET_KEY=\nNEXT_PUBLIC_STRIPE_WEBHOOK_SECRET=\nNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=\n```\n\n### Networking\n- The Application Runs On Port 3000.\n- MongoDB Runs On Default Port 27017.\n- Services Are Connected Through The `rupantor_network` Network.\n\n### Health Checks\n- Built-In Health Checks Are Configured For Both The Application \u0026 MongoDB.\n- Services Will Automatically Restart If They Become Unhealthy.\n\n### Production Deployment\nFor Production Deployment:\n1. Ensure All Environment Variables Are Properly Set.\n2. Use Production-Grade MongoDB Credentials.\n3. Consider Using Docker Volumes For Persistent Data.\n4. Enable \u0026 Configure SSL/TLS For Exposing To The Internet\n\n### Troubleshooting\n1. **Container Fails To Start**\n   - Check Logs: `docker compose logs app`\n   - Verify Environment Variables.\n   - Ensure Ports Are Not In Use.\n\n2. **MongoDB Connection Issues**\n   - Ensure MongoDB Container Is Healthy.\n   - Check Network Connectivity.\n   - Verify MongoDB Credentials.\n\n3. **Performance Issues**\n   - Monitor Container Resources: `docker stats`\n   - Check Application Logs For Bottlenecks.\n   - Consider Scaling Services If Needed.\n\n\u003c/br\u003e\n\n## \u003ca name=\"snippets\"\u003e🕸️ CODE SNIPPETS\u003c/a\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003etailwind.config.ts\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\n/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  darkMode: [\"class\"],\n  content: [\n    \"./pages/**/*.{ts,tsx}\",\n    \"./components/**/*.{ts,tsx}\",\n    \"./app/**/*.{ts,tsx}\",\n    \"./src/**/*.{ts,tsx}\",\n  ],\n  prefix: \"\",\n  theme: {\n    container: {\n      center: true,\n      padding: \"2rem\",\n      screens: {\n        \"2xl\": \"1400px\",\n      },\n    },\n    extend: {\n      colors: {\n        border: \"hsl(var(--border))\",\n        input: \"hsl(var(--input))\",\n        ring: \"hsl(var(--ring))\",\n        background: \"hsl(var(--background))\",\n        foreground: \"hsl(var(--foreground))\",\n        purple: {\n          100: \"#F4F7FE\",\n          200: \"#BCB6FF\",\n          400: \"#868CFF\",\n          500: \"#7857FF\",\n          600: \"#4318FF\",\n        },\n        dark: {\n          400: \"#7986AC\",\n          500: \"#606C80\",\n          600: \"#2B3674\",\n          700: \"#384262\",\n        },\n        primary: {\n          DEFAULT: \"hsl(var(--primary))\",\n          foreground: \"hsl(var(--primary-foreground))\",\n        },\n        secondary: {\n          DEFAULT: \"hsl(var(--secondary))\",\n          foreground: \"hsl(var(--secondary-foreground))\",\n        },\n        destructive: {\n          DEFAULT: \"hsl(var(--destructive))\",\n          foreground: \"hsl(var(--destructive-foreground))\",\n        },\n        muted: {\n          DEFAULT: \"hsl(var(--muted))\",\n          foreground: \"hsl(var(--muted-foreground))\",\n        },\n        accent: {\n          DEFAULT: \"hsl(var(--accent))\",\n          foreground: \"hsl(var(--accent-foreground))\",\n        },\n        popover: {\n          DEFAULT: \"hsl(var(--popover))\",\n          foreground: \"hsl(var(--popover-foreground))\",\n        },\n        card: {\n          DEFAULT: \"hsl(var(--card))\",\n          foreground: \"hsl(var(--card-foreground))\",\n        },\n      },\n      fontFamily: {\n        IBMPlex: [\"var(--font-ibm-plex)\"],\n      },\n      backgroundImage: {\n        \"purple-gradient\": \"url('/assets/images/gradient-bg.svg')\",\n        banner: \"url('/assets/images/banner-bg.png')\",\n      },\n      borderRadius: {\n        lg: \"var(--radius)\",\n        md: \"calc(var(--radius) - 2px)\",\n        sm: \"calc(var(--radius) - 4px)\",\n      },\n      keyframes: {\n        \"accordion-down\": {\n          from: { height: \"0\" },\n          to: { height: \"var(--radix-accordion-content-height)\" },\n        },\n        \"accordion-up\": {\n          from: { height: \"var(--radix-accordion-content-height)\" },\n          to: { height: \"0\" },\n        },\n      },\n      animation: {\n        \"accordion-down\": \"accordion-down 0.2s ease-out\",\n        \"accordion-up\": \"accordion-up 0.2s ease-out\",\n      },\n    },\n  },\n  plugins: [require(\"tailwindcss-animate\")],\n};\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eglobals.css\u003c/code\u003e\u003c/summary\u003e\n\n```css\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n  :root {\n    --background: 0 0% 100%;\n    --foreground: 222.2 84% 4.9%;\n\n    --card: 0 0% 100%;\n    --card-foreground: 222.2 84% 4.9%;\n\n    --popover: 0 0% 100%;\n    --popover-foreground: 222.2 84% 4.9%;\n\n    --primary: 222.2 47.4% 11.2%;\n    --primary-foreground: 210 40% 98%;\n\n    --secondary: 210 40% 96.1%;\n    --secondary-foreground: 222.2 47.4% 11.2%;\n\n    --muted: 210 40% 96.1%;\n    --muted-foreground: 215.4 16.3% 46.9%;\n\n    --accent: 210 40% 96.1%;\n    --accent-foreground: 222.2 47.4% 11.2%;\n\n    --destructive: 0 84.2% 60.2%;\n    --destructive-foreground: 210 40% 98%;\n\n    --border: 214.3 31.8% 91.4%;\n    --input: 214.3 31.8% 91.4%;\n    --ring: 222.2 84% 4.9%;\n\n    --radius: 0.5rem;\n  }\n\n  .dark {\n    --background: 222.2 84% 4.9%;\n    --foreground: 210 40% 98%;\n\n    --card: 222.2 84% 4.9%;\n    --card-foreground: 210 40% 98%;\n\n    --popover: 222.2 84% 4.9%;\n    --popover-foreground: 210 40% 98%;\n\n    --primary: 210 40% 98%;\n    --primary-foreground: 222.2 47.4% 11.2%;\n\n    --secondary: 217.2 32.6% 17.5%;\n    --secondary-foreground: 210 40% 98%;\n\n    --muted: 217.2 32.6% 17.5%;\n    --muted-foreground: 215 20.2% 65.1%;\n\n    --accent: 217.2 32.6% 17.5%;\n    --accent-foreground: 210 40% 98%;\n\n    --destructive: 0 62.8% 30.6%;\n    --destructive-foreground: 210 40% 98%;\n\n    --border: 217.2 32.6% 17.5%;\n    --input: 217.2 32.6% 17.5%;\n    --ring: 212.7 26.8% 83.9%;\n  }\n}\n\n@layer base {\n  * {\n    @apply border-border;\n  }\n  body {\n    @apply bg-background text-foreground;\n  }\n}\n\n.auth {\n  @apply flex-center min-h-screen w-full bg-purple-100\n}\n\n.root {\n  @apply flex min-h-screen w-full flex-col bg-white lg:flex-row;\n}\n\n.root-container {\n  @apply mt-16 flex-1 overflow-auto py-8 lg:mt-0 lg:max-h-screen lg:py-10\n}\n\n/* ========================================== TAILWIND STYLES */\n@layer utilities {\n  /* ===== UTILITIES */\n  .wrapper {\n    @apply max-w-5xl mx-auto px-5 md:px-10 w-full text-dark-400 p-16-regular;\n  }\n\n  .gradient-text {\n    @apply bg-purple-gradient bg-cover bg-clip-text text-transparent;\n  }\n\n  /* ===== ALIGNMENTS */\n  .flex-center {\n    @apply flex justify-center items-center;\n  }\n\n  .flex-between {\n    @apply flex justify-between items-center;\n  }\n\n  /* ===== TYPOGRAPHY */\n  /* 44 */\n  .h1-semibold {\n    @apply text-[36px] font-semibold sm:text-[44px] leading-[120%] sm:leading-[56px];\n  }\n\n  /* 36 */\n  .h2-bold {\n    @apply text-[30px] font-bold md:text-[36px] leading-[110%];\n  }\n\n  /* 30 */\n  .h3-bold {\n    @apply font-bold text-[30px] leading-[140%];\n  }\n\n  /* 24 */\n  .p-24-bold {\n    @apply font-bold text-[24px] leading-[120%];\n  }\n\n  /* 20 */\n  .p-20-semibold {\n    @apply font-semibold text-[20px] leading-[140%];\n  }\n\n  .p-20-regular {\n    @apply font-normal text-[20px] leading-[140%];\n  }\n\n  /* 18 */\n  .p-18-semibold {\n    @apply font-semibold text-[18px] leading-[140%];\n  }\n\n  /* 16 */\n  .p-16-semibold {\n    @apply font-semibold text-[16px] leading-[140%];\n  }\n\n  .p-16-medium {\n    @apply font-medium text-[16px] leading-[140%];\n  }\n\n  .p-16-regular {\n    @apply font-normal text-[16px] leading-[140%];\n  }\n\n  /* 14 */\n  .p-14-medium {\n    @apply font-medium text-[14px] leading-[120%];\n  }\n\n  /* 10 */\n  .p-10-medium {\n    @apply font-medium text-[10px] leading-[140%];\n  }\n\n  /* =====  SHADCN OVERRIDES */\n  .button {\n    @apply py-4 px-6 flex-center gap-3 rounded-full p-16-semibold focus-visible:ring-offset-0 focus-visible:ring-transparent !important;\n  }\n\n  .dropdown-content {\n    @apply shadow-lg rounded-md overflow-hidden p-0;\n  }\n\n  .dropdown-item {\n    @apply p-16-semibold text-dark-700 cursor-pointer transition-all px-4 py-3 rounded-none outline-none hover:border-none focus-visible:ring-transparent hover:text-white hover:bg-purple-gradient hover:bg-cover focus-visible:ring-offset-0 focus-visible:outline-none !important;\n  }\n\n  .input-field {\n    @apply rounded-[16px] border-2 border-purple-200/20 shadow-sm shadow-purple-200/15 text-dark-600 disabled:opacity-100 p-16-semibold h-[50px] md:h-[54px] focus-visible:ring-offset-0 px-4 py-3 focus-visible:ring-transparent !important;\n  }\n\n  .search-field {\n    @apply border-0 bg-transparent text-dark-600 w-full placeholder:text-dark-400 h-[50px] p-16-medium focus-visible:ring-offset-0 p-3 focus-visible:ring-transparent !important;\n  }\n\n  .submit-button {\n    @apply bg-purple-gradient bg-cover rounded-full py-4 px-6 p-16-semibold h-[50px] w-full md:h-[54px];\n  }\n\n  .select-field {\n    @apply w-full border-2 border-purple-200/20 shadow-sm shadow-purple-200/15 rounded-[16px] h-[50px] md:h-[54px] text-dark-600 p-16-semibold disabled:opacity-100 placeholder:text-dark-400/50 px-4 py-3 focus:ring-offset-0 focus-visible:ring-transparent focus:ring-transparent focus-visible:ring-0 focus-visible:outline-none !important;\n  }\n\n  .select-trigger {\n    @apply flex items-center  gap-2 py-5 capitalize focus-visible:outline-none;\n  }\n\n  .select-item {\n    @apply py-3 cursor-pointer hover:bg-purple-100;\n  }\n\n  .IconButton {\n    @apply focus-visible:ring-transparent focus:ring-offset-0 focus-visible:ring-offset-0 focus-visible:outline-none focus-visible:border-none !important;\n  }\n\n  .sheet-content button {\n    @apply focus:ring-0 focus-visible:ring-transparent focus:ring-offset-0 focus-visible:ring-offset-0 focus-visible:outline-none focus-visible:border-none !important;\n  }\n\n  .success-toast {\n    @apply bg-green-100 text-green-900;\n  }\n\n  .error-toast {\n    @apply bg-red-100 text-red-900;\n  }\n\n  /* HOME PAGE */\n  .home {\n    @apply sm:flex-center hidden h-72 flex-col gap-4 rounded-[20px] border bg-banner bg-cover bg-no-repeat p-10 shadow-inner;\n  }\n\n  .home-heading {\n    @apply h1-semibold max-w-[500px] flex-wrap text-center text-white shadow-sm;\n  }\n\n  /* CREDITS PAGE */\n  .credits-list {\n    @apply mt-11 grid grid-cols-1 gap-5 sm:grid-cols-2 md:gap-9 xl:grid-cols-3;\n  }\n\n  .credits-item {\n    @apply w-full rounded-[16px] border-2 border-purple-200/20 bg-white p-8 shadow-xl shadow-purple-200/20 lg:max-w-none;\n  }\n\n  .credits-btn {\n    @apply w-full rounded-full bg-purple-100 bg-cover text-purple-500 hover:text-purple-500;\n  }\n\n  /* PROFILE PAGE*/\n  .profile {\n    @apply mt-5 flex flex-col gap-5 sm:flex-row md:mt-8 md:gap-10;\n  }\n\n  .profile-balance {\n    @apply w-full rounded-[16px] border-2 border-purple-200/20 bg-white p-5 shadow-lg shadow-purple-200/10 md:px-6 md:py-8;\n  }\n\n  .profile-image-manipulation {\n    @apply w-full rounded-[16px] border-2 border-purple-200/20 bg-white p-5 shadow-lg shadow-purple-200/10 md:px-6 md:py-8;\n  }\n\n  /* TRANSFORMATION DETAILS */\n  .transformation-grid {\n    @apply grid h-fit min-h-[200px] grid-cols-1 gap-5 py-8 md:grid-cols-2;\n  }\n\n  .transformation-original_image {\n    @apply h-fit min-h-72 w-full rounded-[10px] border border-dashed bg-purple-100/20 object-cover p-2;\n  }\n\n  /* COLLECTION COMPONENT */\n  .collection-heading {\n    @apply md:flex-between mb-6 flex flex-col gap-5 md:flex-row;\n  }\n\n  .collection-list {\n    @apply grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-3;\n  }\n\n  .collection-empty {\n    @apply flex-center h-60 w-full rounded-[10px] border border-dark-400/10 bg-white/20;\n  }\n\n  .collection-btn {\n    @apply button w-32 bg-purple-gradient bg-cover text-white;\n  }\n\n  .collection-card {\n    @apply flex flex-1 cursor-pointer flex-col gap-5 rounded-[16px] border-2 border-purple-200/15 bg-white p-4 shadow-xl shadow-purple-200/10 transition-all hover:shadow-purple-200/20;\n  }\n\n  /* MEDIA UPLOADER COMPONENT */\n  .media-uploader_cldImage {\n    @apply h-fit min-h-72 w-full rounded-[10px] border border-dashed bg-purple-100/20 object-cover p-2;\n  }\n\n  .media-uploader_cta {\n    @apply flex-center flex h-72 cursor-pointer flex-col gap-5 rounded-[16px] border border-dashed bg-purple-100/20 shadow-inner;\n  }\n\n  .media-uploader_cta-image {\n    @apply rounded-[16px] bg-white  p-5 shadow-sm shadow-purple-200/50;\n  }\n\n  /* NAVBAR COMPONENT */\n  .header {\n    @apply flex-between fixed h-16 w-full border-b-4 border-purple-100 bg-white p-5 lg:hidden;\n  }\n\n  .header-nav_elements {\n    @apply mt-8 flex w-full flex-col items-start gap-5;\n  }\n\n  /* SEARCH COMPONENT */\n  .search {\n    @apply flex w-full rounded-[16px] border-2 border-purple-200/20 bg-white px-4 shadow-sm shadow-purple-200/15 md:max-w-96;\n  }\n\n  /* SIDEBAR COMPONENT */\n  .sidebar {\n    @apply hidden h-screen w-72 bg-white p-5 shadow-md shadow-purple-200/50 lg:flex;\n  }\n\n  .sidebar-logo {\n    @apply flex items-center gap-2 md:py-2;\n  }\n\n  .sidebar-nav {\n    @apply h-full flex-col justify-between md:flex md:gap-4;\n  }\n\n  .sidebar-nav_elements {\n    @apply hidden w-full flex-col items-start gap-2 md:flex;\n  }\n\n  .sidebar-nav_element {\n    @apply flex-center p-16-semibold w-full whitespace-nowrap rounded-full bg-cover  transition-all hover:bg-purple-100 hover:shadow-inner;\n  }\n\n  .sidebar-link {\n    @apply p-16-semibold flex size-full gap-4 p-4;\n  }\n\n  /* TRANSFORMATION COMPONENT */\n  .prompt-field {\n    @apply flex flex-col gap-5 lg:flex-row lg:gap-10;\n  }\n\n  .media-uploader-field {\n    @apply grid h-fit min-h-[200px] grid-cols-1 gap-5 py-4 md:grid-cols-2;\n  }\n\n  /* TRANSFORMED IMAGE COMPONENT */\n  .download-btn {\n    @apply p-14-medium mt-2 flex items-center gap-2 px-2;\n  }\n\n  .transformed-image {\n    @apply h-fit min-h-72 w-full rounded-[10px] border border-dashed bg-purple-100/20 object-cover p-2;\n  }\n\n  .transforming-loader {\n    @apply flex-center absolute left-[50%] top-[50%] size-full -translate-x-1/2 -translate-y-1/2 flex-col gap-2 rounded-[10px] border bg-dark-700/90;\n  }\n\n  .transformed-placeholder {\n    @apply flex-center p-14-medium h-full min-h-72 flex-col gap-5 rounded-[16px] border border-dashed bg-purple-100/20 shadow-inner;\n  }\n}\n\n/* =====  CLERK OVERRIDES */\n.cl-userButtonBox {\n  display: flex;\n  flex-flow: row-reverse;\n  gap: 12px;\n}\n\n.cl-userButtonOuterIdentifier {\n  font-size: 16px;\n  font-weight: 600;\n  color: #384262;\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003econstants/index.ts\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nexport const navLinks = [\n  {\n    label: \"Home\",\n    route: \"/\",\n    icon: \"/assets/icons/home.svg\",\n  },\n  {\n    label: \"Image Restore\",\n    route: \"/transformations/add/restore\",\n    icon: \"/assets/icons/image.svg\",\n  },\n  {\n    label: \"Generative Fill\",\n    route: \"/transformations/add/fill\",\n    icon: \"/assets/icons/stars.svg\",\n  },\n  {\n    label: \"Object Remove\",\n    route: \"/transformations/add/remove\",\n    icon: \"/assets/icons/scan.svg\",\n  },\n  {\n    label: \"Object Recolor\",\n    route: \"/transformations/add/recolor\",\n    icon: \"/assets/icons/filter.svg\",\n  },\n  {\n    label: \"Background Remove\",\n    route: \"/transformations/add/removeBackground\",\n    icon: \"/assets/icons/camera.svg\",\n  },\n  {\n    label: \"Profile\",\n    route: \"/profile\",\n    icon: \"/assets/icons/profile.svg\",\n  },\n  {\n    label: \"Buy Credits\",\n    route: \"/credits\",\n    icon: \"/assets/icons/bag.svg\",\n  },\n];\n\nexport const plans = [\n  {\n    _id: 1,\n    name: \"Free\",\n    icon: \"/assets/icons/free-plan.svg\",\n    price: 0,\n    credits: 20,\n    inclusions: [\n      {\n        label: \"20 Free Credits\",\n        isIncluded: true,\n      },\n      {\n        label: \"Basic Access to Services\",\n        isIncluded: true,\n      },\n      {\n        label: \"Priority Customer Support\",\n        isIncluded: false,\n      },\n      {\n        label: \"Priority Updates\",\n        isIncluded: false,\n      },\n    ],\n  },\n  {\n    _id: 2,\n    name: \"Pro Package\",\n    icon: \"/assets/icons/free-plan.svg\",\n    price: 40,\n    credits: 120,\n    inclusions: [\n      {\n        label: \"120 Credits\",\n        isIncluded: true,\n      },\n      {\n        label: \"Full Access to Services\",\n        isIncluded: true,\n      },\n      {\n        label: \"Priority Customer Support\",\n        isIncluded: true,\n      },\n      {\n        label: \"Priority Updates\",\n        isIncluded: false,\n      },\n    ],\n  },\n  {\n    _id: 3,\n    name: \"Premium Package\",\n    icon: \"/assets/icons/free-plan.svg\",\n    price: 199,\n    credits: 2000,\n    inclusions: [\n      {\n        label: \"2000 Credits\",\n        isIncluded: true,\n      },\n      {\n        label: \"Full Access to Services\",\n        isIncluded: true,\n      },\n      {\n        label: \"Priority Customer Support\",\n        isIncluded: true,\n      },\n      {\n        label: \"Priority Updates\",\n        isIncluded: true,\n      },\n    ],\n  },\n];\n\nexport const transformationTypes = {\n  restore: {\n    type: \"restore\",\n    title: \"Restore Image\",\n    subTitle: \"Refining Images by Removing Noise \u0026 Imperfections\",\n    config: { restore: true },\n    icon: \"image.svg\",\n  },\n  removeBackground: {\n    type: \"removeBackground\",\n    title: \"Background Remove\",\n    subTitle: \"Removes The Background Of The Image Using AI\",\n    config: { removeBackground: true },\n    icon: \"camera.svg\",\n  },\n  fill: {\n    type: \"fill\",\n    title: \"Generative Fill\",\n    subTitle: \"Enhance An Image's Dimensions Using AI Outpainting\",\n    config: { fillBackground: true },\n    icon: \"stars.svg\",\n  },\n  remove: {\n    type: \"remove\",\n    title: \"Object Remove\",\n    subTitle: \"Identify and Eliminate Objects From Images\",\n    config: {\n      remove: { prompt: \"\", removeShadow: true, multiple: true },\n    },\n    icon: \"scan.svg\",\n  },\n  recolor: {\n    type: \"recolor\",\n    title: \"Object Recolor\",\n    subTitle: \"Identify \u0026 Recolor Objects From A Image\",\n    config: {\n      recolor: { prompt: \"\", to: \"\", multiple: true },\n    },\n    icon: \"filter.svg\",\n  },\n};\n\nexport const aspectRatioOptions = {\n  \"1:1\": {\n    aspectRatio: \"1:1\",\n    label: \"Square (1:1)\",\n    width: 1000,\n    height: 1000,\n  },\n  \"3:4\": {\n    aspectRatio: \"3:4\",\n    label: \"Standard Portrait (3:4)\",\n    width: 1000,\n    height: 1334,\n  },\n  \"9:16\": {\n    aspectRatio: \"9:16\",\n    label: \"Phone Portrait (9:16)\",\n    width: 1000,\n    height: 1778,\n  },\n};\n\nexport const defaultValues = {\n  title: \"\",\n  aspectRatio: \"\",\n  color: \"\",\n  prompt: \"\",\n  publicId: \"\",\n};\n\nexport const creditFee = -1;\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003euser.model.ts\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport { Schema, model, models } from \"mongoose\";\n\nconst UserSchema = new Schema({\n  clerkId: {\n    type: String,\n    required: true,\n    unique: true,\n  },\n  email: {\n    type: String,\n    required: true,\n    unique: true,\n  },\n  username: {\n    type: String,\n    required: true,\n    unique: true,\n  },\n  photo: {\n    type: String,\n    required: true,\n  },\n  firstName: {\n    type: String,\n  },\n  lastName: {\n    type: String,\n  },\n  planId: {\n    type: Number,\n    default: 1,\n  },\n  creditBalance: {\n    type: Number,\n    default: 10,\n  },\n});\n\nconst User = models?.User || model(\"User\", UserSchema);\n\nexport default User;\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003etransaction.model.ts\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport { Schema, model, models } from \"mongoose\";\n\nconst TransactionSchema = new Schema({\n  createdAt: {\n    type: Date,\n    default: Date.now,\n  },\n  stripeId: {\n    type: String,\n    required: true,\n    unique: true,\n  },\n  amount: {\n    type: Number,\n    required: true,\n  },\n  plan: {\n    type: String,\n  },\n  credits: {\n    type: Number,\n  },\n  buyer: {\n    type: Schema.Types.ObjectId,\n    ref: \"User\",\n  },\n});\n\nconst Transaction = models?.Transaction || model(\"Transaction\", TransactionSchema);\n\nexport default Transaction;\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eInsufficientCreditsModal.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\n\"use client\";\n\nimport Image from \"next/image\";\nimport { useRouter } from \"next/navigation\";\n\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n} from \"@/components/ui/alert-dialog\";\n\nexport const InsufficientCreditsModal = () =\u003e {\n  const router = useRouter();\n\n  return (\n    \u003cAlertDialog defaultOpen\u003e\n      \u003cAlertDialogContent\u003e\n        \u003cAlertDialogHeader\u003e\n          \u003cdiv className=\"flex-between\"\u003e\n            \u003cp className=\"p-16-semibold text-dark-400\"\u003eInsufficient Credits\u003c/p\u003e\n            \u003cAlertDialogCancel\n              className=\"border-0 p-0 hover:bg-transparent\"\n              onClick={() =\u003e router.push(\"/profile\")}\n            \u003e\n              \u003cImage\n                src=\"/assets/icons/close.svg\"\n                alt=\"credit coins\"\n                width={24}\n                height={24}\n                className=\"cursor-pointer\"\n              /\u003e\n            \u003c/AlertDialogCancel\u003e\n          \u003c/div\u003e\n\n          \u003cImage\n            src=\"/assets/images/stacked-coins.png\"\n            alt=\"credit coins\"\n            width={462}\n            height={122}\n          /\u003e\n\n          \u003cAlertDialogTitle className=\"p-24-bold text-dark-600\"\u003e\n            Oops.... Looks like you\u0026#39;ve run out of free credits!\n          \u003c/AlertDialogTitle\u003e\n\n          \u003cAlertDialogDescription className=\"p-16-regular\"\u003e\n            No worries, though - you can keep enjoying our services by grabbing\n            more credits.\n          \u003c/AlertDialogDescription\u003e\n        \u003c/AlertDialogHeader\u003e\n        \u003cAlertDialogFooter\u003e\n          \u003cAlertDialogCancel\u003eCancel\u003c/AlertDialogCancel\u003e\n          \u003cAlertDialogAction\n            className=\"button w-full bg-purple-gradient  bg-cover\"\n            onClick={() =\u003e router.push(\"/credits\")}\n          \u003e\n            Yes, Proceed\n          \u003c/AlertDialogAction\u003e\n        \u003c/AlertDialogFooter\u003e\n      \u003c/AlertDialogContent\u003e\n    \u003c/AlertDialog\u003e\n  );\n};\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003euser.action.ts\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\n\"use server\";\n\nimport { revalidatePath } from \"next/cache\";\n\nimport User from \"../database/models/user.model\";\nimport { connectToDatabase } from \"../database/mongoose\";\nimport { handleError } from \"../utils\";\n\n// CREATE\nexport async function createUser(user: CreateUserParams) {\n  try {\n    await connectToDatabase();\n\n    const newUser = await User.create(user);\n\n    return JSON.parse(JSON.stringify(newUser));\n  } catch (error) {\n    handleError(error);\n  }\n}\n\n// READ\nexport async function getUserById(userId: string) {\n  try {\n    await connectToDatabase();\n\n    const user = await User.findOne({ clerkId: userId });\n\n    if (!user) throw new Error(\"User not found\");\n\n    return JSON.parse(JSON.stringify(user));\n  } catch (error) {\n    handleError(error);\n  }\n}\n\n// UPDATE\nexport async function updateUser(clerkId: string, user: UpdateUserParams) {\n  try {\n    await connectToDatabase();\n\n    const updatedUser = await User.findOneAndUpdate({ clerkId }, user, {\n      new: true,\n    });\n\n    if (!updatedUser) throw new Error(\"User update failed\");\n    \n    return JSON.parse(JSON.stringify(updatedUser));\n  } catch (error) {\n    handleError(error);\n  }\n}\n\n// DELETE\nexport async function deleteUser(clerkId: string) {\n  try {\n    await connectToDatabase();\n\n    // FIND USER TO DELETE\n    const userToDelete = await User.findOne({ clerkId });\n\n    if (!userToDelete) {\n      throw new Error(\"User not found\");\n    }\n\n    // Delete user\n    const deletedUser = await User.findByIdAndDelete(userToDelete._id);\n    revalidatePath(\"/\");\n\n    return deletedUser ? JSON.parse(JSON.stringify(deletedUser)) : null;\n  } catch (error) {\n    handleError(error);\n  }\n}\n\n// USE CREDITS\nexport async function updateCredits(userId: string, creditFee: number) {\n  try {\n    await connectToDatabase();\n\n    const updatedUserCredits = await User.findOneAndUpdate(\n      { _id: userId },\n      { $inc: { creditBalance: creditFee }},\n      { new: true }\n    )\n\n    if(!updatedUserCredits) throw new Error(\"User credits update failed\");\n\n    return JSON.parse(JSON.stringify(updatedUserCredits));\n  } catch (error) {\n    handleError(error);\n  }\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eutils.ts\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\n/* eslint-disable prefer-const */\n/* eslint-disable no-prototype-builtins */\nimport { type ClassValue, clsx } from \"clsx\";\nimport qs from \"qs\";\nimport { twMerge } from \"tailwind-merge\";\n\nimport { aspectRatioOptions } from \"@/constants\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\n// ERROR HANDLER\nexport const handleError = (error: unknown) =\u003e {\n  if (error instanceof Error) {\n    // This is a native JavaScript error (e.g., TypeError, RangeError)\n    console.error(error.message);\n    throw new Error(`Error: ${error.message}`);\n  } else if (typeof error === \"string\") {\n    // This is a string error message\n    console.error(error);\n    throw new Error(`Error: ${error}`);\n  } else {\n    // This is an unknown type of error\n    console.error(error);\n    throw new Error(`Unknown error: ${JSON.stringify(error)}`);\n  }\n};\n\n// PLACEHOLDER LOADER - WHILE IMAGE IS TRANSFORMING\nconst shimmer = (w: number, h: number) =\u003e `\n\u003csvg width=\"${w}\" height=\"${h}\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\"\u003e\n  \u003cdefs\u003e\n    \u003clinearGradient id=\"g\"\u003e\n      \u003cstop stop-color=\"#7986AC\" offset=\"20%\" /\u003e\n      \u003cstop stop-color=\"#68769e\" offset=\"50%\" /\u003e\n      \u003cstop stop-color=\"#7986AC\" offset=\"70%\" /\u003e\n    \u003c/linearGradient\u003e\n  \u003c/defs\u003e\n  \u003crect width=\"${w}\" height=\"${h}\" fill=\"#7986AC\" /\u003e\n  \u003crect id=\"r\" width=\"${w}\" height=\"${h}\" fill=\"url(#g)\" /\u003e\n  \u003canimate xlink:href=\"#r\" attributeName=\"x\" from=\"-${w}\" to=\"${w}\" dur=\"1s\" repeatCount=\"indefinite\"  /\u003e\n\u003c/svg\u003e`;\n\nconst toBase64 = (str: string) =\u003e\n  typeof window === \"undefined\"\n    ? Buffer.from(str).toString(\"base64\")\n    : window.btoa(str);\n\nexport const dataUrl = `data:image/svg+xml;base64,${toBase64(\n  shimmer(1000, 1000)\n)}`;\n// ==== END\n\n// FORM URL QUERY\nexport const formUrlQuery = ({\n  searchParams,\n  key,\n  value,\n}: FormUrlQueryParams) =\u003e {\n  const params = { ...qs.parse(searchParams.toString()), [key]: value };\n\n  return `${window.location.pathname}?${qs.stringify(params, {\n    skipNulls: true,\n  })}`;\n};\n\n// REMOVE KEY FROM QUERY\nexport function removeKeysFromQuery({\n  searchParams,\n  keysToRemove,\n}: RemoveUrlQueryParams) {\n  const currentUrl = qs.parse(searchParams);\n\n  keysToRemove.forEach((key) =\u003e {\n    delete currentUrl[key];\n  });\n\n  // REMOVE NULL OR UNDEFINED VALUES\n  Object.keys(currentUrl).forEach(\n    (key) =\u003e currentUrl[key] == null \u0026\u0026 delete currentUrl[key]\n  );\n\n  return `${window.location.pathname}?${qs.stringify(currentUrl)}`;\n}\n\n// DEBOUNCE\nexport const debounce = (func: (...args: any[]) =\u003e void, delay: number) =\u003e {\n  let timeoutId: NodeJS.Timeout | null;\n  return (...args: any[]) =\u003e {\n    if (timeoutId) clearTimeout(timeoutId);\n    timeoutId = setTimeout(() =\u003e func.apply(null, args), delay);\n  };\n};\n\n// GE IMAGE SIZE\nexport type AspectRatioKey = keyof typeof aspectRatioOptions;\nexport const getImageSize = (\n  type: string,\n  image: any,\n  dimension: \"width\" | \"height\"\n): number =\u003e {\n  if (type === \"fill\") {\n    return (\n      aspectRatioOptions[image.aspectRatio as AspectRatioKey]?.[dimension] ||\n      1000\n    );\n  }\n  return image?.[dimension] || 1000;\n};\n\n// DOWNLOAD IMAGE\nexport const download = (url: string, filename: string) =\u003e {\n  if (!url) {\n    throw new Error(\"Resource URL not provided! You need to provide one\");\n  }\n\n  fetch(url)\n    .then((response) =\u003e response.blob())\n    .then((blob) =\u003e {\n      const blobURL = URL.createObjectURL(blob);\n      const a = document.createElement(\"a\");\n      a.href = blobURL;\n\n      if (filename \u0026\u0026 filename.length)\n        a.download = `${filename.replace(\" \", \"_\")}.png`;\n      document.body.appendChild(a);\n      a.click();\n    })\n    .catch((error) =\u003e console.log({ error }));\n};\n\n// DEEP MERGE OBJECTS\nexport const deepMergeObjects = (obj1: any, obj2: any) =\u003e {\n  if(obj2 === null || obj2 === undefined) {\n    return obj1;\n  }\n\n  let output = { ...obj2 };\n\n  for (let key in obj1) {\n    if (obj1.hasOwnProperty(key)) {\n      if (\n        obj1[key] \u0026\u0026\n        typeof obj1[key] === \"object\" \u0026\u0026\n        obj2[key] \u0026\u0026\n        typeof obj2[key] === \"object\"\n      ) {\n        output[key] = deepMergeObjects(obj1[key], obj2[key]);\n      } else {\n        output[key] = obj1[key];\n      }\n    }\n  }\n\n  return output;\n};\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003etypes/index.d.ts\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\n/* eslint-disable no-unused-vars */\n\n// ====== USER PARAMS\ndeclare type CreateUserParams = {\n  clerkId: string;\n  email: string;\n  username: string;\n  firstName: string;\n  lastName: string;\n  photo: string;\n};\n\ndeclare type UpdateUserParams = {\n  firstName: string;\n  lastName: string;\n  username: string;\n  photo: string;\n};\n\n// ====== IMAGE PARAMS\ndeclare type AddImageParams = {\n  image: {\n    title: string;\n    publicId: string;\n    transformationType: string;\n    width: number;\n    height: number;\n    config: any;\n    secureURL: string;\n    transformationURL: string;\n    aspectRatio: string | undefined;\n    prompt: string | undefined;\n    color: string | undefined;\n  };\n  userId: string;\n  path: string;\n};\n\ndeclare type UpdateImageParams = {\n  image: {\n    _id: string;\n    title: string;\n    publicId: string;\n    transformationType: string;\n    width: number;\n    height: number;\n    config: any;\n    secureURL: string;\n    transformationURL: string;\n    aspectRatio: string | undefined;\n    prompt: string | undefined;\n    color: string | undefined;\n  };\n  userId: string;\n  path: string;\n};\n\ndeclare type Transformations = {\n  restore?: boolean;\n  fillBackground?: boolean;\n  remove?: {\n    prompt: string;\n    removeShadow?: boolean;\n    multiple?: boolean;\n  };\n  recolor?: {\n    prompt?: string;\n    to: string;\n    multiple?: boolean;\n  };\n  removeBackground?: boolean;\n};\n\n// ====== TRANSACTION PARAMS\ndeclare type CheckoutTransactionParams = {\n  plan: string;\n  credits: number;\n  amount: number;\n  buyerId: string;\n};\n\ndeclare type CreateTransactionParams = {\n  stripeId: string;\n  amount: number;\n  credits: number;\n  plan: string;\n  buyerId: string;\n  createdAt: Date;\n};\n\ndeclare type TransformationTypeKey =\n  | \"restore\"\n  | \"fill\"\n  | \"remove\"\n  | \"recolor\"\n  | \"removeBackground\";\n\n// ====== URL QUERY PARAMS\ndeclare type FormUrlQueryParams = {\n  searchParams: string;\n  key: string;\n  value: string | number | null;\n};\n\ndeclare type UrlQueryParams = {\n  params: string;\n  key: string;\n  value: string | null;\n};\n\ndeclare type RemoveUrlQueryParams = {\n  searchParams: string;\n  keysToRemove: string[];\n};\n\ndeclare type SearchParamProps = {\n  params: { id: string; type: TransformationTypeKey };\n  searchParams: { [key: string]: string | string[] | undefined };\n};\n\ndeclare type TransformationFormProps = {\n  action: \"Add\" | \"Update\";\n  userId: string;\n  type: TransformationTypeKey;\n  creditBalance: number;\n  data?: IImage | null;\n  config?: Transformations | null;\n};\n\ndeclare type TransformedImageProps = {\n  image: any;\n  type: string;\n  title: string;\n  transformationConfig: Transformations | null;\n  isTransforming: boolean;\n  hasDownload?: boolean;\n  setIsTransforming?: React.Dispatch\u003cReact.SetStateAction\u003cboolean\u003e\u003e;\n};\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eapi/webhooks/clerk/route.ts\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\n/* eslint-disable camelcase */\nimport { clerkClient } from \"@clerk/nextjs\";\nimport { WebhookEvent } from \"@clerk/nextjs/server\";\nimport { headers } from \"next/headers\";\nimport { NextResponse } from \"next/server\";\nimport { Webhook } from \"svix\";\n\nimport { createUser, deleteUser, updateUser } from \"@/lib/actions/user.actions\";\n\nexport async function POST(req: Request) {\n  // You can find this in the Clerk Dashboard -\u003e Webhooks -\u003e choose the webhook\n  const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;\n\n  if (!WEBHOOK_SECRET) {\n    throw new Error(\n      \"Please add WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local\"\n    );\n  }\n\n  // GET THE HEADERS\n  const headerPayload = headers();\n  const svix_id = headerPayload.get(\"svix-id\");\n  const svix_timestamp = headerPayload.get(\"svix-timestamp\");\n  const svix_signature = headerPayload.get(\"svix-signature\");\n\n  // IF THERE ARE NO HEADERS, ERROR OUT\n  if (!svix_id || !svix_timestamp || !svix_signature) {\n    return new Response(\"Error occured -- no svix headers\", {\n      status: 400,\n    });\n  }\n\n  // GET THE BODY\n  const payload = await req.json();\n  const body = JSON.stringify(payload);\n\n  // CREATE A NEW Svix INSTANCE WITH YOUR SECRETS.\n  const wh = new Webhook(WEBHOOK_SECRET);\n\n  let evt: WebhookEvent;\n\n  // VERIFY THE PAYLOAD WITH THE HEADERS\n  try {\n    evt = wh.verify(body, {\n      \"svix-id\": svix_id,\n      \"svix-timestamp\": svix_timestamp,\n      \"svix-signature\": svix_signature,\n    }) as WebhookEvent;\n  } catch (err) {\n    console.error(\"Error verifying webhook:\", err);\n    return new Response(\"Error occured\", {\n      status: 400,\n    });\n  }\n\n  // GET THE ID AND TYPE\n  const { id } = evt.data;\n  const eventType = evt.type;\n\n  // CREATE\n  if (eventType === \"user.created\") {\n    const { id, email_addresses, image_url, first_name, last_name, username } = evt.data;\n\n    const user = {\n      clerkId: id,\n      email: email_addresses[0].email_address,\n      username: username!,\n      firstName: first_name,\n      lastName: last_name,\n      photo: image_url,\n    };\n\n    const newUser = await createUser(user);\n\n    // SET PUBLIC METADATA\n    if (newUser) {\n      await clerkClient.users.updateUserMetadata(id, {\n        publicMetadata: {\n          userId: newUser._id,\n        },\n      });\n    }\n\n    return NextResponse.json({ message: \"OK\", user: newUser });\n  }\n\n  // UPDATE\n  if (eventType === \"user.updated\") {\n    const { id, image_url, first_name, last_name, username } = evt.data;\n\n    const user = {\n      firstName: first_name,\n      lastName: last_name,\n      username: username!,\n      photo: image_url,\n    };\n\n    const updatedUser = await updateUser(id, user);\n\n    return NextResponse.json({ message: \"OK\", user: updatedUser });\n  }\n\n  // DELETE\n  if (eventType === \"user.deleted\") {\n    const { id } = evt.data;\n\n    const deletedUser = await deleteUser(id!);\n\n    return NextResponse.json({ message: \"OK\", user: deletedUser });\n  }\n\n  console.log(`Webhook with and ID of ${id} and type of ${eventType}`);\n  console.log(\"Webhook body:\", body);\n\n  return new Response(\"\", { status: 200 });\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003ecomponents/shared/CustomField.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport React from \"react\";\nimport { Control } from \"react-hook-form\";\nimport { z } from \"zod\";\n\nimport {\n  FormField,\n  FormItem,\n  FormControl,\n  FormMessage,\n  FormLabel,\n} from \"../ui/form\";\n\nimport { formSchema } from \"./TransformationForm\";\n\ntype CustomFieldProps = {\n  control: Control\u003cz.infer\u003ctypeof formSchema\u003e\u003e | undefined;\n  render: (props: { field: any }) =\u003e React.ReactNode;\n  name: keyof z.infer\u003ctypeof formSchema\u003e;\n  formLabel?: string;\n  className?: string;\n};\n\nexport const CustomField = ({\n  control,\n  render,\n  name,\n  formLabel,\n  className,\n}: CustomFieldProps) =\u003e {\n  return (\n    \u003cFormField\n      control={control}\n      name={name}\n      render={({ field }) =\u003e (\n        \u003cFormItem className={className}\u003e\n          {formLabel \u0026\u0026 \u003cFormLabel\u003e{formLabel}\u003c/FormLabel\u003e}\n          \u003cFormControl\u003e{render({ field })}\u003c/FormControl\u003e\n          \u003cFormMessage /\u003e\n        \u003c/FormItem\u003e\n      )}\n    /\u003e\n  );\n};\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003ecomponents/shared/Collection.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\n\"use client\";\n\nimport Image from \"next/image\";\nimport Link from \"next/link\";\nimport { useSearchParams, useRouter } from \"next/navigation\";\nimport { CldImage } from \"next-cloudinary\";\n\nimport {\n  Pagination,\n  PaginationContent,\n  PaginationNext,\n  PaginationPrevious,\n} from \"@/components/ui/pagination\";\nimport { transformationTypes } from \"@/constants\";\nimport { IImage } from \"@/lib/database/models/image.model\";\nimport { formUrlQuery } from \"@/lib/utils\";\n\nimport { Button } from \"../ui/button\";\n\nimport { Search } from \"./Search\";\n\nexport const Collection = ({\n  hasSearch = false,\n  images,\n  totalPages = 1,\n  page,\n}: {\n  images: IImage[];\n  totalPages?: number;\n  page: number;\n  hasSearch?: boolean;\n}) =\u003e {\n  const router = useRouter();\n  const searchParams = useSearchParams();\n\n  // PAGINATION HANDLER\n  const onPageChange = (action: string) =\u003e {\n    const pageValue = action === \"next\" ? Number(page) + 1 : Number(page) - 1;\n\n    const newUrl = formUrlQuery({\n      searchParams: searchParams.toString(),\n      key: \"page\",\n      value: pageValue,\n    });\n\n    router.push(newUrl, { scroll: false });\n  };\n\n  return (\n    \u003c\u003e\n      \u003cdiv className=\"collection-heading\"\u003e\n        \u003ch2 className=\"h2-bold text-dark-600\"\u003eRecent Edits\u003c/h2\u003e\n        {hasSearch \u0026\u0026 \u003cSearch /\u003e}\n      \u003c/div\u003e\n\n      {images.length \u003e 0 ? (\n        \u003cul className=\"collection-list\"\u003e\n          {images.map((image) =\u003e (\n            \u003cCard image={image} key={image._id} /\u003e\n          ))}\n        \u003c/ul\u003e\n      ) : (\n        \u003cdiv className=\"collection-empty\"\u003e\n          \u003cp className=\"p-20-semibold\"\u003eEmpty List\u003c/p\u003e\n        \u003c/div\u003e\n      )}\n\n      {totalPages \u003e 1 \u0026\u0026 (\n        \u003cPagination className=\"mt-10\"\u003e\n          \u003cPaginationContent className=\"flex w-full\"\u003e\n            \u003cButton\n              disabled={Number(page) \u003c= 1}\n              className=\"collection-btn\"\n              onClick={() =\u003e onPageChange(\"prev\")}\n            \u003e\n              \u003cPaginationPrevious className=\"hover:bg-transparent hover:text-white\" /\u003e\n            \u003c/Button\u003e\n\n            \u003cp className=\"flex-center p-16-medium w-fit flex-1\"\u003e\n              {page} / {totalPages}\n            \u003c/p\u003e\n\n            \u003cButton\n              className=\"button w-32 bg-purple-gradient bg-cover text-white\"\n              onClick={() =\u003e onPageChange(\"next\")}\n              disabled={Number(page) \u003e= totalPages}\n            \u003e\n              \u003cPaginationNext className=\"hover:bg-transparent hover:text-white\" /\u003e\n            \u003c/Button\u003e\n          \u003c/PaginationContent\u003e\n        \u003c/Pagination\u003e\n      )}\n    \u003c/\u003e\n  );\n};\n\nconst Card = ({ image }: { image: IImage }) =\u003e {\n  return (\n    \u003cli\u003e\n      \u003cLink href={`/transformations/${image._id}`} className=\"collection-card\"\u003e\n        \u003cCldImage\n          src={image.publicId}\n          alt={image.title}\n          width={image.width}\n          height={image.height}\n          {...image.config}\n          loading=\"lazy\"\n          className=\"h-52 w-full rounded-[10px] object-cover\"\n          sizes=\"(max-width: 767px) 100vw, (max-width: 1279px) 50vw, 33vw\"\n        /\u003e\n        \u003cdiv className=\"flex-between\"\u003e\n          \u003cp className=\"p-20-semibold mr-3 line-clamp-1 text-dark-600\"\u003e\n            {image.title}\n          \u003c/p\u003e\n          \u003cImage\n            src={`/assets/icons/${\n              transformationTypes[\n                image.transformationType as TransformationTypeKey\n              ].icon\n            }`}\n            alt={image.title}\n            width={24}\n            height={24}\n          /\u003e\n        \u003c/div\u003e\n      \u003c/Link\u003e\n    \u003c/li\u003e\n  );\n};\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003ecomponents/shared/Search.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\n\"use client\";\n\nimport Image from \"next/image\";\nimport { useRouter, useSearchParams } from \"next/navigation\";\nimport { useEffect, useState } from \"react\";\n\nimport { Input } from \"@/components/ui/input\";\nimport { formUrlQuery, removeKeysFromQuery } from \"@/lib/utils\";\n\nexport const Search = () =\u003e {\n  const router = useRouter();\n  const searchParams = useSearchParams();\n  const [query, setQuery] = useState(\"\");\n\n  useEffect(() =\u003e {\n    const delayDebounceFn = setTimeout(() =\u003e {\n      if (query) {\n        const newUrl = formUrlQuery({\n          searchParams: searchParams.toString(),\n          key: \"query\",\n          value: query,\n        });\n\n        router.push(newUrl, { scroll: false });\n      } else {\n        const newUrl = removeKeysFromQuery({\n          searchParams: searchParams.toString(),\n          keysToRemove: [\"query\"],\n        });\n\n        router.push(newUrl, { scroll: false });\n      }\n    }, 300);\n\n    return () =\u003e clearTimeout(delayDebounceFn);\n  }, [router, searchParams, query]);\n\n  return (\n    \u003cdiv className=\"search\"\u003e\n      \u003cImage\n        src=\"/assets/icons/search.svg\"\n        alt=\"search\"\n        width={24}\n        height={24}\n      /\u003e\n\n      \u003cInput\n        className=\"search-field\"\n        placeholder=\"Search\"\n        onChange={(e) =\u003e setQuery(e.target.value)}\n      /\u003e\n    \u003c/div\u003e\n  );\n};\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eimage.actions.ts\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\n\"use server\";\n\nimport { revalidatePath } from \"next/cache\";\nimport { connectToDatabase } from \"../database/mongoose\";\nimport { handleError } from \"../utils\";\nimport User from \"../database/models/user.model\";\nimport Image from \"../database/models/image.model\";\nimport { redirect } from \"next/navigation\";\n\nimport { v2 as cloudinary } from 'cloudinary'\n\nconst populateUser = (query: any) =\u003e query.populate({\n  path: 'author',\n  model: User,\n  select: '_id firstName lastName clerkId'\n})\n\n// ADD IMAGE\nexport async function addImage({ image, userId, path }: AddImageParams) {\n  try {\n    await connectToDatabase();\n\n    const author = await User.findById(userId);\n\n    if (!author) {\n      throw new Error(\"User not found\");\n    }\n\n    const newImage = await Image.create({\n      ...image,\n      author: author._id,\n    })\n\n    revalidatePath(path);\n\n    return JSON.parse(JSON.stringify(newImage));\n  } catch (error) {\n    handleError(error)\n  }\n}\n\n// UPDATE IMAGE\nexport async function updateImage({ image, userId, path }: UpdateImageParams) {\n  try {\n    await connectToDatabase();\n\n    const imageToUpdate = await Image.findById(image._id);\n\n    if (!imageToUpdate || imageToUpdate.author.toHexString() !== userId) {\n      throw new Error(\"Unauthorized or image not found\");\n    }\n\n    const updatedImage = await Image.findByIdAndUpdate(\n      imageToUpdate._id,\n      image,\n      { new: true }\n    )\n\n    revalidatePath(path);\n\n    return JSON.parse(JSON.stringify(updatedImage));\n  } catch (error) {\n    handleError(error)\n  }\n}\n\n// DELETE IMAGE\nexport async function deleteImage(imageId: string) {\n  try {\n    await connectToDatabase();\n\n    await Image.findByIdAndDelete(imageId);\n  } catch (error) {\n    handleError(error)\n  } finally{\n    redirect('/')\n  }\n}\n\n// GET IMAGE\nexport async function getImageById(imageId: string) {\n  try {\n    await connectToDatabase();\n\n    const image = await populateUser(Image.findById(imageId));\n\n    if(!image) throw new Error(\"Image not found\");\n\n    return JSON.parse(JSON.stringify(image));\n  } catch (error) {\n    handleError(error)\n  }\n}\n\n// GET IMAGES\nexport async function getAllImages({ limit = 9, page = 1, searchQuery = '' }: {\n  limit?: number;\n  page: number;\n  searchQuery?: string;\n}) {\n  try {\n    await connectToDatabase();\n\n    cloudinary.config({\n      cloud_name: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,\n      api_key: process.env.NEXT_PUBLIC_CLOUDINARY_API_KEY,\n      api_secret: process.env.NEXT_PUBLIC_CLOUDINARY_API_SECRET,\n      secure: true,\n    })\n\n    let expression = 'folder=rupantor';\n\n    if (searchQuery) {\n      expression += ` AND ${searchQuery}`\n    }\n\n    const { resources } = await cloudinary.search\n      .expression(expression)\n      .execute();\n\n    const resourceIds = resources.map((resource: any) =\u003e resource.public_id);\n\n    let query = {};\n\n    if(searchQuery) {\n      query = {\n        publicId: {\n          $in: resourceIds\n        }\n      }\n    }\n\n    const skipAmount = (Number(page) -1) * limit;\n\n    const images = await populateUser(Image.find(query))\n      .sort({ updatedAt: -1 })\n      .skip(skipAmount)\n      .limit(limit);\n    \n    const totalImages = await Image.find(query).countDocuments();\n    const savedImages = await Image.find().countDocuments();\n\n    return {\n      data: JSON.parse(JSON.stringify(images)),\n      totalPage: Math.ceil(totalImages / limit),\n      savedImages,\n    }\n  } catch (error) {\n    handleError(error)\n  }\n}\n\n// GET IMAGES BY USER\nexport async function getUserImages({\n  limit = 9,\n  page = 1,\n  userId,\n}: {\n  limit?: number;\n  page: number;\n  userId: string;\n}) {\n  try {\n    await connectToDatabase();\n\n    const skipAmount = (Number(page) - 1) * limit;\n\n    const images = await populateUser(Image.find({ author: userId }))\n      .sort({ updatedAt: -1 })\n      .skip(skipAmount)\n      .limit(limit);\n\n    const totalImages = await Image.find({ author: userId }).countDocuments();\n\n    return {\n      data: JSON.parse(JSON.stringify(images)),\n      totalPages: Math.ceil(totalImages / limit),\n    };\n  } catch (error) {\n    handleError(error);\n  }\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003etransformations/[id]/page.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport { auth } from \"@clerk/nextjs\";\nimport Image from \"next/image\";\nimport Link from \"next/link\";\n\nimport Header from \"@/components/shared/Header\";\nimport TransformedImage from \"@/components/shared/TransformedImage\";\nimport { Button } from \"@/components/ui/button\";\nimport { getImageById } from \"@/lib/actions/image.actions\";\nimport { getImageSize } from \"@/lib/utils\";\nimport { DeleteConfirmation } from \"@/components/shared/DeleteConfirmation\";\n\nconst ImageDetails = async ({ params: { id } }: SearchParamProps) =\u003e {\n  const { userId } = auth();\n\n  const image = await getImageById(id);\n\n  return (\n    \u003c\u003e\n      \u003cHeader title={image.title} /\u003e\n\n      \u003csection className=\"mt-5 flex flex-wrap gap-4\"\u003e\n        \u003cdiv className=\"p-14-medium md:p-16-medium flex gap-2\"\u003e\n          \u003cp className=\"text-dark-600\"\u003eTransformation:\u003c/p\u003e\n          \u003cp className=\" capitalize text-purple-400\"\u003e\n            {image.transformationType}\n          \u003c/p\u003e\n        \u003c/div\u003e\n\n        {image.prompt \u0026\u0026 (\n          \u003c\u003e\n            \u003cp className=\"hidden text-dark-400/50 md:block\"\u003e\u0026#x25CF;\u003c/p\u003e\n            \u003cdiv className=\"p-14-medium md:p-16-medium flex gap-2 \"\u003e\n              \u003cp className=\"text-dark-600\"\u003ePrompt:\u003c/p\u003e\n              \u003cp className=\" capitalize text-purple-400\"\u003e{image.prompt}\u003c/p\u003e\n            \u003c/div\u003e\n          \u003c/\u003e\n        )}\n\n        {image.color \u0026\u0026 (\n          \u003c\u003e\n            \u003cp className=\"hidden text-dark-400/50 md:block\"\u003e\u0026#x25CF;\u003c/p\u003e\n            \u003cdiv className=\"p-14-medium md:p-16-medium flex gap-2\"\u003e\n              \u003cp className=\"text-dark-600\"\u003eColor:\u003c/p\u003e\n              \u003cp className=\" capitalize text-purple-400\"\u003e{image.color}\u003c/p\u003e\n            \u003c/div\u003e\n          \u003c/\u003e\n        )}\n\n        {image.aspectRatio \u0026\u0026 (\n          \u003c\u003e\n            \u003cp className=\"hidden text-dark-400/50 md:block\"\u003e\u0026#x25CF;\u003c/p\u003e\n            \u003cdiv className=\"p-14-medium md:p-16-medium flex gap-2\"\u003e\n              \u003cp className=\"text-dark-600\"\u003eAspect Ratio:\u003c/p\u003e\n              \u003cp className=\" capitalize text-purple-400\"\u003e{image.aspectRatio}\u003c/p\u003e\n            \u003c/div\u003e\n          \u003c/\u003e\n        )}\n      \u003c/section\u003e\n\n      \u003csection className=\"mt-10 border-t border-dark-400/15\"\u003e\n        \u003cdiv className=\"transformation-grid\"\u003e\n          {/* MEDIA UPLOADER */}\n          \u003cdiv className=\"flex flex-col gap-4\"\u003e\n            \u003ch3 className=\"h3-bold text-dark-600\"\u003eOriginal\u003c/h3\u003e\n\n            \u003cImage\n              width={getImageSize(image.transformationType, image, \"width\")}\n              height={getImageSize(image.transformationType, image, \"height\")}\n              src={image.secureURL}\n              alt=\"image\"\n              className=\"transformation-original_image\"\n            /\u003e\n          \u003c/div\u003e\n\n          {/* TRANSFORMED IMAGE */}\n          \u003cTransformedImage\n            image={image}\n            type={image.transformationType}\n            title={image.title}\n            isTransforming={false}\n            transformationConfig={image.config}\n            hasDownload={true}\n          /\u003e\n        \u003c/div\u003e\n\n        {userId === image.author.clerkId \u0026\u0026 (\n          \u003cdiv className=\"mt-4 space-y-4\"\u003e\n            \u003cButton asChild type=\"button\" className=\"submit-button capitalize\"\u003e\n              \u003cLink href={`/transformations/${image._id}/update`}\u003e\n                Update Image\n              \u003c/Link\u003e\n            \u003c/Button\u003e\n\n            \u003cDeleteConfirmation imageId={image._id} /\u003e\n          \u003c/div\u003e\n        )}\n      \u003c/section\u003e\n    \u003c/\u003e\n  );\n};\n\nexport default ImageDetails;\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003etransformations/[id]/update/page.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport { auth } from \"@clerk/nextjs\";\nimport { redirect } from \"next/navigation\";\n\nimport Header from \"@/components/shared/Header\";\nimport TransformationForm from \"@/components/shared/TransformationForm\";\nimport { transformationTypes } from \"@/constants\";\nimport { getUserById } from \"@/lib/actions/user.actions\";\nimport { getImageById } from \"@/lib/actions/image.actions\";\n\nconst Page = async ({ params: { id } }: SearchParamProps) =\u003e {\n  const { userId } = auth();\n\n  if (!userId) redirect(\"/sign-in\");\n\n  const user = await getUserById(userId);\n  const image = await getImageById(id);\n\n  const transformation =\n    transformationTypes[image.transformationType as TransformationTypeKey];\n\n  return (\n    \u003c\u003e\n      \u003cHeader title={transformation.title} subtitle={transformation.subTitle} /\u003e\n\n      \u003csection className=\"mt-10\"\u003e\n        \u003cTransformationForm\n          action=\"Update\"\n          userId={user._id}\n          type={image.transformationType as TransformationTypeKey}\n          creditBalance={user.creditBalance}\n          config={image.config}\n          data={image}\n        /\u003e\n      \u003c/section\u003e\n    \u003c/\u003e\n  );\n};\n\nexport default Page;\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003ecomponents/shared/DeleteConfirmation.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\n\"use client\";\n\nimport { useTransition } from \"react\";\n\nimport {\n  AlertDialog,\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n  AlertDialogTrigger,\n} from \"@/components/ui/alert-dialog\";\nimport { deleteImage } from \"@/lib/actions/image.actions\";\n\nimport { Button } from \"../ui/button\";\n\nexport const DeleteConfirmation = ({ imageId }: { imageId: string }) =\u003e {\n  const [isPending, startTransition] = useTransition();\n\n  return (\n    \u003cAlertDialog\u003e\n      \u003cAlertDialogTrigger asChild className=\"w-full rounded-full\"\u003e\n        \u003cButton\n          type=\"button\"\n          className=\"button h-[44px] w-full md:h-[54px]\"\n          variant=\"destructive\"\n        \u003e\n          Delete Image\n        \u003c/Button\u003e\n      \u003c/AlertDialogTrigger\u003e\n\n      \u003cAlertDialogContent className=\"flex flex-col gap-10\"\u003e\n        \u003cAlertDialogHeader\u003e\n          \u003cAlertDialogTitle\u003e\n            Are you sure you want to delete this image?\n          \u003c/AlertDialogTitle\u003e\n          \u003cAlertDialogDescription className=\"p-16-regular\"\u003e\n            This will permanently delete this image\n          \u003c/AlertDialogDescription\u003e\n        \u003c/AlertDialogHeader\u003e\n\n        \u003cAlertDialogFooter\u003e\n          \u003cAlertDialogCancel\u003eCancel\u003c/AlertDialogCancel\u003e\n          \u003cAlertDialogAction\n            className=\"border bg-red-500 text-white hover:bg-red-600\"\n            onClick={() =\u003e\n              startTransition(async () =\u003e {\n                await deleteImage(imageId);\n              })\n            }\n          \u003e\n            {isPending ? \"Deleting...\" : \"Delete\"}\n          \u003c/AlertDialogAction\u003e\n        \u003c/AlertDialogFooter\u003e\n      \u003c/AlertDialogContent\u003e\n    \u003c/AlertDialog\u003e\n  );\n};\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eapi/webhooks/stripe/route.ts\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\n/* eslint-disable camelcase */\nimport { createTransaction } from \"@/lib/actions/transaction.action\";\nimport { NextResponse } from \"next/server\";\nimport stripe from \"stripe\";\n\nexport async function POST(request: Request) {\n  const body = await request.text();\n\n  const sig = request.headers.get(\"stripe-signature\") as string;\n  const endpointSecret = process.env.NEXT_PUBLIC_STRIPE_WEBHOOK_SECRET!;\n\n  let event;\n\n  try {\n    event = stripe.webhooks.constructEvent(body, sig, endpointSecret);\n  } catch (err) {\n    return NextResponse.json({ message: \"Webhook error\", error: err });\n  }\n\n  // GET THE ID AND TYPE\n  const eventType = event.type;\n\n  // CREATE\n  if (eventType === \"checkout.session.completed\") {\n    const { id, amount_total, metadata } = event.data.object;\n\n    const transaction = {\n      stripeId: id,\n      amount: amount_total ? amount_total / 100 : 0,\n      plan: metadata?.plan || \"\",\n      credits: Number(metadata?.credits) || 0,\n      buyerId: metadata?.buyerId || \"\",\n      createdAt: new Date(),\n    };\n\n    const newTransaction = await createTransaction(transaction);\n    \n    return NextResponse.json({ message: \"OK\", transaction: newTransaction });\n  }\n\n  return new Response(\"\", { status: 200 });\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003ecredits/page.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport { SignedIn, auth } from \"@clerk/nextjs\";\nimport Image from \"next/image\";\nimport { redirect } from \"next/navigation\";\n\nimport Header from \"@/components/shared/Header\";\nimport { Button } from \"@/components/ui/button\";\nimport { plans } from \"@/constants\";\nimport { getUserById } from \"@/lib/actions/user.actions\";\nimport Checkout from \"@/components/shared/Checkout\";\n\nconst Credits = async () =\u003e {\n  const { userId } = auth();\n\n  if (!userId) redirect(\"/sign-in\");\n\n  const user = await getUserById(userId);\n\n  return (\n    \u003c\u003e\n      \u003cHeader\n        title=\"Buy Credits\"\n        subtitle=\"Choose A Credit Package That Suits Your Needs!\"\n      /\u003e\n\n      \u003csection\u003e\n        \u003cul className=\"credits-list\"\u003e\n          {plans.map((plan) =\u003e (\n            \u003cli key={plan.name} className=\"credits-item\"\u003e\n              \u003cdiv className=\"flex-center flex-col gap-3\"\u003e\n                \u003cImage src={plan.icon} alt=\"check\" width={50} height={50} /\u003e\n                \u003cp className=\"p-20-semibold mt-2 text-purple-500\"\u003e\n                  {plan.name}\n                \u003c/p\u003e\n                \u003cp className=\"h1-semibold text-dark-600\"\u003e${plan.price}\u003c/p\u003e\n                \u003cp className=\"p-16-regular\"\u003e{plan.credits} Credits\u003c/p\u003e\n              \u003c/div\u003e\n\n              {/* INCLUSIONS */}\n              \u003cul className=\"flex flex-col gap-5 py-9\"\u003e\n                {plan.inclusions.map((inclusion) =\u003e (\n                  \u003cli\n                    key={plan.name + inclusion.label}\n                    className=\"flex items-center gap-4\"\n                  \u003e\n                    \u003cImage\n                      src={`/assets/icons/${\n                        inclusion.isIncluded ? \"check.svg\" : \"cross.svg\"\n                      }`}\n                      alt=\"check\"\n                      width={24}\n                      height={24}\n                    /\u003e\n                    \u003cp className=\"p-16-regular\"\u003e{inclusion.label}\u003c/p\u003e\n                  \u003c/li\u003e\n                ))}\n              \u003c/ul\u003e\n\n              {plan.name === \"Free\" ? (\n                \u003cButton variant=\"outline\" className=\"credits-btn\"\u003e\n                  Free Consumable\n                \u003c/Button\u003e\n              ) : (\n                \u003cSignedIn\u003e\n                  \u003cCheckout\n                    plan={plan.name}\n                    amount={plan.price}\n                    credits={plan.credits}\n                    buyerId={user._id}\n                  /\u003e\n                \u003c/SignedIn\u003e\n              )}\n            \u003c/li\u003e\n          ))}\n        \u003c/ul\u003e\n      \u003c/section\u003e\n    \u003c/\u003e\n  );\n};\n\nexport default Credits;\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003ecomponents/shared/Checkout.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\n\"use client\";\n\nimport { loadStripe } from \"@stripe/stripe-js\";\nimport { useEffect } from \"react\";\n\nimport { useToast } from \"@/components/ui/use-toast\";\nimport { checkoutCredits } from \"@/lib/actions/transaction.action\";\n\nimport { Button } from \"../ui/button\";\n\nconst Checkout = ({\n  plan,\n  amount,\n  credits,\n  buyerId,\n}: {\n  plan: string;\n  amount: number;\n  credits: number;\n  buyerId: string;\n}) =\u003e {\n  const { toast } = useToast();\n\n  useEffect(() =\u003e {\n    loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!);\n  }, []);\n\n  useEffect(() =\u003e {\n    // Check to see if this is a redirect back from Checkout\n    const query = new URLSearchParams(window.location.search);\n    if (query.get(\"success\")) {\n      toast({\n        title: \"Order placed!\",\n        description: \"You will receive an email confirmation\",\n        duration: 5000,\n        className: \"success-toast\",\n      });\n    }\n\n    if (query.get(\"canceled\")) {\n      toast({\n        title: \"Order canceled!\",\n        description: \"Continue to shop around and checkout when you're ready\",\n        duration: 5000,\n        className: \"error-toast\",\n      });\n    }\n  }, []);\n\n  const onCheckout = async () =\u003e {\n    const transaction = {\n      plan,\n      amount,\n      credits,\n      buyerId,\n    };\n\n    await checkoutCredits(transaction);\n  };\n\n  return (\n    \u003cform action={onCheckout} method=\"POST\"\u003e\n      \u003csection\u003e\n        \u003cButton\n          type=\"submit\"\n          role=\"link\"\n          className=\"w-full rounded-full bg-purple-gradient bg-cover\"\n        \u003e\n          Buy Credit\n        \u003c/Button\u003e\n      \u003c/section\u003e\n    \u003c/form\u003e\n  );\n};\n\nexport default Checkout;\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003eprofile/page.tsx\u003c/code\u003e\u003c/summary\u003e\n\n```typescript\nimport { auth } from \"@clerk/nextjs\";\nimport Image from \"next/image\";\nimport { redirect } from \"next/navigation\";\n\nimport { Collection } from \"@/components/shared/Collection\";\nimport Header from \"@/components/shared/Header\";\nimport { getUserImages } from \"@/lib/actions/image.actions\";\nimport { getUserById } from \"@/lib/actions/user.actions\";\n\nconst Profile = async ({ searchParams }: SearchParamProps) =\u003e {\n  const page = Number(searchParams?.page) || 1;\n  const { userId } = auth();\n\n  if (!userId) redirect(\"/sign-in\");\n\n  const user = await getUserById(userId);\n  const images = await getUserImages({ page, userId: user._id });\n\n  return (\n    \u003c\u003e\n      \u003cHeader title=\"Profile\" /\u003e\n\n      \u003csection className=\"profile\"\u003e\n        \u003cdiv className=\"profile-balance\"\u003e\n          \u003cp className=\"p-14-medium md:p-16-medium\"\u003eCREDITS AVAILABLE\u003c/p\u003e\n          \u003cdiv className=\"mt-4 flex items-center gap-4\"\u003e\n            \u003cImage\n              src=\"/assets/icons/coins.svg\"\n              alt=\"coins\"\n              width={50}\n              height={50}\n              className=\"size-9 md:size-12\"\n            /\u003e\n            \u003ch2 className=\"h2-bold text-dark-600\"\u003e{user.creditBalance}\u003c/h2\u003e\n          \u003c/div\u003e\n        \u003c/div\u003e\n\n        \u003cdiv className=\"profile-image-manipulation\"\u003e\n          \u003cp className=\"p-14-medium md:p-16-medium\"\u003eIMAGE MANIPULATION DONE\u003c/p\u003e\n          \u003cdiv className=\"mt-4 flex items-center gap-4\"\u003e\n            \u003cImage\n              src=\"/assets/icons/photo.svg\"\n              alt=\"coins\"\n              width={50}\n              height={50}\n              className=\"size-9 md:size-12\"\n            /\u003e\n            \u003ch2 className=\"h2-bold text-dark-600\"\u003e{images?.data.length}\u003c/h2\u003e\n          \u003c/div\u003e\n        \u003c/div\u003e\n      \u003c/section\u003e\n\n      \u003csection className=\"mt-8 md:mt-14\"\u003e\n        \u003cCollection\n          images={images?.data}\n          totalPages={images?.totalPages}\n          page={page}\n        /\u003e\n      \u003c/section\u003e\n    \u003c/\u003e\n  );\n};\n\nexport default Profile;\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003enginx.conf\u003c/code\u003e\u003c/summary\u003e\n\n```conf\nuser nginx;\nworker_processes auto;\nerror_log /var/log/nginx/error.log warn;\npid /var/run/nginx.pid;\n\nevents {\n    worker_connections 1024;\n}\n\nhttp {\n    include /etc/nginx/mime.types;\n    default_type application/octet-stream;\n    client_max_body_size 100M;\n\n    # Logging Settings\n    log_format main '$remote_addr - $remote_user [$time_local] \"$request\" '\n                    '$status $body_bytes_sent \"$http_referer\" '\n                    '\"$http_user_agent\" \"$http_x_forwarded_for\"';\n    access_log /var/log/nginx/access.log main;\n\n    # Optimization\n    sendfile on;\n    tcp_nopush on;\n    tcp_nodelay on;\n    keepalive_timeout 65;\n    types_hash_max_size 2048;\n\n    # Gzip Settings\n    gzip on;\n    gzip_disable \"msie6\";\n    gzip_vary on;\n    gzip_proxied any;\n    gzip_comp_level 6;\n    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;\n\n    # Security Headers\n    add_header X-Frame-Options \"SAMEORIGIN\" always;\n    add_header X-XSS-Protection \"1; mode=block\" always;\n    add_header X-Content-Type-Options \"nosniff\" always;\n    add_header Referrer-Policy \"no-referrer-when-downgrade\" always;\n\n    server {\n        listen 80;\n        server_name rupantor.aknibir.dev;\n        root /usr/share/nginx/html;\n        index index.html;\n\n        # Next.js API and Dynamic Routes\n        location / {\n            proxy_pass http://app:3000;\n            proxy_http_version 1.1;\n            proxy_set_header Upgrade $http_upgrade;\n            proxy_set_header Connection 'upgrade';\n            proxy_set_header Host $host;\n            proxy_set_header X-Real-IP $remote_addr;\n            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n            proxy_set_header X-Forwarded-Proto $scheme;\n            proxy_cache_bypass $http_upgrade;\n            proxy_buffering off;\n            proxy_read_timeout 300;\n            proxy_connect_timeout 300;\n\n            # WebSocket Support\n            proxy_set_header Connection \"Upgrade\";\n        }\n\n        # Static Files\n        location /_next/static/ {\n            proxy_pass http://app:3000;\n            proxy_cache_bypass $http_upgrade;\n            proxy_set_header Host $host;\n            expires 365d;\n            access_log off;\n        }\n\n        # Next.js Public Files\n        location /public/ {\n            proxy_pass http://app:3000;\n            proxy_cache_bypass $http_upgrade;\n            proxy_set_header Host $host;\n            expires 365d;\n            access_log off;\n        }\n\n        # Error Pages\n        error_page 404 /404.html;\n        location = /404.html {\n            internal;\n        }\n\n        error_page 500 502 503 504 /50x.html;\n        location = /50x.html {\n            internal;\n        }\n\n        # Additional Security Headers\n        location ~ /\\. {\n            deny all;\n        }\n\n        location = /favicon.ico {\n            access_log off;\n            log_not_found off;\n        }\n\n        location = /robots.txt {\n            access_log off;\n            log_not_found off;\n        }\n    }\n}\n\n\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003edockerfile\u003c/code\u003e\u003c/summary\u003e\n\n```dockerfile\n# STAGE 1: BUILD IMAGE\nFROM node:18-alpine AS builder\n\nWORKDIR /app\n\n# Copy Package Files First For Caching Dependencies\nCOPY package.json package-lock.json ./\n\n# Install Dependencies\nRUN npm cache clean --force \u0026\u0026 npm install --legacy-peer-deps\n\n# Copy The Rest Of The Application Files\nCOPY . .\n\n# Load Environment Variables From the .env File Created In The GitHub Actions Workflow\nRUN if [ -f .env ]; then export $(grep -v '^#' .env | xargs); fi\n\n# Build The Aplication\nRUN npm run build:prod\n\n# STAGE 2: RUN IMAGE\nFROM node:18-alpine AS runner\n\nWORKDIR /app\n\n# Copy Built Files \u0026 Dependencies From The Builder Stage\nCOPY --from=builder /app/package.json package.json\nCOPY --from=builder /app/package-lock.json package-lock.json\nCOPY --from=builder /app/.next ./.next\nCOPY --from=builder /app/public ./public\nCOPY --from=builder /app/node_modules ./node_modules\n\n# Expose Application Port\nEXPOSE 3000\n\n# Load Environment Variables From The .env File\nENV PORT=3000\nCOPY .env .env\n\n# Run The Application\nCMD [\"npm\", \"run\", \"start:prod\"]\n\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003ecompose.yaml\u003c/code\u003e\u003c/summary\u003e\n\n```yaml\nname: 'build-docker-image-container'\n\nservices:\n  app:\n    build:\n      context: .\n      dockerfile: Dockerfile\n      target: runner\n    ports:\n      - \"3000:3000\"  # EXPOSING PORT FOR INTERNAL COMMUNICATIONS\n    env_file:\n      - .env\n    depends_on:\n      mongodb:\n        condition: service_healthy\n    networks:\n      - rupantor_network\n    restart: unless-stopped\n    healthcheck:\n      test: [\"CMD\", \"wget\", \"--spider\", \"-q\", \"http://localhost:3000/api/health\"]\n      interval: 30s\n      timeout: 10s\n      retries: 3\n      start_period: 40s\n\n  mongodb:\n    image: mongo:6.0\n    container_name: mongodb\n    ports:\n      - \"27017:27017\"\n    volumes:\n      - mongodb_data:/data/db\n    networks:\n      - rupantor_network\n    restart: unless-stopped\n    healthcheck:\n      test: [\"CMD\", \"mongosh\", \"--eval\", \"db.adminCommand('ping')\"]\n      interval: 30s\n      timeout: 10s\n      retries: 3\n      start_period: 20s\n\n  nginx:\n    image: nginx:latest\n    volumes:\n      - ./nginx.conf:/etc/nginx/nginx.conf:ro\n      - ./public:/usr/share/nginx/html:ro\n    ports:\n      - \"80:80\"\n    depends_on:\n      app:\n        condition: service_healthy\n    networks:\n      - rupantor_network\n    restart: unless-stopped\n    healthcheck:\n      test: [\"CMD\", \"nginx\", \"-t\"]\n      interval: 30s\n      timeout: 10s\n      retries: 3\n      start_period: 10s\n\nnetworks:\n  rupantor_network:\n    driver: bridge\n\nvolumes:\n  mongodb_data:\n    driver: local\n```\n\u003c/details\u003e\n\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003ccode\u003edocker.img.build-ghcr.io.yaml\u003c/code\u003e\u003c/summary\u003e\n\n```yaml\nname: docker.img.build-ghcr.io\n\non:\n  push:\n    branches: [ \"main\" ]\n  workflow_dispatch: {}\n\nenv:\n  REGISTRY: ghcr.io\n  IMAGE_NAME: ${{ github.repository }}\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n\n    strategy:\n      matrix:\n        node-version: [18.12.0]\n\n    steps:\n    - uses: actions/checkout@v4  \n    - name: Use Node.js ${{ matrix.node-version }}\n      uses: actions/setup-node@v4\n      with:\n        node-version: ${{ matrix.node-version }}\n        cache: 'npm'\n\n    - name: Authenticate GHCR For Dockerization\n      run: |\n        docker login --username ${{ github.actor }} --password ${{ secrets.AUTH_SECRET }} ghcr.io\n\n    - name: Createing Environment Variables\n      run: |\n        echo \"NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}\" \u003e\u003e .env\n        echo \"CLERK_SECRET_KEY=${{ secrets.CLERK_SECRET_KEY }}\" \u003e\u003e .env\n        echo \"NEXT_PUBLIC_CLERK_SIGN_IN_URL=${{ secrets.NEXT_PUBLIC_CLERK_SIGN_IN_URL }}\" \u003e\u003e .env\n        echo \"NEXT_PUBLIC_CLERK_SIGN_UP_URL=${{ secrets.NEXT_PUBLIC_CLERK_SIGN_UP_URL }}\" \u003e\u003e .env\n        echo \"NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=${{ secrets.NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL }}\" \u003e\u003e .env\n        echo \"NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=${{ secrets.NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL }}\" \u003e\u003e .env\n        echo \"MONGODB_URL=${{ secrets.MONGODB_URL }}\" \u003e\u003e .env\n        echo \"WEBHOOK_SECRET=${{ secrets.WEBHOOK_SECRET }}\" \u003e\u003e .env\n        echo \"NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=${{ secrets.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME }}\" \u003e\u003e .env\n        echo \"NEXT_PUBLIC_CLOUDINARY_API_KEY=${{ secrets.NEXT_PUBLIC_CLOUDINARY_API_KEY }}\" \u003e\u003e .env\n        echo \"NEXT_PUBLIC_CLOUDINARY_API_SECRET=${{ secrets.NEXT_PUBLIC_CLOUDINARY_API_SECRET }}\" \u003e\u003e .env\n        echo \"NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET=${{ secrets.NEXT_PUBLIC_CLOUDINARY_UPLOAD_PRESET }}\" \u003e\u003e .env\n        echo \"NEXT_PUBLIC_CLOUDINARY_BUCKET_NAME=${{ secrets.NEXT_PUBLIC_CLOUDINARY_BUCKET_NAME }}\" \u003e\u003e .env\n        echo \"NEXT_PUBLIC_STRIPE_WEBHOOK_CHECKOUT_URL=${{ secrets.NEXT_PUBLIC_STRIPE_WEBHOOK_CHECKOUT_URL }}\" \u003e\u003e .env\n        echo \"NEXT_PUBLIC_STRIPE_SECRET_KEY=${{ secrets.NEXT_PUBLIC_STRIPE_SECRET_KEY }}\" \u003e\u003e .env\n        echo \"NEXT_PUBLIC_STRIPE_WEBHOOK_SECRET=${{ secrets.NEXT_PUBLIC_STRIPE_WEBHOOK_SECRET }}\" \u003e\u003e .env\n        echo \"NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${{ secrets.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY }}\" \u003e\u003e .env\n\n    - name: Adding Commit Hash To Docker Image\n      id: commit_hash\n      run: echo \"IMAGE_TAG=$(echo $GITHUB_SHA | cut -c1-7)\" \u003e\u003e $GITHUB_OUTPUT\n\n    - name: Building Docker Image\n      run: |\n        docker build --no-cache . --tag ghcr.io/aknibircse/rupantor-ai-saas:${{ steps.commit_hash.outputs.IMAGE_TAG }}\n\n    - name: Push Docker Image To GHCR\n      run: docker push ghcr.io/aknibircse/rupantor-ai-saas:${{ steps.commit_hash.outputs.IMAGE_TAG }}\n```\n\n\u003c/details\u003e\n\u003cbr/\u003e\n\n\n## 📃 \u003ca name=\"copyright\"\u003eCOPYRIGHTS\u003c/a\u003e\n*©* ALL RIGHTS OF THIS PROJECT IS RESERVED BY ***[AK NIBIR](https://aknibir.dev)*** -- 📃 UNDER ***[MIT](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/licensing-a-repository)*** LISENCE.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faknibircse%2Frupantor-ai-saas","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faknibircse%2Frupantor-ai-saas","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faknibircse%2Frupantor-ai-saas/lists"}