From fc2e9e621e18327f3d5f30a1738fbbf118940948 Mon Sep 17 00:00:00 2001 From: Kodjo Sossouvi Date: Fri, 26 Sep 2025 23:58:50 +0200 Subject: [PATCH] App with a login page --- Readme.md | 99 +- requirements.txt | 1 + src/file-processor/requirements.txt | 1 + src/frontend/package-lock.json | 1116 ++++++++++++++++- src/frontend/package.json | 9 +- src/frontend/src/App.css | 3 + src/frontend/src/App.jsx | 59 +- .../src/components/auth/AuthLayout.jsx | 33 + .../src/components/auth/LoginForm.jsx | 203 +++ src/frontend/src/components/common/Header.jsx | 70 ++ src/frontend/src/components/common/Layout.jsx | 15 + .../src/components/common/ProtectedRoute.jsx | 69 + src/frontend/src/contexts/AuthContext.jsx | 205 +++ src/frontend/src/hooks/useAuth.js | 12 + src/frontend/src/index.css | 72 +- src/frontend/src/pages/DashboardPage.jsx | 239 ++++ src/frontend/src/pages/LoginPage.jsx | 48 + src/frontend/src/services/authService.js | 101 ++ src/frontend/src/utils/api.js | 55 + src/frontend/tailwind.config.js | 14 + src/frontend/vite.config.js | 7 +- 21 files changed, 2251 insertions(+), 180 deletions(-) create mode 100644 src/frontend/src/components/auth/AuthLayout.jsx create mode 100644 src/frontend/src/components/auth/LoginForm.jsx create mode 100644 src/frontend/src/components/common/Header.jsx create mode 100644 src/frontend/src/components/common/Layout.jsx create mode 100644 src/frontend/src/components/common/ProtectedRoute.jsx create mode 100644 src/frontend/src/contexts/AuthContext.jsx create mode 100644 src/frontend/src/hooks/useAuth.js create mode 100644 src/frontend/src/pages/DashboardPage.jsx create mode 100644 src/frontend/src/pages/LoginPage.jsx create mode 100644 src/frontend/src/services/authService.js create mode 100644 src/frontend/src/utils/api.js create mode 100644 src/frontend/tailwind.config.js diff --git a/Readme.md b/Readme.md index 2625f99..3b17796 100644 --- a/Readme.md +++ b/Readme.md @@ -348,14 +348,6 @@ class ProcessingJob(BaseModel): - **Rationale**: Full compatibility with Celery workers and simplified workflow - **Implementation**: All repositories and services operate synchronously for seamless integration -### Implementation Status - -1. ✅ Pydantic models for MongoDB collections -2. ✅ Repository layer for data access (files + processing_jobs + users + documents) - synchronous -3. ✅ Service layer for business logic (auth, user, document, job) - synchronous -4. ✅ Celery tasks for document processing -5. ✅ Watchdog file monitoring implementation -6. ✅ FastAPI integration and startup coordination ## Job Management Layer @@ -493,15 +485,88 @@ src/file-processor/app/ ### Next Implementation Steps -1. **TODO**: Complete file processing pipeline => - 1. ✅ Create Pydantic models for files and processing_jobs collections - 2. ✅ Implement repository layer for file and processing job data access (synchronous) - 3. ✅ Implement service layer for business logic (synchronous) - 4. ✅ Create Celery tasks for document processing (.txt, .pdf, .docx) - 5. ✅ Implement Watchdog file monitoring with dedicated observer - 6. ✅ Integrate file watcher with FastAPI startup -2. Create protected API routes for user management -3. Build React monitoring interface with authentication +1. Build React Login Page +2. Build React Registration Page +3. Build React Default Dashboard +4. Build React User Management Pages + +#### Validated Folders and files +``` +src/frontend/src/ +├── components/ +│ ├── auth/ +│ │ ├── LoginForm.jsx # Composant formulaire de login => Done +│ │ └── AuthLayout.jsx # Layout pour les pages d'auth => Done +│ └── common/ +│ ├── Header.jsx # Header commun => TODO +│ ├── Layout.jsx # Header commun => TODO +│ └── ProtectedRoutes.jsx # Done +├── contexts/ +│ └── AuthContext.jsx # Done +├── pages/ +│ ├── LoginPage.jsx # Page complète de login => Done +│ └── DashboardPage.jsx # Page tableau de bord (exemple) => TODO +├── services/ +│ └── authService.js # Service API pour auth => Done +├── hooks/ +│ └── useAuth.js # Hook React pour gestion auth => TODO +├── utils/ +│ └── api.js # Configuration axios/fetch => Done +├── App.jsx # Needs to be updated => TODO +``` +#### Choices already made +* Pour la gestion des requêtes API et de l'état d'authentification, je propose + * axios (plus de fonctionnalités) : + * Installation d'axios pour les requêtes HTTP + * Intercepteurs pour gestion automatique du token + * Gestion d'erreurs centralisée +* Pour la gestion de l'état d'authentification et la navigation : Option A + C en même temps + * Option A - Context React + React Router : + * React Context pour l'état global d'auth (user, token, isAuthenticated) + * React Router pour la navigation entre pages + * Routes protégées automatiques + * Option C - Context + localStorage pour persistance : + * Token sauvegardé en localStorage pour rester connecté + * Context qui se recharge au démarrage de l'app +* CSS : Utilisation de daisyUI + +#### Package.json +``` +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@tailwindcss/vite": "^4.1.13", + "axios": "^1.12.2", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-router-dom": "^7.9.3" + }, + "devDependencies": { + "@eslint/js": "^9.33.0", + "@types/react": "^19.1.10", + "@types/react-dom": "^19.1.7", + "@vitejs/plugin-react": "^5.0.0", + "autoprefixer": "^10.4.21", + "daisyui": "^5.1.23", + "eslint": "^9.33.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.13", + "vite": "^7.1.2" + } +} +``` ## Annexes diff --git a/requirements.txt b/requirements.txt index 9b882a8..0bf5cc1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -47,6 +47,7 @@ pytest-mock==3.15.1 python-dateutil==2.9.0.post0 python-dotenv==1.1.1 python-magic==0.4.27 +python-multipart==0.0.20 pytz==2025.2 PyYAML==6.0.2 redis==6.4.0 diff --git a/src/file-processor/requirements.txt b/src/file-processor/requirements.txt index 5198e6f..2e0c05d 100644 --- a/src/file-processor/requirements.txt +++ b/src/file-processor/requirements.txt @@ -8,6 +8,7 @@ motor==3.7.1 pydantic==2.11.9 PyJWT==2.10.1 pymongo==4.15.0 +python-multipart==0.0.20 redis==6.4.0 uvicorn==0.35.0 python-magic==0.4.27 diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index db82ae7..8ce991a 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -8,18 +8,25 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@tailwindcss/vite": "^4.1.13", + "axios": "^1.12.2", "react": "^19.1.1", - "react-dom": "^19.1.1" + "react-dom": "^19.1.1", + "react-router-dom": "^7.9.3" }, "devDependencies": { "@eslint/js": "^9.33.0", "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", "@vitejs/plugin-react": "^5.0.0", + "autoprefixer": "^10.4.21", + "daisyui": "^5.1.23", "eslint": "^9.33.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.13", "vite": "^7.1.2" } }, @@ -54,6 +61,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -312,7 +320,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -329,7 +336,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -346,7 +352,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -363,7 +368,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -380,7 +384,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -397,7 +400,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -414,7 +416,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -431,7 +432,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -448,7 +448,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -465,7 +464,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -482,7 +480,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -499,7 +496,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -516,7 +512,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -533,7 +528,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -550,7 +544,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -567,7 +560,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -584,7 +576,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -601,7 +592,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -618,7 +608,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -635,7 +624,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -652,7 +640,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -669,7 +656,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -686,7 +672,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -703,7 +688,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -720,7 +704,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -737,7 +720,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -953,11 +935,22 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, "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", @@ -968,7 +961,6 @@ "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", @@ -979,7 +971,6 @@ "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" @@ -989,14 +980,12 @@ "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", @@ -1017,7 +1006,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1031,7 +1019,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1045,7 +1032,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1059,7 +1045,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1073,7 +1058,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1087,7 +1071,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1101,7 +1084,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1115,7 +1097,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1129,7 +1110,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1143,7 +1123,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1157,7 +1136,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1171,7 +1149,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1185,7 +1162,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1199,7 +1175,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1213,7 +1188,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1227,7 +1201,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1241,7 +1214,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1255,7 +1227,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1269,7 +1240,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1283,7 +1253,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1297,13 +1266,274 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ] }, + "node_modules/@tailwindcss/node": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.13.tgz", + "integrity": "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.5.1", + "lightningcss": "1.30.1", + "magic-string": "^0.30.18", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.13" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.13.tgz", + "integrity": "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.13", + "@tailwindcss/oxide-darwin-arm64": "4.1.13", + "@tailwindcss/oxide-darwin-x64": "4.1.13", + "@tailwindcss/oxide-freebsd-x64": "4.1.13", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.13", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.13", + "@tailwindcss/oxide-linux-x64-musl": "4.1.13", + "@tailwindcss/oxide-wasm32-wasi": "4.1.13", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.13" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.13.tgz", + "integrity": "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.13.tgz", + "integrity": "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.13.tgz", + "integrity": "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.13.tgz", + "integrity": "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.13.tgz", + "integrity": "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.13.tgz", + "integrity": "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.13.tgz", + "integrity": "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.13.tgz", + "integrity": "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.13.tgz", + "integrity": "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.13.tgz", + "integrity": "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.5", + "@emnapi/runtime": "^1.4.5", + "@emnapi/wasi-threads": "^1.0.4", + "@napi-rs/wasm-runtime": "^0.2.12", + "@tybys/wasm-util": "^0.10.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.13.tgz", + "integrity": "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.13.tgz", + "integrity": "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.13.tgz", + "integrity": "sha512-0PmqLQ010N58SbMTJ7BVJ4I2xopiQn/5i6nlb4JmxzQf8zcS5+m2Cv6tqh+sfDwtIdjoEnOvwsGQ1hkUi8QEHQ==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.13", + "@tailwindcss/oxide": "4.1.13", + "tailwindcss": "4.1.13" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1353,7 +1583,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { @@ -1369,6 +1598,7 @@ "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -1410,6 +1640,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1467,6 +1698,61 @@ "dev": true, "license": "Python-2.0" }, + "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/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1515,6 +1801,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.3", "caniuse-lite": "^1.0.30001741", @@ -1529,6 +1816,19 @@ "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/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1577,6 +1877,15 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1597,6 +1906,18 @@ "dev": true, "license": "MIT" }, + "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/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1611,6 +1932,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1633,6 +1963,16 @@ "dev": true, "license": "MIT" }, + "node_modules/daisyui": { + "version": "5.1.23", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.1.23.tgz", + "integrity": "sha512-HgxmDidqO0FrcrkMVRz7KYwQi+eoZarMVud2kx5cGhTmTlaJPne/gbsSEn4/vM1svbp40ENCMjJsbBldCXhrOA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/saadeghi/daisyui?sponsor=1" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1658,6 +1998,38 @@ "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.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz", + "integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==", + "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.218", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.218.tgz", @@ -1665,11 +2037,68 @@ "dev": true, "license": "ISC" }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "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.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "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/esbuild": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -1736,6 +2165,7 @@ "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -1933,7 +2363,6 @@ "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" @@ -1998,11 +2427,60 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "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.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, "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, @@ -2013,6 +2491,15 @@ "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", @@ -2023,6 +2510,43 @@ "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", @@ -2049,6 +2573,24 @@ "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/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2059,6 +2601,45 @@ "node": ">=8" } }, + "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.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2126,6 +2707,15 @@ "dev": true, "license": "ISC" }, + "node_modules/jiti": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz", + "integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2217,6 +2807,234 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "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-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "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.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "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", @@ -2250,6 +3068,45 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "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": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -2263,6 +3120,27 @@ "node": "*" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2274,7 +3152,6 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, "funding": [ { "type": "github", @@ -2303,6 +3180,16 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -2390,15 +3277,14 @@ "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.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -2410,7 +3296,6 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, "funding": [ { "type": "opencollective", @@ -2426,6 +3311,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -2435,6 +3321,13 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -2445,6 +3338,12 @@ "node": ">= 0.8.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -2460,6 +3359,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -2469,6 +3369,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -2486,6 +3387,44 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.9.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.3.tgz", + "integrity": "sha512-4o2iWCFIwhI/eYAIL43+cjORXYn/aRQPgtFRRZb3VzoyQ5Uej0Bmqj7437L97N9NJW4wnicSwLOLS+yCXfAPgg==", + "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.9.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.3.tgz", + "integrity": "sha512-1QSbA0TGGFKTAc/aWjpfW/zoEukYfU4dc1dLkT/vvf54JoGMkW+fNA+3oyo2gWVW1GM7BxjJVHz5GnPJv40rvg==", + "license": "MIT", + "dependencies": { + "react-router": "7.9.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2500,7 +3439,6 @@ "version": "4.50.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.2.tgz", "integrity": "sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w==", - "dev": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -2553,6 +3491,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2580,7 +3524,6 @@ "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" @@ -2612,11 +3555,54 @@ "node": ">=8" } }, + "node_modules/tailwindcss": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz", + "integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", + "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", + "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -2687,8 +3673,8 @@ "version": "7.1.5", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz", "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==", - "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", diff --git a/src/frontend/package.json b/src/frontend/package.json index c3894ae..29fd015 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -10,18 +10,25 @@ "preview": "vite preview" }, "dependencies": { + "@tailwindcss/vite": "^4.1.13", + "axios": "^1.12.2", "react": "^19.1.1", - "react-dom": "^19.1.1" + "react-dom": "^19.1.1", + "react-router-dom": "^7.9.3" }, "devDependencies": { "@eslint/js": "^9.33.0", "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", "@vitejs/plugin-react": "^5.0.0", + "autoprefixer": "^10.4.21", + "daisyui": "^5.1.23", "eslint": "^9.33.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", "globals": "^16.3.0", + "postcss": "^8.5.6", + "tailwindcss": "^4.1.13", "vite": "^7.1.2" } } diff --git a/src/frontend/src/App.css b/src/frontend/src/App.css index b9d355d..c1572d4 100644 --- a/src/frontend/src/App.css +++ b/src/frontend/src/App.css @@ -1,3 +1,6 @@ +@import "tailwindcss"; +@plugin "daisyui"; + #root { max-width: 1280px; margin: 0 auto; diff --git a/src/frontend/src/App.jsx b/src/frontend/src/App.jsx index f67355a..0c9c15a 100644 --- a/src/frontend/src/App.jsx +++ b/src/frontend/src/App.jsx @@ -1,35 +1,34 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; +import { AuthProvider } from './contexts/AuthContext'; +import ProtectedRoute from './components/common/ProtectedRoute'; +import Layout from './components/common/Layout'; +import LoginPage from './pages/LoginPage'; +import DashboardPage from './pages/DashboardPage'; function App() { - const [count, setCount] = useState(0) - return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.jsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

