diff --git a/Dockerfile b/Dockerfile index ee0b7f6..5c9b8a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,7 @@ # Stage 1: Install dependencies FROM node:20-alpine AS deps WORKDIR /app +RUN apk add --no-cache python3 make g++ COPY package.json package-lock.json* ./ RUN npm ci --omit=dev && npm cache clean --force @@ -10,7 +11,8 @@ ENV NODE_ENV=production WORKDIR /app RUN addgroup -g 1001 -S appgroup && \ - adduser -u 1001 -S appuser -G appgroup + adduser -u 1001 -S appuser -G appgroup && \ + mkdir -p /tmp/health-app && chown appuser:appgroup /tmp/health-app COPY --from=deps /app/node_modules ./node_modules COPY src ./src diff --git a/package-lock.json b/package-lock.json index 5fdde53..cbb740c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,17 @@ { "name": "@infinicaretech/health-app", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@infinicaretech/health-app", - "version": "1.0.0", + "version": "2.0.0", "dependencies": { - "express": "^4.21.2" + "bcryptjs": "^2.4.3", + "better-sqlite3": "^11.7.0", + "express": "^4.21.2", + "jsonwebtoken": "^9.0.2" }, "engines": { "node": ">=20.0.0" @@ -33,6 +36,63 @@ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", + "license": "MIT" + }, + "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/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/body-parser": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", @@ -57,6 +117,36 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -95,6 +185,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -140,6 +236,30 @@ "ms": "2.0.0" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -159,6 +279,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -173,6 +302,15 @@ "node": ">= 0.4" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -188,6 +326,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -233,6 +380,15 @@ "node": ">= 0.6" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/express": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", @@ -279,6 +435,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, "node_modules/finalhandler": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", @@ -315,6 +477,12 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -361,6 +529,12 @@ "node": ">= 0.4" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -429,12 +603,38 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -444,6 +644,97 @@ "node": ">= 0.10" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -513,12 +804,45 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -528,6 +852,18 @@ "node": ">= 0.6" } }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -552,6 +888,15 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -567,6 +912,33 @@ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -580,6 +952,16 @@ "node": ">= 0.10" } }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/qs": { "version": "6.14.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", @@ -619,6 +1001,35 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -645,6 +1056,18 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", @@ -768,6 +1191,51 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -777,6 +1245,52 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -786,6 +1300,18 @@ "node": ">=0.6" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -808,6 +1334,12 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -825,6 +1357,12 @@ "engines": { "node": ">= 0.8" } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" } } } diff --git a/package.json b/package.json index 6587130..9073757 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,18 @@ { "name": "@infinicaretech/health-app", - "version": "1.0.0", + "version": "2.0.0", "private": true, - "description": "Production-ready health check API for GitOps deployment", + "description": "Comprehensive health & fitness web application with workout and meal planning", "main": "src/server.js", "scripts": { "start": "node src/server.js", "dev": "node --watch src/server.js" }, "dependencies": { - "express": "^4.21.2" + "bcryptjs": "^2.4.3", + "better-sqlite3": "^11.7.0", + "express": "^4.21.2", + "jsonwebtoken": "^9.0.2" }, "engines": { "node": ">=20.0.0" diff --git a/src/database.js b/src/database.js new file mode 100644 index 0000000..9512531 --- /dev/null +++ b/src/database.js @@ -0,0 +1,48 @@ +"use strict"; + +const Database = require("better-sqlite3"); +const path = require("path"); + +const DB_PATH = process.env.DB_PATH || "/tmp/health-app/health-app.db"; + +let db; + +function getDb() { + if (!db) { + db = new Database(DB_PATH); + db.pragma("journal_mode = WAL"); + db.pragma("foreign_keys = ON"); + initTables(); + } + return db; +} + +function initTables() { + db.exec(` + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + name TEXT NOT NULL, + created_at TEXT DEFAULT (datetime('now')) + ); + + CREATE TABLE IF NOT EXISTS profiles ( + user_id INTEGER PRIMARY KEY, + age INTEGER, + gender TEXT, + height REAL, + weight REAL, + activity_level TEXT DEFAULT 'moderate', + country TEXT DEFAULT 'Turkey', + city TEXT DEFAULT '', + goal TEXT DEFAULT 'maintain', + allergies TEXT DEFAULT '', + dietary_restrictions TEXT DEFAULT '', + updated_at TEXT DEFAULT (datetime('now')), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ); + `); +} + +module.exports = { getDb }; diff --git a/src/middleware/auth.js b/src/middleware/auth.js new file mode 100644 index 0000000..1835dbc --- /dev/null +++ b/src/middleware/auth.js @@ -0,0 +1,24 @@ +"use strict"; + +const jwt = require("jsonwebtoken"); + +const JWT_SECRET = process.env.JWT_SECRET || "health-app-default-secret-change-in-production"; + +function authenticateToken(req, res, next) { + const authHeader = req.headers["authorization"]; + const token = authHeader && authHeader.startsWith("Bearer ") ? authHeader.slice(7) : null; + + if (!token) { + return res.status(401).json({ error: "Yetkilendirme token'ı gerekli" }); + } + + try { + const decoded = jwt.verify(token, JWT_SECRET); + req.user = decoded; + next(); + } catch (err) { + return res.status(403).json({ error: "Geçersiz veya süresi dolmuş token" }); + } +} + +module.exports = { authenticateToken, JWT_SECRET }; diff --git a/src/public/index.html b/src/public/index.html new file mode 100644 index 0000000..8696e9d --- /dev/null +++ b/src/public/index.html @@ -0,0 +1,1142 @@ + + + + + + FitLife - Sağlık & Fitness + + + + + + + + +
+
+

Giris Yap

+

Hesabiniza giris yapin

