From 6283fcc0673ad520494ae35077dc87f57e4cad39 Mon Sep 17 00:00:00 2001 From: kunzhutich Date: Fri, 28 Aug 2026 07:26:55 -0700 Subject: [PATCH 01/28] initial small setup --- workshops/fullstack-aws/projects/submission/Group3/ReadMe.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 workshops/fullstack-aws/projects/submission/Group3/ReadMe.md diff --git a/workshops/fullstack-aws/projects/submission/Group3/ReadMe.md b/workshops/fullstack-aws/projects/submission/Group3/ReadMe.md new file mode 100644 index 00000000..e69de29b From 528ffd719720c4c22050817901928f2399cb73d8 Mon Sep 17 00:00:00 2001 From: kunzhutich Date: Fri, 28 Aug 2026 07:55:57 -0700 Subject: [PATCH 02/28] Create test.txt --- .../fullstack-aws/projects/submission/Group3/backend/test.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/test.txt diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/test.txt b/workshops/fullstack-aws/projects/submission/Group3/backend/test.txt new file mode 100644 index 00000000..e69de29b From 6a84f1b6602265c8dd1cb665b89a1d270aac55c7 Mon Sep 17 00:00:00 2001 From: Connor Vandrush Date: Fri, 28 Aug 2026 11:23:15 -0400 Subject: [PATCH 03/28] connor test --- .../projects/submission/Group3/frontend/connor/test.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/connor/test.txt diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/connor/test.txt b/workshops/fullstack-aws/projects/submission/Group3/frontend/connor/test.txt new file mode 100644 index 00000000..e69de29b From 1ea77483f23c27c79ea605d4fd09ce1c3a079f0b Mon Sep 17 00:00:00 2001 From: Connor Vandrush Date: Fri, 28 Aug 2026 11:25:25 -0400 Subject: [PATCH 04/28] deleted folders --- .../projects/submission/Group3/frontend/connor/test.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/connor/test.txt diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/connor/test.txt b/workshops/fullstack-aws/projects/submission/Group3/frontend/connor/test.txt deleted file mode 100644 index e69de29b..00000000 From a2043a59cd0631af3dbb9212d52a9b4924e248a0 Mon Sep 17 00:00:00 2001 From: Connor Vandrush Date: Fri, 28 Aug 2026 11:26:45 -0400 Subject: [PATCH 05/28] test commit --- .../fullstack-aws/projects/submission/Group3/frontend/test.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/test.txt diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/test.txt b/workshops/fullstack-aws/projects/submission/Group3/frontend/test.txt new file mode 100644 index 00000000..e69de29b From a1181c278eda614de75f1e9e50974e269bd3c197 Mon Sep 17 00:00:00 2001 From: Connor Vandrush Date: Fri, 28 Aug 2026 12:24:21 -0400 Subject: [PATCH 06/28] Add header component --- package-lock.json | 149 ++ package.json | 8 + .../frontend/task-board-frontend/.gitignore | 24 + .../task-board-frontend/.oxlintrc.json | 8 + .../frontend/task-board-frontend/README.md | 16 + .../frontend/task-board-frontend/index.html | 13 + .../task-board-frontend/package-lock.json | 1262 +++++++++++++++++ .../frontend/task-board-frontend/package.json | 23 + .../task-board-frontend/public/favicon.svg | 1 + .../task-board-frontend/public/icons.svg | 24 + .../frontend/task-board-frontend/src/App.jsx | 12 + .../task-board-frontend/src/assets/hero.png | Bin 0 -> 13057 bytes .../task-board-frontend/src/assets/react.svg | 1 + .../task-board-frontend/src/assets/vite.svg | 1 + .../src/components/ProjectList.jsx | 0 .../src/components/ProjectList.module.css | 0 .../src/components/TraineeHeader.jsx | 28 + .../src/components/TraineeHeader.module.css | 48 + .../frontend/task-board-frontend/src/main.jsx | 10 + .../src/store/TraineeStore.js | 16 + .../task-board-frontend/src/store/store.js | 10 + .../task-board-frontend/vite.config.js | 7 + 22 files changed, 1661 insertions(+) create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/.gitignore create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/.oxlintrc.json create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/README.md create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/index.html create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/package-lock.json create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/package.json create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/public/favicon.svg create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/public/icons.svg create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.jsx create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/assets/hero.png create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/assets/react.svg create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/assets/vite.svg create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/ProjectList.jsx create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/ProjectList.module.css create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeHeader.jsx create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeHeader.module.css create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/main.jsx create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/TraineeStore.js create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/store.js create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/vite.config.js diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..00caf1ad --- /dev/null +++ b/package-lock.json @@ -0,0 +1,149 @@ +{ + "name": "workshops", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@reduxjs/toolkit": "^2.12.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-redux": "^9.3.0" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/immer": { + "version": "11.1.18", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.18.tgz", + "integrity": "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.3.0.tgz", + "integrity": "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..7d008bd6 --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "@reduxjs/toolkit": "^2.12.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-redux": "^9.3.0" + } +} diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/.gitignore b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/.gitignore new file mode 100644 index 00000000..a547bf36 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/.oxlintrc.json b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/.oxlintrc.json new file mode 100644 index 00000000..12550782 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/.oxlintrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["react", "oxc"], + "rules": { + "react/rules-of-hooks": "error", + "react/only-export-components": ["warn", { "allowConstantExport": true }] + } +} diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/README.md b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/README.md new file mode 100644 index 00000000..d937833b --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/README.md @@ -0,0 +1,16 @@ +# React + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the Oxlint configuration + +If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and Oxlint's TypeScript related rules in your project. diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/index.html b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/index.html new file mode 100644 index 00000000..9241e0ff --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + task-board-frontend + + +
+ + + diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/package-lock.json b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/package-lock.json new file mode 100644 index 00000000..76325933 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/package-lock.json @@ -0,0 +1,1262 @@ +{ + "name": "task-board-frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "task-board-frontend", + "version": "0.0.0", + "dependencies": { + "react": "^19.2.8", + "react-dom": "^19.2.8" + }, + "devDependencies": { + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@vitejs/plugin-react": "^6.1.0", + "oxlint": "^1.79.0", + "vite": "^8.2.2" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.147.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.147.0.tgz", + "integrity": "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.80.0.tgz", + "integrity": "sha512-RM3Plj+biQpxa5d1GOOX6ciDlcUROmm4OZ/pLTpitkQt2mJv4jhtY4cbgaetOm5UKWZe05/TGQ6o1Vl8EOHkrA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.80.0.tgz", + "integrity": "sha512-YlO5JEf0Yr2bUUlu8O8daVcUxtcGGbcSmyV7E7nSbJbfAdxTE0PFPwgnIlw7wXJaTYjb+qs5hI5q3jxUkI7cAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.80.0.tgz", + "integrity": "sha512-BULDOyO3AhsmdWfQeIUCykDt3dd7XZBGLhp1eIh56skRv01O+cNjNPwXMIbeW1x4+pxcln5if72wcRgViVo7PA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.80.0.tgz", + "integrity": "sha512-YJ4JzLw7N5TDSQFlA0hAQGHvnDZgyypm1yunObVWcWiF9KM7eGCJKYKLgTC2Fi/57OdnBhbj4OkzPGdFQJ6HyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.80.0.tgz", + "integrity": "sha512-AYUIk5QnL0s8oWAYsREZwkRYy1SupJTXALo93J1TgzHywxQtdM99FecRMQ87MXEdPQ0j1TmEpeeq3fGNkpvMqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.80.0.tgz", + "integrity": "sha512-9hBZVANupQ89W9dXyE0n8doCyaW5pDyGn3y6XlIMPZ+rIKuyqkr3SNUXmVJIhuvUq0NBU3RBiSXXE69l4XI6KA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.80.0.tgz", + "integrity": "sha512-SvS2uKqzY+pbfuvAHzH4338R6Zwo805GAwrIMVvK1KxoOWCIjZUdfzTCvilD7z6JK91v011+zYMryabhDo2AsQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.80.0.tgz", + "integrity": "sha512-tCLadyqRVL3pQTRPNg7cjXKvcvS4fbyXeQHhKk5BTJ1oftQln5/yIIWbu/Xom/DX41zv2P9QGt6+D/TtQVtY3A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.80.0.tgz", + "integrity": "sha512-XfpCNRlOPcLlJl4Bn/FUhjqlR6BVavEykERBf/MV7YA9VZDa5g5znVqYhyviMafcxS9Pe/i/kPvHNO0U6svEHQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.80.0.tgz", + "integrity": "sha512-3I4yMwcFG9NeO8ioY6JBBuKsIm5GL/x7MATt1S4tVWaxPu5HcJ+XnLUbcVBTxG8q2Wu56HSj+NmXQiVYb1lp6A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.80.0.tgz", + "integrity": "sha512-E1wAKymkpe1/E8helzBKdm81OBOF+ezxRyXRMEuik3ZpWDER5CPOKZwF66RsdwW98uwZv8UTFremUQtC1CzdJA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.80.0.tgz", + "integrity": "sha512-+gLRGD4sIo3+VA++iham5UxD9tKSoJ/VOrROCEXIcknrYtQg6iIQgvjN0cpiRF7N6UYC7pJbvHJlDnMge5LRpQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.80.0.tgz", + "integrity": "sha512-aR0PrzHj9leW3NmzBAAP4EzdoBNoJcs9sjnIQPIwyRnBGYrRbXUIpEB5Q39AqK3PLY5JK5uEhDQDiUa1QSAstw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.80.0.tgz", + "integrity": "sha512-vSVh5cSo3Xxs6ghBCcFJlpbkbENzDog1qXtoXLa/HC3aCrR4XO76GZbXmQoCPHnu99nQpdCeC3H9tdNICfDh7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.80.0.tgz", + "integrity": "sha512-FfzBXpNQ8u7/ZI/p8bl73MeZ508Ax3hxWp3SiJpEFiC+BB9XcXy5FAZHTLKDPSzrUpxQZSZJAVdDmuJp/+HDBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.80.0.tgz", + "integrity": "sha512-zMzbkumtmprCgRwoYNzcB3iC39fXdJIMLMU33KdCjEGLlJGOEt1+LwQ4LF8ndLzAEKVz4BR0y3V6Xrkk3Nm3yA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.80.0.tgz", + "integrity": "sha512-ib6iRcrXsk4t1fm3iKcwksyWh1ZkZXC/2mEzakl0ai2+6HZunf1WWMZ/xP9EJAvw9g9K4UVTC3NF/+G2qLrbTQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.80.0.tgz", + "integrity": "sha512-xhRWBMpLxZvgKAH6+DJZmpP+W8Y8UdQOSU1JfxSWNXsaBaRGW77j+1hCuNHlzj7OH4SPN8fYd1q0o2qrDtoVyw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.80.0.tgz", + "integrity": "sha512-yAnO7lwBYQnz2pcfBPIGQQZWIX5zd5R/1aAKIF3oE+TVj7IhoHcROjOkz3sRDngzqhfPKfFaXqug5j5rE5dn6Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.6.tgz", + "integrity": "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.6.tgz", + "integrity": "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.6.tgz", + "integrity": "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.6.tgz", + "integrity": "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.6.tgz", + "integrity": "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.6.tgz", + "integrity": "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.6.tgz", + "integrity": "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.6.tgz", + "integrity": "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.6.tgz", + "integrity": "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.6.tgz", + "integrity": "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.6.tgz", + "integrity": "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.6.tgz", + "integrity": "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.6.tgz", + "integrity": "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.6.tgz", + "integrity": "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.6.tgz", + "integrity": "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz", + "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oxlint": { + "version": "1.80.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.80.0.tgz", + "integrity": "sha512-5nTiSps4qdbCWLbxzuO00alHkEO2exR9YMN/ig6QXWrLsYSG0KaObOAM+l6oU2LcKPWoSAGYbkZIGEu1ViiWKA==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.80.0", + "@oxlint/binding-android-arm64": "1.80.0", + "@oxlint/binding-darwin-arm64": "1.80.0", + "@oxlint/binding-darwin-x64": "1.80.0", + "@oxlint/binding-freebsd-x64": "1.80.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.80.0", + "@oxlint/binding-linux-arm-musleabihf": "1.80.0", + "@oxlint/binding-linux-arm64-gnu": "1.80.0", + "@oxlint/binding-linux-arm64-musl": "1.80.0", + "@oxlint/binding-linux-ppc64-gnu": "1.80.0", + "@oxlint/binding-linux-riscv64-gnu": "1.80.0", + "@oxlint/binding-linux-riscv64-musl": "1.80.0", + "@oxlint/binding-linux-s390x-gnu": "1.80.0", + "@oxlint/binding-linux-x64-gnu": "1.80.0", + "@oxlint/binding-linux-x64-musl": "1.80.0", + "@oxlint/binding-openharmony-arm64": "1.80.0", + "@oxlint/binding-win32-arm64-msvc": "1.80.0", + "@oxlint/binding-win32-ia32-msvc": "1.80.0", + "@oxlint/binding-win32-x64-msvc": "1.80.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/rolldown": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.6.tgz", + "integrity": "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.147.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.6", + "@rolldown/binding-android-arm64": "1.2.6", + "@rolldown/binding-darwin-arm64": "1.2.6", + "@rolldown/binding-darwin-x64": "1.2.6", + "@rolldown/binding-freebsd-x64": "1.2.6", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", + "@rolldown/binding-linux-arm64-gnu": "1.2.6", + "@rolldown/binding-linux-arm64-musl": "1.2.6", + "@rolldown/binding-linux-ppc64-gnu": "1.2.6", + "@rolldown/binding-linux-s390x-gnu": "1.2.6", + "@rolldown/binding-linux-x64-gnu": "1.2.6", + "@rolldown/binding-linux-x64-musl": "1.2.6", + "@rolldown/binding-openharmony-arm64": "1.2.6", + "@rolldown/binding-win32-arm64-msvc": "1.2.6", + "@rolldown/binding-win32-x64-msvc": "1.2.6" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/package.json b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/package.json new file mode 100644 index 00000000..2ba3240e --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/package.json @@ -0,0 +1,23 @@ +{ + "name": "task-board-frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "oxlint", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.2.8", + "react-dom": "^19.2.8" + }, + "devDependencies": { + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@vitejs/plugin-react": "^6.1.0", + "oxlint": "^1.79.0", + "vite": "^8.2.2" + } +} diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/public/favicon.svg b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/public/favicon.svg new file mode 100644 index 00000000..6893eb13 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/public/icons.svg b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/public/icons.svg new file mode 100644 index 00000000..e9522193 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.jsx b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.jsx new file mode 100644 index 00000000..6b170138 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.jsx @@ -0,0 +1,12 @@ +import TraineeHeader from "./components/TraineeHeader"; + +function App() { + return ( + <> + +

hello

+ + ); +} + +export default App; diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/assets/hero.png b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/assets/hero.png new file mode 100644 index 0000000000000000000000000000000000000000..02251f4b956c55af2d76fd0788124d7eee2b45eb GIT binary patch literal 13057 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 P00000NkvXXu0mjft=yBf literal 0 HcmV?d00001 diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/assets/react.svg b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/assets/react.svg new file mode 100644 index 00000000..6c87de9b --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/assets/vite.svg b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/assets/vite.svg new file mode 100644 index 00000000..5101b674 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/ProjectList.jsx b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/ProjectList.jsx new file mode 100644 index 00000000..e69de29b diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/ProjectList.module.css b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/ProjectList.module.css new file mode 100644 index 00000000..e69de29b diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeHeader.jsx b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeHeader.jsx new file mode 100644 index 00000000..5f1e8517 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeHeader.jsx @@ -0,0 +1,28 @@ +import styles from "./TraineeHeader.module.css"; + +import { useSelector } from "react-redux"; + +export default function TraineeHeader() { + const traineeName = useSelector((state) => state.TraineeSlice.name); + + return ( +
+
+

NEW TRAINEE

+ +

+ Welcome to C.B.M.H. +

+ +

+ {traineeName ? `Welcome, ${traineeName}!` : "Welcome!"} +

+ +

+ Your trainee task board is ready. View your tasks, track your + progress, and stay organized throughout your training. +

+
+
+ ); +} diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeHeader.module.css b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeHeader.module.css new file mode 100644 index 00000000..fd5c64a4 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeHeader.module.css @@ -0,0 +1,48 @@ +.traineeHeader { + width: 100%; + padding: 2rem; + border-radius: 12px; + background-color: #ffffff; + border: 1px solid #e0e0e0; + box-sizing: border-box; +} + +.headerContent { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.welcomeLabel { + margin: 0; + color: #666666; + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.08em; +} + +.traineeHeader h1 { + margin: 0; + color: #222222; + font-size: 2rem; + font-weight: 700; +} + +.traineeHeader h1 span { + color: #2f6f4e; +} + +.traineeName { + margin: 0.25rem 0 0; + color: #333333; + font-size: 1.1rem; + font-weight: 600; +} + +.description { + max-width: 650px; + margin: 0.25rem 0 0; + color: #666666; + font-size: 0.95rem; + line-height: 1.5; +} diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/main.jsx b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/main.jsx new file mode 100644 index 00000000..2ab3c44a --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/main.jsx @@ -0,0 +1,10 @@ +import { Provider } from "react-redux"; +import { createRoot } from "react-dom/client"; +import App from "./App.jsx"; +import store from "./store/store.js"; + +createRoot(document.getElementById("root")).render( + + + , +); diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/TraineeStore.js b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/TraineeStore.js new file mode 100644 index 00000000..dd2c50be --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/TraineeStore.js @@ -0,0 +1,16 @@ +import { createSlice } from "@reduxjs/toolkit"; + +const TraineeSlice = createSlice({ + name: "TraineeSlice", + initialState: { + name: "test", + }, + reducers: { + setName: (state, action) => { + state.name = action.payload; + }, + }, +}); + +export const { setName } = TraineeSlice.actions; +export default TraineeSlice.reducer; diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/store.js b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/store.js new file mode 100644 index 00000000..f190953c --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/store.js @@ -0,0 +1,10 @@ +import { configureStore } from "@reduxjs/toolkit"; + +export const Store = configureStore({ + reducer: { + TraineeSlice: require("./TraineeStore").default, + }, + middleware: (getDefault) => getDefault(), +}); + +export default Store; diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/vite.config.js b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/vite.config.js new file mode 100644 index 00000000..9982072c --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/vite.config.js @@ -0,0 +1,7 @@ +import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite' + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [react()], +}) From ee2530c9aff318e70f91d9a5ddf9751b251f534a Mon Sep 17 00:00:00 2001 From: Connor Vandrush Date: Fri, 28 Aug 2026 12:38:52 -0400 Subject: [PATCH 07/28] fixed --- package-lock.json | 149 ------------------ package.json | 8 - .../task-board-frontend/package-lock.json | 45 +++++- .../frontend/task-board-frontend/package.json | 3 +- .../frontend/task-board-frontend/src/App.jsx | 4 +- .../task-board-frontend/src/store/store.js | 4 +- 6 files changed, 49 insertions(+), 164 deletions(-) delete mode 100644 package-lock.json delete mode 100644 package.json diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 00caf1ad..00000000 --- a/package-lock.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "name": "workshops", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@reduxjs/toolkit": "^2.12.0", - "react": "^19.2.8", - "react-dom": "^19.2.8", - "react-redux": "^9.3.0" - } - }, - "node_modules/@reduxjs/toolkit": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", - "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "@standard-schema/utils": "^0.3.0", - "immer": "^11.0.0", - "redux": "^5.0.1", - "redux-thunk": "^3.1.0", - "reselect": "^5.1.0" - }, - "peerDependencies": { - "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", - "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-redux": { - "optional": true - } - } - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "license": "MIT" - }, - "node_modules/@standard-schema/utils": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", - "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", - "license": "MIT" - }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", - "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", - "license": "MIT" - }, - "node_modules/immer": { - "version": "11.1.18", - "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.18.tgz", - "integrity": "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/react": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", - "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", - "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.8" - } - }, - "node_modules/react-redux": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", - "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", - "license": "MIT", - "dependencies": { - "@types/use-sync-external-store": "^0.0.6", - "use-sync-external-store": "^1.4.0" - }, - "peerDependencies": { - "@types/react": "^18.2.25 || ^19", - "react": "^18.0 || ^19", - "redux": "^5.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "redux": { - "optional": true - } - } - }, - "node_modules/redux": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" - }, - "node_modules/redux-thunk": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", - "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", - "license": "MIT", - "peerDependencies": { - "redux": "^5.0.0" - } - }, - "node_modules/reselect": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.3.0.tgz", - "integrity": "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg==", - "license": "MIT" - }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 7d008bd6..00000000 --- a/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "dependencies": { - "@reduxjs/toolkit": "^2.12.0", - "react": "^19.2.8", - "react-dom": "^19.2.8", - "react-redux": "^9.3.0" - } -} diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/package-lock.json b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/package-lock.json index 76325933..ca2647f1 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/package-lock.json +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/package-lock.json @@ -9,7 +9,8 @@ "version": "0.0.0", "dependencies": { "react": "^19.2.8", - "react-dom": "^19.2.8" + "react-dom": "^19.2.8", + "react-redux": "^9.3.0" }, "devDependencies": { "@types/react": "^19.2.18", @@ -618,7 +619,7 @@ "version": "19.2.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -634,6 +635,12 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-react": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz", @@ -668,7 +675,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/detect-libc": { @@ -1113,6 +1120,29 @@ "react": "^19.2.8" } }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/rolldown": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.6.tgz", @@ -1180,6 +1210,15 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/package.json b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/package.json index 2ba3240e..e14bca68 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/package.json +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/package.json @@ -11,7 +11,8 @@ }, "dependencies": { "react": "^19.2.8", - "react-dom": "^19.2.8" + "react-dom": "^19.2.8", + "react-redux": "^9.3.0" }, "devDependencies": { "@types/react": "^19.2.18", diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.jsx b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.jsx index 6b170138..1a56439b 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.jsx +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.jsx @@ -2,10 +2,10 @@ import TraineeHeader from "./components/TraineeHeader"; function App() { return ( - <> +

hello

- +
); } diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/store.js b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/store.js index f190953c..c88d47eb 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/store.js +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/store.js @@ -1,8 +1,10 @@ import { configureStore } from "@reduxjs/toolkit"; +import TraineeSlice from "./TraineeStore"; + export const Store = configureStore({ reducer: { - TraineeSlice: require("./TraineeStore").default, + TraineeSlice: TraineeSlice, }, middleware: (getDefault) => getDefault(), }); From 3c395e84a743ecb6e3845a276656fb3511fcd7d3 Mon Sep 17 00:00:00 2001 From: kunzhutich Date: Fri, 28 Aug 2026 09:55:16 -0700 Subject: [PATCH 08/28] initial BE layer ready but not tested --- .../submission/Group3/backend/.env.example | 5 + .../submission/Group3/backend/README.md | 34 +++++ .../submission/Group3/backend/alembic.ini | 38 ++++++ .../submission/Group3/backend/alembic/env.py | 39 ++++++ .../Group3/backend/alembic/script.py.mako | 23 ++++ .../alembic/versions/0001_initial_schema.py | 118 +++++++++++++++++ .../backend/{test.txt => app/__init__.py} | 0 .../Group3/backend/app/api/__init__.py | 0 .../Group3/backend/app/api/v1/__init__.py | 0 .../Group3/backend/app/api/v1/auth.py | 25 ++++ .../backend/app/api/v1/manager_progress.py | 27 ++++ .../backend/app/api/v1/manager_tasks.py | 120 +++++++++++++++++ .../Group3/backend/app/api/v1/router.py | 10 ++ .../backend/app/api/v1/student_tasks.py | 61 +++++++++ .../Group3/backend/app/core/__init__.py | 0 .../Group3/backend/app/core/config.py | 18 +++ .../Group3/backend/app/core/database.py | 21 +++ .../Group3/backend/app/core/security.py | 30 +++++ .../backend/app/dependencies/__init__.py | 0 .../Group3/backend/app/dependencies/auth.py | 38 ++++++ .../submission/Group3/backend/app/main.py | 24 ++++ .../Group3/backend/app/models/__init__.py | 17 +++ .../Group3/backend/app/models/cohort.py | 18 +++ .../Group3/backend/app/models/enums.py | 14 ++ .../backend/app/models/progress_update.py | 23 ++++ .../Group3/backend/app/models/subtask.py | 18 +++ .../backend/app/models/subtask_completion.py | 20 +++ .../Group3/backend/app/models/task.py | 28 ++++ .../backend/app/models/task_assignment.py | 27 ++++ .../Group3/backend/app/models/user.py | 24 ++++ .../backend/app/repositories/__init__.py | 0 .../app/repositories/progress_repository.py | 52 ++++++++ .../app/repositories/subtask_repository.py | 27 ++++ .../task_assignment_repository.py | 26 ++++ .../app/repositories/task_repository.py | 27 ++++ .../app/repositories/user_repository.py | 17 +++ .../Group3/backend/app/schemas/__init__.py | 0 .../Group3/backend/app/schemas/auth.py | 23 ++++ .../Group3/backend/app/schemas/base.py | 12 ++ .../Group3/backend/app/schemas/progress.py | 47 +++++++ .../Group3/backend/app/schemas/task.py | 95 ++++++++++++++ .../Group3/backend/app/services/__init__.py | 0 .../backend/app/services/auth_service.py | 19 +++ .../backend/app/services/progress_service.py | 97 ++++++++++++++ .../app/services/task_assignment_factory.py | 14 ++ .../backend/app/services/task_service.py | 121 ++++++++++++++++++ .../Group3/backend/requirements.txt | 10 ++ 47 files changed, 1407 insertions(+) create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/.env.example create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/README.md create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/alembic.ini create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/alembic/env.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/alembic/script.py.mako create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/alembic/versions/0001_initial_schema.py rename workshops/fullstack-aws/projects/submission/Group3/backend/{test.txt => app/__init__.py} (100%) create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/api/__init__.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/__init__.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/auth.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/manager_progress.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/manager_tasks.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/router.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/student_tasks.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/core/__init__.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/core/config.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/core/database.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/core/security.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/dependencies/__init__.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/dependencies/auth.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/main.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/models/__init__.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/models/cohort.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/models/enums.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/models/progress_update.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/models/subtask.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/models/subtask_completion.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/models/task.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/models/task_assignment.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/models/user.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/repositories/__init__.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/repositories/progress_repository.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/repositories/subtask_repository.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/repositories/task_assignment_repository.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/repositories/task_repository.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/repositories/user_repository.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/schemas/__init__.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/schemas/auth.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/schemas/base.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/schemas/progress.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/schemas/task.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/services/__init__.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/services/auth_service.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/services/progress_service.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/services/task_assignment_factory.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/app/services/task_service.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/requirements.txt diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/.env.example b/workshops/fullstack-aws/projects/submission/Group3/backend/.env.example new file mode 100644 index 00000000..60f829a9 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/.env.example @@ -0,0 +1,5 @@ +DATABASE_URL=postgresql://user:password@host:5432/noticeboardtracker +JWT_SECRET_KEY=changeme +JWT_ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=60 +CORS_ORIGINS=["http://localhost:5173"] diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/README.md b/workshops/fullstack-aws/projects/submission/Group3/backend/README.md new file mode 100644 index 00000000..489af4e4 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/README.md @@ -0,0 +1,34 @@ +# NoticeBoardTracker API + +FastAPI backend, layered: `api` (controllers) -> `services` (business logic) -> `repositories` (DB access) -> `models` (ORM). `schemas` holds the Pydantic request/response DTOs. + +## Setup + +1. `python -m venv .venv` then activate it (`.venv\Scripts\activate` on Windows, `source .venv/bin/activate` elsewhere) +2. `pip install -r requirements.txt` +3. Copy `.env.example` to `.env`, fill in `DATABASE_URL` (from whoever set up Postgres) and a `JWT_SECRET_KEY` +4. `alembic upgrade head` to create the schema +5. `uvicorn app.main:app --reload` + +Interactive API docs: `http://localhost:8000/docs` + +## Layout + +- `app/models` — SQLAlchemy ORM classes, the DB shape +- `app/schemas` — Pydantic DTOs, the API shape. All inherit `CamelModel` (`app/schemas/base.py`), so Python stays snake_case internally and JSON on the wire is camelCase automatically +- `app/repositories` — DB queries only, no business logic +- `app/services` — business logic; `TaskAssignmentFactory` is where a task fans out into one `task_assignments` row per trainee +- `app/api/v1` — routers, thin HTTP layer only +- `app/dependencies/auth.py` — JWT decode + role guards (`get_current_manager`, `get_current_trainee`) +- `alembic/versions` — migrations, the source of truth for DB structure; don't hand-edit the shared DB, add a migration instead + +## Adding a migration + +After changing a model in `app/models/`: + +``` +alembic revision --autogenerate -m "describe the change" +alembic upgrade head +``` + +Review the generated file before committing — autogenerate doesn't always get constraints/enums exactly right. diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/alembic.ini b/workshops/fullstack-aws/projects/submission/Group3/backend/alembic.ini new file mode 100644 index 00000000..a97945fe --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/alembic.ini @@ -0,0 +1,38 @@ +[alembic] +script_location = alembic +prepend_sys_path = . +sqlalchemy.url = + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/alembic/env.py b/workshops/fullstack-aws/projects/submission/Group3/backend/alembic/env.py new file mode 100644 index 00000000..6296e632 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/alembic/env.py @@ -0,0 +1,39 @@ +from logging.config import fileConfig + +from alembic import context +from sqlalchemy import engine_from_config, pool + +from app.core.config import get_settings +from app.core.database import Base +from app.models import * # noqa: F401,F403 - register all models on Base.metadata + +config = context.config +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +config.set_main_option("sqlalchemy.url", get_settings().database_url) + +target_metadata = Base.metadata + + +def run_migrations_offline() -> None: + url = config.get_main_option("sqlalchemy.url") + context.configure(url=url, target_metadata=target_metadata, literal_binds=True) + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + connectable = engine_from_config( + config.get_section(config.config_ini_section), prefix="sqlalchemy.", poolclass=pool.NullPool + ) + with connectable.connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata) + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/alembic/script.py.mako b/workshops/fullstack-aws/projects/submission/Group3/backend/alembic/script.py.mako new file mode 100644 index 00000000..3217cf0f --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/alembic/script.py.mako @@ -0,0 +1,23 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/alembic/versions/0001_initial_schema.py b/workshops/fullstack-aws/projects/submission/Group3/backend/alembic/versions/0001_initial_schema.py new file mode 100644 index 00000000..02fe1b96 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/alembic/versions/0001_initial_schema.py @@ -0,0 +1,118 @@ +"""initial schema + +Revision ID: 0001 +Revises: +Create Date: 2026-08-28 + +""" +from alembic import op +import sqlalchemy as sa + +revision = "0001" +down_revision = None +branch_labels = None +depends_on = None + +user_role = sa.Enum("trainee", "manager", "hr", name="user_role") +urgency_level = sa.Enum("low", "medium", "high", "urgent", name="urgency_level") + + +def upgrade() -> None: + op.create_table( + "cohorts", + sa.Column("id", sa.Integer, primary_key=True), + sa.Column("name", sa.String(255), nullable=False), + sa.Column("description", sa.Text, nullable=True), + sa.Column("manager_id", sa.Integer, nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + ) + + op.create_table( + "users", + sa.Column("id", sa.Integer, primary_key=True), + sa.Column("email", sa.String(255), unique=True, nullable=False), + sa.Column("hashed_password", sa.String(255), nullable=False), + sa.Column("full_name", sa.String(255), nullable=False), + sa.Column("role", user_role, nullable=False), + sa.Column("cohort_id", sa.Integer, sa.ForeignKey("cohorts.id"), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + ) + + op.create_foreign_key("fk_cohorts_manager_id", "cohorts", "users", ["manager_id"], ["id"]) + + op.create_table( + "tasks", + sa.Column("id", sa.Integer, primary_key=True), + sa.Column("title", sa.String(255), nullable=False), + sa.Column("description", sa.Text, nullable=True), + sa.Column("created_by", sa.Integer, sa.ForeignKey("users.id"), nullable=False), + sa.Column("cohort_id", sa.Integer, sa.ForeignKey("cohorts.id"), nullable=True), + sa.Column("due_date", sa.DateTime(timezone=True), nullable=True), + sa.Column("urgency", urgency_level, nullable=False, server_default="medium"), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + ) + + op.create_table( + "subtasks", + sa.Column("id", sa.Integer, primary_key=True), + sa.Column("task_id", sa.Integer, sa.ForeignKey("tasks.id", ondelete="CASCADE"), nullable=False), + sa.Column("title", sa.String(255), nullable=False), + sa.Column("order_index", sa.Integer, nullable=False, server_default="0"), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + ) + + op.create_table( + "task_assignments", + sa.Column("id", sa.Integer, primary_key=True), + sa.Column("task_id", sa.Integer, sa.ForeignKey("tasks.id", ondelete="CASCADE"), nullable=False), + sa.Column("trainee_id", sa.Integer, sa.ForeignKey("users.id"), nullable=False), + sa.Column("current_percentage", sa.SmallInteger, nullable=False, server_default="0"), + sa.Column("last_updated_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.UniqueConstraint("task_id", "trainee_id", name="uq_task_assignment"), + sa.CheckConstraint("current_percentage BETWEEN 0 AND 100", name="ck_task_assignment_pct"), + ) + + op.create_table( + "subtask_completions", + sa.Column("id", sa.Integer, primary_key=True), + sa.Column("subtask_id", sa.Integer, sa.ForeignKey("subtasks.id", ondelete="CASCADE"), nullable=False), + sa.Column("trainee_id", sa.Integer, sa.ForeignKey("users.id"), nullable=False), + sa.Column("is_completed", sa.Boolean, nullable=False, server_default=sa.false()), + sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True), + sa.UniqueConstraint("subtask_id", "trainee_id", name="uq_subtask_completion"), + ) + + op.create_table( + "progress_updates", + sa.Column("id", sa.Integer, primary_key=True), + sa.Column( + "task_assignment_id", + sa.Integer, + sa.ForeignKey("task_assignments.id", ondelete="CASCADE"), + nullable=False, + ), + sa.Column("percentage", sa.SmallInteger, nullable=False), + sa.Column("comment", sa.Text, nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), + sa.CheckConstraint("percentage BETWEEN 0 AND 100", name="ck_progress_update_pct"), + ) + + op.create_index("idx_task_assignments_task", "task_assignments", ["task_id"]) + op.create_index("idx_task_assignments_trainee", "task_assignments", ["trainee_id"]) + op.create_index("idx_progress_updates_assignment", "progress_updates", ["task_assignment_id", "created_at"]) + + +def downgrade() -> None: + op.drop_table("progress_updates") + op.drop_table("subtask_completions") + op.drop_table("task_assignments") + op.drop_table("subtasks") + op.drop_table("tasks") + op.drop_constraint("fk_cohorts_manager_id", "cohorts", type_="foreignkey") + op.drop_table("users") + op.drop_table("cohorts") + urgency_level.drop(op.get_bind(), checkfirst=True) + user_role.drop(op.get_bind(), checkfirst=True) diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/test.txt b/workshops/fullstack-aws/projects/submission/Group3/backend/app/__init__.py similarity index 100% rename from workshops/fullstack-aws/projects/submission/Group3/backend/test.txt rename to workshops/fullstack-aws/projects/submission/Group3/backend/app/__init__.py diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/api/__init__.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/__init__.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/auth.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/auth.py new file mode 100644 index 00000000..3416c72a --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/auth.py @@ -0,0 +1,25 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.dependencies.auth import get_current_user +from app.models.user import User +from app.schemas.auth import LoginRequest, TokenResponse, UserOut +from app.services.auth_service import AuthService + +router = APIRouter(prefix="/auth", tags=["auth"]) + + +@router.post("/login", response_model=TokenResponse) +def login(payload: LoginRequest, db: Session = Depends(get_db)) -> TokenResponse: + service = AuthService(db) + user = service.authenticate(payload.email, payload.password) + if user is None: + raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid email or password") + token = service.issue_token(user) + return TokenResponse(access_token=token, role=user.role.value, user_id=user.id) + + +@router.get("/me", response_model=UserOut) +def me(current_user: User = Depends(get_current_user)) -> User: + return current_user diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/manager_progress.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/manager_progress.py new file mode 100644 index 00000000..21ef9f86 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/manager_progress.py @@ -0,0 +1,27 @@ +from fastapi import APIRouter, Depends +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.dependencies.auth import get_current_manager +from app.models.user import User +from app.schemas.task import TaskProgressOut, TraineeProgressDetailOut +from app.services.task_service import TaskService + +router = APIRouter(prefix="/manager/tasks", tags=["manager-progress"]) + + +@router.get("/{task_id}/progress", response_model=TaskProgressOut) +def task_progress( + task_id: int, db: Session = Depends(get_db), manager: User = Depends(get_current_manager) +) -> dict: + return TaskService(db).get_task_progress_summary(manager.id, task_id) + + +@router.get("/{task_id}/progress/{trainee_id}", response_model=TraineeProgressDetailOut) +def trainee_progress( + task_id: int, + trainee_id: int, + db: Session = Depends(get_db), + manager: User = Depends(get_current_manager), +) -> dict: + return TaskService(db).get_trainee_progress_detail(manager.id, task_id, trainee_id) diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/manager_tasks.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/manager_tasks.py new file mode 100644 index 00000000..b850006f --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/manager_tasks.py @@ -0,0 +1,120 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.dependencies.auth import get_current_manager +from app.models.user import User +from app.schemas.task import ( + SubtaskCreate, + SubtaskOut, + SubtaskUpdate, + TaskCreate, + TaskDetailOut, + TaskOut, + TaskUpdate, +) +from app.services.task_service import TaskService + +router = APIRouter(prefix="/manager/tasks", tags=["manager-tasks"]) +subtask_router = APIRouter(prefix="/manager/subtasks", tags=["manager-tasks"]) + + +def _to_task_out(task) -> TaskOut: + return TaskOut( + id=task.id, + title=task.title, + description=task.description, + due_date=task.due_date, + urgency=task.urgency, + assigned_trainee_count=len(task.assignments), + ) + + +@router.post("", response_model=TaskOut, status_code=status.HTTP_201_CREATED) +def create_task( + payload: TaskCreate, db: Session = Depends(get_db), manager: User = Depends(get_current_manager) +) -> TaskOut: + task = TaskService(db).create_task(manager.id, payload) + return _to_task_out(task) + + +@router.get("", response_model=list[TaskOut]) +def list_tasks(db: Session = Depends(get_db), manager: User = Depends(get_current_manager)) -> list[TaskOut]: + tasks = TaskService(db).list_manager_tasks(manager.id) + return [_to_task_out(t) for t in tasks] + + +@router.get("/{task_id}", response_model=TaskDetailOut) +def get_task( + task_id: int, db: Session = Depends(get_db), manager: User = Depends(get_current_manager) +) -> TaskDetailOut: + service = TaskService(db) + task = service.get_owned_task(manager.id, task_id) + subtasks = service.subtasks.list_by_task(task_id) + return TaskDetailOut( + id=task.id, + title=task.title, + description=task.description, + due_date=task.due_date, + urgency=task.urgency, + subtasks=[SubtaskOut(id=s.id, title=s.title, order_index=s.order_index) for s in subtasks], + ) + + +@router.put("/{task_id}", response_model=TaskOut) +def update_task( + task_id: int, + payload: TaskUpdate, + db: Session = Depends(get_db), + manager: User = Depends(get_current_manager), +) -> TaskOut: + task = TaskService(db).update_task(manager.id, task_id, payload) + return _to_task_out(task) + + +@router.delete("/{task_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_task(task_id: int, db: Session = Depends(get_db), manager: User = Depends(get_current_manager)) -> None: + TaskService(db).delete_task(manager.id, task_id) + + +@router.post("/{task_id}/subtasks", response_model=SubtaskOut, status_code=status.HTTP_201_CREATED) +def add_subtask( + task_id: int, + payload: SubtaskCreate, + db: Session = Depends(get_db), + manager: User = Depends(get_current_manager), +) -> SubtaskOut: + subtask = TaskService(db).add_subtask(manager.id, task_id, payload.title, payload.order_index) + return SubtaskOut(id=subtask.id, title=subtask.title, order_index=subtask.order_index) + + +@subtask_router.put("/{subtask_id}", response_model=SubtaskOut) +def update_subtask( + subtask_id: int, + payload: SubtaskUpdate, + db: Session = Depends(get_db), + manager: User = Depends(get_current_manager), +) -> SubtaskOut: + service = TaskService(db) + subtask = service.subtasks.get_by_id(subtask_id) + if subtask is None: + raise HTTPException(status.HTTP_404_NOT_FOUND, "Subtask not found") + service.get_owned_task(manager.id, subtask.task_id) + for field, value in payload.model_dump(exclude_unset=True).items(): + setattr(subtask, field, value) + db.commit() + db.refresh(subtask) + return SubtaskOut(id=subtask.id, title=subtask.title, order_index=subtask.order_index) + + +@subtask_router.delete("/{subtask_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_subtask( + subtask_id: int, db: Session = Depends(get_db), manager: User = Depends(get_current_manager) +) -> None: + service = TaskService(db) + subtask = service.subtasks.get_by_id(subtask_id) + if subtask is None: + raise HTTPException(status.HTTP_404_NOT_FOUND, "Subtask not found") + service.get_owned_task(manager.id, subtask.task_id) + service.subtasks.delete(subtask) + db.commit() diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/router.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/router.py new file mode 100644 index 00000000..9e6387cc --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/router.py @@ -0,0 +1,10 @@ +from fastapi import APIRouter + +from app.api.v1 import auth, manager_progress, manager_tasks, student_tasks + +api_router = APIRouter(prefix="/api") +api_router.include_router(auth.router) +api_router.include_router(manager_tasks.router) +api_router.include_router(manager_tasks.subtask_router) +api_router.include_router(manager_progress.router) +api_router.include_router(student_tasks.router) diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/student_tasks.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/student_tasks.py new file mode 100644 index 00000000..76912403 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/student_tasks.py @@ -0,0 +1,61 @@ +from fastapi import APIRouter, Depends, Query, status +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.dependencies.auth import get_current_trainee +from app.models.user import User +from app.schemas.progress import ( + ProgressUpdateCreate, + ProgressUpdateOut, + StudentTaskDetail, + StudentTaskListItem, + SubtaskToggle, + SubtaskToggleOut, +) +from app.services.progress_service import ProgressService + +router = APIRouter(prefix="/student", tags=["student"]) + + +@router.get("/tasks", response_model=list[StudentTaskListItem]) +def list_tasks( + sort: str | None = Query(default=None), + db: Session = Depends(get_db), + trainee: User = Depends(get_current_trainee), +) -> list[dict]: + return ProgressService(db).list_student_tasks(trainee.id, sort) + + +@router.get("/tasks/{task_id}", response_model=StudentTaskDetail) +def task_detail( + task_id: int, db: Session = Depends(get_db), trainee: User = Depends(get_current_trainee) +) -> dict: + return ProgressService(db).get_student_task_detail(trainee.id, task_id) + + +@router.patch("/subtasks/{subtask_id}", response_model=SubtaskToggleOut) +def toggle_subtask( + subtask_id: int, + payload: SubtaskToggle, + db: Session = Depends(get_db), + trainee: User = Depends(get_current_trainee), +) -> dict: + return ProgressService(db).toggle_subtask(trainee.id, subtask_id, payload.is_completed) + + +@router.post("/tasks/{task_id}/progress", response_model=ProgressUpdateOut, status_code=status.HTTP_201_CREATED) +def submit_progress( + task_id: int, + payload: ProgressUpdateCreate, + db: Session = Depends(get_db), + trainee: User = Depends(get_current_trainee), +) -> dict: + return ProgressService(db).submit_progress_update(trainee.id, task_id, payload.percentage, payload.comment) + + +@router.get("/tasks/{task_id}/progress/history", response_model=list[ProgressUpdateOut]) +def progress_history( + task_id: int, db: Session = Depends(get_db), trainee: User = Depends(get_current_trainee) +) -> list[ProgressUpdateOut]: + history = ProgressService(db).get_progress_history(trainee.id, task_id) + return [ProgressUpdateOut(task_id=task_id, **entry) for entry in history] diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/core/__init__.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/core/config.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/core/config.py new file mode 100644 index 00000000..16ef9eb6 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/core/config.py @@ -0,0 +1,18 @@ +from functools import lru_cache + +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + database_url: str + jwt_secret_key: str + jwt_algorithm: str = "HS256" + access_token_expire_minutes: int = 60 + cors_origins: list[str] = ["http://localhost:5173"] + + model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") + + +@lru_cache +def get_settings() -> Settings: + return Settings() diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/core/database.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/core/database.py new file mode 100644 index 00000000..33ab17bd --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/core/database.py @@ -0,0 +1,21 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import declarative_base, sessionmaker + +from app.core.config import get_settings + +settings = get_settings() + +# One engine, one session factory for the whole process - the Singleton +# the rest of the app depends on for DB access. +engine = create_engine(settings.database_url, pool_pre_ping=True) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/core/security.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/core/security.py new file mode 100644 index 00000000..43d1f03f --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/core/security.py @@ -0,0 +1,30 @@ +from datetime import datetime, timedelta, timezone + +from jose import JWTError, jwt +from passlib.context import CryptContext + +from app.core.config import get_settings + +settings = get_settings() +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +def hash_password(password: str) -> str: + return pwd_context.hash(password) + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + + +def create_access_token(subject: int, role: str) -> str: + expire = datetime.now(timezone.utc) + timedelta(minutes=settings.access_token_expire_minutes) + payload = {"sub": str(subject), "role": role, "exp": expire} + return jwt.encode(payload, settings.jwt_secret_key, algorithm=settings.jwt_algorithm) + + +def decode_access_token(token: str) -> dict: + try: + return jwt.decode(token, settings.jwt_secret_key, algorithms=[settings.jwt_algorithm]) + except JWTError as exc: + raise ValueError("invalid token") from exc diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/dependencies/__init__.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/dependencies/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/dependencies/auth.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/dependencies/auth.py new file mode 100644 index 00000000..0392dd2c --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/dependencies/auth.py @@ -0,0 +1,38 @@ +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from sqlalchemy.orm import Session + +from app.core.database import get_db +from app.core.security import decode_access_token +from app.models.enums import UserRole +from app.models.user import User +from app.repositories.user_repository import UserRepository + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login") + + +def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)) -> User: + credentials_error = HTTPException(status.HTTP_401_UNAUTHORIZED, "Could not validate credentials") + try: + payload = decode_access_token(token) + user_id = int(payload["sub"]) + except (ValueError, KeyError): + raise credentials_error + + user = UserRepository(db).get_by_id(user_id) + if user is None: + raise credentials_error + return user + + +def require_role(*roles: UserRole): + def dependency(user: User = Depends(get_current_user)) -> User: + if user.role not in roles: + raise HTTPException(status.HTTP_403_FORBIDDEN, "Not permitted for this role") + return user + + return dependency + + +get_current_manager = require_role(UserRole.manager) +get_current_trainee = require_role(UserRole.trainee) diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/main.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/main.py new file mode 100644 index 00000000..686c4252 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/main.py @@ -0,0 +1,24 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.api.v1.router import api_router +from app.core.config import get_settings + +settings = get_settings() + +app = FastAPI(title="NoticeBoardTracker API") + +app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(api_router) + + +@app.get("/health") +def health() -> dict: + return {"status": "ok"} diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/__init__.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/__init__.py new file mode 100644 index 00000000..184a50d6 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/__init__.py @@ -0,0 +1,17 @@ +from app.models.cohort import Cohort +from app.models.progress_update import ProgressUpdate +from app.models.subtask import Subtask +from app.models.subtask_completion import SubtaskCompletion +from app.models.task import Task +from app.models.task_assignment import TaskAssignment +from app.models.user import User + +__all__ = [ + "User", + "Cohort", + "Task", + "Subtask", + "TaskAssignment", + "SubtaskCompletion", + "ProgressUpdate", +] diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/cohort.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/cohort.py new file mode 100644 index 00000000..d9d82ae8 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/cohort.py @@ -0,0 +1,18 @@ +from datetime import datetime + +from sqlalchemy import DateTime, ForeignKey, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.core.database import Base + + +class Cohort(Base): + __tablename__ = "cohorts" + + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(String(255), nullable=False) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + manager_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + + trainees = relationship("User", foreign_keys="User.cohort_id", back_populates="cohort") diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/enums.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/enums.py new file mode 100644 index 00000000..f219a65a --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/enums.py @@ -0,0 +1,14 @@ +import enum + + +class UserRole(str, enum.Enum): + trainee = "trainee" + manager = "manager" + hr = "hr" + + +class UrgencyLevel(str, enum.Enum): + low = "low" + medium = "medium" + high = "high" + urgent = "urgent" diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/progress_update.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/progress_update.py new file mode 100644 index 00000000..e0856587 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/progress_update.py @@ -0,0 +1,23 @@ +from datetime import datetime + +from sqlalchemy import CheckConstraint, DateTime, ForeignKey, SmallInteger, Text, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.core.database import Base + + +class ProgressUpdate(Base): + """Append-only history log - a new row per trainee update, never an overwrite.""" + + __tablename__ = "progress_updates" + __table_args__ = (CheckConstraint("percentage BETWEEN 0 AND 100", name="ck_progress_update_pct"),) + + id: Mapped[int] = mapped_column(primary_key=True) + task_assignment_id: Mapped[int] = mapped_column( + ForeignKey("task_assignments.id", ondelete="CASCADE"), nullable=False + ) + percentage: Mapped[int] = mapped_column(SmallInteger, nullable=False) + comment: Mapped[str | None] = mapped_column(Text, nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + + assignment = relationship("TaskAssignment", back_populates="history") diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/subtask.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/subtask.py new file mode 100644 index 00000000..b734dc12 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/subtask.py @@ -0,0 +1,18 @@ +from datetime import datetime + +from sqlalchemy import DateTime, ForeignKey, Integer, String, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.core.database import Base + + +class Subtask(Base): + __tablename__ = "subtasks" + + id: Mapped[int] = mapped_column(primary_key=True) + task_id: Mapped[int] = mapped_column(ForeignKey("tasks.id", ondelete="CASCADE"), nullable=False) + title: Mapped[str] = mapped_column(String(255), nullable=False) + order_index: Mapped[int] = mapped_column(Integer, default=0, nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + + task = relationship("Task", back_populates="subtasks") diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/subtask_completion.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/subtask_completion.py new file mode 100644 index 00000000..b4735bf3 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/subtask_completion.py @@ -0,0 +1,20 @@ +from datetime import datetime + +from sqlalchemy import Boolean, DateTime, ForeignKey, UniqueConstraint +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.core.database import Base + + +class SubtaskCompletion(Base): + __tablename__ = "subtask_completions" + __table_args__ = (UniqueConstraint("subtask_id", "trainee_id", name="uq_subtask_completion"),) + + id: Mapped[int] = mapped_column(primary_key=True) + subtask_id: Mapped[int] = mapped_column(ForeignKey("subtasks.id", ondelete="CASCADE"), nullable=False) + trainee_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False) + is_completed: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + + subtask = relationship("Subtask") + trainee = relationship("User") diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/task.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/task.py new file mode 100644 index 00000000..e7948514 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/task.py @@ -0,0 +1,28 @@ +from datetime import datetime + +from sqlalchemy import DateTime, Enum, ForeignKey, String, Text, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.core.database import Base +from app.models.enums import UrgencyLevel + + +class Task(Base): + __tablename__ = "tasks" + + id: Mapped[int] = mapped_column(primary_key=True) + title: Mapped[str] = mapped_column(String(255), nullable=False) + description: Mapped[str | None] = mapped_column(Text, nullable=True) + created_by: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False) + cohort_id: Mapped[int | None] = mapped_column(ForeignKey("cohorts.id"), nullable=True) + due_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + urgency: Mapped[UrgencyLevel] = mapped_column( + Enum(UrgencyLevel, name="urgency_level"), default=UrgencyLevel.medium, nullable=False + ) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) + + subtasks = relationship("Subtask", back_populates="task", cascade="all, delete-orphan") + assignments = relationship("TaskAssignment", back_populates="task", cascade="all, delete-orphan") diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/task_assignment.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/task_assignment.py new file mode 100644 index 00000000..91dc82ac --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/task_assignment.py @@ -0,0 +1,27 @@ +from datetime import datetime + +from sqlalchemy import CheckConstraint, DateTime, ForeignKey, SmallInteger, UniqueConstraint, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.core.database import Base + + +class TaskAssignment(Base): + """Per-trainee anchor for a task - a cohort task fans out into one row per trainee here.""" + + __tablename__ = "task_assignments" + __table_args__ = ( + UniqueConstraint("task_id", "trainee_id", name="uq_task_assignment"), + CheckConstraint("current_percentage BETWEEN 0 AND 100", name="ck_task_assignment_pct"), + ) + + id: Mapped[int] = mapped_column(primary_key=True) + task_id: Mapped[int] = mapped_column(ForeignKey("tasks.id", ondelete="CASCADE"), nullable=False) + trainee_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False) + current_percentage: Mapped[int] = mapped_column(SmallInteger, default=0, nullable=False) + last_updated_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + + task = relationship("Task", back_populates="assignments") + trainee = relationship("User") + history = relationship("ProgressUpdate", back_populates="assignment", cascade="all, delete-orphan") diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/user.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/user.py new file mode 100644 index 00000000..f585999b --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/models/user.py @@ -0,0 +1,24 @@ +from datetime import datetime + +from sqlalchemy import DateTime, Enum, ForeignKey, String, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.core.database import Base +from app.models.enums import UserRole + + +class User(Base): + __tablename__ = "users" + + id: Mapped[int] = mapped_column(primary_key=True) + email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False) + hashed_password: Mapped[str] = mapped_column(String(255), nullable=False) + full_name: Mapped[str] = mapped_column(String(255), nullable=False) + role: Mapped[UserRole] = mapped_column(Enum(UserRole, name="user_role"), nullable=False) + cohort_id: Mapped[int | None] = mapped_column(ForeignKey("cohorts.id"), nullable=True) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) + + cohort = relationship("Cohort", foreign_keys=[cohort_id], back_populates="trainees") diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/repositories/__init__.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/repositories/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/repositories/progress_repository.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/repositories/progress_repository.py new file mode 100644 index 00000000..9315e932 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/repositories/progress_repository.py @@ -0,0 +1,52 @@ +from datetime import datetime, timezone + +from sqlalchemy.orm import Session + +from app.models.progress_update import ProgressUpdate +from app.models.subtask_completion import SubtaskCompletion + + +class ProgressRepository: + def __init__(self, db: Session): + self.db = db + + def add_history_entry(self, task_assignment_id: int, percentage: int, comment: str | None) -> ProgressUpdate: + entry = ProgressUpdate(task_assignment_id=task_assignment_id, percentage=percentage, comment=comment) + self.db.add(entry) + self.db.flush() + return entry + + def list_history(self, task_assignment_id: int) -> list[ProgressUpdate]: + return ( + self.db.query(ProgressUpdate) + .filter(ProgressUpdate.task_assignment_id == task_assignment_id) + .order_by(ProgressUpdate.created_at.desc()) + .all() + ) + + def get_completion(self, subtask_id: int, trainee_id: int) -> SubtaskCompletion | None: + return ( + self.db.query(SubtaskCompletion) + .filter(SubtaskCompletion.subtask_id == subtask_id, SubtaskCompletion.trainee_id == trainee_id) + .first() + ) + + def set_completion(self, subtask_id: int, trainee_id: int, is_completed: bool) -> SubtaskCompletion: + completion = self.get_completion(subtask_id, trainee_id) + if completion is None: + completion = SubtaskCompletion(subtask_id=subtask_id, trainee_id=trainee_id) + self.db.add(completion) + completion.is_completed = is_completed + completion.completed_at = datetime.now(timezone.utc) if is_completed else None + self.db.flush() + return completion + + def list_completions_by_task(self, subtask_ids: list[int], trainee_id: int) -> dict[int, bool]: + if not subtask_ids: + return {} + rows = ( + self.db.query(SubtaskCompletion) + .filter(SubtaskCompletion.subtask_id.in_(subtask_ids), SubtaskCompletion.trainee_id == trainee_id) + .all() + ) + return {row.subtask_id: row.is_completed for row in rows} diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/repositories/subtask_repository.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/repositories/subtask_repository.py new file mode 100644 index 00000000..6ff78445 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/repositories/subtask_repository.py @@ -0,0 +1,27 @@ +from sqlalchemy.orm import Session + +from app.models.subtask import Subtask + + +class SubtaskRepository: + def __init__(self, db: Session): + self.db = db + + def create(self, subtask: Subtask) -> Subtask: + self.db.add(subtask) + self.db.flush() + return subtask + + def get_by_id(self, subtask_id: int) -> Subtask | None: + return self.db.get(Subtask, subtask_id) + + def list_by_task(self, task_id: int) -> list[Subtask]: + return ( + self.db.query(Subtask) + .filter(Subtask.task_id == task_id) + .order_by(Subtask.order_index) + .all() + ) + + def delete(self, subtask: Subtask) -> None: + self.db.delete(subtask) diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/repositories/task_assignment_repository.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/repositories/task_assignment_repository.py new file mode 100644 index 00000000..3a3e5a97 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/repositories/task_assignment_repository.py @@ -0,0 +1,26 @@ +from sqlalchemy.orm import Session + +from app.models.task_assignment import TaskAssignment + + +class TaskAssignmentRepository: + def __init__(self, db: Session): + self.db = db + + def bulk_create(self, assignments: list[TaskAssignment]) -> list[TaskAssignment]: + self.db.add_all(assignments) + self.db.flush() + return assignments + + def list_by_task(self, task_id: int) -> list[TaskAssignment]: + return self.db.query(TaskAssignment).filter(TaskAssignment.task_id == task_id).all() + + def get_by_task_and_trainee(self, task_id: int, trainee_id: int) -> TaskAssignment | None: + return ( + self.db.query(TaskAssignment) + .filter(TaskAssignment.task_id == task_id, TaskAssignment.trainee_id == trainee_id) + .first() + ) + + def list_by_trainee(self, trainee_id: int) -> list[TaskAssignment]: + return self.db.query(TaskAssignment).filter(TaskAssignment.trainee_id == trainee_id).all() diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/repositories/task_repository.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/repositories/task_repository.py new file mode 100644 index 00000000..3016f3d0 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/repositories/task_repository.py @@ -0,0 +1,27 @@ +from sqlalchemy.orm import Session + +from app.models.task import Task + + +class TaskRepository: + def __init__(self, db: Session): + self.db = db + + def create(self, task: Task) -> Task: + self.db.add(task) + self.db.flush() + return task + + def get_by_id(self, task_id: int) -> Task | None: + return self.db.get(Task, task_id) + + def list_by_manager(self, manager_id: int) -> list[Task]: + return ( + self.db.query(Task) + .filter(Task.created_by == manager_id) + .order_by(Task.created_at.desc()) + .all() + ) + + def delete(self, task: Task) -> None: + self.db.delete(task) diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/repositories/user_repository.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/repositories/user_repository.py new file mode 100644 index 00000000..70c77e70 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/repositories/user_repository.py @@ -0,0 +1,17 @@ +from sqlalchemy.orm import Session + +from app.models.user import User + + +class UserRepository: + def __init__(self, db: Session): + self.db = db + + def get_by_id(self, user_id: int) -> User | None: + return self.db.get(User, user_id) + + def get_by_email(self, email: str) -> User | None: + return self.db.query(User).filter(User.email == email).first() + + def list_by_cohort(self, cohort_id: int) -> list[User]: + return self.db.query(User).filter(User.cohort_id == cohort_id).all() diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/schemas/__init__.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/schemas/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/schemas/auth.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/schemas/auth.py new file mode 100644 index 00000000..5b6cc165 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/schemas/auth.py @@ -0,0 +1,23 @@ +from pydantic import EmailStr + +from app.schemas.base import CamelModel + + +class LoginRequest(CamelModel): + email: EmailStr + password: str + + +class TokenResponse(CamelModel): + access_token: str + token_type: str = "bearer" + role: str + user_id: int + + +class UserOut(CamelModel): + id: int + email: EmailStr + full_name: str + role: str + cohort_id: int | None = None diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/schemas/base.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/schemas/base.py new file mode 100644 index 00000000..0eda4fab --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/schemas/base.py @@ -0,0 +1,12 @@ +from pydantic import BaseModel, ConfigDict +from pydantic.alias_generators import to_camel + + +class CamelModel(BaseModel): + """Base for every schema: Python stays snake_case, JSON on the wire is camelCase.""" + + model_config = ConfigDict( + alias_generator=to_camel, + populate_by_name=True, + from_attributes=True, + ) diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/schemas/progress.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/schemas/progress.py new file mode 100644 index 00000000..6b83cd70 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/schemas/progress.py @@ -0,0 +1,47 @@ +from datetime import datetime + +from pydantic import Field + +from app.models.enums import UrgencyLevel +from app.schemas.base import CamelModel +from app.schemas.task import SubtaskOut + + +class SubtaskToggle(CamelModel): + is_completed: bool + + +class SubtaskToggleOut(CamelModel): + id: int + is_completed: bool + + +class ProgressUpdateCreate(CamelModel): + percentage: int = Field(ge=0, le=100) + comment: str | None = None + + +class ProgressUpdateOut(CamelModel): + task_id: int + percentage: int + comment: str | None + created_at: datetime + + +class StudentTaskListItem(CamelModel): + task_id: int + title: str + due_date: datetime | None + urgency: UrgencyLevel + current_percentage: int + subtask_summary: str + + +class StudentTaskDetail(CamelModel): + id: int + title: str + description: str | None + due_date: datetime | None + urgency: UrgencyLevel + subtasks: list[SubtaskOut] + current_percentage: int diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/schemas/task.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/schemas/task.py new file mode 100644 index 00000000..541a85e1 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/schemas/task.py @@ -0,0 +1,95 @@ +from datetime import datetime + +from pydantic import Field, model_validator + +from app.models.enums import UrgencyLevel +from app.schemas.base import CamelModel + + +class SubtaskCreate(CamelModel): + title: str + order_index: int = 0 + + +class SubtaskUpdate(CamelModel): + title: str | None = None + order_index: int | None = None + + +class SubtaskOut(CamelModel): + id: int + title: str + order_index: int + is_completed: bool = False + + +class TaskCreate(CamelModel): + title: str + description: str | None = None + due_date: datetime | None = None + urgency: UrgencyLevel = UrgencyLevel.medium + cohort_id: int | None = None + trainee_ids: list[int] | None = None + subtasks: list[SubtaskCreate] = Field(default_factory=list) + + @model_validator(mode="after") + def check_exactly_one_target(self) -> "TaskCreate": + if bool(self.cohort_id) == bool(self.trainee_ids): + raise ValueError("provide exactly one of cohortId or traineeIds") + return self + + +class TaskUpdate(CamelModel): + title: str | None = None + description: str | None = None + due_date: datetime | None = None + urgency: UrgencyLevel | None = None + + +class TaskOut(CamelModel): + id: int + title: str + description: str | None + due_date: datetime | None + urgency: UrgencyLevel + assigned_trainee_count: int + + +class TaskDetailOut(CamelModel): + id: int + title: str + description: str | None + due_date: datetime | None + urgency: UrgencyLevel + subtasks: list[SubtaskOut] + + +class TraineeProgressSummary(CamelModel): + trainee_id: int + full_name: str + current_percentage: int + last_updated_at: datetime | None + + +class TaskProgressOut(CamelModel): + task_id: int + average_percentage: float + trainees: list[TraineeProgressSummary] + + +class ProgressHistoryEntry(CamelModel): + percentage: int + comment: str | None + created_at: datetime + + +class TraineeRef(CamelModel): + id: int + full_name: str + + +class TraineeProgressDetailOut(CamelModel): + trainee: TraineeRef + current_percentage: int + subtasks: list[SubtaskOut] + history: list[ProgressHistoryEntry] diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/services/__init__.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/services/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/services/auth_service.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/services/auth_service.py new file mode 100644 index 00000000..1b20430c --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/services/auth_service.py @@ -0,0 +1,19 @@ +from sqlalchemy.orm import Session + +from app.core.security import create_access_token, verify_password +from app.models.user import User +from app.repositories.user_repository import UserRepository + + +class AuthService: + def __init__(self, db: Session): + self.users = UserRepository(db) + + def authenticate(self, email: str, password: str) -> User | None: + user = self.users.get_by_email(email) + if user is None or not verify_password(password, user.hashed_password): + return None + return user + + def issue_token(self, user: User) -> str: + return create_access_token(subject=user.id, role=user.role.value) diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/services/progress_service.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/services/progress_service.py new file mode 100644 index 00000000..ba4ee000 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/services/progress_service.py @@ -0,0 +1,97 @@ +from datetime import datetime, timezone + +from fastapi import HTTPException, status +from sqlalchemy.orm import Session + +from app.models.task_assignment import TaskAssignment +from app.repositories.progress_repository import ProgressRepository +from app.repositories.subtask_repository import SubtaskRepository +from app.repositories.task_assignment_repository import TaskAssignmentRepository + +_URGENCY_RANK = {"urgent": 0, "high": 1, "medium": 2, "low": 3} + + +class ProgressService: + def __init__(self, db: Session): + self.db = db + self.subtasks = SubtaskRepository(db) + self.assignments = TaskAssignmentRepository(db) + self.progress = ProgressRepository(db) + + def _get_own_assignment(self, trainee_id: int, task_id: int) -> TaskAssignment: + assignment = self.assignments.get_by_task_and_trainee(task_id, trainee_id) + if assignment is None: + raise HTTPException(status.HTTP_404_NOT_FOUND, "Task not assigned to this trainee") + return assignment + + def list_student_tasks(self, trainee_id: int, sort: str | None = None) -> list[dict]: + assignments = self.assignments.list_by_trainee(trainee_id) + items = [] + for a in assignments: + task = a.task + subtasks = self.subtasks.list_by_task(task.id) + completions = self.progress.list_completions_by_task([s.id for s in subtasks], trainee_id) + done = sum(1 for s in subtasks if completions.get(s.id, False)) + items.append( + { + "task_id": task.id, + "title": task.title, + "due_date": task.due_date, + "urgency": task.urgency, + "current_percentage": a.current_percentage, + "subtask_summary": f"{done}/{len(subtasks)} completed", + } + ) + if sort == "urgency": + items.sort(key=lambda item: _URGENCY_RANK.get(item["urgency"].value, 4)) + return items + + def get_student_task_detail(self, trainee_id: int, task_id: int) -> dict: + assignment = self._get_own_assignment(trainee_id, task_id) + task = assignment.task + subtasks = self.subtasks.list_by_task(task_id) + completions = self.progress.list_completions_by_task([s.id for s in subtasks], trainee_id) + return { + "id": task.id, + "title": task.title, + "description": task.description, + "due_date": task.due_date, + "urgency": task.urgency, + "subtasks": [ + { + "id": s.id, + "title": s.title, + "order_index": s.order_index, + "is_completed": completions.get(s.id, False), + } + for s in subtasks + ], + "current_percentage": assignment.current_percentage, + } + + def toggle_subtask(self, trainee_id: int, subtask_id: int, is_completed: bool) -> dict: + subtask = self.subtasks.get_by_id(subtask_id) + if subtask is None: + raise HTTPException(status.HTTP_404_NOT_FOUND, "Subtask not found") + self._get_own_assignment(trainee_id, subtask.task_id) + completion = self.progress.set_completion(subtask_id, trainee_id, is_completed) + self.db.commit() + return {"id": subtask_id, "is_completed": completion.is_completed} + + def submit_progress_update(self, trainee_id: int, task_id: int, percentage: int, comment: str | None) -> dict: + assignment = self._get_own_assignment(trainee_id, task_id) + entry = self.progress.add_history_entry(assignment.id, percentage, comment) + assignment.current_percentage = percentage + assignment.last_updated_at = datetime.now(timezone.utc) + self.db.commit() + return { + "task_id": task_id, + "percentage": entry.percentage, + "comment": entry.comment, + "created_at": entry.created_at, + } + + def get_progress_history(self, trainee_id: int, task_id: int) -> list[dict]: + assignment = self._get_own_assignment(trainee_id, task_id) + history = self.progress.list_history(assignment.id) + return [{"percentage": h.percentage, "comment": h.comment, "created_at": h.created_at} for h in history] diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/services/task_assignment_factory.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/services/task_assignment_factory.py new file mode 100644 index 00000000..fa3b9c96 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/services/task_assignment_factory.py @@ -0,0 +1,14 @@ +from app.models.task_assignment import TaskAssignment +from app.models.user import User + + +class TaskAssignmentFactory: + """Builds the per-trainee task_assignments fan-out for a newly created task.""" + + @staticmethod + def build_for_cohort(task_id: int, trainees: list[User]) -> list[TaskAssignment]: + return [TaskAssignment(task_id=task_id, trainee_id=trainee.id) for trainee in trainees] + + @staticmethod + def build_for_trainee_ids(task_id: int, trainee_ids: list[int]) -> list[TaskAssignment]: + return [TaskAssignment(task_id=task_id, trainee_id=trainee_id) for trainee_id in trainee_ids] diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/services/task_service.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/services/task_service.py new file mode 100644 index 00000000..ccb75b69 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/services/task_service.py @@ -0,0 +1,121 @@ +from fastapi import HTTPException, status +from sqlalchemy.orm import Session + +from app.models.subtask import Subtask +from app.models.task import Task +from app.repositories.progress_repository import ProgressRepository +from app.repositories.subtask_repository import SubtaskRepository +from app.repositories.task_assignment_repository import TaskAssignmentRepository +from app.repositories.task_repository import TaskRepository +from app.repositories.user_repository import UserRepository +from app.schemas.task import TaskCreate, TaskUpdate +from app.services.task_assignment_factory import TaskAssignmentFactory + + +class TaskService: + def __init__(self, db: Session): + self.db = db + self.tasks = TaskRepository(db) + self.subtasks = SubtaskRepository(db) + self.assignments = TaskAssignmentRepository(db) + self.users = UserRepository(db) + self.progress = ProgressRepository(db) + + def create_task(self, manager_id: int, payload: TaskCreate) -> Task: + task = Task( + title=payload.title, + description=payload.description, + created_by=manager_id, + cohort_id=payload.cohort_id, + due_date=payload.due_date, + urgency=payload.urgency, + ) + self.tasks.create(task) + + for item in payload.subtasks: + self.subtasks.create(Subtask(task_id=task.id, title=item.title, order_index=item.order_index)) + + if payload.cohort_id: + trainees = self.users.list_by_cohort(payload.cohort_id) + assignments = TaskAssignmentFactory.build_for_cohort(task.id, trainees) + else: + assignments = TaskAssignmentFactory.build_for_trainee_ids(task.id, payload.trainee_ids or []) + + self.assignments.bulk_create(assignments) + self.db.commit() + self.db.refresh(task) + return task + + def get_owned_task(self, manager_id: int, task_id: int) -> Task: + task = self.tasks.get_by_id(task_id) + if task is None or task.created_by != manager_id: + raise HTTPException(status.HTTP_404_NOT_FOUND, "Task not found") + return task + + def list_manager_tasks(self, manager_id: int) -> list[Task]: + return self.tasks.list_by_manager(manager_id) + + def update_task(self, manager_id: int, task_id: int, payload: TaskUpdate) -> Task: + task = self.get_owned_task(manager_id, task_id) + for field, value in payload.model_dump(exclude_unset=True).items(): + setattr(task, field, value) + self.db.commit() + self.db.refresh(task) + return task + + def delete_task(self, manager_id: int, task_id: int) -> None: + task = self.get_owned_task(manager_id, task_id) + self.tasks.delete(task) + self.db.commit() + + def add_subtask(self, manager_id: int, task_id: int, title: str, order_index: int) -> Subtask: + self.get_owned_task(manager_id, task_id) + subtask = self.subtasks.create(Subtask(task_id=task_id, title=title, order_index=order_index)) + self.db.commit() + self.db.refresh(subtask) + return subtask + + def get_task_progress_summary(self, manager_id: int, task_id: int) -> dict: + self.get_owned_task(manager_id, task_id) + rows = self.assignments.list_by_task(task_id) + average = sum(row.current_percentage for row in rows) / len(rows) if rows else 0.0 + return { + "task_id": task_id, + "average_percentage": round(average, 1), + "trainees": [ + { + "trainee_id": row.trainee_id, + "full_name": row.trainee.full_name, + "current_percentage": row.current_percentage, + "last_updated_at": row.last_updated_at, + } + for row in rows + ], + } + + def get_trainee_progress_detail(self, manager_id: int, task_id: int, trainee_id: int) -> dict: + self.get_owned_task(manager_id, task_id) + assignment = self.assignments.get_by_task_and_trainee(task_id, trainee_id) + if assignment is None: + raise HTTPException(status.HTTP_404_NOT_FOUND, "Trainee is not assigned to this task") + + subtasks = self.subtasks.list_by_task(task_id) + completions = self.progress.list_completions_by_task([s.id for s in subtasks], trainee_id) + history = self.progress.list_history(assignment.id) + + return { + "trainee": {"id": assignment.trainee.id, "full_name": assignment.trainee.full_name}, + "current_percentage": assignment.current_percentage, + "subtasks": [ + { + "id": s.id, + "title": s.title, + "order_index": s.order_index, + "is_completed": completions.get(s.id, False), + } + for s in subtasks + ], + "history": [ + {"percentage": h.percentage, "comment": h.comment, "created_at": h.created_at} for h in history + ], + } diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/requirements.txt b/workshops/fullstack-aws/projects/submission/Group3/backend/requirements.txt new file mode 100644 index 00000000..6996c1cb --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/requirements.txt @@ -0,0 +1,10 @@ +fastapi>=0.115 +uvicorn[standard]>=0.30 +sqlalchemy>=2.0 +psycopg2-binary>=2.9 +pydantic>=2.7 +pydantic-settings>=2.3 +python-jose[cryptography]>=3.3 +passlib[bcrypt]>=1.7 +python-multipart>=0.0.9 +alembic>=1.13 From b8c1e55311726331bfae7470145ad9aa1228f868 Mon Sep 17 00:00:00 2001 From: BrendanGeary2020 <94076430+BrendanGeary2020@users.noreply.github.com> Date: Fri, 28 Aug 2026 12:59:07 -0400 Subject: [PATCH 09/28] update --- .../task-board-frontend/package-lock.json | 70 ++++++++++ .../frontend/task-board-frontend/package.json | 1 + .../frontend/task-board-frontend/src/App.jsx | 14 +- .../src/components/TraineeHeader.jsx | 4 +- .../src/components/TraineeSidebar.css | 124 ++++++++++++++++++ .../src/components/TraineeSidebar.jsx | 67 ++++++++++ .../src/store/TraineeStore.js | 13 +- .../task-board-frontend/src/store/store.js | 4 +- 8 files changed, 285 insertions(+), 12 deletions(-) create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeSidebar.css create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeSidebar.jsx diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/package-lock.json b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/package-lock.json index ca2647f1..2974b106 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/package-lock.json +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "task-board-frontend", "version": "0.0.0", "dependencies": { + "@reduxjs/toolkit": "^2.12.0", "react": "^19.2.8", "react-dom": "^19.2.8", "react-redux": "^9.3.0" @@ -353,6 +354,32 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rolldown/binding-android-arm-eabi": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.6.tgz", @@ -615,6 +642,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.18", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", @@ -721,6 +760,16 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/immer": { + "version": "11.1.18", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.18.tgz", + "integrity": "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/lightningcss": { "version": "1.33.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", @@ -1143,6 +1192,27 @@ } } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.3.0.tgz", + "integrity": "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg==", + "license": "MIT" + }, "node_modules/rolldown": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.6.tgz", diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/package.json b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/package.json index e14bca68..537f42e8 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/package.json +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@reduxjs/toolkit": "^2.12.0", "react": "^19.2.8", "react-dom": "^19.2.8", "react-redux": "^9.3.0" diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.jsx b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.jsx index 1a56439b..630755f3 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.jsx +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.jsx @@ -1,10 +1,18 @@ import TraineeHeader from "./components/TraineeHeader"; +import TraineeSidebar from "./components/TraineeSidebar"; function App() { return ( -
- -

