From 51cb2d8c30ed4ac592451fcd9b37179dec06f131 Mon Sep 17 00:00:00 2001 From: Anil Chandra Naidu Matcha Date: Wed, 3 Jun 2026 14:18:31 +0530 Subject: [PATCH 1/5] Add cross-links to related projects --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c8ff8b9..cd5cde3 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ > **Open-source AI Design Agent** -> https://github.com/Anil-matcha/Open-AI-Design-Agent +> **Schedule your UGC content automatically with AI** -> https://github.com/Anil-matcha/Free-AI-Social-Media-Scheduler + ## 🌐 Live Demo & Deploy **Live Demo:** [open-ai-ugc.vercel.app](https://open-ai-ugc.vercel.app/) From 193a24522d3818037c66c6064fd75f00d1409fea Mon Sep 17 00:00:00 2001 From: Jaya Prasad Kavuru Date: Tue, 9 Jun 2026 17:15:00 +0530 Subject: [PATCH 2/5] upgrade: integrate standard SaaS components (NextAuth, Stripe billing, layouts, and theme support) --- package-lock.json | 182 +++++++++++++++++--- package.json | 6 +- src/app/api/checkout/route.js | 24 +++ src/app/api/webhook/stripe/route.js | 51 ++---- src/app/globals.css | 200 +++++++++++++--------- src/app/layout.js | 16 +- src/app/login/page.js | 64 +++++++ src/app/page.js | 1 - src/app/pricing/page.js | 256 +++++++++------------------- src/app/providers.js | 20 +++ src/components/Footer.js | 26 +++ src/components/Navbar.js | 226 ++++++++++++++++++++++++ src/components/NavbarWrapper.jsx | 8 - src/components/Providers.jsx | 7 - src/components/saas/AuthButtons.jsx | 28 --- src/components/saas/Navbar.jsx | 174 ------------------- src/lib/auth.js | 7 + src/lib/config.js | 30 ++++ src/lib/prisma.js | 6 +- src/lib/services/billing.js | 53 ++++++ src/lib/services/user.js | 48 ++++++ src/lib/stripe.js | 13 ++ 22 files changed, 906 insertions(+), 540 deletions(-) create mode 100644 src/app/api/checkout/route.js create mode 100644 src/app/login/page.js create mode 100644 src/app/providers.js create mode 100644 src/components/Footer.js create mode 100644 src/components/Navbar.js delete mode 100644 src/components/NavbarWrapper.jsx delete mode 100644 src/components/Providers.jsx delete mode 100644 src/components/saas/AuthButtons.jsx delete mode 100644 src/components/saas/Navbar.jsx create mode 100644 src/lib/config.js create mode 100644 src/lib/services/billing.js create mode 100644 src/lib/services/user.js create mode 100644 src/lib/stripe.js diff --git a/package-lock.json b/package-lock.json index 3666d6a..4830157 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "ugc-creator", + "name": "open-ai-ugc", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "ugc-creator", + "name": "open-ai-ugc", "version": "0.1.0", "hasInstallScript": true, "dependencies": { @@ -13,6 +13,7 @@ "@prisma/adapter-pg": "^7.7.0", "@prisma/client": "^7.7.0", "@stripe/stripe-js": "^9.2.0", + "axios": "^1.16.1", "clsx": "^2.1.1", "framer-motion": "^12.38.0", "next": "16.2.3", @@ -20,6 +21,7 @@ "pg": "^8.20.0", "react": "19.2.4", "react-dom": "19.2.4", + "react-hot-toast": "^2.5.2", "react-icons": "^5.6.0", "stripe": "^22.0.1", "tailwind-merge": "^3.5.0" @@ -2758,6 +2760,18 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -2985,6 +2999,12 @@ "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -3021,6 +3041,18 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz", + "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -3167,7 +3199,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3305,6 +3336,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3354,9 +3397,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/damerau-levenshtein": { "version": "1.0.8", @@ -3423,7 +3464,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3497,6 +3537,15 @@ "devOptional": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/denque": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", @@ -3554,7 +3603,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -3700,7 +3748,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3710,7 +3757,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3748,7 +3794,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -3761,7 +3806,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4405,6 +4449,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -4438,6 +4502,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/framer-motion": { "version": "12.38.0", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", @@ -4469,7 +4549,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4540,7 +4619,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -4572,7 +4650,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -4666,11 +4743,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/goober": { + "version": "2.1.19", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.19.tgz", + "integrity": "sha512-U7veizMqxyKlM58+Z5j2ngJBH/r9siDmxpvNxSw0PylF6WQvrASJEZrxh1hidRBJc2jqoBVSyOban5u8m+6Rxg==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4756,7 +4841,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4769,7 +4853,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -4785,7 +4868,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4828,6 +4910,19 @@ "devOptional": true, "license": "MIT" }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -5853,7 +5948,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5883,6 +5977,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -5925,7 +6040,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mysql2": { @@ -6775,6 +6889,15 @@ "devOptional": true, "license": "ISC" }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -6855,6 +6978,23 @@ "react": "^19.2.4" } }, + "node_modules/react-hot-toast": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", + "integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-icons": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz", diff --git a/package.json b/package.json index 8a8f8cf..6682f96 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,9 @@ "react-dom": "19.2.4", "react-icons": "^5.6.0", "stripe": "^22.0.1", - "tailwind-merge": "^3.5.0" + "tailwind-merge": "^3.5.0", + "axios": "^1.16.1", + "react-hot-toast": "^2.5.2" }, "devDependencies": { "@tailwindcss/postcss": "^4", @@ -53,4 +55,4 @@ "prisma": "^7.7.0", "tailwindcss": "^4" } -} +} \ No newline at end of file diff --git a/src/app/api/checkout/route.js b/src/app/api/checkout/route.js new file mode 100644 index 0000000..66aafb8 --- /dev/null +++ b/src/app/api/checkout/route.js @@ -0,0 +1,24 @@ +import { NextResponse } from "next/server"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/lib/auth"; +import { BillingService } from "@/lib/services/billing"; + +export async function POST(req) { + try { + const session = await getServerSession(authOptions); + if (!session || !session.user) { + return NextResponse.json({ error: "Unauthorized. Please sign in." }, { status: 401 }); + } + + const { planId } = await req.json(); + if (!planId) { + return NextResponse.json({ error: "Missing planId parameter" }, { status: 400 }); + } + + const checkoutUrl = await BillingService.createCheckoutSession(session.user.id, planId); + return NextResponse.json({ url: checkoutUrl }); + } catch (error) { + console.error("Checkout route error:", error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/src/app/api/webhook/stripe/route.js b/src/app/api/webhook/stripe/route.js index 55d1d38..3354845 100644 --- a/src/app/api/webhook/stripe/route.js +++ b/src/app/api/webhook/stripe/route.js @@ -1,48 +1,21 @@ import { NextResponse } from "next/server"; import { headers } from "next/headers"; -import Stripe from "stripe"; -import { prisma } from "@/lib/prisma"; - -const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); -const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET; +import { BillingService } from "@/lib/services/billing"; export async function POST(req) { - const body = await req.text(); - const sig = headers().get("stripe-signature"); - - let event; - try { - event = stripe.webhooks.constructEvent(body, sig, endpointSecret); - } catch (err) { - console.error(`Webhook Error: ${err.message}`); - return new NextResponse(`Webhook Error: ${err.message}`, { status: 400 }); - } + const body = await req.text(); + const headersList = await headers(); + const signature = headersList.get("stripe-signature"); - // Handle the event - if (event.type === "checkout.session.completed") { - const session = event.data.object; - - const userId = session.metadata.userId; - const creditsToAdd = parseInt(session.metadata.credits); - - if (userId && creditsToAdd) { - try { - await prisma.user.update({ - where: { id: userId }, - data: { - credits: { - increment: creditsToAdd - } - } - }); - console.log(`Successfully added ${creditsToAdd} credits to user ${userId}`); - } catch (error) { - console.error(`Error updating user credits: ${error.message}`); - return new NextResponse("Error updating user", { status: 500 }); - } + if (!signature) { + return NextResponse.json({ error: "Missing stripe-signature header" }, { status: 400 }); } - } - return new NextResponse(null, { status: 200 }); + const result = await BillingService.handleWebhook(body, signature); + return NextResponse.json(result); + } catch (error) { + console.error("Stripe webhook processing error:", error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } } diff --git a/src/app/globals.css b/src/app/globals.css index 30a1525..1162605 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,106 +1,148 @@ @import "tailwindcss"; @theme { - --color-primary-50: var(--primary-50); - --color-primary-100: var(--primary-100); - --color-primary-200: var(--primary-200); - --color-primary-300: var(--primary-300); - --color-primary-400: var(--primary-400); - --color-primary-500: var(--primary-500); - --color-primary-600: var(--primary-600); - --color-secondary-500: var(--secondary-500); - --color-secondary-600: var(--secondary-600); + --font-sans: var(--font-inter), sans-serif; + --font-outfit: var(--font-inter), sans-serif; - --color-glass-bg: var(--glass-bg); - --color-glass-border: var(--glass-border); - --color-glass-hover: var(--glass-hover); - --color-muted: var(--muted); -} - -:root { - --background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); - --foreground: #0f172a; - --muted: #475569; - --solid-bg: #ffffff; - - /* Default Theme (Indigo) */ - --primary-50: #f5f3ff; - --primary-100: #ede9fe; - --primary-200: #ddd6fe; - --primary-300: #c4b5fd; - --primary-400: #a78bfa; - --primary-500: #6366f1; - --primary-600: #4f46e5; - --secondary-500: #ec4899; - - --glass-bg: rgba(255, 255, 255, 0.7); - --glass-border: rgba(0, 0, 0, 0.05); - --glass-hover: rgba(255, 255, 255, 0.9); - --bg-gradient: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); + --color-bg-page: var(--bg-page, #09090b); + --color-bg-card: var(--bg-card, #18181b); + --color-bg-card-hover: var(--bg-card-hover, #27272a); + --color-bg-elevated: var(--bg-elevated, #202024); + --color-divider: var(--divider, #3f3f46); + --color-primary: var(--primary, #8b5cf6); + --color-primary-hover: var(--primary-hover, #7c3aed); + --color-primary-text: var(--primary-text, #f4f4f5); + --color-secondary-text: var(--secondary-text, #a1a1aa); + --color-primary-btn-text: var(--primary-btn-text, #ffffff); +} + +/* Premium Visual Themes Override Definitions */ +[data-theme="cyberpunk"] { + --bg-page: #05010a; + --bg-card: #0e051c; + --bg-card-hover: #1b0c32; + --bg-elevated: #150827; + --divider: #ec4899; + --primary: #f43f5e; + --primary-hover: #e11d48; + --primary-text: #fff1f2; + --secondary-text: #06b6d4; + --primary-btn-text: #ffffff; } [data-theme="emerald"] { - --primary-50: #ecfdf5; - --primary-500: #10b981; - --primary-600: #059669; - --bg-gradient: linear-gradient(135deg, #a7f3d0 0%, #34d399 100%); + --bg-page: #022c22; + --bg-card: #064e3b; + --bg-card-hover: #0f766e; + --bg-elevated: #042f2e; + --divider: #115e59; + --primary: #10b981; + --primary-hover: #059669; + --primary-text: #ecfdf5; + --secondary-text: #34d399; + --primary-btn-text: #ffffff; } -[data-theme="rose"] { - --primary-50: #fff1f2; - --primary-500: #f43f5e; - --primary-600: #e11d48; - --bg-gradient: linear-gradient(135deg, #fecdd3 0%, #fb7185 100%); +[data-theme="sunset"] { + --bg-page: #1c0a00; + --bg-card: #2d1500; + --bg-card-hover: #452400; + --bg-elevated: #371b00; + --divider: #ea580c; + --primary: #f97316; + --primary-hover: #ea580c; + --primary-text: #fff7ed; + --secondary-text: #fb923c; + --primary-btn-text: #ffffff; } -[data-theme="amber"] { - --primary-50: #fffbeb; - --primary-500: #f59e0b; - --primary-600: #d97706; - --bg-gradient: linear-gradient(135deg, #fde68a 0%, #f59e0b 100%); +[data-theme="midnight"] { + --bg-page: #000000; + --bg-card: #0c0c0e; + --bg-card-hover: #1e1e24; + --bg-elevated: #151518; + --divider: #2d2d34; + --primary: #ffffff; + --primary-hover: #e4e4e7; + --primary-text: #f4f4f5; + --secondary-text: #a1a1aa; + --primary-btn-text: #000000; } -[data-theme="violet"] { - --primary-50: #f5f3ff; - --primary-500: #8b5cf6; - --primary-600: #7c3aed; - --bg-gradient: linear-gradient(135deg, #ddd6fe 0%, #8b5cf6 100%); +.bg-primary { + color: var(--primary-btn-text, #ffffff) !important; } -[data-theme="dark"] { - --foreground: #f8fafc; - --muted: #94a3b8; - --solid-bg: #0f172a; - --glass-bg: rgba(15, 23, 42, 0.8); - --glass-border: rgba(255, 255, 255, 0.1); - --glass-hover: rgba(30, 41, 59, 0.9); - --bg-gradient: linear-gradient(135deg, #020617 0%, #0f172a 100%); +@layer base { + html, body { + background-color: var(--color-bg-page); + color: var(--color-primary-text); + font-family: var(--font-sans); + height: 100%; + margin: 0; + padding: 0; + } } -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-muted: var(--muted); +/* Glassmorphism Utilities */ +.glass-panel { + background: rgba(24, 24, 27, 0.7); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(63, 63, 70, 0.5); } -body { - background: var(--bg-gradient); - background-attachment: fixed; - color: var(--foreground); - min-height: 100vh; +.glass-dropdown { + background: rgba(15, 15, 18, 0.95); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid rgba(63, 63, 70, 0.6); } -.custom-scrollbar::-webkit-scrollbar { - width: 2px; +/* Premium Scrollbars */ +.scrollbar-subtle::-webkit-scrollbar { + width: 6px; + height: 6px; } -.custom-scrollbar::-webkit-scrollbar-track { + +.scrollbar-subtle::-webkit-scrollbar-track { background: transparent; } -.custom-scrollbar::-webkit-scrollbar-thumb { - background: #cbd5e1; + +.scrollbar-subtle::-webkit-scrollbar-thumb { + background: var(--color-divider); border-radius: 10px; } -.custom-scrollbar { - scrollbar-width: thin; - scrollbar-color: #cbd5e1 transparent; + +.scrollbar-subtle::-webkit-scrollbar-thumb:hover { + background: var(--color-primary); +} + +/* Animations */ +@keyframes pulseGlow { + 0%, 100% { + opacity: 0.6; + transform: scale(1); + } + 50% { + opacity: 1; + transform: scale(1.02); + } +} + +.animate-pulse-glow { + animation: pulseGlow 2s infinite ease-in-out; +} + +@keyframes gradientX { + 0%, 100% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } +} + +.animate-gradient-x { + animation: gradientX 6s ease infinite; } diff --git a/src/app/layout.js b/src/app/layout.js index 6e5d94a..2db89a7 100644 --- a/src/app/layout.js +++ b/src/app/layout.js @@ -1,7 +1,8 @@ import "./globals.css"; -import { Providers } from "@/components/Providers"; -import { NavbarWrapper } from "@/components/NavbarWrapper"; +import { Providers } from "./providers"; +import Navbar from "../components/Navbar"; import { Inter } from "next/font/google"; +import config from "@/lib/config"; const inter = Inter({ subsets: ["latin"], display: "swap" }); @@ -23,14 +24,14 @@ export const metadata = { }; export default function RootLayout({ children }) { - const theme = 'light'; + const theme = config?.theme || "slate-indigo"; return ( - - + + - -
+ +
{children}
@@ -38,3 +39,4 @@ export default function RootLayout({ children }) { ); } + diff --git a/src/app/login/page.js b/src/app/login/page.js new file mode 100644 index 0000000..fc8732e --- /dev/null +++ b/src/app/login/page.js @@ -0,0 +1,64 @@ +"use client"; + +import { signIn, useSession } from "next-auth/react"; +import { useEffect, Suspense } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { FaGoogle, FaInfoCircle } from "react-icons/fa"; + +function LoginContent() { + const { data: session, status } = useSession(); + const router = useRouter(); + const searchParams = useSearchParams(); + const next = searchParams.get("callbackUrl") || searchParams.get("next") || "/"; + + useEffect(() => { + if (status === "authenticated") { + router.push(next); + } + }, [status, router, next]); + + return ( +
+
+
+
+ A +
+

Sign In to Studio

+

+ Sign in with Google to enable predictions, save generation history, and top up credits packages. +

+
+ +
+ +
+ +
+ + + By signing in, you agree to our Terms of Service. Purchases are stripe-secured and credit balance addition is automated. + +
+
+
+ ); +} + +export default function Login() { + return ( + +
+
+ }> + +
+ ); +} diff --git a/src/app/page.js b/src/app/page.js index eac607d..5f2a918 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -18,7 +18,6 @@ import { } from "react-icons/fi"; import { useEffect, useState, useRef } from "react"; import { motion, AnimatePresence } from "framer-motion"; -import { LoginButton } from "@/components/saas/AuthButtons"; import { FaCoins } from "react-icons/fa"; import Link from "next/link"; import { useRouter } from "next/navigation"; diff --git a/src/app/pricing/page.js b/src/app/pricing/page.js index eecd8f3..5847e99 100644 --- a/src/app/pricing/page.js +++ b/src/app/pricing/page.js @@ -1,209 +1,121 @@ "use client"; -import { motion } from "framer-motion"; -import { FiCheck, FiZap, FiStar, FiShield, FiArrowRight } from "react-icons/fi"; -import { Button } from "@/components/ui/Button"; +import { useSession } from "next-auth/react"; +import { useState } from "react"; +import Navbar from "@/components/Navbar"; +import Footer from "@/components/Footer"; +import { FaCheck, FaInfoCircle } from "react-icons/fa"; +import axios from "axios"; +import toast, { Toaster } from "react-hot-toast"; const PLANS = [ - { - name: "Starter", - price: "Free", - description: "Begin your manifestation journey", - features: [ - "10 Credits included", - "Standard Rendering speed", - "Basic Video Models", - "Standard Resolution", - "Public Gallery Storage" - ], - cta: "Start Free", - popular: false, - color: "slate", - credits: 10 - }, - { - name: "Pro Studio", - price: "$19.99", - period: "/mo", - description: "For active professional creators", - features: [ - "500 Credits monthly", - "Seedance 2.0 Priority Access", - "HD Rendering (1080p)", - "Priority Manifestation Queue", - "Private Studio Workspace", - "Commercial Usage License" - ], - cta: "Go Pro", - popular: true, - color: "primary", - credits: 500 - }, - { - name: "Elite Creator", - price: "$49.99", - period: "/mo", - description: "Scale your creative operations", - features: [ - "1500 Credits monthly", - "Native Synchronized Audio", - "4K Ultra-HD Output", - "Multi-shot Generation", - "API Access for Workflows", - "24/7 Priority Support" - ], - cta: "Go Elite", - popular: false, - color: "slate", - credits: 1500 - } + { id: "basic", name: "Basic Pack", price: "$5", credits: 100, description: "Perfect for testing custom prompts and exploring styles." }, + { id: "standard", name: "Standard Pack", price: "$10", credits: 250, description: "Ideal for regular creators wanting high resolution outputs." }, + { id: "pro", name: "Professional Pack", price: "$20", credits: 600, description: "Designed for power users demanding batch exports and high speed.", popular: true }, + { id: "business", name: "Business Pack", price: "$50", credits: 2000, description: "Maximum value pack for agency workflows and large volume generations." } ]; -export default function PricingPage() { - const handlePurchase = async (plan) => { - if (plan.price === "Free") { - alert("You already have access to the Starter plan!"); +export default function Pricing() { + const { data: session, status } = useSession(); + const [loadingPlan, setLoadingPlan] = useState(null); + + const handleCheckout = async (planId) => { + if (status !== "authenticated") { + toast.error("You must sign in with Google to purchase credit packages."); return; } + setLoadingPlan(planId); try { - const response = await fetch("/api/checkout/stripe", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ plan }), - }); - - const data = await response.json(); - + const { data } = await axios.post("/api/checkout", { planId }); if (data.url) { window.location.href = data.url; } else { - throw new Error("No checkout URL received"); + throw new Error("No redirection URL returned"); } - } catch (error) { - console.error("[PURCHASE_ERROR]", error); - alert("Something went wrong with the checkout process. Please try again."); + } catch (err) { + console.error(err); + toast.error(err.response?.data?.error || "Failed to trigger Stripe checkout session."); + } finally { + setLoadingPlan(null); } }; + return ( -
-
- {/* Header */} -
- - - Pricing Plans - - - Fuel your Creativity - - - Choose the manifestation plan that fits your studio workflow. - From solo creators to global creative teams. - +
+ + + +
+
+
+ + Pricing Plans +
+

Buy Credits Packs

+

+ Purchase flexible credit packages to perform high-resolution predictions. Keep all profits — we handle AI infrastructure. +

- {/* Plans Grid */} -
- {PLANS.map((plan, i) => ( - + {PLANS.map((plan) => ( +
{plan.popular && ( -
+ Most Popular -
+ )} -
-
-

{plan.name}

-

{plan.description}

+
+
+

{plan.name}

+

{plan.price}

-
- {plan.price} - {plan.period && {plan.period}} + +
+ {plan.credits} Art Credits
-
-
- {plan.features.map((feature) => ( -
-
- -
- {feature} -
- ))} +

{plan.description}

+ +
    +
  • + + Dynamic aspect ratios +
  • +
  • + + HD image downloads +
  • +
  • + + No subscription required +
  • +
- - - ))} -
- - {/* Bottom Section */} -
-
-
-

Need a custom credit package?

-

- If your creative volume exceeds our pro plans, we offer dynamic bulk credit packages - tailored to your production cycle. Connect with our studio engineers today. -

-
-
- - Secure Payments -
-
- - Cancel Anytime -
+ {loadingPlan === plan.id ? "Loading checkout..." : "Purchase Credits"} +
-
+ ))}
-
+
- +
); } diff --git a/src/app/providers.js b/src/app/providers.js new file mode 100644 index 0000000..659111e --- /dev/null +++ b/src/app/providers.js @@ -0,0 +1,20 @@ +"use client"; + +import { SessionProvider } from "next-auth/react"; +import { useEffect } from "react"; +import config from "@/lib/config"; + +export function Providers({ children }) { + useEffect(() => { + if (typeof window !== "undefined") { + const theme = config?.theme || "slate-indigo"; + document.documentElement.setAttribute("data-theme", theme); + } + }, []); + + return ( + + {children} + + ); +} diff --git a/src/components/Footer.js b/src/components/Footer.js new file mode 100644 index 0000000..42fa648 --- /dev/null +++ b/src/components/Footer.js @@ -0,0 +1,26 @@ +"use client"; + +import Link from "next/link"; + +export default function Footer() { + const currentYear = new Date().getFullYear(); + + return ( +
+
+
+ © {currentYear} AI SaaS Studio. All rights reserved. +
+
+ + Terms of Service + + + + Privacy Policy + +
+
+
+ ); +} diff --git a/src/components/Navbar.js b/src/components/Navbar.js new file mode 100644 index 0000000..e5aa792 --- /dev/null +++ b/src/components/Navbar.js @@ -0,0 +1,226 @@ +"use client"; + +import Link from "next/link"; +import { useSession, signOut } from "next-auth/react"; +import { useState, useEffect } from "react"; +import { usePathname } from "next/navigation"; +import { IoClose, IoMenu } from "react-icons/io5"; +import { FiMoon, FiSun, FiLogOut, FiDollarSign, FiPlus, FiUser } from "react-icons/fi"; +import { SiVercel } from "react-icons/si"; +import config from "@/lib/config"; + +export default function Navbar() { + const { data: session, status } = useSession(); + const pathname = usePathname(); + const [isOpen, setIsOpen] = useState(false); + const [isProfileOpen, setIsProfileOpen] = useState(false); + + const appName = config?.appName || "AI SaaS"; + const logoLetter = appName.trim().charAt(0).toUpperCase(); + + // Eagerly prefetch workspace/gallery routes for fast tabs + useEffect(() => { + // Prefetch common routes + }, []); + + const appMatch = pathname ? pathname.match(/^\/app\/([^\/]+)/) : null; + const currentAppId = appMatch ? appMatch[1] : null; + + const navLinks = currentAppId + ? [ + { name: "Workspace", path: `/app/${currentAppId}` }, + { name: "Gallery", path: `/app/${currentAppId}/gallery` }, + { name: "Pricing", path: `/app/${currentAppId}/pricing` }, + ] + : [ + { name: "Workspace", path: "/" }, + { name: "Gallery", path: "/gallery" }, + { name: "Pricing", path: "/pricing" }, + ]; + + return ( +
+
+ + {/* Logo and Brand Title (Visible at all times) */} + +
+ {logoLetter} +
+ + {appName} + + + + {/* Desktop Navigation Links */} + + + {/* Desktop Actions Section */} +
+ + {/* Vercel Deploy Button */} + + + Deploy + + + {status === "authenticated" ? ( +
+ {/* Credit Balance indicator */} +
+ + + {session.user.credits !== undefined ? session.user.credits : 0} + + + + +
+ + {/* Profile Menu Toggle */} +
+ + + {/* Profile Dropdown */} + {isProfileOpen && ( +
+
+ {session.user.email} +
+ +
+ )} +
+
+ ) : ( + + Sign In + + )} +
+ + {/* Mobile Navbar Hamburger Menu Controls */} +
+ {status === "authenticated" && ( +
+ + {session.user.credits !== undefined ? session.user.credits : 0} +
+ )} + + +
+
+ + {/* Absolutely Positioned Mobile Menu Dropdown (Backdrop blur + matches layout) */} + {isOpen && ( +
+ +
+ )} +
+ ); +} diff --git a/src/components/NavbarWrapper.jsx b/src/components/NavbarWrapper.jsx deleted file mode 100644 index c10151b..0000000 --- a/src/components/NavbarWrapper.jsx +++ /dev/null @@ -1,8 +0,0 @@ -"use client"; - -import { usePathname } from "next/navigation"; -import { Navbar } from "./saas/Navbar"; - -export function NavbarWrapper() { - return ; -} diff --git a/src/components/Providers.jsx b/src/components/Providers.jsx deleted file mode 100644 index 05e1c78..0000000 --- a/src/components/Providers.jsx +++ /dev/null @@ -1,7 +0,0 @@ -"use client"; - -import { SessionProvider } from "next-auth/react"; - -export function Providers({ children }) { - return {children}; -} diff --git a/src/components/saas/AuthButtons.jsx b/src/components/saas/AuthButtons.jsx deleted file mode 100644 index 0c5a02b..0000000 --- a/src/components/saas/AuthButtons.jsx +++ /dev/null @@ -1,28 +0,0 @@ -"use client"; - -import { signIn, signOut } from "next-auth/react"; -import { FaGoogle, FaSignOutAlt } from "react-icons/fa"; - -export function LoginButton({ className }) { - return ( - - ); -} - -export function SignOutButton({ className }) { - return ( - - ); -} diff --git a/src/components/saas/Navbar.jsx b/src/components/saas/Navbar.jsx deleted file mode 100644 index 6b41dcb..0000000 --- a/src/components/saas/Navbar.jsx +++ /dev/null @@ -1,174 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { usePathname } from "next/navigation"; -import { signOut, useSession } from "next-auth/react"; -import { FaCoins, FaUser, FaSignOutAlt, FaChevronDown, FaRocket, FaBars, FaTimes } from "react-icons/fa"; -import { SiVercel } from "react-icons/si"; -import { useState } from "react"; -import { motion, AnimatePresence } from "framer-motion"; -import { LoginButton } from "./AuthButtons"; - -export function Navbar() { - const { data: session } = useSession(); - const pathname = usePathname(); - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); - - const navLinks = [ - { name: "UGC Generator", href: "/" }, - { name: "My Creations", href: "/dashboard" }, - { name: "Pricing", href: "/pricing" }, - ]; - - return ( - - ); -} diff --git a/src/lib/auth.js b/src/lib/auth.js index de232e0..f4b2c3a 100644 --- a/src/lib/auth.js +++ b/src/lib/auth.js @@ -19,4 +19,11 @@ export const authOptions = { return session; }, }, + secret: process.env.NEXTAUTH_SECRET, + pages: { + signIn: "/login", + }, }; + +export default authOptions; + diff --git a/src/lib/config.js b/src/lib/config.js new file mode 100644 index 0000000..e16846e --- /dev/null +++ b/src/lib/config.js @@ -0,0 +1,30 @@ +const config = { + appName: "Open AI UGC", + auth: { + google: { + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + }, + secret: process.env.NEXTAUTH_SECRET, + url: process.env.NEXTAUTH_URL || "http://localhost:3000", + webhook_url: process.env.WEBHOOK_URL || process.env.NEXTAUTH_URL || "http://localhost:3000", + }, + stripe: { + publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, + secretKey: process.env.STRIPE_SECRET_KEY, + webhookSecret: process.env.STRIPE_WEBHOOK_SECRET, + plans: { + basic: { id: "basic", name: "Basic Pack", credits: 1000, price: 500 }, + standard: { id: "standard", name: "Standard Pack", credits: 2000, price: 1000 }, + pro: { id: "pro", name: "Professional Pack", credits: 4000, price: 2000 }, + business: { id: "business", name: "Business Pack", credits: 10000, price: 5000 }, + }, + }, + ai: { + apiKey: process.env.MUAPIAPP_API_KEY, + generationCost: 10, + model: "gpt-4o", + }, +}; + +export default config; diff --git a/src/lib/prisma.js b/src/lib/prisma.js index b8335ea..6f4ef8e 100644 --- a/src/lib/prisma.js +++ b/src/lib/prisma.js @@ -11,8 +11,10 @@ export const prisma = globalForPrisma.prisma || new PrismaClient({ adapter, - log: - process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], + log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], }); if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; + +export default prisma; + diff --git a/src/lib/services/billing.js b/src/lib/services/billing.js new file mode 100644 index 0000000..7b66189 --- /dev/null +++ b/src/lib/services/billing.js @@ -0,0 +1,53 @@ +import { stripe } from "../stripe"; +import config from "../config"; +import { UserService } from "./user"; + +export const BillingService = { + async createCheckoutSession(userId, planId) { + const plan = config.stripe.plans[planId]; + if (!plan) throw new Error("Invalid plan selected"); + + const session = await stripe.checkout.sessions.create({ + payment_method_types: ["card"], + line_items: [ + { + price_data: { + currency: "usd", + product_data: { + name: `${config.stripe.plans[planId].name}`, + description: `Purchase ${plan.credits} credits to perform AI generations.`, + }, + unit_amount: plan.price, + }, + quantity: 1, + }, + ], + mode: "payment", + success_url: `${config.auth.url}/pricing?success=true`, + cancel_url: `${config.auth.url}/pricing?canceled=true`, + metadata: { userId, credits: plan.credits.toString() }, + }); + + return session.url; + }, + + async handleWebhook(body, signature) { + const event = stripe.webhooks.constructEvent(body, signature, config.stripe.webhookSecret); + if (event.type === "checkout.session.completed") { + const session = event.data.object; + const userId = session.metadata.userId; + const credits = parseInt(session.metadata.credits || "0", 10); + + if (userId && credits > 0) { + await UserService.addCredits(userId, credits); + return { success: true, userId, credits }; + } + } + return { success: false }; + } +}; + +export const createCheckoutSession = BillingService.createCheckoutSession.bind(BillingService); +export const handleWebhook = BillingService.handleWebhook.bind(BillingService); +export default BillingService; + diff --git a/src/lib/services/user.js b/src/lib/services/user.js new file mode 100644 index 0000000..e0b43ad --- /dev/null +++ b/src/lib/services/user.js @@ -0,0 +1,48 @@ +import { prisma } from "../prisma"; + +export const UserService = { + async getCredits(userId) { + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { credits: true }, + }); + return user ? user.credits : 0; + }, + + async addCredits(userId, amount) { + if (amount <= 0) return; + return await prisma.user.update({ + where: { id: userId }, + data: { + credits: { + increment: amount, + }, + }, + }); + }, + + async deductCredits(userId, amount) { + if (amount <= 0) return; + + // Check if the user has enough credits + const currentCredits = await this.getCredits(userId); + if (currentCredits < amount) { + throw new Error("Insufficient credits available"); + } + + return await prisma.user.update({ + where: { id: userId }, + data: { + credits: { + decrement: amount, + }, + }, + }); + }, +}; + +export const getCredits = UserService.getCredits.bind(UserService); +export const addCredits = UserService.addCredits.bind(UserService); +export const deductCredits = UserService.deductCredits.bind(UserService); +export default UserService; + diff --git a/src/lib/stripe.js b/src/lib/stripe.js new file mode 100644 index 0000000..c40dd63 --- /dev/null +++ b/src/lib/stripe.js @@ -0,0 +1,13 @@ +import Stripe from "stripe"; +import config from "./config"; + +const apiKey = config.stripe.secretKey && config.stripe.secretKey.trim() !== "" + ? config.stripe.secretKey + : "sk_test_placeholder_key_for_build_purposes"; + +export const stripe = new Stripe(apiKey, { + apiVersion: "2023-10-16", +}); + +export default stripe; + From 2fec30d2112f278590cb03c6b5a3859ac18f563b Mon Sep 17 00:00:00 2001 From: Jaya Prasad Kavuru Date: Sun, 28 Jun 2026 12:00:01 +0530 Subject: [PATCH 3/5] Rename dashboard route to gallery, update Stripe checkout success redirect URL, configure default root light mode variables in globals.css, remove duplicate Navbar from pricing page, and clean up hardcoded primary-500 classes --- src/app/api/checkout/stripe/route.js | 2 +- src/app/{dashboard => gallery}/page.js | 10 +++++----- src/app/globals.css | 13 +++++++++++++ src/app/page.js | 8 ++++---- src/app/pricing/page.js | 2 -- src/components/Navbar.js | 4 ++-- src/components/ui/Button.jsx | 8 ++++---- src/components/ui/Card.jsx | 2 +- 8 files changed, 30 insertions(+), 19 deletions(-) rename src/app/{dashboard => gallery}/page.js (95%) diff --git a/src/app/api/checkout/stripe/route.js b/src/app/api/checkout/stripe/route.js index 34ad356..823d3ea 100644 --- a/src/app/api/checkout/stripe/route.js +++ b/src/app/api/checkout/stripe/route.js @@ -36,7 +36,7 @@ export async function POST(req) { }, ], mode: "payment", - success_url: `${process.env.NEXTAUTH_URL}/dashboard?status=success`, + success_url: `${process.env.NEXTAUTH_URL}/gallery?status=success`, cancel_url: `${process.env.NEXTAUTH_URL}/pricing?status=cancelled`, metadata: { userId: session.user.id, diff --git a/src/app/dashboard/page.js b/src/app/gallery/page.js similarity index 95% rename from src/app/dashboard/page.js rename to src/app/gallery/page.js index 99a6c2f..03e249a 100644 --- a/src/app/dashboard/page.js +++ b/src/app/gallery/page.js @@ -66,7 +66,7 @@ export default function Dashboard() { @@ -76,7 +76,7 @@ export default function Dashboard() {
{isLoading ? (
- + Syncing Archive...
) : creations.length === 0 ? ( @@ -119,7 +119,7 @@ export default function Dashboard() {
) : (
- + Processing...
)} @@ -183,7 +183,7 @@ export default function Dashboard() {
) : (
- + Syncing manifesting...
)} @@ -242,7 +242,7 @@ export default function Dashboard() { download target="_blank" rel="noopener noreferrer" - className="w-full py-4 bg-primary-500 text-white rounded-full font-bold text-xs flex items-center justify-center gap-3 transition-all hover:bg-primary-600 active:scale-[0.98] shadow-xl shadow-primary-500/20" + className="w-full py-4 bg-primary text-white rounded-full font-bold text-xs flex items-center justify-center gap-3 transition-all hover:bg-primary-hover active:scale-[0.98] shadow-xl shadow-primary/20" > Download Asset diff --git a/src/app/globals.css b/src/app/globals.css index 1162605..e652cd4 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -16,6 +16,19 @@ --color-primary-btn-text: var(--primary-btn-text, #ffffff); } +:root { + --bg-page: #f9fafb; + --bg-card: #ffffff; + --bg-card-hover: #f3f4f6; + --bg-elevated: #ffffff; + --divider: #e5e7eb; + --primary: #8b5cf6; + --primary-hover: #7c3aed; + --primary-text: #111827; + --secondary-text: #4b5563; + --primary-btn-text: #ffffff; +} + /* Premium Visual Themes Override Definitions */ [data-theme="cyberpunk"] { --bg-page: #05010a; diff --git a/src/app/page.js b/src/app/page.js index 5f2a918..0df1c12 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -397,7 +397,7 @@ export default function Home() { > {['processing', 'pending', 'starting', 'queued'].includes(lastGeneration.status) ? (
-
+
Manifesting ({lastGeneration.status})... @@ -467,7 +467,7 @@ export default function Home() { /> {img.status === 'uploading' && (
- +
)} @@ -596,7 +596,7 @@ export default function Home() {
@@ -648,11 +648,11 @@ export default function Home() {
handleModelSelect(model)} - className={`p-5 rounded border transition-all cursor-pointer space-y-4 group ${selectedModel.id === model.id ? "border-slate-900 ring-1 ring-slate-900 bg-slate-50" : "border-[#ececec] bg-white hover:border-slate-300 hover:shadow-md"}`} + className={`p-5 rounded border transition-all cursor-pointer space-y-4 group ${selectedModel.id === model.id ? "border-primary ring-1 ring-primary bg-bg-card-hover" : "border-divider/50 bg-bg-card hover:border-divider hover:shadow-md"}`} >
diff --git a/src/components/FloatingToolbar.jsx b/src/components/FloatingToolbar.jsx index cceafb3..9ca26fa 100644 --- a/src/components/FloatingToolbar.jsx +++ b/src/components/FloatingToolbar.jsx @@ -4,22 +4,22 @@ import { FiVideo, FiImage, FiSearch, FiUser } from "react-icons/fi"; export function FloatingToolbar({ onSeeMore }) { return ( -
- - -