From 024e5ca6560565f7d729f9accd5f0e7437863bd9 Mon Sep 17 00:00:00 2001 From: Alexandru Eduard Farcas Date: Fri, 4 Jul 2025 12:24:00 +0300 Subject: [PATCH] First protorype --- package-lock.json | 363 ++++++++++++++++++ package.json | 4 + .../migration.sql | 59 +++ .../migration.sql | 12 + prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 9 +- src/app/api/auth/[...nextauth]/route.ts | 44 +++ src/app/api/auth/register/route.ts | 27 ++ src/app/api/cars/route.ts | 36 ++ src/app/api/fillups/route.ts | 54 +++ src/app/api/mileage/route.ts | 55 +++ src/app/auth/login/page.tsx | 56 +++ src/app/auth/register/page.tsx | 56 +++ .../dashboard/cars/[carId]/fillups/page.tsx | 116 ++++++ .../dashboard/cars/[carId]/mileage/page.tsx | 76 ++++ src/app/dashboard/cars/[carId]/page.tsx | 58 +++ src/app/dashboard/cars/[carId]/stats/page.tsx | 64 +++ src/app/dashboard/cars/new/page.tsx | 94 +++++ src/app/dashboard/cars/page.tsx | 0 src/app/dashboard/page.tsx | 37 ++ src/components/CarCard.tsx | 14 + src/components/FillUpCard.tsx | 12 + src/components/Layout.tsx | 7 + src/components/StatCard.tsx | 8 + src/lib/auth.ts | 6 + src/lib/prisma.ts | 9 + 26 files changed, 1278 insertions(+), 1 deletion(-) create mode 100644 prisma/migrations/20250703084306_your_migration_name/migration.sql create mode 100644 prisma/migrations/20250704075156_add_make_model_year/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 src/app/api/auth/[...nextauth]/route.ts create mode 100644 src/app/api/auth/register/route.ts create mode 100644 src/app/api/cars/route.ts create mode 100644 src/app/api/fillups/route.ts create mode 100644 src/app/api/mileage/route.ts create mode 100644 src/app/auth/login/page.tsx create mode 100644 src/app/auth/register/page.tsx create mode 100644 src/app/dashboard/cars/[carId]/fillups/page.tsx create mode 100644 src/app/dashboard/cars/[carId]/mileage/page.tsx create mode 100644 src/app/dashboard/cars/[carId]/page.tsx create mode 100644 src/app/dashboard/cars/[carId]/stats/page.tsx create mode 100644 src/app/dashboard/cars/new/page.tsx create mode 100644 src/app/dashboard/cars/page.tsx create mode 100644 src/app/dashboard/page.tsx create mode 100644 src/components/CarCard.tsx create mode 100644 src/components/FillUpCard.tsx create mode 100644 src/components/Layout.tsx create mode 100644 src/components/StatCard.tsx create mode 100644 src/lib/auth.ts create mode 100644 src/lib/prisma.ts diff --git a/package-lock.json b/package-lock.json index 2e354cf..aaac36e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,12 @@ "name": "car_app", "version": "0.1.0", "dependencies": { + "@auth/prisma-adapter": "^2.10.0", + "@next-auth/prisma-adapter": "^1.0.7", "@prisma/client": "^6.11.0", + "bcryptjs": "^3.0.2", "next": "15.3.4", + "next-auth": "^4.24.11", "react": "^19.0.0", "react-dom": "^19.0.0" }, @@ -53,6 +57,174 @@ "node": ">=6.0.0" } }, + "node_modules/@auth/core": { + "version": "0.34.2", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.34.2.tgz", + "integrity": "sha512-KywHKRgLiF3l7PLyL73fjLSIBe1YNcA6sMeew4yMP6cfCWGXZrkkXd32AjRi1hlJ9nvovUBGZHvbn+LijO6ZeQ==", + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "@panva/hkdf": "^1.1.1", + "@types/cookie": "0.6.0", + "cookie": "0.6.0", + "jose": "^5.1.3", + "oauth4webapi": "^2.10.4", + "preact": "10.11.3", + "preact-render-to-string": "5.2.3" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/@auth/core/node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/@auth/core/node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "license": "MIT", + "optional": true, + "peer": true, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@auth/core/node_modules/preact": { + "version": "10.11.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", + "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", + "license": "MIT", + "optional": true, + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/@auth/core/node_modules/preact-render-to-string": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz", + "integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, + "node_modules/@auth/prisma-adapter": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@auth/prisma-adapter/-/prisma-adapter-2.10.0.tgz", + "integrity": "sha512-EliOQoTjGK87jWWqnJvlQjbR4PjQZQqtwRwPAe108WwT9ubuuJJIrL68aNnQr4hFESz6P7SEX2bZy+y2yL37Gw==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.40.0" + }, + "peerDependencies": { + "@prisma/client": ">=2.26.0 || >=3 || >=4 || >=5 || >=6" + } + }, + "node_modules/@auth/prisma-adapter/node_modules/@auth/core": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.40.0.tgz", + "integrity": "sha512-n53uJE0RH5SqZ7N1xZoMKekbHfQgjd0sAEyUbE+IYJnmuQkbvuZnXItCU7d+i7Fj8VGOgqvNO7Mw4YfBTlZeQw==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/@auth/prisma-adapter/node_modules/jose": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.11.tgz", + "integrity": "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@auth/prisma-adapter/node_modules/oauth4webapi": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.5.5.tgz", + "integrity": "sha512-1K88D2GiAydGblHo39NBro5TebGXa+7tYoyIbxvqv3+haDDry7CBE1eSYuNbOSsYCCU6y0gdynVZAkm4YPw4hg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@auth/prisma-adapter/node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/@auth/prisma-adapter/node_modules/preact-render-to-string": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "license": "MIT", + "peerDependencies": { + "preact": ">=10" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emnapi/core": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", @@ -767,6 +939,16 @@ "@tybys/wasm-util": "^0.9.0" } }, + "node_modules/@next-auth/prisma-adapter": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@next-auth/prisma-adapter/-/prisma-adapter-1.0.7.tgz", + "integrity": "sha512-Cdko4KfcmKjsyHFrWwZ//lfLUbcLqlyFqjd/nYE2m3aZ7tjMNUjpks47iw7NTCnXf+5UWz5Ypyt1dSs1EP5QJw==", + "license": "ISC", + "peerDependencies": { + "@prisma/client": ">=2.26.0 || >=3", + "next-auth": "^4" + } + }, "node_modules/@next/env": { "version": "15.3.4", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.4.tgz", @@ -959,6 +1141,15 @@ "node": ">=12.4.0" } }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/@prisma/client": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.11.0.tgz", @@ -1357,6 +1548,14 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2257,6 +2456,15 @@ "dev": true, "license": "MIT" }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2457,6 +2665,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4194,6 +4411,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4583,6 +4809,24 @@ "loose-envify": "cli.js" } }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-cache/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -4791,6 +5035,38 @@ } } }, + "node_modules/next-auth": { + "version": "4.24.11", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.11.tgz", + "integrity": "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==", + "license": "ISC", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@panva/hkdf": "^1.0.2", + "cookie": "^0.7.0", + "jose": "^4.15.5", + "oauth": "^0.9.15", + "openid-client": "^5.4.0", + "preact": "^10.6.3", + "preact-render-to-string": "^5.1.19", + "uuid": "^8.3.2" + }, + "peerDependencies": { + "@auth/core": "0.34.2", + "next": "^12.2.5 || ^13 || ^14 || ^15", + "nodemailer": "^6.6.5", + "react": "^17.0.2 || ^18 || ^19", + "react-dom": "^17.0.2 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@auth/core": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -4819,6 +5095,23 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==", + "license": "MIT" + }, + "node_modules/oauth4webapi": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.17.0.tgz", + "integrity": "sha512-lbC0Z7uzAFNFyzEYRIC+pkSVvDHJTbEW+dYlSBAlCYDe6RxUkJ26bClhk8ocBZip1wfI9uKTe0fm4Ib4RHn6uQ==", + "license": "MIT", + "optional": true, + "peer": true, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4829,6 +5122,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -4942,6 +5244,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oidc-token-hash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz", + "integrity": "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==", + "license": "MIT", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, + "node_modules/openid-client": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", + "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", + "license": "MIT", + "dependencies": { + "jose": "^4.15.9", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5108,6 +5434,28 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/preact": { + "version": "10.26.9", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.9.tgz", + "integrity": "sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz", + "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==", + "license": "MIT", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5118,6 +5466,12 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", + "license": "MIT" + }, "node_modules/prisma": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.11.0.tgz", @@ -6133,6 +6487,15 @@ "punycode": "^2.1.0" } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 9ef9797..08428ee 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,12 @@ "lint": "next lint" }, "dependencies": { + "@auth/prisma-adapter": "^2.10.0", + "@next-auth/prisma-adapter": "^1.0.7", "@prisma/client": "^6.11.0", + "bcryptjs": "^3.0.2", "next": "15.3.4", + "next-auth": "^4.24.11", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/prisma/migrations/20250703084306_your_migration_name/migration.sql b/prisma/migrations/20250703084306_your_migration_name/migration.sql new file mode 100644 index 0000000..2bd8fa7 --- /dev/null +++ b/prisma/migrations/20250703084306_your_migration_name/migration.sql @@ -0,0 +1,59 @@ +-- CreateEnum +CREATE TYPE "FuelType" AS ENUM ('GASOLINE', 'DIESEL', 'LPG'); + +-- CreateEnum +CREATE TYPE "Currency" AS ENUM ('EUR', 'USD', 'RON', 'GBP'); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "hashedPassword" TEXT NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Car" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "fuelType" "FuelType" NOT NULL, + + CONSTRAINT "Car_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "FillUp" ( + "id" TEXT NOT NULL, + "carId" TEXT NOT NULL, + "mileage" INTEGER NOT NULL, + "liters" DOUBLE PRECISION NOT NULL, + "cost" DOUBLE PRECISION NOT NULL, + "currency" "Currency" NOT NULL, + "date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "FillUp_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MileageEntry" ( + "id" TEXT NOT NULL, + "carId" TEXT NOT NULL, + "mileage" INTEGER NOT NULL, + "date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "MileageEntry_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- AddForeignKey +ALTER TABLE "Car" ADD CONSTRAINT "Car_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "FillUp" ADD CONSTRAINT "FillUp_carId_fkey" FOREIGN KEY ("carId") REFERENCES "Car"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MileageEntry" ADD CONSTRAINT "MileageEntry_carId_fkey" FOREIGN KEY ("carId") REFERENCES "Car"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20250704075156_add_make_model_year/migration.sql b/prisma/migrations/20250704075156_add_make_model_year/migration.sql new file mode 100644 index 0000000..8e7f144 --- /dev/null +++ b/prisma/migrations/20250704075156_add_make_model_year/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - Added the required column `make` to the `Car` table without a default value. This is not possible if the table is not empty. + - Added the required column `model` to the `Car` table without a default value. This is not possible if the table is not empty. + - Added the required column `year` to the `Car` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Car" ADD COLUMN "make" TEXT NOT NULL, +ADD COLUMN "model" TEXT NOT NULL, +ADD COLUMN "year" INTEGER NOT NULL; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 17c4b85..c7a70d3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,6 +10,7 @@ datasource db { model User { id String @id @default(cuid()) email String @unique + hashedPassword String cars Car[] } @@ -17,12 +18,18 @@ model Car { id String @id @default(cuid()) user User @relation(fields: [userId], references: [id]) userId String - name String + + name String // User-defined name (e.g. "Red BMW") + make String // Manufacturer (e.g. "BMW") + model String // Model (e.g. "320i") + year Int // Year (e.g. 2019) fuelType FuelType + fillUps FillUp[] mileage MileageEntry[] } + model FillUp { id String @id @default(cuid()) car Car @relation(fields: [carId], references: [id]) diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..0f7912f --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,44 @@ +import NextAuth, { AuthOptions } from "next-auth"; +import CredentialsProvider from "next-auth/providers/credentials"; +import { PrismaAdapter } from "@auth/prisma-adapter"; +import { prisma } from "@/lib/prisma"; +import bcrypt from "bcryptjs"; + +export const authOptions: AuthOptions = { + adapter: PrismaAdapter(prisma), + providers: [ + CredentialsProvider({ + name: "Credentials", + credentials: { + email: { label: "Email", type: "text" }, + password: { label: "Password", type: "password" }, + }, + async authorize(credentials) { + // Check for missing credentials + if (!credentials?.email || !credentials?.password) { + return null; + } + + // Find user by email + const user = await prisma.user.findUnique({ + where: { email: credentials.email }, + }); + + // Validate password + if (user && await bcrypt.compare(credentials.password, user.hashedPassword)) { + return user; + } + + // Return null if authentication fails + return null; + }, + }), + ], + session: { strategy: "jwt" }, + pages: { + signIn: "/auth/login", + }, +}; + +const handler = NextAuth(authOptions); +export { handler as GET, handler as POST }; diff --git a/src/app/api/auth/register/route.ts b/src/app/api/auth/register/route.ts new file mode 100644 index 0000000..8842f02 --- /dev/null +++ b/src/app/api/auth/register/route.ts @@ -0,0 +1,27 @@ +import { NextResponse } from 'next/server'; +import { prisma } from '@/lib/prisma'; +import bcrypt from 'bcryptjs'; + +export async function POST(req: Request) { + const { email, password } = await req.json(); + + if (!email || !password) { + return NextResponse.json({ message: 'Email and password are required' }, { status: 400 }); + } + + const existingUser = await prisma.user.findUnique({ where: { email } }); + if (existingUser) { + return NextResponse.json({ message: 'Email already exists' }, { status: 400 }); + } + + const hashedPassword = await bcrypt.hash(password, 10); + + await prisma.user.create({ + data: { + email, + hashedPassword, + }, + }); + + return NextResponse.json({ message: 'User registered successfully' }, { status: 200 }); +} diff --git a/src/app/api/cars/route.ts b/src/app/api/cars/route.ts new file mode 100644 index 0000000..55c922a --- /dev/null +++ b/src/app/api/cars/route.ts @@ -0,0 +1,36 @@ +import { NextResponse } from 'next/server'; +import { getSession } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +export async function POST(req: Request) { + const session = await getSession(); + if (!session) return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); + + const body = await req.json(); + const { name, make, model, year, fuelType } = body; + + if (!name || !make || !model || !year || !fuelType) { + return NextResponse.json({ message: 'Missing fields' }, { status: 400 }); + } + + const user = await prisma.user.findUnique({ + where: { email: session.user?.email! }, + }); + + if (!user) { + return NextResponse.json({ message: 'User not found' }, { status: 404 }); + } + + await prisma.car.create({ + data: { + name, + make, + model, + year: parseInt(year), + fuelType, + userId: user.id, + }, + }); + + return NextResponse.json({ message: 'Car added successfully' }, { status: 200 }); +} diff --git a/src/app/api/fillups/route.ts b/src/app/api/fillups/route.ts new file mode 100644 index 0000000..7780697 --- /dev/null +++ b/src/app/api/fillups/route.ts @@ -0,0 +1,54 @@ +import { NextResponse } from 'next/server'; +import { getSession } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +export async function GET(req: Request) { + const session = await getSession(); + if (!session) return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); + + const { searchParams } = new URL(req.url); + const carId = searchParams.get('carId'); + + const fillUps = await prisma.fillUp.findMany({ + where: { + car: { + id: carId || '', + user: { email: session.user?.email! }, + }, + }, + orderBy: { date: 'desc' }, + }); + + return NextResponse.json(fillUps); +} + +export async function POST(req: Request) { + const session = await getSession(); + if (!session) return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); + + const { carId, mileage, liters, cost, currency } = await req.json(); + if (!carId || !mileage || !liters || !cost || !currency) { + return NextResponse.json({ message: 'Missing fields' }, { status: 400 }); + } + + const car = await prisma.car.findFirst({ + where: { + id: carId, + user: { email: session.user?.email! }, + }, + }); + + if (!car) return NextResponse.json({ message: 'Car not found' }, { status: 404 }); + + const fillUp = await prisma.fillUp.create({ + data: { + mileage: parseInt(mileage), + liters: parseFloat(liters), + cost: parseFloat(cost), + currency, + carId, + }, + }); + + return NextResponse.json(fillUp); +} diff --git a/src/app/api/mileage/route.ts b/src/app/api/mileage/route.ts new file mode 100644 index 0000000..32b94cf --- /dev/null +++ b/src/app/api/mileage/route.ts @@ -0,0 +1,55 @@ +import { NextResponse } from 'next/server'; +import { getSession } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; + +// GET: Return mileage entries for a car +export async function GET(req: Request) { + const session = await getSession(); + if (!session) return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); + + const { searchParams } = new URL(req.url); + const carId = searchParams.get('carId'); + + const entries = await prisma.mileageEntry.findMany({ + where: { + car: { + id: carId || '', + user: { + email: session.user?.email!, + }, + }, + }, + orderBy: { date: 'desc' }, + }); + + return NextResponse.json(entries); +} + +// POST: Add mileage entry +export async function POST(req: Request) { + const session = await getSession(); + if (!session) return NextResponse.json({ message: 'Unauthorized' }, { status: 401 }); + + const { carId, mileage } = await req.json(); + if (!carId || !mileage) { + return NextResponse.json({ message: 'Missing fields' }, { status: 400 }); + } + + const car = await prisma.car.findFirst({ + where: { + id: carId, + user: { email: session.user?.email! }, + }, + }); + + if (!car) return NextResponse.json({ message: 'Car not found' }, { status: 404 }); + + const entry = await prisma.mileageEntry.create({ + data: { + mileage: parseInt(mileage), + carId, + }, + }); + + return NextResponse.json(entry); +} diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx new file mode 100644 index 0000000..0ed84d8 --- /dev/null +++ b/src/app/auth/login/page.tsx @@ -0,0 +1,56 @@ +'use client'; + +import { useState } from 'react'; +import { signIn } from 'next-auth/react'; +import { useRouter } from 'next/navigation'; + +export default function LoginPage() { + const router = useRouter(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + const res = await signIn('credentials', { + redirect: false, + email, + password, + }); + + if (res?.ok) { + router.push('/dashboard'); + } else { + setError('Invalid credentials'); + } + }; + + return ( +
+

Login

+
+ setEmail(e.target.value)} + className="w-full border px-3 py-2 rounded" + /> + setPassword(e.target.value)} + className="w-full border px-3 py-2 rounded" + /> + + {error &&

{error}

} +
+
+ ); +} diff --git a/src/app/auth/register/page.tsx b/src/app/auth/register/page.tsx new file mode 100644 index 0000000..ac43d92 --- /dev/null +++ b/src/app/auth/register/page.tsx @@ -0,0 +1,56 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; + +export default function RegisterPage() { + const router = useRouter(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + + const handleRegister = async (e: React.FormEvent) => { + e.preventDefault(); + + const res = await fetch('/api/auth/register', { + method: 'POST', + body: JSON.stringify({ email, password }), + headers: { 'Content-Type': 'application/json' }, + }); + + if (res.ok) { + router.push('/auth/login'); + } else { + const data = await res.json(); + setError(data.message || 'Registration failed'); + } + }; + + return ( +
+

Register

+
+ setEmail(e.target.value)} + className="w-full border px-3 py-2 rounded" + /> + setPassword(e.target.value)} + className="w-full border px-3 py-2 rounded" + /> + + {error &&

{error}

} +
+
+ ); +} diff --git a/src/app/dashboard/cars/[carId]/fillups/page.tsx b/src/app/dashboard/cars/[carId]/fillups/page.tsx new file mode 100644 index 0000000..e5c8690 --- /dev/null +++ b/src/app/dashboard/cars/[carId]/fillups/page.tsx @@ -0,0 +1,116 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useParams } from 'next/navigation'; +import FillUpCard from '@/components/FillUpCard'; + +interface FillUp { + id: string; + mileage: number; + liters: number; + cost: number; + currency: string; + date: string; +} + +const currencies = ['EUR', 'USD', 'RON', 'GBP']; + +export default function FillUpsPage() { + const { carId } = useParams(); + const [fillups, setFillups] = useState([]); + const [form, setForm] = useState({ + mileage: '', + liters: '', + cost: '', + currency: 'EUR', + }); + const [error, setError] = useState(''); + + // Fetch fill-up entries + useEffect(() => { + fetch(`/api/fillups?carId=${carId}`) + .then((res) => res.json()) + .then(setFillups); + }, [carId]); + + const handleChange = (e: React.ChangeEvent) => { + setForm({ ...form, [e.target.name]: e.target.value }); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const res = await fetch('/api/fillups', { + method: 'POST', + body: JSON.stringify({ ...form, carId }), + headers: { 'Content-Type': 'application/json' }, + }); + + if (res.ok) { + const newFill = await res.json(); + setFillups((prev) => [newFill, ...prev]); + setForm({ mileage: '', liters: '', cost: '', currency: 'EUR' }); + setError(''); + } else { + const data = await res.json(); + setError(data.message || 'Error adding fill-up'); + } + }; + + return ( +
+

Fuel Fill-Ups

+ +
+ + + + + + {error &&

{error}

} +
+ +
    + {fillups.map((fill) => ( + // ✅ + ))} +
+
+ ); +} diff --git a/src/app/dashboard/cars/[carId]/mileage/page.tsx b/src/app/dashboard/cars/[carId]/mileage/page.tsx new file mode 100644 index 0000000..a937c58 --- /dev/null +++ b/src/app/dashboard/cars/[carId]/mileage/page.tsx @@ -0,0 +1,76 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useParams, useRouter } from 'next/navigation'; + +interface MileageEntry { + id: string; + mileage: number; + date: string; +} + +export default function MileagePage() { + const { carId } = useParams(); + const router = useRouter(); + + const [entries, setEntries] = useState([]); + const [mileage, setMileage] = useState(''); + const [error, setError] = useState(''); + + // Fetch existing entries + useEffect(() => { + fetch(`/api/mileage?carId=${carId}`) + .then((res) => res.json()) + .then((data) => setEntries(data)); + }, [carId]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + const res = await fetch('/api/mileage', { + method: 'POST', + body: JSON.stringify({ carId, mileage }), + headers: { 'Content-Type': 'application/json' }, + }); + + if (res.ok) { + const newEntry = await res.json(); + setEntries((prev) => [newEntry, ...prev]); + setMileage(''); + } else { + const data = await res.json(); + setError(data.message || 'Error adding mileage'); + } + }; + + return ( +
+

Mileage Log

+ +
+ setMileage(e.target.value)} + placeholder="Odometer (e.g. 102300)" + className="flex-1 border px-3 py-2 rounded" + required + /> + +
+ + {error &&

{error}

} + +
    + {entries.map((entry) => ( +
  • +

    {entry.mileage} km

    +

    {new Date(entry.date).toLocaleString()}

    +
  • + ))} +
+
+ ); +} diff --git a/src/app/dashboard/cars/[carId]/page.tsx b/src/app/dashboard/cars/[carId]/page.tsx new file mode 100644 index 0000000..ecf4978 --- /dev/null +++ b/src/app/dashboard/cars/[carId]/page.tsx @@ -0,0 +1,58 @@ +import { getSession } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; +import { redirect } from 'next/navigation'; +import Link from 'next/link'; + +interface CarDetailPageProps { + params: { + carId: string; + }; +} + +export default async function CarDetailPage({ params }: CarDetailPageProps) { + const session = await getSession(); + if (!session) redirect('/auth/login'); + + const userEmail = session.user?.email!; + const car = await prisma.car.findFirst({ + where: { + id: params.carId, + user: { email: userEmail }, + }, + }); + + if (!car) { + return
Car not found or unauthorized access.
; + } + + return ( +
+

{car.name}

+

{car.make} {car.model} ({car.year})

+

Fuel Type: {car.fuelType}

+ +
+ + ➕ Add Mileage + + + + ➕ Add Fill-Up + + + + 📊 View Stats + +
+
+ ); +} diff --git a/src/app/dashboard/cars/[carId]/stats/page.tsx b/src/app/dashboard/cars/[carId]/stats/page.tsx new file mode 100644 index 0000000..6df3f63 --- /dev/null +++ b/src/app/dashboard/cars/[carId]/stats/page.tsx @@ -0,0 +1,64 @@ +import { getSession } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; +import { redirect } from 'next/navigation'; +import StatCard from '@/components/StatCard'; + +interface StatsProps { + params: { + carId: string; + }; +} + +export default async function StatsPage({ params }: StatsProps) { + const session = await getSession(); + if (!session) redirect('/auth/login'); + + const car = await prisma.car.findFirst({ + where: { + id: params.carId, + user: { email: session.user?.email! }, + }, + include: { + fillUps: { + orderBy: { mileage: 'asc' }, + }, + }, + }); + + if (!car) return
Car not found or unauthorized.
; + const fillUps = car.fillUps; + + if (fillUps.length < 2) { + return ( +
+

Not Enough Data

+

Add at least 2 fill-ups to see statistics.

+
+ ); + } + + // Compute stats + const first = fillUps[0]; + const last = fillUps[fillUps.length - 1]; + + const totalDistance = last.mileage - first.mileage; + const totalLiters = fillUps.slice(1).reduce((sum, f) => sum + f.liters, 0); // ignore first fill + const totalCost = fillUps.slice(1).reduce((sum, f) => sum + f.cost, 0); + + const avgConsumption = (totalLiters / totalDistance) * 100; // L/100km + const costPerKm = totalCost / totalDistance; + + return ( +
+

Fuel Stats

+ +
+ + + + + +
+
+ ); +} diff --git a/src/app/dashboard/cars/new/page.tsx b/src/app/dashboard/cars/new/page.tsx new file mode 100644 index 0000000..b89db08 --- /dev/null +++ b/src/app/dashboard/cars/new/page.tsx @@ -0,0 +1,94 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; + +const fuelTypes = ['GASOLINE', 'DIESEL', 'LPG']; + +export default function AddCarPage() { + const router = useRouter(); + const [form, setForm] = useState({ + name: '', + make: '', + model: '', + year: '', + fuelType: 'GASOLINE', + }); + const [error, setError] = useState(''); + + const handleChange = (e: React.ChangeEvent) => { + setForm({ ...form, [e.target.name]: e.target.value }); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + const res = await fetch('/api/cars', { + method: 'POST', + body: JSON.stringify(form), + headers: { 'Content-Type': 'application/json' }, + }); + + if (res.ok) { + router.push('/dashboard'); + } else { + const data = await res.json(); + setError(data.message || 'Failed to add car'); + } + }; + + return ( +
+

Add New Car

+
+ + + + + + + {error &&

{error}

} +
+
+ ); +} diff --git a/src/app/dashboard/cars/page.tsx b/src/app/dashboard/cars/page.tsx new file mode 100644 index 0000000..e69de29 diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx new file mode 100644 index 0000000..7824ddc --- /dev/null +++ b/src/app/dashboard/page.tsx @@ -0,0 +1,37 @@ +import { getSession } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; +import Link from 'next/link'; +import { redirect } from 'next/navigation'; +import CarCard from '@/components/CarCard'; + +export default async function DashboardPage() { + const session = await getSession(); + if (!session) redirect('/auth/login'); + + const userEmail = session.user.email!; + const user = await prisma.user.findUnique({ + where: { email: userEmail }, + include: { cars: true }, + }); + + return ( +
+
+

Your Cars

+ + + Add Car + +
+ + {user?.cars.length ? ( +
    + {user.cars.map((car) => ( + + ))} +
+ ) : ( +

No cars added yet.

+ )} +
+ ); +} diff --git a/src/components/CarCard.tsx b/src/components/CarCard.tsx new file mode 100644 index 0000000..c87421f --- /dev/null +++ b/src/components/CarCard.tsx @@ -0,0 +1,14 @@ +import Link from 'next/link'; +import { Car } from '@prisma/client'; + +export default function CarCard({ car }: { car: Car }) { + return ( + +
+

{car.name}

+

{car.make} {car.model} ({car.year})

+

Fuel Type: {car.fuelType}

+
+ + ); +} diff --git a/src/components/FillUpCard.tsx b/src/components/FillUpCard.tsx new file mode 100644 index 0000000..0bb3ba0 --- /dev/null +++ b/src/components/FillUpCard.tsx @@ -0,0 +1,12 @@ +import { FillUp } from '@prisma/client'; + +export default function FillUpCard({ fill }: { fill: FillUp }) { + return ( +
+
{fill.mileage} km - {fill.liters} L
+
+ {fill.cost} {fill.currency} — {new Date(fill.date).toLocaleString()} +
+
+ ); +} diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx new file mode 100644 index 0000000..16133f7 --- /dev/null +++ b/src/components/Layout.tsx @@ -0,0 +1,7 @@ +export default function Layout({ children }: { children: React.ReactNode }) { + return ( +
+
{children}
+
+ ); +} diff --git a/src/components/StatCard.tsx b/src/components/StatCard.tsx new file mode 100644 index 0000000..2d36197 --- /dev/null +++ b/src/components/StatCard.tsx @@ -0,0 +1,8 @@ +export default function StatCard({ label, value }: { label: string; value: string }) { + return ( +
+

{label}

+

{value}

+
+ ); +} diff --git a/src/lib/auth.ts b/src/lib/auth.ts new file mode 100644 index 0000000..e641eaa --- /dev/null +++ b/src/lib/auth.ts @@ -0,0 +1,6 @@ +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; + +export function getSession() { + return getServerSession(authOptions); +} diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts new file mode 100644 index 0000000..e2b9605 --- /dev/null +++ b/src/lib/prisma.ts @@ -0,0 +1,9 @@ +import { PrismaClient } from '@prisma/client'; + +const globalForPrisma = globalThis as unknown as { + prisma: PrismaClient | undefined; +}; + +export const prisma = globalForPrisma.prisma ?? new PrismaClient(); + +if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;