invite and saving fitness profile

added, need polishing
This commit is contained in:
echo 2025-11-20 18:36:40 +01:00
parent daaf5aa2dd
commit 118efad70f
10 changed files with 202 additions and 72 deletions

Binary file not shown.

View File

@ -22,6 +22,7 @@
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"axios": "^1.13.2", "axios": "^1.13.2",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"better-sqlite3": "^11.10.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
@ -112,6 +113,7 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5", "@babel/generator": "^7.28.5",
@ -2396,7 +2398,6 @@
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.10.4", "@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5", "@babel/runtime": "^7.12.5",
@ -2417,7 +2418,6 @@
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"dequal": "^2.0.3" "dequal": "^2.0.3"
} }
@ -2427,8 +2427,7 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/@testing-library/jest-dom": { "node_modules/@testing-library/jest-dom": {
"version": "6.9.1", "version": "6.9.1",
@ -2504,8 +2503,7 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/@types/babel__core": { "node_modules/@types/babel__core": {
"version": "7.20.5", "version": "7.20.5",
@ -2689,6 +2687,7 @@
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
@ -2699,6 +2698,7 @@
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
@ -2789,6 +2789,7 @@
"integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.46.3", "@typescript-eslint/scope-manager": "8.46.3",
"@typescript-eslint/types": "8.46.3", "@typescript-eslint/types": "8.46.3",
@ -3333,6 +3334,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@ -3988,6 +3990,17 @@
"bcrypt": "bin/bcrypt" "bcrypt": "bin/bcrypt"
} }
}, },
"node_modules/better-sqlite3": {
"version": "11.10.0",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
}
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@ -4062,6 +4075,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.8.19", "baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751", "caniuse-lite": "^1.0.30001751",
@ -5296,6 +5310,7 @@
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@ -5481,6 +5496,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@rtsao/scc": "^1.1.0", "@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9", "array-includes": "^3.1.9",
@ -8324,18 +8340,6 @@
"url": "https://github.com/chalk/supports-color?sponsor=1" "url": "https://github.com/chalk/supports-color?sponsor=1"
} }
}, },
"node_modules/jiti": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-cookie": { "node_modules/js-cookie": {
"version": "3.0.5", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
@ -8563,7 +8567,6 @@
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"lz-string": "bin/bin.js" "lz-string": "bin/bin.js"
} }
@ -9076,6 +9079,7 @@
"resolved": "https://registry.npmjs.org/next/-/next-16.0.1.tgz", "resolved": "https://registry.npmjs.org/next/-/next-16.0.1.tgz",
"integrity": "sha512-e9RLSssZwd35p7/vOa+hoDFggUZIUbZhIUSLZuETCwrCVvxOs87NamoUzT+vbcNAL8Ld9GobBnWOA6SbV/arOw==", "integrity": "sha512-e9RLSssZwd35p7/vOa+hoDFggUZIUbZhIUSLZuETCwrCVvxOs87NamoUzT+vbcNAL8Ld9GobBnWOA6SbV/arOw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@next/env": "16.0.1", "@next/env": "16.0.1",
"@swc/helpers": "0.5.15", "@swc/helpers": "0.5.15",
@ -9744,6 +9748,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.6", "nanoid": "^3.3.6",
"picocolors": "^1.0.0", "picocolors": "^1.0.0",
@ -9881,7 +9886,6 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.1", "ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0", "ansi-styles": "^5.0.0",
@ -9897,7 +9901,6 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=10" "node": ">=10"
}, },
@ -9910,8 +9913,7 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/promise-inflight": { "node_modules/promise-inflight": {
"version": "1.0.1", "version": "1.0.1",
@ -10043,6 +10045,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -10052,6 +10055,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@ -10064,6 +10068,7 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz",
"integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
@ -10087,6 +10092,7 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/use-sync-external-store": "^0.0.6", "@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0" "use-sync-external-store": "^1.4.0"
@ -10185,7 +10191,8 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/redux-thunk": { "node_modules/redux-thunk": {
"version": "3.1.0", "version": "3.1.0",
@ -11638,6 +11645,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@ -11857,6 +11865,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -12443,6 +12452,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }

View File

@ -25,6 +25,7 @@
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"axios": "^1.13.2", "axios": "^1.13.2",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"better-sqlite3": "^11.10.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",

View File

