diff --git a/build_utf8.txt b/build_utf8.txt new file mode 100644 index 0000000..3b3644e --- /dev/null +++ b/build_utf8.txt @@ -0,0 +1,43 @@ + +> plumeia@0.1.0 build +> next build + +node.exe : ÔÜá Warning: Next.js inferred your workspace root, but it +may not be correct. +Au caractère Ligne:1 : 1 ++ & "C:\Program Files\nodejs/node.exe" "C:\Program +Files\nodejs/node_mo ... ++ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + CategoryInfo : NotSpecified: (ÔÜá Warning: Ne...not b + e correct.:String) [], RemoteException + + FullyQualifiedErrorId : NativeCommandError + + We detected multiple lockfiles and selected the directory of +C:\Users\streaper2\package-lock.json as the root directory. + To silence this warning, set `turbopack.root` in your Next.js +config, or consider removing one of the lockfiles if it's not needed. + See https://nextjs.org/docs/app/api-reference/config/next-config-j +s/turbopack#root-directory for more information. + Detected additional lockfiles: + * C:\Users\streaper2\Documents\00 - +projet\plumeia\package-lock.json + +Ôû▓ Next.js 16.1.6 (Turbopack) +- Environments: .env.local, .env + + Creating an optimized production build ... +Ô£ô Compiled successfully in 1253.2ms + Skipping validation of types + Collecting page data using 31 workers ... + Generating static pages using 31 workers (0/10) ... +Error occurred prerendering page "/pricing". Read more: +https://nextjs.org/docs/messages/prerender-error +ReferenceError: useState is not defined + at h (C:\Users\streaper2\Documents\00 - projet\plumeia\.next\serv +er\chunks\ssr\[root-of-the-server]__1b51e774._.js:1:4513) { + digest: '4248291053' +} +Export encountered an error on /pricing/page: /pricing, exiting the +build. +Ô¿» Next.js build worker exited with code: 1 and signal: null diff --git a/package-lock.json b/package-lock.json index b8a593e..45608da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "plumeia", "version": "0.1.0", + "hasInstallScript": true, "dependencies": { "@google/genai": "^1.38.0", "@prisma/adapter-pg": "^7.4.1", @@ -31,7 +32,11 @@ "eslint": "^9", "eslint-config-next": "16.1.6", "tailwindcss": "^4", + "tsx": "^4.21.0", "typescript": "^5" + }, + "engines": { + "node": ">=22.12.0" } }, "node_modules/@alloc/quick-lru": { @@ -409,6 +414,448 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -3719,6 +4166,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -4398,6 +4887,21 @@ "node": ">=12.20.0" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -8000,6 +8504,26 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 83c808f..1ba8512 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,10 @@ "eslint": "^9", "eslint-config-next": "16.1.6", "tailwindcss": "^4", + "tsx": "^4.21.0", "typescript": "^5" + }, + "prisma": { + "seed": "npx tsx prisma/seed.ts" } -} \ No newline at end of file +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 6d18d30..6216c1f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -7,16 +7,38 @@ generator client { } // ===================== -// AUTH +// AUTH & SUBSCRIPTIONS // ===================== +model Plan { + id String @id // e.g., 'free', 'pro', 'master' + name String @unique + displayName String + price Float + description String + maxProjects Int // -1 for unlimited + maxAiActions Int // -1 for unlimited + features String[] + isPopular Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + users User[] +} + model User { id String @id @default(cuid()) name String? email String @unique hashedPassword String - avatar String? bio String? - plan String @default("free") // free | pro | master + + // Legacy string plan (temporarily kept to avoid DB drop errors) + plan String @default("free") + + // New Subscription + planId String? @default("free") + subscriptionPlan Plan? @relation(fields: [planId], references: [id]) + aiActionsUsed Int @default(0) dailyWordGoal Int @default(500) writingStreak Int @default(0) diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..0e8bb27 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,64 @@ +import { config } from 'dotenv'; +config({ path: '.env.local' }); +import getDB from '../src/lib/prisma'; + +const prisma = getDB(); + +async function main() { + console.log('Seeding plans...'); + + const plans = [ + { + id: 'free', + name: 'free', + displayName: 'Gratuit', + price: 0, + description: 'Idéal pour découvrir PlumeIA.', + maxProjects: 1, + maxAiActions: 10, + features: ['10 actions IA / mois', '1 projet actif', 'Bible du monde simple'], + isPopular: false, + }, + { + id: 'pro', + name: 'pro', + displayName: 'Auteur Pro', + price: 12, + description: 'Pour les écrivains sérieux.', + maxProjects: -1, // -1 means unlimited + maxAiActions: 500, + features: ['500 actions IA / mois', 'Projets illimités', 'Export Word & EPUB', 'Support prioritaire'], + isPopular: true, + }, + { + id: 'master', + name: 'master', + displayName: 'Maître Plume', + price: 29, + description: 'Le summum de l\'écriture IA.', + maxProjects: -1, + maxAiActions: -1, // -1 means unlimited + features: ['Actions IA illimitées', 'Accès Gemini 3 Pro', 'Bible du monde avancée', 'Outils de révision avancés'], + isPopular: false, + }, + ]; + + for (const plan of plans) { + await prisma.plan.upsert({ + where: { id: plan.id }, + update: plan, + create: plan, + }); + } + + console.log('Plans seeded successfully.'); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/push_log.txt b/push_log.txt new file mode 100644 index 0000000..8e50231 Binary files /dev/null and b/push_log.txt differ diff --git a/src/app/api/ai/generate/route.ts b/src/app/api/ai/generate/route.ts index 4c2649e..f45780f 100644 --- a/src/app/api/ai/generate/route.ts +++ b/src/app/api/ai/generate/route.ts @@ -5,11 +5,7 @@ import { auth } from '@/lib/auth'; import getDB from '@/lib/prisma'; import { generateStoryContent } from '@/lib/gemini'; -const PLAN_AI_LIMITS: Record = { - free: 100, - pro: 5000, - master: 999999, -}; + export async function POST(request: NextRequest) { try { @@ -23,17 +19,19 @@ export async function POST(request: NextRequest) { // Check AI usage limit from DB const dbUser = await prisma.user.findUnique({ where: { id: session.user.id }, - select: { plan: true, aiActionsUsed: true }, - }); + include: { subscriptionPlan: true }, + }) as any; // Bypass Prisma client types for this relation if (!dbUser) { return NextResponse.json({ error: 'Utilisateur non trouvé' }, { status: 404 }); } - const limit = PLAN_AI_LIMITS[dbUser.plan] || PLAN_AI_LIMITS.free; - if (dbUser.aiActionsUsed >= limit) { + const limit = dbUser.subscriptionPlan?.maxAiActions ?? 100; + const planName = dbUser.subscriptionPlan?.displayName || 'Gratuit'; + + if (limit !== -1 && dbUser.aiActionsUsed >= limit) { return NextResponse.json( - { error: `Limite de ${limit} actions IA atteinte pour le plan ${dbUser.plan}. Passez au plan supérieur !` }, + { error: `Limite de ${limit} actions IA atteinte pour le plan ${planName}. Passez au plan supérieur !` }, { status: 403 } ); } diff --git a/src/app/api/ai/transform/route.ts b/src/app/api/ai/transform/route.ts index 7155644..f1805be 100644 --- a/src/app/api/ai/transform/route.ts +++ b/src/app/api/ai/transform/route.ts @@ -5,11 +5,7 @@ import { auth } from '@/lib/auth'; import getDB from '@/lib/prisma'; import { transformTextServer } from '@/lib/gemini'; -const PLAN_AI_LIMITS: Record = { - free: 100, - pro: 5000, - master: 999999, -}; + export async function POST(request: NextRequest) { try { @@ -23,17 +19,19 @@ export async function POST(request: NextRequest) { // Check AI usage limit from DB const dbUser = await prisma.user.findUnique({ where: { id: session.user.id }, - select: { plan: true, aiActionsUsed: true }, - }); + include: { subscriptionPlan: true }, + }) as any; // Bypass Prisma type cache if (!dbUser) { return NextResponse.json({ error: 'Utilisateur non trouvé' }, { status: 404 }); } - const limit = PLAN_AI_LIMITS[dbUser.plan] || PLAN_AI_LIMITS.free; - if (dbUser.aiActionsUsed >= limit) { + const limit = dbUser.subscriptionPlan?.maxAiActions ?? 100; + const planName = dbUser.subscriptionPlan?.displayName || 'Gratuit'; + + if (limit !== -1 && dbUser.aiActionsUsed >= limit) { return NextResponse.json( - { error: `Limite de ${limit} actions IA atteinte pour le plan ${dbUser.plan}. Passez au plan supérieur !` }, + { error: `Limite de ${limit} actions IA atteinte pour le plan ${planName}. Passez au plan supérieur !` }, { status: 403 } ); } diff --git a/src/app/api/plans/route.ts b/src/app/api/plans/route.ts new file mode 100644 index 0000000..ff1b7bb --- /dev/null +++ b/src/app/api/plans/route.ts @@ -0,0 +1,19 @@ +import { NextResponse } from 'next/server'; +import getDB from '@/lib/prisma'; + +export const dynamic = 'force-dynamic'; + +export async function GET() { + try { + const prisma = getDB(); + const plans = await prisma.plan.findMany({ + orderBy: { price: 'asc' } + }); + const response = NextResponse.json(plans); + response.headers.set('Cache-Control', 'no-store, max-age=0'); + return response; + } catch (error) { + console.error('Failed to fetch plans', error); + return NextResponse.json({ error: 'Failed to fetch plans' }, { status: 500 }); + } +} diff --git a/src/app/api/projects/route.ts b/src/app/api/projects/route.ts index 2078f98..34e5ec8 100644 --- a/src/app/api/projects/route.ts +++ b/src/app/api/projects/route.ts @@ -22,12 +22,7 @@ export async function GET() { return NextResponse.json(projects); } -// Plan limits for project creation -const PLAN_PROJECT_LIMITS: Record = { - free: 3, - pro: 20, - master: 999, -}; + // POST /api/projects — Create a new project export async function POST(request: NextRequest) { @@ -36,17 +31,20 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Non autorisé' }, { status: 401 }); } + // Check plan limits const prisma = getDB(); + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + include: { subscriptionPlan: true } + }) as any; // Cast to any to bypass Prisma type cache issues - // Check plan limit - const user = await prisma.user.findUnique({ where: { id: session.user.id }, select: { plan: true } }); - const plan = user?.plan || 'free'; - const limit = PLAN_PROJECT_LIMITS[plan] || PLAN_PROJECT_LIMITS.free; + const limit = user?.subscriptionPlan?.maxProjects ?? 3; + const planName = user?.subscriptionPlan?.displayName || 'Gratuit'; const currentCount = await prisma.project.count({ where: { userId: session.user.id } }); - if (currentCount >= limit) { + if (limit !== -1 && currentCount >= limit) { return NextResponse.json( - { error: `Limite de ${limit} projets atteinte pour le plan ${plan}. Passez au plan supérieur !` }, + { error: `Limite de ${limit} projets atteinte pour le plan ${planName}. Passez au plan supérieur !` }, { status: 403 } ); } diff --git a/src/app/api/user/profile/route.ts b/src/app/api/user/profile/route.ts index ed91262..e935e84 100644 --- a/src/app/api/user/profile/route.ts +++ b/src/app/api/user/profile/route.ts @@ -14,7 +14,8 @@ export async function GET() { const prisma = getDB(); const user = await prisma.user.findUnique({ where: { id: session.user.id }, - }); + include: { subscriptionPlan: true } + } as any) as any; // Bypass Prisma type cache if (!user) { return NextResponse.json({ error: 'Utilisateur non trouvé' }, { status: 404 }); @@ -31,13 +32,24 @@ export async function GET() { return total + (text ? text.split(/\s+/).length : 0); }, 0); - return NextResponse.json({ + const response = NextResponse.json({ id: user.id, email: user.email, name: user.name, avatar: user.avatar, bio: user.bio, - plan: user.plan, + plan: user.planId || user.plan || 'free', + planDetails: user.subscriptionPlan ? { + id: user.subscriptionPlan.id, + name: user.subscriptionPlan.name, + displayName: user.subscriptionPlan.displayName, + price: user.subscriptionPlan.price, + description: user.subscriptionPlan.description, + features: user.subscriptionPlan.features, + maxProjects: user.subscriptionPlan.maxProjects, + maxAiActions: user.subscriptionPlan.maxAiActions, + isPopular: user.subscriptionPlan.isPopular + } : undefined, aiActionsUsed: user.aiActionsUsed, dailyWordGoal: user.dailyWordGoal, writingStreak: user.writingStreak, @@ -45,6 +57,8 @@ export async function GET() { createdAt: user.createdAt, totalWords, }); + response.headers.set('Cache-Control', 'no-store, max-age=0'); + return response; } // PUT /api/user/profile — Update user profile diff --git a/src/app/pricing/page.tsx b/src/app/pricing/page.tsx index fbba736..c5e5227 100644 --- a/src/app/pricing/page.tsx +++ b/src/app/pricing/page.tsx @@ -1,5 +1,6 @@ 'use client'; +import React, { useState, useEffect } from 'react'; import Pricing from '@/components/Pricing'; import { useRouter } from 'next/navigation'; import { useAuthContext } from '@/providers/AuthProvider'; @@ -8,8 +9,26 @@ export default function PricingPage() { const router = useRouter(); const { user } = useAuthContext(); + const [plans, setPlans] = useState([]); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + fetch('/api/plans', { cache: 'no-store' }) + .then(res => res.json()) + .then(data => { + setPlans(data); + setIsLoading(false); + }) + .catch(err => { + console.error(err); + setIsLoading(false); + }); + }, []); + return ( router.push(user ? '/dashboard' : '/')} onSelectPlan={() => router.push(user ? '/checkout' : '/login')} diff --git a/src/components/AppRouter.tsx b/src/components/AppRouter.tsx index d8653a6..67c0f32 100644 --- a/src/components/AppRouter.tsx +++ b/src/components/AppRouter.tsx @@ -81,9 +81,28 @@ const AppRouter: React.FC = (props) => { // but AuthPage manages its own state. We can pass a prop if AuthPage supports it, or just let user toggle. // Since AuthPage has internal state for mode, we might just render it. // Ideally AuthPage should accept an initialMode prop. Let's check AuthPage again or just render it. + const [plans, setPlans] = useState([]); + const [isPricingLoading, setIsPricingLoading] = useState(true); + + React.useEffect(() => { + if (viewMode === 'pricing' && plans.length === 0) { + setIsPricingLoading(true); + fetch('/api/plans', { cache: 'no-store' }) + .then(res => res.json()) + .then(data => { + setPlans(data); + setIsPricingLoading(false); + }) + .catch(err => { + console.error(err); + setIsPricingLoading(false); + }); + } + }, [viewMode, plans.length]); + if (viewMode === 'signup') return props.onViewModeChange('landing')} onSuccess={() => props.onViewModeChange('dashboard')} initialMode='signup' />; if (viewMode === 'features') return props.onViewModeChange(user ? 'dashboard' : 'landing')} />; - if (viewMode === 'pricing') return props.onViewModeChange(user ? 'dashboard' : 'landing')} onSelectPlan={() => user ? props.onViewModeChange('checkout') : props.onViewModeChange('auth')} />; + if (viewMode === 'pricing') return props.onViewModeChange(user ? 'dashboard' : 'landing')} onSelectPlan={() => user ? props.onViewModeChange('checkout') : props.onViewModeChange('auth')} />; if (viewMode === 'checkout') return props.onUpgradePlan('pro')} onCancel={() => props.onViewModeChange('pricing')} />; if (viewMode === 'dashboard' && user) return props.onViewModeChange('pricing')} onProfile={() => props.onViewModeChange('profile')} />; if (viewMode === 'profile' && user) return props.onViewModeChange('dashboard')} />; diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index b504193..746bbe7 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -30,7 +30,7 @@ const Dashboard: React.FC = ({ user, projects, onSelect, onCreat