+ +
+
+ + +
+
+ + +
+ +
+ +
+
+ + + + + + + + + + + + + diff --git a/src/routes/auth.js b/src/routes/auth.js new file mode 100644 index 0000000..ce0dfe0 --- /dev/null +++ b/src/routes/auth.js @@ -0,0 +1,92 @@ +"use strict"; + +const express = require("express"); +const bcrypt = require("bcryptjs"); +const jwt = require("jsonwebtoken"); +const { getDb } = require("../database"); +const { authenticateToken, JWT_SECRET } = require("../middleware/auth"); + +const router = express.Router(); + +// POST /api/auth/register +router.post("/register", (req, res) => { + try { + const { email, password, name } = req.body; + + if (!email || !password || !name) { + return res.status(400).json({ error: "Email, şifre ve isim gereklidir" }); + } + + if (password.length < 6) { + return res.status(400).json({ error: "Şifre en az 6 karakter olmalıdır" }); + } + + const db = getDb(); + const existing = db.prepare("SELECT id FROM users WHERE email = ?").get(email); + if (existing) { + return res.status(409).json({ error: "Bu email adresi zaten kayıtlı" }); + } + + const passwordHash = bcrypt.hashSync(password, 10); + const result = db.prepare("INSERT INTO users (email, password_hash, name) VALUES (?, ?, ?)").run(email, passwordHash, name); + + const token = jwt.sign({ id: result.lastInsertRowid, email, name }, JWT_SECRET, { expiresIn: "7d" }); + + res.status(201).json({ + message: "Kayıt başarılı", + token, + user: { id: result.lastInsertRowid, email, name }, + }); + } catch (err) { + console.error("Register error:", err.message); + res.status(500).json({ error: "Sunucu hatası" }); + } +}); + +// POST /api/auth/login +router.post("/login", (req, res) => { + try { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ error: "Email ve şifre gereklidir" }); + } + + const db = getDb(); + const user = db.prepare("SELECT * FROM users WHERE email = ?").get(email); + + if (!user || !bcrypt.compareSync(password, user.password_hash)) { + return res.status(401).json({ error: "Geçersiz email veya şifre" }); + } + + const token = jwt.sign({ id: user.id, email: user.email, name: user.name }, JWT_SECRET, { expiresIn: "7d" }); + + res.json({ + message: "Giriş başarılı", + token, + user: { id: user.id, email: user.email, name: user.name }, + }); + } catch (err) { + console.error("Login error:", err.message); + res.status(500).json({ error: "Sunucu hatası" }); + } +}); + +// GET /api/auth/me +router.get("/me", authenticateToken, (req, res) => { + try { + const db = getDb(); + const user = db.prepare("SELECT id, email, name, created_at FROM users WHERE id = ?").get(req.user.id); + + if (!user) { + return res.status(404).json({ error: "Kullanıcı bulunamadı" }); + } + + res.json({ user }); + } catch (err) { + console.error("Me error:", err.message); + res.status(500).json({ error: "Sunucu hatası" }); + } +}); + +module.exports = router; diff --git a/src/routes/profile.js b/src/routes/profile.js new file mode 100644 index 0000000..6289b12 --- /dev/null +++ b/src/routes/profile.js @@ -0,0 +1,72 @@ +"use strict"; + +const express = require("express"); +const { getDb } = require("../database"); +const { authenticateToken } = require("../middleware/auth"); + +const router = express.Router(); + +// GET /api/profile +router.get("/", authenticateToken, (req, res) => { + try { + const db = getDb(); + const profile = db.prepare("SELECT * FROM profiles WHERE user_id = ?").get(req.user.id); + + if (!profile) { + return res.json({ profile: null }); + } + + res.json({ profile }); + } catch (err) { + console.error("Get profile error:", err.message); + res.status(500).json({ error: "Sunucu hatası" }); + } +}); + +// POST /api/profile +router.post("/", authenticateToken, (req, res) => { + try { + const { age, gender, height, weight, activity_level, country, city, goal, allergies, dietary_restrictions } = req.body; + + if (!age || !gender || !height || !weight) { + return res.status(400).json({ error: "Yaş, cinsiyet, boy ve kilo gereklidir" }); + } + + const validGenders = ["male", "female"]; + const validActivityLevels = ["sedentary", "light", "moderate", "active", "very_active"]; + const validGoals = ["lose_weight", "gain_weight", "build_muscle", "maintain"]; + + if (!validGenders.includes(gender)) { + return res.status(400).json({ error: "Geçersiz cinsiyet değeri" }); + } + if (activity_level && !validActivityLevels.includes(activity_level)) { + return res.status(400).json({ error: "Geçersiz aktivite seviyesi" }); + } + if (goal && !validGoals.includes(goal)) { + return res.status(400).json({ error: "Geçersiz hedef" }); + } + + const db = getDb(); + const existing = db.prepare("SELECT user_id FROM profiles WHERE user_id = ?").get(req.user.id); + + if (existing) { + db.prepare(` + UPDATE profiles SET age=?, gender=?, height=?, weight=?, activity_level=?, country=?, city=?, goal=?, allergies=?, dietary_restrictions=?, updated_at=datetime('now') + WHERE user_id=? + `).run(age, gender, height, weight, activity_level || "moderate", country || "Turkey", city || "", goal || "maintain", allergies || "", dietary_restrictions || "", req.user.id); + } else { + db.prepare(` + INSERT INTO profiles (user_id, age, gender, height, weight, activity_level, country, city, goal, allergies, dietary_restrictions) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run(req.user.id, age, gender, height, weight, activity_level || "moderate", country || "Turkey", city || "", goal || "maintain", allergies || "", dietary_restrictions || ""); + } + + const profile = db.prepare("SELECT * FROM profiles WHERE user_id = ?").get(req.user.id); + res.json({ message: "Profil güncellendi", profile }); + } catch (err) { + console.error("Save profile error:", err.message); + res.status(500).json({ error: "Sunucu hatası" }); + } +}); + +module.exports = router; diff --git a/src/routes/program.js b/src/routes/program.js new file mode 100644 index 0000000..6ce21f3 --- /dev/null +++ b/src/routes/program.js @@ -0,0 +1,63 @@ +"use strict"; + +const express = require("express"); +const { getDb } = require("../database"); +const { authenticateToken } = require("../middleware/auth"); +const { generateWorkoutProgram } = require("../services/trainer"); +const { generateMealPlan } = require("../services/dietitian"); +const { generateShoppingList } = require("../services/shopping"); + +const router = express.Router(); + +function getProfile(userId) { + const db = getDb(); + return db.prepare("SELECT * FROM profiles WHERE user_id = ?").get(userId); +} + +// GET /api/program/workout +router.get("/workout", authenticateToken, (req, res) => { + try { + const profile = getProfile(req.user.id); + if (!profile) { + return res.status(400).json({ error: "Lütfen önce profilinizi oluşturun" }); + } + const program = generateWorkoutProgram(profile); + res.json(program); + } catch (err) { + console.error("Workout program error:", err.message); + res.status(500).json({ error: "Program oluşturulurken hata oluştu" }); + } +}); + +// GET /api/program/meal +router.get("/meal", authenticateToken, (req, res) => { + try { + const profile = getProfile(req.user.id); + if (!profile) { + return res.status(400).json({ error: "Lütfen önce profilinizi oluşturun" }); + } + const plan = generateMealPlan(profile); + res.json(plan); + } catch (err) { + console.error("Meal plan error:", err.message); + res.status(500).json({ error: "Beslenme planı oluşturulurken hata oluştu" }); + } +}); + +// GET /api/program/shopping +router.get("/shopping", authenticateToken, (req, res) => { + try { + const profile = getProfile(req.user.id); + if (!profile) { + return res.status(400).json({ error: "Lütfen önce profilinizi oluşturun" }); + } + const mealPlan = generateMealPlan(profile); + const shoppingList = generateShoppingList(mealPlan); + res.json(shoppingList); + } catch (err) { + console.error("Shopping list error:", err.message); + res.status(500).json({ error: "Alışveriş listesi oluşturulurken hata oluştu" }); + } +}); + +module.exports = router; diff --git a/src/server.js b/src/server.js index 47335fb..5d34ad5 100644 --- a/src/server.js +++ b/src/server.js @@ -1,6 +1,12 @@ "use strict"; const express = require("express"); +const path = require("path"); +const { getDb } = require("./database"); + +const authRoutes = require("./routes/auth"); +const profileRoutes = require("./routes/profile"); +const programRoutes = require("./routes/program"); const app = express(); const PORT = parseInt(process.env.PORT, 10) || 3000; @@ -8,14 +14,17 @@ const startTime = Date.now(); app.disable("x-powered-by"); -app.get("/", (_req, res) => { - res.json({ - service: "health-app", - version: process.env.APP_VERSION || "1.0.0", - environment: process.env.NODE_ENV || "development", - }); -}); +// Body parsing +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +// Static files +app.use(express.static(path.join(__dirname, "public"))); + +// Initialize database on startup +getDb(); + +// Health & readiness probes (Kubernetes) app.get("/health", (_req, res) => { res.json({ status: "healthy", @@ -26,16 +35,36 @@ app.get("/health", (_req, res) => { }); app.get("/ready", (_req, res) => { - res.json({ ready: true }); + try { + getDb(); + res.json({ ready: true }); + } catch (err) { + res.status(503).json({ ready: false, error: err.message }); + } }); -app.use((_req, res) => { - res.status(404).json({ error: "Not Found" }); +// API routes +app.use("/api/auth", authRoutes); +app.use("/api/profile", profileRoutes); +app.use("/api/program", programRoutes); + +// SPA fallback - serve index.html for non-API routes +app.get("*", (req, res) => { + if (req.path.startsWith("/api/")) { + return res.status(404).json({ error: "API endpoint bulunamadı" }); + } + res.sendFile(path.join(__dirname, "public", "index.html")); }); +// 404 handler for API +app.use((req, res) => { + res.status(404).json({ error: "Bulunamadı" }); +}); + +// Error handler app.use((err, _req, res, _next) => { console.error("Unhandled error:", err.message); - res.status(500).json({ error: "Internal Server Error" }); + res.status(500).json({ error: "Sunucu hatası" }); }); const server = app.listen(PORT, "0.0.0.0", () => { diff --git a/src/services/dietitian.js b/src/services/dietitian.js new file mode 100644 index 0000000..e6e50e6 --- /dev/null +++ b/src/services/dietitian.js @@ -0,0 +1,754 @@ +"use strict"; + +// Mifflin-St Jeor BMR +function calculateBMR(weight, height, age, gender) { + if (gender === "male") { + return 10 * weight + 6.25 * height - 5 * age + 5; + } + return 10 * weight + 6.25 * height - 5 * age - 161; +} + +const ACTIVITY_MULTIPLIERS = { + sedentary: 1.2, + light: 1.375, + moderate: 1.55, + active: 1.725, + very_active: 1.9, +}; + +function calculateTDEE(bmr, activityLevel) { + return bmr * (ACTIVITY_MULTIPLIERS[activityLevel] || 1.55); +} + +function adjustCalories(tdee, goal) { + switch (goal) { + case "lose_weight": return tdee - 500; + case "gain_weight": return tdee + 500; + case "build_muscle": return tdee + 300; + default: return tdee; + } +} + +function calculateMacros(calories, goal) { + let proteinPct, carbsPct, fatPct; + switch (goal) { + case "lose_weight": + proteinPct = 0.35; carbsPct = 0.35; fatPct = 0.30; break; + case "build_muscle": + proteinPct = 0.35; carbsPct = 0.40; fatPct = 0.25; break; + case "gain_weight": + proteinPct = 0.30; carbsPct = 0.45; fatPct = 0.25; break; + default: + proteinPct = 0.30; carbsPct = 0.40; fatPct = 0.30; + } + return { + protein_g: Math.round((calories * proteinPct) / 4), + carbs_g: Math.round((calories * carbsPct) / 4), + fat_g: Math.round((calories * fatPct) / 9), + }; +} + +// ==================== TURKISH MEALS ==================== +const TURKISH_BREAKFASTS = [ + { + name: "Menemen & Simit Kahvaltısı", + ingredients: [ + { item: "Yumurta", amount: "2 adet", grams: 120 }, + { item: "Domates", amount: "1 adet", grams: 100 }, + { item: "Biber (sivri)", amount: "2 adet", grams: 60 }, + { item: "Zeytinyağı", amount: "1 yemek kaşığı", grams: 14 }, + { item: "Simit", amount: "1/2 adet", grams: 60 }, + { item: "Beyaz peynir", amount: "40g", grams: 40 }, + ], + calories: 420, protein: 22, carbs: 35, fat: 22, + }, + { + name: "Peynirli Kahvaltı Tabağı", + ingredients: [ + { item: "Beyaz peynir", amount: "60g", grams: 60 }, + { item: "Kaşar peyniri", amount: "30g", grams: 30 }, + { item: "Zeytin (siyah)", amount: "8 adet", grams: 30 }, + { item: "Domates", amount: "1 adet", grams: 120 }, + { item: "Salatalık", amount: "1 adet", grams: 100 }, + { item: "Bal", amount: "1 tatlı kaşığı", grams: 10 }, + { item: "Tam buğday ekmek", amount: "2 dilim", grams: 60 }, + { item: "Tereyağı", amount: "10g", grams: 10 }, + ], + calories: 450, protein: 20, carbs: 38, fat: 24, + }, + { + name: "Sucuklu Yumurta", + ingredients: [ + { item: "Yumurta", amount: "2 adet", grams: 120 }, + { item: "Sucuk", amount: "40g", grams: 40 }, + { item: "Tam buğday ekmek", amount: "1 dilim", grams: 30 }, + { item: "Domates", amount: "1/2 adet", grams: 60 }, + { item: "Yeşil biber", amount: "1 adet", grams: 30 }, + ], + calories: 400, protein: 24, carbs: 18, fat: 26, + }, + { + name: "Kaymak & Bal ile Simit", + ingredients: [ + { item: "Simit", amount: "1 adet", grams: 120 }, + { item: "Kaymak", amount: "30g", grams: 30 }, + { item: "Bal", amount: "1 yemek kaşığı", grams: 20 }, + { item: "Çay", amount: "1 bardak", grams: 200 }, + { item: "Beyaz peynir", amount: "30g", grams: 30 }, + ], + calories: 460, protein: 14, carbs: 55, fat: 20, + }, + { + name: "Sahanda Yumurta & Peynir", + ingredients: [ + { item: "Yumurta", amount: "2 adet", grams: 120 }, + { item: "Tereyağı", amount: "10g", grams: 10 }, + { item: "Beyaz peynir", amount: "40g", grams: 40 }, + { item: "Domates", amount: "1 adet", grams: 100 }, + { item: "Zeytin", amount: "6 adet", grams: 24 }, + { item: "Tam buğday ekmek", amount: "1 dilim", grams: 30 }, + ], + calories: 410, protein: 23, carbs: 20, fat: 27, + }, + { + name: "Gözleme (Peynirli & Ispanaklı)", + ingredients: [ + { item: "Un (hamur)", amount: "80g", grams: 80 }, + { item: "Ispanak", amount: "60g", grams: 60 }, + { item: "Beyaz peynir", amount: "50g", grams: 50 }, + { item: "Tereyağı", amount: "10g", grams: 10 }, + { item: "Ayran", amount: "1 bardak", grams: 200 }, + ], + calories: 430, protein: 20, carbs: 42, fat: 20, + }, + { + name: "Çılbır (Yumurtalı Yoğurt)", + ingredients: [ + { item: "Yumurta", amount: "2 adet", grams: 120 }, + { item: "Yoğurt", amount: "150g", grams: 150 }, + { item: "Tereyağı", amount: "10g", grams: 10 }, + { item: "Pul biber", amount: "1 çay kaşığı", grams: 2 }, + { item: "Tam buğday ekmek", amount: "1 dilim", grams: 30 }, + ], + calories: 380, protein: 24, carbs: 22, fat: 22, + }, +]; + +const TURKISH_LUNCHES = [ + { + name: "Mercimek Çorbası & Bulgur Pilavı", + ingredients: [ + { item: "Kırmızı mercimek", amount: "80g", grams: 80 }, + { item: "Soğan", amount: "1/2 adet", grams: 40 }, + { item: "Havuç", amount: "1/2 adet", grams: 40 }, + { item: "Zeytinyağı", amount: "1 yemek kaşığı", grams: 14 }, + { item: "Limon", amount: "1/2 adet", grams: 30 }, + { item: "Bulgur pilavı", amount: "150g (pişmiş)", grams: 150 }, + { item: "Tereyağı", amount: "5g", grams: 5 }, + ], + calories: 480, protein: 22, carbs: 68, fat: 14, + }, + { + name: "Izgara Köfte & Salata", + ingredients: [ + { item: "Dana kıyma (az yağlı)", amount: "150g", grams: 150 }, + { item: "Soğan (rendelenmiş)", amount: "30g", grams: 30 }, + { item: "Maydanoz", amount: "10g", grams: 10 }, + { item: "Çoban salatası", amount: "150g", grams: 150 }, + { item: "Tam buğday ekmek", amount: "1 dilim", grams: 30 }, + { item: "Sumak", amount: "1 çay kaşığı", grams: 2 }, + ], + calories: 450, protein: 38, carbs: 22, fat: 24, + }, + { + name: "Tavuk Şiş & Bulgur", + ingredients: [ + { item: "Tavuk göğsü", amount: "180g", grams: 180 }, + { item: "Biber", amount: "2 adet", grams: 60 }, + { item: "Domates", amount: "1 adet", grams: 100 }, + { item: "Bulgur pilavı", amount: "150g (pişmiş)", grams: 150 }, + { item: "Zeytinyağı", amount: "1 tatlı kaşığı", grams: 7 }, + { item: "Soğan", amount: "1/4 adet", grams: 20 }, + ], + calories: 480, protein: 45, carbs: 42, fat: 14, + }, + { + name: "Kuru Fasulye & Pilav", + ingredients: [ + { item: "Kuru fasulye (pişmiş)", amount: "200g", grams: 200 }, + { item: "Domates salçası", amount: "1 yemek kaşığı", grams: 15 }, + { item: "Soğan", amount: "1/2 adet", grams: 40 }, + { item: "Tereyağlı pirinç pilavı", amount: "150g (pişmiş)", grams: 150 }, + { item: "Turşu", amount: "50g", grams: 50 }, + ], + calories: 520, protein: 24, carbs: 78, fat: 12, + }, + { + name: "Patlıcan Musakka", + ingredients: [ + { item: "Patlıcan", amount: "200g", grams: 200 }, + { item: "Dana kıyma", amount: "100g", grams: 100 }, + { item: "Domates", amount: "1 adet", grams: 100 }, + { item: "Biber", amount: "1 adet", grams: 30 }, + { item: "Soğan", amount: "1/2 adet", grams: 40 }, + { item: "Zeytinyağı", amount: "1 yemek kaşığı", grams: 14 }, + { item: "Pilav", amount: "100g (pişmiş)", grams: 100 }, + ], + calories: 470, protein: 28, carbs: 35, fat: 24, + }, + { + name: "Karnıyarık", + ingredients: [ + { item: "Patlıcan", amount: "250g", grams: 250 }, + { item: "Dana kıyma", amount: "100g", grams: 100 }, + { item: "Domates", amount: "1 adet", grams: 100 }, + { item: "Soğan", amount: "1/2 adet", grams: 40 }, + { item: "Sarımsak", amount: "2 diş", grams: 6 }, + { item: "Zeytinyağı", amount: "1 yemek kaşığı", grams: 14 }, + { item: "Bulgur pilavı", amount: "100g (pişmiş)", grams: 100 }, + ], + calories: 490, protein: 28, carbs: 38, fat: 25, + }, + { + name: "İmam Bayıldı & Pilav", + ingredients: [ + { item: "Patlıcan", amount: "250g", grams: 250 }, + { item: "Domates", amount: "2 adet", grams: 200 }, + { item: "Soğan", amount: "1 adet", grams: 80 }, + { item: "Sarımsak", amount: "3 diş", grams: 9 }, + { item: "Zeytinyağı", amount: "2 yemek kaşığı", grams: 28 }, + { item: "Pilav", amount: "120g (pişmiş)", grams: 120 }, + ], + calories: 440, protein: 10, carbs: 48, fat: 24, + }, + { + name: "Lahmacun & Ayran", + ingredients: [ + { item: "Lahmacun", amount: "2 adet", grams: 200 }, + { item: "Maydanoz", amount: "15g", grams: 15 }, + { item: "Limon", amount: "1/2 adet", grams: 30 }, + { item: "Domates", amount: "1/2 adet", grams: 60 }, + { item: "Ayran", amount: "1 bardak", grams: 200 }, + ], + calories: 460, protein: 24, carbs: 52, fat: 16, + }, + { + name: "Etli Pide", + ingredients: [ + { item: "Pide hamuru", amount: "150g", grams: 150 }, + { item: "Dana kıyma", amount: "100g", grams: 100 }, + { item: "Domates", amount: "1/2 adet", grams: 60 }, + { item: "Biber", amount: "1 adet", grams: 30 }, + { item: "Soğan", amount: "1/4 adet", grams: 20 }, + { item: "Ayran", amount: "1 bardak", grams: 200 }, + ], + calories: 530, protein: 30, carbs: 52, fat: 22, + }, + { + name: "Ezogelin Çorbası & Tavuk Sote", + ingredients: [ + { item: "Kırmızı mercimek", amount: "50g", grams: 50 }, + { item: "Bulgur", amount: "20g", grams: 20 }, + { item: "Domates salçası", amount: "1 yemek kaşığı", grams: 15 }, + { item: "Tavuk göğsü", amount: "150g", grams: 150 }, + { item: "Biber", amount: "2 adet", grams: 60 }, + { item: "Domates", amount: "1 adet", grams: 100 }, + { item: "Zeytinyağı", amount: "1 yemek kaşığı", grams: 14 }, + ], + calories: 460, protein: 40, carbs: 36, fat: 16, + }, + { + name: "Yayla Çorbası & Mantı", + ingredients: [ + { item: "Yoğurt", amount: "150g", grams: 150 }, + { item: "Pirinç", amount: "30g", grams: 30 }, + { item: "Yumurta", amount: "1 adet", grams: 60 }, + { item: "Nane (kuru)", amount: "1 çay kaşığı", grams: 1 }, + { item: "Mantı (dondurulmuş veya ev yapımı)", amount: "150g", grams: 150 }, + { item: "Tereyağı", amount: "10g", grams: 10 }, + ], + calories: 500, protein: 26, carbs: 54, fat: 20, + }, + { + name: "Taze Fasulye Yemeği & Pilav", + ingredients: [ + { item: "Taze fasulye", amount: "250g", grams: 250 }, + { item: "Domates", amount: "1 adet", grams: 100 }, + { item: "Soğan", amount: "1/2 adet", grams: 40 }, + { item: "Zeytinyağı", amount: "1 yemek kaşığı", grams: 14 }, + { item: "Pilav", amount: "150g (pişmiş)", grams: 150 }, + ], + calories: 420, protein: 12, carbs: 58, fat: 16, + }, + { + name: "Bamya Yemeği & Bulgur", + ingredients: [ + { item: "Bamya", amount: "200g", grams: 200 }, + { item: "Domates", amount: "1 adet", grams: 100 }, + { item: "Et (kuşbaşı)", amount: "100g", grams: 100 }, + { item: "Soğan", amount: "1/2 adet", grams: 40 }, + { item: "Limon suyu", amount: "1 yemek kaşığı", grams: 15 }, + { item: "Bulgur pilavı", amount: "120g (pişmiş)", grams: 120 }, + ], + calories: 440, protein: 30, carbs: 42, fat: 16, + }, + { + name: "Adana Kebap & Şalgam", + ingredients: [ + { item: "Adana kebap", amount: "200g", grams: 200 }, + { item: "Lavaş ekmek", amount: "1 adet", grams: 60 }, + { item: "Közlenmiş domates", amount: "1 adet", grams: 100 }, + { item: "Közlenmiş biber", amount: "2 adet", grams: 60 }, + { item: "Soğan", amount: "1/4 adet", grams: 20 }, + { item: "Şalgam suyu", amount: "1 bardak", grams: 200 }, + ], + calories: 520, protein: 36, carbs: 30, fat: 28, + }, +]; + +const TURKISH_DINNERS = [ + { + name: "Zeytinyağlı Yaprak Sarma & Cacık", + ingredients: [ + { item: "Asma yaprağı sarması", amount: "8 adet", grams: 160 }, + { item: "Yoğurt (cacık)", amount: "150g", grams: 150 }, + { item: "Salatalık", amount: "1/2 adet", grams: 50 }, + { item: "Sarımsak", amount: "1 diş", grams: 3 }, + { item: "Nane", amount: "1 çay kaşığı", grams: 1 }, + { item: "Tam buğday ekmek", amount: "1 dilim", grams: 30 }, + ], + calories: 380, protein: 14, carbs: 48, fat: 14, + }, + { + name: "Tavuk Izgara & Kısır", + ingredients: [ + { item: "Tavuk göğsü (ızgara)", amount: "180g", grams: 180 }, + { item: "Kısır (bulgur salatası)", amount: "150g", grams: 150 }, + { item: "Nar ekşisi", amount: "1 tatlı kaşığı", grams: 5 }, + { item: "Maydanoz", amount: "15g", grams: 15 }, + { item: "Domates", amount: "1/2 adet", grams: 60 }, + ], + calories: 440, protein: 44, carbs: 38, fat: 10, + }, + { + name: "Kabak Mücver & Yoğurt", + ingredients: [ + { item: "Kabak (rendelenmiş)", amount: "200g", grams: 200 }, + { item: "Un", amount: "30g", grams: 30 }, + { item: "Yumurta", amount: "1 adet", grams: 60 }, + { item: "Dereotu", amount: "10g", grams: 10 }, + { item: "Beyaz peynir", amount: "30g", grams: 30 }, + { item: "Zeytinyağı", amount: "1 yemek kaşığı", grams: 14 }, + { item: "Yoğurt", amount: "100g", grams: 100 }, + ], + calories: 360, protein: 18, carbs: 28, fat: 20, + }, + { + name: "Su Böreği", + ingredients: [ + { item: "Yufka", amount: "3 yaprak", grams: 120 }, + { item: "Beyaz peynir", amount: "100g", grams: 100 }, + { item: "Maydanoz", amount: "15g", grams: 15 }, + { item: "Yumurta", amount: "1 adet", grams: 60 }, + { item: "Süt", amount: "50ml", grams: 50 }, + { item: "Tereyağı", amount: "15g", grams: 15 }, + ], + calories: 440, protein: 22, carbs: 38, fat: 22, + }, + { + name: "Mercimek Köftesi & Salata", + ingredients: [ + { item: "Kırmızı mercimek", amount: "100g", grams: 100 }, + { item: "İnce bulgur", amount: "80g", grams: 80 }, + { item: "Soğan", amount: "1 adet", grams: 80 }, + { item: "Domates salçası", amount: "1 yemek kaşığı", grams: 15 }, + { item: "Marul", amount: "4 yaprak", grams: 40 }, + { item: "Limon", amount: "1/2 adet", grams: 30 }, + { item: "Zeytinyağı", amount: "1 yemek kaşığı", grams: 14 }, + ], + calories: 400, protein: 20, carbs: 58, fat: 10, + }, + { + name: "Sigara Böreği & Çorba", + ingredients: [ + { item: "Yufka (sigara böreği)", amount: "4 adet", grams: 120 }, + { item: "Beyaz peynir", amount: "60g", grams: 60 }, + { item: "Maydanoz", amount: "10g", grams: 10 }, + { item: "Zeytinyağı (kızartma)", amount: "15g", grams: 15 }, + { item: "Domates çorbası", amount: "250ml", grams: 250 }, + ], + calories: 420, protein: 18, carbs: 36, fat: 22, + }, + { + name: "Çoban Salatası & Humus & Ekmek", + ingredients: [ + { item: "Domates", amount: "2 adet", grams: 200 }, + { item: "Salatalık", amount: "1 adet", grams: 100 }, + { item: "Soğan", amount: "1/2 adet", grams: 40 }, + { item: "Biber", amount: "1 adet", grams: 30 }, + { item: "Humus", amount: "100g", grams: 100 }, + { item: "Zeytinyağı", amount: "1 yemek kaşığı", grams: 14 }, + { item: "Tam buğday ekmek", amount: "2 dilim", grams: 60 }, + ], + calories: 400, protein: 16, carbs: 42, fat: 20, + }, + { + name: "Havuç Tarator", + ingredients: [ + { item: "Havuç", amount: "300g", grams: 300 }, + { item: "Yoğurt", amount: "200g", grams: 200 }, + { item: "Sarımsak", amount: "2 diş", grams: 6 }, + { item: "Zeytinyağı", amount: "1 yemek kaşığı", grams: 14 }, + { item: "Ceviz", amount: "20g", grams: 20 }, + { item: "Tam buğday ekmek", amount: "2 dilim", grams: 60 }, + ], + calories: 380, protein: 16, carbs: 44, fat: 16, + }, + { + name: "İçli Köfte", + ingredients: [ + { item: "İnce bulgur (dış)", amount: "100g", grams: 100 }, + { item: "Dana kıyma (iç)", amount: "80g", grams: 80 }, + { item: "Soğan", amount: "1/2 adet", grams: 40 }, + { item: "Ceviz", amount: "15g", grams: 15 }, + { item: "Maydanoz", amount: "10g", grams: 10 }, + { item: "Yoğurt", amount: "100g", grams: 100 }, + ], + calories: 470, protein: 28, carbs: 52, fat: 16, + }, + { + name: "Balık Izgara & Roka Salatası", + ingredients: [ + { item: "Levrek veya çipura", amount: "200g", grams: 200 }, + { item: "Roka", amount: "50g", grams: 50 }, + { item: "Limon", amount: "1/2 adet", grams: 30 }, + { item: "Zeytinyağı", amount: "1 yemek kaşığı", grams: 14 }, + { item: "Domates", amount: "1/2 adet", grams: 60 }, + { item: "Tam buğday ekmek", amount: "1 dilim", grams: 30 }, + ], + calories: 380, protein: 42, carbs: 16, fat: 16, + }, +]; + +const TURKISH_SNACKS = [ + { name: "Ayran & Ceviz", ingredients: [{ item: "Ayran", amount: "1 bardak", grams: 200 }, { item: "Ceviz", amount: "30g", grams: 30 }], calories: 240, protein: 10, carbs: 10, fat: 18 }, + { name: "Meyve & Yoğurt", ingredients: [{ item: "Mevsim meyvesi", amount: "1 porsiyon", grams: 150 }, { item: "Yoğurt", amount: "100g", grams: 100 }], calories: 170, protein: 6, carbs: 28, fat: 4 }, + { name: "Kuru Meyve & Fındık", ingredients: [{ item: "Kuru kayısı", amount: "4 adet", grams: 30 }, { item: "Fındık", amount: "20g", grams: 20 }, { item: "Badem", amount: "10g", grams: 10 }], calories: 200, protein: 5, carbs: 22, fat: 12 }, + { name: "Peynirli Kraker", ingredients: [{ item: "Tam buğday kraker", amount: "4 adet", grams: 30 }, { item: "Beyaz peynir", amount: "30g", grams: 30 }], calories: 170, protein: 8, carbs: 18, fat: 8 }, + { name: "Muzlu Süt", ingredients: [{ item: "Muz", amount: "1 adet", grams: 120 }, { item: "Süt (yarım yağlı)", amount: "200ml", grams: 200 }], calories: 210, protein: 8, carbs: 34, fat: 4 }, + { name: "Havuç & Humus", ingredients: [{ item: "Havuç çubukları", amount: "100g", grams: 100 }, { item: "Humus", amount: "50g", grams: 50 }], calories: 160, protein: 6, carbs: 18, fat: 8 }, + { name: "Yumurta (Haşlanmış)", ingredients: [{ item: "Yumurta", amount: "2 adet", grams: 120 }, { item: "Tuz & karabiber", amount: "az", grams: 1 }], calories: 155, protein: 13, carbs: 1, fat: 11 }, + { name: "Elma & Fıstık Ezmesi", ingredients: [{ item: "Elma", amount: "1 adet", grams: 180 }, { item: "Fıstık ezmesi", amount: "1 yemek kaşığı", grams: 16 }], calories: 190, protein: 5, carbs: 28, fat: 8 }, + { name: "Lor Peyniri & Domates", ingredients: [{ item: "Lor peyniri", amount: "80g", grams: 80 }, { item: "Domates", amount: "1/2 adet", grams: 60 }, { item: "Zeytinyağı", amount: "az", grams: 5 }], calories: 140, protein: 12, carbs: 6, fat: 8 }, + { name: "Türk Kahvesi & Hurma", ingredients: [{ item: "Türk kahvesi", amount: "1 fincan", grams: 60 }, { item: "Hurma", amount: "3 adet", grams: 30 }], calories: 100, protein: 1, carbs: 22, fat: 1 }, +]; + +// ==================== USA / DEFAULT MEALS ==================== +const USA_BREAKFASTS = [ + { + name: "Oatmeal with Berries", + ingredients: [ + { item: "Rolled oats", amount: "80g", grams: 80 }, + { item: "Milk (low-fat)", amount: "200ml", grams: 200 }, + { item: "Mixed berries", amount: "100g", grams: 100 }, + { item: "Honey", amount: "1 tsp", grams: 7 }, + { item: "Almonds (sliced)", amount: "15g", grams: 15 }, + ], + calories: 400, protein: 16, carbs: 58, fat: 12, + }, + { + name: "Scrambled Eggs & Toast", + ingredients: [ + { item: "Eggs", amount: "3", grams: 180 }, + { item: "Whole wheat toast", amount: "2 slices", grams: 60 }, + { item: "Butter", amount: "10g", grams: 10 }, + { item: "Avocado", amount: "1/4", grams: 50 }, + { item: "Cherry tomatoes", amount: "5", grams: 75 }, + ], + calories: 450, protein: 26, carbs: 30, fat: 26, + }, + { + name: "Greek Yogurt Parfait", + ingredients: [ + { item: "Greek yogurt", amount: "200g", grams: 200 }, + { item: "Granola", amount: "40g", grams: 40 }, + { item: "Banana", amount: "1/2", grams: 60 }, + { item: "Honey", amount: "1 tbsp", grams: 20 }, + { item: "Chia seeds", amount: "10g", grams: 10 }, + ], + calories: 420, protein: 24, carbs: 54, fat: 12, + }, + { + name: "Protein Smoothie Bowl", + ingredients: [ + { item: "Banana (frozen)", amount: "1", grams: 120 }, + { item: "Protein powder", amount: "1 scoop (30g)", grams: 30 }, + { item: "Almond milk", amount: "150ml", grams: 150 }, + { item: "Peanut butter", amount: "1 tbsp", grams: 16 }, + { item: "Granola topping", amount: "20g", grams: 20 }, + ], + calories: 430, protein: 30, carbs: 48, fat: 14, + }, + { + name: "Avocado Toast with Egg", + ingredients: [ + { item: "Whole wheat bread", amount: "2 slices", grams: 60 }, + { item: "Avocado", amount: "1/2", grams: 100 }, + { item: "Egg (poached)", amount: "2", grams: 120 }, + { item: "Red pepper flakes", amount: "pinch", grams: 1 }, + { item: "Lemon juice", amount: "1 tsp", grams: 5 }, + ], + calories: 440, protein: 20, carbs: 32, fat: 26, + }, +]; + +const USA_LUNCHES = [ + { + name: "Grilled Chicken Salad", + ingredients: [ + { item: "Chicken breast (grilled)", amount: "180g", grams: 180 }, + { item: "Mixed greens", amount: "100g", grams: 100 }, + { item: "Cherry tomatoes", amount: "80g", grams: 80 }, + { item: "Cucumber", amount: "80g", grams: 80 }, + { item: "Olive oil dressing", amount: "1 tbsp", grams: 14 }, + { item: "Feta cheese", amount: "30g", grams: 30 }, + ], + calories: 420, protein: 44, carbs: 12, fat: 22, + }, + { + name: "Turkey Wrap", + ingredients: [ + { item: "Whole wheat tortilla", amount: "1 large", grams: 60 }, + { item: "Turkey breast slices", amount: "120g", grams: 120 }, + { item: "Lettuce", amount: "30g", grams: 30 }, + { item: "Tomato", amount: "1/2", grams: 60 }, + { item: "Hummus", amount: "30g", grams: 30 }, + { item: "Swiss cheese", amount: "20g", grams: 20 }, + ], + calories: 400, protein: 36, carbs: 30, fat: 16, + }, + { + name: "Salmon & Sweet Potato", + ingredients: [ + { item: "Salmon fillet", amount: "180g", grams: 180 }, + { item: "Sweet potato (baked)", amount: "200g", grams: 200 }, + { item: "Broccoli (steamed)", amount: "100g", grams: 100 }, + { item: "Olive oil", amount: "1 tsp", grams: 5 }, + { item: "Lemon", amount: "1/2", grams: 30 }, + ], + calories: 500, protein: 40, carbs: 42, fat: 18, + }, + { + name: "Chicken & Brown Rice Bowl", + ingredients: [ + { item: "Chicken breast", amount: "180g", grams: 180 }, + { item: "Brown rice (cooked)", amount: "150g", grams: 150 }, + { item: "Mixed vegetables", amount: "100g", grams: 100 }, + { item: "Soy sauce", amount: "1 tbsp", grams: 15 }, + { item: "Sesame oil", amount: "1 tsp", grams: 5 }, + ], + calories: 480, protein: 42, carbs: 48, fat: 12, + }, + { + name: "Tuna Sandwich", + ingredients: [ + { item: "Whole wheat bread", amount: "2 slices", grams: 60 }, + { item: "Canned tuna", amount: "120g", grams: 120 }, + { item: "Greek yogurt (instead of mayo)", amount: "30g", grams: 30 }, + { item: "Celery", amount: "30g", grams: 30 }, + { item: "Lettuce", amount: "20g", grams: 20 }, + { item: "Apple", amount: "1 small", grams: 120 }, + ], + calories: 420, protein: 38, carbs: 42, fat: 10, + }, +]; + +const USA_DINNERS = [ + { + name: "Grilled Steak & Vegetables", + ingredients: [ + { item: "Lean steak", amount: "180g", grams: 180 }, + { item: "Asparagus", amount: "100g", grams: 100 }, + { item: "Baked potato", amount: "150g", grams: 150 }, + { item: "Olive oil", amount: "1 tbsp", grams: 14 }, + { item: "Garlic", amount: "2 cloves", grams: 6 }, + ], + calories: 480, protein: 42, carbs: 30, fat: 22, + }, + { + name: "Baked Chicken Thighs & Quinoa", + ingredients: [ + { item: "Chicken thighs (skinless)", amount: "200g", grams: 200 }, + { item: "Quinoa (cooked)", amount: "150g", grams: 150 }, + { item: "Roasted vegetables", amount: "150g", grams: 150 }, + { item: "Olive oil", amount: "1 tbsp", grams: 14 }, + { item: "Herbs", amount: "mixed", grams: 5 }, + ], + calories: 520, protein: 44, carbs: 38, fat: 20, + }, + { + name: "Shrimp Stir-Fry", + ingredients: [ + { item: "Shrimp", amount: "200g", grams: 200 }, + { item: "Brown rice (cooked)", amount: "150g", grams: 150 }, + { item: "Bell peppers", amount: "100g", grams: 100 }, + { item: "Broccoli", amount: "80g", grams: 80 }, + { item: "Soy sauce", amount: "1 tbsp", grams: 15 }, + { item: "Sesame oil", amount: "1 tsp", grams: 5 }, + ], + calories: 440, protein: 38, carbs: 46, fat: 10, + }, + { + name: "Turkey Meatballs & Pasta", + ingredients: [ + { item: "Ground turkey", amount: "150g", grams: 150 }, + { item: "Whole wheat pasta (cooked)", amount: "150g", grams: 150 }, + { item: "Marinara sauce", amount: "100g", grams: 100 }, + { item: "Parmesan", amount: "15g", grams: 15 }, + { item: "Side salad", amount: "80g", grams: 80 }, + ], + calories: 500, protein: 38, carbs: 50, fat: 16, + }, + { + name: "Baked Cod & Roasted Vegetables", + ingredients: [ + { item: "Cod fillet", amount: "200g", grams: 200 }, + { item: "Zucchini", amount: "100g", grams: 100 }, + { item: "Cherry tomatoes", amount: "80g", grams: 80 }, + { item: "Olive oil", amount: "1 tbsp", grams: 14 }, + { item: "Brown rice (cooked)", amount: "120g", grams: 120 }, + ], + calories: 420, protein: 40, carbs: 34, fat: 14, + }, +]; + +const USA_SNACKS = [ + { name: "Protein Bar", ingredients: [{ item: "Protein bar", amount: "1", grams: 60 }], calories: 220, protein: 20, carbs: 24, fat: 8 }, + { name: "Apple & Almond Butter", ingredients: [{ item: "Apple", amount: "1 medium", grams: 180 }, { item: "Almond butter", amount: "1 tbsp", grams: 16 }], calories: 190, protein: 4, carbs: 28, fat: 8 }, + { name: "Trail Mix", ingredients: [{ item: "Mixed nuts", amount: "20g", grams: 20 }, { item: "Dried cranberries", amount: "15g", grams: 15 }, { item: "Dark chocolate chips", amount: "10g", grams: 10 }], calories: 200, protein: 5, carbs: 20, fat: 12 }, + { name: "Cottage Cheese & Berries", ingredients: [{ item: "Cottage cheese", amount: "150g", grams: 150 }, { item: "Mixed berries", amount: "80g", grams: 80 }], calories: 170, protein: 18, carbs: 16, fat: 4 }, + { name: "Veggie Sticks & Hummus", ingredients: [{ item: "Carrot sticks", amount: "60g", grams: 60 }, { item: "Celery sticks", amount: "40g", grams: 40 }, { item: "Hummus", amount: "50g", grams: 50 }], calories: 150, protein: 6, carbs: 16, fat: 8 }, + { name: "Hard Boiled Eggs", ingredients: [{ item: "Eggs", amount: "2", grams: 120 }], calories: 155, protein: 13, carbs: 1, fat: 11 }, + { name: "Greek Yogurt & Honey", ingredients: [{ item: "Greek yogurt", amount: "150g", grams: 150 }, { item: "Honey", amount: "1 tsp", grams: 7 }], calories: 160, protein: 15, carbs: 16, fat: 4 }, + { name: "Banana & Peanut Butter", ingredients: [{ item: "Banana", amount: "1", grams: 120 }, { item: "Peanut butter", amount: "1 tbsp", grams: 16 }], calories: 210, protein: 6, carbs: 30, fat: 9 }, +]; + +function getMealsByCountry(country) { + const c = (country || "").toLowerCase().trim(); + if (c === "turkey" || c === "türkiye" || c === "turkiye") { + return { + breakfasts: TURKISH_BREAKFASTS, + lunches: TURKISH_LUNCHES, + dinners: TURKISH_DINNERS, + snacks: TURKISH_SNACKS, + }; + } + if (c === "usa" || c === "united states" || c === "us" || c === "america") { + return { + breakfasts: USA_BREAKFASTS, + lunches: USA_LUNCHES, + dinners: USA_DINNERS, + snacks: USA_SNACKS, + }; + } + // Default: Mediterranean mix (use USA meals as base) + return { + breakfasts: USA_BREAKFASTS, + lunches: USA_LUNCHES, + dinners: USA_DINNERS, + snacks: USA_SNACKS, + }; +} + +function scaleMeal(meal, targetCalories, mealCalorieShare) { + const target = targetCalories * mealCalorieShare; + const ratio = target / meal.calories; + return { + ...meal, + calories: Math.round(meal.calories * ratio), + protein: Math.round(meal.protein * ratio), + carbs: Math.round(meal.carbs * ratio), + fat: Math.round(meal.fat * ratio), + ingredients: meal.ingredients.map((ing) => ({ + ...ing, + grams: Math.round(ing.grams * ratio), + })), + }; +} + +function pickDifferent(arr, count, usedIndices) { + const available = arr.map((item, idx) => ({ item, idx })).filter((x) => !usedIndices.has(x.idx)); + const shuffled = available.sort(() => 0.5 - Math.random()); + const picked = shuffled.slice(0, count); + picked.forEach((p) => usedIndices.add(p.idx)); + if (picked.length < count) { + // Reset and pick more if needed + const extra = arr.map((item, idx) => ({ item, idx })).sort(() => 0.5 - Math.random()).slice(0, count - picked.length); + return [...picked.map((p) => p.item), ...extra.map((p) => p.item)]; + } + return picked.map((p) => p.item); +} + +function generateMealPlan(profile) { + const { weight, height, age, gender, activity_level, goal, country } = profile; + + const bmr = calculateBMR(weight, height, age, gender); + const tdee = calculateTDEE(bmr, activity_level); + const targetCalories = Math.round(adjustCalories(tdee, goal)); + const macros = calculateMacros(targetCalories, goal); + + const meals = getMealsByCountry(country); + + // Meal calorie distribution: breakfast 25%, snack1 10%, lunch 30%, snack2 10%, dinner 25% + const shares = { breakfast: 0.25, snack1: 0.10, lunch: 0.30, snack2: 0.10, dinner: 0.25 }; + + const dayNames = ["Pazartesi", "Salı", "Çarşamba", "Perşembe", "Cuma", "Cumartesi", "Pazar"]; + + const usedBreakfasts = new Set(); + const usedLunches = new Set(); + const usedDinners = new Set(); + const usedSnacks1 = new Set(); + const usedSnacks2 = new Set(); + + const days = dayNames.map((dayName) => { + const breakfast = scaleMeal(pickDifferent(meals.breakfasts, 1, usedBreakfasts)[0], targetCalories, shares.breakfast); + const snack1 = scaleMeal(pickDifferent(meals.snacks, 1, usedSnacks1)[0], targetCalories, shares.snack1); + const lunch = scaleMeal(pickDifferent(meals.lunches, 1, usedLunches)[0], targetCalories, shares.lunch); + const snack2 = scaleMeal(pickDifferent(meals.snacks, 1, usedSnacks2)[0], targetCalories, shares.snack2); + const dinner = scaleMeal(pickDifferent(meals.dinners, 1, usedDinners)[0], targetCalories, shares.dinner); + + const dayCalories = breakfast.calories + snack1.calories + lunch.calories + snack2.calories + dinner.calories; + + return { + day: dayName, + total_calories: dayCalories, + meals: [ + { type: "Kahvaltı", ...breakfast }, + { type: "Ara Öğün 1", ...snack1 }, + { type: "Öğle Yemeği", ...lunch }, + { type: "Ara Öğün 2", ...snack2 }, + { type: "Akşam Yemeği", ...dinner }, + ], + }; + }); + + return { + title: "Kişisel Beslenme Programı", + bmr: Math.round(bmr), + tdee: Math.round(tdee), + target_calories: targetCalories, + macros, + goal, + country: country || "Turkey", + notes: [ + `Günlük hedef kalori: ${targetCalories} kcal`, + `Protein: ${macros.protein_g}g | Karbonhidrat: ${macros.carbs_g}g | Yağ: ${macros.fat_g}g`, + "Günde en az 2.5-3 litre su için", + "Öğün saatlerinizi düzenli tutun", + "Yemekleri yavaş yiyin, iyi çiğneyin", + ], + days, + }; +} + +module.exports = { generateMealPlan }; diff --git a/src/services/shopping.js b/src/services/shopping.js new file mode 100644 index 0000000..59c6acc --- /dev/null +++ b/src/services/shopping.js @@ -0,0 +1,162 @@ +"use strict"; + +const CATEGORY_MAP = { + // Protein + "yumurta": "protein", "eggs": "protein", "egg": "protein", + "tavuk": "protein", "chicken": "protein", "turkey breast": "protein", + "dana": "protein", "beef": "protein", "steak": "protein", "lean steak": "protein", + "kıyma": "protein", "ground turkey": "protein", "ground beef": "protein", + "sucuk": "protein", "köfte": "protein", "kebap": "protein", "adana": "protein", + "balık": "protein", "levrek": "protein", "çipura": "protein", "salmon": "protein", + "cod": "protein", "tuna": "protein", "shrimp": "protein", + "et ": "protein", "kuşbaşı": "protein", "mantı": "protein", + "lahmacun": "protein", "pide hamuru": "protein", + "protein powder": "protein", "protein bar": "protein", + + // Dairy + "peynir": "dairy", "cheese": "dairy", "feta": "dairy", "parmesan": "dairy", "swiss": "dairy", + "süt": "dairy", "milk": "dairy", "almond milk": "dairy", + "yoğurt": "dairy", "yogurt": "dairy", "greek yogurt": "dairy", "cottage cheese": "dairy", + "kaymak": "dairy", "tereyağı": "dairy", "butter": "dairy", + "ayran": "dairy", "lor": "dairy", "kaşar": "dairy", + "cream": "dairy", + + // Vegetables + "domates": "vegetables", "tomato": "vegetables", "cherry tomato": "vegetables", + "salatalık": "vegetables", "cucumber": "vegetables", + "biber": "vegetables", "bell pepper": "vegetables", "pepper": "vegetables", + "soğan": "vegetables", "onion": "vegetables", + "sarımsak": "vegetables", "garlic": "vegetables", + "patlıcan": "vegetables", "havuç": "vegetables", "carrot": "vegetables", + "kabak": "vegetables", "zucchini": "vegetables", + "ıspanak": "vegetables", "ispanak": "vegetables", "spinach": "vegetables", + "marul": "vegetables", "lettuce": "vegetables", "mixed greens": "vegetables", + "roka": "vegetables", "asparagus": "vegetables", "broccoli": "vegetables", + "fasulye": "vegetables", "bamya": "vegetables", "taze fasulye": "vegetables", + "maydanoz": "vegetables", "dereotu": "vegetables", "nane": "vegetables", + "asma yaprağı": "vegetables", "celery": "vegetables", + "roasted vegetables": "vegetables", "mixed vegetables": "vegetables", + "sweet potato": "vegetables", "baked potato": "vegetables", "potato": "vegetables", + + // Fruits + "muz": "fruits", "banana": "fruits", + "elma": "fruits", "apple": "fruits", + "meyve": "fruits", "berries": "fruits", "mixed berries": "fruits", + "limon": "fruits", "lemon": "fruits", + "nar ekşisi": "fruits", "cranberries": "fruits", + "hurma": "fruits", "kuru kayısı": "fruits", + + // Grains + "bulgur": "grains", "pirinç": "grains", "pilav": "grains", + "rice": "grains", "brown rice": "grains", "quinoa": "grains", + "ekmek": "grains", "bread": "grains", "whole wheat": "grains", "toast": "grains", + "simit": "grains", "lavaş": "grains", "tortilla": "grains", + "un": "grains", "yufka": "grains", + "mercimek": "grains", "kuru fasulye": "grains", "nohut": "grains", + "oats": "grains", "granola": "grains", "pasta": "grains", + "chia": "grains", "kraker": "grains", + + // Spices & Condiments + "pul biber": "spices", "tuz": "spices", "karabiber": "spices", + "sumak": "spices", "herbs": "spices", "red pepper": "spices", + "salça": "spices", "domates salçası": "spices", + "soy sauce": "spices", "sesame oil": "spices", "marinara": "spices", + "honey": "spices", "bal": "spices", + + // Fats & Nuts + "zeytinyağı": "fats_nuts", "olive oil": "fats_nuts", + "zeytin": "fats_nuts", + "ceviz": "fats_nuts", "fındık": "fats_nuts", "badem": "fats_nuts", + "almonds": "fats_nuts", "peanut butter": "fats_nuts", "almond butter": "fats_nuts", + "fıstık ezmesi": "fats_nuts", "avocado": "fats_nuts", + "mixed nuts": "fats_nuts", "dark chocolate": "fats_nuts", + "humus": "fats_nuts", + + // Beverages + "çay": "beverages", "kahve": "beverages", "türk kahvesi": "beverages", + "şalgam": "beverages", + "su": "beverages", "water": "beverages", + + // Other + "turşu": "other", +}; + +function categorizeIngredient(itemName) { + const lower = itemName.toLowerCase(); + for (const [keyword, category] of Object.entries(CATEGORY_MAP)) { + if (lower.includes(keyword)) { + return category; + } + } + return "other"; +} + +function generateShoppingList(mealPlan) { + const ingredientMap = {}; + + for (const day of mealPlan.days) { + for (const meal of day.meals) { + for (const ing of meal.ingredients) { + const key = ing.item.toLowerCase().trim(); + if (!ingredientMap[key]) { + ingredientMap[key] = { + item: ing.item, + total_grams: 0, + category: categorizeIngredient(ing.item), + }; + } + ingredientMap[key].total_grams += ing.grams; + } + } + } + + const categories = { + protein: { name: "Protein Kaynakları", icon: "🥩", items: [] }, + dairy: { name: "Süt Ürünleri", icon: "🧀", items: [] }, + vegetables: { name: "Sebzeler", icon: "🥬", items: [] }, + fruits: { name: "Meyveler", icon: "🍎", items: [] }, + grains: { name: "Tahıllar & Baklagiller", icon: "🌾", items: [] }, + fats_nuts: { name: "Yağlar & Kuruyemişler", icon: "🥜", items: [] }, + spices: { name: "Baharat & Sos", icon: "🧂", items: [] }, + beverages: { name: "İçecekler", icon: "🥤", items: [] }, + other: { name: "Diğer", icon: "📦", items: [] }, + }; + + for (const data of Object.values(ingredientMap)) { + const cat = categories[data.category] || categories.other; + cat.items.push({ + item: data.item, + total_grams: Math.round(data.total_grams), + display_amount: formatAmount(data.total_grams), + }); + } + + // Sort items within each category alphabetically + for (const cat of Object.values(categories)) { + cat.items.sort((a, b) => a.item.localeCompare(b.item, "tr")); + } + + // Remove empty categories + const result = {}; + for (const [key, cat] of Object.entries(categories)) { + if (cat.items.length > 0) { + result[key] = cat; + } + } + + return { + title: "Haftalık Alışveriş Listesi", + note: "Bu liste 7 günlük beslenme programınıza göre hazırlanmıştır.", + categories: result, + total_items: Object.values(ingredientMap).length, + }; +} + +function formatAmount(grams) { + if (grams >= 1000) { + return `${(grams / 1000).toFixed(1)} kg`; + } + return `${grams}g`; +} + +module.exports = { generateShoppingList }; diff --git a/src/services/trainer.js b/src/services/trainer.js new file mode 100644 index 0000000..a2b7c0d --- /dev/null +++ b/src/services/trainer.js @@ -0,0 +1,371 @@ +"use strict"; + +const EXERCISES = { + chest: [ + { name: "Bench Press (Göğüs Presi)", sets: 4, reps: "8-10", rest_seconds: 90, notes: "Kontrollü iniş, göğüs kaslarını sıkın" }, + { name: "Incline Dumbbell Press (Üst Göğüs)", sets: 4, reps: "10-12", rest_seconds: 75, notes: "30 derece açı, tam hareket açıklığı" }, + { name: "Cable Flyes (Kablo Çapraz)", sets: 3, reps: "12-15", rest_seconds: 60, notes: "Kollar hafif bükük, göğüs merkezinde sıkın" }, + { name: "Dips (Paralel Bar)", sets: 3, reps: "10-12", rest_seconds: 75, notes: "Öne eğilin, göğüs hedefleyin" }, + { name: "Push-ups (Şınav)", sets: 3, reps: "15-20", rest_seconds: 45, notes: "Geniş tutuş, göğüs yere değsin" }, + { name: "Dumbbell Pullover", sets: 3, reps: "12", rest_seconds: 60, notes: "Kollar hafif bükük, göğüs gerilmesini hissedin" }, + ], + back: [ + { name: "Deadlift (Toplu Kaldırma)", sets: 4, reps: "6-8", rest_seconds: 120, notes: "Sırt düz, kalçadan kaldırın" }, + { name: "Barbell Row (Sırt Çekişi)", sets: 4, reps: "8-10", rest_seconds: 90, notes: "Gövde yere paralel, dirsekler geri" }, + { name: "Lat Pulldown (Lat Çekme)", sets: 4, reps: "10-12", rest_seconds: 75, notes: "Geniş tutuş, göğüse çekin" }, + { name: "Seated Cable Row (Oturarak Çekiş)", sets: 3, reps: "10-12", rest_seconds: 75, notes: "Sırt düz, kürek kemiklerini sıkın" }, + { name: "T-Bar Row", sets: 3, reps: "10-12", rest_seconds: 75, notes: "Göğüse doğru çekin, sırt kaslarını sıkın" }, + { name: "Face Pull (Yüz Çekişi)", sets: 3, reps: "15", rest_seconds: 45, notes: "Omuz arkası ve üst sırt hedefli" }, + ], + legs: [ + { name: "Squat (Çömelme)", sets: 4, reps: "8-10", rest_seconds: 120, notes: "Diz açısı 90 derece, sırt düz" }, + { name: "Romanian Deadlift (Romen Kaldırma)", sets: 4, reps: "10-12", rest_seconds: 90, notes: "Bacak arkası gerilmeli, kalça menteşesi" }, + { name: "Leg Press (Bacak Presi)", sets: 4, reps: "10-12", rest_seconds: 90, notes: "Ayaklar omuz genişliğinde" }, + { name: "Walking Lunges (Yürüyüş Hamle)", sets: 3, reps: "12 (her bacak)", rest_seconds: 75, notes: "Diz parmak ucunu geçmesin" }, + { name: "Leg Curl (Bacak Büküm)", sets: 3, reps: "12-15", rest_seconds: 60, notes: "Kontrollü hareket, hamstring sıkın" }, + { name: "Leg Extension (Bacak Açma)", sets: 3, reps: "12-15", rest_seconds: 60, notes: "Tepe noktada sıkın" }, + { name: "Calf Raise (Baldır Kaldırma)", sets: 4, reps: "15-20", rest_seconds: 45, notes: "Tam gerilme ve tam kasılma" }, + ], + shoulders: [ + { name: "Overhead Press (Omuz Presi)", sets: 4, reps: "8-10", rest_seconds: 90, notes: "Ayakta, core sıkı, başın üstüne itin" }, + { name: "Lateral Raise (Yan Kaldırma)", sets: 4, reps: "12-15", rest_seconds: 60, notes: "Hafif ağırlık, kontrollü hareket" }, + { name: "Front Raise (Ön Kaldırma)", sets: 3, reps: "12", rest_seconds: 60, notes: "Dönüşümlü kollarla" }, + { name: "Reverse Fly (Ters Sinek)", sets: 3, reps: "15", rest_seconds: 60, notes: "Arka omuz hedefli, kürek kemiklerini sıkın" }, + { name: "Arnold Press", sets: 3, reps: "10-12", rest_seconds: 75, notes: "Rotasyonlu hareket, omuzun 3 başını çalıştırır" }, + { name: "Shrugs (Omuz Silkme)", sets: 3, reps: "12-15", rest_seconds: 60, notes: "Trapez hedefli, yukarı çekin" }, + ], + arms: [ + { name: "Barbell Curl (Halter Biceps)", sets: 4, reps: "10-12", rest_seconds: 60, notes: "Dirsekler sabit, biceps sıkın" }, + { name: "Triceps Pushdown (Triceps İtme)", sets: 4, reps: "10-12", rest_seconds: 60, notes: "Dirsekler vücuda yapışık" }, + { name: "Hammer Curl (Çekiç Curl)", sets: 3, reps: "12", rest_seconds: 60, notes: "Nötr tutuş, ön kol da çalışır" }, + { name: "Overhead Triceps Extension", sets: 3, reps: "12", rest_seconds: 60, notes: "Dirsekler sabit, tam gerilme" }, + { name: "Concentration Curl (Konsantrasyon)", sets: 3, reps: "12 (her kol)", rest_seconds: 45, notes: "Yavaş ve kontrollü" }, + { name: "Skull Crushers", sets: 3, reps: "10-12", rest_seconds: 60, notes: "Alına doğru indirin, triceps sıkın" }, + { name: "Wrist Curl (Bilek Büküm)", sets: 2, reps: "15-20", rest_seconds: 30, notes: "Ön kol güçlendirme" }, + ], + cardio: [ + { name: "Koşu Bandı (Tempolu Koşu)", sets: 1, reps: "25 dakika", rest_seconds: 0, notes: "Kalp atış hızı %65-75 arası" }, + { name: "Bisiklet", sets: 1, reps: "20 dakika", rest_seconds: 0, notes: "Orta direnç, stabil tempo" }, + { name: "Kürek Çekme Makinesi", sets: 1, reps: "15 dakika", rest_seconds: 0, notes: "Tüm vücut kardiyosu" }, + { name: "Merdiven Çıkma (StairMaster)", sets: 1, reps: "15 dakika", rest_seconds: 0, notes: "Bacak ve kalça hedefli" }, + { name: "Eliptik Bisiklet", sets: 1, reps: "20 dakika", rest_seconds: 0, notes: "Düşük etki, tüm vücut" }, + ], + hiit: [ + { name: "Burpee", sets: 4, reps: "30 saniye", rest_seconds: 30, notes: "Maksimum efor" }, + { name: "Mountain Climber (Dağcı)", sets: 4, reps: "30 saniye", rest_seconds: 30, notes: "Core sıkı, hızlı hareket" }, + { name: "Jump Squat (Sıçramalı Çömelme)", sets: 4, reps: "30 saniye", rest_seconds: 30, notes: "Yumuşak iniş, diz koruması" }, + { name: "High Knees (Yüksek Diz)", sets: 4, reps: "30 saniye", rest_seconds: 30, notes: "Dizleri göğüse çekin" }, + { name: "Box Jump (Kutu Atlama)", sets: 4, reps: "30 saniye", rest_seconds: 30, notes: "İki ayakla atlayıp yumuşak inin" }, + { name: "Battle Ropes (Savaş İpleri)", sets: 4, reps: "30 saniye", rest_seconds: 30, notes: "Kollar ve core hedefli" }, + { name: "Kettlebell Swing", sets: 4, reps: "30 saniye", rest_seconds: 30, notes: "Kalça menteşesi, explosive hareket" }, + ], + core: [ + { name: "Plank (Düz Plank)", sets: 3, reps: "45 saniye", rest_seconds: 30, notes: "Vücut düz çizgi, core sıkı" }, + { name: "Russian Twist (Rus Bükülmesi)", sets: 3, reps: "20 (her taraf)", rest_seconds: 30, notes: "Ayaklar yerden, kontrollü rotasyon" }, + { name: "Bicycle Crunch (Bisiklet Karın)", sets: 3, reps: "20 (her taraf)", rest_seconds: 30, notes: "Karşı diz-dirsek buluşsun" }, + { name: "Leg Raise (Bacak Kaldırma)", sets: 3, reps: "15", rest_seconds: 30, notes: "Alt karın hedefli, sırt yere yapışık" }, + { name: "Dead Bug", sets: 3, reps: "12 (her taraf)", rest_seconds: 30, notes: "Sırt yere yapışık, kontrollü hareket" }, + { name: "Side Plank (Yan Plank)", sets: 2, reps: "30 saniye (her taraf)", rest_seconds: 30, notes: "Kalça düşmesin" }, + ], + flexibility: [ + { name: "Kedi-İnek Germe", sets: 1, reps: "10 tekrar", rest_seconds: 0, notes: "Nefesle senkronize" }, + { name: "Hamstring Germe", sets: 1, reps: "30 saniye (her bacak)", rest_seconds: 0, notes: "Bacak arkasını hissedin" }, + { name: "Quadriceps Germe", sets: 1, reps: "30 saniye (her bacak)", rest_seconds: 0, notes: "Dengeyi koruyun" }, + { name: "Güvercin Pozu", sets: 1, reps: "30 saniye (her taraf)", rest_seconds: 0, notes: "Kalça açıcı" }, + { name: "Çocuk Pozu", sets: 1, reps: "45 saniye", rest_seconds: 0, notes: "Sırt ve kalça rahatlama" }, + { name: "Kelebek Germe", sets: 1, reps: "30 saniye", rest_seconds: 0, notes: "İç bacak ve kasık" }, + { name: "Omuz Çapraz Germe", sets: 1, reps: "30 saniye (her kol)", rest_seconds: 0, notes: "Omuz esnekliği" }, + { name: "Aşağı Bakan Köpek", sets: 1, reps: "45 saniye", rest_seconds: 0, notes: "Tüm arka zincir" }, + { name: "Dünya Germe (World's Greatest Stretch)", sets: 1, reps: "5 tekrar (her taraf)", rest_seconds: 0, notes: "Tüm vücut mobilitesi" }, + { name: "Foam Rolling - Sırt", sets: 1, reps: "60 saniye", rest_seconds: 0, notes: "Yavaş ve kontrollü" }, + { name: "Foam Rolling - Bacaklar", sets: 1, reps: "60 saniye (her bacak)", rest_seconds: 0, notes: "Ağrılı noktada durun" }, + ], + compound: [ + { name: "Squat (Çömelme)", sets: 5, reps: "5", rest_seconds: 180, notes: "Ağır, compound hareket, güç odaklı" }, + { name: "Bench Press (Göğüs Presi)", sets: 5, reps: "5", rest_seconds: 180, notes: "Ağır yükleme, temel hareket" }, + { name: "Deadlift (Toplu Kaldırma)", sets: 5, reps: "5", rest_seconds: 180, notes: "En güçlü compound hareket" }, + { name: "Overhead Press (Omuz Presi)", sets: 5, reps: "5", rest_seconds: 150, notes: "Ayakta, tüm üst vücut" }, + { name: "Barbell Row (Sırt Çekişi)", sets: 5, reps: "5", rest_seconds: 150, notes: "Ağır, sırt kalınlığı" }, + ], + circuit: [ + { name: "Kettlebell Swing", sets: 3, reps: "15", rest_seconds: 15, notes: "Hızlı geçiş" }, + { name: "Push-ups (Şınav)", sets: 3, reps: "15", rest_seconds: 15, notes: "Hızlı geçiş" }, + { name: "Goblet Squat", sets: 3, reps: "15", rest_seconds: 15, notes: "Hızlı geçiş" }, + { name: "Dumbbell Row", sets: 3, reps: "12 (her kol)", rest_seconds: 15, notes: "Hızlı geçiş" }, + { name: "Jumping Lunges", sets: 3, reps: "12 (her bacak)", rest_seconds: 15, notes: "Hızlı geçiş" }, + { name: "Plank", sets: 3, reps: "30 saniye", rest_seconds: 60, notes: "Devrenin sonu, 60sn dinlenme sonra tekrar" }, + ], +}; + +function pickRandom(arr, count) { + const shuffled = [...arr].sort(() => 0.5 - Math.random()); + return shuffled.slice(0, count); +} + +function adjustForAge(exercises, age) { + if (age > 55) { + return exercises.map((e) => ({ + ...e, + sets: Math.max(2, e.sets - 1), + notes: e.notes + " | Yaşa uygun: Ağırlığı azaltın, kontrollü hareket", + })); + } + if (age < 20) { + return exercises.map((e) => ({ + ...e, + notes: e.notes + " | Genç sporcu: Form ve tekniğe odaklanın", + })); + } + return exercises; +} + +function generateLoseWeightProgram(profile) { + const { age, gender } = profile; + const days = [ + { + day: "Pazartesi", + type: "cardio", + title: "HIIT Kardiyo", + exercises: adjustForAge([...pickRandom(EXERCISES.hiit, 5), ...pickRandom(EXERCISES.core, 3)], age), + }, + { + day: "Salı", + type: "strength", + title: "Üst Vücut Devre", + exercises: adjustForAge([...pickRandom(EXERCISES.chest, 2), ...pickRandom(EXERCISES.back, 2), ...pickRandom(EXERCISES.shoulders, 2), ...pickRandom(EXERCISES.core, 2)], age), + }, + { + day: "Çarşamba", + type: "cardio", + title: "Kardiyo & Core", + exercises: adjustForAge([...pickRandom(EXERCISES.cardio, 3), ...pickRandom(EXERCISES.core, 4)], age), + }, + { + day: "Perşembe", + type: "strength", + title: "Alt Vücut Devre", + exercises: adjustForAge([...pickRandom(EXERCISES.legs, 4), ...pickRandom(EXERCISES.core, 2)], age), + }, + { + day: "Cuma", + type: "cardio", + title: "HIIT & Circuit", + exercises: adjustForAge(EXERCISES.circuit, age), + }, + { + day: "Cumartesi", + type: "cardio", + title: "Uzun Kardiyo", + exercises: adjustForAge([ + { name: "Açık Hava Koşusu veya Yürüyüş", sets: 1, reps: "45-60 dakika", rest_seconds: 0, notes: "Orta tempo, yağ yakım bölgesi" }, + ...pickRandom(EXERCISES.flexibility, 4), + ], age), + }, + { + day: "Pazar", + type: "rest", + title: "Dinlenme & İyileşme", + exercises: adjustForAge(EXERCISES.flexibility, age), + }, + ]; + return days; +} + +function generateBuildMuscleProgram(profile) { + const { age, gender } = profile; + const days = [ + { + day: "Pazartesi", + type: "strength", + title: "Göğüs & Triceps", + exercises: adjustForAge([...EXERCISES.chest.slice(0, 4), ...EXERCISES.arms.filter((e) => e.name.includes("Triceps") || e.name.includes("Skull")).slice(0, 3)], age), + }, + { + day: "Salı", + type: "strength", + title: "Sırt & Biceps", + exercises: adjustForAge([...EXERCISES.back.slice(0, 4), ...EXERCISES.arms.filter((e) => e.name.includes("Curl") || e.name.includes("Hammer")).slice(0, 3)], age), + }, + { + day: "Çarşamba", + type: "strength", + title: "Bacak Günü", + exercises: adjustForAge(EXERCISES.legs, age), + }, + { + day: "Perşembe", + type: "rest", + title: "Aktif Dinlenme", + exercises: adjustForAge([ + { name: "Hafif Yürüyüş", sets: 1, reps: "20 dakika", rest_seconds: 0, notes: "Kan dolaşımı için" }, + ...pickRandom(EXERCISES.flexibility, 5), + ], age), + }, + { + day: "Cuma", + type: "strength", + title: "Omuz & Trapez", + exercises: adjustForAge([...EXERCISES.shoulders, ...pickRandom(EXERCISES.core, 3)], age), + }, + { + day: "Cumartesi", + type: "strength", + title: "Kol Günü (Biceps & Triceps)", + exercises: adjustForAge([...EXERCISES.arms, ...pickRandom(EXERCISES.core, 2)], age), + }, + { + day: "Pazar", + type: "rest", + title: "Tam Dinlenme", + exercises: adjustForAge(EXERCISES.flexibility, age), + }, + ]; + return days; +} + +function generateGainWeightProgram(profile) { + const { age, gender } = profile; + const days = [ + { + day: "Pazartesi", + type: "strength", + title: "Compound Güç - Üst Vücut", + exercises: adjustForAge([...EXERCISES.compound.filter((e) => e.name.includes("Bench") || e.name.includes("Overhead") || e.name.includes("Row")), ...pickRandom(EXERCISES.chest, 2), ...pickRandom(EXERCISES.shoulders, 1)], age), + }, + { + day: "Salı", + type: "strength", + title: "Compound Güç - Alt Vücut", + exercises: adjustForAge([...EXERCISES.compound.filter((e) => e.name.includes("Squat") || e.name.includes("Deadlift")), ...pickRandom(EXERCISES.legs, 3)], age), + }, + { + day: "Çarşamba", + type: "rest", + title: "Dinlenme & Toparlanma", + exercises: adjustForAge(pickRandom(EXERCISES.flexibility, 6), age), + }, + { + day: "Perşembe", + type: "strength", + title: "Göğüs, Sırt & Omuz", + exercises: adjustForAge([...pickRandom(EXERCISES.chest, 3), ...pickRandom(EXERCISES.back, 3), ...pickRandom(EXERCISES.shoulders, 2)], age), + }, + { + day: "Cuma", + type: "strength", + title: "Bacak & Core", + exercises: adjustForAge([...pickRandom(EXERCISES.legs, 5), ...pickRandom(EXERCISES.core, 3)], age), + }, + { + day: "Cumartesi", + type: "strength", + title: "Kollar & Yardımcı Kaslar", + exercises: adjustForAge([...EXERCISES.arms, ...pickRandom(EXERCISES.core, 2)], age), + }, + { + day: "Pazar", + type: "rest", + title: "Tam Dinlenme", + exercises: adjustForAge(EXERCISES.flexibility, age), + }, + ]; + return days; +} + +function generateMaintainProgram(profile) { + const { age, gender } = profile; + const days = [ + { + day: "Pazartesi", + type: "strength", + title: "Üst Vücut Kuvvet", + exercises: adjustForAge([...pickRandom(EXERCISES.chest, 2), ...pickRandom(EXERCISES.back, 2), ...pickRandom(EXERCISES.shoulders, 2)], age), + }, + { + day: "Salı", + type: "cardio", + title: "Kardiyo & Core", + exercises: adjustForAge([...pickRandom(EXERCISES.cardio, 2), ...pickRandom(EXERCISES.core, 4)], age), + }, + { + day: "Çarşamba", + type: "strength", + title: "Alt Vücut Kuvvet", + exercises: adjustForAge(pickRandom(EXERCISES.legs, 5), age), + }, + { + day: "Perşembe", + type: "flexibility", + title: "Esneklik & Mobilite", + exercises: adjustForAge(EXERCISES.flexibility, age), + }, + { + day: "Cuma", + type: "strength", + title: "Tüm Vücut", + exercises: adjustForAge([...pickRandom(EXERCISES.compound, 3), ...pickRandom(EXERCISES.arms, 2), ...pickRandom(EXERCISES.core, 2)], age), + }, + { + day: "Cumartesi", + type: "cardio", + title: "Açık Hava Aktivitesi", + exercises: adjustForAge([ + { name: "Yürüyüş veya Koşu", sets: 1, reps: "30-45 dakika", rest_seconds: 0, notes: "Doğada, orta tempo" }, + { name: "Bisiklet veya Yüzme", sets: 1, reps: "30 dakika", rest_seconds: 0, notes: "Keyifli tempo" }, + ...pickRandom(EXERCISES.flexibility, 3), + ], age), + }, + { + day: "Pazar", + type: "rest", + title: "Tam Dinlenme", + exercises: adjustForAge(pickRandom(EXERCISES.flexibility, 5), age), + }, + ]; + return days; +} + +function generateWorkoutProgram(profile) { + const goalMap = { + lose_weight: generateLoseWeightProgram, + build_muscle: generateBuildMuscleProgram, + gain_weight: generateGainWeightProgram, + maintain: generateMaintainProgram, + }; + + const generator = goalMap[profile.goal] || goalMap.maintain; + const program = generator(profile); + + const goalNames = { + lose_weight: "Yağ Yakma & Zayıflama", + build_muscle: "Kas Geliştirme", + gain_weight: "Kilo Alma & Güçlenme", + maintain: "Formu Koruma", + }; + + const activityNotes = { + sedentary: "Başlangıç seviyesi - yavaş başlayın, formu öğrenin", + light: "Hafif seviye - ağırlıkları kademeli artırın", + moderate: "Orta seviye - standart program", + active: "İleri seviye - ağırlıkları artırabilirsiniz", + very_active: "Elit seviye - süper setler ve drop setler ekleyebilirsiniz", + }; + + return { + title: `${goalNames[profile.goal] || "Fitness"} Programı`, + goal: profile.goal, + level: profile.activity_level, + note: activityNotes[profile.activity_level] || "", + general_tips: [ + "Her antrenmandan önce 5-10 dakika ısınma yapın", + "Her antrenmandan sonra 5-10 dakika soğuma ve germe yapın", + "Günde en az 2-3 litre su için", + "Uyku düzeninize dikkat edin (7-9 saat)", + "Ağrı hissederseniz hareketi durdurun", + "Haftada en az 1 gün tam dinlenme yapın", + ], + days: program, + }; +} + +module.exports = { generateWorkoutProgram };