mirror of
https://github.com/EdiFarcas/Car-Fuel-Tracking-App.git
synced 2026-06-22 05:00:53 +03:00
First protorype
This commit is contained in:
Generated
+363
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
@@ -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])
|
||||
|
||||
@@ -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 };
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 (
|
||||
<main className="max-w-md mx-auto p-6">
|
||||
<h1 className="text-2xl font-bold mb-4">Login</h1>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
required
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full border px-3 py-2 rounded"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
required
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full border px-3 py-2 rounded"
|
||||
/>
|
||||
<button type="submit" className="w-full bg-blue-600 text-white py-2 rounded">
|
||||
Sign In
|
||||
</button>
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
</form>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<main className="max-w-md mx-auto p-6">
|
||||
<h1 className="text-2xl font-bold mb-4">Register</h1>
|
||||
<form onSubmit={handleRegister} className="space-y-4">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
required
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full border px-3 py-2 rounded"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password}
|
||||
required
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full border px-3 py-2 rounded"
|
||||
/>
|
||||
<button type="submit" className="w-full bg-green-600 text-white py-2 rounded">
|
||||
Register
|
||||
</button>
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
</form>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -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<FillUp[]>([]);
|
||||
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<HTMLInputElement | HTMLSelectElement>) => {
|
||||
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 (
|
||||
<main className="max-w-xl mx-auto p-6 space-y-6">
|
||||
<h1 className="text-2xl font-bold">Fuel Fill-Ups</h1>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<input
|
||||
name="mileage"
|
||||
type="number"
|
||||
placeholder="Odometer"
|
||||
value={form.mileage}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full border px-3 py-2 rounded"
|
||||
/>
|
||||
<input
|
||||
name="liters"
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="Liters"
|
||||
value={form.liters}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full border px-3 py-2 rounded"
|
||||
/>
|
||||
<input
|
||||
name="cost"
|
||||
type="number"
|
||||
step="0.01"
|
||||
placeholder="Total Cost"
|
||||
value={form.cost}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full border px-3 py-2 rounded"
|
||||
/>
|
||||
<select
|
||||
name="currency"
|
||||
value={form.currency}
|
||||
onChange={handleChange}
|
||||
className="w-full border px-3 py-2 rounded"
|
||||
>
|
||||
{currencies.map((c) => (
|
||||
<option key={c} value={c}>{c}</option>
|
||||
))}
|
||||
</select>
|
||||
<button type="submit" className="w-full bg-green-600 text-white py-2 rounded">
|
||||
➕ Add Fill-Up
|
||||
</button>
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
</form>
|
||||
|
||||
<ul className="space-y-2">
|
||||
{fillups.map((fill) => (
|
||||
<FillUpCard key={fill.id} fill={fill} /> // ✅
|
||||
))}
|
||||
</ul>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -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<MileageEntry[]>([]);
|
||||
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 (
|
||||
<main className="max-w-xl mx-auto p-6 space-y-6">
|
||||
<h1 className="text-2xl font-bold">Mileage Log</h1>
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex items-center gap-4">
|
||||
<input
|
||||
type="number"
|
||||
value={mileage}
|
||||
onChange={(e) => setMileage(e.target.value)}
|
||||
placeholder="Odometer (e.g. 102300)"
|
||||
className="flex-1 border px-3 py-2 rounded"
|
||||
required
|
||||
/>
|
||||
<button type="submit" className="bg-blue-600 text-white px-4 py-2 rounded">
|
||||
➕ Add
|
||||
</button>
|
||||
</form>
|
||||
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
|
||||
<ul className="space-y-2">
|
||||
{entries.map((entry) => (
|
||||
<li key={entry.id} className="border rounded p-3">
|
||||
<p className="font-medium">{entry.mileage} km</p>
|
||||
<p className="text-sm text-gray-500">{new Date(entry.date).toLocaleString()}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -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 <div className="p-6">Car not found or unauthorized access.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="max-w-3xl mx-auto p-6 space-y-4">
|
||||
<h1 className="text-3xl font-bold">{car.name}</h1>
|
||||
<p className="text-gray-700">{car.make} {car.model} ({car.year})</p>
|
||||
<p className="text-gray-600">Fuel Type: {car.fuelType}</p>
|
||||
|
||||
<div className="flex gap-4 mt-6">
|
||||
<Link
|
||||
href={`/dashboard/cars/${car.id}/mileage`}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded"
|
||||
>
|
||||
➕ Add Mileage
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href={`/dashboard/cars/${car.id}/fillups`}
|
||||
className="bg-green-600 text-white px-4 py-2 rounded"
|
||||
>
|
||||
➕ Add Fill-Up
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href={`/dashboard/cars/${car.id}/stats`}
|
||||
className="bg-purple-600 text-white px-4 py-2 rounded"
|
||||
>
|
||||
📊 View Stats
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -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 <div className="p-6">Car not found or unauthorized.</div>;
|
||||
const fillUps = car.fillUps;
|
||||
|
||||
if (fillUps.length < 2) {
|
||||
return (
|
||||
<main className="max-w-xl mx-auto p-6">
|
||||
<h1 className="text-2xl font-bold mb-2">Not Enough Data</h1>
|
||||
<p className="text-gray-600">Add at least 2 fill-ups to see statistics.</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<main className="max-w-xl mx-auto p-6 space-y-4">
|
||||
<h1 className="text-2xl font-bold">Fuel Stats</h1>
|
||||
|
||||
<div className="space-y-2">
|
||||
<StatCard label="Total Distance" value={`${totalDistance.toFixed(0)} km`} />
|
||||
<StatCard label="Total Fuel Used" value={`${totalLiters.toFixed(2)} L`} />
|
||||
<StatCard label="Total Fuel Cost" value={`${totalCost.toFixed(2)} ${fillUps[0].currency}`} />
|
||||
<StatCard label="Average Consumption" value={`${avgConsumption.toFixed(2)} L / 100km`} />
|
||||
<StatCard label="Average Cost / km" value={`${costPerKm.toFixed(2)} ${fillUps[0].currency}`} />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -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<HTMLInputElement | HTMLSelectElement>) => {
|
||||
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 (
|
||||
<main className="max-w-md mx-auto p-6 space-y-4">
|
||||
<h1 className="text-2xl font-bold">Add New Car</h1>
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<input
|
||||
name="name"
|
||||
value={form.name}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="Car Name"
|
||||
className="w-full border px-3 py-2 rounded"
|
||||
/>
|
||||
<input
|
||||
name="make"
|
||||
value={form.make}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="Make (e.g. BMW)"
|
||||
className="w-full border px-3 py-2 rounded"
|
||||
/>
|
||||
<input
|
||||
name="model"
|
||||
value={form.model}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="Model (e.g. 320i)"
|
||||
className="w-full border px-3 py-2 rounded"
|
||||
/>
|
||||
<input
|
||||
name="year"
|
||||
type="number"
|
||||
value={form.year}
|
||||
onChange={handleChange}
|
||||
required
|
||||
placeholder="Year"
|
||||
className="w-full border px-3 py-2 rounded"
|
||||
/>
|
||||
<select
|
||||
name="fuelType"
|
||||
value={form.fuelType}
|
||||
onChange={handleChange}
|
||||
className="w-full border px-3 py-2 rounded"
|
||||
>
|
||||
{fuelTypes.map((type) => (
|
||||
<option key={type} value={type}>{type}</option>
|
||||
))}
|
||||
</select>
|
||||
<button type="submit" className="w-full bg-blue-600 text-white py-2 rounded">
|
||||
Save Car
|
||||
</button>
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
</form>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<main className="max-w-4xl mx-auto p-6 space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-2xl font-bold">Your Cars</h1>
|
||||
<Link href="/dashboard/cars/new" className="bg-blue-600 text-white px-4 py-2 rounded">
|
||||
+ Add Car
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{user?.cars.length ? (
|
||||
<ul className="grid gap-4">
|
||||
{user.cars.map((car) => (
|
||||
<CarCard key={car.id} car={car} />
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p>No cars added yet.</p>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import Link from 'next/link';
|
||||
import { Car } from '@prisma/client';
|
||||
|
||||
export default function CarCard({ car }: { car: Car }) {
|
||||
return (
|
||||
<Link href={`/dashboard/cars/${car.id}`}>
|
||||
<div className="border rounded-xl p-4 shadow-sm hover:shadow-md transition bg-gray-50">
|
||||
<h2 className="text-xl font-bold text-black">{car.name}</h2>
|
||||
<p className="text-gray-600">{car.make} {car.model} ({car.year})</p>
|
||||
<p className="text-sm text-gray-500 mt-1">Fuel Type: {car.fuelType}</p>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { FillUp } from '@prisma/client';
|
||||
|
||||
export default function FillUpCard({ fill }: { fill: FillUp }) {
|
||||
return (
|
||||
<div className="border rounded p-3 bg-white shadow-sm">
|
||||
<div className="font-semibold text-black">{fill.mileage} km - {fill.liters} L</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{fill.cost} {fill.currency} — {new Date(fill.date).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 text-gray-900">
|
||||
<main className="max-w-4xl mx-auto p-6">{children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export default function StatCard({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="border rounded-xl p-4 shadow-sm bg-white">
|
||||
<p className="text-sm text-gray-500">{label}</p>
|
||||
<p className="text-2xl font-semibold">{value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
|
||||
|
||||
export function getSession() {
|
||||
return getServerSession(authOptions);
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user