hello

+
+ + +
+ + +
+

hello

+
+
); } diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeHeader.jsx b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeHeader.jsx index 5f1e8517..9368e0cb 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeHeader.jsx +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeHeader.jsx @@ -3,7 +3,7 @@ import styles from "./TraineeHeader.module.css"; import { useSelector } from "react-redux"; export default function TraineeHeader() { - const traineeName = useSelector((state) => state.TraineeSlice.name); + const traineeName = useSelector((state) => state.trainee.name); return (
@@ -25,4 +25,4 @@ export default function TraineeHeader() {
); -} +} \ No newline at end of file diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeSidebar.css b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeSidebar.css new file mode 100644 index 00000000..8f7f8a10 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeSidebar.css @@ -0,0 +1,124 @@ +.trainee-sidebar { + width: 260px; + height: 100vh; + background: #111827; + color: white; + display: flex; + flex-direction: column; + padding: 24px 16px; + box-sizing: border-box; +} + +.sidebar-header { + padding: 0 12px 24px; + border-bottom: 1px solid #374151; +} + +.sidebar-header h2 { + margin: 0; + font-size: 22px; + font-weight: 700; + text-transform: capitalize; +} + +/* Trainee profile */ + +.trainee-profile { + display: flex; + align-items: center; + gap: 12px; + padding: 24px 12px; + border-bottom: 1px solid #374151; +} + +.trainee-avatar { + width: 42px; + height: 42px; + border-radius: 50%; + background: #2563eb; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + font-weight: 700; +} + +.trainee-details { + min-width: 0; +} + +.trainee-details h3 { + margin: 0; + font-size: 15px; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.trainee-details span { + display: block; + margin-top: 3px; + color: #9ca3af; + font-size: 13px; +} + +/* Navigation */ + +.sidebar-navigation { + flex: 1; + padding-top: 24px; +} + +.navigation-title { + margin: 0 12px 10px; + color: #6b7280; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.08em; +} + +.sidebar-link { + width: 100%; + display: flex; + align-items: center; + gap: 12px; + padding: 11px 12px; + margin-bottom: 5px; + border-radius: 8px; + color: #d1d5db; + text-decoration: none; + font-size: 14px; + box-sizing: border-box; + transition: background 0.2s ease, color 0.2s ease; +} + +.sidebar-link span:first-child { + width: 20px; + text-align: center; +} + +.sidebar-link:hover { + background: #1f2937; + color: white; +} + +.sidebar-link.active { + background: #2563eb; + color: white; +} + +/* Bottom section */ + +.sidebar-bottom { + border-top: 1px solid #374151; + padding-top: 16px; +} + +.logout-button { + background: transparent; + border: none; + cursor: pointer; + font-family: inherit; + text-align: left; +} \ No newline at end of file diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeSidebar.jsx b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeSidebar.jsx new file mode 100644 index 00000000..db22dca9 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeSidebar.jsx @@ -0,0 +1,67 @@ +import { useSelector } from "react-redux"; +import "./TraineeSidebar.css"; + +const TraineeSidebar = () => { + const traineeName = useSelector((state) => state.trainee.name); + + return ( + + ); +}; + +export default TraineeSidebar; diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/TraineeStore.js b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/TraineeStore.js index dd2c50be..5b974d7c 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/TraineeStore.js +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/TraineeStore.js @@ -1,10 +1,14 @@ import { createSlice } from "@reduxjs/toolkit"; +const initialState = { + name: "Test Trainee", +}; + const TraineeSlice = createSlice({ - name: "TraineeSlice", - initialState: { - name: "test", - }, + name: "trainee", + + initialState, + reducers: { setName: (state, action) => { state.name = action.payload; @@ -13,4 +17,5 @@ const TraineeSlice = createSlice({ }); export const { setName } = TraineeSlice.actions; + export default TraineeSlice.reducer; diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/store.js b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/store.js index c88d47eb..bd0fa813 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/store.js +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/store.js @@ -1,12 +1,10 @@ import { configureStore } from "@reduxjs/toolkit"; - import TraineeSlice from "./TraineeStore"; export const Store = configureStore({ reducer: { - TraineeSlice: TraineeSlice, + trainee: TraineeSlice, }, - middleware: (getDefault) => getDefault(), }); export default Store; From 9392a123c75533357102817229791c01bc9717e5 Mon Sep 17 00:00:00 2001 From: kunzhutich Date: Fri, 28 Aug 2026 10:18:24 -0700 Subject: [PATCH 10/28] seeding data --- .../submission/Group3/backend/.gitignore | 4 ++ .../Group3/backend/scripts/__init__.py | 0 .../Group3/backend/scripts/seed_dev_data.py | 54 +++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/.gitignore create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/scripts/__init__.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/scripts/seed_dev_data.py diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/.gitignore b/workshops/fullstack-aws/projects/submission/Group3/backend/.gitignore new file mode 100644 index 00000000..17cd2fc3 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/.gitignore @@ -0,0 +1,4 @@ +.venv/ +__pycache__/ +*.pyc +.pytest_cache/ diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/scripts/__init__.py b/workshops/fullstack-aws/projects/submission/Group3/backend/scripts/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/scripts/seed_dev_data.py b/workshops/fullstack-aws/projects/submission/Group3/backend/scripts/seed_dev_data.py new file mode 100644 index 00000000..c03c8a15 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/scripts/seed_dev_data.py @@ -0,0 +1,54 @@ +"""Seeds a manager, a cohort, and two trainees for local testing. + +There's no signup endpoint yet (accounts are manager/admin-created), so this +script is the stand-in until that exists. Run with the venv active and .env +configured, from the backend/ directory: + + python -m scripts.seed_dev_data +""" +from app.core.database import SessionLocal +from app.core.security import hash_password +from app.models.cohort import Cohort +from app.models.enums import UserRole +from app.models.user import User + + +def run() -> None: + db = SessionLocal() + try: + manager = User( + email="manager@noticeboard.test", + hashed_password=hash_password("Manager123!"), + full_name="Morgan Manager", + role=UserRole.manager, + ) + db.add(manager) + db.flush() + + cohort = Cohort(name="Cohort Alpha", description="Test cohort", manager_id=manager.id) + db.add(cohort) + db.flush() + + trainees = [ + User( + email=f"trainee{i}@noticeboard.test", + hashed_password=hash_password("Trainee123!"), + full_name=f"Trainee {i}", + role=UserRole.trainee, + cohort_id=cohort.id, + ) + for i in range(1, 3) + ] + db.add_all(trainees) + db.commit() + + print("Seeded:") + print(" manager -> manager@noticeboard.test / Manager123!") + print(f" trainee 1 -> trainee1@noticeboard.test / Trainee123! (cohort_id={cohort.id})") + print(f" trainee 2 -> trainee2@noticeboard.test / Trainee123! (cohort_id={cohort.id})") + finally: + db.close() + + +if __name__ == "__main__": + run() From 2ee39f69754507d6a56b9ac187503c470d9f54c4 Mon Sep 17 00:00:00 2001 From: BrendanGeary2020 <94076430+BrendanGeary2020@users.noreply.github.com> Date: Fri, 28 Aug 2026 13:48:10 -0400 Subject: [PATCH 11/28] update --- .../frontend/task-board-frontend/src/App.css | 51 +++++++++++++++++++ .../frontend/task-board-frontend/src/App.jsx | 3 +- 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.css diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.css b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.css new file mode 100644 index 00000000..90126a16 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.css @@ -0,0 +1,51 @@ +/* Main application layout */ + +.app-layout { + display: flex; + width: 100%; + min-height: 100vh; + background: #f8fafc; +} + +/* Right side of the sidebar */ + +.app-main { + flex: 1; + min-width: 0; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* Main page content */ + +.main-content { + flex: 1; + padding: 32px; + background: #f8fafc; + box-sizing: border-box; +} + +/* Simple content styling */ + +.main-content p { + margin: 0; + font-size: 18px; + color: #334155; +} + +/* Mobile */ + +@media (max-width: 768px) { + .app-layout { + flex-direction: column; + } + + .app-main { + width: 100%; + } + + .main-content { + padding: 20px; + } +} \ No newline at end of file diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.jsx b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.jsx index 630755f3..798ebd84 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.jsx +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.jsx @@ -1,5 +1,6 @@ import TraineeHeader from "./components/TraineeHeader"; import TraineeSidebar from "./components/TraineeSidebar"; +import "./App.css"; function App() { return ( @@ -17,4 +18,4 @@ function App() { ); } -export default App; +export default App; \ No newline at end of file From 8b27a9941980f420ef4a10ce9682e3e47a28dd8e Mon Sep 17 00:00:00 2001 From: kunzhutich Date: Fri, 28 Aug 2026 11:03:22 -0700 Subject: [PATCH 12/28] moved infra folder out so that it can capture FE as well --- .../submission/Group3/backend/alembic/env.py | 4 +- .../Group3/backend/lambda_handler.py | 5 ++ .../Group3/backend/requirements.txt | 1 + .../submission/Group3/infra/.gitignore | 3 + .../projects/submission/Group3/infra/build.sh | 32 +++++++ .../projects/submission/Group3/infra/main.tf | 86 +++++++++++++++++++ .../submission/Group3/infra/outputs.tf | 4 + .../Group3/infra/requirements-lambda.txt | 13 +++ .../Group3/infra/terraform.tfvars.example | 7 ++ .../submission/Group3/infra/variables.tf | 35 ++++++++ .../submission/Group3/infra/zip_package.py | 22 +++++ 11 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/lambda_handler.py create mode 100644 workshops/fullstack-aws/projects/submission/Group3/infra/.gitignore create mode 100644 workshops/fullstack-aws/projects/submission/Group3/infra/build.sh create mode 100644 workshops/fullstack-aws/projects/submission/Group3/infra/main.tf create mode 100644 workshops/fullstack-aws/projects/submission/Group3/infra/outputs.tf create mode 100644 workshops/fullstack-aws/projects/submission/Group3/infra/requirements-lambda.txt create mode 100644 workshops/fullstack-aws/projects/submission/Group3/infra/terraform.tfvars.example create mode 100644 workshops/fullstack-aws/projects/submission/Group3/infra/variables.tf create mode 100644 workshops/fullstack-aws/projects/submission/Group3/infra/zip_package.py diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/alembic/env.py b/workshops/fullstack-aws/projects/submission/Group3/backend/alembic/env.py index 6296e632..7366706e 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/backend/alembic/env.py +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/alembic/env.py @@ -11,7 +11,9 @@ if config.config_file_name is not None: fileConfig(config.config_file_name) -config.set_main_option("sqlalchemy.url", get_settings().database_url) +# configparser treats "%" as interpolation syntax (URL-encoded passwords hit +# this), so escape it before handing the URL to alembic's Config. +config.set_main_option("sqlalchemy.url", get_settings().database_url.replace("%", "%%")) target_metadata = Base.metadata diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/lambda_handler.py b/workshops/fullstack-aws/projects/submission/Group3/backend/lambda_handler.py new file mode 100644 index 00000000..a6a2d6fe --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/lambda_handler.py @@ -0,0 +1,5 @@ +from mangum import Mangum + +from app.main import app + +handler = Mangum(app) diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/requirements.txt b/workshops/fullstack-aws/projects/submission/Group3/backend/requirements.txt index 6996c1cb..418e3e01 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/backend/requirements.txt +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/requirements.txt @@ -6,5 +6,6 @@ pydantic>=2.7 pydantic-settings>=2.3 python-jose[cryptography]>=3.3 passlib[bcrypt]>=1.7 +bcrypt<4.0 python-multipart>=0.0.9 alembic>=1.13 diff --git a/workshops/fullstack-aws/projects/submission/Group3/infra/.gitignore b/workshops/fullstack-aws/projects/submission/Group3/infra/.gitignore new file mode 100644 index 00000000..82ea96ca --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/infra/.gitignore @@ -0,0 +1,3 @@ +# vendored Lambda deps + zip, terraform local state - regenerate, don't commit +build/ +.terraform/ diff --git a/workshops/fullstack-aws/projects/submission/Group3/infra/build.sh b/workshops/fullstack-aws/projects/submission/Group3/infra/build.sh new file mode 100644 index 00000000..a7668cb9 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/infra/build.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Builds infra/build/lambda_package.zip. Run from anywhere; paths are +# resolved relative to this script. Requires Python 3.11 + pip. +set -euo pipefail + +INFRA_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BACKEND_DIR="$(cd "$INFRA_DIR/../backend" && pwd)" +BUILD_DIR="$INFRA_DIR/build" +PACKAGE_DIR="$BUILD_DIR/package" + +echo "Cleaning previous build..." +rm -rf "$PACKAGE_DIR" +mkdir -p "$PACKAGE_DIR" + +echo "Installing dependencies for Lambda (manylinux2014_x86_64, cp311)..." +python -m pip install \ + --platform manylinux2014_x86_64 \ + --implementation cp \ + --python-version 3.11 \ + --only-binary=:all: \ + --target "$PACKAGE_DIR" \ + -r "$INFRA_DIR/requirements-lambda.txt" + +echo "Copying application code..." +cp -r "$BACKEND_DIR/app" "$PACKAGE_DIR/app" +cp "$BACKEND_DIR/lambda_handler.py" "$PACKAGE_DIR/lambda_handler.py" +find "$PACKAGE_DIR" -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true + +echo "Zipping..." +python "$INFRA_DIR/zip_package.py" "$PACKAGE_DIR" "$BUILD_DIR/lambda_package.zip" + +echo "Done: infra/build/lambda_package.zip" diff --git a/workshops/fullstack-aws/projects/submission/Group3/infra/main.tf b/workshops/fullstack-aws/projects/submission/Group3/infra/main.tf new file mode 100644 index 00000000..27cce29c --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/infra/main.tf @@ -0,0 +1,86 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.0" + } + } +} + +provider "aws" { + region = var.aws_region +} + +# This lab account doesn't allow creating OR reading IAM roles - it provides +# one shared Lambda execution role instead, referenced directly by ARN +# (a data source lookup would need iam:GetRole, which is also denied). + +# --- Lambda --- +# The zip is produced by infra/build.sh before `terraform apply`. +# Secrets aren't in Secrets Manager (not permitted in this account) - they're +# passed directly as Lambda environment variables via Terraform variables, +# which are themselves passed in via TF_VAR_* so they're never hardcoded here. +# +# DATABASE_URL must point at a publicly reachable Postgres (Neon/Supabase). +# An RDS instance sitting in a VPC would need this Lambda placed in that VPC +# too (subnets, security groups) - out of scope here, and likely blocked by +# the same "no IAM/network resource creation" restriction on this account. + +locals { + allowed_origins_list = split(",", var.allowed_origins) +} + +resource "aws_lambda_function" "api" { + function_name = "${var.project_name}-api" + role = var.lambda_exec_role_arn + handler = "lambda_handler.handler" + runtime = "python3.11" + filename = "${path.module}/build/lambda_package.zip" + source_code_hash = filebase64sha256("${path.module}/build/lambda_package.zip") + timeout = 15 + memory_size = 256 + + environment { + variables = { + DATABASE_URL = var.database_url + JWT_SECRET_KEY = var.jwt_secret_key + JWT_ALGORITHM = "HS256" + ACCESS_TOKEN_EXPIRE_MINUTES = "60" + CORS_ORIGINS = jsonencode(local.allowed_origins_list) + } + } +} + +# --- API Gateway (HTTP API - simpler/cheaper than REST API for a full proxy) --- + +resource "aws_apigatewayv2_api" "http_api" { + name = "${var.project_name}-api" + protocol_type = "HTTP" +} + +resource "aws_apigatewayv2_integration" "lambda" { + api_id = aws_apigatewayv2_api.http_api.id + integration_type = "AWS_PROXY" + integration_uri = aws_lambda_function.api.invoke_arn + payload_format_version = "2.0" +} + +resource "aws_apigatewayv2_route" "default" { + api_id = aws_apigatewayv2_api.http_api.id + route_key = "$default" + target = "integrations/${aws_apigatewayv2_integration.lambda.id}" +} + +resource "aws_apigatewayv2_stage" "default" { + api_id = aws_apigatewayv2_api.http_api.id + name = "$default" + auto_deploy = true +} + +resource "aws_lambda_permission" "apigw" { + statement_id = "AllowAPIGatewayInvoke" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.api.function_name + principal = "apigateway.amazonaws.com" + source_arn = "${aws_apigatewayv2_api.http_api.execution_arn}/*/*" +} diff --git a/workshops/fullstack-aws/projects/submission/Group3/infra/outputs.tf b/workshops/fullstack-aws/projects/submission/Group3/infra/outputs.tf new file mode 100644 index 00000000..e800987a --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/infra/outputs.tf @@ -0,0 +1,4 @@ +output "api_endpoint" { + description = "Base URL of the deployed API" + value = aws_apigatewayv2_stage.default.invoke_url +} diff --git a/workshops/fullstack-aws/projects/submission/Group3/infra/requirements-lambda.txt b/workshops/fullstack-aws/projects/submission/Group3/infra/requirements-lambda.txt new file mode 100644 index 00000000..32c484d4 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/infra/requirements-lambda.txt @@ -0,0 +1,13 @@ +# Mirrors ../requirements.txt, minus uvicorn (local dev server only, Mangum +# replaces it inside Lambda) and alembic (migrations run locally against the +# DB before a deploy, never inside the Lambda itself). +fastapi>=0.115 +pydantic>=2.7 +pydantic-settings>=2.3 +sqlalchemy>=2.0 +psycopg2-binary>=2.9 +python-jose[cryptography]>=3.3 +passlib[bcrypt]>=1.7 +bcrypt<4.0 +python-multipart>=0.0.9 +mangum>=0.17 diff --git a/workshops/fullstack-aws/projects/submission/Group3/infra/terraform.tfvars.example b/workshops/fullstack-aws/projects/submission/Group3/infra/terraform.tfvars.example new file mode 100644 index 00000000..7e7172d8 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/infra/terraform.tfvars.example @@ -0,0 +1,7 @@ +aws_region = "us-east-1" +project_name = "noticeboardtracker" + +# Do not put real secrets in a committed tfvars file. +# Set these as environment variables instead, e.g. in PowerShell: +# $env:TF_VAR_database_url = "postgresql://user:pass@host:5432/noticeboardtracker" +# $env:TF_VAR_jwt_secret_key = "" diff --git a/workshops/fullstack-aws/projects/submission/Group3/infra/variables.tf b/workshops/fullstack-aws/projects/submission/Group3/infra/variables.tf new file mode 100644 index 00000000..030fd96c --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/infra/variables.tf @@ -0,0 +1,35 @@ +variable "aws_region" { + description = "AWS region to deploy into" + type = string + default = "us-east-1" +} + +variable "project_name" { + description = "Name prefix for created resources" + type = string + default = "noticeboardtracker" +} + +variable "database_url" { + description = "Postgres connection string (Neon/Supabase - must be reachable over the public internet, see note in main.tf)" + type = string + sensitive = true +} + +variable "jwt_secret_key" { + description = "Secret key used to sign JWTs" + type = string + sensitive = true +} + +variable "allowed_origins" { + description = "Comma-separated list of origins allowed by CORS" + type = string + default = "http://localhost:5173,http://127.0.0.1:5173" +} + +variable "lambda_exec_role_arn" { + description = "ARN of the pre-existing IAM role Lambda should run as (this account doesn't allow creating or reading IAM roles)" + type = string + default = "arn:aws:iam::279249498881:role/quicklabs-fullstack-shared-lambda-exec" +} diff --git a/workshops/fullstack-aws/projects/submission/Group3/infra/zip_package.py b/workshops/fullstack-aws/projects/submission/Group3/infra/zip_package.py new file mode 100644 index 00000000..5398c742 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/infra/zip_package.py @@ -0,0 +1,22 @@ +import sys +import zipfile +from pathlib import Path + + +def main() -> None: + src_dir = Path(sys.argv[1]) + dest_zip = Path(sys.argv[2]) + dest_zip.parent.mkdir(parents=True, exist_ok=True) + if dest_zip.exists(): + dest_zip.unlink() + + with zipfile.ZipFile(dest_zip, "w", zipfile.ZIP_DEFLATED) as zf: + for file_path in src_dir.rglob("*"): + if file_path.is_file(): + zf.write(file_path, file_path.relative_to(src_dir)) + + print(f"Wrote {dest_zip}") + + +if __name__ == "__main__": + main() From 4633442ceb951eb6f0cf113cc9fae746d3e2604a Mon Sep 17 00:00:00 2001 From: BrendanGeary2020 <94076430+BrendanGeary2020@users.noreply.github.com> Date: Fri, 28 Aug 2026 14:10:58 -0400 Subject: [PATCH 13/28] trainer side bar --- .../src/components/TrainerSidebar.jsx | 101 +++++++++ .../src/components/TrainerSidebar.module.css | 205 ++++++++++++++++++ .../src/store/TrainerStore.js | 21 ++ .../task-board-frontend/src/store/store.js | 3 + 4 files changed, 330 insertions(+) create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TrainerSidebar.jsx create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TrainerSidebar.module.css create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/TrainerStore.js diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TrainerSidebar.jsx b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TrainerSidebar.jsx new file mode 100644 index 00000000..7398b76a --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TrainerSidebar.jsx @@ -0,0 +1,101 @@ +import { useSelector } from "react-redux"; +import styles from "./TrainerSidebar.module.css"; + +export default function TrainerSidebar() { + const trainerName = useSelector((state) => state.trainer.name); + + const initial = trainerName + ? trainerName.charAt(0).toUpperCase() + : "T"; + + return ( + + ); +} diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TrainerSidebar.module.css b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TrainerSidebar.module.css new file mode 100644 index 00000000..3fe7a677 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TrainerSidebar.module.css @@ -0,0 +1,205 @@ +.sidebar { + width: 260px; + min-width: 260px; + height: 100vh; + background: #111827; + color: #ffffff; + display: flex; + flex-direction: column; + box-sizing: border-box; + padding: 24px 16px; + position: sticky; + top: 0; +} + +/* -------------------------------- + Logo +-------------------------------- */ + +.logo { + display: flex; + align-items: center; + gap: 12px; + padding: 0 8px 24px; + border-bottom: 1px solid #374151; +} + +.logoMark { + width: 40px; + height: 40px; + border-radius: 10px; + background: #2563eb; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + font-weight: 700; +} + +.logo h2 { + margin: 0; + font-size: 17px; + font-weight: 700; +} + +.logo span { + display: block; + margin-top: 2px; + color: #9ca3af; + font-size: 12px; +} + +/* -------------------------------- + Trainer Profile +-------------------------------- */ + +.profile { + display: flex; + align-items: center; + gap: 12px; + padding: 22px 8px; + border-bottom: 1px solid #374151; +} + +.avatar { + width: 42px; + height: 42px; + flex-shrink: 0; + border-radius: 50%; + background: #2563eb; + display: flex; + align-items: center; + justify-content: center; + font-size: 17px; + font-weight: 700; +} + +.profileInfo { + min-width: 0; +} + +.profileInfo h3 { + margin: 0; + color: #ffffff; + font-size: 14px; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.profileInfo span { + display: block; + margin-top: 3px; + color: #9ca3af; + font-size: 12px; +} + +/* -------------------------------- + Navigation +-------------------------------- */ + +.navigation { + flex: 1; + padding-top: 24px; +} + +.sectionTitle { + margin: 0 8px 10px; + color: #6b7280; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.1em; +} + +.navItem { + width: 100%; + min-height: 44px; + display: flex; + align-items: center; + gap: 12px; + padding: 0 12px; + margin-bottom: 5px; + border-radius: 8px; + box-sizing: border-box; + color: #d1d5db; + text-decoration: none; + font-size: 14px; + transition: + background-color 0.2s ease, + color 0.2s ease; +} + +.navItem:hover { + background: #1f2937; + color: #ffffff; +} + +.navItem.active { + background: #2563eb; + color: #ffffff; +} + +.icon { + width: 20px; + min-width: 20px; + text-align: center; + font-size: 16px; +} + +/* -------------------------------- + Bottom Navigation +-------------------------------- */ + +.bottomNavigation { + border-top: 1px solid #374151; + padding-top: 16px; +} + +.logoutButton { + width: 100%; + min-height: 44px; + display: flex; + align-items: center; + gap: 12px; + padding: 0 12px; + border: 0; + border-radius: 8px; + background: transparent; + color: #d1d5db; + font-family: inherit; + font-size: 14px; + text-align: left; + cursor: pointer; + transition: + background-color 0.2s ease, + color 0.2s ease; +} + +.logoutButton:hover { + background: #1f2937; + color: #ffffff; +} + +/* -------------------------------- + Mobile +-------------------------------- */ + +@media (max-width: 768px) { + .sidebar { + width: 100%; + min-width: 100%; + height: auto; + min-height: auto; + position: relative; + padding: 16px; + } + + .navigation { + padding-top: 16px; + } + + .bottomNavigation { + margin-top: 12px; + } +} diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/TrainerStore.js b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/TrainerStore.js new file mode 100644 index 00000000..86d40b94 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/TrainerStore.js @@ -0,0 +1,21 @@ +import { createSlice } from "@reduxjs/toolkit"; + +const initialState = { + name: "Test Trainer", +}; + +const TrainerSlice = createSlice({ + name: "trainer", + + initialState, + + reducers: { + setName: (state, action) => { + state.name = action.payload; + }, + }, +}); + +export const { setName } = TrainerSlice.actions; + +export default TrainerSlice.reducer; \ No newline at end of file diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/store.js b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/store.js index bd0fa813..72e23753 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/store.js +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/store.js @@ -1,9 +1,12 @@ import { configureStore } from "@reduxjs/toolkit"; + import TraineeSlice from "./TraineeStore"; +import TrainerSlice from "./TrainerStore"; export const Store = configureStore({ reducer: { trainee: TraineeSlice, + trainer: TrainerSlice, }, }); From 96cda40502917f15e10409650146f866611b9e64 Mon Sep 17 00:00:00 2001 From: kunzhutich Date: Fri, 28 Aug 2026 11:13:58 -0700 Subject: [PATCH 14/28] updated dependencies --- .../projects/submission/Group3/backend/requirements.txt | 2 +- .../projects/submission/Group3/infra/requirements-lambda.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/requirements.txt b/workshops/fullstack-aws/projects/submission/Group3/backend/requirements.txt index 418e3e01..e6057915 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/backend/requirements.txt +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/requirements.txt @@ -2,7 +2,7 @@ fastapi>=0.115 uvicorn[standard]>=0.30 sqlalchemy>=2.0 psycopg2-binary>=2.9 -pydantic>=2.7 +pydantic[email]>=2.7 pydantic-settings>=2.3 python-jose[cryptography]>=3.3 passlib[bcrypt]>=1.7 diff --git a/workshops/fullstack-aws/projects/submission/Group3/infra/requirements-lambda.txt b/workshops/fullstack-aws/projects/submission/Group3/infra/requirements-lambda.txt index 32c484d4..bba81447 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/infra/requirements-lambda.txt +++ b/workshops/fullstack-aws/projects/submission/Group3/infra/requirements-lambda.txt @@ -2,7 +2,7 @@ # replaces it inside Lambda) and alembic (migrations run locally against the # DB before a deploy, never inside the Lambda itself). fastapi>=0.115 -pydantic>=2.7 +pydantic[email]>=2.7 pydantic-settings>=2.3 sqlalchemy>=2.0 psycopg2-binary>=2.9 From 388509ea17bbfedfcfe749e61a4d2e5d2b1a9269 Mon Sep 17 00:00:00 2001 From: Connor Vandrush Date: Fri, 28 Aug 2026 14:45:09 -0400 Subject: [PATCH 15/28] Work in progress --- package-lock.json | 141 ++++++++ .../submission/Group3/backend/.env.example | 5 - .../backend/app/api/v1/student_tasks.py | 61 +++- .../frontend/task-board-frontend/src/App.jsx | 29 +- .../src/components/Login.jsx | 152 ++++++++ .../src/components/Login.module.css | 95 +++++ .../src/components/TraineeTasks.jsx | 331 ++++++++++++++++++ .../src/components/TraineeTasks.module.css | 196 +++++++++++ .../src/store/TraineeTasksSlice.js | 58 +++ .../task-board-frontend/src/store/store.js | 2 + 10 files changed, 1057 insertions(+), 13 deletions(-) create mode 100644 package-lock.json delete mode 100644 workshops/fullstack-aws/projects/submission/Group3/backend/.env.example create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/Login.jsx create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/Login.module.css create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeTasks.jsx create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeTasks.module.css create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/TraineeTasksSlice.js diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..0e51da77 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,141 @@ +{ + "name": "workshops", + "lockfileVersion": 3, + "requires": true, + "packages": { + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/immer": { + "version": "11.1.18", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.18.tgz", + "integrity": "sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reselect": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.3.0.tgz", + "integrity": "sha512-XGoLeRAVzUTcJ1qkxPQhDJyIZ5d6zzZD9nT7AEZOaaU9UbWclhycElmhO+VD5bFeLuzhPBaOV2oXC8uG35ZSpg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + } + } +} diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/.env.example b/workshops/fullstack-aws/projects/submission/Group3/backend/.env.example deleted file mode 100644 index 60f829a9..00000000 --- a/workshops/fullstack-aws/projects/submission/Group3/backend/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -DATABASE_URL=postgresql://user:password@host:5432/noticeboardtracker -JWT_SECRET_KEY=changeme -JWT_ALGORITHM=HS256 -ACCESS_TOKEN_EXPIRE_MINUTES=60 -CORS_ORIGINS=["http://localhost:5173"] diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/student_tasks.py b/workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/student_tasks.py index 76912403..658fe8f7 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/student_tasks.py +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/app/api/v1/student_tasks.py @@ -14,6 +14,35 @@ ) from app.services.progress_service import ProgressService + +# ============================================================ +# STUDENT / TRAINEE ENDPOINTS +# +# Base prefix: /student +# +# GET /student/tasks +# Get all tasks assigned to the current trainee. +# +# GET /student/tasks/{task_id} +# Get details for a specific task. +# +# PATCH /student/subtasks/{subtask_id} +# Toggle a subtask's completion status. +# +# POST /student/tasks/{task_id}/progress +# Submit a progress update for a task. +# +# GET /student/tasks/{task_id}/progress/history +# Get the progress history for a task. +# +# NOTE: +# The application may add an additional prefix such as +# /api/v1 when this router is included in main.py. +# +# Example: +# /api/v1/student/tasks +# ============================================================ + router = APIRouter(prefix="/student", tags=["student"]) @@ -28,7 +57,9 @@ def list_tasks( @router.get("/tasks/{task_id}", response_model=StudentTaskDetail) def task_detail( - task_id: int, db: Session = Depends(get_db), trainee: User = Depends(get_current_trainee) + task_id: int, + db: Session = Depends(get_db), + trainee: User = Depends(get_current_trainee), ) -> dict: return ProgressService(db).get_student_task_detail(trainee.id, task_id) @@ -40,22 +71,40 @@ def toggle_subtask( db: Session = Depends(get_db), trainee: User = Depends(get_current_trainee), ) -> dict: - return ProgressService(db).toggle_subtask(trainee.id, subtask_id, payload.is_completed) + return ProgressService(db).toggle_subtask( + trainee.id, + subtask_id, + payload.is_completed, + ) -@router.post("/tasks/{task_id}/progress", response_model=ProgressUpdateOut, status_code=status.HTTP_201_CREATED) +@router.post( + "/tasks/{task_id}/progress", + response_model=ProgressUpdateOut, + status_code=status.HTTP_201_CREATED, +) def submit_progress( task_id: int, payload: ProgressUpdateCreate, db: Session = Depends(get_db), trainee: User = Depends(get_current_trainee), ) -> dict: - return ProgressService(db).submit_progress_update(trainee.id, task_id, payload.percentage, payload.comment) + return ProgressService(db).submit_progress_update( + trainee.id, + task_id, + payload.percentage, + payload.comment, + ) -@router.get("/tasks/{task_id}/progress/history", response_model=list[ProgressUpdateOut]) +@router.get( + "/tasks/{task_id}/progress/history", + response_model=list[ProgressUpdateOut], +) def progress_history( - task_id: int, db: Session = Depends(get_db), trainee: User = Depends(get_current_trainee) + task_id: int, + db: Session = Depends(get_db), + trainee: User = Depends(get_current_trainee), ) -> list[ProgressUpdateOut]: history = ProgressService(db).get_progress_history(trainee.id, task_id) return [ProgressUpdateOut(task_id=task_id, **entry) for entry in history] diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.jsx b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.jsx index 798ebd84..77ef47e2 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.jsx +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.jsx @@ -1,8 +1,33 @@ +import { useState } from "react"; + +import Login from "./components/Login"; import TraineeHeader from "./components/TraineeHeader"; import TraineeSidebar from "./components/TraineeSidebar"; +import TraineeTasks from "./components/TraineeTasks"; + import "./App.css"; function App() { + const [isLoggedIn, setIsLoggedIn] = useState( + !!localStorage.getItem("access_token"), + ); + + function handleLogin() { + setIsLoggedIn(true); + } + + function handleLogout() { + localStorage.removeItem("access_token"); + localStorage.removeItem("role"); + localStorage.removeItem("user_id"); + + setIsLoggedIn(false); + } + + if (!isLoggedIn) { + return ; + } + return (
@@ -11,11 +36,11 @@ function App() {
-

hello

+
); } -export default App; \ No newline at end of file +export default App; diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/Login.jsx b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/Login.jsx new file mode 100644 index 00000000..a5f61460 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/Login.jsx @@ -0,0 +1,152 @@ +import { useState } from "react"; +import styles from "./Login.module.css"; + +const API_BASE_URL = "http://127.0.0.1:8000/api"; + +export default function Login({ onLogin }) { + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + async function handleSubmit(event) { + event.preventDefault(); + + setLoading(true); + setError(null); + + try { + const response = await fetch(`${API_BASE_URL}/auth/login`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email, + password, + }), + }); + + const data = await response.json(); + + // Handle FastAPI validation errors + if (!response.ok) { + if (Array.isArray(data.detail)) { + const messages = data.detail + .map((validationError) => { + const field = + validationError.loc?.[validationError.loc.length - 1]; + + const message = validationError.msg || "Invalid value"; + + return field ? `${field}: ${message}` : message; + }) + .join(", "); + + throw new Error(messages); + } + + // Handle normal FastAPI errors + if (typeof data.detail === "string") { + throw new Error(data.detail); + } + + throw new Error(`Login failed with status ${response.status}`); + } + + // Make sure the backend actually returned a token + if (!data.access_token) { + throw new Error("Login succeeded, but no access token was returned."); + } + + // Store authentication information + localStorage.setItem("access_token", data.access_token); + + localStorage.setItem("role", data.role); + + localStorage.setItem("user_id", data.user_id); + + console.log("Login successful"); + + // Tell App.jsx that the user is now logged in + if (onLogin) { + onLogin(); + } + } catch (error) { + console.error("Login failed:", error); + + setError(error instanceof Error ? error.message : "Unable to log in."); + } finally { + setLoading(false); + } + } + + return ( +
+
+ {/* Header */} + +
+

