diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/.gitignore b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/.gitignore
new file mode 100644
index 00000000..966a64d3
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/.gitignore
@@ -0,0 +1,10 @@
+__pycache__/
+.venv/
+node_modules
+*.db
+.env
+.terraform/
+terraform.tfstate
+terraform.tfstate.backup
+terraform.tfvars
+frontend/dist/
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/Citi Training Database.session.sql b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/Citi Training Database.session.sql
new file mode 100644
index 00000000..e69de29b
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/.env.example b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/.env.example
new file mode 100644
index 00000000..ad82eb26
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/.env.example
@@ -0,0 +1 @@
+VITE_API_URL=http://127.0.0.1:8000
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/.gitignore b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/.gitignore
new file mode 100644
index 00000000..b50664c7
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/.gitignore
@@ -0,0 +1,27 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+.env
+.env.*
+!.env.example
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/README.md b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/README.md
new file mode 100644
index 00000000..2d31cb68
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/README.md
@@ -0,0 +1,75 @@
+# React + TypeScript + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint 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 ESLint configuration
+
+If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
+
+```js
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ // Other configs...
+
+ // Remove tseslint.configs.recommended and replace with this
+ tseslint.configs.recommendedTypeChecked,
+ // Alternatively, use this for stricter rules
+ tseslint.configs.strictTypeChecked,
+ // Optionally, add this for stylistic rules
+ tseslint.configs.stylisticTypeChecked,
+
+ // Other configs...
+ ],
+ languageOptions: {
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ // other options...
+ },
+ },
+])
+
+```
+
+You can also install [eslint-plugin-react-x](https://npmx.dev/package/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://npmx.dev/package/eslint-plugin-react-dom) for React-specific lint rules:
+
+```js
+// eslint.config.js
+import reactX from 'eslint-plugin-react-x'
+import reactDom from 'eslint-plugin-react-dom'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ // Other configs...
+ // Enable lint rules for React
+ reactX.configs['recommended-typescript'],
+ // Enable lint rules for React DOM
+ reactDom.configs.recommended,
+ ],
+ languageOptions: {
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ // other options...
+ },
+ },
+])
+
+```
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/eslint.config.js b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/eslint.config.js
new file mode 100644
index 00000000..ef614d25
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/eslint.config.js
@@ -0,0 +1,22 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ globals: globals.browser,
+ },
+ },
+])
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/index.html b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/index.html
new file mode 100644
index 00000000..0fca6f04
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ frontend
+
+
+
+
+
+
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/package-lock.json b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/package-lock.json
new file mode 100644
index 00000000..36839cfa
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/package-lock.json
@@ -0,0 +1,3492 @@
+{
+ "name": "frontend",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "frontend",
+ "version": "0.0.0",
+ "dependencies": {
+ "axios": "^1.20.0",
+ "lucide-react": "^1.34.0",
+ "react": "^19.2.8",
+ "react-dom": "^19.2.8",
+ "react-router-dom": "^7.18.2",
+ "recharts": "^3.10.1"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "@types/node": "^24.13.3",
+ "@types/react": "^19.2.18",
+ "@types/react-dom": "^19.2.4",
+ "@vitejs/plugin-react": "^6.1.0",
+ "eslint": "^10.9.0",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "eslint-plugin-react-refresh": "^0.5.4",
+ "globals": "^17.11.0",
+ "prettier": "^3.9.6",
+ "typescript": "~6.0.2",
+ "typescript-eslint": "^8.67.0",
+ "vite": "^8.2.2"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
+ "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.8",
+ "@babel/types": "^7.29.8",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
+ "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.8"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",
+ "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.8",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.8",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.8",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.8",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
+ "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
+ "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.23.5",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
+ "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^3.0.5",
+ "debug": "^4.3.1",
+ "minimatch": "^10.2.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz",
+ "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
+ "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
+ "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "eslint": "^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
+ "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz",
+ "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.2.1",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "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/@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",
+ "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,
+ "libc": [
+ "glibc"
+ ],
+ "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,
+ "libc": [
+ "musl"
+ ],
+ "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,
+ "libc": [
+ "glibc"
+ ],
+ "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,
+ "libc": [
+ "glibc"
+ ],
+ "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,
+ "libc": [
+ "glibc"
+ ],
+ "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,
+ "libc": [
+ "musl"
+ ],
+ "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/@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/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/esrecurse": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
+ "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.13.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
+ "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.18.0"
+ }
+ },
+ "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==",
+ "devOptional": 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/@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/@typescript-eslint/eslint-plugin": {
+ "version": "8.68.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.68.0.tgz",
+ "integrity": "sha512-WASHDpCm6qO5jj9g1a+8NiW5+GCkAyLReR56/4VruYmNgfUmqpxOfZ2Yfb8xGfJPWv5Qi6LSD8sXdces3vbp/Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.68.0",
+ "@typescript-eslint/type-utils": "8.68.0",
+ "@typescript-eslint/utils": "8.68.0",
+ "@typescript-eslint/visitor-keys": "8.68.0",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.68.0",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
+ "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.68.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.68.0.tgz",
+ "integrity": "sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.68.0",
+ "@typescript-eslint/types": "8.68.0",
+ "@typescript-eslint/typescript-estree": "8.68.0",
+ "@typescript-eslint/visitor-keys": "8.68.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.68.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.68.0.tgz",
+ "integrity": "sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.68.0",
+ "@typescript-eslint/types": "^8.68.0",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.68.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.68.0.tgz",
+ "integrity": "sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.68.0",
+ "@typescript-eslint/visitor-keys": "8.68.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.68.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.68.0.tgz",
+ "integrity": "sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.68.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.68.0.tgz",
+ "integrity": "sha512-X77zqoY1EjeWGs/0JNxeaMfp5C5lIz4Tw8y66F1Ne8Faq6g424sBNYM6xBAqElfGZPLpWS+CZAp0DXyKDzWiHg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.68.0",
+ "@typescript-eslint/typescript-estree": "8.68.0",
+ "@typescript-eslint/utils": "8.68.0",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.68.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.68.0.tgz",
+ "integrity": "sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.68.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.68.0.tgz",
+ "integrity": "sha512-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.68.0",
+ "@typescript-eslint/tsconfig-utils": "8.68.0",
+ "@typescript-eslint/types": "8.68.0",
+ "@typescript-eslint/visitor-keys": "8.68.0",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.68.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.68.0.tgz",
+ "integrity": "sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.68.0",
+ "@typescript-eslint/types": "8.68.0",
+ "@typescript-eslint/typescript-estree": "8.68.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.68.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.68.0.tgz",
+ "integrity": "sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.68.0",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.0.tgz",
+ "integrity": "sha512-qd2BzUBehkov86WFhg0JkEFEYyCLG9uPCe6qWTY/kRlss9OvJrOF2UbIWT7p+8IzZHkEu0DNGHc4HSv+JdDLsw==",
+ "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/acorn": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
+ "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
+ "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.20.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz",
+ "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.16.0",
+ "form-data": "^4.0.6",
+ "https-proxy-agent": "^5.0.1",
+ "proxy-from-env": "^2.1.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.11.19",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.19.tgz",
+ "integrity": "sha512-Grytf1xOxOEMTGRwx6rLGKkTabd4vMg3VrKdj/7joCmV0qgh4QwMMO6xh34YEXQqirAuUdgQGa5orJQQ+69RBw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.9",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
+ "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.8",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz",
+ "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.11.12",
+ "caniuse-lite": "^1.0.30001809",
+ "electron-to-chromium": "^1.5.402",
+ "node-releases": "^2.0.53",
+ "update-browserslist-db": "^1.3.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001810",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
+ "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "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==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js-light": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
+ "license": "MIT"
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "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/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.415",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.415.tgz",
+ "integrity": "sha512-958V+Kbhtgz+SxXeEVKBjrlKRBIDAYvUJfwhjxMZ5S6ut9jAl7l9ZKBkBrvjyjZE36PabLUo2L8kEeV5O4vgJg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-toolkit": {
+ "version": "1.51.0",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.51.0.tgz",
+ "integrity": "sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw==",
+ "license": "MIT",
+ "workspaces": [
+ "docs",
+ "benchmarks",
+ "tests/types",
+ "tests/browser-compat"
+ ]
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "10.9.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.9.1.tgz",
+ "integrity": "sha512-9VaAkDURekixUQJy0oJYl2DcN6oKMfxay7XzaGYAWQwsb6qfKf+x76R2k1L8kb1boc+FyCAaTA9GmiKaaiaF+A==",
+ "dev": true,
+ "license": "MIT",
+ "workspaces": [
+ "packages/*"
+ ],
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@eslint/config-array": "^0.23.5",
+ "@eslint/config-helpers": "^0.7.0",
+ "@eslint/core": "^1.2.1",
+ "@eslint/plugin-kit": "^0.7.2",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^9.1.2",
+ "eslint-visitor-keys": "^5.0.1",
+ "espree": "^11.2.0",
+ "esquery": "^1.7.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "minimatch": "^10.2.5",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
+ "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.24.4",
+ "@babel/parser": "^7.24.4",
+ "hermes-parser": "^0.25.1",
+ "zod": "^3.25.0 || ^4.0.0",
+ "zod-validation-error": "^3.5.0 || ^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.5.5",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.5.tgz",
+ "integrity": "sha512-vG7yLURXNvCHy0FBdbZRwIu0BLPJMlUUJS2Ep7ud9w1YCLftFZtuEjyjhym0Qq9yuZ6LJUitNlu/hMk0gakXAw==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": "^9 || ^10"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
+ "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@types/esrecurse": "^4.3.1",
+ "@types/estree": "^1.0.8",
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "11.2.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
+ "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.16.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^5.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eventemitter3": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+ "license": "MIT"
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.4",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz",
+ "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
+ "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.4",
+ "mime-types": "^2.1.35"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "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/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "17.11.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.11.0.tgz",
+ "integrity": "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
+ "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "6",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "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/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.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,
+ "libc": [
+ "glibc"
+ ],
+ "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,
+ "libc": [
+ "musl"
+ ],
+ "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,
+ "libc": [
+ "glibc"
+ ],
+ "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,
+ "libc": [
+ "musl"
+ ],
+ "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/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "1.34.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.34.0.tgz",
+ "integrity": "sha512-vnjGJNI7Htk5+oWW8gXGuaLgwgAb0T6/iZbBrp9JCfRFwdNWZ0YTm3eyxjOLgwN6r8iyAf3UA70zNmBRBNv7yg==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
+ "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.8"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/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/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.53",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz",
+ "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.9.6",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz",
+ "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "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-is": {
+ "version": "19.2.8",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz",
+ "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==",
+ "license": "MIT",
+ "peer": true
+ },
+ "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/react-router": {
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz",
+ "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz",
+ "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.18.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
+ "node_modules/recharts": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.10.1.tgz",
+ "integrity": "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==",
+ "license": "MIT",
+ "workspaces": [
+ "www"
+ ],
+ "dependencies": {
+ "@reduxjs/toolkit": "^1.9.0 || 2.x.x",
+ "clsx": "^2.1.1",
+ "decimal.js-light": "^2.5.1",
+ "es-toolkit": "^1.39.3",
+ "eventemitter3": "^5.0.1",
+ "immer": "^11.1.8",
+ "react-redux": "8.x.x || 9.x.x",
+ "reselect": "5.2.0",
+ "tiny-invariant": "^1.3.3",
+ "use-sync-external-store": "^1.2.2",
+ "victory-vendor": "^37.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "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.2.0",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz",
+ "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==",
+ "license": "MIT"
+ },
+ "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/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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/tiny-invariant": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
+ "license": "MIT"
+ },
+ "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/ts-api-utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.68.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.68.0.tgz",
+ "integrity": "sha512-MHy0Y0ynqeEbx/S45+i/bBssdy3X6KNBfmJAP35GrgtNxu2TQ5K5xsFDhAnmsq1jvpdoZOPG1LGtJo0HWqYCrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.68.0",
+ "@typescript-eslint/parser": "8.68.0",
+ "@typescript-eslint/typescript-estree": "8.68.0",
+ "@typescript-eslint/utils": "8.68.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz",
+ "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "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/victory-vendor": {
+ "version": "37.3.6",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
+ "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
+ "license": "MIT AND ISC",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ },
+ "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
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
+ "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ }
+ }
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/package.json b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/package.json
new file mode 100644
index 00000000..19c8c8cf
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "axios": "^1.20.0",
+ "lucide-react": "^1.34.0",
+ "react": "^19.2.8",
+ "react-dom": "^19.2.8",
+ "react-router-dom": "^7.18.2",
+ "recharts": "^3.10.1"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "@types/node": "^24.13.3",
+ "@types/react": "^19.2.18",
+ "@types/react-dom": "^19.2.4",
+ "@vitejs/plugin-react": "^6.1.0",
+ "eslint": "^10.9.0",
+ "eslint-plugin-react-hooks": "^7.1.1",
+ "eslint-plugin-react-refresh": "^0.5.4",
+ "globals": "^17.11.0",
+ "prettier": "^3.9.6",
+ "typescript": "~6.0.2",
+ "typescript-eslint": "^8.67.0",
+ "vite": "^8.2.2"
+ }
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/public/favicon.svg b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/public/favicon.svg
new file mode 100644
index 00000000..6893eb13
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/public/icons.svg b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/public/icons.svg
new file mode 100644
index 00000000..e9522193
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/public/icons.svg
@@ -0,0 +1,24 @@
+
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/App.css b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/App.css
new file mode 100644
index 00000000..464c73f9
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/App.css
@@ -0,0 +1,49 @@
+.app-shell { min-height: 100vh; background: var(--bg); color: var(--text); }
+.topbar { height: 76px; display: flex; align-items: center; gap: 22px; padding: 0 5vw; border-bottom: 1px solid var(--border); background: rgba(255,255,255,.86); position: sticky; top: 0; z-index: 5; backdrop-filter: blur(14px); }
+.brand { display: flex; align-items: center; gap: 10px; color: var(--text-h); font-family: var(--heading); font-weight: 700; letter-spacing: .02em; }.brand svg { color: var(--accent); }
+.topbar-actions { display: flex; align-items: center; gap: 18px; margin-left: auto; }.icon-button { display: inline-grid; place-items: center; border: 0; background: transparent; color: var(--text-h); cursor: pointer; padding: 8px; border-radius: 8px; }.icon-button:hover { background: var(--accent-bg); }
+.signout-button, .mode-toggle, .open-account-actions button, .edit-status, .full-width-button { border: 0; cursor: pointer; font: inherit; }.signout-button { display: flex; align-items: center; gap: 8px; color: var(--text-h); background: transparent; font-size: 14px; }.mode-toggle { padding: 8px 12px; border-radius: 6px; background: var(--accent-bg); color: var(--accent); font-size: 13px; font-weight: 700; }
+.page-content { max-width: 1120px; margin: 0 auto; padding: 74px 5vw 100px; }.welcome-row, .section-heading, .modal-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; }.modal-owner { margin: 4px 0 0; color: var(--text); font-size: 13px; font-weight: 700; }.eyebrow { margin: 0 0 10px; color: var(--accent); font-size: 11px; font-weight: 800; letter-spacing: .14em; }h1, h2, h3 { color: var(--text-h); }h1 { margin: 0; font-size: clamp(2.2rem, 5vw, 4.2rem); line-height: 1.04; letter-spacing: -.05em; }h2 { margin: 0; font-size: 26px; letter-spacing: -.03em; }h3 { margin: 0; }.subtitle { margin-top: 14px; font-size: 16px; }
+.profile-chip { display: flex; align-items: center; gap: 11px; padding: 10px 14px 10px 10px; border: 1px solid var(--border); border-radius: 10px; background: var(--surface); }.profile-chip > span { display: grid; place-items: center; width: 35px; height: 35px; border-radius: 50%; background: var(--accent); color: white; font-size: 12px; font-weight: 800; }.profile-chip div { display: grid; gap: 1px; color: var(--text-h); font-size: 13px; }.profile-chip small { color: var(--text); font-size: 11px; }
+.account-section { margin-top: 76px; }.account-count { font-size: 13px; color: var(--text); padding-top: 8px; }.account-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin-top: 22px; }.account-card, .open-account-card { min-height: 235px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface); overflow: hidden; box-shadow: 0 14px 30px rgba(25, 31, 44, .06); }.account-card-main { display: flex; flex-direction: column; align-items: flex-start; gap: 0; width: 100%; min-height: 235px; padding: 24px; border: 0; color: inherit; text-align: left; background: transparent; cursor: pointer; }.account-card-main:hover { background: var(--accent-bg); }.card-topline { display: flex; justify-content: space-between; width: 100%; color: var(--text-h); }.account-type { display: flex; align-items: center; gap: 7px; font-size: 13px; font-weight: 800; }.status-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--green); }.balance { margin-top: auto; color: var(--text-h); font-size: 30px; letter-spacing: -.04em; }.balance-label, .account-number { color: var(--text); font-size: 12px; }.account-owner { color: var(--text-h); font-size: 13px; font-weight: 700; margin-top: 6px; }.account-number { margin-top: 22px; letter-spacing: .15em; }.status-label { margin-top: 14px; font-size: 11px; font-weight: 800; }.status-active { color: var(--green); }.status-inactive { color: var(--red); }.account-closed { opacity: .76; }.account-closed .status-dot { background: var(--red); }
+.open-account-card { display: flex; flex-direction: column; align-items: flex-start; padding: 24px; border-style: dashed; background: linear-gradient(135deg, var(--surface), #eef7f4); }.plus-mark { display: grid; place-items: center; width: 36px; height: 36px; border-radius: 8px; color: var(--accent); background: var(--accent-bg); }.open-account-card h3 { margin-top: 22px; font-size: 18px; }.open-account-card p { margin-top: 7px; font-size: 13px; line-height: 1.5; }.open-account-actions { display: flex; gap: 8px; margin-top: auto; }.open-account-actions button { display: flex; align-items: center; gap: 4px; padding: 8px 10px; border-radius: 6px; background: var(--text-h); color: white; font-size: 12px; font-weight: 700; }
+.edit-status { display: flex; justify-content: center; align-items: center; gap: 7px; width: 100%; padding: 12px; background: #f3f5f6; color: var(--text-h); font-size: 12px; font-weight: 700; }.edit-status:hover { background: var(--accent-bg); color: var(--accent); }.admin-note { display: flex; align-items: flex-start; gap: 13px; margin-top: 28px; padding: 17px 20px; border-left: 3px solid var(--accent); background: var(--accent-bg); color: var(--text-h); }.admin-note svg { color: var(--accent); }.admin-note strong { font-size: 14px; }.admin-note p { margin-top: 3px; font-size: 13px; }
+.activity-preview { margin-top: 74px; max-width: 700px; }.activity-preview a { color: var(--accent); font-size: 13px; font-weight: 800; text-decoration: none; padding-top: 7px; }.transaction-list { margin-top: 18px; border-top: 1px solid var(--border); }.transaction-row { display: flex; align-items: center; gap: 12px; padding: 15px 0; border-bottom: 1px solid var(--border); }.transaction-actions { display: flex; align-items: center; gap: 12px; margin-left: auto; }.transaction-actions > strong { color: var(--red); font-size: 14px; }.transaction-actions > .credit { color: var(--green); }.transaction-icon { display: grid; place-items: center; width: 34px; height: 34px; border-radius: 8px; color: var(--accent); background: var(--accent-bg); }.transaction-copy { display: grid; gap: 2px; }.transaction-copy strong { color: var(--text-h); font-size: 13px; }.transaction-copy span { font-size: 11px; }
+.drawer-backdrop, .modal-backdrop { position: fixed; inset: 0; border: 0; background: rgba(12, 24, 31, .42); z-index: 10; }.drawer { position: fixed; inset: 0 auto 0 0; width: 280px; padding: 24px; background: var(--surface); box-shadow: 8px 0 25px rgba(0,0,0,.12); transform: translateX(-105%); transition: transform .25s ease; z-index: 11; }.drawer-open { transform: translateX(0); }.drawer-header { display: flex; justify-content: space-between; align-items: center; }.drawer-nav { display: grid; gap: 5px; margin-top: 58px; }.nav-link { display: flex; align-items: center; gap: 12px; padding: 12px; border-radius: 7px; color: var(--text); font-size: 14px; text-decoration: none; }.nav-link:hover, .nav-link.active { color: var(--accent); background: var(--accent-bg); }.drawer-footer { position: absolute; left: 24px; right: 24px; bottom: 30px; padding-top: 20px; border-top: 1px solid var(--border); color: var(--text-h); font-size: 13px; }.drawer-footer p { margin-bottom: 4px; color: var(--text); font-size: 11px; }
+.modal-backdrop { display: grid; place-items: center; padding: 20px; }.account-modal { width: min(580px, 100%); max-height: min(720px, 90vh); overflow-y: auto; padding: 30px; border-radius: 12px; background: var(--surface); box-shadow: 0 25px 70px rgba(0,0,0,.2); }.modal-balance { display: grid; gap: 4px; margin-top: 28px; padding: 20px; border-radius: 8px; background: var(--ink); color: white; }.modal-balance span { font-size: 12px; opacity: .7; }.modal-balance strong { font-size: 32px; letter-spacing: -.04em; }.modal-meta { display: flex; justify-content: space-between; gap: 10px; padding: 20px 0 7px; font-size: 12px; }.modal-meta span:first-child { display: flex; align-items: center; gap: 6px; }.account-modal h3 { margin-top: 24px; font-size: 16px; }.full-width-button { display: flex; justify-content: center; align-items: center; gap: 6px; width: 100%; margin-top: 18px; padding: 13px; border-radius: 6px; color: white; background: var(--accent); font-size: 13px; font-weight: 800; }
+@media (max-width: 760px) { .topbar { height: 64px; padding: 0 20px; }.topbar-actions { gap: 10px; }.mode-toggle { font-size: 11px; }.signout-button { font-size: 0; }.signout-button svg { width: 19px; height: 19px; }.page-content { padding: 48px 20px 70px; }.welcome-row { display: block; }.profile-chip { width: fit-content; margin-top: 26px; }.account-section, .activity-preview { margin-top: 54px; }.account-grid { grid-template-columns: 1fr; }.account-card, .account-card-main { min-height: 215px; }.modal-meta { align-items: flex-start; flex-direction: column; }.account-modal { padding: 22px; } }
+.back-link { display: inline-flex; align-items: center; gap: 7px; color: var(--accent); font-size: 13px; font-weight: 800; text-decoration: none; }.account-detail-hero { display: flex; justify-content: space-between; gap: 30px; margin-top: 58px; padding-bottom: 34px; border-bottom: 1px solid var(--border); }.account-detail-hero h1 { margin-bottom: 14px; }.detail-balance { display: grid; align-content: end; gap: 4px; text-align: right; }.detail-balance span, .account-info-strip span, .section-heading > span { font-size: 12px; }.detail-balance strong { color: var(--text-h); font-size: 38px; letter-spacing: -.04em; }.account-info-strip { display: flex; gap: 28px; padding: 18px 0; color: var(--text); font-size: 12px; }.account-info-strip span:first-child { display: flex; align-items: center; gap: 6px; }.detail-section { max-width: 760px; margin-top: 48px; }.form-panel { max-width: 520px; margin-top: 56px; padding: 25px; border: 1px solid var(--border); border-radius: 10px; background: var(--surface); }.form-panel h2 { margin-bottom: 24px; font-size: 21px; }.large-form { max-width: 590px; margin-top: 45px; }.form-panel form { display: grid; gap: 9px; }.form-panel label { margin-top: 9px; color: var(--text-h); font-size: 13px; font-weight: 800; }.form-panel label span { color: var(--text); font-weight: 400; }.form-panel input, .form-panel select { box-sizing: border-box; width: 100%; padding: 12px; border: 1px solid var(--border); border-radius: 6px; outline: 0; color: var(--text-h); background: var(--bg); font: inherit; font-size: 14px; }.form-panel input:focus, .form-panel select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-bg); }.money-input { display: flex; align-items: center; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); }.money-input:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-bg); }.money-input span { padding-left: 12px; color: var(--text-h); font-weight: 800; }.money-input input { border: 0; background: transparent; }.primary-button { margin-top: 18px; padding: 13px; border: 0; border-radius: 6px; color: white; background: var(--accent); cursor: pointer; font-weight: 800; }.form-message { padding: 10px; border-radius: 6px; color: var(--green); background: var(--accent-bg); font-size: 13px; }.profile-details { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1px; margin-top: 48px; border: 1px solid var(--border); background: var(--border); }.profile-details div { display: grid; gap: 7px; padding: 22px; background: var(--surface); }.profile-details span { color: var(--text); font-size: 12px; }.profile-details strong { color: var(--text-h); font-size: 15px; }.modal-section-heading { display: flex; align-items: baseline; justify-content: space-between; margin-top: 25px; }.modal-section-heading span { font-size: 11px; }.admin-badge { display: flex; align-items: center; gap: 6px; color: var(--accent); font-size: 12px; font-weight: 800; }.admin-accounts { margin-top: 52px; }
+@media (max-width: 760px) { .account-detail-hero { display: grid; margin-top: 42px; }.detail-balance { text-align: left; }.account-info-strip { flex-direction: column; gap: 8px; }.profile-details { grid-template-columns: 1fr; }.admin-badge { display: none; } }
+.landing-page, .auth-page { min-height: 100vh; background: var(--bg); color: var(--text); }.landing-nav { display: flex; align-items: center; justify-content: space-between; max-width: 1240px; margin: 0 auto; padding: 25px 5vw; }.landing-brand { display: flex; align-items: center; gap: 10px; color: var(--text-h); font-family: var(--heading); font-size: 16px; font-weight: 800; text-decoration: none; }.logo-mark { display: grid; place-items: center; width: 38px; height: 38px; border-radius: 10px; color: white; background: var(--accent); box-shadow: 0 8px 16px rgba(8,127,115,.2); }.landing-signup { display: flex; align-items: center; gap: 8px; color: var(--accent); font-size: 13px; font-weight: 800; text-decoration: none; }.landing-content { display: grid; grid-template-columns: minmax(0, 1fr) 440px; align-items: center; gap: 10vw; max-width: 1120px; min-height: calc(100vh - 145px); margin: 0 auto; padding: 50px 5vw 90px; }.landing-copy { position: relative; padding: 50px 0; }.landing-copy::before { content: ''; position: absolute; top: 0; left: -10vw; width: 75%; height: 100%; z-index: 0; background: repeating-linear-gradient(135deg, transparent 0 22px, rgba(8,127,115,.06) 23px 24px); pointer-events: none; }.landing-copy > * { position: relative; z-index: 1; }.landing-copy .eyebrow { display: flex; align-items: center; gap: 7px; }.landing-copy h1 { max-width: 610px; font-size: clamp(3rem, 6vw, 5.6rem); }.landing-lede { max-width: 540px; margin-top: 24px; color: var(--text); font-size: 18px; line-height: 1.6; }.landing-points { display: grid; gap: 13px; margin-top: 38px; color: var(--text-h); font-size: 13px; font-weight: 700; }.landing-points span { display: flex; align-items: center; gap: 9px; }.landing-points svg { color: var(--accent); }.login-panel, .signup-panel { padding: 34px; border: 1px solid var(--border); border-radius: 12px; background: var(--surface); box-shadow: 0 22px 50px rgba(25,31,44,.1); }.login-panel h2 { font-size: 27px; }.login-subtitle { margin-top: 8px; font-size: 13px; }.login-panel form, .signup-panel form { display: grid; gap: 9px; margin-top: 25px; }.login-panel label, .signup-panel label { margin-top: 8px; color: var(--text-h); font-size: 12px; font-weight: 800; }.login-panel input, .signup-panel input { width: 100%; box-sizing: border-box; padding: 13px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text-h); font: inherit; font-size: 14px; outline: 0; }.login-panel input:focus, .signup-panel input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-bg); }.checkbox-label { display: flex; align-items: center; gap: 8px; margin: 10px 0 3px; font-weight: 600 !important; }.checkbox-label input { width: auto; accent-color: var(--accent); }.primary-button { display: flex; align-items: center; justify-content: center; gap: 8px; }.primary-button:disabled { cursor: wait; opacity: .65; }.form-footer { margin-top: 23px; text-align: center; font-size: 12px; }.form-footer a { color: var(--accent); font-weight: 800; }.error-message { padding: 10px; border-radius: 6px; color: var(--red); background: #fceeed; font-size: 12px; }.success-message { padding: 10px; border-radius: 6px; color: var(--green); background: #eafcef; font-size: 12px; }.landing-footer { display: flex; justify-content: space-between; gap: 20px; max-width: 1120px; margin: 0 auto; padding: 18px 5vw 25px; border-top: 1px solid var(--border); color: var(--text); font-size: 11px; }.landing-footer span:first-child { color: var(--text-h); font-weight: 800; }.auth-page .landing-nav { max-width: 1120px; }.signup-content { max-width: 680px; margin: 0 auto; padding: 70px 5vw; }.signup-content h1 { font-size: clamp(2.5rem, 5vw, 4.5rem); }.signup-content .landing-lede { margin-top: 15px; font-size: 16px; }.signup-panel { margin-top: 38px; }.form-columns { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }.form-columns > div { display: grid; gap: 9px; }.form-columns input { width: 100%; }.modal-section-heading { display: flex; justify-content: space-between; align-items: baseline; }.error-message { margin-top: 4px; }
+@media (max-width: 800px) { .landing-content { grid-template-columns: 1fr; gap: 25px; padding-top: 20px; }.landing-copy { padding: 25px 0 5px; }.landing-copy h1 { font-size: clamp(2.8rem, 13vw, 4.5rem); }.login-panel { max-width: 520px; }.landing-footer { flex-wrap: wrap; }.landing-footer span:last-child { width: 100%; }.signup-content { padding-top: 35px; } }
+@media (max-width: 500px) { .landing-nav { padding: 18px 20px; }.landing-brand { font-size: 14px; }.landing-signup { font-size: 12px; }.landing-content { padding: 20px 20px 60px; }.login-panel, .signup-panel { padding: 24px 20px; }.form-columns { grid-template-columns: 1fr; gap: 0; }.signup-content { padding-inline: 20px; }.landing-footer { padding-inline: 20px; } }
+.field-message { margin: 0 0 4px; color: var(--red); font-size: 11px; font-weight: 800; }.field-error > label { color: var(--red); }.login-panel input.input-error, .signup-panel input.input-error { border-color: var(--red); color: var(--red); box-shadow: 0 0 0 3px rgba(189,75,75,.12); }.signup-panel select { width: 100%; box-sizing: border-box; padding: 13px; border: 1px solid var(--border); border-radius: 6px; color: var(--text-h); background: var(--bg); font: inherit; font-size: 14px; }.checking-text { color: var(--accent) !important; font-weight: 600 !important; }
+.open-account-modal { max-width: 480px; }.account-opening-form { max-width: none; margin-top: 28px; padding: 0; border: 0; }.form-hint { font-size: 12px; }
+input[type='number'] { appearance: textfield; -moz-appearance: textfield; }input[type='number']::-webkit-inner-spin-button, input[type='number']::-webkit-outer-spin-button { margin: 0; -webkit-appearance: none; }
+
+.transaction-main { display: grid; grid-template-columns: 1fr; gap: 8px; flex: 1; min-width: 0; align-items: center; }
+.transaction-category { max-width: 160px; padding: 4px 8px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text-h); font: inherit; font-size: 11px; justify-self: start; }
+@media (min-width: 600px) {
+ .transaction-main { grid-template-columns: auto auto minmax(0, 1fr); gap: 16px; }
+}
+.coin-flip { display: grid; justify-items: center; gap: 4px; min-width: 52px; perspective: 240px; }
+.coin { width: 52px; height: 52px; padding: 0; border: 0; background: transparent; cursor: pointer; }
+.coin:disabled { cursor: default; }
+.coin-inner { position: relative; display: grid; place-items: center; width: 52px; height: 52px; transform-style: preserve-3d; }
+.coin.is-flipping .coin-inner, .coin.did-flip .coin-inner { transition: transform 700ms ease-in-out; }
+.coin.is-flipping .coin-inner { transform: rotateY(720deg); }
+.coin.is-revealed .coin-inner { transform: rotateY(180deg); }
+.coin.did-flip.is-revealed .coin-inner { transform: rotateY(900deg); }
+.coin-face { position: absolute; inset: 0; display: grid; place-items: center; border-radius: 50%; border: 3px solid #d49a24; background: linear-gradient(145deg, #ffe58a, #d99b24); color: #6b4500; font-size: 22px; font-weight: 800; backface-visibility: hidden; box-shadow: inset 0 1px 0 rgba(255,255,255,.45), 0 4px 8px rgba(107,69,0,.18); }
+.coin-back { transform: rotateY(180deg); }
+.coin-back.is-free { border-color: #c9a227; background: linear-gradient(145deg, #fff1a8, #d4b12a); }
+.coin-back.is-double { border-color: #c57d1f; background: linear-gradient(145deg, #ffd27a, #c98422); }
+.coin-icon { width: 40px; height: 40px; display: block; object-fit: contain; }
+.coin-status { color: var(--red); font-size: 10px; font-weight: 700; text-align: center; }
+.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
+.wager-amount-win, .wager-amount-loss { display: inline-flex; flex-direction: column; align-items: flex-end; gap: 2px; line-height: 1.15; text-align: right; }
+.wager-amount-original { text-decoration: line-through; color: var(--text); font-weight: 600; font-size: 12px; }
+.wager-amount-win .wager-amount-original { color: var(--green); }
+.wager-amount-win .wager-amount-new { color: var(--green); font-size: 14px; letter-spacing: .04em; }
+.wager-amount-new { color: var(--red); }
+.transaction-actions.has-coin { min-width: 160px; justify-content: space-between; }
\ No newline at end of file
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/App.tsx b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/App.tsx
new file mode 100644
index 00000000..1d6c9ec7
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/App.tsx
@@ -0,0 +1,29 @@
+import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'
+import { CustomerLayout } from './components/layout/CustomerLayout'
+import { AdminAccountsPage } from './pages/admin/AdminAccountsPage'
+import { AccountPage } from './pages/customer/AccountPage'
+import { DashboardPage } from './pages/customer/DashboardPage'
+import { DepositPage } from './pages/customer/DepositPage'
+import { ProfilePage } from './pages/customer/ProfilePage'
+import { TransferPage } from './pages/customer/TransferPage'
+import { LandingPage } from './pages/auth/LandingPage'
+import { SignupPage } from './pages/auth/SignupPage'
+import './App.css'
+
+function App() {
+ return
+ } />
+ } />
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+
+ } />
+ } />
+
+}
+
+export default App
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/api/.gitkeep b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/api/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/api/bankingApi.ts b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/api/bankingApi.ts
new file mode 100644
index 00000000..49de6721
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/api/bankingApi.ts
@@ -0,0 +1,165 @@
+import axios from 'axios'
+import type { Account, AccountType, Transaction, User } from '../types/banking'
+
+const api = axios.create({
+ baseURL: import.meta.env.VITE_API_URL ?? 'http://127.0.0.1:8000'
+})
+
+
+// Add JWT to every API request
+api.interceptors.request.use((config) => {
+ const token = localStorage.getItem('banking_access_token')
+
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`
+ }
+
+ return config
+})
+
+export const getSessionUserId = () =>
+ localStorage.getItem('banking_user_id')
+
+export async function login(
+ email: string,
+ password: string
+): Promise<{ userId: string; isAdmin: boolean }> {
+ const response = await api.post('/login', {
+ email,
+ password
+ })
+
+ localStorage.setItem(
+ 'banking_access_token',
+ response.data.access_token
+ )
+
+ localStorage.setItem(
+ 'banking_user_id',
+ response.data.user_id
+ )
+
+ localStorage.setItem(
+ 'banking_is_admin',
+ String(response.data.is_admin)
+ )
+
+ return {
+ userId: response.data.user_id,
+ isAdmin: response.data.is_admin
+ }
+}
+
+export function logout() {
+ localStorage.removeItem('banking_access_token')
+ localStorage.removeItem('banking_user_id')
+ localStorage.removeItem('banking_is_admin')
+}
+
+export async function createUser(user: { email: string; password: string; isAdmin: boolean; birthday: string; phoneNumber: string; firstName: string; lastName: string }) {
+ const response = await api.post('/users', { email: user.email, password: user.password, is_admin: user.isAdmin, birthday: user.birthday, phone_number: user.phoneNumber, first_name: user.firstName, last_name: user.lastName })
+ return response.data as { id: string; is_admin: boolean }
+}
+
+//goes through
+export async function isEmailAvailable(email: string) {
+ const response = await api.get('/users')
+ return !(response.data.users as { email: string }[]).some((user) => user.email.toLowerCase() === email.trim().toLowerCase())
+}
+
+export async function fetchUsers(): Promise<{ id: string; firstName: string; lastName: string }[]> {
+ const response = await api.get('/users')
+ return (response.data.users as { id: string; first_name: string; last_name: string }[]).map((user) => ({ id: String(user.id), firstName: user.first_name, lastName: user.last_name }))
+}
+
+//this function is used to convert our account objects from the API format to the frontend format
+const accountFromApi = (account: { account_number: number; account_type: AccountType; balance: number; created_date: string; is_active: boolean; owner_id?: string }): Account => ({
+ accountNumber: String(account.account_number), accountType: account.account_type, balance: Number(account.balance), createdDate: account.created_date, isActive: account.is_active, ownerId: account.owner_id ? String(account.owner_id) : undefined,
+})
+
+const transactionFromApi = (transaction: { id: number; from_owner_id: string; from_owner_account_number: number; to_owner_id?: string | null; to_owner_account_number?: number | null; description?: string; amount: number; transaction_date: string; category?: string; type: string; wager_result?: 'win' | 'loss' | null }, viewerId: string): Transaction => {
+ //transfers are only a credit for the recipient; everyone else sees them as money leaving their account
+ const isRecipient = transaction.type === 'transfer' && String(transaction.to_owner_id) === viewerId
+ const accountNumber = isRecipient && transaction.to_owner_account_number ? transaction.to_owner_account_number : transaction.from_owner_account_number
+ const amount = transaction.type === 'deposit' || isRecipient ? Number(transaction.amount) : -Number(transaction.amount)
+ return {
+ id: String(transaction.id), merchant: transaction.description || (isRecipient ? 'Transfer received' : transaction.type), date: new Date(transaction.transaction_date).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' }), amount, category: transaction.category ?? undefined, accountNumber: String(accountNumber), type: transaction.type, wagerResult: transaction.wager_result,
+ }
+}
+
+type ApiTransaction = { id: number; from_owner_id: string; from_owner_account_number: number; to_owner_id?: string | null; to_owner_account_number?: number | null; description?: string; amount: number; transaction_date: string; category?: string; type: string; wager_result?: 'win' | 'loss' | null }
+
+//admins aren't the sender or recipient, so transfers must be split into a debit row (sender's account) and a credit row (recipient's account) to show up under both
+const adminTransactionsFromApi = (transaction: ApiTransaction): Transaction[] => {
+ const date = new Date(transaction.transaction_date).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' })
+ const category = transaction.category ?? undefined
+ const debit: Transaction = { id: String(transaction.id), merchant: transaction.description || transaction.type, date, amount: transaction.type === 'deposit' ? Number(transaction.amount) : -Number(transaction.amount), category, accountNumber: String(transaction.from_owner_account_number), type: transaction.type, wagerResult: transaction.wager_result }
+ if (transaction.type !== 'transfer' || !transaction.to_owner_account_number) return [debit]
+ const credit: Transaction = { id: `${transaction.id}-credit`, merchant: 'Transfer received', date, amount: Number(transaction.amount), category, accountNumber: String(transaction.to_owner_account_number), type: transaction.type, wagerResult: transaction.wager_result }
+ return [debit, credit]
+}
+
+export async function fetchUser(userId: string): Promise {
+ const response = await api.get(`/users/${userId}`)
+ return { firstName: response.data.first_name, lastName: response.data.last_name, email: response.data.email, phoneNumber: response.data.phone_number, birthday: response.data.birthday }
+}
+
+export async function fetchAccounts(userId: string, admin = false): Promise {
+ const response = await api.get('/accounts', { params: { requester_id: userId, ...(admin ? {} : { owner_id: userId }) } })
+ return response.data.accounts.map(accountFromApi)
+}
+
+export async function fetchAccount(accountNumber: string): Promise {
+ const response = await api.get(`/accounts/${accountNumber}`)
+ return accountFromApi(response.data)
+}
+
+export async function fetchTransactions(userId: string, admin = false): Promise {
+ const response = await api.get('/transactions', { params: { owner_id: userId } })
+ if (admin) return (response.data as ApiTransaction[]).flatMap(adminTransactionsFromApi)
+ return response.data.map((transaction: ApiTransaction) => transactionFromApi(transaction, userId))
+}
+
+export async function fetchSpending(userId: string): Promise {
+ const response = await api.get('/transactions/spending', { params: { owner_id: userId } })
+ return response.data.map(transactionFromApi)
+}
+
+export async function createAccount(userId: string, accountType: AccountType, amount = '0') {
+ const response = await api.post('/accounts', { owner_id: userId, account_type: accountType, amount }, { params: { requester_id: userId } })
+ return accountFromApi(response.data)
+}
+
+export async function deposit(userId: string, accountNumber: string, amount: string, description?: string) {
+ const response = await api.post('/transactions/deposit', { account_number: Number(accountNumber), amount, description }, { params: { owner_id: userId } })
+ return accountFromApi(response.data)
+}
+
+export async function withdraw(userId: string, accountNumber: string, amount: string, description?: string) {
+ const response = await api.post('/transactions/withdraw', { account_number: Number(accountNumber), amount, description }, { params: { owner_id: userId } })
+ return accountFromApi(response.data)
+}
+
+export async function transfer(userId: string, fromAccountNumber: string, toAccountNumber: string, amount: string) {
+ return api.post('/transactions/transfer', { from_account_number: Number(fromAccountNumber), to_account_number: Number(toAccountNumber), amount }, { params: { owner_id: userId } })
+}
+
+export async function createWager(transactionId: string, ownerId: string): Promise<{ wagerResult: 'win' | 'loss'; updatedBalance: number }> {
+ const response = await api.post(`/transactions/${transactionId}/wager`, null, { params: { owner_id: ownerId } })
+ return { wagerResult: response.data.wager_result, updatedBalance: Number(response.data.updated_balance) }
+}
+
+export async function fetchTransactionCategories(ownerId: string): Promise {
+ const response = await api.get(`/transactions/owners/${ownerId}/categories`)
+ return (response.data as string[]).map(String)
+}
+
+export async function updateTransactionCategory(transactionId: string, ownerId: string, category?: string) {
+ const response = await api.put(`/transactions/${transactionId}/category`, null, { params: { owner_id: ownerId, ...(category ? { category } : {}) } })
+ return response.data
+}
+
+export async function updateAccountStatus(requesterId: string, accountNumber: string, isActive: boolean) {
+ const response = await api.patch(`/accounts/${accountNumber}/status`, { is_active: isActive }, { params: { requester_id: requesterId } })
+ return accountFromApi(response.data)
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/app/providers/.gitkeep b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/app/providers/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/assets/bomb.svg b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/assets/bomb.svg
new file mode 100644
index 00000000..791ead54
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/assets/bomb.svg
@@ -0,0 +1,34 @@
+
\ No newline at end of file
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/assets/hero.png b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/assets/hero.png
new file mode 100644
index 00000000..02251f4b
Binary files /dev/null and b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/assets/hero.png differ
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/assets/money_bag.svg b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/assets/money_bag.svg
new file mode 100644
index 00000000..d2678d8f
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/assets/money_bag.svg
@@ -0,0 +1,103 @@
+
+
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/assets/react.svg b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/assets/react.svg
new file mode 100644
index 00000000..6c87de9b
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/assets/vite.svg b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/assets/vite.svg
new file mode 100644
index 00000000..5101b674
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/assets/vite.svg
@@ -0,0 +1 @@
+
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/accounts/.gitkeep b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/accounts/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/accounts/AccountCard.tsx b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/accounts/AccountCard.tsx
new file mode 100644
index 00000000..bfe3b8ee
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/accounts/AccountCard.tsx
@@ -0,0 +1,18 @@
+import { Ban, CheckCircle2, ChevronRight } from 'lucide-react'
+import type { Account } from '../../types/banking'
+import { formatMoney } from '../../data/mockData'
+
+type Props = { account: Account; admin?: boolean; ownerName?: string; onSelect: (account: Account) => void; onToggleStatus?: (accountNumber: string) => void }
+
+export function AccountCard({ account, admin = false, ownerName, onSelect, onToggleStatus }: Props) {
+ return
+
+ {admin && onToggleStatus && }
+
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/accounts/AccountDetailsModal.tsx b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/accounts/AccountDetailsModal.tsx
new file mode 100644
index 00000000..f0d61438
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/accounts/AccountDetailsModal.tsx
@@ -0,0 +1,17 @@
+import { CalendarDays, ChevronRight, X } from 'lucide-react'
+import type { Account, Transaction } from '../../types/banking'
+import { formatMoney } from '../../data/mockData'
+import { TransactionList } from '../transactions/TransactionList'
+
+type Props = { account: Account; transactions: Transaction[]; onClose: () => void; onWithdraw?: (account: Account) => void; onBalanceUpdated?: (accountNumber: string, balance: number) => void; admin?: boolean; ownerName?: string }
+
+export function AccountDetailsModal({ account, transactions, onClose, onWithdraw, onBalanceUpdated, admin = false, ownerName }: Props) {
+ const accountTransactions = transactions.filter((transaction) => transaction.accountNumber === account.accountNumber)
+ return event.stopPropagation()}>
+ {account.accountType.toUpperCase()} ACCOUNT
{account.accountNumber}
{admin &&
{ownerName ?? 'Unknown owner'}
}
+ Available balance{formatMoney(account.balance)}
+ Opened {account.createdDate}{account.isActive ? 'Currently active' : 'Currently closed'}
+ Recent transactions
{accountTransactions.length} shown
+ {account.isActive && !admin && onWithdraw && }
+
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/charts/.gitkeep b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/charts/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/coinflip/.gitkeep b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/coinflip/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/coinflip/CoinFlip.tsx b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/coinflip/CoinFlip.tsx
new file mode 100644
index 00000000..2cadb368
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/coinflip/CoinFlip.tsx
@@ -0,0 +1,100 @@
+import { useState } from "react";
+import bombIcon from "../../assets/bomb.svg";
+import moneyBagIcon from "../../assets/money_bag.svg";
+
+export interface CoinFlipProps {
+ /** Amount that is at stake for this transaction. */
+ amount: number;
+ currency?: string;
+ onComplete?: (amount: number, won: boolean) => void | boolean | Promise;
+ initialResult?: boolean | null;
+ disabled?: boolean;
+}
+
+/** A small, self-contained free-or-double control for transaction rows. */
+export default function CoinFlip({
+ amount,
+ currency = "USD",
+ onComplete,
+ initialResult = null,
+ disabled = false,
+}: CoinFlipProps) {
+ const [flipping, setFlipping] = useState(false);
+ const [result, setResult] = useState(initialResult);
+ const [didFlip, setDidFlip] = useState(false);
+ const [error, setError] = useState(false);
+ const completed = result !== null;
+
+ const flip = () => {
+ if (flipping || disabled || completed) return;
+
+ setFlipping(true);
+ setError(false);
+
+ window.setTimeout(async () => {
+ try {
+ const completedResult = await onComplete?.(amount, false);
+ if (typeof completedResult === "boolean") setResult(completedResult);
+ } catch {
+ setError(true);
+ } finally {
+ setDidFlip(true);
+ setFlipping(false);
+ }
+ }, 700);
+ };
+
+ const formattedAmount = new Intl.NumberFormat(undefined, {
+ style: "currency",
+ currency,
+ }).format(amount);
+
+ const coinClass = [
+ "coin",
+ flipping ? "is-flipping" : "",
+ didFlip ? "did-flip" : "",
+ result === true ? "is-revealed is-free" : "",
+ result === false ? "is-revealed is-double" : "",
+ ]
+ .filter(Boolean)
+ .join(" ");
+
+ return (
+
+
+
+ {error && "Wager could not be completed."}
+ {!error && flipping && "Flipping"}
+ {!error && result === true && "Free"}
+ {!error && result === false && "Doubled"}
+
+ {error &&
Could not complete
}
+
+ );
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/layout/.gitkeep b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/layout/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/layout/CustomerLayout.tsx b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/layout/CustomerLayout.tsx
new file mode 100644
index 00000000..8579f4d4
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/layout/CustomerLayout.tsx
@@ -0,0 +1,47 @@
+import { useEffect, useState } from 'react'
+import { ArrowLeftRight, Home, Landmark, LogOut, Menu, UserCircle, X } from 'lucide-react'
+import { NavLink, Outlet, useNavigate } from 'react-router-dom'
+import { currentUser } from '../../data/mockData'
+import { fetchUser, getSessionUserId, logout } from '../../api/bankingApi'
+
+export function CustomerLayout() {
+ const navigate = useNavigate()
+ const [menuOpen, setMenuOpen] = useState(false)
+ const [user, setUser] = useState(currentUser)
+ const initials = `${user.firstName[0]}${user.lastName[0]}`
+ const signOut = () => { logout(); navigate('/') }
+ useEffect(() => {
+ const sessionUserId = getSessionUserId()
+ if (sessionUserId) fetchUser(sessionUserId).then(setUser).catch(() => {})
+ }, [])
+ useEffect(() => {
+ const preventFormSubmit = (event: KeyboardEvent) => {
+ if (event.key === 'Enter' && event.target instanceof HTMLInputElement) {
+ event.preventDefault()
+ event.target.blur()
+ }
+ }
+ document.addEventListener('keydown', preventFormSubmit, true)
+ return () => document.removeEventListener('keydown', preventFormSubmit, true)
+ }, [])
+
+ return
+
+
+ Northstar Bank
+
+
+ {menuOpen &&
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/transactions/.gitkeep b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/transactions/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/transactions/TransactionList.tsx b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/transactions/TransactionList.tsx
new file mode 100644
index 00000000..5e9928fd
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/transactions/TransactionList.tsx
@@ -0,0 +1,59 @@
+import { useEffect, useState } from 'react'
+import { ArrowLeftRight } from 'lucide-react'
+import type { Transaction } from '../../types/banking'
+import { formatMoney } from '../../data/mockData'
+import { createWager, fetchTransactionCategories, getSessionUserId, updateTransactionCategory } from '../../api/bankingApi'
+import CoinFlip from '../coinflip/CoinFlip'
+
+export function TransactionList({ transactions, onBalanceUpdated, onWagerCreated, wagersEnabled = true, categoriesEnabled = true }: { transactions: Transaction[]; onBalanceUpdated?: (accountNumber: string, balance: number) => void; onWagerCreated?: (transactionId: string, result: 'win' | 'loss') => void; wagersEnabled?: boolean; categoriesEnabled?: boolean }) {
+ const ownerId = getSessionUserId()
+ const [categories, setCategories] = useState([])
+ const [selectedCategories, setSelectedCategories] = useState>({})
+
+ useEffect(() => {
+ if (!ownerId || !categoriesEnabled) return
+ fetchTransactionCategories(ownerId).then(setCategories).catch(() => setCategories([]))
+ }, [categoriesEnabled, ownerId])
+
+ const changeCategory = async (transaction: Transaction, category: string) => {
+ if (!ownerId) return
+ const previousCategory = selectedCategories[transaction.id] ?? transaction.category ?? ''
+ setSelectedCategories((current) => ({ ...current, [transaction.id]: category }))
+ try {
+ await updateTransactionCategory(transaction.id, ownerId, category || undefined)
+ } catch {
+ setSelectedCategories((current) => ({ ...current, [transaction.id]: previousCategory }))
+ }
+ }
+
+ return {transactions.map((transaction) =>
+
+
+
{transaction.merchant}{transaction.date}
+ {categoriesEnabled &&
}
+
+
+ {wagersEnabled && ownerId && transaction.amount < 0 && transaction.type === 'purchase' && {
+ const wager = await createWager(transaction.id, ownerId)
+ onBalanceUpdated?.(transaction.accountNumber, wager.updatedBalance)
+ onWagerCreated?.(transaction.id, wager.wagerResult)
+ return wager.wagerResult === 'win'
+ }} />}
+ {transaction.wagerResult === 'win' && (
+
+ {formatMoney(transaction.amount)}
+ FREE
+
+ )}
+ {transaction.wagerResult === 'loss' && (
+
+ {formatMoney(transaction.amount)}
+ {formatMoney(transaction.amount * 2)}
+
+ )}
+ {transaction.wagerResult !== 'win' && transaction.wagerResult !== 'loss' && (
+ 0 ? 'credit' : ''}>{formatMoney(transaction.amount)}
+ )}
+
+
)}
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/users/.gitkeep b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/components/users/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/data/mockData.ts b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/data/mockData.ts
new file mode 100644
index 00000000..19d5c5d3
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/data/mockData.ts
@@ -0,0 +1,24 @@
+import type { Account, Transaction, User } from '../types/banking'
+
+export const currentUser: User = {
+ firstName: 'Jesiel',
+ lastName: 'Smith',
+ email: 'jesiel.smith@example.com',
+ phoneNumber: '(555) 013-4820',
+ birthday: '1998-04-12',
+}
+
+export const initialAccounts: Account[] = [
+ { accountNumber: '8472', accountType: 'Checking', balance: 4280.55, createdDate: 'March 14, 2024', isActive: true },
+ { accountNumber: '1936', accountType: 'Savings', balance: 12650, createdDate: 'November 02, 2023', isActive: true },
+]
+
+export const initialTransactions: Transaction[] = [
+ { id: '1', merchant: 'Northstar Market', date: 'Aug 24, 2026', amount: -84.32, category: 'Groceries', accountNumber: '8472' },
+ { id: '2', merchant: 'Payroll deposit', date: 'Aug 22, 2026', amount: 2450, category: 'Income', accountNumber: '8472' },
+ { id: '3', merchant: 'Metro Transit', date: 'Aug 20, 2026', amount: -42, category: 'Transport', accountNumber: '1936' },
+ { id: '4', merchant: 'Harbor Coffee', date: 'Aug 19, 2026', amount: -6.75, category: 'Dining', accountNumber: '8472' },
+ { id: '5', merchant: 'Utility payment', date: 'Aug 16, 2026', amount: -126.4, category: 'Bills', accountNumber: '1936' },
+]
+
+export const formatMoney = (amount: number) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(amount)
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/hooks/.gitkeep b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/hooks/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/index.css b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/index.css
new file mode 100644
index 00000000..bccc8ef3
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/index.css
@@ -0,0 +1,40 @@
+:root {
+ --text: #667477;
+ --text-h: #172d2c;
+ --bg: #f4f8f6;
+ --surface: #ffffff;
+ --border: #dce8e3;
+ --accent: #087f73;
+ --accent-bg: #e2f3ee;
+ --green: #25835c;
+ --red: #bd4b4b;
+ --ink: #173b3a;
+
+ --sans: 'Avenir Next', 'Trebuchet MS', sans-serif;
+ --heading: 'Avenir Next', 'Trebuchet MS', sans-serif;
+ --mono: ui-monospace, Consolas, monospace;
+
+ font: 16px/145% var(--sans);
+ letter-spacing: 0;
+ color-scheme: light;
+ color: var(--text);
+ background: var(--bg);
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+
+ @media (max-width: 1024px) {
+ font-size: 16px;
+ }
+}
+
+#root { min-height: 100svh; }
+
+body {
+ margin: 0;
+}
+
+h1, h2, h3 { font-family: var(--heading); font-weight: 700; }
+p { margin: 0; }
+button { font-family: inherit; }
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/main.tsx b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/main.tsx
new file mode 100644
index 00000000..bef5202a
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.tsx'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/UserDetails.jsx b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/UserDetails.jsx
new file mode 100644
index 00000000..a21bdb0e
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/UserDetails.jsx
@@ -0,0 +1,182 @@
+import { useEffect, useState } from "react";
+import { useParams } from "react-router-dom";
+
+import {
+ getUser,
+ updateUser,
+} from "../services/userService";
+
+import {
+ getAccountsByOwner,
+} from "../services/accountService";
+
+
+function UserDetails() {
+ const { userId } = useParams();
+
+ const [user, setUser] = useState(null);
+ const [accounts, setAccounts] = useState([]);
+ const [editing, setEditing] = useState(false);
+ const [error, setError] = useState("");
+
+ const [formData, setFormData] = useState({
+ first_name: "",
+ last_name: "",
+ email: "",
+ birthday: "",
+ phone_number: "",
+ });
+
+
+ useEffect(() => {
+ async function loadUser() {
+ try {
+ const userData = await getUser(userId);
+ const accountData = await getAccountsByOwner(userId);
+
+ setUser(userData);
+
+ setFormData({
+ first_name: userData.first_name,
+ last_name: userData.last_name,
+ email: userData.email,
+ birthday: userData.birthday,
+ phone_number: userData.phone_number,
+ });
+
+ setAccounts(accountData.accounts);
+ } catch (error) {
+ setError(error.message);
+ }
+ }
+
+ loadUser();
+ }, [userId]);
+
+
+ function handleChange(event) {
+ const { name, value } = event.target;
+
+ setFormData({
+ ...formData,
+ [name]: value,
+ });
+ }
+
+
+ async function handleSave() {
+ try {
+ const updatedUser = await updateUser(
+ userId,
+ formData
+ );
+
+ setUser(updatedUser);
+ setEditing(false);
+ } catch (error) {
+ setError(error.message);
+ }
+ }
+
+
+ if (error) {
+ return {error}
;
+ }
+
+ if (!user) {
+ return Loading...
;
+ }
+
+
+ return (
+
+
User Details
+
+ {!editing ? (
+
+
+ Name: {user.first_name} {user.last_name}
+
+
+
Email: {user.email}
+
Birthday: {user.birthday}
+
Phone: {user.phone_number}
+
+
+
+ ) : (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
Accounts
+
+ {accounts.length === 0 ? (
+
No accounts found.
+ ) : (
+ accounts.map((account) => (
+
+
+ Type: {account.account_type}
+
+
+
+ Account Number: {account.account_number}
+
+
+
+ Balance: ${account.balance}
+
+
+ ))
+ )}
+
+ );
+}
+
+export default UserDetails;
\ No newline at end of file
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/admin/.gitkeep b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/admin/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/admin/AdminAccountsPage.tsx b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/admin/AdminAccountsPage.tsx
new file mode 100644
index 00000000..b7abb08e
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/admin/AdminAccountsPage.tsx
@@ -0,0 +1,25 @@
+import { useEffect, useState } from 'react'
+import { Landmark, LogOut, ShieldCheck } from 'lucide-react'
+import { useNavigate } from 'react-router-dom'
+import { AccountCard } from '../../components/accounts/AccountCard'
+import { AccountDetailsModal } from '../../components/accounts/AccountDetailsModal'
+import { initialAccounts, initialTransactions } from '../../data/mockData'
+import type { Account } from '../../types/banking'
+import { fetchAccounts, fetchTransactions, fetchUsers, getSessionUserId, logout, updateAccountStatus } from '../../api/bankingApi'
+import type { Transaction } from '../../types/banking'
+
+export function AdminAccountsPage() {
+ const navigate = useNavigate() //useNaviagte is for programmatic navigation within the app meaning you can redirect users without them clicking a link
+ const sessionUserId = getSessionUserId()
+ const [accounts, setAccounts] = useState(sessionUserId ? [] : initialAccounts) //gets accounts of all users since admins have access to all accounts
+ const [transactions, setTransactions] = useState(sessionUserId ? [] : initialTransactions) //can see the transactions of all users since admins have access to all transactions
+ const [ownerNames, setOwnerNames] = useState>({}) //populates account boxes with owner names
+ const [selectedAccount, setSelectedAccount] = useState(null) //stores the account currently selected for viewing details in the modal
+ const [error, setError] = useState('')
+ useEffect(() => { if (!sessionUserId) return; fetchAccounts(sessionUserId, true).then(setAccounts).catch(() => setError('Live admin data could not be loaded.')) }, [sessionUserId])
+ useEffect(() => { if (!sessionUserId) return; fetchTransactions(sessionUserId, true).then(setTransactions).catch(() => setError('Live transaction history could not be loaded.')) }, [sessionUserId])
+ useEffect(() => { if (!sessionUserId) return; fetchUsers().then((users) => setOwnerNames(Object.fromEntries(users.map((user) => [user.id, `${user.firstName} ${user.lastName}`])))).catch(() => setError('Account owners could not be loaded.')) }, [sessionUserId])
+ const signOut = () => { logout(); navigate('/') }
+ const toggleStatus = async (accountNumber: string) => { const account = accounts.find((item) => item.accountNumber === accountNumber); if (!account) return; if (!sessionUserId) { setAccounts((items) => items.map((item) => item.accountNumber === accountNumber ? { ...item, isActive: !item.isActive } : item)); return } try { const updated = await updateAccountStatus(sessionUserId, accountNumber, !account.isActive); setAccounts((items) => items.map((item) => item.accountNumber === accountNumber ? updated : item)); setSelectedAccount((item) => item?.accountNumber === accountNumber ? updated : item) } catch { setError('The account status could not be updated.') } }
+ return Northstar Admin
Administrator
ADMINISTRATION
Account overview
Review customer accounts and manage their active status.
{error && {error}
}ALL CUSTOMER ACCOUNTS
Accounts
{accounts.length} accounts{accounts.map((account) =>
)}
{selectedAccount &&
setSelectedAccount(null)} />}
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/auth/.gitkeep b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/auth/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/auth/LandingPage.tsx b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/auth/LandingPage.tsx
new file mode 100644
index 00000000..24334d35
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/auth/LandingPage.tsx
@@ -0,0 +1,21 @@
+import { useState } from 'react'
+import type { FormEvent } from 'react'
+import { ArrowRight, Landmark, ShieldCheck, Sparkles } from 'lucide-react'
+import { Link, useLocation, useNavigate } from 'react-router-dom'
+import { login } from '../../api/bankingApi'
+
+export function LandingPage() {
+ const navigate = useNavigate()
+ const location = useLocation() //useLocation is for accessing the current location object which contains information about the URL and state passed during navigation
+ const accountCreated = Boolean((location.state as { accountCreated?: boolean } | null)?.accountCreated) //checks if customer or admin successfuly created a user account
+ const [email, setEmail] = useState('') //use states to store the inputted values
+ const [password, setPassword] = useState('')
+ const [adminLogin, setAdminLogin] = useState(false)
+ const [error, setError] = useState('')
+ const [submitting, setSubmitting] = useState(false)
+ const submit = async (event: FormEvent) => {
+ event.preventDefault(); setSubmitting(true); setError('')
+ try { const result = await login(email, password); if (result.isAdmin !== adminLogin) { setError(`This account is registered as a ${result.isAdmin ? 'admin' : 'customer'} account.`); return } localStorage.setItem('banking_user_id', result.userId); navigate(result.isAdmin ? '/admin/accounts' : '/dashboard') } catch { setError('We could not sign you in. Check your email and password.') } finally { setSubmitting(false) }
+ }
+ return Northstar BankOpen an account BANKING, MADE CLEAR
A steadier way to manage your money.
Everyday banking with a clearer view of what matters, from your first account to your next milestone.
Built around your privacy Accounts that work together
WELCOME BACK
Sign in to Northstar
Access your accounts securely.
{accountCreated && Your account was created. Sign in to continue.
}New to Northstar? Create an account
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/auth/SignupPage.tsx b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/auth/SignupPage.tsx
new file mode 100644
index 00000000..cad591c6
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/auth/SignupPage.tsx
@@ -0,0 +1,71 @@
+import { useState } from 'react'
+import type { ChangeEvent, FormEvent } from 'react'
+import { ArrowLeft, ArrowRight, Landmark } from 'lucide-react'
+import { Link, useNavigate } from 'react-router-dom'
+import { createUser, isEmailAvailable } from '../../api/bankingApi'
+
+type FormValues = { first_name: string; last_name: string; email: string; password: string; birthday: string; phone_number: string; is_admin: boolean }
+type FormErrors = Partial>
+
+const emptyValues: FormValues = { first_name: '', last_name: '', email: '', password: '', birthday: '', phone_number: '', is_admin: false }
+const phonePattern = /^\d{3}-\d{3}-\d{4}$/
+const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+
+function formatPhone(value: string) {
+ const digits = value.replace(/\D/g, '').slice(0, 10)
+ if (digits.length <= 3) return digits
+ if (digits.length <= 6) return `${digits.slice(0, 3)}-${digits.slice(3)}`
+ return `${digits.slice(0, 3)}-${digits.slice(3, 6)}-${digits.slice(6)}`
+}
+
+export function SignupPage() {
+ const navigate = useNavigate()
+ const [values, setValues] = useState(emptyValues)
+ const [errors, setErrors] = useState({})
+ const [message, setMessage] = useState('')
+ const [submitting, setSubmitting] = useState(false)
+ const [checkingEmail, setCheckingEmail] = useState(false)
+
+ const updateValue = (field: keyof FormValues, value: string | boolean) => {
+ setValues((current) => ({ ...current, [field]: value }))
+ setErrors((current) => ({ ...current, [field]: undefined }))
+ setMessage('')
+ }
+
+ const validate = () => {
+ const nextErrors: FormErrors = {}
+ if (!values.first_name.trim()) nextErrors.first_name = 'Enter your first name.'
+ if (!values.last_name.trim()) nextErrors.last_name = 'Enter your last name.'
+ if (!values.email.trim()) nextErrors.email = 'Enter your email address.'
+ else if (!emailPattern.test(values.email)) nextErrors.email = 'Enter a valid email address.'
+ if (!values.password) nextErrors.password = 'Enter a password.'
+ else if (values.password.length < 8) nextErrors.password = 'Use at least 8 characters.'
+ if (!values.birthday) nextErrors.birthday = 'Enter your birthday.'
+ if (!values.phone_number) nextErrors.phone_number = 'Enter your phone number.'
+ else if (!phonePattern.test(values.phone_number)) nextErrors.phone_number = 'Use the format 555-555-5555.'
+ setErrors(nextErrors)
+ return Object.keys(nextErrors).length === 0
+ }
+
+ const checkEmail = async () => {
+ if (!values.email || !emailPattern.test(values.email)) return
+ setCheckingEmail(true)
+ try { const available = await isEmailAvailable(values.email); setErrors((current) => ({ ...current, email: available ? undefined : 'This email is already registered.' })) }
+ catch { setErrors((current) => ({ ...current, email: 'Email availability could not be checked.' })) }
+ finally { setCheckingEmail(false) }
+ }
+
+ const submit = async (event: FormEvent) => {
+ event.preventDefault(); setMessage('')
+ if (!validate()) return
+ setSubmitting(true)
+ try { if (!(await isEmailAvailable(values.email))) { setErrors({ email: 'This email is already registered.' }); return } await createUser({ email: values.email, password: values.password, isAdmin: values.is_admin, birthday: values.birthday, phoneNumber: values.phone_number, firstName: values.first_name, lastName: values.last_name }); navigate('/', { state: { accountCreated: true } }) }
+ catch (error) { setMessage(error instanceof Error ? error.message : 'We could not create your account. Check your information and try again.') }
+ finally { setSubmitting(false) }
+ }
+
+ const inputClass = (field: keyof FormValues) => errors[field] ? 'input-error' : ''
+ const handlePhoneChange = (event: ChangeEvent) => updateValue('phone_number', formatPhone(event.target.value))
+
+ return Northstar Bank Back to sign inGET STARTED
Open your Northstar account.
A few details now, a simpler view of your money next.
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/customer/.gitkeep b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/customer/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/customer/AccountPage.tsx b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/customer/AccountPage.tsx
new file mode 100644
index 00000000..2ade9a9e
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/customer/AccountPage.tsx
@@ -0,0 +1,47 @@
+import { useEffect, useState } from 'react'
+import type { FormEvent } from 'react'
+import { ArrowLeft, CalendarDays } from 'lucide-react'
+import { Link, useParams, useSearchParams } from 'react-router-dom'
+import { TransactionList } from '../../components/transactions/TransactionList'
+import { formatMoney, initialAccounts, initialTransactions } from '../../data/mockData'
+import { fetchAccount, fetchTransactions, getSessionUserId, withdraw } from '../../api/bankingApi'
+import type { Account, Transaction } from '../../types/banking'
+
+export function AccountPage() {
+ const { accountNumber = '' } = useParams()
+ const [searchParams] = useSearchParams()
+ const sessionUserId = getSessionUserId()
+ const fallbackAccount = initialAccounts.find((item) => item.accountNumber === accountNumber) ?? initialAccounts[0]
+ const [account, setAccount] = useState(sessionUserId ? null : fallbackAccount)
+ const [transactions, setTransactions] = useState(sessionUserId ? [] : initialTransactions.filter((item) => item.accountNumber === fallbackAccount.accountNumber))
+ const [amount, setAmount] = useState('')
+ const [message, setMessage] = useState(searchParams.get('withdraw') ? 'Enter an amount to withdraw.' : '')
+ const [error, setError] = useState('')
+
+ useEffect(() => {
+ if (!sessionUserId || !accountNumber) return
+ Promise.all([fetchAccount(accountNumber), fetchTransactions(sessionUserId)]).then(([loadedAccount, loadedTransactions]) => {
+ setAccount(loadedAccount)
+ setTransactions(loadedTransactions.filter((item) => item.accountNumber === accountNumber))
+ }).catch(() => setError('We could not load this account from the API.'))
+ }, [accountNumber, sessionUserId])
+
+ const submitWithdrawal = async (event: FormEvent) => {
+ event.preventDefault()
+ if (!amount || Number(amount) <= 0) { setMessage('Enter a valid withdrawal amount.'); return }
+ if (!account || !sessionUserId) { setMessage(`${formatMoney(Number(amount))} withdrawal is ready for API connection.`); setAmount(''); return }
+ try { const updatedAccount = await withdraw(sessionUserId, account.accountNumber, amount); setAccount(updatedAccount); setTransactions(await fetchTransactions(sessionUserId).then((items) => items.filter((item) => item.accountNumber === account.accountNumber))); setMessage(`${formatMoney(Number(amount))} withdrawal submitted.`); setAmount('') }
+ catch { setMessage('The withdrawal could not be completed.') }
+ }
+
+ const updateAccountBalance = (accountNumber: string, balance: number) => {
+ if (accountNumber === account?.accountNumber) setAccount((current) => current ? { ...current, balance } : current)
+ }
+
+ const handleWagerCreated = (transactionId: string, result: 'win' | 'loss') => {
+ setTransactions((current) => current.map((t) => t.id === transactionId ? { ...t, wagerResult: result } : t))
+ }
+
+ if (!account) return {error || 'Loading account details...'}
+ return Back to dashboard{error && {error}
}{account.accountType.toUpperCase()} ACCOUNT
•••• {account.accountNumber.slice(-4)}
{account.isActive ? 'Active account' : 'Closed account'}Available balance{formatMoney(account.balance)}
Opened {account.createdDate}Account ending in {account.accountNumber.slice(-4)}
ACCOUNT ACTIVITY
Transaction history
{transactions.length} transactions {account.isActive && WITHDRAW FUNDS
Move money out of this account
}
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/customer/DashboardPage.tsx b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/customer/DashboardPage.tsx
new file mode 100644
index 00000000..d2006518
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/customer/DashboardPage.tsx
@@ -0,0 +1,150 @@
+import { useEffect, useState } from 'react'
+import type { FormEvent } from 'react'
+import { Plus, X } from 'lucide-react'
+import { useNavigate, useOutletContext } from 'react-router-dom'
+
+import { AccountCard } from '../../components/accounts/AccountCard'
+import { AccountDetailsModal } from '../../components/accounts/AccountDetailsModal'
+import { TransactionList } from '../../components/transactions/TransactionList'
+
+import {
+ initialAccounts,
+ initialTransactions,
+} from '../../data/mockData'
+
+import type { Account } from '../../types/banking'
+import { createAccount, fetchAccounts, fetchTransactions, fetchUser, getSessionUserId, fetchSpending } from '../../api/bankingApi'
+import { PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer } from 'recharts'
+
+interface SpendingTransaction {
+ id: string;
+ merchant: string;
+ date: string;
+ category: string;
+ accountNumber: string;
+ amount: number;
+}
+
+export function DashboardPage() {
+ const navigate = useNavigate()
+ const { initials = '' } = useOutletContext<{ initials: string }>()
+ const sessionUserId = getSessionUserId()
+
+ const [accounts, setAccounts] = useState(
+ sessionUserId ? [] : initialAccounts
+ )
+
+ const [transactions, setTransactions] = useState(
+ sessionUserId ? [] : initialTransactions
+ )
+
+ const [displayName, setDisplayName] = useState('')
+
+ const [error, setError] = useState('')
+
+ const [selectedAccount, setSelectedAccount] =
+ useState(null)
+
+ const [openAccountType, setOpenAccountType] =
+ useState<'Checking' | 'Savings' | null>(null)
+
+ const [openingDeposit, setOpeningDeposit] =
+ useState('0')
+
+
+ const [spending, setSpending] = useState([])
+ useEffect(() => {
+ if (!sessionUserId) return
+ Promise.all([fetchAccounts(sessionUserId), fetchTransactions(sessionUserId), fetchUser(sessionUserId), fetchSpending(sessionUserId)])
+ .then(([loadedAccounts, loadedTransactions, user, loadedSpending]) => { setAccounts(loadedAccounts); setTransactions(loadedTransactions); setDisplayName(`${user.firstName} ${user.lastName}`); setSpending(
+ loadedSpending.map((transaction) => ({
+ ...transaction,
+ amount: Math.abs(Number(transaction.amount)),
+ category: transaction.category || ''
+ }))
+ ) })
+ .catch(() => setError('We could not load your live account data. Please try again.'))
+ }, [sessionUserId])
+ const hour = new Date().getHours()
+ const greeting = hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening'
+
+ const spendingByCategory = spending.reduce((acc: Record, transaction: any) => {
+ const category = transaction.category || 'Other'
+ const amount = Math.abs(Number(transaction.amount))
+
+ acc[category] = (acc[category] || 0) + amount
+
+ return acc
+ }, {})
+
+ const spendingChartData = Object.entries(spendingByCategory).map(([category, amount]) => ({
+ name: category,
+ value: amount,
+ }))
+
+ const openAccount = async (event: FormEvent) => { event.preventDefault(); if (!sessionUserId || !openAccountType) return; if (Number(openingDeposit) < 0 || openingDeposit === '') { setError('Enter zero or a positive opening deposit.'); return } try { const account = await createAccount(sessionUserId, openAccountType, openingDeposit); setAccounts((items) => [...items, account]); setOpenAccountType(null); setOpeningDeposit('0') } catch { setError('The account could not be opened right now.') } }
+ const updateAccountBalance = (accountNumber: string, balance: number) => {
+ setAccounts((items) => items.map((account) => account.accountNumber === accountNumber ? { ...account, balance } : account))
+ setSelectedAccount((account) => account?.accountNumber === accountNumber ? { ...account, balance } : account)
+ }
+ return PERSONAL BANKING
{greeting}, {displayName.split(' ')[0]}
Here is your financial snapshot.
{initials}{displayName}Customer
+ {error && {error}
}
+ {accounts.length} accounts
+ {accounts.map((account) =>
)}
+
Grow your banking setup
Open an account that fits the way you save and spend.
+
+ {/* Pi Chart */}
+ {spendingChartData.length > 0 && (
+
+
+
+
SPENDING
+
Spending by category this month
+
+
+
+
+
+
+ `${((percent ?? 0) * 100).toFixed(0)}%`}
+>
+ {spendingChartData.map((_, index) => (
+ |
+ ))}
+
+
+ [
+ `$${Number(value).toFixed(2)}`,
+ 'Spent',
+ ]}
+ />
+
+
+
+
+
+
+)}
+ LATEST ACTIVITY
Recent transactions
View all
+ {selectedAccount && setSelectedAccount(null)} onBalanceUpdated={updateAccountBalance} onWithdraw={(account) => navigate(`/account/${account.accountNumber}?withdraw=true`)} />}
+ {openAccountType && setOpenAccountType(null)}>
event.stopPropagation()}>NEW ACCOUNT
Open {openAccountType.toLowerCase()}
}
+
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/customer/DepositPage.tsx b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/customer/DepositPage.tsx
new file mode 100644
index 00000000..a4b81d40
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/customer/DepositPage.tsx
@@ -0,0 +1,31 @@
+import { useEffect, useState } from 'react'
+import type { FormEvent } from 'react'
+import { initialAccounts, formatMoney } from '../../data/mockData'
+import { deposit, fetchAccounts, getSessionUserId } from '../../api/bankingApi'
+import type { Account } from '../../types/banking'
+
+export function DepositPage() {
+ const sessionUserId = getSessionUserId()
+ const [accounts, setAccounts] = useState(sessionUserId ? [] : initialAccounts)
+ const [accountNumber, setAccountNumber] = useState(sessionUserId ? '' : initialAccounts[0]?.accountNumber ?? '')
+ const [amount, setAmount] = useState('')
+ const [description, setDescription] = useState('')
+ const [message, setMessage] = useState('')
+ const [loading, setLoading] = useState(Boolean(sessionUserId))
+
+ useEffect(() => {
+ if (!sessionUserId) return
+ fetchAccounts(sessionUserId).then((loadedAccounts) => { setAccounts(loadedAccounts); setAccountNumber(loadedAccounts[0]?.accountNumber ?? '') }).catch(() => setMessage('We could not load your accounts.')).finally(() => setLoading(false))
+ }, [sessionUserId])
+
+ const submit = async (event: FormEvent) => {
+ event.preventDefault()
+ if (!accountNumber) { setMessage('Choose an account for this deposit.'); return }
+ if (!amount || Number(amount) <= 0) { setMessage('Enter a valid deposit amount.'); return }
+ if (!sessionUserId) { setMessage(`${formatMoney(Number(amount))} deposit is ready for API connection.`); setAmount(''); return }
+ try { const updatedAccount = await deposit(sessionUserId, accountNumber, amount, description); setAccounts((items) => items.map((account) => account.accountNumber === updatedAccount.accountNumber ? updatedAccount : account)); setMessage(`${formatMoney(Number(amount))} deposit submitted.`); setAmount(''); setDescription('') }
+ catch { setMessage('The deposit could not be completed.') }
+ }
+
+ return MOVE MONEY
Deposit funds
Add money to one of your Northstar accounts.
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/customer/ProfilePage.tsx b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/customer/ProfilePage.tsx
new file mode 100644
index 00000000..7d1f1013
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/customer/ProfilePage.tsx
@@ -0,0 +1,16 @@
+import { useEffect, useState } from 'react'
+import { currentUser } from '../../data/mockData'
+import { fetchUser, getSessionUserId } from '../../api/bankingApi'
+import type { User } from '../../types/banking'
+
+export function ProfilePage() {
+ const sessionUserId = getSessionUserId()
+ const [user, setUser] = useState(sessionUserId ? null : currentUser)
+ const [error, setError] = useState('')
+ useEffect(() => {
+ if (!sessionUserId) return
+ fetchUser(sessionUserId).then(setUser).catch(() => setError('We could not load your personal information.'))
+ }, [sessionUserId])
+ if (!user) return {error || 'Loading your personal information...'}
+ return YOUR PROFILE
Personal information
Review the information connected to your banking profile.
{error && {error}
}Full name{user.firstName} {user.lastName}
Email address{user.email}
Phone number{user.phoneNumber}
Date of birth{user.birthday}
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/customer/TransferPage.tsx b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/customer/TransferPage.tsx
new file mode 100644
index 00000000..4895e741
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/pages/customer/TransferPage.tsx
@@ -0,0 +1,33 @@
+import { useEffect, useState } from 'react'
+import type { FormEvent } from 'react'
+import { initialAccounts, formatMoney } from '../../data/mockData'
+import { fetchAccounts, getSessionUserId, transfer } from '../../api/bankingApi'
+import type { Account } from '../../types/banking'
+
+export function TransferPage() {
+ const sessionUserId = getSessionUserId()
+ const [accounts, setAccounts] = useState(sessionUserId ? [] : initialAccounts)
+ const [fromAccount, setFromAccount] = useState('')
+ const [amount, setAmount] = useState('')
+ const [message, setMessage] = useState('')
+ useEffect(() => {
+ if (!sessionUserId) return
+ fetchAccounts(sessionUserId).then((loadedAccounts) => {
+ setAccounts(loadedAccounts)
+ setFromAccount(loadedAccounts[0]?.accountNumber ?? '')
+ }).catch(() => setMessage('We could not load your accounts.'))
+ }, [sessionUserId])
+
+ const submit = async (event: FormEvent) => {
+ event.preventDefault()
+ const form = new FormData(event.currentTarget)
+ const recipient = String(form.get('transfer-to') ?? '')
+ if (!amount || Number(amount) <= 0) { setMessage('Enter a valid transfer amount.'); return }
+ if (!recipient) { setMessage('Enter a recipient account number.'); return }
+ if (!sessionUserId) { setMessage(`${formatMoney(Number(amount))} transfer is ready for API connection.`); setAmount(''); return }
+ try { await transfer(sessionUserId, fromAccount, recipient, amount); setMessage(`${formatMoney(Number(amount))} transfer submitted.`); setAmount('') }
+ catch { setMessage('The transfer could not be completed.') }
+ }
+
+ return MOVE MONEY
Transfer funds
Move money between accounts or to another Northstar customer.
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/styles/.gitkeep b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/styles/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/types/.gitkeep b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/types/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/types/banking.ts b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/types/banking.ts
new file mode 100644
index 00000000..5478ed83
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/types/banking.ts
@@ -0,0 +1,29 @@
+export type AccountType = 'Checking' | 'Savings'
+
+export type Account = {
+ accountNumber: string
+ accountType: AccountType
+ balance: number
+ createdDate: string
+ isActive: boolean
+ ownerId?: string
+}
+
+export type Transaction = {
+ id: string
+ merchant: string
+ date: string
+ amount: number
+ category?: string
+ accountNumber: string
+ type?: string
+ wagerResult?: 'win' | 'loss' | null
+}
+
+export type User = {
+ firstName: string
+ lastName: string
+ email: string
+ phoneNumber: string
+ birthday: string
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/utils/.gitkeep b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/src/utils/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/tsconfig.app.json b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/tsconfig.app.json
new file mode 100644
index 00000000..6830b6f7
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/tsconfig.app.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023", "DOM"],
+ "module": "esnext",
+ "types": ["vite/client"],
+ "allowArbitraryExtensions": true,
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/tsconfig.json b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/tsconfig.json
new file mode 100644
index 00000000..1ffef600
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/tsconfig.node.json b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/tsconfig.node.json
new file mode 100644
index 00000000..8455dcbc
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/tsconfig.node.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "es2023",
+ "lib": ["ES2023"],
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "module": "nodenext",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/vite.config.ts b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/vite.config.ts
new file mode 100644
index 00000000..9982072c
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/frontend/vite.config.ts
@@ -0,0 +1,7 @@
+import react from '@vitejs/plugin-react'
+import { defineConfig } from 'vite'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [react()],
+})
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/Plan.txt b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/Plan.txt
new file mode 100644
index 00000000..237ffb01
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/Plan.txt
@@ -0,0 +1,67 @@
+
+All users should have:
+email and password for login (email must be unique)
+An admin toggle (upon creation)
+A user id for account ownership (uuid)
+Birthday, phone number
+First name last name
+
+Customers specifically:
+Can make own accounts
+Allowed to have bank accounts (checking or savings)
+Allowed to make transactions
+Can only see their own transactions
+
+Admin specifically:
+Allowed to see all accounts of all customers can see balances, amount of accounts, etc but cannot make transactions.
+Can see all transactions
+
+All accounts should have:
+A user id (Customer only) denoting who owns it
+Account number
+Balance details (USD and cents)
+account type
+
+Checking account specifically:
+Allowed to overdraft (fee of our choosing)
+
+Savings account specifically:
+Nothing extra needed for a savings account
+
+All transactions must require:
+From owner id
+From owner account number
+To owner id
+To owner account number
+And a description for the payment
+As well as a reminder of how much was sent (ex $50)
+Transaction date stored
+Transactions stored in log
+Category
+
+
+Logins:
+email and password (depending on if admin determines which UI you see)
+Logout
+
+Coinflip:
+During purchases pop up will ask to agree to pay double current
+50/50 if customer wins they pay $0 (payment description says wager won), if they lose they pay double (and payment description says wager lost)
+
+Category Tracker:
+Dashboard apis for each category
+Pie chart potentially
+
+Tasks:
+Darren: Users both customer and admin & ORM
+Jesiel: Accounts & ORM
+Adam: Transactions & ORM
+Blake: Coin Flip & Category
+Tamana: Login & Category
+
+Database:
+ORM will be SQLModel
+Backend Testing is mandatory requirement, frontend is optional
+Pytest along with pytest-asyncio and TestClient for unit testing
+
+
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/database.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/database.py
new file mode 100644
index 00000000..f354d3ba
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/database.py
@@ -0,0 +1,42 @@
+import os #os is used to access our environement variables, in this case we are using it to access the DATABASE_URL variable that we set in our .env file
+from typing import Annotated #this is used to create a type hint for our session dependency, which is used in our routers to access the database session. Basically a shortcut to avoid having to write the same code over and over again in our routers
+
+from fastapi import Depends #this is going to tell fastapi that we want to use the results from another function before we run the function we are currently in. We will be using it to get our database session before running our routers
+from sqlmodel import Session, SQLModel, create_engine
+from sqlalchemy import text
+
+from models.account_model import AccountRecord #imports the account table model we made so SQLModel knows the layout when creating our database tables
+
+#assigns our database url to DATABASE_URL using os to grab it from the environmant variables in the terminal, raises an error if not set
+DATABASE_URL = os.getenv("DATABASE_URL")
+
+if not DATABASE_URL:
+ raise RuntimeError(
+ "DATABASE_URL must be set before starting the application"
+ )
+#an engine is just a connection manager. It will know what database to connect to, which driver to use, how to open the connections for the databse, and how to manage and reuse those connections.
+engine = create_engine(
+ DATABASE_URL,
+ connect_args={"check_same_thread": False} if DATABASE_URL.startswith("sqlite") else {}
+)
+
+#only used if the tables dont currently exist in the database
+def create_db_and_tables():
+ SQLModel.metadata.create_all(engine)
+ if not DATABASE_URL.startswith("sqlite"):
+ with engine.begin() as connection:
+ connection.execute(text(
+ "ALTER TABLE transaction ALTER COLUMN category DROP NOT NULL"
+ ))
+ #create_all only adds new tables, not new columns on existing ones, so patch wager_result in manually
+ connection.execute(text(
+ "ALTER TABLE transaction ADD COLUMN IF NOT EXISTS wager_result VARCHAR"
+ ))
+
+#a session is a temp connection to the database, used to query records, add records, update, etc
+def get_session():
+ with Session(engine) as session: #creates the engine
+ yield session #yield is used to return this session to the endpoint that called this function. After endpoint finishes with it the session is closes automatically by the with statement.
+
+#assigns the session created from the get_session function to SessionDep. a shortcut to avoid a lot of code repetition
+SessionDep = Annotated[Session, Depends(get_session)]
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/dto/ResponseDTO.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/dto/ResponseDTO.py
new file mode 100644
index 00000000..12e1fcd6
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/dto/ResponseDTO.py
@@ -0,0 +1,27 @@
+from typing import Generic, TypeVar
+
+from fastapi.encoders import jsonable_encoder
+from fastapi.responses import JSONResponse
+
+
+T = TypeVar("T")
+
+
+class ResponseDTO(JSONResponse, Generic[T]):
+
+ def __init__(
+ self,
+ status_code: int,
+ message: str,
+ data: T | None = None
+ ):
+ content = {
+ "status_code": status_code,
+ "message": message,
+ "data": data
+ }
+
+ super().__init__(
+ status_code=status_code,
+ content=jsonable_encoder(content)
+ )
\ No newline at end of file
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/dto/login_request.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/dto/login_request.py
new file mode 100644
index 00000000..4070ce0c
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/dto/login_request.py
@@ -0,0 +1,6 @@
+from pydantic import BaseModel, EmailStr
+
+
+class LoginRequest(BaseModel):
+ email: EmailStr
+ password: str
\ No newline at end of file
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/dto/user_db.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/dto/user_db.py
new file mode 100644
index 00000000..67c37bff
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/dto/user_db.py
@@ -0,0 +1,17 @@
+from datetime import date
+from uuid import UUID, uuid4
+
+from sqlmodel import Field, SQLModel
+
+
+class UserDB(SQLModel, table=True):
+ __tablename__ = "users"
+
+ id: UUID = Field(default_factory=uuid4, primary_key=True)
+ email: str = Field(index=True, unique=True, max_length=320)
+ password_hash: str
+ is_admin: bool = False
+ birthday: date
+ phone_number: str
+ first_name: str
+ last_name: str
\ No newline at end of file
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/main.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/main.py
new file mode 100644
index 00000000..b0f8dde3
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/main.py
@@ -0,0 +1,68 @@
+from contextlib import asynccontextmanager
+
+from fastapi import FastAPI, Request
+from fastapi.exceptions import RequestValidationError
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import JSONResponse
+
+from database import create_db_and_tables
+from routers import accounts, users, transactions, login
+
+@asynccontextmanager
+async def lifespan(app: FastAPI):
+ create_db_and_tables()
+ yield
+
+
+app = FastAPI(
+ title="Banking API",
+ lifespan=lifespan
+)
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origin_regex=r"https?://.*", # Allows any http/https IP or localhost dynamically
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+
+@app.exception_handler(RequestValidationError)
+async def validation_exception_handler(
+ request: Request,
+ exception: RequestValidationError
+):
+ return JSONResponse(
+ status_code=422,
+ content={
+ "status_code": 422,
+ "message": "Invalid request",
+ "data": exception.errors(),
+ },
+ )
+
+
+app.include_router(
+ accounts.router,
+ prefix="/accounts",
+ tags=["accounts"],
+)
+
+app.include_router(
+ users.router,
+ prefix="/users",
+ tags=["users"],
+)
+
+app.include_router(
+ transactions.router,
+ prefix="/transactions",
+ tags=["transactions"],
+)
+
+app.include_router(
+ login.router,
+ prefix="/login",
+ tags=["login"],
+)
\ No newline at end of file
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/models/CheckingAccount.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/models/CheckingAccount.py
new file mode 100644
index 00000000..3cf07ae2
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/models/CheckingAccount.py
@@ -0,0 +1,17 @@
+from models.account import Account
+class CheckingAccount(Account):
+ def __init__(self, balance=0, owner_id=None):
+ super().__init__("Checking", balance, owner_id)
+
+ #instead of calling super here we override the withdraw method to allow overdraft
+ def withdraw(self, amount):
+ if amount > 0:
+ self._balance -= amount
+ return True
+ return False
+
+ def deposit(self, amount):
+ if amount > 0:
+ self._balance += amount
+ return True
+ return False
\ No newline at end of file
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/models/SavingsAccount.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/models/SavingsAccount.py
new file mode 100644
index 00000000..6f811cbf
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/models/SavingsAccount.py
@@ -0,0 +1,14 @@
+from models.account import Account
+
+
+class SavingsAccount(Account):
+
+ def __init__(self, balance=0, owner_id=None, saving_period=1, interest_rate=0.02, minimum_balance=2000):
+ super().__init__("Savings", balance, owner_id)
+
+ def deposit(self, amount):
+ return super().deposit(amount)
+
+ def withdraw(self, amount):
+ return super().withdraw(amount)
+
\ No newline at end of file
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/models/account.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/models/account.py
new file mode 100644
index 00000000..234224d4
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/models/account.py
@@ -0,0 +1,36 @@
+import random
+
+class Account:
+ def __init__(self, account_type, balance=0, owner_id=None):
+ if balance < 0:
+ raise ValueError("Balance cannot be negative")
+ if owner_id is not None and owner_id <= 0:
+ raise ValueError("Owner ID must be positive")
+ self._balance = balance
+ self._account_type = account_type
+ self._owner_id = owner_id
+ self._account_number = random.randint(10000000, 99999999)
+
+ def deposit(self, amount):
+ if amount > 0:
+ self._balance += amount
+ return True
+ return False
+
+ def withdraw(self, amount):
+ if 0 < amount <= self._balance:
+ self._balance -= amount
+ return True
+ return False
+
+ def get_balance(self):
+ return self._balance
+
+ def get_account_number(self):
+ return self._account_number
+
+ def get_owner_id(self):
+ return self._owner_id
+
+ def get_account_type(self):
+ return self._account_type
\ No newline at end of file
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/models/account_model.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/models/account_model.py
new file mode 100644
index 00000000..3dd71ed5
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/models/account_model.py
@@ -0,0 +1,23 @@
+#sqlmodel is base class used to define database
+#field is used to configure individual columns of the table
+from datetime import date
+import random
+
+from sqlmodel import Field, SQLModel
+
+#this class is used to represent the overall databse table structure, shows us what each row will contain
+class AccountRecord(SQLModel, table=True):
+ account_number: int = Field(
+ default_factory=lambda: random.randint(10000000, 99999999),
+ primary_key=True
+ )
+ owner_id: str = Field(index=True)
+ account_type: str
+ balance: float = Field(default=0)
+ created_date: date = Field(default_factory=date.today)
+ is_active: bool = True
+ closed_date: date | None = None
+ overdraft_fee_cents: int = Field(default=35, ge=0)
+
+ def get_balance(self) -> float:
+ return self.balance
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/models/admin.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/models/admin.py
new file mode 100644
index 00000000..a1375043
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/models/admin.py
@@ -0,0 +1,24 @@
+from models.user import User
+
+
+class Admin(User):
+ def __init__(
+ self,
+ email,
+ owner_id=None,
+ birthday=None,
+ phone_number=None,
+ first_name=None,
+ last_name=None
+ ):
+ super().__init__(
+ email,
+ owner_id,
+ birthday,
+ phone_number,
+ first_name,
+ last_name
+ )
+
+ def get_role(self):
+ return "Admin"
\ No newline at end of file
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/models/customer.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/models/customer.py
new file mode 100644
index 00000000..97588208
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/models/customer.py
@@ -0,0 +1,47 @@
+from models.user import User
+from models.CheckingAccount import CheckingAccount
+from models.SavingsAccount import SavingsAccount
+
+
+class Customer(User):
+ def __init__(self, email, owner_id, birthday, phone_number, first_name, last_name):
+ super().__init__(email, owner_id, birthday, phone_number, first_name, last_name)
+
+ self._accounts = {
+ "Checking": {},
+ "Savings": {}
+ }
+
+ def get_role(self):
+ return "Customer"
+
+ def create_account(self, pin, account_type, balance=0):
+ if not self.check_pin(pin):
+ raise ValueError("Invalid PIN")
+
+ if account_type == "Checking":
+ account = CheckingAccount(
+ balance,
+ self._owner_id
+ )
+
+ elif account_type == "Savings":
+ account = SavingsAccount(
+ balance,
+ self._owner_id
+ )
+
+ else:
+ raise ValueError("Invalid account type")
+
+ account_number = account.get_account_number()
+
+ self._accounts[account_type][account_number] = account
+
+ return account
+
+ def get_accounts(self, pin):
+ if not self.check_pin(pin):
+ raise ValueError("Invalid PIN")
+
+ return self._accounts
\ No newline at end of file
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/models/transaction.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/models/transaction.py
new file mode 100644
index 00000000..de2d8e06
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/models/transaction.py
@@ -0,0 +1,63 @@
+from datetime import datetime
+from decimal import Decimal
+from enum import Enum
+from uuid import UUID
+
+from pydantic import BaseModel
+from sqlmodel import Field, SQLModel
+
+class TransactionType(str, Enum):
+ DEPOSIT = "deposit"
+ WITHDRAWAL = "withdrawal"
+ PURCHASE = "purchase"
+ TRANSFER = "transfer"
+
+class WagerResult(str, Enum):
+ WIN = "win"
+ LOSS = "loss"
+ PENDING = "pending"
+
+class Transaction(SQLModel, table=True):
+ id: int | None = Field(default=None, primary_key=True)
+ type: TransactionType
+ from_owner_id: UUID
+ from_owner_account_number: int
+ to_owner_id: UUID | None = None
+ to_owner_account_number: int | None = None
+ description: str | None = None
+ amount: Decimal = Field(gt=0, max_digits=12, decimal_places=2)
+ transaction_date: datetime = Field(default_factory=datetime.utcnow)
+ category: str | None = None
+ wager_result: WagerResult | None = None
+
+class DepositTransaction(BaseModel):
+ account_number: int
+ amount: Decimal = Field(gt=0, max_digits=12, decimal_places=2)
+ description: str | None = None
+ category: str | None = None
+
+class WithdrawalTransaction(BaseModel):
+ account_number: int
+ amount: Decimal = Field(gt=0, max_digits=12, decimal_places=2)
+ description: str | None = None
+ category: str | None = None
+
+class TransferTransaction(BaseModel):
+ from_account_number: int
+ to_account_number: int
+ amount: Decimal = Field(gt=0, max_digits=12, decimal_places=2)
+ category: None = None
+
+class FetchTransactions(BaseModel):
+ user_id: int
+ account_number: int
+
+class defaultCategories(str, Enum):
+ FOOD = "food"
+ ENTERTAINMENT = "entertainment"
+ UTILITIES = "utilities"
+ TRANSPORTATION = "transportation"
+ HEALTHCARE = "healthcare"
+ EDUCATION = "education"
+ PERSONAL_CARE = "personal_care"
+ MISCELLANEOUS = "miscellaneous"
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/models/user.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/models/user.py
new file mode 100644
index 00000000..b7ab0208
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/models/user.py
@@ -0,0 +1,41 @@
+from abc import ABC, abstractmethod
+
+
+class User(ABC):
+ def __init__(
+ self,
+ email,
+ owner_id=None,
+ birthday=None,
+ phone_number=None,
+ first_name=None,
+ last_name=None
+ ):
+ self._email = email
+ self._owner_id = owner_id
+ self._birthday = birthday
+ self._phone_number = phone_number
+ self._first_name = first_name
+ self._last_name = last_name
+
+ def get_owner_id(self):
+ return self._owner_id
+
+ def get_email(self):
+ return self._email
+
+ def get_birthday(self):
+ return self._birthday
+
+ def get_phone_number(self):
+ return self._phone_number
+
+ def get_first_name(self):
+ return self._first_name
+
+ def get_last_name(self):
+ return self._last_name
+
+ @abstractmethod
+ def get_role(self):
+ pass
\ No newline at end of file
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/package-lock.json b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/package-lock.json
new file mode 100644
index 00000000..ae2ed82c
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/package-lock.json
@@ -0,0 +1,441 @@
+{
+ "name": "project1",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "dependencies": {
+ "recharts": "^3.10.1"
+ }
+ },
+ "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/d3-array": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-color": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-ease": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-interpolate": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-color": "*"
+ }
+ },
+ "node_modules/@types/d3-path": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-scale": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-time": "*"
+ }
+ },
+ "node_modules/@types/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/d3-path": "*"
+ }
+ },
+ "node_modules/@types/d3-time": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
+ "license": "MIT"
+ },
+ "node_modules/@types/d3-timer": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
+ "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/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/decimal.js-light": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
+ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
+ "license": "MIT"
+ },
+ "node_modules/es-toolkit": {
+ "version": "1.51.0",
+ "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.51.0.tgz",
+ "integrity": "sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw==",
+ "license": "MIT",
+ "workspaces": [
+ "docs",
+ "benchmarks",
+ "tests/types",
+ "tests/browser-compat"
+ ]
+ },
+ "node_modules/eventemitter3": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+ "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/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "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",
+ "peer": true,
+ "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",
+ "peer": true,
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.8"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "19.2.8",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz",
+ "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==",
+ "license": "MIT",
+ "peer": true
+ },
+ "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",
+ "peer": true,
+ "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/recharts": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.10.1.tgz",
+ "integrity": "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==",
+ "license": "MIT",
+ "workspaces": [
+ "www"
+ ],
+ "dependencies": {
+ "@reduxjs/toolkit": "^1.9.0 || 2.x.x",
+ "clsx": "^2.1.1",
+ "decimal.js-light": "^2.5.1",
+ "es-toolkit": "^1.39.3",
+ "eventemitter3": "^5.0.1",
+ "immer": "^11.1.8",
+ "react-redux": "8.x.x || 9.x.x",
+ "reselect": "5.2.0",
+ "tiny-invariant": "^1.3.3",
+ "use-sync-external-store": "^1.2.2",
+ "victory-vendor": "^37.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "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",
+ "peer": true
+ },
+ "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.2.0",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz",
+ "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==",
+ "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/tiny-invariant": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
+ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
+ "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"
+ }
+ },
+ "node_modules/victory-vendor": {
+ "version": "37.3.6",
+ "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
+ "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
+ "license": "MIT AND ISC",
+ "dependencies": {
+ "@types/d3-array": "^3.0.3",
+ "@types/d3-ease": "^3.0.0",
+ "@types/d3-interpolate": "^3.0.1",
+ "@types/d3-scale": "^4.0.2",
+ "@types/d3-shape": "^3.1.0",
+ "@types/d3-time": "^3.0.0",
+ "@types/d3-timer": "^3.0.0",
+ "d3-array": "^3.1.6",
+ "d3-ease": "^3.0.1",
+ "d3-interpolate": "^3.0.1",
+ "d3-scale": "^4.0.2",
+ "d3-shape": "^3.1.0",
+ "d3-time": "^3.0.0",
+ "d3-timer": "^3.0.1"
+ }
+ }
+ }
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/package.json b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/package.json
new file mode 100644
index 00000000..b5198368
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/package.json
@@ -0,0 +1,5 @@
+{
+ "dependencies": {
+ "recharts": "^3.10.1"
+ }
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/reminders.txt b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/reminders.txt
new file mode 100644
index 00000000..988aee88
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/reminders.txt
@@ -0,0 +1,47 @@
+steps for testing local database
+
+start up postgres on local machine
+brew services start postgresql@18
+
+verify that postgres is running
+pg_isready
+
+go to project folder
+cd /Users/jesiel/Documents/GitHub/ConsolePythonApp
+
+start the local environment
+source .venv/bin/activate
+
+install requirements if you dont have them
+pip install -r requirements.txt
+
+set local database url
+export DATABASE_URL="postgresql://postgres.quoarrypegdpebjdesha:beqcaz-4hidpu-pemveV@aws-0-us-west-2.pooler.supabase.com:6543/postgres"
+start fastapi
+cd project1
+uvicorn main:app --reload
+
+open api docs on browser
+http://127.0.0.1:8000/docs
+
+test create account
+{
+ "owner_id": "CUSTOMER_UUID",
+ "account_type": "Checking",
+ "amount": 100
+}
+
+stop the app
+control C
+
+stop postgres
+brew services stop postgresql@18
+
+Steps for starting front end
+cd frontend
+npm ci
+cp .env.example .env
+npm run dev
+
+front end will be here
+http://localhost:5173
\ No newline at end of file
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/repository/accountsRepo.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/repository/accountsRepo.py
new file mode 100644
index 00000000..c59b93b4
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/repository/accountsRepo.py
@@ -0,0 +1,16 @@
+from sqlmodel import Session, select
+from models.account_model import AccountRecord
+
+def get_user_account_by_number(session: Session, user_id, account_num: int):
+ statement = select(AccountRecord).where(
+ AccountRecord.account_number == account_num,
+ AccountRecord.owner_id == str(user_id)
+ )
+
+ return session.exec(statement).first()
+
+def get_account_by_number(session: Session, account_num: int):
+ statement = select(AccountRecord).where(
+ AccountRecord.account_number == account_num
+ )
+ return session.exec(statement).first()
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/repository/transactionRepo.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/repository/transactionRepo.py
new file mode 100644
index 00000000..a9aa7da9
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/repository/transactionRepo.py
@@ -0,0 +1,49 @@
+from models.transaction import Transaction
+from sqlmodel import Session, select
+from sqlalchemy import or_
+from uuid import UUID
+from datetime import datetime, timezone
+
+def save_transaction(session: Session, transaction: Transaction):
+ session.add(transaction)
+ return transaction
+
+def get_transactions(session: Session, owner_id: UUID, is_admin=False):
+ statement = select(Transaction)
+ if not is_admin:
+ statement = statement.where(
+ or_(Transaction.from_owner_id == owner_id, Transaction.to_owner_id == owner_id)
+ )
+ return session.exec(statement).all()
+
+def get_withdrawals(session: Session, owner_id: UUID):
+ now = datetime.now(timezone.utc)
+
+ start_of_month = datetime(
+ now.year,
+ now.month,
+ 1,
+ tzinfo=timezone.utc
+ )
+
+ if now.month == 12:
+ start_of_next_month = datetime(
+ now.year + 1,
+ 1,
+ 1,
+ tzinfo=timezone.utc
+ )
+ else:
+ start_of_next_month = datetime(
+ now.year,
+ now.month + 1,
+ 1,
+ tzinfo=timezone.utc
+ )
+
+ statement = select(Transaction).where(
+ Transaction.from_owner_id == owner_id,
+ Transaction.transaction_date >= start_of_month,
+ Transaction.transaction_date < start_of_next_month,
+ )
+ return session.exec(statement).all()
\ No newline at end of file
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/requirements.txt b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/requirements.txt
new file mode 100644
index 00000000..16a51c43
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/requirements.txt
@@ -0,0 +1,13 @@
+fastapi==0.141.1
+uvicorn==0.52.4
+sqlmodel==0.0.39
+SQLAlchemy==2.0.52
+psycopg-binary==3.3.4
+psycopg2-binary==2.9.12
+python-jose[cryptography]==3.5.0
+passlib==1.7.4
+pwdlib==0.3.1
+argon2-cffi==25.1.0
+python-multipart==0.0.32
+email-validator==2.3.0
+pydantic==2.13.4
\ No newline at end of file
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/routers/accounts.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/routers/accounts.py
new file mode 100644
index 00000000..178bddb6
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/routers/accounts.py
@@ -0,0 +1,151 @@
+from datetime import date
+from typing import Literal
+from uuid import UUID
+
+from fastapi import APIRouter, HTTPException, Query, status
+from pydantic import BaseModel, Field
+from database import SessionDep
+from service import account_service
+from service.user_service import get_db_user
+
+router = APIRouter()
+
+#used to tell pydantic that we can only recieve checking or savings as account type
+AccountType = Literal["Checking", "Savings"]
+
+#this class is used to define the structure and validation rules of the data our api will recieve when creating accounts
+from decimal import Decimal
+class AccountCreate(BaseModel):
+ owner_id: UUID
+ account_type: AccountType
+ amount: Decimal = Field(default=Decimal("0.00"), ge=0, max_digits=12, decimal_places=2)
+
+class AccountStatusUpdate(BaseModel):
+ is_active: bool
+
+#converts an account object to a dictionary so we can return it as a json
+def account_to_dict(account):
+ return {
+ "account_number": account.account_number,
+ "owner_id": account.owner_id,
+ "balance": account.balance,
+ "account_type": account.account_type,
+ "created_date": account.created_date,
+ "is_active": account.is_active,
+ "closed_date": account.closed_date,
+ }
+
+#this can eitherbe used to get all acounts or just the accounts that have a specific owner id
+@router.get("")
+def list_accounts(
+ session: SessionDep,
+ requester_id: UUID = Query(...),
+ owner_id: UUID | None = Query(default=None)
+):
+ try:
+ requester = get_db_user(session, requester_id)
+ if not requester.is_admin:
+ if owner_id is not None and owner_id != requester_id:
+ raise HTTPException(status_code=403, detail="Customers can only see their own accounts")
+ owner_id = requester_id
+ accounts = account_service.get_accounts(session, str(owner_id) if owner_id is not None else None)
+
+ return {
+ "accounts": [
+ account_to_dict(account)
+ for account in accounts
+ ]
+ }
+
+ except ValueError as error:
+ raise HTTPException(
+ status_code=404,
+ detail=str(error)
+ )
+
+#gets a specific account using the account number
+@router.get("/{account_number}")
+def read_account(account_number: int, session: SessionDep):
+ try:
+ account = account_service.get_account(
+ session,
+ account_number
+ )
+
+ return account_to_dict(account)
+
+ except ValueError as error:
+ raise HTTPException(
+ status_code=404,
+ detail=str(error)
+ )
+
+#this is used for account creation
+@router.post(
+ "",
+ status_code=status.HTTP_201_CREATED
+)
+def create_account(
+ account_request: AccountCreate,
+ session: SessionDep,
+ requester_id: UUID = Query(...)
+):
+ try:
+ requester = get_db_user(session, requester_id)
+ if requester.is_admin or account_request.owner_id != requester_id:
+ raise HTTPException(status_code=403, detail="Only customers can create their own accounts")
+ account = account_service.create_account(
+ session=session,
+ owner_id=str(account_request.owner_id),
+ account_type=account_request.account_type,
+ balance=float(account_request.amount)
+ )
+
+ return account_to_dict(account)
+
+ except ValueError as error:
+ raise HTTPException(
+ status_code=400,
+ detail=str(error)
+ )
+
+
+@router.delete("/{account_number}")
+def delete_account(account_number: int, session: SessionDep):
+ try:
+ account_service.delete_account(
+ session,
+ account_number
+ )
+
+ return {
+ "account_number": account_number,
+ "deleted": True
+ }
+
+ except ValueError as error:
+ raise HTTPException(
+ status_code=400,
+ detail=str(error)
+ )
+
+@router.patch("/{account_number}/status")
+def update_account_status(
+ account_number: int,
+ status_request: AccountStatusUpdate,
+ session: SessionDep,
+ requester_id: UUID = Query(...)
+):
+ try:
+ requester = get_db_user(session, requester_id)
+ if not requester.is_admin:
+ raise HTTPException(status_code=403, detail="Only admins can update account status")
+ account = account_service.get_account(session, account_number)
+ account.is_active = status_request.is_active
+ account.closed_date = None if status_request.is_active else date.today()
+ session.add(account)
+ session.commit()
+ session.refresh(account)
+ return account_to_dict(account)
+ except ValueError as error:
+ raise HTTPException(status_code=404, detail=str(error))
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/routers/login.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/routers/login.py
new file mode 100644
index 00000000..57c1b84f
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/routers/login.py
@@ -0,0 +1,26 @@
+from fastapi import APIRouter, HTTPException
+
+from database import SessionDep
+from dto.login_request import LoginRequest
+from service import user_service
+
+router = APIRouter()
+
+
+@router.post("")
+def login(
+ request: LoginRequest,
+ session: SessionDep
+):
+ try:
+ return user_service.authenticate(
+ session,
+ request.email,
+ request.password
+ )
+
+ except ValueError as error:
+ raise HTTPException(
+ status_code=401,
+ detail=str(error)
+ )
\ No newline at end of file
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/routers/transactions.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/routers/transactions.py
new file mode 100644
index 00000000..61ca17a2
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/routers/transactions.py
@@ -0,0 +1,92 @@
+from fastapi import APIRouter, HTTPException
+from uuid import UUID
+
+from database import SessionDep
+from models.transaction import DepositTransaction, TransactionType, TransferTransaction, WithdrawalTransaction
+from service import transaction_service
+from service.user_service import get_db_user
+
+router = APIRouter()
+
+
+@router.post("/deposit")
+def deposit(transaction: DepositTransaction, session: SessionDep, owner_id: UUID):
+ try:
+ if get_db_user(session, owner_id).is_admin:
+ raise HTTPException(status_code=403, detail="Admins cannot make transactions")
+ return transaction_service.deposit(session, transaction, owner_id)
+ except ValueError as error:
+ raise HTTPException(status_code=400, detail=str(error))
+
+
+@router.post("/withdraw")
+def withdraw(transaction: WithdrawalTransaction, session: SessionDep, owner_id: UUID):
+ try:
+ if get_db_user(session, owner_id).is_admin:
+ raise HTTPException(status_code=403, detail="Admins cannot make transactions")
+ return transaction_service.withdrawal(session, transaction, owner_id)
+ except ValueError as error:
+ raise HTTPException(status_code=400, detail=str(error))
+
+
+@router.post("/purchase")
+def purchase(transaction: WithdrawalTransaction, session: SessionDep, owner_id: UUID):
+ try:
+ if get_db_user(session, owner_id).is_admin:
+ raise HTTPException(status_code=403, detail="Admins cannot make transactions")
+ return transaction_service.withdrawal(session, transaction, owner_id, TransactionType.PURCHASE)
+ except ValueError as error:
+ raise HTTPException(status_code=400, detail=str(error))
+
+
+@router.post("/transfer")
+def transfer(transaction: TransferTransaction, session: SessionDep, owner_id: UUID):
+ try:
+ if get_db_user(session, owner_id).is_admin:
+ raise HTTPException(status_code=403, detail="Admins cannot make transactions")
+ transaction_service.transfer(session, transaction, owner_id)
+ return {"message": "Transfer success"}
+ except ValueError as error:
+ raise HTTPException(status_code=400, detail=str(error))
+ except Exception as error:
+ raise HTTPException(status_code=400, detail="Transfer could not be completed") from error
+
+
+@router.get("")
+def read_transaction_history(session: SessionDep, owner_id: UUID):
+ user = get_db_user(session, owner_id)
+ return transaction_service.fetch_transactions(session, owner_id, user.is_admin)
+
+
+@router.post("/{transaction_id}/wager")
+def create_wager(transaction_id: int, session: SessionDep, owner_id: UUID):
+ try:
+ return transaction_service.create_wager(session, transaction_id, owner_id)
+ except ValueError as error:
+ raise HTTPException(status_code=400, detail=str(error))
+ except Exception as error:
+ raise HTTPException(status_code=500, detail=str(error)) from error
+
+
+@router.get("/spending")
+def read_spending_history(session: SessionDep, owner_id: UUID):
+
+ try:
+ return transaction_service.get_withdrawals(session, owner_id)
+ except Exception as error:
+ raise HTTPException(status_code=500, detail=str(error)) from error
+
+@router.get("/owners/{owner_id}/categories")
+def read_transaction_categories(owner_id: UUID, session: SessionDep):
+ try:
+ return transaction_service.fetchTransactionCategories(session, owner_id)
+ except Exception as error:
+ raise HTTPException(status_code=500, detail=str(error)) from error
+
+
+@router.put("/{transaction_id}/category")
+def update_transaction_category(transaction_id: int, session: SessionDep, owner_id: UUID, category: str | None = None):
+ try:
+ return transaction_service.update_transaction_category(session, transaction_id, category, owner_id)
+ except Exception as error:
+ raise HTTPException(status_code=500, detail=str(error)) from error
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/routers/users.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/routers/users.py
new file mode 100644
index 00000000..81cfdce0
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/routers/users.py
@@ -0,0 +1,118 @@
+from datetime import date
+from uuid import UUID
+
+from fastapi import APIRouter, HTTPException, status
+from pydantic import BaseModel, EmailStr
+
+
+from database import SessionDep
+from service import user_service
+
+router = APIRouter()
+
+
+class UserCreate(BaseModel):
+ email: EmailStr
+ password: str
+ is_admin: bool = False
+ birthday: date
+ phone_number: str
+ first_name: str
+ last_name: str
+
+def user_to_dict(user):
+ return {
+ "id": str(user.get_owner_id()),
+ "email": user.get_email(),
+ "is_admin": user.get_role() == "Admin",
+ "birthday": str(user.get_birthday()),
+ "phone_number": user.get_phone_number(),
+ "first_name": user.get_first_name(),
+ "last_name": user.get_last_name()
+ }
+
+
+@router.get("")
+def list_users(
+ session: SessionDep
+):
+ users = user_service.get_all_users(session)
+
+ return {
+ "users": [
+ user_to_dict(user)
+ for user in users
+ ]
+ }
+
+
+@router.get("/{user_id}")
+def read_user(
+ user_id: UUID,
+ session: SessionDep
+):
+ try:
+ user = user_service.get_user(
+ session,
+ user_id
+ )
+
+ return user_to_dict(user)
+
+ except ValueError as error:
+ raise HTTPException(
+ status_code=404,
+ detail=str(error)
+ )
+
+
+@router.post(
+ "",
+ status_code=status.HTTP_201_CREATED
+)
+def create_user(
+ user_request: UserCreate,
+ session: SessionDep
+):
+ try:
+ user = user_service.create_user(
+ session,
+ email=user_request.email,
+ password=user_request.password,
+ is_admin=user_request.is_admin,
+ birthday=user_request.birthday,
+ phone_number=user_request.phone_number,
+ first_name=user_request.first_name,
+ last_name=user_request.last_name
+ )
+
+ return user_to_dict(user)
+
+ except ValueError as error:
+ raise HTTPException(
+ status_code=409,
+ detail=str(error)
+ )
+
+
+@router.delete("/{user_id}")
+def delete_user(
+ user_id: UUID,
+ session: SessionDep
+):
+ try:
+ user_service.delete_user(
+ session,
+ user_id
+ )
+
+ return {
+ "user_id": str(user_id),
+ "deleted": True
+ }
+
+ except ValueError as error:
+ raise HTTPException(
+ status_code=404,
+ detail=str(error)
+ )
\ No newline at end of file
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/security/__init__.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/security/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/security/jwt.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/security/jwt.py
new file mode 100644
index 00000000..9729cbd2
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/security/jwt.py
@@ -0,0 +1,28 @@
+from datetime import datetime, timedelta, timezone
+from jose import jwt
+
+
+SECRET_KEY = "change-this-later"
+ALGORITHM = "HS256"
+ACCESS_TOKEN_EXPIRE_MINUTES = 30
+
+
+def create_access_token(
+ user_id: str,
+ is_admin: bool
+):
+ expire = datetime.now(timezone.utc) + timedelta(
+ minutes=ACCESS_TOKEN_EXPIRE_MINUTES
+ )
+
+ payload = {
+ "sub": user_id,
+ "is_admin": is_admin,
+ "exp": expire
+ }
+
+ return jwt.encode(
+ payload,
+ SECRET_KEY,
+ algorithm=ALGORITHM
+ )
\ No newline at end of file
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/security/password.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/security/password.py
new file mode 100644
index 00000000..885b2f48
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/security/password.py
@@ -0,0 +1,18 @@
+from pwdlib import PasswordHash
+
+
+password_hasher = PasswordHash.recommended()
+
+
+def hash_password(password: str) -> str:
+ return password_hasher.hash(password)
+
+
+def verify_password(
+ plain_password: str,
+ hashed_password: str
+) -> bool:
+ return password_hasher.verify(
+ plain_password,
+ hashed_password
+ )
\ No newline at end of file
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/service/account_service.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/service/account_service.py
new file mode 100644
index 00000000..c3754cc5
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/service/account_service.py
@@ -0,0 +1,66 @@
+#import select so we can query our database
+from sqlmodel import Session, select
+
+from models.account_model import AccountRecord
+
+#used to create a new account in the database
+from decimal import Decimal
+def create_account(
+ session: Session, #the session we will use to communicate with the db
+ owner_id,
+ account_type,
+ balance=0
+):
+ #creates a new account record object
+ account = AccountRecord(
+ owner_id=owner_id,
+ account_type=account_type,
+ balance=balance
+ )
+
+ #adds and commits the accountrecord object to the database, then refreshes the saved record
+ session.add(account)
+ session.commit()
+ session.refresh(account)
+
+ #returns the accountrecord object to the api that called this function
+ return account
+
+#used to find an account using the account number
+def get_account(session: Session, account_number):
+ account = session.get(AccountRecord, account_number)
+
+ if account is None:
+ raise ValueError("Account not found")
+
+ return account
+
+
+def get_accounts(session: Session, owner_id=None):
+ #select state is used to query our database
+ statement = select(AccountRecord)
+
+ #if no id is provided return all accounts, this will be for admins
+ if owner_id is None:
+ return session.exec(statement).all()
+
+ #if an id is provided return only the accounts owned by that id
+ statement = statement.where(AccountRecord.owner_id == owner_id)
+ return session.exec(statement).all()
+
+
+def delete_account(session: Session, account_number):
+ account = get_account(session, account_number)
+
+ #an account must have a zero balance to be deleted
+ if account.get_balance() != 0:
+ raise ValueError(
+ "Account balance must be zero before deletion"
+ )
+
+ #deletes the account from our database
+ session.delete(account)
+ #commits the changes
+ session.commit()
+
+ return True
\ No newline at end of file
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/service/transaction_service.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/service/transaction_service.py
new file mode 100644
index 00000000..5fb0649f
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/service/transaction_service.py
@@ -0,0 +1,121 @@
+from models.transaction import FetchTransactions, defaultCategories
+
+from sqlmodel import Session
+from uuid import UUID
+
+from models.transaction import DepositTransaction, Transaction, TransactionType, TransferTransaction, WithdrawalTransaction, defaultCategories, WagerResult
+from repository.accountsRepo import get_account_by_number, get_user_account_by_number
+from repository.transactionRepo import get_transactions, save_transaction, get_withdrawals
+
+
+def deposit(session: Session, request: DepositTransaction, owner_id):
+ account = get_user_account_by_number(session, owner_id, request.account_number)
+ if account is None:
+ raise ValueError("Account does not exist")
+ account.balance += float(request.amount)
+ save_transaction(session, Transaction(
+ type=TransactionType.DEPOSIT, from_owner_id=owner_id,
+ from_owner_account_number=request.account_number,
+ description=None, category=None, amount=request.amount,
+ ))
+ session.commit()
+ session.refresh(account)
+ return account
+
+
+def withdrawal(session: Session, request: WithdrawalTransaction, owner_id, transaction_type=TransactionType.WITHDRAWAL):
+ account = get_user_account_by_number(session, owner_id, request.account_number)
+ if account is None:
+ raise ValueError("Account does not exist")
+ amount = float(request.amount)
+ current = account.balance
+ if account.account_type != "Checking" and amount > current:
+ raise ValueError("Insufficient funds")
+ if account.account_type == "Checking" and current - amount < -50000:
+ raise ValueError("Overdraft limit exceeded")
+ account.balance = current - amount
+ save_transaction(session, Transaction(
+ type=transaction_type, from_owner_id=owner_id,
+ from_owner_account_number=request.account_number,
+ description=request.description, category=request.category, amount=request.amount,
+ ))
+ session.commit()
+ session.refresh(account)
+ return account
+
+
+def transfer(session: Session, request: TransferTransaction, owner_id):
+ try:
+ source = get_user_account_by_number(session, owner_id, request.from_account_number)
+ target = get_account_by_number(session, request.to_account_number)
+ if source is None or target is None:
+ raise ValueError("Account does not exist or is not owned by the specified user")
+ amount = float(request.amount)
+ if amount > source.balance:
+ raise ValueError("Insufficient funds")
+ source.balance -= amount
+ target.balance += amount
+ save_transaction(session, Transaction(
+ type=TransactionType.TRANSFER, from_owner_id=owner_id,
+ from_owner_account_number=request.from_account_number,
+ to_owner_id=target.owner_id, to_owner_account_number=request.to_account_number,
+ description=None, category=None, amount=request.amount,
+ ))
+ session.commit()
+ except Exception:
+ session.rollback()
+ raise
+
+def fetchTransactionCategories(session: Session, owner_id: UUID):
+ # TODO: support custom categories in the future
+ return list(defaultCategories)
+
+
+def fetch_transactions(session: Session, owner_id: UUID, is_admin=False):
+ return get_transactions(session, owner_id, is_admin)
+
+
+def update_transaction_category(session: Session, transaction_id: int, category: str | None, owner_id: UUID):
+ transaction = session.get(Transaction, transaction_id)
+ if transaction is None:
+ raise ValueError("Transaction not found")
+ if transaction.from_owner_id != owner_id and transaction.to_owner_id != owner_id:
+ raise ValueError("You do not have permission to update this transaction")
+ transaction.category = category
+ session.add(transaction)
+ session.commit()
+ session.refresh(transaction)
+ return transaction
+
+def create_wager(session: Session, transaction_id: int, owner_id: UUID):
+ transaction = session.get(Transaction, transaction_id)
+ if transaction is None or (transaction.from_owner_id != owner_id and transaction.to_owner_id != owner_id):
+ raise ValueError("Transaction not found")
+ if transaction.type != TransactionType.PURCHASE:
+ raise ValueError("Wagers can only be created for purchase transactions")
+ if transaction.wager_result is not None:
+ raise ValueError("Wager has already been created for this transaction")
+
+ # TODO: add usage limits
+ random = __import__('random').random()
+ wager_result = WagerResult.WIN if random > 0.5 else WagerResult.LOSS
+ transaction.wager_result = wager_result
+ # Update the account balance based on the wager result
+ account = get_user_account_by_number(session, owner_id, transaction.from_owner_account_number)
+ if wager_result == WagerResult.WIN:
+ account.balance += float(transaction.amount)
+ else:
+ account.balance -= float(transaction.amount)
+
+ session.add(transaction)
+ session.add(account)
+ session.commit()
+
+ # return wager result and updated account balance
+ return {"message": "Wager created successfully", "wager_result": wager_result, "updated_balance": account.balance}
+
+
+def fetch_withdrawals(session: Session, owner_id: UUID):
+ return get_withdrawals(session, owner_id)
+
+
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/service/user_service.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/service/user_service.py
new file mode 100644
index 00000000..56848888
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/service/user_service.py
@@ -0,0 +1,144 @@
+from datetime import date
+from uuid import UUID
+
+from sqlmodel import Session, select
+
+from dto.user_db import UserDB
+from models.admin import Admin
+from models.customer import Customer
+from security.password import hash_password, verify_password
+from security.jwt import create_access_token
+
+
+def db_user_to_domain(db_user):
+ if db_user.is_admin:
+ return Admin(
+ email=db_user.email,
+ owner_id=db_user.id,
+ birthday=db_user.birthday,
+ phone_number=db_user.phone_number,
+ first_name=db_user.first_name,
+ last_name=db_user.last_name,
+ )
+
+ return Customer(
+ email=db_user.email,
+ owner_id=db_user.id,
+ birthday=db_user.birthday,
+ phone_number=db_user.phone_number,
+ first_name=db_user.first_name,
+ last_name=db_user.last_name,
+ )
+
+
+def get_all_users(session: Session):
+ statement = select(UserDB)
+ db_users = session.exec(statement).all()
+
+ return [
+ db_user_to_domain(user)
+ for user in db_users
+ ]
+
+
+def get_user(
+ session: Session,
+ user_id: UUID
+):
+ db_user = session.get(UserDB, user_id)
+
+ if db_user is None:
+ raise ValueError("User not found")
+
+ return db_user_to_domain(db_user)
+
+
+def create_user(
+ session: Session,
+ email: str,
+ password: str,
+ is_admin: bool,
+ birthday: date,
+ phone_number: str,
+ first_name: str,
+ last_name: str,
+):
+ existing_user = session.exec(
+ select(UserDB).where(UserDB.email == email)
+ ).first()
+
+ if existing_user is not None:
+ raise ValueError("Email is already in use")
+
+ db_user = UserDB(
+ email=email,
+ password_hash=hash_password(password),
+ is_admin=is_admin,
+ birthday=birthday,
+ phone_number=phone_number,
+ first_name=first_name,
+ last_name=last_name,
+ )
+
+ session.add(db_user)
+ session.commit()
+ session.refresh(db_user)
+
+ return db_user_to_domain(db_user)
+
+
+def delete_user(
+ session: Session,
+ user_id: UUID
+):
+ db_user = session.get(UserDB, user_id)
+
+ if db_user is None:
+ raise ValueError("User not found")
+
+ session.delete(db_user)
+ session.commit()
+
+ return True
+
+
+def authenticate(
+ session: Session,
+ email: str,
+ password: str,
+):
+ user = session.exec(
+ select(UserDB).where(UserDB.email == email)
+ ).first()
+
+ if user is None:
+ raise ValueError("Invalid email or password")
+
+ if not verify_password(
+ password,
+ user.password_hash
+ ):
+ raise ValueError("Invalid email or password")
+
+ token = create_access_token(
+ user_id=str(user.id),
+ is_admin=user.is_admin,
+ )
+
+ return {
+ "access_token": token,
+ "token_type": "bearer",
+ "user_id": str(user.id),
+ "is_admin": user.is_admin,
+ }
+
+def get_db_user(
+ session: Session,
+ user_id: UUID
+):
+ user = session.get(UserDB, user_id)
+
+ if user is None:
+ raise ValueError("User not found")
+
+ return user
\ No newline at end of file
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/setup-local-dev.sh b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/setup-local-dev.sh
new file mode 100755
index 00000000..4cd2a1d9
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/setup-local-dev.sh
@@ -0,0 +1,47 @@
+#!/bin/zsh
+
+# venv activation only persists in the current shell if this script is sourced, not executed
+if [[ $ZSH_EVAL_CONTEXT != *file* ]]; then
+ echo "Run this with 'source setup-local-dev.sh' (not ./setup-local-dev.sh) so the venv activation persists in your shell." >&2
+ exit 1
+fi
+
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+VENV_DIR="$SCRIPT_DIR/.venv"
+CONTAINER_NAME="banking-postgres"
+POSTGRES_PORT="5432"
+
+if [ ! -d "$VENV_DIR" ]; then
+ echo "Creating virtual environment..."
+ python3 -m venv "$VENV_DIR" || return 1
+fi
+
+source "$VENV_DIR/bin/activate"
+
+echo "Installing requirements..."
+pip install -r "$SCRIPT_DIR/requirements.txt" || return 1
+
+if [ -n "$(docker ps -aq -f name="^${CONTAINER_NAME}$")" ]; then
+ if [ -z "$(docker ps -q -f name="^${CONTAINER_NAME}$")" ]; then
+ echo "Starting existing ${CONTAINER_NAME} container..."
+ docker start "$CONTAINER_NAME" > /dev/null
+ else
+ echo "${CONTAINER_NAME} is already running."
+ fi
+else
+ echo "Creating ${CONTAINER_NAME} container..."
+ docker run -d \
+ --name "$CONTAINER_NAME" \
+ -e POSTGRES_USER=admin \
+ -e POSTGRES_PASSWORD=admin \
+ -e POSTGRES_DB=banking \
+ -p "${POSTGRES_PORT}:5432" \
+ postgres:16-alpine > /dev/null
+fi
+
+export DATABASE_URL="postgresql://admin:admin@localhost:${POSTGRES_PORT}/banking"
+
+echo ""
+echo "Setup complete. venv is active and DATABASE_URL is exported in this shell."
+echo "To start local dev server:"
+echo " uvicorn main:app --reload"
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/tests/test_users_database.py b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/project1/tests/test_users_database.py
new file mode 100644
index 00000000..e69de29b
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/scripts/deploy-api.sh b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/scripts/deploy-api.sh
new file mode 100755
index 00000000..140e72e9
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/scripts/deploy-api.sh
@@ -0,0 +1,54 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+KEY_PATH="${1:-}"
+
+if [[ -z "$KEY_PATH" ]]; then
+ echo "Usage: $0 /path/to/private_ssh_key" >&2
+ exit 1
+fi
+
+if [[ ! -f "$KEY_PATH" ]]; then
+ echo "SSH key not found: $KEY_PATH" >&2
+ exit 1
+fi
+
+API_IP="$(terraform -chdir="$ROOT/terraform" output -raw api_public_ip)"
+SSH_OPTS=(-i "$KEY_PATH" -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null)
+
+echo "Install rsync on the EC2 instance"
+ssh "${SSH_OPTS[@]}" "ec2-user@${API_IP}" 'bash -s' <<'REMOTE'
+sudo dnf update -y && sudo dnf install -y rsync
+REMOTE
+
+
+echo "Copying project1 to ec2-user@${API_IP}:/opt/banking-api"
+rsync -az --delete \
+ --exclude '.venv/' \
+ --exclude '__pycache__/' \
+ --exclude '*.pyc' \
+ --exclude '.pytest_cache/' \
+ --exclude 'banking.db' \
+ -e "ssh ${SSH_OPTS[*]}" \
+ "$ROOT/project1/" \
+ "ec2-user@${API_IP}:/opt/banking-api/"
+
+echo "Installing Python deps and starting banking-api"
+ssh "${SSH_OPTS[@]}" "ec2-user@${API_IP}" 'bash -s' <<'REMOTE'
+set -euo pipefail
+cd /opt/banking-api
+sudo dnf install python3.11 python3.11-pip -y
+rm -rf .venv
+python3.11 -m venv .venv
+.venv/bin/pip install --upgrade pip
+.venv/bin/pip install -r requirements.txt
+sudo systemctl daemon-reload
+sudo systemctl enable --now banking-api
+sudo systemctl restart banking-api
+sleep 2
+sudo systemctl --no-pager --full status banking-api || true
+REMOTE
+
+echo "API deploy complete."
+echo "Gateway URL: $(terraform -chdir="$ROOT/terraform" output -raw api_gateway_url)"
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/scripts/deploy-frontend.sh b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/scripts/deploy-frontend.sh
new file mode 100755
index 00000000..cbb5de2b
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/scripts/deploy-frontend.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+API_URL="$(terraform -chdir="$ROOT/terraform" output -raw api_gateway_url)"
+BUCKET="$(terraform -chdir="$ROOT/terraform" output -raw frontend_bucket)"
+WEBSITE_URL="$(terraform -chdir="$ROOT/terraform" output -raw frontend_website_url)"
+
+echo "Building frontend with VITE_API_URL=${API_URL}"
+cd "$ROOT/frontend"
+VITE_API_URL="$API_URL" npm ci
+VITE_API_URL="$API_URL" npm run build
+
+echo "Uploading dist/ to s3://${BUCKET}"
+aws s3 sync "$ROOT/frontend/dist/" "s3://${BUCKET}/" --delete
+
+echo "Frontend deployed: ${WEBSITE_URL}"
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/.gitignore b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/.gitignore
new file mode 100644
index 00000000..ec120ca0
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/.gitignore
@@ -0,0 +1,9 @@
+.terraform/
+terraform.tfstate
+terraform.tfstate.backup
+terraform.tfvars
+crash.log
+override.tf
+override.tf.json
+*_override.tf
+*_override.tf.json
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/README.md b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/README.md
new file mode 100644
index 00000000..716a5200
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/README.md
@@ -0,0 +1,101 @@
+# Banking app Terraform bootstrap
+
+Provisions the grade-ready AWS network plus a Supabase Postgres database that FastAPI already speaks through `DATABASE_URL`.
+
+Created resources:
+
+- VPC, public subnet, internet gateway, public route
+- EC2 host for FastAPI (`project1`)
+- HTTP API Gateway proxying to that host
+- S3 website bucket for the Vite frontend
+- Supabase project (managed PostgreSQL)
+
+```text
+Browser -> S3 website
+ |
+ v
+ API Gateway HTTP API
+ |
+ v
+ EC2 FastAPI :8000 ---> Supabase Postgres
+```
+
+## Prerequisites
+
+- Terraform >= 1.5
+- AWS credentials (`aws configure` or `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`)
+- An SSH key pair
+- A Supabase account and [access token](https://supabase.com/dashboard/account/tokens)
+- The organization slug from the Supabase dashboard URL
+
+## 1. Configure
+
+```bash
+cd terraform
+cp terraform.tfvars.example terraform.tfvars
+```
+
+Edit `terraform.tfvars`:
+
+- `ssh_public_key` — contents of `~/.ssh/id_ed25519.pub`
+- `supabase_organization_id` — org slug
+- `supabase_db_password` — at least 12 characters
+- `ssh_ingress_cidr` — your public IP `/32` if you can
+
+Export the Supabase token (do not put it in tfvars):
+
+```bash
+export SUPABASE_ACCESS_TOKEN="sbp_..."
+```
+
+## 2. Apply
+
+```bash
+terraform init
+terraform apply
+```
+
+Copy these outputs:
+
+- `api_gateway_url` — frontend `VITE_API_URL`
+- `frontend_website_url` — public UI
+- `api_public_ip` — used by the API deploy script
+
+## 3. Deploy the FastAPI app
+
+From the repo root, using the private key that matches `ssh_public_key`:
+
+```bash
+./scripts/deploy-api.sh ~/.ssh/id_ed25519
+```
+
+This copies `project1/` to the instance, installs `requirements.txt`, and starts `uvicorn`. SQLModel will create tables on first boot against Supabase.
+
+## 4. Deploy the frontend
+
+```bash
+./scripts/deploy-frontend.sh
+```
+
+Opens the S3 website URL printed by Terraform.
+
+## Local vs deployed database
+
+Local setup still uses Docker Postgres via `project1/setup-local-dev.sh`.
+
+Deployed FastAPI uses:
+
+```text
+postgresql+psycopg2://postgres:@db..supabase.co:5432/postgres?sslmode=require
+```
+
+That is standard PostgreSQL. No application code change is required beyond `DATABASE_URL`, which Terraform injects on the EC2 host.
+
+## Tear down
+
+```bash
+cd terraform
+terraform destroy
+```
+
+This deletes the AWS resources and the Supabase project.
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/api_gateway.tf b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/api_gateway.tf
new file mode 100644
index 00000000..65daf621
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/api_gateway.tf
@@ -0,0 +1,49 @@
+resource "aws_apigatewayv2_api" "http" {
+ name = "${var.project_name}-http-api"
+ protocol_type = "HTTP"
+ description = "Public HTTP proxy in front of the FastAPI EC2 host"
+
+ cors_configuration {
+ allow_credentials = false
+ allow_headers = ["*"]
+ allow_methods = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
+ allow_origins = ["*"]
+ max_age = 300
+ }
+}
+
+resource "aws_apigatewayv2_integration" "root" {
+ api_id = aws_apigatewayv2_api.http.id
+ integration_type = "HTTP_PROXY"
+ integration_method = "ANY"
+ integration_uri = "http://${aws_instance.api.public_ip}:8000/"
+ connection_type = "INTERNET"
+ timeout_milliseconds = 29000
+}
+
+resource "aws_apigatewayv2_integration" "proxy" {
+ api_id = aws_apigatewayv2_api.http.id
+ integration_type = "HTTP_PROXY"
+ integration_method = "ANY"
+ integration_uri = "http://${aws_instance.api.public_ip}:8000/{proxy}"
+ connection_type = "INTERNET"
+ timeout_milliseconds = 29000
+}
+
+resource "aws_apigatewayv2_route" "root" {
+ api_id = aws_apigatewayv2_api.http.id
+ route_key = "ANY /"
+ target = "integrations/${aws_apigatewayv2_integration.root.id}"
+}
+
+resource "aws_apigatewayv2_route" "proxy" {
+ api_id = aws_apigatewayv2_api.http.id
+ route_key = "ANY /{proxy+}"
+ target = "integrations/${aws_apigatewayv2_integration.proxy.id}"
+}
+
+resource "aws_apigatewayv2_stage" "default" {
+ api_id = aws_apigatewayv2_api.http.id
+ name = "$default"
+ auto_deploy = true
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/ec2.tf b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/ec2.tf
new file mode 100644
index 00000000..6c29eac2
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/ec2.tf
@@ -0,0 +1,56 @@
+data "aws_ami" "amazon_linux" {
+ most_recent = true
+ owners = ["amazon"]
+
+ filter {
+ name = "name"
+ values = ["al2023-ami-*-x86_64"]
+ }
+
+ filter {
+ name = "virtualization-type"
+ values = ["hvm"]
+ }
+}
+
+resource "aws_key_pair" "api" {
+ key_name = "${var.project_name}-api-key"
+ public_key = var.ssh_public_key
+}
+
+locals {
+ database_url = format(
+ "postgresql+psycopg2://postgres.%s:%s@aws-0-us-east-1.pooler.supabase.com:5432/postgres?sslmode=require",
+ supabase_project.db.id,
+ urlencode(var.supabase_db_password),
+ )
+}
+
+resource "aws_instance" "api" {
+ ami = data.aws_ami.amazon_linux.id
+ instance_type = var.instance_type
+ subnet_id = aws_subnet.public.id
+ vpc_security_group_ids = [aws_security_group.api.id]
+ key_name = aws_key_pair.api.key_name
+ associate_public_ip_address = true
+
+ user_data = templatefile("${path.module}/user_data.sh.tpl", {
+ database_url = local.database_url
+ })
+
+ user_data_replace_on_change = true
+
+ root_block_device {
+ volume_size = 16
+ volume_type = "gp3"
+ }
+
+ tags = {
+ Name = "${var.project_name}-api"
+ }
+
+ depends_on = [
+ aws_internet_gateway.main,
+ supabase_settings.db,
+ ]
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/outputs.tf b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/outputs.tf
new file mode 100644
index 00000000..a91aa5c2
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/outputs.tf
@@ -0,0 +1,42 @@
+output "vpc_id" {
+ value = aws_vpc.main.id
+}
+
+output "public_subnet_id" {
+ value = aws_subnet.public.id
+}
+
+output "internet_gateway_id" {
+ value = aws_internet_gateway.main.id
+}
+
+output "api_public_ip" {
+ value = aws_instance.api.public_ip
+}
+
+output "api_gateway_url" {
+ description = "Set frontend VITE_API_URL to this value."
+ value = aws_apigatewayv2_api.http.api_endpoint
+}
+
+output "frontend_bucket" {
+ value = aws_s3_bucket.frontend.bucket
+}
+
+output "frontend_website_url" {
+ value = "http://${aws_s3_bucket_website_configuration.frontend.website_endpoint}"
+}
+
+output "supabase_project_ref" {
+ value = supabase_project.db.id
+}
+
+output "supabase_postgres_host" {
+ value = "db.${supabase_project.db.id}.supabase.co"
+}
+
+output "database_url" {
+ description = "Postgres URL used by FastAPI. Keep this private."
+ value = local.database_url
+ sensitive = true
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/providers.tf b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/providers.tf
new file mode 100644
index 00000000..b3ff4075
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/providers.tf
@@ -0,0 +1,16 @@
+provider "aws" {
+ region = var.aws_region
+
+ default_tags {
+ tags = {
+ Project = var.project_name
+ Environment = var.environment
+ ManagedBy = "terraform"
+ }
+ }
+}
+
+provider "supabase" {
+ # Set SUPABASE_ACCESS_TOKEN in your shell.
+ # Create a token at https://supabase.com/dashboard/account/tokens
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/s3.tf b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/s3.tf
new file mode 100644
index 00000000..a9a47d9a
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/s3.tf
@@ -0,0 +1,79 @@
+resource "random_id" "bucket" {
+ byte_length = 4
+}
+
+resource "aws_s3_bucket" "frontend" {
+ bucket = "${var.project_name}-frontend-${random_id.bucket.hex}"
+ force_destroy = true
+}
+
+resource "aws_s3_bucket_public_access_block" "frontend" {
+ bucket = aws_s3_bucket.frontend.id
+
+ block_public_acls = false
+ block_public_policy = false
+ ignore_public_acls = false
+ restrict_public_buckets = false
+}
+
+resource "aws_s3_bucket_website_configuration" "frontend" {
+ bucket = aws_s3_bucket.frontend.id
+
+ index_document {
+ suffix = "index.html"
+ }
+
+ error_document {
+ key = "index.html"
+ }
+}
+
+resource "aws_s3_bucket_ownership_controls" "frontend" {
+ bucket = aws_s3_bucket.frontend.id
+
+ rule {
+ object_ownership = "BucketOwnerPreferred"
+ }
+}
+
+data "aws_iam_policy_document" "frontend_public_read" {
+ statement {
+ sid = "PublicReadGetObject"
+ effect = "Allow"
+
+ principals {
+ type = "*"
+ identifiers = ["*"]
+ }
+
+ actions = ["s3:GetObject"]
+ resources = ["${aws_s3_bucket.frontend.arn}/*"]
+ }
+}
+
+resource "aws_s3_bucket_policy" "frontend" {
+ bucket = aws_s3_bucket.frontend.id
+ policy = data.aws_iam_policy_document.frontend_public_read.json
+
+ depends_on = [
+ aws_s3_bucket_public_access_block.frontend,
+ aws_s3_bucket_ownership_controls.frontend,
+ ]
+}
+
+resource "aws_s3_object" "placeholder" {
+ bucket = aws_s3_bucket.frontend.id
+ key = "index.html"
+ content_type = "text/html"
+ content = <<-HTML
+
+
+ ${var.project_name}
+
+ Frontend bucket is ready. Run scripts/deploy-frontend.sh after terraform apply.
+
+
+ HTML
+
+ depends_on = [aws_s3_bucket_policy.frontend]
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/supabase.tf b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/supabase.tf
new file mode 100644
index 00000000..27faa919
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/supabase.tf
@@ -0,0 +1,26 @@
+resource "supabase_project" "db" {
+ organization_id = var.supabase_organization_id
+ name = var.supabase_project_name
+ database_password = var.supabase_db_password
+ region = var.supabase_region
+
+ timeouts {
+ create = "30m"
+ update = "15m"
+ }
+}
+
+resource "supabase_settings" "db" {
+ project_ref = supabase_project.db.id
+
+ network = jsonencode({
+ restrictions = [
+ "0.0.0.0/0",
+ "::/0"
+ ]
+ })
+
+ database = jsonencode({
+ statement_timeout = "30s"
+ })
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/terraform.tfvars.example b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/terraform.tfvars.example
new file mode 100644
index 00000000..2df1c766
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/terraform.tfvars.example
@@ -0,0 +1,12 @@
+aws_region = "us-east-1"
+project_name = "banking-app"
+environment = "prod"
+vpc_cidr = "10.0.0.0/16"
+public_subnet_cidr = "10.0.1.0/24"
+instance_type = "t3.micro"
+ssh_public_key = "ssh-ed25519 AAAA... your-key"
+ssh_ingress_cidr = "0.0.0.0/0"
+supabase_organization_id = "your-org-slug"
+supabase_project_name = "banking-db"
+supabase_region = "us-east-1"
+supabase_db_password = "change-me-to-a-long-password"
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/user_data.sh.tpl b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/user_data.sh.tpl
new file mode 100644
index 00000000..73e1bbee
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/user_data.sh.tpl
@@ -0,0 +1,35 @@
+#!/bin/bash
+set -euxo pipefail
+
+dnf update -y
+dnf install -y python3 python3-pip git
+
+mkdir -p /opt/banking-api
+chown ec2-user:ec2-user /opt/banking-api
+
+cat >/etc/banking-api.env <<'EOF'
+DATABASE_URL=${database_url}
+EOF
+chmod 600 /etc/banking-api.env
+
+cat >/etc/systemd/system/banking-api.service <<'EOF'
+[Unit]
+Description=Banking FastAPI service
+After=network.target
+
+[Service]
+Type=simple
+User=ec2-user
+WorkingDirectory=/opt/banking-api
+EnvironmentFile=/etc/banking-api.env
+ExecStart=/opt/banking-api/.venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000
+Restart=on-failure
+RestartSec=5
+
+[Install]
+WantedBy=multi-user.target
+EOF
+
+systemctl daemon-reload
+systemctl enable banking-api
+# The unit starts after project1 is copied by scripts/deploy-api.sh
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/variables.tf b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/variables.tf
new file mode 100644
index 00000000..1215bc44
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/variables.tf
@@ -0,0 +1,75 @@
+variable "project_name" {
+ type = string
+ description = "Short name used for AWS resource names."
+ default = "banking-app"
+}
+
+variable "environment" {
+ type = string
+ description = "Deployment environment label."
+ default = "prod"
+}
+
+variable "aws_region" {
+ type = string
+ description = "AWS region for VPC, EC2, API Gateway, and S3."
+ default = "us-east-1"
+}
+
+variable "vpc_cidr" {
+ type = string
+ description = "CIDR block for the VPC."
+ default = "10.0.0.0/16"
+}
+
+variable "public_subnet_cidr" {
+ type = string
+ description = "CIDR block for the public subnet."
+ default = "10.0.1.0/24"
+}
+
+variable "instance_type" {
+ type = string
+ description = "EC2 instance type that runs FastAPI."
+ default = "t3.micro"
+}
+
+variable "ssh_public_key" {
+ type = string
+ description = "Public key uploaded as an EC2 key pair for API deploys."
+}
+
+variable "ssh_ingress_cidr" {
+ type = string
+ description = "CIDR allowed to SSH to the API instance. Tighten this to your IP."
+ default = "0.0.0.0/0"
+}
+
+variable "supabase_organization_id" {
+ type = string
+ description = "Supabase organization slug from the dashboard URL or org settings."
+}
+
+variable "supabase_project_name" {
+ type = string
+ description = "Name of the Supabase Postgres project."
+ default = "banking-db"
+}
+
+variable "supabase_region" {
+ type = string
+ description = "Supabase region. Use a region close to aws_region."
+ default = "us-east-1"
+}
+
+variable "supabase_db_password" {
+ type = string
+ sensitive = true
+ description = "Postgres password for the Supabase project. Use a strong password."
+
+ validation {
+ condition = length(var.supabase_db_password) >= 12
+ error_message = "supabase_db_password must be at least 12 characters."
+ }
+}
+
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/versions.tf b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/versions.tf
new file mode 100644
index 00000000..c9f41ede
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/versions.tf
@@ -0,0 +1,18 @@
+terraform {
+ required_version = ">= 1.5.0"
+
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = "~> 5.0"
+ }
+ supabase = {
+ source = "supabase/supabase"
+ version = "~> 1.0"
+ }
+ random = {
+ source = "hashicorp/random"
+ version = "~> 3.6"
+ }
+ }
+}
diff --git a/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/vpc.tf b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/vpc.tf
new file mode 100644
index 00000000..5d7bd22a
--- /dev/null
+++ b/workshops/fullstack-aws/projects/submissions/Group_1__Blake_Celestian__Banking_App/ConsolePythonApp/terraform/vpc.tf
@@ -0,0 +1,83 @@
+data "aws_availability_zones" "available" {
+ state = "available"
+}
+
+resource "aws_vpc" "main" {
+ cidr_block = var.vpc_cidr
+ enable_dns_support = true
+ enable_dns_hostnames = true
+
+ tags = {
+ Name = "${var.project_name}-vpc"
+ }
+}
+
+resource "aws_internet_gateway" "main" {
+ vpc_id = aws_vpc.main.id
+
+ tags = {
+ Name = "${var.project_name}-igw"
+ }
+}
+
+resource "aws_subnet" "public" {
+ vpc_id = aws_vpc.main.id
+ cidr_block = var.public_subnet_cidr
+ availability_zone = data.aws_availability_zones.available.names[0]
+ map_public_ip_on_launch = true
+
+ tags = {
+ Name = "${var.project_name}-public-subnet"
+ }
+}
+
+resource "aws_route_table" "public" {
+ vpc_id = aws_vpc.main.id
+
+ route {
+ cidr_block = "0.0.0.0/0"
+ gateway_id = aws_internet_gateway.main.id
+ }
+
+ tags = {
+ Name = "${var.project_name}-public-rt"
+ }
+}
+
+resource "aws_route_table_association" "public" {
+ subnet_id = aws_subnet.public.id
+ route_table_id = aws_route_table.public.id
+}
+
+resource "aws_security_group" "api" {
+ name = "${var.project_name}-api-sg"
+ description = "Allow SSH and FastAPI traffic to the API host"
+ vpc_id = aws_vpc.main.id
+
+ ingress {
+ description = "SSH"
+ from_port = 22
+ to_port = 22
+ protocol = "tcp"
+ cidr_blocks = [var.ssh_ingress_cidr]
+ }
+
+ ingress {
+ description = "FastAPI via API Gateway HTTP proxy"
+ from_port = 8000
+ to_port = 8000
+ protocol = "tcp"
+ cidr_blocks = ["0.0.0.0/0"]
+ }
+
+ egress {
+ from_port = 0
+ to_port = 0
+ protocol = "-1"
+ cidr_blocks = ["0.0.0.0/0"]
+ }
+
+ tags = {
+ Name = "${var.project_name}-api-sg"
+ }
+}