- - ) + + +
+ + {/* Public Routes */} + } /> + + {/* Protected Routes */} + }> + } /> + } /> + Documents Page - Coming Soon
} /> + User Management - Coming Soon} /> + + + {/* Catch all route */} + } /> + + +
+
+ ); } -export default App +export default App; \ No newline at end of file diff --git a/src/frontend/src/components/auth/AuthLayout.jsx b/src/frontend/src/components/auth/AuthLayout.jsx new file mode 100644 index 0000000..c26ac84 --- /dev/null +++ b/src/frontend/src/components/auth/AuthLayout.jsx @@ -0,0 +1,33 @@ +import React from 'react'; + +/** + * AuthLayout component for authentication pages + * Provides centered layout with background and responsive design + * + * @param {Object} props - Component props + * @param {React.ReactNode} props.children - Child components to render + */ +function AuthLayout({children}) { + return ( +
+ {/* Main container with flex centering */} +
+ {/* Content wrapper for responsive spacing */} +
+ {children} +
+
+ + {/* Optional decorative elements */} +
+ {/* Subtle geometric background pattern */} +
+
+
+
+
+ ); +} + +export default AuthLayout; \ No newline at end of file diff --git a/src/frontend/src/components/auth/LoginForm.jsx b/src/frontend/src/components/auth/LoginForm.jsx new file mode 100644 index 0000000..d53e0f5 --- /dev/null +++ b/src/frontend/src/components/auth/LoginForm.jsx @@ -0,0 +1,203 @@ +import React, {useEffect, useState} from 'react'; +import {useAuth} from '../../contexts/AuthContext'; + +/** + * LoginForm component with DaisyUI styling + * Handles user authentication with form validation and error display + */ +function LoginForm() { + const {login, loading, error, clearError} = useAuth(); + const [formData, setFormData] = useState({ + username: '', + password: '', + }); + const [formErrors, setFormErrors] = useState({}); + + // Clear errors when component mounts or form data changes + useEffect(() => { + if (error) { + const timer = setTimeout(() => { + clearError(); + }, 5000); // Clear error after 5 seconds + + return () => clearTimeout(timer); + } + }, [error, clearError]); + + /** + * Handle input changes and clear related errors + * @param {Event} e - Input change event + */ + const handleInputChange = (e) => { + const {name, value} = e.target; + + setFormData(prev => ({ + ...prev, + [name]: value, + })); + + // Clear field error when user starts typing + if (formErrors[name]) { + setFormErrors(prev => ({ + ...prev, + [name]: '', + })); + } + + // Clear global error when user modifies form + if (error) { + clearError(); + } + }; + + /** + * Validate form data before submission + * @returns {boolean} True if form is valid + */ + const validateForm = () => { + const errors = {}; + + if (!formData.username.trim()) { + errors.username = 'Username is required'; + } + + if (!formData.password.trim()) { + errors.password = 'Password is required'; + } else if (formData.password.length < 3) { + errors.password = 'Password must be at least 3 characters'; + } + + setFormErrors(errors); + return Object.keys(errors).length === 0; + }; + + /** + * Handle form submission + * @param {Event} e - Form submission event + */ + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!validateForm()) { + return; + } + + const success = await login(formData.username, formData.password); + + if (success) { + // Reset form on successful login + setFormData({username: '', password: ''}); + setFormErrors({}); + } + }; + + return ( +
+
+ {/* Card Header */} +
+

MyDocManager

+

Sign in to your account

+
+ + {/* Global Error Alert */} + {error && ( +
+ + + + {error} +
+ )} + + {/* Login Form */} +
+ {/* Username Field */} +
+ + + {formErrors.username && ( + + )} +
+ + {/* Password Field */} +
+ + + {formErrors.password && ( + + )} +
+ + {/* Submit Button */} +
+ +
+
+ + {/* Additional Info */} +
+

+ Don't have an account? Contact your administrator. +

+
+
+
+ ); +} + +export default LoginForm; \ No newline at end of file diff --git a/src/frontend/src/components/common/Header.jsx b/src/frontend/src/components/common/Header.jsx new file mode 100644 index 0000000..24b8b46 --- /dev/null +++ b/src/frontend/src/components/common/Header.jsx @@ -0,0 +1,70 @@ +import { useAuth } from '../../hooks/useAuth'; +import { useNavigate } from 'react-router-dom'; + +const Header = () => { + const { user, logout } = useAuth(); + const navigate = useNavigate(); + + const handleLogout = async () => { + await logout(); + navigate('/login'); + }; + + return ( +
+
+
+
+ + + +
+ +
+ + MyDocManager + +
+ +
+ +
+ +
+
+
+
+ + {user?.username?.charAt(0).toUpperCase()} + +
+
+
    +
  • +
    + Profile + {user?.role} +
    +
  • +
  • Settings
  • +
  • +
+
+
+
+ ); +}; + +export default Header; \ No newline at end of file diff --git a/src/frontend/src/components/common/Layout.jsx b/src/frontend/src/components/common/Layout.jsx new file mode 100644 index 0000000..3a65081 --- /dev/null +++ b/src/frontend/src/components/common/Layout.jsx @@ -0,0 +1,15 @@ +import Header from './Header'; +import {Outlet} from 'react-router-dom'; + +const Layout = () => { + return ( +
+
+
+ +
+
+ ); +}; + +export default Layout; \ No newline at end of file diff --git a/src/frontend/src/components/common/ProtectedRoute.jsx b/src/frontend/src/components/common/ProtectedRoute.jsx new file mode 100644 index 0000000..530529e --- /dev/null +++ b/src/frontend/src/components/common/ProtectedRoute.jsx @@ -0,0 +1,69 @@ +import React from 'react'; +import {Navigate, useLocation} from 'react-router-dom'; +import {useAuth} from '../../contexts/AuthContext'; + +/** + * ProtectedRoute component to guard routes that require authentication + * Redirects to login if user is not authenticated, preserving intended destination + * + * @param {Object} props - Component props + * @param {React.ReactNode} props.children - Child components to render if authenticated + * @param {string[]} props.allowedRoles - Array of roles allowed to access this route (optional) + */ +function ProtectedRoute({children, allowedRoles = []}) { + const {isAuthenticated, loading, user} = useAuth(); + const location = useLocation(); + + // Show loading spinner while checking authentication + if (loading) { + return ( +
+
+ +

Checking authentication...

+
+
+ ); + } + + // Redirect to login if not authenticated + if (!isAuthenticated) { + return ( + + ); + } + + // Check role-based access if allowedRoles is specified + if (allowedRoles.length > 0 && user && !allowedRoles.includes(user.role)) { + return ( +
+
+
+
🚫
+

