diff --git a/index.html b/index.html index 77635af..d0f94ae 100644 --- a/index.html +++ b/index.html @@ -1,86 +1,41 @@ - - - - GymFlow AI - - - - - - - + + + + + GymFlow AI + + + + + - + - -
+ + +
+ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9a26e7e..1c13cda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,11 +22,27 @@ "@playwright/test": "^1.57.0", "@types/node": "^22.14.0", "@vitejs/plugin-react": "^5.0.0", + "autoprefixer": "^10.4.22", "concurrently": "^8.2.2", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", "typescript": "~5.8.2", "vite": "^6.2.0" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@arr/every": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@arr/every/-/every-1.0.1.tgz", @@ -873,6 +889,44 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1491,6 +1545,47 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", @@ -1550,6 +1645,44 @@ "node": ">= 0.4" } }, + "node_modules/autoprefixer": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -1610,6 +1743,19 @@ "node": "*" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -1619,6 +1765,19 @@ "balanced-match": "^1.0.0" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.28.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", @@ -1781,6 +1940,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001756", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz", @@ -2004,6 +2173,16 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2098,6 +2277,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", @@ -2353,6 +2545,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2676,6 +2882,46 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2731,6 +2977,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2790,6 +3049,20 @@ "node": ">=12.20.0" } }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3002,6 +3275,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/globalthis": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", @@ -3329,6 +3615,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-boolean-object": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", @@ -3405,6 +3704,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-finalizationregistry": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", @@ -3448,6 +3757,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-interactive": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", @@ -3500,6 +3822,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-number-object": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", @@ -3763,6 +4095,16 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3853,6 +4195,13 @@ "url": "https://github.com/sponsors/antonk52" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, "node_modules/load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -4025,6 +4374,43 @@ "node": ">=8" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -4085,6 +4471,18 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -4176,6 +4574,26 @@ "semver": "bin/semver" } }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/npm-run-all": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", @@ -4380,6 +4798,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -4704,6 +5142,16 @@ "node": ">=4" } }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/playwright": { "version": "1.57.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", @@ -5331,6 +5779,133 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/premove": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/premove/-/premove-4.0.0.tgz", @@ -5367,6 +5942,27 @@ "node": ">= 0.6.0" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "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/react": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", @@ -5466,6 +6062,26 @@ "react-dom": ">=18" } }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-cache/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -5645,6 +6261,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rimraf": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", @@ -5702,6 +6329,30 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "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": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -6303,6 +6954,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -6331,6 +7005,108 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwindcss": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz", + "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.6", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tailwindcss/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/temp-dir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", @@ -6384,6 +7160,29 @@ "node": ">=18" } }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -6406,6 +7205,19 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -6437,6 +7249,13 @@ "node": ">=6" } }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -7013,6 +7832,22 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index cadf227..64c3e63 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,11 @@ "@playwright/test": "^1.57.0", "@types/node": "^22.14.0", "@vitejs/plugin-react": "^5.0.0", + "autoprefixer": "^10.4.22", "concurrently": "^8.2.2", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", "typescript": "~5.8.2", "vite": "^6.2.0" } -} \ No newline at end of file +} diff --git a/playwright-report/index.html b/playwright-report/index.html index 3733c2b..db3428b 100644 --- a/playwright-report/index.html +++ b/playwright-report/index.html @@ -82,4 +82,4 @@ Error generating stack: `+n.message+`
- \ No newline at end of file + \ No newline at end of file diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..d41ad63 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/server/package-lock.json b/server/package-lock.json index e373e05..d79940b 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -18,7 +18,8 @@ "dotenv": "17.2.3", "express": "5.1.0", "jsonwebtoken": "9.0.2", - "ts-node-dev": "^2.0.0" + "ts-node-dev": "^2.0.0", + "zod": "^4.1.13" }, "devDependencies": { "@types/bcryptjs": "*", @@ -3454,6 +3455,15 @@ "dependencies": { "grammex": "^3.1.10" } + }, + "node_modules/zod": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", + "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/server/package.json b/server/package.json index 21aac07..c600a41 100644 --- a/server/package.json +++ b/server/package.json @@ -22,7 +22,8 @@ "dotenv": "17.2.3", "express": "5.1.0", "jsonwebtoken": "9.0.2", - "ts-node-dev": "^2.0.0" + "ts-node-dev": "^2.0.0", + "zod": "^4.1.13" }, "devDependencies": { "@types/bcryptjs": "*", @@ -37,4 +38,4 @@ "ts-node": "*", "typescript": "*" } -} \ No newline at end of file +} diff --git a/server/prisma/dev.db b/server/prisma/dev.db index ff48569..b2bebb9 100644 Binary files a/server/prisma/dev.db and b/server/prisma/dev.db differ diff --git a/server/src/middleware/validate.ts b/server/src/middleware/validate.ts new file mode 100644 index 0000000..26609be --- /dev/null +++ b/server/src/middleware/validate.ts @@ -0,0 +1,15 @@ +import { Request, Response, NextFunction } from 'express'; +import { ZodSchema } from 'zod'; + +export const validate = (schema: ZodSchema) => async (req: Request, res: Response, next: NextFunction) => { + try { + await schema.parseAsync({ + body: req.body, + query: req.query, + params: req.params, + }); + return next(); + } catch (error) { + return res.status(400).json(error); + } +}; diff --git a/server/src/routes/ai.ts b/server/src/routes/ai.ts index cceb312..dda76f4 100644 --- a/server/src/routes/ai.ts +++ b/server/src/routes/ai.ts @@ -13,8 +13,8 @@ interface AuthRequest extends Request { const router = express.Router(); const JWT_SECRET = process.env.JWT_SECRET || 'secret'; -const API_KEY = process.env.API_KEY; -const MODEL_ID = 'gemini-2.0-flash'; +const API_KEY = process.env.GEMINI_API_KEY || process.env.API_KEY; +const MODEL_ID = 'gemini-flash-lite-latest'; // Store chat sessions in memory (in production, use Redis or similar) const chatSessions = new Map(); diff --git a/server/src/routes/auth.ts b/server/src/routes/auth.ts index 7d7e770..bcce45d 100644 --- a/server/src/routes/auth.ts +++ b/server/src/routes/auth.ts @@ -2,6 +2,8 @@ import express from 'express'; import jwt from 'jsonwebtoken'; import bcrypt from 'bcryptjs'; import prisma from '../lib/prisma'; +import { validate } from '../middleware/validate'; +import { loginSchema, registerSchema, changePasswordSchema, updateProfileSchema } from '../schemas/auth'; const router = express.Router(); const JWT_SECRET = process.env.JWT_SECRET || 'secret'; @@ -30,7 +32,7 @@ router.get('/me', async (req, res) => { }); // Login -router.post('/login', async (req, res) => { +router.post('/login', validate(loginSchema), async (req, res) => { try { const { email, password } = req.body; @@ -63,7 +65,7 @@ router.post('/login', async (req, res) => { }); // Register -router.post('/register', async (req, res) => { +router.post('/register', validate(registerSchema), async (req, res) => { try { const { email, password } = req.body; @@ -73,10 +75,6 @@ router.post('/register', async (req, res) => { return res.status(400).json({ error: 'User already exists' }); } - if (!password || password.length < 4) { - return res.status(400).json({ error: 'Password too short' }); - } - const hashedPassword = await bcrypt.hash(password, 10); const user = await prisma.user.create({ @@ -104,7 +102,7 @@ router.post('/register', async (req, res) => { }); // Change Password -router.post('/change-password', async (req, res) => { +router.post('/change-password', validate(changePasswordSchema), async (req, res) => { try { const token = req.headers.authorization?.split(' ')[1]; @@ -118,10 +116,6 @@ router.post('/change-password', async (req, res) => { return res.status(403).json({ error: 'Forbidden' }); } - if (!newPassword || newPassword.length < 4) { - return res.status(400).json({ error: 'Password too short' }); - } - const hashed = await bcrypt.hash(newPassword, 10); await prisma.user.update({ @@ -140,7 +134,7 @@ router.post('/change-password', async (req, res) => { }); // Update Profile -router.patch('/profile', async (req, res) => { +router.patch('/profile', validate(updateProfileSchema), async (req, res) => { try { const token = req.headers.authorization?.split(' ')[1]; if (!token) return res.status(401).json({ error: 'Unauthorized' }); diff --git a/server/src/routes/sessions.ts b/server/src/routes/sessions.ts index ac41a07..4bf53ca 100644 --- a/server/src/routes/sessions.ts +++ b/server/src/routes/sessions.ts @@ -1,6 +1,8 @@ import express from 'express'; import jwt from 'jsonwebtoken'; import prisma from '../lib/prisma'; +import { validate } from '../middleware/validate'; +import { sessionSchema, logSetSchema, updateSetSchema } from '../schemas/sessions'; const router = express.Router(); const JWT_SECRET = process.env.JWT_SECRET || 'secret'; @@ -53,7 +55,7 @@ router.get('/', async (req: any, res) => { }); // Save session (create or update) -router.post('/', async (req: any, res) => { +router.post('/', validate(sessionSchema), async (req: any, res) => { try { const userId = req.user.userId; const { id, startTime, endTime, userBodyWeight, note, planId, planName, sets } = req.body; @@ -148,15 +150,6 @@ router.post('/', async (req: any, res) => { return res.json(created); } - // Update user profile weight if session has weight and is finished (for update case too) - if (weight && end) { - await prisma.userProfile.upsert({ - where: { userId }, - create: { userId, weight }, - update: { weight } - }); - } - } catch (error) { console.error(error); res.status(500).json({ error: 'Server error' }); @@ -188,7 +181,7 @@ router.get('/active', async (req: any, res) => { }); // Update active session (for real-time set updates) -router.put('/active', async (req: any, res) => { +router.put('/active', validate(sessionSchema), async (req: any, res) => { try { const userId = req.user.userId; const { id, startTime, endTime, userBodyWeight, note, planId, planName, sets } = req.body; @@ -293,7 +286,7 @@ router.get('/quick-log', async (req: any, res) => { }); // Log a set to today's quick log session -router.post('/quick-log/set', async (req: any, res) => { +router.post('/quick-log/set', validate(logSetSchema), async (req: any, res) => { try { const userId = req.user.userId; const { exerciseId, weight, reps, distanceMeters, durationSeconds, height, bodyWeightPercentage, note, side } = req.body; @@ -355,7 +348,7 @@ router.post('/quick-log/set', async (req: any, res) => { }); // Log a set to the active session -router.post('/active/log-set', async (req: any, res) => { +router.post('/active/log-set', validate(logSetSchema), async (req: any, res) => { try { const userId = req.user.userId; const { exerciseId, reps, weight, distanceMeters, durationSeconds, side } = req.body; @@ -486,7 +479,7 @@ router.put('/active/set/:setId', async (req: any, res) => { }); // Update a set in the active session (STANDARD or QUICK_LOG) -router.patch('/active/set/:setId', async (req: any, res) => { +router.patch('/active/set/:setId', validate(updateSetSchema), async (req: any, res) => { try { const userId = req.user.userId; const { setId } = req.params; @@ -591,113 +584,4 @@ router.delete('/:id', async (req: any, res) => { } }); -// Get today's quick log session -router.get('/quick-log', async (req: any, res) => { - try { - const userId = req.user.userId; - const startOfDay = new Date(); - startOfDay.setHours(0, 0, 0, 0); - const endOfDay = new Date(); - endOfDay.setHours(23, 59, 59, 999); - - const session = await prisma.workoutSession.findFirst({ - where: { - userId, - type: 'QUICK_LOG', - startTime: { - gte: startOfDay, - lte: endOfDay - } - }, - include: { sets: { include: { exercise: true }, orderBy: { timestamp: 'desc' } } } - }); - - if (!session) { - return res.json({ success: true, session: null }); - } - - // Map exercise properties to sets for frontend compatibility - const mappedSession = { - ...session, - sets: session.sets.map((set: any) => ({ - ...set, - exerciseName: set.exercise.name, - type: set.exercise.type - })) - }; - - res.json({ success: true, session: mappedSession }); - } catch (error) { - console.error(error); - res.status(500).json({ error: 'Server error' }); - } -}); - -// Log a set to today's quick log session -router.post('/quick-log/set', async (req: any, res) => { - try { - const userId = req.user.userId; - const { exerciseId, weight, reps, distanceMeters, durationSeconds, height, bodyWeightPercentage, note, side } = req.body; - - const startOfDay = new Date(); - startOfDay.setHours(0, 0, 0, 0); - const endOfDay = new Date(); - endOfDay.setHours(23, 59, 59, 999); - - // Find or create today's quick log session - let session = await prisma.workoutSession.findFirst({ - where: { - userId, - type: 'QUICK_LOG', - startTime: { - gte: startOfDay, - lte: endOfDay - } - } - }); - - if (!session) { - session = await prisma.workoutSession.create({ - data: { - userId, - startTime: startOfDay, - type: 'QUICK_LOG', - note: 'Daily Quick Log' - } - }); - } - - // Create the set - const newSet = await prisma.workoutSet.create({ - data: { - sessionId: session.id, - exerciseId, - order: 0, // Order not strictly enforced for quick log - weight: weight ? parseFloat(weight) : null, - reps: reps ? parseInt(reps) : null, - distanceMeters: distanceMeters ? parseFloat(distanceMeters) : null, - durationSeconds: durationSeconds ? parseInt(durationSeconds) : null, - height: height ? parseFloat(height) : null, - bodyWeightPercentage: bodyWeightPercentage ? parseFloat(bodyWeightPercentage) : null, - side: side || null, - completed: true, - timestamp: new Date() - }, - include: { exercise: true } - }); - - const mappedSet = { - ...newSet, - exerciseName: newSet.exercise.name, - type: newSet.exercise.type - }; - - res.json({ success: true, newSet: mappedSet }); - - } catch (error) { - console.error(error); - res.status(500).json({ error: 'Server error' }); - } -}); - export default router; diff --git a/server/src/schemas/auth.ts b/server/src/schemas/auth.ts new file mode 100644 index 0000000..3addad3 --- /dev/null +++ b/server/src/schemas/auth.ts @@ -0,0 +1,32 @@ +import { z } from 'zod'; + +export const loginSchema = z.object({ + body: z.object({ + email: z.string().email(), + password: z.string().min(1), + }), +}); + +export const registerSchema = z.object({ + body: z.object({ + email: z.string().email(), + password: z.string().min(4), + role: z.enum(['USER', 'ADMIN']).optional(), + }), +}); + +export const changePasswordSchema = z.object({ + body: z.object({ + newPassword: z.string().min(4), + }), +}); + +export const updateProfileSchema = z.object({ + body: z.object({ + weight: z.number().optional(), + height: z.number().optional(), + gender: z.string().optional(), + birthDate: z.string().optional(), + language: z.string().optional() + }) +}) diff --git a/server/src/schemas/sessions.ts b/server/src/schemas/sessions.ts new file mode 100644 index 0000000..8da3bec --- /dev/null +++ b/server/src/schemas/sessions.ts @@ -0,0 +1,47 @@ +import { z } from 'zod'; + +export const sessionSchema = z.object({ + body: z.object({ + startTime: z.union([z.number(), z.string(), z.date()]), // Date.now or ISO string + endTime: z.union([z.number(), z.string(), z.date()]).nullable().optional(), + userBodyWeight: z.number().nullable().optional(), + note: z.string().nullable().optional(), + planId: z.string().nullable().optional(), + planName: z.string().nullable().optional(), + sets: z.array(z.object({ + exerciseId: z.string(), + weight: z.number().nullable().optional(), + reps: z.number().nullable().optional(), + distanceMeters: z.number().nullable().optional(), + durationSeconds: z.number().nullable().optional(), + completed: z.boolean().optional().default(true), + side: z.string().nullable().optional() + })) + }) +}); + +export const logSetSchema = z.object({ + body: z.object({ + exerciseId: z.string(), + weight: z.number().nullable().optional(), + reps: z.number().nullable().optional(), + distanceMeters: z.number().nullable().optional(), + durationSeconds: z.number().nullable().optional(), + height: z.number().nullable().optional(), + bodyWeightPercentage: z.number().nullable().optional(), + note: z.string().nullable().optional(), + side: z.string().nullable().optional(), + }) +}); + +export const updateSetSchema = z.object({ + body: z.object({ + weight: z.number().nullable().optional(), + reps: z.number().nullable().optional(), + distanceMeters: z.number().nullable().optional(), + durationSeconds: z.number().nullable().optional(), + height: z.number().nullable().optional(), + bodyWeightPercentage: z.number().nullable().optional(), + side: z.string().nullable().optional(), + }) +}); diff --git a/src/App.tsx b/src/App.tsx index 243dc1a..56f9087 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,27 +8,12 @@ import AICoach from './components/AICoach'; import Plans from './components/Plans'; import Login from './components/Login'; import Profile from './components/Profile'; -import { Language, User } from './types'; // Removed unused imports +import { Language, User } from './types'; import { getSystemLanguage } from './services/i18n'; import { useAuth } from './context/AuthContext'; -import { useData } from './context/DataContext'; function App() { const { currentUser, updateUser, logout } = useAuth(); - const { - sessions, - plans, - activeSession, - activePlan, - startSession, - endSession, - quitSession, - addSet, - removeSet, - updateSet, - updateSession, - deleteSessionById - } = useData(); const [language, setLanguage] = useState('en'); const navigate = useNavigate(); @@ -70,36 +55,19 @@ function App() { ) } /> + } /> + } /> + } /> + } /> + } /> = ({ history, userProfile, plans, lang }) => { +const AICoach: React.FC = ({ lang }) => { + const { currentUser } = useAuth(); + const { sessions: history, plans } = useSession(); + const userProfile = currentUser?.profile; + const [messages, setMessages] = useState([ { id: 'intro', role: 'model', text: t('ai_intro', lang) } ]); diff --git a/src/components/History.tsx b/src/components/History.tsx index 94ddd10..e27e2a1 100644 --- a/src/components/History.tsx +++ b/src/components/History.tsx @@ -3,16 +3,16 @@ import React, { useState } from 'react'; import { Calendar, Clock, TrendingUp, Gauge, Pencil, Trash2, X, Save, ArrowRight, ArrowUp, Timer, Activity, Dumbbell, Percent } from 'lucide-react'; import { WorkoutSession, ExerciseType, WorkoutSet, Language } from '../types'; import { t } from '../services/i18n'; +import { useSession } from '../context/SessionContext'; interface HistoryProps { - sessions: WorkoutSession[]; - onUpdateSession?: (session: WorkoutSession) => void; - onDeleteSession?: (sessionId: string) => void; lang: Language; } -const History: React.FC = ({ sessions, onUpdateSession, onDeleteSession, lang }) => { +const History: React.FC = ({ lang }) => { + const { sessions, updateSession, deleteSession } = useSession(); const [editingSession, setEditingSession] = useState(null); + const [deletingId, setDeletingId] = useState(null); const [deletingSetInfo, setDeletingSetInfo] = useState<{ sessionId: string, setId: string } | null>(null); @@ -60,10 +60,15 @@ const History: React.FC = ({ sessions, onUpdateSession, onDeleteSe return `${minutes}m`; }; - const handleSaveEdit = () => { - if (editingSession && onUpdateSession) { - onUpdateSession(editingSession); - setEditingSession(null); + + const handleSaveEdit = async () => { + if (editingSession) { + try { + await updateSession(editingSession); + setEditingSession(null); + } catch (e) { + console.error("Failed to update session", e); + } } }; @@ -83,11 +88,15 @@ const History: React.FC = ({ sessions, onUpdateSession, onDeleteSe }); }; - const handleConfirmDelete = () => { - if (deletingId && onDeleteSession) { - onDeleteSession(deletingId); - setDeletingId(null); - } else if (deletingSetInfo && onUpdateSession) { + const handleConfirmDelete = async () => { + if (deletingId) { + try { + await deleteSession(deletingId); + setDeletingId(null); + } catch (e) { + console.error("Failed to delete session", e); + } + } else if (deletingSetInfo) { // Find the session const session = sessions.find(s => s.id === deletingSetInfo.sessionId); if (session) { @@ -96,7 +105,11 @@ const History: React.FC = ({ sessions, onUpdateSession, onDeleteSe ...session, sets: session.sets.filter(s => s.id !== deletingSetInfo.setId) }; - onUpdateSession(updatedSession); + try { + await updateSession(updatedSession); + } catch (e) { + console.error("Failed to update session after set delete", e); + } } setDeletingSetInfo(null); } @@ -104,6 +117,7 @@ const History: React.FC = ({ sessions, onUpdateSession, onDeleteSe + if (sessions.length === 0) { return (
diff --git a/src/components/Plans.tsx b/src/components/Plans.tsx index c676204..c3d9bd5 100644 --- a/src/components/Plans.tsx +++ b/src/components/Plans.tsx @@ -2,21 +2,26 @@ import React, { useState, useEffect } from 'react'; import { Plus, Trash2, PlayCircle, Dumbbell, Save, X, ChevronRight, List, ArrowUp, ArrowDown, Scale, Edit2, User, Flame, Timer as TimerIcon, Ruler, Footprints, Activity, Percent, CheckCircle, GripVertical } from 'lucide-react'; import { WorkoutPlan, ExerciseDef, PlannedSet, Language, ExerciseType } from '../types'; -import { getPlans, savePlan, deletePlan, getExercises, saveExercise } from '../services/storage'; +import { getExercises, saveExercise } from '../services/storage'; import { t } from '../services/i18n'; import { generateId } from '../utils/uuid'; +import { useAuth } from '../context/AuthContext'; +import { useSession } from '../context/SessionContext'; +import { useActiveWorkout } from '../context/ActiveWorkoutContext'; import FilledInput from './FilledInput'; import { toTitleCase } from '../utils/text'; interface PlansProps { - userId: string; - onStartPlan: (plan: WorkoutPlan) => void; lang: Language; } -const Plans: React.FC = ({ userId, onStartPlan, lang }) => { - const [plans, setPlans] = useState([]); +const Plans: React.FC = ({ lang }) => { + const { currentUser } = useAuth(); + const userId = currentUser?.id || ''; + const { plans, savePlan, deletePlan } = useSession(); + const { startSession } = useActiveWorkout(); + const [isEditing, setIsEditing] = useState(false); const [editId, setEditId] = useState(null); @@ -39,9 +44,6 @@ const Plans: React.FC = ({ userId, onStartPlan, lang }) => { useEffect(() => { const loadData = async () => { - const fetchedPlans = await getPlans(userId); - setPlans(fetchedPlans); - const fetchedExercises = await getExercises(userId); // Filter out archived exercises if (Array.isArray(fetchedExercises)) { @@ -50,7 +52,7 @@ const Plans: React.FC = ({ userId, onStartPlan, lang }) => { setAvailableExercises([]); } }; - loadData(); + if (userId) loadData(); }, [userId]); const handleCreateNew = () => { @@ -72,18 +74,14 @@ const Plans: React.FC = ({ userId, onStartPlan, lang }) => { const handleSave = async () => { if (!name.trim() || !editId) return; const newPlan: WorkoutPlan = { id: editId, name, description, steps }; - await savePlan(userId, newPlan); - const updated = await getPlans(userId); - setPlans(updated); + await savePlan(newPlan); setIsEditing(false); }; const handleDelete = async (id: string, e: React.MouseEvent) => { e.stopPropagation(); if (confirm(t('delete_confirm', lang))) { - await deletePlan(userId, id); - const updated = await getPlans(userId); - setPlans(updated); + await deletePlan(id); } }; @@ -391,7 +389,7 @@ const Plans: React.FC = ({ userId, onStartPlan, lang }) => { {plan.steps.length} {t('exercises_count', lang)}