C.B.M.H.

+ +

Welcome Back

+ +

Sign in to access your trainee task board.

+
+ + {/* Login Form */} + +
+ {/* Email */} + +
+ + + setEmail(event.target.value)} + placeholder="Enter your email" + autoComplete="email" + required + /> +
+ + {/* Password */} + +
+ + + setPassword(event.target.value)} + placeholder="Enter your password" + autoComplete="current-password" + required + /> +
+ + {/* Error */} + + {error && ( +
+ {error} +
+ )} + + {/* Submit */} + + +
+
+
+ ); +} diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/Login.module.css b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/Login.module.css new file mode 100644 index 00000000..69df1438 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/Login.module.css @@ -0,0 +1,95 @@ +.loginContainer { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; + background: #f4f5f3; +} + +.loginCard { + width: 100%; + max-width: 420px; + padding: 2.5rem; + border: 1px solid #ddd; + border-radius: 12px; + background: #fff; + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08); +} + +.header { + margin-bottom: 2rem; + text-align: center; +} + +.label { + margin: 0 0 0.5rem; + color: #2f6f4e; + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.1em; +} + +.header h1 { + margin: 0 0 0.5rem; + color: #222; +} + +.header p:last-child { + margin: 0; + color: #777; +} + +.inputGroup { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-bottom: 1.25rem; +} + +.inputGroup label { + color: #333; + font-weight: 600; +} + +.inputGroup input { + padding: 0.8rem; + border: 1px solid #ccc; + border-radius: 6px; + font-size: 1rem; +} + +.inputGroup input:focus { + outline: none; + border-color: #2f6f4e; +} + +button { + width: 100%; + padding: 0.85rem; + border: none; + border-radius: 6px; + background: #2f6f4e; + color: white; + font-size: 1rem; + font-weight: 600; + cursor: pointer; +} + +button:hover { + background: #245a3e; +} + +button:disabled { + cursor: not-allowed; + opacity: 0.6; +} + +.error { + margin-bottom: 1rem; + padding: 0.75rem; + border: 1px solid #e57373; + border-radius: 6px; + background: #ffebee; + color: #b71c1c; +} diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeTasks.jsx b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeTasks.jsx new file mode 100644 index 00000000..c6c9b8b1 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeTasks.jsx @@ -0,0 +1,331 @@ +import { useEffect, useState } from "react"; +import styles from "./TraineeTasks.module.css"; + +const API_BASE_URL = "http://127.0.0.1:8000/api"; + +function getAuthHeaders() { + const token = localStorage.getItem("access_token"); + + return { + Authorization: `Bearer ${token}`, + }; +} + +export default function TraineeTasks() { + const [tasks, setTasks] = useState([]); + const [selectedTask, setSelectedTask] = useState(null); + const [progressHistory, setProgressHistory] = useState([]); + + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // ============================================================ + // GET /api/student/tasks + // ============================================================ + + useEffect(() => { + async function fetchTasks() { + try { + setLoading(true); + setError(null); + + const token = localStorage.getItem("access_token"); + + if (!token) { + throw new Error("You are not logged in."); + } + + const response = await fetch(`${API_BASE_URL}/student/tasks`, { + method: "GET", + headers: getAuthHeaders(), + }); + + if (response.status === 401) { + throw new Error("Your session has expired. Please log in again."); + } + + if (!response.ok) { + throw new Error(`Failed to fetch tasks: ${response.status}`); + } + + const data = await response.json(); + + setTasks(data); + } catch (error) { + console.error("Error fetching trainee tasks:", error); + + setError(error.message); + } finally { + setLoading(false); + } + } + + fetchTasks(); + }, []); + + // ============================================================ + // GET /api/student/tasks/{task_id} + // ============================================================ + + async function fetchTask(taskId) { + try { + setError(null); + setSelectedTask(null); + setProgressHistory([]); + + const token = localStorage.getItem("access_token"); + + if (!token) { + throw new Error("You are not logged in."); + } + + const response = await fetch(`${API_BASE_URL}/student/tasks/${taskId}`, { + method: "GET", + headers: getAuthHeaders(), + }); + + if (response.status === 401) { + throw new Error("Your session has expired. Please log in again."); + } + + if (!response.ok) { + throw new Error(`Failed to fetch task: ${response.status}`); + } + + const data = await response.json(); + + setSelectedTask(data); + + await fetchProgressHistory(taskId); + } catch (error) { + console.error("Error fetching task:", error); + + setError(error.message); + } + } + + // ============================================================ + // GET /api/student/tasks/{task_id}/progress/history + // ============================================================ + + async function fetchProgressHistory(taskId) { + try { + const token = localStorage.getItem("access_token"); + + if (!token) { + throw new Error("You are not logged in."); + } + + const response = await fetch( + `${API_BASE_URL}/student/tasks/${taskId}/progress/history`, + { + method: "GET", + headers: getAuthHeaders(), + }, + ); + + if (response.status === 401) { + throw new Error("Your session has expired. Please log in again."); + } + + if (!response.ok) { + throw new Error(`Failed to fetch progress history: ${response.status}`); + } + + const data = await response.json(); + + setProgressHistory(data); + } catch (error) { + console.error("Error fetching progress history:", error); + + setError(error.message); + } + } + + // ============================================================ + // Loading + // ============================================================ + + if (loading) { + return ( +
+

Loading trainee tasks...

+
+ ); + } + + // ============================================================ + // Error + // ============================================================ + + if (error) { + return ( +
+