Access Denied

+

+ You don't have permission to access this page. +

+
+ +
+
+
+
+ ); + } + + // User is authenticated and authorized, render children + return children; +} + +export default ProtectedRoute; \ No newline at end of file diff --git a/src/frontend/src/contexts/AuthContext.jsx b/src/frontend/src/contexts/AuthContext.jsx new file mode 100644 index 0000000..bb0bcb2 --- /dev/null +++ b/src/frontend/src/contexts/AuthContext.jsx @@ -0,0 +1,205 @@ +import React, {createContext, useContext, useEffect, useReducer} from 'react'; +import authService from '../services/authService'; + +// Auth state actions +const AUTH_ACTIONS = { + LOGIN_START: 'LOGIN_START', + LOGIN_SUCCESS: 'LOGIN_SUCCESS', + LOGIN_FAILURE: 'LOGIN_FAILURE', + LOGOUT: 'LOGOUT', + LOAD_USER: 'LOAD_USER', + CLEAR_ERROR: 'CLEAR_ERROR', +}; + +// Initial state +const initialState = { + user: null, + token: null, + isAuthenticated: false, + loading: true, // Loading true initially to check stored auth + error: null, +}; + +// Auth reducer to manage state transitions +function authReducer(state, action) { + switch (action.type) { + case AUTH_ACTIONS.LOGIN_START: + return { + ...state, + loading: true, + error: null, + }; + + case AUTH_ACTIONS.LOGIN_SUCCESS: + return { + ...state, + user: action.payload.user, + token: action.payload.token, + isAuthenticated: true, + loading: false, + error: null, + }; + + case AUTH_ACTIONS.LOGIN_FAILURE: + return { + ...state, + user: null, + token: null, + isAuthenticated: false, + loading: false, + error: action.payload.error, + }; + + case AUTH_ACTIONS.LOGOUT: + return { + ...state, + user: null, + token: null, + isAuthenticated: false, + loading: false, + error: null, + }; + + case AUTH_ACTIONS.LOAD_USER: + return { + ...state, + user: action.payload.user, + token: action.payload.token, + isAuthenticated: !!action.payload.token, + loading: false, + error: null, + }; + + case AUTH_ACTIONS.CLEAR_ERROR: + return { + ...state, + error: null, + }; + + default: + return state; + } +} + +// Create context +const AuthContext = createContext(null); + +/** + * AuthProvider component to wrap the app and provide authentication state + * @param {Object} props - Component props + * @param {React.ReactNode} props.children - Child components + */ +export function AuthProvider({children}) { + const [state, dispatch] = useReducer(authReducer, initialState); + + // Load stored authentication data on app startup + useEffect(() => { + const loadStoredAuth = () => { + const token = authService.getStoredToken(); + const user = authService.getStoredUser(); + + dispatch({ + type: AUTH_ACTIONS.LOAD_USER, + payload: {user, token}, + }); + }; + + loadStoredAuth(); + }, []); + + /** + * Login function to authenticate user + * @param {string} username - User's username + * @param {string} password - User's password + * @returns {Promise} True if login successful + */ + const login = async (username, password) => { + try { + dispatch({type: AUTH_ACTIONS.LOGIN_START}); + + const {access_token, user} = await authService.login(username, password); + + dispatch({ + type: AUTH_ACTIONS.LOGIN_SUCCESS, + payload: {user, token: access_token}, + }); + + return true; + } catch (error) { + dispatch({ + type: AUTH_ACTIONS.LOGIN_FAILURE, + payload: {error: error.message}, + }); + return false; + } + }; + + /** + * Logout function to clear authentication state + */ + const logout = () => { + authService.logout(); + dispatch({type: AUTH_ACTIONS.LOGOUT}); + }; + + /** + * Clear error message from state + */ + const clearError = () => { + dispatch({type: AUTH_ACTIONS.CLEAR_ERROR}); + }; + + /** + * Refresh user data from API + */ + const refreshUser = async () => { + try { + const user = await authService.getCurrentUser(); + dispatch({ + type: AUTH_ACTIONS.LOGIN_SUCCESS, + payload: {user, token: state.token}, + }); + } catch (error) { + console.error('Failed to refresh user data:', error); + // Don't logout on refresh failure, just log error + } + }; + + // Context value object + const value = { + // State + user: state.user, + token: state.token, + isAuthenticated: state.isAuthenticated, + loading: state.loading, + error: state.error, + + // Actions + login, + logout, + clearError, + refreshUser, + }; + + return ( + + {children} + + ); +} + +/** + * Custom hook to use authentication context + * @returns {Object} Auth context value + * @throws {Error} If used outside AuthProvider + */ +export function useAuth() { + const context = useContext(AuthContext); + + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + + return context; +} +export { AuthContext }; \ No newline at end of file diff --git a/src/frontend/src/hooks/useAuth.js b/src/frontend/src/hooks/useAuth.js new file mode 100644 index 0000000..c5c6ffb --- /dev/null +++ b/src/frontend/src/hooks/useAuth.js @@ -0,0 +1,12 @@ +import {useContext} from 'react'; +import {AuthContext} from '../contexts/AuthContext'; + +export const useAuth = () => { + const context = useContext(AuthContext); + + if (!context) { + throw new Error('useAuth must be used within an AuthProvider'); + } + + return context; +}; \ No newline at end of file diff --git a/src/frontend/src/index.css b/src/frontend/src/index.css index 08a3ac9..eb62d20 100644 --- a/src/frontend/src/index.css +++ b/src/frontend/src/index.css @@ -1,68 +1,10 @@ -:root { - font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} +@tailwind base; +@tailwind components; +@tailwind utilities; +@plugin "daisyui"; +/* Custom styles for the application */ body { - margin: 0; - display: flex; - place-items: center; - min-width: 320px; - min-height: 100vh; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } + @apply bg-base-100 text-base-content; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; } diff --git a/src/frontend/src/pages/DashboardPage.jsx b/src/frontend/src/pages/DashboardPage.jsx new file mode 100644 index 0000000..42414ee --- /dev/null +++ b/src/frontend/src/pages/DashboardPage.jsx @@ -0,0 +1,239 @@ +import {useEffect, useState} from 'react'; +import {useAuth} from '../hooks/useAuth'; + +const DashboardPage = () => { + const {user} = useAuth(); + const [stats, setStats] = useState({ + totalDocuments: 0, + processingJobs: 0, + completedJobs: 0, + failedJobs: 0 + }); + + const [recentFiles, setRecentFiles] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Simulate API calls for dashboard data + const fetchDashboardData = async () => { + try { + // TODO: Replace with actual API calls + setTimeout(() => { + setStats({ + totalDocuments: 42, + processingJobs: 3, + completedJobs: 38, + failedJobs: 1 + }); + + setRecentFiles([ + { + id: 1, + filename: 'invoice_2024.pdf', + status: 'completed', + processedAt: '2024-01-15 14:30:00', + fileType: 'pdf' + }, + { + id: 2, + filename: 'contract_draft.docx', + status: 'processing', + processedAt: '2024-01-15 14:25:00', + fileType: 'docx' + }, + { + id: 3, + filename: 'receipt_scan.jpg', + status: 'completed', + processedAt: '2024-01-15 14:20:00', + fileType: 'image' + } + ]); + + setLoading(false); + }, 1000); + } catch (error) { + console.error('Error fetching dashboard data:', error); + setLoading(false); + } + }; + + fetchDashboardData(); + }, []); + + const getStatusBadge = (status) => { + const statusColors = { + completed: 'badge-success', + processing: 'badge-warning', + failed: 'badge-error', + pending: 'badge-info' + }; + + return `badge ${statusColors[status] || 'badge-neutral'}`; + }; + + const getFileTypeIcon = (fileType) => { + const icons = { + pdf: '📄', + docx: '📝', + image: '🖼️', + txt: '📄' + }; + + return icons[fileType] || '📄'; + }; + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* Welcome Header */} +
+

