diff --git a/.actrc b/.actrc new file mode 100644 index 0000000..61dbaa5 --- /dev/null +++ b/.actrc @@ -0,0 +1,2 @@ +--container-architecture linux/amd64 +-P self-hosted=catthehacker/ubuntu:act-latest diff --git a/PROJECT_OVERVIEW.md b/PROJECT_OVERVIEW.md index 4a8f0a2..a2881b5 100644 --- a/PROJECT_OVERVIEW.md +++ b/PROJECT_OVERVIEW.md @@ -88,6 +88,11 @@ Basic relationships: - **Exercise Library**: Authenticated users can browse system exercises, create their own exercises, and tag them with muscle groups. - **Feedback**: Authenticated users submit feedback; server stores entry and can open a GitHub issue. +## Current Implementation Status + +- ✅ Exercise API + client CRUD flow is implemented (`/api/exercises`, `/api/muscle-groups`, `/exercises` page). +- 🚧 Workout API + client workflow is the primary remaining feature area. + ## API Surface (Current) - `POST /api/auth/*`: request-access, register, login, refresh-token, logout, forgot/reset-password diff --git a/README.md b/README.md index a7caf45..24b96f4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ [![CI / Build Images](https://github.com/arcot-labs/RepTrack/actions/workflows/build.yml/badge.svg)](https://github.com/arcot-labs/RepTrack/actions/workflows/build.yml) [![CD / Deploy Application](https://github.com/arcot-labs/RepTrack/actions/workflows/deploy.yml/badge.svg)](https://github.com/arcot-labs/RepTrack/actions/workflows/deploy.yml) -### Local Development +# RepTrack + +## Local Development Copy `.env.example` to `.env` & populate variables @@ -25,8 +27,34 @@ Start containers: ./scripts/dev.sh ``` -### Database +## Local GitHub Actions Testing + +Use `act` to run workflows locally for quick validation. + +List all workflows: + +```bash +act -l +``` + +Run a specific job: + +```bash +act -j {job-id} +``` + +## Database Conventions All writes should go through SQLAlchemy Alembic updates & bulk SQLAlchemy updates must explicitly set `updated_at` + +## Shadcn Component Conventions + +shadcn adds components under `client/src/components/ui/` + +To ensure custom styles & behavior survive component updates, follow these conventions: + +- Create custom component overrides under `client/src/components/ui/overrides/` +- Import override components in app code instead of generated shadcn components +- Add ESLint rules to prevent direct imports of generated components & point to override paths diff --git a/client/eslint.config.js b/client/eslint.config.js index 39f3d18..8cf2ff6 100644 --- a/client/eslint.config.js +++ b/client/eslint.config.js @@ -46,6 +46,11 @@ export default defineConfig([ }, ], paths: [ + { + name: '@/components/ui/button', + message: + 'Use Button from @/components/ui/overrides/button', + }, { name: 'sonner', importNames: ['toast'], diff --git a/client/package-lock.json b/client/package-lock.json index 79d960c..dd266a4 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,7 +8,7 @@ "name": "client", "version": "0.0.0", "dependencies": { - "@hey-api/openapi-ts": "^0.94.1", + "@hey-api/openapi-ts": "^0.94.2", "@hookform/resolvers": "^5.2.2", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", @@ -48,14 +48,14 @@ "@types/node": "^25.5.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.4", + "@vitejs/plugin-react": "^5.2.0", "eslint": "^9.39.4", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.4.0", "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", - "typescript-eslint": "^8.57.0", + "typescript-eslint": "^8.57.1", "vite": "^7.3.1", "vite-tsconfig-paths": "^6.1.1" } @@ -1046,12 +1046,12 @@ "license": "MIT" }, "node_modules/@hey-api/codegen-core": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.7.2.tgz", - "integrity": "sha512-nWEyUNbc1O7R5FHMwPh24+127jCbIs6vT89ncHqFSprE0tUVNemGO3cZflZCeGw8XhJAk6O18TcruUbMmwv0Rg==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.7.3.tgz", + "integrity": "sha512-tRxwr8hPbPgETbcfxbg0K4nZZxUM51I0hEHfLEWLmsTbnpY/E4CU25/PyHXD0KdKQS4TwkAL2csEXFQth6AzBQ==", "license": "MIT", "dependencies": { - "@hey-api/types": "0.1.3", + "@hey-api/types": "0.1.4", "ansi-colors": "4.1.3", "c12": "3.3.3", "color-support": "1.1.3" @@ -1061,9 +1061,6 @@ }, "funding": { "url": "https://github.com/sponsors/hey-api" - }, - "peerDependencies": { - "typescript": ">=5.5.3" } }, "node_modules/@hey-api/json-schema-ref-parser": { @@ -1084,18 +1081,19 @@ } }, "node_modules/@hey-api/openapi-ts": { - "version": "0.94.1", - "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.94.1.tgz", - "integrity": "sha512-WffyHMzsB8uVrzVxjK+0zII0msuqLL5JOGaZsWQzRvoZQJsJcGe9+kUaBNKPVDupcghHncSRJCdcC5ZcCdy/sw==", + "version": "0.94.2", + "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.94.2.tgz", + "integrity": "sha512-k8BmVfRZ3Ntpt99+0wzbw18sssf9mMgCpUFi9hTdUTGCgXEFlCnM6HW6tWZjPtT4CbclNqLVvSrfIABrRE88YA==", "license": "MIT", "dependencies": { - "@hey-api/codegen-core": "0.7.2", + "@hey-api/codegen-core": "0.7.3", "@hey-api/json-schema-ref-parser": "1.3.1", - "@hey-api/shared": "0.2.3", - "@hey-api/types": "0.1.3", + "@hey-api/shared": "0.2.4", + "@hey-api/types": "0.1.4", "ansi-colors": "4.1.3", "color-support": "1.1.3", - "commander": "14.0.3" + "commander": "14.0.3", + "get-tsconfig": "4.13.6" }, "bin": { "openapi-ts": "bin/run.js" @@ -1107,18 +1105,18 @@ "url": "https://github.com/sponsors/hey-api" }, "peerDependencies": { - "typescript": ">=5.5.3" + "typescript": ">=5.5.3 || 6.0.1-rc" } }, "node_modules/@hey-api/shared": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@hey-api/shared/-/shared-0.2.3.tgz", - "integrity": "sha512-XQsI/VmQeoHJFdZmBshQnMLGRq6kvSIXFgpxsb8k4F8nuKZ+54GAnq0DuTZcgnL6egAO/pN0KBaFbyl/yl9WFg==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@hey-api/shared/-/shared-0.2.4.tgz", + "integrity": "sha512-a3YPByL+odEY9QnEh0uhPJZFgLxzB4YEss+8mexU4ZQ3sL3Y/BbEiDPhLT2HI9xXgagYK/KScy8J7Jm14Zma0g==", "license": "MIT", "dependencies": { - "@hey-api/codegen-core": "0.7.2", + "@hey-api/codegen-core": "0.7.3", "@hey-api/json-schema-ref-parser": "1.3.1", - "@hey-api/types": "0.1.3", + "@hey-api/types": "0.1.4", "ansi-colors": "4.1.3", "cross-spawn": "7.0.6", "open": "11.0.0", @@ -1129,9 +1127,6 @@ }, "funding": { "url": "https://github.com/sponsors/hey-api" - }, - "peerDependencies": { - "typescript": ">=5.5.3" } }, "node_modules/@hey-api/shared/node_modules/semver": { @@ -1147,13 +1142,10 @@ } }, "node_modules/@hey-api/types": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@hey-api/types/-/types-0.1.3.tgz", - "integrity": "sha512-mZaiPOWH761yD4GjDQvtjS2ZYLu5o5pI1TVSvV/u7cmbybv51/FVtinFBeaE1kFQCKZ8OQpn2ezjLBJrKsGATw==", - "license": "MIT", - "peerDependencies": { - "typescript": ">=5.5.3" - } + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@hey-api/types/-/types-0.1.4.tgz", + "integrity": "sha512-thWfawrDIP7wSI9ioT13I5soaaqB5vAPIiZmgD8PbeEVKNrkonc0N/Sjj97ezl7oQgusZmaNphGdMKipPO6IBg==", + "license": "MIT" }, "node_modules/@hookform/resolvers": { "version": "5.2.2", @@ -5456,17 +5448,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", - "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", + "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/type-utils": "8.57.0", - "@typescript-eslint/utils": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/type-utils": "8.57.1", + "@typescript-eslint/utils": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -5479,7 +5471,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.57.0", + "@typescript-eslint/parser": "^8.57.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -5495,16 +5487,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", - "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz", + "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", "debug": "^4.4.3" }, "engines": { @@ -5520,14 +5512,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", - "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", + "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.0", - "@typescript-eslint/types": "^8.57.0", + "@typescript-eslint/tsconfig-utils": "^8.57.1", + "@typescript-eslint/types": "^8.57.1", "debug": "^4.4.3" }, "engines": { @@ -5542,14 +5534,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", - "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", + "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0" + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5560,9 +5552,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", - "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", + "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", "dev": true, "license": "MIT", "engines": { @@ -5577,15 +5569,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", - "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz", + "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/utils": "8.57.0", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/utils": "8.57.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -5602,9 +5594,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", - "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", + "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", "dev": true, "license": "MIT", "engines": { @@ -5616,16 +5608,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", - "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", + "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.57.0", - "@typescript-eslint/tsconfig-utils": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/visitor-keys": "8.57.0", + "@typescript-eslint/project-service": "8.57.1", + "@typescript-eslint/tsconfig-utils": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -5696,16 +5688,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", - "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz", + "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.0", - "@typescript-eslint/types": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0" + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5720,13 +5712,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", - "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", + "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/types": "8.57.1", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -5767,9 +5759,9 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.4.tgz", - "integrity": "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", "dev": true, "license": "MIT", "dependencies": { @@ -5784,7 +5776,7 @@ "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/acorn": { @@ -7456,9 +7448,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", "dev": true, "license": "ISC" }, @@ -7577,6 +7569,18 @@ "node": ">= 0.4" } }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/giget": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", @@ -10294,6 +10298,15 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/robust-predicates": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", @@ -10676,16 +10689,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.0.tgz", - "integrity": "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.1.tgz", + "integrity": "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.57.0", - "@typescript-eslint/parser": "8.57.0", - "@typescript-eslint/typescript-estree": "8.57.0", - "@typescript-eslint/utils": "8.57.0" + "@typescript-eslint/eslint-plugin": "8.57.1", + "@typescript-eslint/parser": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/utils": "8.57.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/client/package.json b/client/package.json index 1db7d27..d192fc6 100644 --- a/client/package.json +++ b/client/package.json @@ -9,7 +9,7 @@ "generate-api": "openapi-ts", "lint": "eslint --fix --max-warnings=0", "preview": "vite preview", - "update-components": "for file in src/components/ui/*.tsx; do npx shadcn@latest add -o $(basename \"$file\" .tsx); done" + "update-components": "for file in src/components/ui/*.tsx; do npx shadcn@latest add -o $(basename \"$file\" .tsx); done && cd .. && npx prettier --write client/src/components/ui" }, "lint-staged": { "*": [ @@ -18,7 +18,7 @@ ] }, "dependencies": { - "@hey-api/openapi-ts": "^0.94.1", + "@hey-api/openapi-ts": "^0.94.2", "@hookform/resolvers": "^5.2.2", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", @@ -58,14 +58,14 @@ "@types/node": "^25.5.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.4", + "@vitejs/plugin-react": "^5.2.0", "eslint": "^9.39.4", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.4.0", "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", - "typescript-eslint": "^8.57.0", + "typescript-eslint": "^8.57.1", "vite": "^7.3.1", "vite-tsconfig-paths": "^6.1.1" } diff --git a/client/src/AppRoutes.tsx b/client/src/AppRoutes.tsx index ad74c38..c7119d2 100644 --- a/client/src/AppRoutes.tsx +++ b/client/src/AppRoutes.tsx @@ -6,6 +6,7 @@ import { AppLayout } from '@/layout/AppLayout' import { Admin } from '@/pages/Admin' import { Dashboard } from '@/pages/Dashboard' import { Docs } from '@/pages/Docs' +import { Exercises } from '@/pages/Exercises' import { ForgotPassword } from '@/pages/ForgotPassword' import { Login } from '@/pages/Login' import { Register } from '@/pages/Register' @@ -25,6 +26,7 @@ export function AppRoutes() { } > } /> + } /> }> } /> } /> diff --git a/client/src/auth/RequireAuth.tsx b/client/src/auth/RequireAuth.tsx index f7d3942..8adb3f4 100644 --- a/client/src/auth/RequireAuth.tsx +++ b/client/src/auth/RequireAuth.tsx @@ -9,9 +9,9 @@ interface RequireAuthProps { } export function RequireAuth({ children, requireAdmin }: RequireAuthProps) { - const { loading, authenticated, user } = useSession() + const { isLoading, authenticated, user } = useSession() const location = useLocation() - if (loading) return + if (isLoading) return if (!authenticated) return if (requireAdmin && !user?.is_admin) return diff --git a/client/src/auth/RequireGuest.tsx b/client/src/auth/RequireGuest.tsx index a786c05..3101b52 100644 --- a/client/src/auth/RequireGuest.tsx +++ b/client/src/auth/RequireGuest.tsx @@ -5,10 +5,10 @@ import type { JSX } from 'react' import { Navigate, useLocation } from 'react-router-dom' export function RequireGuest({ children }: { children: JSX.Element }) { - const { loading, authenticated } = useSession() + const { isLoading, authenticated } = useSession() const location = useLocation() const state = location.state as LocationState | null - if (loading) return + if (isLoading) return if (authenticated) { const to = state?.from?.pathname ?? '/' return diff --git a/client/src/auth/SessionProvider.tsx b/client/src/auth/SessionProvider.tsx index 49f2b1b..6897567 100644 --- a/client/src/auth/SessionProvider.tsx +++ b/client/src/auth/SessionProvider.tsx @@ -5,10 +5,10 @@ import { type ReactNode, useEffect, useState } from 'react' export function SessionProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null) - const [loading, setLoading] = useState(true) + const [isLoading, setIsLoading] = useState(true) const loadSession = async () => { - setLoading(true) + setIsLoading(true) try { const { data, error } = await UserService.getCurrentUser() if (error) { @@ -18,7 +18,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { logger.info('Fetched current user', data) setUser(data) } finally { - setLoading(false) + setIsLoading(false) } } @@ -30,7 +30,7 @@ export function SessionProvider({ children }: { children: ReactNode }) { >( + const [isLoadingRequestIds, setIsLoadingRequestIds] = useState>( new Set() ) const [confirmDialog, setConfirmDialog] = useState<{ @@ -92,13 +86,7 @@ export function AccessRequestsTable({ action: null, }) - const handleConfirmAction = () => { - if (confirmDialog.request && confirmDialog.action) - void handleUpdateStatus(confirmDialog.request, confirmDialog.action) - setConfirmDialog({ isOpen: false, request: null, action: null }) - } - - const handleShowConfirmDialog = ( + const openConfirmDialog = ( request: AccessRequestPublic, action: 'approved' | 'rejected' ) => { @@ -109,11 +97,25 @@ export function AccessRequestsTable({ }) } + const closeConfirmDialog = () => { + setConfirmDialog({ + isOpen: false, + request: null, + action: null, + }) + } + + const handleConfirmAction = () => { + if (confirmDialog.request && confirmDialog.action) + void handleUpdateStatus(confirmDialog.request, confirmDialog.action) + closeConfirmDialog() + } + const handleUpdateStatus = async ( request: AccessRequestPublic, status: 'approved' | 'rejected' ) => { - setLoadingRequestIds((prev) => new Set(prev).add(request.id)) + setIsLoadingRequestIds((prev) => new Set(prev).add(request.id)) try { const { error } = await AdminService.updateAccessRequestStatus({ path: { @@ -148,7 +150,7 @@ export function AccessRequestsTable({ } onRequestUpdated(updatedRequest) } finally { - setLoadingRequestIds((prev) => { + setIsLoadingRequestIds((prev) => { const next = new Set(prev) next.delete(request.id) return next @@ -161,25 +163,23 @@ export function AccessRequestsTable({ menuItems: (row) => { if (row.status !== 'pending') return [] - const isRowLoading = loadingRequestIds.has(row.id) + const isRowLoading = isLoadingRequestIds.has(row.id) return [ { type: 'action', - label: 'Approve', className: greenText, icon: Check, onSelect: () => { - handleShowConfirmDialog(row, 'approved') + openConfirmDialog(row, 'approved') }, disabled: isRowLoading, }, { type: 'action', className: redText, - label: 'Reject', icon: X, onSelect: () => { - handleShowConfirmDialog(row, 'rejected') + openConfirmDialog(row, 'rejected') }, disabled: isRowLoading, }, @@ -188,7 +188,6 @@ export function AccessRequestsTable({ } const columns: ColumnDef[] = [ - createSelectColumn(), { id: 'name', accessorFn: (row) => `${row.first_name} ${row.last_name}`, @@ -327,31 +326,21 @@ export function AccessRequestsTable({ ?
This action is irreversible.
-
- + + -
+ diff --git a/client/src/components/Feedback.tsx b/client/src/components/FeedbackFormDialog.tsx similarity index 95% rename from client/src/components/Feedback.tsx rename to client/src/components/FeedbackFormDialog.tsx index 08c78b8..156a47d 100644 --- a/client/src/components/Feedback.tsx +++ b/client/src/components/FeedbackFormDialog.tsx @@ -1,6 +1,5 @@ import { FeedbackService } from '@/api/generated' import { zCreateFeedbackRequest } from '@/api/generated/zod.gen' -import { Button } from '@/components/ui/button' import { Dialog, DialogClose, @@ -13,6 +12,7 @@ import { } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' +import { Button } from '@/components/ui/overrides/button' import { Textarea } from '@/components/ui/textarea' import { handleApiError } from '@/lib/http' import { notify } from '@/lib/notify' @@ -27,7 +27,7 @@ const feedbackFormSchema = zCreateFeedbackRequest.omit({ }) type FeedbackForm = z.infer -export function Feedback() { +export function FeedbackFormDialog() { const [open, setOpen] = useState(false) const [files, setFiles] = useState([]) @@ -136,7 +136,9 @@ export function Feedback() { }} >
+
+