Unable to load tasks

+ +

{error}

+
+ ); + } + + // ============================================================ + // Render + // ============================================================ + + return ( +
+ {/* ====================================================== + HEADER + ====================================================== */} + +
+
+

TRAINEE TASK BOARD

+ +

My Tasks

+
+ +

+ {tasks.length} {tasks.length === 1 ? "task" : "tasks"} +

+
+ + {/* ====================================================== + TASK LIST + ====================================================== */} + +
+ {tasks.length === 0 ? ( +
+

No tasks assigned

+ +

You currently don't have any tasks to complete.

+
+ ) : ( + tasks.map((task) => ( + + )) + )} +
+ + {/* ====================================================== + SELECTED TASK + ====================================================== */} + + {selectedTask && ( +
+
+
+

TASK DETAILS

+ +

{selectedTask.title || `Task ${selectedTask.id}`}

+
+ + {selectedTask.status && ( + {selectedTask.status} + )} +
+ + {/* Description */} + + {selectedTask.description && ( +

{selectedTask.description}

+ )} + + {/* Due Date */} + + {selectedTask.due_date && ( +
+ Due Date + + {selectedTask.due_date} +
+ )} + + {/* Progress */} + + {selectedTask.progress !== undefined && + selectedTask.progress !== null && ( +
+ Progress + + {selectedTask.progress}% +
+ )} + + {/* ================================================== + SUBTASKS + ================================================== */} + + {selectedTask.subtasks && selectedTask.subtasks.length > 0 && ( +
+

Subtasks

+ + {selectedTask.subtasks.map((subtask) => ( +
+ + + + {subtask.title || subtask.name || `Subtask ${subtask.id}`} + +
+ ))} +
+ )} + + {/* ================================================== + PROGRESS HISTORY + ================================================== */} + +
+

Progress History

+ + {progressHistory.length === 0 ? ( +

No progress updates yet.

+ ) : ( +
+ {progressHistory.map((update, index) => ( +
+ {update.percentage}% + + {update.comment && {update.comment}} + + {update.created_at && {update.created_at}} +
+ ))} +
+ )} +
+
+ )} +
+ ); +} diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeTasks.module.css b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeTasks.module.css new file mode 100644 index 00000000..f71f87d5 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeTasks.module.css @@ -0,0 +1,196 @@ +.container { + width: 100%; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 1.5rem; +} + +.label { + margin: 0 0 0.25rem; + color: #777; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.08em; +} + +.header h2 { + margin: 0; + color: #222; + font-size: 1.75rem; +} + +.taskCount { + margin: 0; + color: #666; +} + +.taskList { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1rem; +} + +.taskCard { + padding: 1.25rem; + border: 1px solid #ddd; + border-radius: 10px; + background: #fff; + text-align: left; + cursor: pointer; + transition: + border-color 0.2s, + box-shadow 0.2s, + transform 0.2s; +} + +.taskCard:hover { + border-color: #2f6f4e; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + transform: translateY(-2px); +} + +.taskCardHeader { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; +} + +.taskCard h3 { + margin: 0; + color: #222; + font-size: 1.05rem; +} + +.description { + margin: 0.75rem 0; + color: #666; + line-height: 1.5; +} + +.taskMeta { + display: flex; + gap: 1rem; + color: #777; + font-size: 0.85rem; +} + +.statusBadge { + flex-shrink: 0; + padding: 0.25rem 0.6rem; + border-radius: 999px; + background: #e8f3ec; + color: #2f6f4e; + font-size: 0.75rem; + font-weight: 600; +} + +.selectedTask { + margin-top: 2rem; + padding: 1.5rem; + border: 1px solid #ddd; + border-radius: 10px; + background: #fff; +} + +.selectedHeader { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.selectedHeader h2 { + margin: 0; +} + +.subtasks { + margin-top: 1.5rem; +} + +.subtasks h3, +.progress h3 { + margin-bottom: 0.75rem; +} + +.subtask { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem; + border-bottom: 1px solid #eee; +} + +.subtask input { + width: 1rem; + height: 1rem; +} + +.progress { + margin-top: 1.5rem; +} + +.progressList { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.progressItem { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.75rem; + border-radius: 6px; + background: #f7f7f7; +} + +.progressItem strong { + color: #2f6f4e; +} + +.progressItem span { + color: #555; +} + +.progressItem small { + margin-left: auto; + color: #888; +} + +.empty { + padding: 3rem; + border: 1px dashed #ccc; + border-radius: 10px; + text-align: center; +} + +.empty h3 { + margin: 0 0 0.5rem; +} + +.empty p { + margin: 0; + color: #777; +} + +.status { + padding: 2rem; + text-align: center; +} + +.error { + padding: 1.5rem; + border: 1px solid #e57373; + border-radius: 8px; + background: #ffebee; + color: #b71c1c; +} + +.error h2 { + margin-top: 0; +} diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/TraineeTasksSlice.js b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/TraineeTasksSlice.js new file mode 100644 index 00000000..624ac3d4 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/TraineeTasksSlice.js @@ -0,0 +1,58 @@ +import { createSlice } from "@reduxjs/toolkit"; + +const initialState = { + tasks: [], + selectedTask: null, + progressHistory: [], + loading: false, + error: null, +}; + +const TraineeTaskSlice = createSlice({ + name: "TraineeTaskSlice", + + initialState, + + reducers: { + setTasks: (state, action) => { + state.tasks = action.payload; + }, + + setSelectedTask: (state, action) => { + state.selectedTask = action.payload; + }, + + setProgressHistory: (state, action) => { + state.progressHistory = action.payload; + }, + + setLoading: (state, action) => { + state.loading = action.payload; + }, + + setError: (state, action) => { + state.error = action.payload; + }, + + clearSelectedTask: (state) => { + state.selectedTask = null; + state.progressHistory = []; + }, + + clearError: (state) => { + state.error = null; + }, + }, +}); + +export const { + setTasks, + setSelectedTask, + setProgressHistory, + setLoading, + setError, + clearSelectedTask, + clearError, +} = TraineeTaskSlice.actions; + +export default TraineeTaskSlice.reducer; diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/store.js b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/store.js index bd0fa813..abe56c0b 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/store.js +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/store/store.js @@ -1,9 +1,11 @@ import { configureStore } from "@reduxjs/toolkit"; import TraineeSlice from "./TraineeStore"; +import TraineeTasksSlice from "./TraineeTasksSlice"; export const Store = configureStore({ reducer: { trainee: TraineeSlice, + traineeTasks: TraineeTasksSlice, }, }); From 206cd7264a8a25ad65cf045c2a16cd585fadd489 Mon Sep 17 00:00:00 2001 From: kunzhutich Date: Fri, 28 Aug 2026 11:53:02 -0700 Subject: [PATCH 16/28] Fix FE/BE field casing mismatch and reserved-TLD seed emails BE schemas serialize camelCase (accessToken, dueDate, isCompleted, etc.) via CamelModel, but the FE trainee components were reading snake_case keys that never existed in the response, so tokens/dates/progress/ checkboxes silently showed as empty. Also fixes task card click-through, which was reading task.id/status/progress/description - none of which exist on the student task list response (only taskId, urgency, currentPercentage, subtaskSummary do). Also renames seeded emails off the *.test domain, which email-validator rejects as an IANA reserved special-use TLD (RFC 2606), causing login to 500 with "special-use or reserved name" even with correct credentials. --- .../Group3/backend/scripts/seed_dev_data.py | 10 ++-- .../src/components/Login.jsx | 6 +-- .../src/components/TraineeTasks.jsx | 47 +++++++++---------- 3 files changed, 30 insertions(+), 33 deletions(-) diff --git a/workshops/fullstack-aws/projects/submission/Group3/backend/scripts/seed_dev_data.py b/workshops/fullstack-aws/projects/submission/Group3/backend/scripts/seed_dev_data.py index c03c8a15..00d4ab0e 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/backend/scripts/seed_dev_data.py +++ b/workshops/fullstack-aws/projects/submission/Group3/backend/scripts/seed_dev_data.py @@ -17,7 +17,7 @@ def run() -> None: db = SessionLocal() try: manager = User( - email="manager@noticeboard.test", + email="manager@noticeboardtracker.dev", hashed_password=hash_password("Manager123!"), full_name="Morgan Manager", role=UserRole.manager, @@ -31,7 +31,7 @@ def run() -> None: trainees = [ User( - email=f"trainee{i}@noticeboard.test", + email=f"trainee{i}@noticeboardtracker.dev", hashed_password=hash_password("Trainee123!"), full_name=f"Trainee {i}", role=UserRole.trainee, @@ -43,9 +43,9 @@ def run() -> None: db.commit() print("Seeded:") - print(" manager -> manager@noticeboard.test / Manager123!") - print(f" trainee 1 -> trainee1@noticeboard.test / Trainee123! (cohort_id={cohort.id})") - print(f" trainee 2 -> trainee2@noticeboard.test / Trainee123! (cohort_id={cohort.id})") + print(" manager -> manager@noticeboardtracker.dev / Manager123!") + print(f" trainee 1 -> trainee1@noticeboardtracker.dev / Trainee123! (cohort_id={cohort.id})") + print(f" trainee 2 -> trainee2@noticeboardtracker.dev / Trainee123! (cohort_id={cohort.id})") finally: db.close() diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/Login.jsx b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/Login.jsx index a5f61460..0cc29796 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/Login.jsx +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/Login.jsx @@ -56,16 +56,16 @@ export default function Login({ onLogin }) { } // Make sure the backend actually returned a token - if (!data.access_token) { + if (!data.accessToken) { throw new Error("Login succeeded, but no access token was returned."); } // Store authentication information - localStorage.setItem("access_token", data.access_token); + localStorage.setItem("access_token", data.accessToken); localStorage.setItem("role", data.role); - localStorage.setItem("user_id", data.user_id); + localStorage.setItem("user_id", data.userId); console.log("Login successful"); diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeTasks.jsx b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeTasks.jsx index c6c9b8b1..fba82002 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeTasks.jsx +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeTasks.jsx @@ -204,28 +204,27 @@ export default function TraineeTasks() { ) : ( tasks.map((task) => ( )) @@ -245,8 +244,8 @@ export default function TraineeTasks() {

{selectedTask.title || `Task ${selectedTask.id}`}

- {selectedTask.status && ( - {selectedTask.status} + {selectedTask.urgency && ( + {selectedTask.urgency} )} @@ -258,22 +257,22 @@ export default function TraineeTasks() { {/* Due Date */} - {selectedTask.due_date && ( + {selectedTask.dueDate && (
Due Date - {selectedTask.due_date} + {selectedTask.dueDate}
)} {/* Progress */} - {selectedTask.progress !== undefined && - selectedTask.progress !== null && ( + {selectedTask.currentPercentage !== undefined && + selectedTask.currentPercentage !== null && (
Progress - {selectedTask.progress}% + {selectedTask.currentPercentage}%
)} @@ -289,13 +288,11 @@ export default function TraineeTasks() {
- - {subtask.title || subtask.name || `Subtask ${subtask.id}`} - + {subtask.title || `Subtask ${subtask.id}`}
))} @@ -318,7 +315,7 @@ export default function TraineeTasks() { {update.comment && {update.comment}} - {update.created_at && {update.created_at}} + {update.createdAt && {update.createdAt}} ))} From 2e734e7d74fbf7ad3b4cb06e2c5125c2f4dbe2c7 Mon Sep 17 00:00:00 2001 From: kunzhutich Date: Fri, 28 Aug 2026 12:20:28 -0700 Subject: [PATCH 17/28] Implement manager (Trainer) dashboard: create tasks, view cohort progress Adds the manager-side POV to close out the MVP dashboards: - TrainerTasks.jsx: create-task form (title, description, due date, urgency, cohort id, subtasks), task list with delete, task progress panel (average % + per-trainee bars), and per-trainee drill-down (subtask checklist + progress history) - wired to the existing /manager/tasks and /manager/tasks/{id}/progress[/{trainee_id}] endpoints. - TrainerHeader.jsx: mirrors TraineeHeader for the manager view. - App.jsx: routes to Trainer* vs Trainee* components based on the logged-in user's role, and now calls GET /auth/me on login to populate the header/sidebar with the real name instead of the "Test Trainer"/"Test Trainee" placeholder. - TraineeSidebar.jsx / TrainerSidebar.jsx: wire the logout button to App's handleLogout (previously a no-op / console.log). Verified end-to-end with a headless-browser run: manager login -> create task -> task appears in list -> progress panel loads average + per-trainee bars -> trainee drill-down loads subtasks/history -> logout -> trainee login shows the new task with correct urgency/ progress/subtask-summary. Zero console or network errors across the whole flow. --- .../frontend/task-board-frontend/src/App.jsx | 62 +- .../src/components/TraineeSidebar.jsx | 8 +- .../src/components/TrainerHeader.jsx | 28 + .../src/components/TrainerHeader.module.css | 48 ++ .../src/components/TrainerSidebar.jsx | 4 +- .../src/components/TrainerTasks.jsx | 606 ++++++++++++++++++ .../src/components/TrainerTasks.module.css | 427 ++++++++++++ 7 files changed, 1175 insertions(+), 8 deletions(-) create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TrainerHeader.jsx create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TrainerHeader.module.css create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TrainerTasks.jsx create mode 100644 workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TrainerTasks.module.css diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.jsx b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.jsx index 77ef47e2..0f46439f 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.jsx +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/App.jsx @@ -1,19 +1,66 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; import Login from "./components/Login"; import TraineeHeader from "./components/TraineeHeader"; import TraineeSidebar from "./components/TraineeSidebar"; import TraineeTasks from "./components/TraineeTasks"; +import TrainerHeader from "./components/TrainerHeader"; +import TrainerSidebar from "./components/TrainerSidebar"; +import TrainerTasks from "./components/TrainerTasks"; + +import { setName as setTraineeName } from "./store/TraineeStore"; +import { setName as setTrainerName } from "./store/TrainerStore"; import "./App.css"; +const API_BASE_URL = "http://127.0.0.1:8000/api"; + function App() { + const dispatch = useDispatch(); + const [isLoggedIn, setIsLoggedIn] = useState( !!localStorage.getItem("access_token"), ); + const [role, setRole] = useState(localStorage.getItem("role")); + + // ============================================================ + // GET /api/auth/me - populate the header/sidebar with the + // logged-in user's real name instead of the "Test Trainee" / + // "Test Trainer" placeholder. + // ============================================================ + + useEffect(() => { + if (!isLoggedIn) return; + + async function fetchProfile() { + try { + const token = localStorage.getItem("access_token"); + + const response = await fetch(`${API_BASE_URL}/auth/me`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!response.ok) return; + + const data = await response.json(); + + if (data.role === "manager") { + dispatch(setTrainerName(data.fullName)); + } else { + dispatch(setTraineeName(data.fullName)); + } + } catch (err) { + console.error("Error fetching profile:", err); + } + } + + fetchProfile(); + }, [isLoggedIn, dispatch]); function handleLogin() { setIsLoggedIn(true); + setRole(localStorage.getItem("role")); } function handleLogout() { @@ -22,21 +69,28 @@ function App() { localStorage.removeItem("user_id"); setIsLoggedIn(false); + setRole(null); } if (!isLoggedIn) { return ; } + const isManager = role === "manager"; + return (
- + {isManager ? ( + + ) : ( + + )}
- + {isManager ? : }
- + {isManager ? : }
diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeSidebar.jsx b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeSidebar.jsx index db22dca9..d4b5ad61 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeSidebar.jsx +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TraineeSidebar.jsx @@ -1,7 +1,7 @@ import { useSelector } from "react-redux"; import "./TraineeSidebar.css"; -const TraineeSidebar = () => { +const TraineeSidebar = ({ onLogout }) => { const traineeName = useSelector((state) => state.trainee.name); return ( @@ -55,7 +55,11 @@ const TraineeSidebar = () => { Profile - diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TrainerHeader.jsx b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TrainerHeader.jsx new file mode 100644 index 00000000..4f0f3456 --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TrainerHeader.jsx @@ -0,0 +1,28 @@ +import styles from "./TrainerHeader.module.css"; + +import { useSelector } from "react-redux"; + +export default function TrainerHeader() { + const trainerName = useSelector((state) => state.trainer.name); + + return ( +
+
+

TRAINER PORTAL

+ +

+ Welcome to C.B.M.H. +

+ +

+ {trainerName ? `Welcome, ${trainerName}!` : "Welcome!"} +

+ +

+ Create tasks, track cohort progress, and see how each trainee is + doing at a glance. +

+
+
+ ); +} diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TrainerHeader.module.css b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TrainerHeader.module.css new file mode 100644 index 00000000..9aa0edee --- /dev/null +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TrainerHeader.module.css @@ -0,0 +1,48 @@ +.trainerHeader { + width: 100%; + padding: 2rem; + border-radius: 12px; + background-color: #ffffff; + border: 1px solid #e0e0e0; + box-sizing: border-box; +} + +.headerContent { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.welcomeLabel { + margin: 0; + color: #666666; + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.08em; +} + +.trainerHeader h1 { + margin: 0; + color: #222222; + font-size: 2rem; + font-weight: 700; +} + +.trainerHeader h1 span { + color: #2563eb; +} + +.trainerName { + margin: 0.25rem 0 0; + color: #333333; + font-size: 1.1rem; + font-weight: 600; +} + +.description { + max-width: 650px; + margin: 0.25rem 0 0; + color: #666666; + font-size: 0.95rem; + line-height: 1.5; +} diff --git a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TrainerSidebar.jsx b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TrainerSidebar.jsx index 7398b76a..2650b894 100644 --- a/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TrainerSidebar.jsx +++ b/workshops/fullstack-aws/projects/submission/Group3/frontend/task-board-frontend/src/components/TrainerSidebar.jsx @@ -1,7 +1,7 @@ import { useSelector } from "react-redux"; import styles from "./TrainerSidebar.module.css"; -export default function TrainerSidebar() { +export default function TrainerSidebar({ onLogout }) { const trainerName = useSelector((state) => state.trainer.name); const initial = trainerName @@ -90,7 +90,7 @@ export default function TrainerSidebar() {