+ Welcome back, {user?.username}! +

+

+ Here's your document processing overview +

+
+ + {/* Stats Cards */} +
+
+
+ + + +
+
Total Documents
+
{stats.totalDocuments}
+
+ +
+
+ + + +
+
Processing
+
{stats.processingJobs}
+
+ +
+
+ + + +
+
Completed
+
{stats.completedJobs}
+
+ +
+
+ + + +
+
Failed
+
{stats.failedJobs}
+
+
+ + {/* Recent Files */} +
+
+

Recent Files

+
+
+ + + + + + + + + + + + {recentFiles.map((file) => ( + + + + + + + + ))} + +
FileTypeStatusProcessed AtActions
+
+
+ {getFileTypeIcon(file.fileType)} +
+
{file.filename}
+
+
+ + {file.fileType.toUpperCase()} + + + + {file.status.charAt(0).toUpperCase() + file.status.slice(1)} + + {file.processedAt} +
+ + +
+
+
+
+ + {/* Quick Actions */} +
+

Quick Actions

+
+ + + + + {user?.role === 'admin' && ( + + )} +
+
+
+ ); +}; + +export default DashboardPage; \ No newline at end of file diff --git a/src/frontend/src/pages/LoginPage.jsx b/src/frontend/src/pages/LoginPage.jsx new file mode 100644 index 0000000..d81da0c --- /dev/null +++ b/src/frontend/src/pages/LoginPage.jsx @@ -0,0 +1,48 @@ +import React, {useEffect} from 'react'; +import {useNavigate} from 'react-router-dom'; +import {useAuth} from '../contexts/AuthContext'; +import AuthLayout from '../components/auth/AuthLayout'; +import LoginForm from '../components/auth/LoginForm'; + +/** + * LoginPage component + * Full page component that handles login functionality and redirects + */ +function LoginPage() { + const {isAuthenticated, loading} = useAuth(); + const navigate = useNavigate(); + + // Redirect to dashboard if already authenticated + useEffect(() => { + if (!loading && isAuthenticated) { + navigate('/dashboard', {replace: true}); + } + }, [isAuthenticated, loading, navigate]); + + // Show loading spinner while checking authentication + if (loading) { + return ( + +
+
+ +

Loading...

+
+
+
+ ); + } + + // Don't render login form if user is authenticated (prevents flash) + if (isAuthenticated) { + return null; + } + + return ( + + + + ); +} + +export default LoginPage; \ No newline at end of file diff --git a/src/frontend/src/services/authService.js b/src/frontend/src/services/authService.js new file mode 100644 index 0000000..6b76e2b --- /dev/null +++ b/src/frontend/src/services/authService.js @@ -0,0 +1,101 @@ +import api from '../utils/api'; + +/** + * Authentication service for handling login, logout, and user profile operations + */ +class AuthService { + /** + * Login user with username and password + * @param {string} username - User's username + * @param {string} password - User's password + * @returns {Promise<{access_token: string, user: Object}>} Login response with token and user data + */ + async login(username, password) { + try { + // FastAPI expects form data for OAuth2PasswordRequestForm + const formData = new FormData(); + formData.append('username', username); + formData.append('password', password); + + const response = await api.post('/auth/login', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + + const {access_token, user} = response.data; + + // Store token and user data in localStorage + localStorage.setItem('access_token', access_token); + localStorage.setItem('user', JSON.stringify(user)); + + return {access_token, user}; + } catch (error) { + // Extract error message from response + const errorMessage = error.response?.data?.detail || 'Login failed'; + throw new Error(errorMessage); + } + } + + /** + * Logout user by clearing stored data + */ + logout() { + localStorage.removeItem('access_token'); + localStorage.removeItem('user'); + } + + /** + * Get current user profile from API + * @returns {Promise} Current user profile + */ + async getCurrentUser() { + try { + const response = await api.get('/auth/me'); + const user = response.data; + + // Update stored user data + localStorage.setItem('user', JSON.stringify(user)); + + return user; + } catch (error) { + const errorMessage = error.response?.data?.detail || 'Failed to get user profile'; + throw new Error(errorMessage); + } + } + + /** + * Check if user is authenticated by verifying token existence + * @returns {boolean} True if user has valid token + */ + isAuthenticated() { + const token = localStorage.getItem('access_token'); + return !!token; + } + + /** + * Get stored user data from localStorage + * @returns {Object|null} User data or null if not found + */ + getStoredUser() { + try { + const userStr = localStorage.getItem('user'); + return userStr ? JSON.parse(userStr) : null; + } catch (error) { + console.error('Error parsing stored user data:', error); + return null; + } + } + + /** + * Get stored access token from localStorage + * @returns {string|null} Access token or null if not found + */ + getStoredToken() { + return localStorage.getItem('access_token'); + } +} + +// Export singleton instance +const authService = new AuthService(); +export default authService; \ No newline at end of file diff --git a/src/frontend/src/utils/api.js b/src/frontend/src/utils/api.js new file mode 100644 index 0000000..d839df6 --- /dev/null +++ b/src/frontend/src/utils/api.js @@ -0,0 +1,55 @@ +import axios from 'axios'; + +// Base API configuration +const API_BASE_URL = 'http://localhost:8000'; + +// Create axios instance with default configuration +const api = axios.create({ + baseURL: API_BASE_URL, + timeout: 10000, // 10 seconds timeout + headers: { + 'Content-Type': 'application/json', + }, +}); + +// Request interceptor to add authentication token +api.interceptors.request.use( + (config) => { + // Get token from localStorage + const token = localStorage.getItem('access_token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +// Response interceptor to handle common errors +api.interceptors.response.use( + (response) => { + return response; + }, + (error) => { + // Handle 401 errors (unauthorized) + if (error.response?.status === 401) { + // Clear token from localStorage on 401 + localStorage.removeItem('access_token'); + localStorage.removeItem('user'); + + // Redirect to login page + window.location.href = '/login'; + } + + // Handle other common errors + if (error.response?.status >= 500) { + console.error('Server error:', error.response.data); + } + + return Promise.reject(error); + } +); + +export default api; \ No newline at end of file diff --git a/src/frontend/tailwind.config.js b/src/frontend/tailwind.config.js new file mode 100644 index 0000000..0b9aa2e --- /dev/null +++ b/src/frontend/tailwind.config.js @@ -0,0 +1,14 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + "./index.html", + "./src/**/*.{js,ts,jsx,tsx}", + ], + theme: { + extend: {}, + }, + plugins: [require("daisyui")], + daisyui: { + themes: ["light", "dark"], + }, +} \ No newline at end of file diff --git a/src/frontend/vite.config.js b/src/frontend/vite.config.js index 8b0f57b..1f6c328 100644 --- a/src/frontend/vite.config.js +++ b/src/frontend/vite.config.js @@ -1,7 +1,10 @@ -import { defineConfig } from 'vite' +import {defineConfig} from 'vite' import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' // https://vite.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [react(), + tailwindcss(), + ], })