@ -0,0 +1,33 @@
const Database = require('better-sqlite3');
const path = require('path');
const dbPath = path.join(__dirname, '../../../data/fitai.db');
const db = new Database(dbPath);
console.log('Migrating fitness_profiles table...');
try {
// Check if columns exist
const tableInfo = db.prepare('PRAGMA table_info(fitness_profiles)').all();
const columns = tableInfo.map(c => c.name);
if (!columns.includes('allergies')) {
console.log('Adding allergies column...');
db.prepare('ALTER TABLE fitness_profiles ADD COLUMN allergies TEXT').run();
} else {
console.log('allergies column already exists.');
}
if (!columns.includes('injuries')) {
console.log('Adding injuries column...');
db.prepare('ALTER TABLE fitness_profiles ADD COLUMN injuries TEXT').run();
} else {
console.log('injuries column already exists.');
}
console.log('Migration completed successfully.');
} catch (error) {
console.error('Migration failed:', error);
} finally {
db.close();
}

View File

@ -16,10 +16,20 @@ export async function GET(request: NextRequest) {
const profile = db const profile = db
.prepare( .prepare(
`SELECT * FROM fitness_profiles WHERE user_id = ?` `SELECT * FROM fitness_profiles WHERE userId = ?`
) )
.get(userId); .get(userId);
if (profile) {
const p = profile as any;
// Parse JSON fields
try {
p.fitnessGoals = JSON.parse(p.fitnessGoals || '[]');
} catch (e) {
p.fitnessGoals = [];
}
}
return NextResponse.json({ profile: profile || null }); return NextResponse.json({ profile: profile || null });
} catch (error) { } catch (error) {
console.error("Error fetching fitness profile:", error); console.error("Error fetching fitness profile:", error);
@ -45,19 +55,22 @@ export async function POST(request: NextRequest) {
weight, weight,
age, age,
gender, gender,
fitnessGoal, fitnessGoals, // Changed from fitnessGoal
activityLevel, activityLevel,
medicalConditions, medicalConditions,
allergies, allergies,
injuries, injuries,
exerciseHabits,
dietHabits
} = body; } = body;
// Check if profile exists // Check if profile exists
const existingProfile = db const existingProfile = db
.prepare(`SELECT id FROM fitness_profiles WHERE user_id = ?`) .prepare(`SELECT userId FROM fitness_profiles WHERE userId = ?`)
.get(userId) as { id: string } | undefined; .get(userId) as { userId: string } | undefined;
const now = Date.now(); const now = new Date().toISOString();
const fitnessGoalsJson = JSON.stringify(fitnessGoals || []);
if (existingProfile) { if (existingProfile) {
// Update existing profile // Update existing profile
@ -67,52 +80,55 @@ export async function POST(request: NextRequest) {
weight = ?, weight = ?,
age = ?, age = ?,
gender = ?, gender = ?,
fitness_goal = ?, fitnessGoals = ?,
activity_level = ?, activityLevel = ?,
medical_conditions = ?, medicalConditions = ?,
allergies = ?, allergies = ?,
injuries = ?, injuries = ?,
updated_at = ? exerciseHabits = ?,
WHERE user_id = ?` dietHabits = ?,
updatedAt = ?
WHERE userId = ?`
).run( ).run(
height || null, height || null,
weight || null, weight || null,
age || null, age || null,
gender || null, gender || null,
fitnessGoal || null, fitnessGoalsJson,
activityLevel || null, activityLevel || null,
medicalConditions || null, medicalConditions || null,
allergies || null, allergies || null,
injuries || null, injuries || null,
exerciseHabits || null,
dietHabits || null,
now, now,
userId userId
); );
return NextResponse.json({ return NextResponse.json({
message: "Fitness profile updated successfully", message: "Fitness profile updated successfully",
profileId: existingProfile.id, userId: userId,
}); });
} else { } else {
// Create new profile // Create new profile
const profileId = `fp_${randomBytes(16).toString("hex")}`;
db.prepare( db.prepare(
`INSERT INTO fitness_profiles `INSERT INTO fitness_profiles
(id, user_id, height, weight, age, gender, fitness_goal, activity_level, (userId, height, weight, age, gender, fitnessGoals, activityLevel,
medical_conditions, allergies, injuries, created_at, updated_at) medicalConditions, allergies, injuries, exerciseHabits, dietHabits, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
).run( ).run(
profileId,
userId, userId,
height || null, height || null,
weight || null, weight || null,
age || null, age || null,
gender || null, gender || null,
fitnessGoal || null, fitnessGoalsJson,
activityLevel || null, activityLevel || null,
medicalConditions || null, medicalConditions || null,
allergies || null, allergies || null,
injuries || null, injuries || null,
exerciseHabits || null,
dietHabits || null,
now, now,
now now
); );
@ -120,7 +136,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json( return NextResponse.json(
{ {
message: "Fitness profile created successfully", message: "Fitness profile created successfully",
profileId, userId,
}, },
{ status: 201 } { status: 201 }
); );

View File

@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { getDatabase } from "../../../lib/database/index"; import { getDatabase } from "../../../lib/database/index";
import bcrypt from "bcryptjs"; import bcrypt from "bcryptjs";
import { auth } from "@clerk/nextjs/server"; import { auth, clerkClient } from "@clerk/nextjs/server";
export async function GET(request: NextRequest) { export async function GET(request: NextRequest) {
try { try {
@ -64,9 +64,9 @@ export async function POST(request: NextRequest) {
} }
const body = await request.json(); const body = await request.json();
const { email, password, firstName, lastName, role, phone } = body; const { email, firstName, lastName, role, phone } = body;
if (!email || !password || !firstName || !lastName || !role) { if (!email || !firstName || !lastName || !role) {
return NextResponse.json( return NextResponse.json(
{ error: "Missing required fields" }, { error: "Missing required fields" },
{ status: 400 }, { status: 400 },
@ -75,7 +75,7 @@ export async function POST(request: NextRequest) {
// Enforce Hierarchy // Enforce Hierarchy
const allowed = { const allowed = {
superAdmin: ["admin", "trainer", "client"], // Super Admin can create anyone (except maybe another superAdmin via this UI?) superAdmin: ["admin", "trainer", "client"],
admin: ["trainer", "client"], admin: ["trainer", "client"],
trainer: ["client"], trainer: ["client"],
client: [] client: []
@ -89,7 +89,7 @@ export async function POST(request: NextRequest) {
); );
} }
// Check if user already exists // Check if user already exists locally
const existingUser = await db.getUserByEmail(email); const existingUser = await db.getUserByEmail(email);
if (existingUser) { if (existingUser) {
return NextResponse.json( return NextResponse.json(
@ -98,13 +98,38 @@ export async function POST(request: NextRequest) {
); );
} }
// Hash password // Create Clerk Invitation
const hashedPassword = await bcrypt.hash(password, 12); // Note: We pass the role in publicMetadata so it persists when they sign up
try {
const client = await clerkClient();
await client.invitations.createInvitation({
emailAddress: email,
publicMetadata: {
role,
},
ignoreExisting: true // Don't fail if invite exists
});
} catch (clerkError: any) {
console.error("Clerk invitation error:", clerkError);
// If user already exists in Clerk, we might want to handle it.
// But for now, let's proceed to create local record if invite sent or if they exist.
if (clerkError.errors?.[0]?.code === 'form_identifier_exists') {
return NextResponse.json(
{ error: "User already exists in Clerk system" },
{ status: 409 },
);
}
return NextResponse.json(
{ error: "Failed to send invitation: " + (clerkError.message || "Unknown error") },
{ status: 500 },
);
}
// Create user // Create user in local DB with temporary ID (will be migrated on first login)
// We set a placeholder password since it's required by schema but won't be used
const newUserId = await db.createUser({ const newUserId = await db.createUser({
email, email,
password: hashedPassword, password: "INVITED_USER_PENDING",
firstName, firstName,
lastName, lastName,
role, role,
@ -121,7 +146,7 @@ export async function POST(request: NextRequest) {
}); });
} }
return NextResponse.json({ userId: newUserId.id }, { status: 201 }); return NextResponse.json({ userId: newUserId.id, message: "Invitation sent" }, { status: 201 });
} catch (error) { } catch (error) {
console.error("Create user error:", error); console.error("Create user error:", error);
return NextResponse.json( return NextResponse.json(

View File

@ -139,23 +139,44 @@ export function UserManagement() {
}; };
const handleSaveEdit = async () => { const handleSaveEdit = async () => {
if (!editForm || !selectedUser) return; if (!editForm) return;
try { try {
const response = await fetch("/api/users", { if (selectedUser) {
method: "PUT", // Update existing user
headers: { "Content-Type": "application/json" }, const response = await fetch("/api/users", {
body: JSON.stringify({ id: selectedUser.id, ...editForm }), method: "PUT",
}); headers: { "Content-Type": "application/json" },
if (response.ok) { body: JSON.stringify({ id: selectedUser.id, ...editForm }),
setIsEditing(false); });
setEditForm(null); if (response.ok) {
fetchUsers(); setIsEditing(false);
setEditForm(null);
fetchUsers();
} else {
alert("Error updating user");
}
} else { } else {
alert("Error updating user"); // Create (Invite) new user
const response = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(editForm),
});
if (response.ok) {
setIsEditing(false);
setEditForm(null);
fetchUsers();
alert("Invitation sent successfully!");
} else {
const errorData = await response.json();
alert(`Error sending invitation: ${errorData.error}`);
}
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
alert("An unexpected error occurred");
} }
}; };
@ -196,6 +217,22 @@ export function UserManagement() {
> >
Edit User Edit User
</Button> </Button>
<Button
variant="secondary"
onClick={() => {
setEditForm({
firstName: "",
lastName: "",
email: "",
role: "client",
phone: "",
});
setSelectedUser(null);
setIsEditing(true);
}}
>
Invite User
</Button>
<Button <Button
variant="secondary" variant="secondary"
onClick={handleDeleteUser} onClick={handleDeleteUser}
@ -265,7 +302,7 @@ export function UserManagement() {
{isEditing && editForm && ( {isEditing && editForm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white p-6 rounded-lg shadow-lg max-w-md w-full"> <div className="bg-white p-6 rounded-lg shadow-lg max-w-md w-full">
<h3 className="text-lg font-semibold mb-4">Edit User</h3> <h3 className="text-lg font-semibold mb-4">{selectedUser ? 'Edit User' : 'Invite New User'}</h3>
<form <form
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
@ -310,6 +347,7 @@ export function UserManagement() {
} }
className="w-full border border-gray-300 rounded px-3 py-2" className="w-full border border-gray-300 rounded px-3 py-2"
required required
disabled={!!selectedUser} // Disable email edit for existing users if desired, or keep enabled
/> />
</div> </div>
<div className="mb-4"> <div className="mb-4">
@ -360,7 +398,7 @@ export function UserManagement() {
type="submit" type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700" className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
> >
Save {selectedUser ? 'Save Changes' : 'Send Invitation'}
</button> </button>
</div> </div>
</form> </form>

View File

@ -91,6 +91,8 @@ export class SQLiteDatabase implements IDatabase {
exerciseHabits TEXT, exerciseHabits TEXT,
dietHabits TEXT, dietHabits TEXT,
medicalConditions TEXT, medicalConditions TEXT,
allergies TEXT,
injuries TEXT,
createdAt DATETIME NOT NULL, createdAt DATETIME NOT NULL,
updatedAt DATETIME NOT NULL, updatedAt DATETIME NOT NULL,
FOREIGN KEY (userId) REFERENCES users (id) ON DELETE CASCADE FOREIGN KEY (userId) REFERENCES users (id) ON DELETE CASCADE
@ -198,6 +200,7 @@ export class SQLiteDatabase implements IDatabase {
async deleteUser(id: string): Promise<boolean> { async deleteUser(id: string): Promise<boolean> {
if (!this.db) throw new Error('Database not connected') if (!this.db) throw new Error('Database not connected')
const stmt = this.db.prepare('DELETE FROM users WHERE id = ?')
const result = stmt.run(id) const result = stmt.run(id)
return (result.changes || 0) > 0 return (result.changes || 0) > 0
} }
@ -298,15 +301,15 @@ export class SQLiteDatabase implements IDatabase {
const stmt = this.db.prepare( const stmt = this.db.prepare(
`INSERT INTO fitness_profiles `INSERT INTO fitness_profiles
(userId, height, weight, age, gender, activityLevel, fitnessGoals, (userId, height, weight, age, gender, activityLevel, fitnessGoals,
exerciseHabits, dietHabits, medicalConditions, createdAt, updatedAt) exerciseHabits, dietHabits, medicalConditions, allergies, injuries, createdAt, updatedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
) )
stmt.run( stmt.run(
profile.userId, profile.height, profile.weight, profile.age, profile.gender, profile.userId, profile.height, profile.weight, profile.age, profile.gender,
profile.activityLevel, JSON.stringify(profile.fitnessGoals), profile.exerciseHabits, profile.activityLevel, JSON.stringify(profile.fitnessGoals), profile.exerciseHabits,
profile.dietHabits, profile.medicalConditions, profile.createdAt.toISOString(), profile.dietHabits, profile.medicalConditions, profile.allergies, profile.injuries,
profile.updatedAt.toISOString() profile.createdAt.toISOString(), profile.updatedAt.toISOString()
) )
return profile return profile
@ -468,6 +471,8 @@ export class SQLiteDatabase implements IDatabase {
exerciseHabits: row.exerciseHabits, exerciseHabits: row.exerciseHabits,
dietHabits: row.dietHabits, dietHabits: row.dietHabits,
medicalConditions: row.medicalConditions, medicalConditions: row.medicalConditions,
allergies: row.allergies,
injuries: row.injuries,
createdAt: new Date(row.createdAt), createdAt: new Date(row.createdAt),
updatedAt: new Date(row.updatedAt) updatedAt: new Date(row.updatedAt)
} }

View File

@ -31,6 +31,8 @@ export interface FitnessProfile {
exerciseHabits: string; exerciseHabits: string;
dietHabits: string; dietHabits: string;
medicalConditions: string; medicalConditions: string;
allergies?: string;
injuries?: string;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
} }

View File

@ -1,5 +1,5 @@
export const API_BASE_URL = __DEV__ export const API_BASE_URL = __DEV__
? 'https://5cb23f31d8c1.ngrok-free.app' ? 'https://390dfd6ece05.ngrok-free.app'
: 'https://your-production-url.com' : 'https://your-production-url.com'
export const API_ENDPOINTS = { export const API_ENDPOINTS = {