Bonjour, {user.name} 👋

- {user.subscription.plan} + {user.subscription.planDetails?.displayName || user.subscription.plan} Membre depuis le 24 janv.
diff --git a/src/components/Pricing.tsx b/src/components/Pricing.tsx index 36fb56e..9c58abd 100644 --- a/src/components/Pricing.tsx +++ b/src/components/Pricing.tsx @@ -3,55 +3,61 @@ import React from 'react'; import { Check, ArrowLeft } from 'lucide-react'; -import { PlanType } from '@/lib/types'; -interface PricingProps { - currentPlan: PlanType; - onBack: () => void; - onSelectPlan: (plan: PlanType) => void; +interface PlanData { + id: string; + name: string; + displayName: string; + price: number; + description: string; + features: string[]; + isPopular: boolean; } -const Pricing: React.FC = ({ currentPlan, onBack, onSelectPlan }) => { - const plans = [ - { id: 'free', name: 'Gratuit', price: '0€', desc: 'Idéal pour découvrir PlumeIA.', features: ['10 actions IA / mois', '1 projet actif', 'Bible du monde simple'] }, - { id: 'pro', name: 'Auteur Pro', price: '12€', desc: 'Pour les écrivains sérieux.', features: ['500 actions IA / mois', 'Projets illimités', 'Export Word & EPUB', 'Support prioritaire'], popular: true }, - { id: 'master', name: 'Maître Plume', price: '29€', desc: 'Le summum de l\'écriture IA.', features: ['Actions IA illimitées', 'Accès Gemini 3 Pro', 'Bible du monde avancée', 'Outils de révision avancés'] }, - ]; +interface PricingProps { + plans: PlanData[]; + currentPlan: string; + onBack: () => void; + onSelectPlan: (planId: string) => void; + isLoading?: boolean; +} +const Pricing: React.FC = ({ plans, currentPlan, onBack, onSelectPlan, isLoading }) => { return (
-

Choisissez votre destin d'écrivain.

-

Passez au plan supérieur pour libérer toute la puissance de l'IA.

+

Choisissez votre destin d'écrivain.

+

Passez au plan supérieur pour libérer toute la puissance de l'IA.

- {plans.map((p) => ( -
-
-

{p.name}

-
{p.price}/mois
-

{p.desc}

-
-
    - {p.features.map((f, i) => ( -
  • -
    - {f} -
  • - ))} -
- -
- ))} + {isLoading &&

Chargement des offres...

} + {!isLoading && plans.map((p) => ( +
+
+

{p.displayName}

+
{p.price}€/mois
+

{p.description}

+
+
    + {p.features.map((f, i) => ( +
  • +
    + {f} +
  • + ))} +
+ +
+ ))}
diff --git a/src/components/UserProfileSettings.tsx b/src/components/UserProfileSettings.tsx index 5deddd0..0a64e2c 100644 --- a/src/components/UserProfileSettings.tsx +++ b/src/components/UserProfileSettings.tsx @@ -165,7 +165,7 @@ const UserProfileSettings: React.FC = ({ user, onUpdat
-

Plan {user.subscription.plan.toUpperCase()}

+

Plan {(user.subscription.planDetails?.displayName || user.subscription.plan).toUpperCase()}

Prochaine facturation le 15 Mars 2024

diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index 36fe56f..e29b6f4 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -2,15 +2,9 @@ import { useState, useEffect, useCallback } from 'react'; import { signIn, signOut, useSession } from 'next-auth/react'; -import { UserProfile, PlanType } from '@/lib/types'; +import { UserProfile } from '@/lib/types'; import api from '@/lib/api'; -const PLAN_LIMITS: Record = { - free: { aiActions: 100, projects: 3 }, - pro: { aiActions: 5000, projects: 20 }, - master: { aiActions: 999999, projects: 999 }, -}; - export const useAuth = () => { const { data: session, status } = useSession(); const [user, setUser] = useState(null); @@ -19,11 +13,16 @@ export const useAuth = () => { // Fetch real profile from DB when session is available useEffect(() => { if (session?.user?.id) { - fetch('/api/user/profile') + fetch('/api/user/profile', { cache: 'no-store' }) .then(res => res.json()) .then(dbUser => { - const plan = (dbUser.plan || 'free') as PlanType; - const limits = PLAN_LIMITS[plan] || PLAN_LIMITS.free; + const planId = dbUser.plan || 'free'; + const planDetails = dbUser.planDetails || { + id: 'free', + displayName: 'Gratuit', + maxAiActions: 100, + maxProjects: 3 + }; setUser({ id: dbUser.id, @@ -31,11 +30,16 @@ export const useAuth = () => { name: dbUser.name || 'User', avatar: dbUser.avatar, bio: dbUser.bio, - subscription: { plan, startDate: new Date(dbUser.createdAt).getTime(), status: 'active' }, + subscription: { + plan: planId, + planDetails: planDetails, + startDate: new Date(dbUser.createdAt).getTime(), + status: 'active' + }, usage: { aiActionsCurrent: dbUser.aiActionsUsed || 0, - aiActionsLimit: limits.aiActions, - projectsLimit: limits.projects, + aiActionsLimit: planDetails.maxAiActions, + projectsLimit: planDetails.maxProjects, }, preferences: { theme: 'light', dailyWordGoal: dbUser.dailyWordGoal || 500, language: 'fr' }, stats: { diff --git a/src/lib/types.ts b/src/lib/types.ts index 2ac6a27..a72cc57 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -129,10 +129,21 @@ export interface ChatMessage { // --- SAAS TYPES --- -export type PlanType = 'free' | 'pro' | 'master'; +export interface PlanData { + id: string; + name: string; + displayName: string; + price: number; + description: string; + features: string[]; + isPopular: boolean; + maxProjects: number; + maxAiActions: number; +} export interface Subscription { - plan: PlanType; + plan: string; // The ID of the plan + planDetails?: PlanData; // The populated plan details from DB startDate: number; status: 'active' | 'canceled' | 'past_due'; }