Spaces:
Running
Running
Upload 136 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +6 -0
- web/backend/SETUP.md +403 -0
- web/backend/cpp/Makefile +79 -0
- web/backend/cpp/image_processor.cpp +243 -0
- web/backend/database/init.sql +224 -0
- web/backend/database/schema.sql +298 -0
- web/backend/database/water_quality.db +0 -0
- web/backend/package-lock.json +0 -0
- web/backend/package.json +27 -0
- web/backend/python/audio_analyzer.py +102 -0
- web/backend/python/blast_db/biostream_db.ndb +3 -0
- web/backend/python/blast_db/biostream_db.nhr +0 -0
- web/backend/python/blast_db/biostream_db.nin +0 -0
- web/backend/python/blast_db/biostream_db.njs +22 -0
- web/backend/python/blast_db/biostream_db.not +0 -0
- web/backend/python/blast_db/biostream_db.nsq +0 -0
- web/backend/python/blast_db/biostream_db.ntf +3 -0
- web/backend/python/blast_db/biostream_db.nto +0 -0
- web/backend/python/dna_analyzer.py +108 -0
- web/backend/python/ewaste_analyzer.py +253 -0
- web/backend/python/phantom_footprint_analyzer.py +121 -0
- web/backend/python/requirements.txt +9 -0
- web/backend/python/water_analysis.py +138 -0
- web/backend/results/.gitkeep +1 -0
- web/backend/server.js +0 -0
- web/backend/uploads/.gitkeep +1 -0
- web/package.json +43 -0
- web/public/index.html +13 -0
- web/public/manifest.json +25 -0
- web/public/sounds/bird-call.mp3 +3 -0
- web/public/sounds/forest-ambience.mp3 +3 -0
- web/public/sounds/industrial-hum.mp3 +3 -0
- web/public/sounds/river.mp3 +3 -0
- web/src/App.css +514 -0
- web/src/App.js +514 -0
- web/src/App.test.js +18 -0
- web/src/SimpleComponent.js +12 -0
- web/src/components/Analytics.js +219 -0
- web/src/components/Header.css +48 -0
- web/src/components/Header.js +263 -0
- web/src/components/LoadingScreen.js +137 -0
- web/src/components/Navigation.css +92 -0
- web/src/components/layout/Header.css +473 -0
- web/src/components/layout/Header.js +136 -0
- web/src/components/layout/Sidebar.css +496 -0
- web/src/components/layout/Sidebar.js +184 -0
- web/src/components/layout/index.js +3 -0
- web/src/components/ui/ActionButton.js +132 -0
- web/src/components/ui/Button.css +278 -0
- web/src/components/ui/Button.js +60 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,9 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
web/backend/python/blast_db/biostream_db.ndb filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
web/backend/python/blast_db/biostream_db.ntf filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
web/public/sounds/bird-call.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
web/public/sounds/forest-ambience.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
web/public/sounds/industrial-hum.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
web/public/sounds/river.mp3 filter=lfs diff=lfs merge=lfs -text
|
web/backend/SETUP.md
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🧪 Aqua-Lens Backend Setup Guide
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
The Aqua-Lens backend provides enhanced processing capabilities using multiple programming languages for maximum accuracy and performance.
|
| 6 |
+
|
| 7 |
+
## 🏗️ Architecture
|
| 8 |
+
|
| 9 |
+
- **Node.js** - API server and coordination
|
| 10 |
+
- **Python** - AI-powered image analysis with OpenCV
|
| 11 |
+
- **C++** - High-performance image preprocessing
|
| 12 |
+
- **SQLite** - Comprehensive data storage
|
| 13 |
+
|
| 14 |
+
## 🚀 Quick Setup
|
| 15 |
+
|
| 16 |
+
### 1. Install Node.js Dependencies
|
| 17 |
+
```bash
|
| 18 |
+
cd web/backend
|
| 19 |
+
npm install
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
### 2. Setup Python Environment
|
| 23 |
+
```bash
|
| 24 |
+
# Install Python dependencies
|
| 25 |
+
pip install -r python/requirements.txt
|
| 26 |
+
|
| 27 |
+
# Or using virtual environment
|
| 28 |
+
python -m venv venv
|
| 29 |
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
| 30 |
+
pip install -r python/requirements.txt
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
### 3. Build C++ Components (Optional)
|
| 34 |
+
```bash
|
| 35 |
+
# Install OpenCV (Ubuntu/Debian)
|
| 36 |
+
sudo apt-get install libopencv-dev
|
| 37 |
+
|
| 38 |
+
# Install OpenCV (macOS)
|
| 39 |
+
brew install opencv
|
| 40 |
+
|
| 41 |
+
# Build the image processor
|
| 42 |
+
cd cpp
|
| 43 |
+
make
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
### 4. Initialize Database
|
| 47 |
+
```bash
|
| 48 |
+
npm run init-db
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
### 5. Start the Server
|
| 52 |
+
```bash
|
| 53 |
+
npm start
|
| 54 |
+
# Or for development
|
| 55 |
+
npm run dev
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
## 📋 Dependencies
|
| 59 |
+
|
| 60 |
+
### Node.js Packages
|
| 61 |
+
- `express` - Web server framework
|
| 62 |
+
- `multer` - File upload handling
|
| 63 |
+
- `sqlite3` - Database interface
|
| 64 |
+
- `sharp` - Image processing
|
| 65 |
+
- `cors` - Cross-origin requests
|
| 66 |
+
- `uuid` - Unique ID generation
|
| 67 |
+
|
| 68 |
+
### Python Packages
|
| 69 |
+
- `opencv-python` - Computer vision
|
| 70 |
+
- `Pillow` - Image processing
|
| 71 |
+
- `numpy` - Numerical computing
|
| 72 |
+
- `scipy` - Scientific computing
|
| 73 |
+
- `scikit-image` - Image analysis
|
| 74 |
+
|
| 75 |
+
### C++ Dependencies
|
| 76 |
+
- `OpenCV 4.x` - Computer vision library
|
| 77 |
+
- `g++` - C++ compiler
|
| 78 |
+
- `make` - Build system
|
| 79 |
+
|
| 80 |
+
## 🔧 Configuration
|
| 81 |
+
|
| 82 |
+
### Environment Variables
|
| 83 |
+
Create a `.env` file in the backend directory:
|
| 84 |
+
```env
|
| 85 |
+
PORT=5000
|
| 86 |
+
NODE_ENV=development
|
| 87 |
+
DATABASE_PATH=./database/water_quality.db
|
| 88 |
+
UPLOAD_DIR=./uploads
|
| 89 |
+
TEMP_DIR=./temp
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
### API Endpoints
|
| 93 |
+
|
| 94 |
+
#### POST /api/analyze-water
|
| 95 |
+
Upload and analyze water test strip image
|
| 96 |
+
- **Body**: FormData with image file
|
| 97 |
+
- **Response**: Water quality analysis results
|
| 98 |
+
|
| 99 |
+
#### GET /api/water-map
|
| 100 |
+
Get water quality map data
|
| 101 |
+
- **Query**: lat, lng, radius
|
| 102 |
+
- **Response**: Array of water quality points
|
| 103 |
+
|
| 104 |
+
#### GET /api/alerts
|
| 105 |
+
Get contamination alerts
|
| 106 |
+
- **Query**: lat, lng, radius, severity
|
| 107 |
+
- **Response**: Array of active alerts
|
| 108 |
+
|
| 109 |
+
#### GET /api/health
|
| 110 |
+
System health check
|
| 111 |
+
- **Response**: Service status information
|
| 112 |
+
|
| 113 |
+
## 🧪 Testing
|
| 114 |
+
|
| 115 |
+
### Test Python Analysis
|
| 116 |
+
```bash
|
| 117 |
+
cd python
|
| 118 |
+
python water_analysis.py ../test_images/sample.jpg tap_water
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
### Test C++ Processing
|
| 122 |
+
```bash
|
| 123 |
+
cd cpp
|
| 124 |
+
make test
|
| 125 |
+
```
|
| 126 |
+
|
| 127 |
+
### Test API Endpoints
|
| 128 |
+
```bash
|
| 129 |
+
# Health check
|
| 130 |
+
curl http://localhost:5000/api/health
|
| 131 |
+
|
| 132 |
+
# Upload test image
|
| 133 |
+
curl -X POST -F "image=@test.jpg" -F "waterSource=Tap Water" \
|
| 134 |
+
http://localhost:5000/api/analyze-water
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
## 🐳 Docker Setup (Optional)
|
| 138 |
+
|
| 139 |
+
### Build Docker Image
|
| 140 |
+
```bash
|
| 141 |
+
docker build -t aqua-lens-backend .
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
### Run Container
|
| 145 |
+
```bash
|
| 146 |
+
docker run -p 5000:5000 -v $(pwd)/database:/app/database aqua-lens-backend
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
## 🔍 Troubleshooting
|
| 150 |
+
|
| 151 |
+
### Common Issues
|
| 152 |
+
|
| 153 |
+
#### Python Dependencies
|
| 154 |
+
```bash
|
| 155 |
+
# If OpenCV installation fails
|
| 156 |
+
pip install opencv-python-headless
|
| 157 |
+
|
| 158 |
+
# For ARM-based systems (M1 Mac)
|
| 159 |
+
pip install opencv-python --no-binary opencv-python
|
| 160 |
+
```
|
| 161 |
+
|
| 162 |
+
#### C++ Compilation
|
| 163 |
+
```bash
|
| 164 |
+
# If OpenCV not found
|
| 165 |
+
export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH
|
| 166 |
+
|
| 167 |
+
# Alternative compilation
|
| 168 |
+
g++ -std=c++11 -o image_processor image_processor.cpp \
|
| 169 |
+
-lopencv_core -lopencv_imgproc -lopencv_imgcodecs
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
#### Database Issues
|
| 173 |
+
```bash
|
| 174 |
+
# Reset database
|
| 175 |
+
rm database/water_quality.db
|
| 176 |
+
npm run init-db
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
## 📊 Performance Optimization
|
| 180 |
+
|
| 181 |
+
### Image Processing
|
| 182 |
+
- Images are automatically resized to 800x600 for faster processing
|
| 183 |
+
- JPEG quality set to 95% for optimal balance
|
| 184 |
+
- Temporary files cleaned up after 5 seconds
|
| 185 |
+
|
| 186 |
+
### Database Optimization
|
| 187 |
+
- Indexes on location, timestamp, and quality fields
|
| 188 |
+
- Automatic cleanup of old temporary data
|
| 189 |
+
- Connection pooling for concurrent requests
|
| 190 |
+
|
| 191 |
+
### Memory Management
|
| 192 |
+
- Sharp image processing with automatic memory cleanup
|
| 193 |
+
- Python process spawning with proper cleanup
|
| 194 |
+
- C++ RAII for resource management
|
| 195 |
+
|
| 196 |
+
## 🔒 Security Features
|
| 197 |
+
|
| 198 |
+
- File type validation for uploads
|
| 199 |
+
- File size limits (10MB max)
|
| 200 |
+
- Input sanitization
|
| 201 |
+
- Rate limiting
|
| 202 |
+
- CORS configuration
|
| 203 |
+
- Helmet security headers
|
| 204 |
+
|
| 205 |
+
## 📈 Monitoring
|
| 206 |
+
|
| 207 |
+
### Logs
|
| 208 |
+
- Server logs to console
|
| 209 |
+
- Error tracking with stack traces
|
| 210 |
+
- Performance metrics logging
|
| 211 |
+
- Database query logging
|
| 212 |
+
|
| 213 |
+
### Health Checks
|
| 214 |
+
- Service availability monitoring
|
| 215 |
+
- Database connection status
|
| 216 |
+
- Python/C++ component availability
|
| 217 |
+
- Memory and CPU usage tracking
|
| 218 |
+
|
| 219 |
+
## 🚀 Production Deployment
|
| 220 |
+
|
| 221 |
+
### Environment Setup
|
| 222 |
+
```bash
|
| 223 |
+
NODE_ENV=production
|
| 224 |
+
PORT=80
|
| 225 |
+
DATABASE_PATH=/data/water_quality.db
|
| 226 |
+
```
|
| 227 |
+
|
| 228 |
+
### Process Management
|
| 229 |
+
```bash
|
| 230 |
+
# Using PM2
|
| 231 |
+
npm install -g pm2
|
| 232 |
+
pm2 start server.js --name aqua-lens-backend
|
| 233 |
+
|
| 234 |
+
# Using systemd
|
| 235 |
+
sudo systemctl enable aqua-lens-backend
|
| 236 |
+
sudo systemctl start aqua-lens-backend
|
| 237 |
+
```
|
| 238 |
+
|
| 239 |
+
### Reverse Proxy (Nginx)
|
| 240 |
+
```nginx
|
| 241 |
+
server {
|
| 242 |
+
listen 80;
|
| 243 |
+
server_name your-domain.com;
|
| 244 |
+
|
| 245 |
+
location /api/ {
|
| 246 |
+
proxy_pass http://localhost:5000;
|
| 247 |
+
proxy_set_header Host $host;
|
| 248 |
+
proxy_set_header X-Real-IP $remote_addr;
|
| 249 |
+
}
|
| 250 |
+
}
|
| 251 |
+
```
|
| 252 |
+
|
| 253 |
+
## 📚 API Documentation
|
| 254 |
+
|
| 255 |
+
Full API documentation available at:
|
| 256 |
+
- Swagger UI: `http://localhost:5000/api-docs`
|
| 257 |
+
- OpenAPI spec: `http://localhost:5000/api/openapi.json`
|
| 258 |
+
|
| 259 |
+
## 🤝 Contributing
|
| 260 |
+
|
| 261 |
+
1. Fork the repository
|
| 262 |
+
2. Create feature branch
|
| 263 |
+
3. Add tests for new functionality
|
| 264 |
+
4. Ensure all tests pass
|
| 265 |
+
5. Submit pull request
|
| 266 |
+
|
| 267 |
+
## 📄 License
|
| 268 |
+
|
| 269 |
+
MIT License - see LICENSE file for details
|
| 270 |
+
|
| 271 |
+
---
|
| 272 |
+
|
| 273 |
+
*For frontend-only usage, the system works completely without the backend using the self-contained JavaScript analysis engine.*
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
# 🌿 EcoSpire Backend Setup Guide
|
| 284 |
+
|
| 285 |
+
## Overview
|
| 286 |
+
|
| 287 |
+
The EcoSpire backend is a high-performance, multi-language system designed to power a suite of environmental intelligence tools. It uses a Node.js coordinator to manage specialized Python and C++ services for maximum accuracy.
|
| 288 |
+
|
| 289 |
+
## 🏗️ Architecture
|
| 290 |
+
|
| 291 |
+
- **Node.js** - Main API server and request coordination.
|
| 292 |
+
- **Python** - AI-powered analysis for computer vision (AquaLens), audio (BiodiversityEar), and bioinformatics (Bio-Stream AI).
|
| 293 |
+
- **C++** - High-performance image preprocessing for AquaLens.
|
| 294 |
+
- **SQLite** - Lightweight and comprehensive data storage.
|
| 295 |
+
- **NCBI BLAST+** - Scientific engine for DNA sequence analysis.
|
| 296 |
+
|
| 297 |
+
## 🚀 Quick Setup
|
| 298 |
+
|
| 299 |
+
### 1. Install Node.js Dependencies
|
| 300 |
+
```bash
|
| 301 |
+
cd web/backend
|
| 302 |
+
npm install
|
| 303 |
+
2. Setup Python Environment
|
| 304 |
+
code
|
| 305 |
+
Bash
|
| 306 |
+
# It is highly recommended to use a virtual environment
|
| 307 |
+
python -m venv venv
|
| 308 |
+
# On Windows:
|
| 309 |
+
venv\Scripts\activate
|
| 310 |
+
# On Mac/Linux:
|
| 311 |
+
source venv/bin/activate
|
| 312 |
+
|
| 313 |
+
# Install all Python dependencies
|
| 314 |
+
pip install -r python/requirements.txt
|
| 315 |
+
3. Install Scientific Toolkit (for Bio-Stream AI)
|
| 316 |
+
The Bio-Stream AI tool requires the NCBI BLAST+ command-line toolkit for its analysis engine.
|
| 317 |
+
Download: Get the installer from the official NCBI BLAST website.
|
| 318 |
+
Install: Run the installer. Crucially, during setup, you must check the box to "Add BLAST to the system PATH". This allows the backend to find and use the tool.
|
| 319 |
+
Verify: After installation, open a new terminal and run makeblastdb -version. You should see a version number printed.
|
| 320 |
+
4. Build Bio-Stream AI Database
|
| 321 |
+
Bio-Stream AI uses a custom DNA database. To build it, run the following commands from the web/backend/ directory:
|
| 322 |
+
code
|
| 323 |
+
Bash
|
| 324 |
+
# Navigate to the database source directory
|
| 325 |
+
cd python/blast_db
|
| 326 |
+
|
| 327 |
+
# (On Windows) Combine the sample DNA files
|
| 328 |
+
copy *.fasta custom_database.fasta
|
| 329 |
+
# (On Mac/Linux)
|
| 330 |
+
cat *.fasta > custom_database.fasta
|
| 331 |
+
|
| 332 |
+
# Build the database
|
| 333 |
+
makeblastdb -in "custom_database.fasta" -dbtype nucl -out "biostream_db"
|
| 334 |
+
Note: If makeblastdb fails due to a space in your project path, please use the "temporary folder" method documented in the BioStreamAI-README.md.
|
| 335 |
+
5. Build C++ Components (Optional for AquaLens)
|
| 336 |
+
code
|
| 337 |
+
Bash
|
| 338 |
+
# For detailed instructions, see the "Troubleshooting" section below.
|
| 339 |
+
cd cpp
|
| 340 |
+
make
|
| 341 |
+
6. Initialize Database
|
| 342 |
+
This creates the water_quality.db file for AquaLens data.
|
| 343 |
+
code
|
| 344 |
+
Bash
|
| 345 |
+
npm run init-db
|
| 346 |
+
7. Start the Server
|
| 347 |
+
code
|
| 348 |
+
Bash
|
| 349 |
+
# For development with live reloading
|
| 350 |
+
npm run dev
|
| 351 |
+
|
| 352 |
+
# For production
|
| 353 |
+
npm start```
|
| 354 |
+
|
| 355 |
+
## 📋 Dependencies
|
| 356 |
+
|
| 357 |
+
### Node.js Packages
|
| 358 |
+
- `express` - Web server framework
|
| 359 |
+
- `multer` - File upload handling
|
| 360 |
+
- `sqlite3` - Database interface
|
| 361 |
+
- `sharp` - Image processing
|
| 362 |
+
- `cors` - Cross-origin requests
|
| 363 |
+
- `uuid` - Unique ID generation
|
| 364 |
+
|
| 365 |
+
### Python Packages
|
| 366 |
+
- `opencv-python` - Computer vision
|
| 367 |
+
- `Pillow` - Image processing
|
| 368 |
+
- `numpy` - Numerical computing
|
| 369 |
+
- `scipy` - Scientific computing
|
| 370 |
+
- `scikit-image` - Image analysis
|
| 371 |
+
- `librosa` - Audio analysis
|
| 372 |
+
- `biopython` - Toolkit for bioinformatics
|
| 373 |
+
|
| 374 |
+
### C++ Dependencies
|
| 375 |
+
- `OpenCV 4.x` - Computer vision library
|
| 376 |
+
- `g++` / `make` - Build tools
|
| 377 |
+
|
| 378 |
+
## 🔧 Configuration
|
| 379 |
+
|
| 380 |
+
Create a `.env` file in the `web/backend` directory:
|
| 381 |
+
```env
|
| 382 |
+
PORT=5000
|
| 383 |
+
NODE_ENV=development
|
| 384 |
+
DATABASE_PATH=./database/water_quality.db
|
| 385 |
+
UPLOAD_DIR=./uploads
|
| 386 |
+
TEMP_DIR=./temp
|
| 387 |
+
📡 API Endpoints
|
| 388 |
+
POST /api/analyze-water
|
| 389 |
+
Tool: AquaLens
|
| 390 |
+
Body: FormData with image file
|
| 391 |
+
Response: Water quality analysis results
|
| 392 |
+
POST /api/analyze-audio
|
| 393 |
+
Tool: BiodiversityEar
|
| 394 |
+
Body: FormData with audioFile
|
| 395 |
+
Response: Acoustic biodiversity analysis report
|
| 396 |
+
POST /api/analyze-dna
|
| 397 |
+
Tool: Bio-Stream AI
|
| 398 |
+
Body: FormData with dnaFile
|
| 399 |
+
Response: A full ecosystem health and species identification report.
|
| 400 |
+
GET /api/water-map
|
| 401 |
+
Tool: AquaLens
|
| 402 |
+
Query: lat, lng, radius
|
| 403 |
+
Response: Array of water quality data points for mapping
|
web/backend/cpp/Makefile
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Aqua-Lens C++ Image Processor Makefile
|
| 2 |
+
|
| 3 |
+
CXX = g++
|
| 4 |
+
CXXFLAGS = -std=c++11 -O3 -Wall -Wextra
|
| 5 |
+
TARGET = image_processor
|
| 6 |
+
SOURCE = image_processor.cpp
|
| 7 |
+
|
| 8 |
+
# Try to detect OpenCV installation
|
| 9 |
+
OPENCV_VERSION := $(shell pkg-config --exists opencv4 && echo "opencv4" || echo "opencv")
|
| 10 |
+
OPENCV_CFLAGS := $(shell pkg-config --cflags $(OPENCV_VERSION) 2>/dev/null)
|
| 11 |
+
OPENCV_LIBS := $(shell pkg-config --libs $(OPENCV_VERSION) 2>/dev/null)
|
| 12 |
+
|
| 13 |
+
# Fallback if pkg-config doesn't work
|
| 14 |
+
ifeq ($(OPENCV_LIBS),)
|
| 15 |
+
OPENCV_LIBS = -lopencv_core -lopencv_imgproc -lopencv_imgcodecs
|
| 16 |
+
endif
|
| 17 |
+
|
| 18 |
+
# Build target
|
| 19 |
+
$(TARGET): $(SOURCE)
|
| 20 |
+
@echo "Building Aqua-Lens Image Processor..."
|
| 21 |
+
@echo "OpenCV Version: $(OPENCV_VERSION)"
|
| 22 |
+
$(CXX) $(CXXFLAGS) $(OPENCV_CFLAGS) -o $(TARGET) $(SOURCE) $(OPENCV_LIBS)
|
| 23 |
+
@echo "Build complete: $(TARGET)"
|
| 24 |
+
|
| 25 |
+
# Clean target
|
| 26 |
+
clean:
|
| 27 |
+
rm -f $(TARGET)
|
| 28 |
+
@echo "Cleaned build files"
|
| 29 |
+
|
| 30 |
+
# Install dependencies (Ubuntu/Debian)
|
| 31 |
+
install-deps-ubuntu:
|
| 32 |
+
sudo apt-get update
|
| 33 |
+
sudo apt-get install -y build-essential cmake pkg-config
|
| 34 |
+
sudo apt-get install -y libopencv-dev libopencv-contrib-dev
|
| 35 |
+
|
| 36 |
+
# Install dependencies (macOS with Homebrew)
|
| 37 |
+
install-deps-macos:
|
| 38 |
+
brew install opencv pkg-config
|
| 39 |
+
|
| 40 |
+
# Install dependencies (Windows with vcpkg)
|
| 41 |
+
install-deps-windows:
|
| 42 |
+
@echo "For Windows, install OpenCV using vcpkg:"
|
| 43 |
+
@echo "vcpkg install opencv[contrib]:x64-windows"
|
| 44 |
+
@echo "Then compile with: make windows"
|
| 45 |
+
|
| 46 |
+
# Windows build (requires vcpkg)
|
| 47 |
+
windows:
|
| 48 |
+
$(CXX) $(CXXFLAGS) -I"$(VCPKG_ROOT)/installed/x64-windows/include" \
|
| 49 |
+
-L"$(VCPKG_ROOT)/installed/x64-windows/lib" \
|
| 50 |
+
-o $(TARGET).exe $(SOURCE) \
|
| 51 |
+
-lopencv_core -lopencv_imgproc -lopencv_imgcodecs
|
| 52 |
+
|
| 53 |
+
# Test the processor
|
| 54 |
+
test: $(TARGET)
|
| 55 |
+
@echo "Testing image processor..."
|
| 56 |
+
@if [ -f "test_image.jpg" ]; then \
|
| 57 |
+
./$(TARGET) test_image.jpg test_output.jpg; \
|
| 58 |
+
else \
|
| 59 |
+
echo "No test image found. Place a test image as 'test_image.jpg' to test."; \
|
| 60 |
+
fi
|
| 61 |
+
|
| 62 |
+
# Help
|
| 63 |
+
help:
|
| 64 |
+
@echo "Aqua-Lens C++ Image Processor Build System"
|
| 65 |
+
@echo ""
|
| 66 |
+
@echo "Targets:"
|
| 67 |
+
@echo " $(TARGET) - Build the image processor"
|
| 68 |
+
@echo " clean - Remove build files"
|
| 69 |
+
@echo " test - Test the processor with test_image.jpg"
|
| 70 |
+
@echo " install-deps-ubuntu - Install dependencies on Ubuntu/Debian"
|
| 71 |
+
@echo " install-deps-macos - Install dependencies on macOS"
|
| 72 |
+
@echo " install-deps-windows - Show Windows installation instructions"
|
| 73 |
+
@echo " windows - Build for Windows (requires vcpkg)"
|
| 74 |
+
@echo " help - Show this help message"
|
| 75 |
+
@echo ""
|
| 76 |
+
@echo "Usage:"
|
| 77 |
+
@echo " ./$(TARGET) <input_image> <output_image>"
|
| 78 |
+
|
| 79 |
+
.PHONY: clean install-deps-ubuntu install-deps-macos install-deps-windows windows test help
|
web/backend/cpp/image_processor.cpp
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
* Aqua-Lens High-Performance Image Processor
|
| 3 |
+
* C++ implementation for advanced image preprocessing
|
| 4 |
+
* Optimized for test strip color analysis
|
| 5 |
+
*/
|
| 6 |
+
|
| 7 |
+
#include <opencv2/opencv.hpp>
|
| 8 |
+
#include <opencv2/imgproc.hpp>
|
| 9 |
+
#include <opencv2/imgcodecs.hpp>
|
| 10 |
+
#include <iostream>
|
| 11 |
+
#include <vector>
|
| 12 |
+
#include <string>
|
| 13 |
+
#include <cmath>
|
| 14 |
+
#include <algorithm>
|
| 15 |
+
|
| 16 |
+
class TestStripProcessor {
|
| 17 |
+
private:
|
| 18 |
+
cv::Mat originalImage;
|
| 19 |
+
cv::Mat processedImage;
|
| 20 |
+
|
| 21 |
+
public:
|
| 22 |
+
TestStripProcessor() {}
|
| 23 |
+
|
| 24 |
+
bool loadImage(const std::string& imagePath) {
|
| 25 |
+
originalImage = cv::imread(imagePath, cv::IMREAD_COLOR);
|
| 26 |
+
if (originalImage.empty()) {
|
| 27 |
+
std::cerr << "Error: Could not load image " << imagePath << std::endl;
|
| 28 |
+
return false;
|
| 29 |
+
}
|
| 30 |
+
return true;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
void preprocessImage() {
|
| 34 |
+
cv::Mat temp;
|
| 35 |
+
originalImage.copyTo(temp);
|
| 36 |
+
|
| 37 |
+
// Step 1: Noise reduction using bilateral filter
|
| 38 |
+
cv::bilateralFilter(temp, processedImage, 9, 75, 75);
|
| 39 |
+
|
| 40 |
+
// Step 2: Enhance contrast using CLAHE (Contrast Limited Adaptive Histogram Equalization)
|
| 41 |
+
cv::Mat lab;
|
| 42 |
+
cv::cvtColor(processedImage, lab, cv::COLOR_BGR2Lab);
|
| 43 |
+
|
| 44 |
+
std::vector<cv::Mat> labChannels;
|
| 45 |
+
cv::split(lab, labChannels);
|
| 46 |
+
|
| 47 |
+
cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE(2.0, cv::Size(8, 8));
|
| 48 |
+
clahe->apply(labChannels[0], labChannels[0]);
|
| 49 |
+
|
| 50 |
+
cv::merge(labChannels, lab);
|
| 51 |
+
cv::cvtColor(lab, processedImage, cv::COLOR_Lab2BGR);
|
| 52 |
+
|
| 53 |
+
// Step 3: Color correction and white balance
|
| 54 |
+
correctWhiteBalance();
|
| 55 |
+
|
| 56 |
+
// Step 4: Sharpen the image
|
| 57 |
+
sharpenImage();
|
| 58 |
+
|
| 59 |
+
// Step 5: Normalize lighting conditions
|
| 60 |
+
normalizeLighting();
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
void correctWhiteBalance() {
|
| 64 |
+
cv::Mat temp;
|
| 65 |
+
processedImage.copyTo(temp);
|
| 66 |
+
|
| 67 |
+
// Simple white balance using gray world assumption
|
| 68 |
+
cv::Scalar meanBGR = cv::mean(temp);
|
| 69 |
+
double meanGray = (meanBGR[0] + meanBGR[1] + meanBGR[2]) / 3.0;
|
| 70 |
+
|
| 71 |
+
std::vector<cv::Mat> channels;
|
| 72 |
+
cv::split(temp, channels);
|
| 73 |
+
|
| 74 |
+
// Adjust each channel
|
| 75 |
+
for (int i = 0; i < 3; i++) {
|
| 76 |
+
if (meanBGR[i] > 0) {
|
| 77 |
+
double scale = meanGray / meanBGR[i];
|
| 78 |
+
channels[i] *= scale;
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
cv::merge(channels, processedImage);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
void sharpenImage() {
|
| 86 |
+
cv::Mat kernel = (cv::Mat_<float>(3, 3) <<
|
| 87 |
+
0, -1, 0,
|
| 88 |
+
-1, 5, -1,
|
| 89 |
+
0, -1, 0);
|
| 90 |
+
|
| 91 |
+
cv::Mat sharpened;
|
| 92 |
+
cv::filter2D(processedImage, sharpened, -1, kernel);
|
| 93 |
+
processedImage = sharpened;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
void normalizeLighting() {
|
| 97 |
+
cv::Mat temp;
|
| 98 |
+
processedImage.copyTo(temp);
|
| 99 |
+
|
| 100 |
+
// Convert to HSV for better lighting control
|
| 101 |
+
cv::Mat hsv;
|
| 102 |
+
cv::cvtColor(temp, hsv, cv::COLOR_BGR2HSV);
|
| 103 |
+
|
| 104 |
+
std::vector<cv::Mat> hsvChannels;
|
| 105 |
+
cv::split(hsv, hsvChannels);
|
| 106 |
+
|
| 107 |
+
// Normalize the V (brightness) channel
|
| 108 |
+
cv::equalizeHist(hsvChannels[2], hsvChannels[2]);
|
| 109 |
+
|
| 110 |
+
cv::merge(hsvChannels, hsv);
|
| 111 |
+
cv::cvtColor(hsv, processedImage, cv::COLOR_HSV2BGR);
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
std::vector<cv::Rect> detectTestStripRegions() {
|
| 115 |
+
std::vector<cv::Rect> regions;
|
| 116 |
+
|
| 117 |
+
cv::Mat gray, binary;
|
| 118 |
+
cv::cvtColor(processedImage, gray, cv::COLOR_BGR2GRAY);
|
| 119 |
+
|
| 120 |
+
// Use adaptive thresholding to find colored regions
|
| 121 |
+
cv::adaptiveThreshold(gray, binary, 255, cv::ADAPTIVE_THRESH_GAUSSIAN_C,
|
| 122 |
+
cv::THRESH_BINARY_INV, 11, 2);
|
| 123 |
+
|
| 124 |
+
// Find contours
|
| 125 |
+
std::vector<std::vector<cv::Point>> contours;
|
| 126 |
+
std::vector<cv::Vec4i> hierarchy;
|
| 127 |
+
cv::findContours(binary, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
|
| 128 |
+
|
| 129 |
+
// Filter contours by area and aspect ratio
|
| 130 |
+
for (const auto& contour : contours) {
|
| 131 |
+
cv::Rect boundingRect = cv::boundingRect(contour);
|
| 132 |
+
double area = cv::contourArea(contour);
|
| 133 |
+
double aspectRatio = (double)boundingRect.width / boundingRect.height;
|
| 134 |
+
|
| 135 |
+
// Filter based on reasonable test strip pad characteristics
|
| 136 |
+
if (area > 100 && area < 10000 && aspectRatio > 0.5 && aspectRatio < 3.0) {
|
| 137 |
+
regions.push_back(boundingRect);
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
// Sort regions by position (left to right, top to bottom)
|
| 142 |
+
std::sort(regions.begin(), regions.end(), [](const cv::Rect& a, const cv::Rect& b) {
|
| 143 |
+
if (abs(a.y - b.y) < 50) { // Same row
|
| 144 |
+
return a.x < b.x;
|
| 145 |
+
}
|
| 146 |
+
return a.y < b.y;
|
| 147 |
+
});
|
| 148 |
+
|
| 149 |
+
return regions;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
cv::Vec3b extractAverageColor(const cv::Rect& region) {
|
| 153 |
+
cv::Mat roi = processedImage(region);
|
| 154 |
+
cv::Scalar meanColor = cv::mean(roi);
|
| 155 |
+
return cv::Vec3b(meanColor[0], meanColor[1], meanColor[2]);
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
bool saveProcessedImage(const std::string& outputPath) {
|
| 159 |
+
if (processedImage.empty()) {
|
| 160 |
+
std::cerr << "Error: No processed image to save" << std::endl;
|
| 161 |
+
return false;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
std::vector<int> compression_params;
|
| 165 |
+
compression_params.push_back(cv::IMWRITE_JPEG_QUALITY);
|
| 166 |
+
compression_params.push_back(95);
|
| 167 |
+
|
| 168 |
+
return cv::imwrite(outputPath, processedImage, compression_params);
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
void analyzeColorAccuracy() {
|
| 172 |
+
// Calculate color distribution and quality metrics
|
| 173 |
+
cv::Mat hsv;
|
| 174 |
+
cv::cvtColor(processedImage, hsv, cv::COLOR_BGR2HSV);
|
| 175 |
+
|
| 176 |
+
std::vector<cv::Mat> hsvChannels;
|
| 177 |
+
cv::split(hsv, hsvChannels);
|
| 178 |
+
|
| 179 |
+
// Calculate histogram for each channel
|
| 180 |
+
int histSize = 256;
|
| 181 |
+
float range[] = {0, 256};
|
| 182 |
+
const float* histRange = {range};
|
| 183 |
+
|
| 184 |
+
cv::Mat hHist, sHist, vHist;
|
| 185 |
+
cv::calcHist(&hsvChannels[0], 1, 0, cv::Mat(), hHist, 1, &histSize, &histRange);
|
| 186 |
+
cv::calcHist(&hsvChannels[1], 1, 0, cv::Mat(), sHist, 1, &histSize, &histRange);
|
| 187 |
+
cv::calcHist(&hsvChannels[2], 1, 0, cv::Mat(), vHist, 1, &histSize, &histRange);
|
| 188 |
+
|
| 189 |
+
// Output color analysis results
|
| 190 |
+
std::cout << "Color Analysis Complete:" << std::endl;
|
| 191 |
+
std::cout << "Image Size: " << processedImage.cols << "x" << processedImage.rows << std::endl;
|
| 192 |
+
std::cout << "Processing: Enhanced contrast, white balance, sharpening applied" << std::endl;
|
| 193 |
+
}
|
| 194 |
+
};
|
| 195 |
+
|
| 196 |
+
int main(int argc, char* argv[]) {
|
| 197 |
+
if (argc != 3) {
|
| 198 |
+
std::cerr << "Usage: " << argv[0] << " <input_image> <output_image>" << std::endl;
|
| 199 |
+
return -1;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
std::string inputPath = argv[1];
|
| 203 |
+
std::string outputPath = argv[2];
|
| 204 |
+
|
| 205 |
+
TestStripProcessor processor;
|
| 206 |
+
|
| 207 |
+
// Load the image
|
| 208 |
+
if (!processor.loadImage(inputPath)) {
|
| 209 |
+
return -1;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
std::cout << "Processing image: " << inputPath << std::endl;
|
| 213 |
+
|
| 214 |
+
// Process the image
|
| 215 |
+
processor.preprocessImage();
|
| 216 |
+
|
| 217 |
+
// Detect test strip regions
|
| 218 |
+
std::vector<cv::Rect> regions = processor.detectTestStripRegions();
|
| 219 |
+
std::cout << "Detected " << regions.size() << " test strip regions" << std::endl;
|
| 220 |
+
|
| 221 |
+
// Analyze color accuracy
|
| 222 |
+
processor.analyzeColorAccuracy();
|
| 223 |
+
|
| 224 |
+
// Save the processed image
|
| 225 |
+
if (processor.saveProcessedImage(outputPath)) {
|
| 226 |
+
std::cout << "Processed image saved to: " << outputPath << std::endl;
|
| 227 |
+
return 0;
|
| 228 |
+
} else {
|
| 229 |
+
std::cerr << "Failed to save processed image" << std::endl;
|
| 230 |
+
return -1;
|
| 231 |
+
}
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
/*
|
| 235 |
+
Compilation instructions:
|
| 236 |
+
g++ -std=c++11 -o image_processor image_processor.cpp `pkg-config --cflags --libs opencv4`
|
| 237 |
+
|
| 238 |
+
Or if opencv4 is not available:
|
| 239 |
+
g++ -std=c++11 -o image_processor image_processor.cpp -lopencv_core -lopencv_imgproc -lopencv_imgcodecs
|
| 240 |
+
|
| 241 |
+
For Windows with vcpkg:
|
| 242 |
+
g++ -std=c++11 -o image_processor.exe image_processor.cpp -I"C:/vcpkg/installed/x64-windows/include" -L"C:/vcpkg/installed/x64-windows/lib" -lopencv_core -lopencv_imgproc -lopencv_imgcodecs
|
| 243 |
+
*/
|
web/backend/database/init.sql
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- Aqua-Lens Water Quality Database Schema
|
| 2 |
+
-- SQLite database initialization script
|
| 3 |
+
|
| 4 |
+
-- Main water tests table
|
| 5 |
+
CREATE TABLE IF NOT EXISTS water_tests (
|
| 6 |
+
id TEXT PRIMARY KEY,
|
| 7 |
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 8 |
+
user_id TEXT,
|
| 9 |
+
latitude REAL,
|
| 10 |
+
longitude REAL,
|
| 11 |
+
water_source TEXT NOT NULL,
|
| 12 |
+
image_path TEXT,
|
| 13 |
+
|
| 14 |
+
-- Water quality parameters
|
| 15 |
+
ph REAL NOT NULL,
|
| 16 |
+
chlorine REAL NOT NULL,
|
| 17 |
+
nitrates INTEGER NOT NULL,
|
| 18 |
+
hardness INTEGER NOT NULL,
|
| 19 |
+
alkalinity INTEGER NOT NULL,
|
| 20 |
+
bacteria INTEGER NOT NULL DEFAULT 0,
|
| 21 |
+
|
| 22 |
+
-- Analysis results
|
| 23 |
+
overall_quality TEXT NOT NULL,
|
| 24 |
+
safety_level TEXT NOT NULL,
|
| 25 |
+
confidence REAL NOT NULL DEFAULT 95.0,
|
| 26 |
+
processing_time REAL,
|
| 27 |
+
alerts TEXT, -- JSON array of alerts
|
| 28 |
+
color_analysis TEXT, -- JSON object with color data
|
| 29 |
+
|
| 30 |
+
-- Metadata
|
| 31 |
+
strip_type TEXT DEFAULT 'multi-parameter',
|
| 32 |
+
calibration_used TEXT DEFAULT 'standard',
|
| 33 |
+
lighting_conditions TEXT,
|
| 34 |
+
image_quality_score REAL,
|
| 35 |
+
|
| 36 |
+
-- Indexing for performance
|
| 37 |
+
FOREIGN KEY(user_id) REFERENCES users(id)
|
| 38 |
+
);
|
| 39 |
+
|
| 40 |
+
-- Water quality alerts table
|
| 41 |
+
CREATE TABLE IF NOT EXISTS water_alerts (
|
| 42 |
+
id TEXT PRIMARY KEY,
|
| 43 |
+
test_id TEXT NOT NULL,
|
| 44 |
+
alert_type TEXT NOT NULL, -- 'contamination', 'ph_warning', 'bacteria', etc.
|
| 45 |
+
severity TEXT NOT NULL, -- 'low', 'medium', 'high', 'critical'
|
| 46 |
+
message TEXT NOT NULL,
|
| 47 |
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 48 |
+
latitude REAL,
|
| 49 |
+
longitude REAL,
|
| 50 |
+
resolved BOOLEAN DEFAULT FALSE,
|
| 51 |
+
resolved_timestamp DATETIME,
|
| 52 |
+
|
| 53 |
+
FOREIGN KEY(test_id) REFERENCES water_tests(id)
|
| 54 |
+
);
|
| 55 |
+
|
| 56 |
+
-- User profiles table (optional)
|
| 57 |
+
CREATE TABLE IF NOT EXISTS users (
|
| 58 |
+
id TEXT PRIMARY KEY,
|
| 59 |
+
username TEXT UNIQUE,
|
| 60 |
+
email TEXT UNIQUE,
|
| 61 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 62 |
+
location_name TEXT,
|
| 63 |
+
default_latitude REAL,
|
| 64 |
+
default_longitude REAL,
|
| 65 |
+
test_count INTEGER DEFAULT 0,
|
| 66 |
+
last_test_date DATETIME
|
| 67 |
+
);
|
| 68 |
+
|
| 69 |
+
-- Calibration data for improving accuracy
|
| 70 |
+
CREATE TABLE IF NOT EXISTS calibration_data (
|
| 71 |
+
id TEXT PRIMARY KEY,
|
| 72 |
+
parameter TEXT NOT NULL, -- 'ph', 'chlorine', etc.
|
| 73 |
+
color_rgb TEXT NOT NULL, -- JSON array [r, g, b]
|
| 74 |
+
actual_value REAL NOT NULL,
|
| 75 |
+
confidence REAL DEFAULT 100.0,
|
| 76 |
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 77 |
+
source TEXT DEFAULT 'lab_verified', -- 'lab_verified', 'user_reported', 'estimated'
|
| 78 |
+
|
| 79 |
+
-- For machine learning model training
|
| 80 |
+
image_path TEXT,
|
| 81 |
+
lighting_condition TEXT,
|
| 82 |
+
strip_brand TEXT
|
| 83 |
+
);
|
| 84 |
+
|
| 85 |
+
-- Water source locations for mapping
|
| 86 |
+
CREATE TABLE IF NOT EXISTS water_sources (
|
| 87 |
+
id TEXT PRIMARY KEY,
|
| 88 |
+
name TEXT NOT NULL,
|
| 89 |
+
type TEXT NOT NULL, -- 'well', 'lake', 'river', 'tap', etc.
|
| 90 |
+
latitude REAL NOT NULL,
|
| 91 |
+
longitude REAL NOT NULL,
|
| 92 |
+
description TEXT,
|
| 93 |
+
last_tested DATETIME,
|
| 94 |
+
average_quality TEXT,
|
| 95 |
+
test_count INTEGER DEFAULT 0,
|
| 96 |
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
| 97 |
+
);
|
| 98 |
+
|
| 99 |
+
-- Community reports and feedback
|
| 100 |
+
CREATE TABLE IF NOT EXISTS community_reports (
|
| 101 |
+
id TEXT PRIMARY KEY,
|
| 102 |
+
test_id TEXT,
|
| 103 |
+
user_id TEXT,
|
| 104 |
+
report_type TEXT NOT NULL, -- 'accuracy_feedback', 'contamination_report', 'false_positive'
|
| 105 |
+
message TEXT,
|
| 106 |
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 107 |
+
latitude REAL,
|
| 108 |
+
longitude REAL,
|
| 109 |
+
status TEXT DEFAULT 'pending', -- 'pending', 'verified', 'dismissed'
|
| 110 |
+
|
| 111 |
+
FOREIGN KEY(test_id) REFERENCES water_tests(id),
|
| 112 |
+
FOREIGN KEY(user_id) REFERENCES users(id)
|
| 113 |
+
);
|
| 114 |
+
|
| 115 |
+
-- System performance metrics
|
| 116 |
+
CREATE TABLE IF NOT EXISTS system_metrics (
|
| 117 |
+
id TEXT PRIMARY KEY,
|
| 118 |
+
metric_name TEXT NOT NULL,
|
| 119 |
+
metric_value REAL NOT NULL,
|
| 120 |
+
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
| 121 |
+
details TEXT -- JSON object with additional data
|
| 122 |
+
);
|
| 123 |
+
|
| 124 |
+
-- Create indexes for better query performance
|
| 125 |
+
CREATE INDEX IF NOT EXISTS idx_water_tests_location ON water_tests(latitude, longitude);
|
| 126 |
+
CREATE INDEX IF NOT EXISTS idx_water_tests_timestamp ON water_tests(timestamp);
|
| 127 |
+
CREATE INDEX IF NOT EXISTS idx_water_tests_quality ON water_tests(overall_quality, safety_level);
|
| 128 |
+
CREATE INDEX IF NOT EXISTS idx_water_tests_user ON water_tests(user_id);
|
| 129 |
+
|
| 130 |
+
CREATE INDEX IF NOT EXISTS idx_alerts_location ON water_alerts(latitude, longitude);
|
| 131 |
+
CREATE INDEX IF NOT EXISTS idx_alerts_severity ON water_alerts(severity, resolved);
|
| 132 |
+
CREATE INDEX IF NOT EXISTS idx_alerts_timestamp ON water_alerts(timestamp);
|
| 133 |
+
|
| 134 |
+
CREATE INDEX IF NOT EXISTS idx_calibration_parameter ON calibration_data(parameter);
|
| 135 |
+
CREATE INDEX IF NOT EXISTS idx_water_sources_location ON water_sources(latitude, longitude);
|
| 136 |
+
|
| 137 |
+
-- Insert some sample calibration data
|
| 138 |
+
INSERT OR IGNORE INTO calibration_data (id, parameter, color_rgb, actual_value, source) VALUES
|
| 139 |
+
('cal_ph_1', 'ph', '[255, 0, 0]', 4.0, 'lab_verified'),
|
| 140 |
+
('cal_ph_2', 'ph', '[255, 140, 0]', 6.0, 'lab_verified'),
|
| 141 |
+
('cal_ph_3', 'ph', '[255, 255, 0]', 7.0, 'lab_verified'),
|
| 142 |
+
('cal_ph_4', 'ph', '[0, 255, 0]', 8.0, 'lab_verified'),
|
| 143 |
+
('cal_ph_5', 'ph', '[0, 0, 255]', 9.0, 'lab_verified'),
|
| 144 |
+
|
| 145 |
+
('cal_chlorine_1', 'chlorine', '[255, 255, 255]', 0.0, 'lab_verified'),
|
| 146 |
+
('cal_chlorine_2', 'chlorine', '[255, 182, 193]', 1.0, 'lab_verified'),
|
| 147 |
+
('cal_chlorine_3', 'chlorine', '[255, 105, 180]', 2.0, 'lab_verified'),
|
| 148 |
+
('cal_chlorine_4', 'chlorine', '[220, 20, 60]', 4.0, 'lab_verified'),
|
| 149 |
+
|
| 150 |
+
('cal_nitrates_1', 'nitrates', '[255, 255, 255]', 0, 'lab_verified'),
|
| 151 |
+
('cal_nitrates_2', 'nitrates', '[255, 192, 203]', 10, 'lab_verified'),
|
| 152 |
+
('cal_nitrates_3', 'nitrates', '[255, 105, 180]', 25, 'lab_verified'),
|
| 153 |
+
('cal_nitrates_4', 'nitrates', '[255, 69, 0]', 50, 'lab_verified');
|
| 154 |
+
|
| 155 |
+
-- Create a view for easy water quality mapping
|
| 156 |
+
CREATE VIEW IF NOT EXISTS water_quality_map AS
|
| 157 |
+
SELECT
|
| 158 |
+
wt.id,
|
| 159 |
+
wt.latitude,
|
| 160 |
+
wt.longitude,
|
| 161 |
+
wt.water_source,
|
| 162 |
+
wt.overall_quality,
|
| 163 |
+
wt.safety_level,
|
| 164 |
+
wt.ph,
|
| 165 |
+
wt.chlorine,
|
| 166 |
+
wt.nitrates,
|
| 167 |
+
wt.timestamp,
|
| 168 |
+
CASE
|
| 169 |
+
WHEN wt.safety_level = 'Unsafe' THEN 'red'
|
| 170 |
+
WHEN wt.overall_quality = 'Poor' THEN 'orange'
|
| 171 |
+
WHEN wt.overall_quality = 'Fair' THEN 'yellow'
|
| 172 |
+
WHEN wt.overall_quality = 'Good' THEN 'lightgreen'
|
| 173 |
+
ELSE 'green'
|
| 174 |
+
END as marker_color,
|
| 175 |
+
COUNT(wa.id) as alert_count
|
| 176 |
+
FROM water_tests wt
|
| 177 |
+
LEFT JOIN water_alerts wa ON wt.id = wa.test_id AND wa.resolved = FALSE
|
| 178 |
+
WHERE wt.latitude IS NOT NULL AND wt.longitude IS NOT NULL
|
| 179 |
+
GROUP BY wt.id
|
| 180 |
+
ORDER BY wt.timestamp DESC;
|
| 181 |
+
|
| 182 |
+
-- Create a view for recent alerts
|
| 183 |
+
CREATE VIEW IF NOT EXISTS recent_alerts AS
|
| 184 |
+
SELECT
|
| 185 |
+
wa.*,
|
| 186 |
+
wt.water_source,
|
| 187 |
+
wt.overall_quality,
|
| 188 |
+
wt.ph,
|
| 189 |
+
wt.chlorine,
|
| 190 |
+
wt.nitrates
|
| 191 |
+
FROM water_alerts wa
|
| 192 |
+
JOIN water_tests wt ON wa.test_id = wt.id
|
| 193 |
+
WHERE wa.resolved = FALSE
|
| 194 |
+
ORDER BY wa.timestamp DESC;
|
| 195 |
+
|
| 196 |
+
-- Trigger to update user test count
|
| 197 |
+
CREATE TRIGGER IF NOT EXISTS update_user_test_count
|
| 198 |
+
AFTER INSERT ON water_tests
|
| 199 |
+
FOR EACH ROW
|
| 200 |
+
WHEN NEW.user_id IS NOT NULL
|
| 201 |
+
BEGIN
|
| 202 |
+
UPDATE users
|
| 203 |
+
SET test_count = test_count + 1,
|
| 204 |
+
last_test_date = NEW.timestamp
|
| 205 |
+
WHERE id = NEW.user_id;
|
| 206 |
+
END;
|
| 207 |
+
|
| 208 |
+
-- Trigger to create alerts for unsafe water
|
| 209 |
+
CREATE TRIGGER IF NOT EXISTS create_safety_alerts
|
| 210 |
+
AFTER INSERT ON water_tests
|
| 211 |
+
FOR EACH ROW
|
| 212 |
+
WHEN NEW.safety_level = 'Unsafe'
|
| 213 |
+
BEGIN
|
| 214 |
+
INSERT INTO water_alerts (id, test_id, alert_type, severity, message, latitude, longitude)
|
| 215 |
+
VALUES (
|
| 216 |
+
'alert_' || NEW.id,
|
| 217 |
+
NEW.id,
|
| 218 |
+
'contamination',
|
| 219 |
+
'high',
|
| 220 |
+
'Unsafe water quality detected: ' || NEW.overall_quality,
|
| 221 |
+
NEW.latitude,
|
| 222 |
+
NEW.longitude
|
| 223 |
+
);
|
| 224 |
+
END;
|
web/backend/database/schema.sql
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- EcoSpire Database Schema
|
| 2 |
+
-- Complete database structure for production deployment
|
| 3 |
+
|
| 4 |
+
-- Users table
|
| 5 |
+
CREATE TABLE users (
|
| 6 |
+
id SERIAL PRIMARY KEY,
|
| 7 |
+
email VARCHAR(255) UNIQUE NOT NULL,
|
| 8 |
+
password_hash VARCHAR(255) NOT NULL,
|
| 9 |
+
name VARCHAR(255) NOT NULL,
|
| 10 |
+
avatar_url VARCHAR(500),
|
| 11 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 12 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 13 |
+
is_active BOOLEAN DEFAULT true,
|
| 14 |
+
email_verified BOOLEAN DEFAULT false,
|
| 15 |
+
last_login TIMESTAMP,
|
| 16 |
+
preferences JSONB DEFAULT '{}'::jsonb
|
| 17 |
+
);
|
| 18 |
+
|
| 19 |
+
-- User sessions
|
| 20 |
+
CREATE TABLE user_sessions (
|
| 21 |
+
id SERIAL PRIMARY KEY,
|
| 22 |
+
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
| 23 |
+
session_token VARCHAR(255) UNIQUE NOT NULL,
|
| 24 |
+
expires_at TIMESTAMP NOT NULL,
|
| 25 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 26 |
+
ip_address INET,
|
| 27 |
+
user_agent TEXT
|
| 28 |
+
);
|
| 29 |
+
|
| 30 |
+
-- Environmental data
|
| 31 |
+
CREATE TABLE environmental_data (
|
| 32 |
+
id SERIAL PRIMARY KEY,
|
| 33 |
+
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
| 34 |
+
data_type VARCHAR(50) NOT NULL, -- 'air_quality', 'water_quality', 'biodiversity', etc.
|
| 35 |
+
location_lat DECIMAL(10, 8),
|
| 36 |
+
location_lon DECIMAL(11, 8),
|
| 37 |
+
data_values JSONB NOT NULL,
|
| 38 |
+
confidence_score DECIMAL(5, 2),
|
| 39 |
+
source VARCHAR(100), -- 'user_input', 'api', 'sensor', etc.
|
| 40 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 41 |
+
is_public BOOLEAN DEFAULT false
|
| 42 |
+
);
|
| 43 |
+
|
| 44 |
+
-- E-waste items
|
| 45 |
+
CREATE TABLE ewaste_items (
|
| 46 |
+
id SERIAL PRIMARY KEY,
|
| 47 |
+
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
| 48 |
+
device_type VARCHAR(100) NOT NULL,
|
| 49 |
+
brand VARCHAR(100),
|
| 50 |
+
model VARCHAR(200),
|
| 51 |
+
condition VARCHAR(50),
|
| 52 |
+
storage_capacity VARCHAR(50),
|
| 53 |
+
accessories TEXT[],
|
| 54 |
+
estimated_value_min INTEGER,
|
| 55 |
+
estimated_value_max INTEGER,
|
| 56 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 57 |
+
recycled_at TIMESTAMP,
|
| 58 |
+
recycling_method VARCHAR(100)
|
| 59 |
+
);
|
| 60 |
+
|
| 61 |
+
-- Upcycling projects
|
| 62 |
+
CREATE TABLE upcycling_projects (
|
| 63 |
+
id SERIAL PRIMARY KEY,
|
| 64 |
+
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
| 65 |
+
title VARCHAR(255) NOT NULL,
|
| 66 |
+
description TEXT,
|
| 67 |
+
item_type VARCHAR(100),
|
| 68 |
+
material VARCHAR(100),
|
| 69 |
+
condition VARCHAR(100),
|
| 70 |
+
skill_level VARCHAR(50),
|
| 71 |
+
time_required VARCHAR(100),
|
| 72 |
+
estimated_cost VARCHAR(50),
|
| 73 |
+
instructions JSONB,
|
| 74 |
+
materials_needed TEXT[],
|
| 75 |
+
tools_needed TEXT[],
|
| 76 |
+
sustainability_impact TEXT,
|
| 77 |
+
images TEXT[],
|
| 78 |
+
is_completed BOOLEAN DEFAULT false,
|
| 79 |
+
is_public BOOLEAN DEFAULT false,
|
| 80 |
+
likes_count INTEGER DEFAULT 0,
|
| 81 |
+
saves_count INTEGER DEFAULT 0,
|
| 82 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 83 |
+
completed_at TIMESTAMP
|
| 84 |
+
);
|
| 85 |
+
|
| 86 |
+
-- Biodiversity recordings
|
| 87 |
+
CREATE TABLE biodiversity_recordings (
|
| 88 |
+
id SERIAL PRIMARY KEY,
|
| 89 |
+
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
| 90 |
+
location_lat DECIMAL(10, 8),
|
| 91 |
+
location_lon DECIMAL(11, 8),
|
| 92 |
+
habitat_type VARCHAR(100),
|
| 93 |
+
region VARCHAR(100),
|
| 94 |
+
audio_file_url VARCHAR(500),
|
| 95 |
+
duration_seconds INTEGER,
|
| 96 |
+
detected_species JSONB,
|
| 97 |
+
biodiversity_metrics JSONB,
|
| 98 |
+
confidence_score DECIMAL(5, 2),
|
| 99 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 100 |
+
is_verified BOOLEAN DEFAULT false
|
| 101 |
+
);
|
| 102 |
+
|
| 103 |
+
-- Water quality tests
|
| 104 |
+
CREATE TABLE water_quality_tests (
|
| 105 |
+
id SERIAL PRIMARY KEY,
|
| 106 |
+
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
| 107 |
+
location_lat DECIMAL(10, 8),
|
| 108 |
+
location_lon DECIMAL(11, 8),
|
| 109 |
+
water_source VARCHAR(100),
|
| 110 |
+
test_method VARCHAR(50), -- 'image_analysis', 'test_strip', 'manual'
|
| 111 |
+
ph_level DECIMAL(4, 2),
|
| 112 |
+
chlorine_level DECIMAL(6, 3),
|
| 113 |
+
nitrate_level DECIMAL(6, 3),
|
| 114 |
+
hardness_level INTEGER,
|
| 115 |
+
alkalinity_level INTEGER,
|
| 116 |
+
bacteria_count INTEGER,
|
| 117 |
+
turbidity DECIMAL(5, 2),
|
| 118 |
+
overall_quality VARCHAR(50),
|
| 119 |
+
safety_level VARCHAR(50),
|
| 120 |
+
confidence_score DECIMAL(5, 2),
|
| 121 |
+
image_url VARCHAR(500),
|
| 122 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 123 |
+
);
|
| 124 |
+
|
| 125 |
+
-- Carbon footprint tracking
|
| 126 |
+
CREATE TABLE carbon_activities (
|
| 127 |
+
id SERIAL PRIMARY KEY,
|
| 128 |
+
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
| 129 |
+
activity_type VARCHAR(100) NOT NULL, -- 'transport', 'energy', 'food', 'waste'
|
| 130 |
+
activity_name VARCHAR(200) NOT NULL,
|
| 131 |
+
amount DECIMAL(10, 3),
|
| 132 |
+
unit VARCHAR(50),
|
| 133 |
+
co2_equivalent DECIMAL(10, 3), -- kg CO2
|
| 134 |
+
date_recorded DATE NOT NULL,
|
| 135 |
+
location VARCHAR(200),
|
| 136 |
+
notes TEXT,
|
| 137 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 138 |
+
);
|
| 139 |
+
|
| 140 |
+
-- Smart farming data
|
| 141 |
+
CREATE TABLE farming_data (
|
| 142 |
+
id SERIAL PRIMARY KEY,
|
| 143 |
+
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
| 144 |
+
location_lat DECIMAL(10, 8),
|
| 145 |
+
location_lon DECIMAL(11, 8),
|
| 146 |
+
farm_size DECIMAL(10, 2),
|
| 147 |
+
crop_type VARCHAR(100),
|
| 148 |
+
soil_moisture DECIMAL(5, 2),
|
| 149 |
+
soil_temperature DECIMAL(5, 2),
|
| 150 |
+
soil_ph DECIMAL(4, 2),
|
| 151 |
+
ndvi DECIMAL(4, 3),
|
| 152 |
+
precipitation DECIMAL(6, 2),
|
| 153 |
+
temperature DECIMAL(5, 2),
|
| 154 |
+
humidity DECIMAL(5, 2),
|
| 155 |
+
wind_speed DECIMAL(5, 2),
|
| 156 |
+
growing_degree_days INTEGER,
|
| 157 |
+
pest_pressure INTEGER,
|
| 158 |
+
disease_risk INTEGER,
|
| 159 |
+
recommendations TEXT[],
|
| 160 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 161 |
+
);
|
| 162 |
+
|
| 163 |
+
-- Air quality monitoring
|
| 164 |
+
CREATE TABLE air_quality_data (
|
| 165 |
+
id SERIAL PRIMARY KEY,
|
| 166 |
+
location_lat DECIMAL(10, 8) NOT NULL,
|
| 167 |
+
location_lon DECIMAL(11, 8) NOT NULL,
|
| 168 |
+
city_name VARCHAR(200),
|
| 169 |
+
country VARCHAR(100),
|
| 170 |
+
aqi INTEGER,
|
| 171 |
+
pm25 DECIMAL(6, 2),
|
| 172 |
+
pm10 DECIMAL(6, 2),
|
| 173 |
+
o3 DECIMAL(6, 2),
|
| 174 |
+
no2 DECIMAL(6, 2),
|
| 175 |
+
so2 DECIMAL(6, 2),
|
| 176 |
+
co DECIMAL(6, 2),
|
| 177 |
+
temperature DECIMAL(5, 2),
|
| 178 |
+
humidity DECIMAL(5, 2),
|
| 179 |
+
pressure DECIMAL(7, 2),
|
| 180 |
+
wind_speed DECIMAL(5, 2),
|
| 181 |
+
wind_direction INTEGER,
|
| 182 |
+
visibility DECIMAL(5, 2),
|
| 183 |
+
uv_index INTEGER,
|
| 184 |
+
data_source VARCHAR(100),
|
| 185 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 186 |
+
);
|
| 187 |
+
|
| 188 |
+
-- User achievements
|
| 189 |
+
CREATE TABLE user_achievements (
|
| 190 |
+
id SERIAL PRIMARY KEY,
|
| 191 |
+
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
| 192 |
+
achievement_type VARCHAR(100) NOT NULL,
|
| 193 |
+
achievement_name VARCHAR(200) NOT NULL,
|
| 194 |
+
description TEXT,
|
| 195 |
+
points_earned INTEGER DEFAULT 0,
|
| 196 |
+
badge_icon VARCHAR(100),
|
| 197 |
+
earned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 198 |
+
is_public BOOLEAN DEFAULT true
|
| 199 |
+
);
|
| 200 |
+
|
| 201 |
+
-- Community projects
|
| 202 |
+
CREATE TABLE community_projects (
|
| 203 |
+
id SERIAL PRIMARY KEY,
|
| 204 |
+
creator_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
| 205 |
+
title VARCHAR(255) NOT NULL,
|
| 206 |
+
description TEXT,
|
| 207 |
+
category VARCHAR(100),
|
| 208 |
+
location VARCHAR(200),
|
| 209 |
+
funding_goal INTEGER,
|
| 210 |
+
funding_raised INTEGER DEFAULT 0,
|
| 211 |
+
start_date DATE,
|
| 212 |
+
end_date DATE,
|
| 213 |
+
status VARCHAR(50) DEFAULT 'active',
|
| 214 |
+
impact_metrics JSONB,
|
| 215 |
+
images TEXT[],
|
| 216 |
+
website_url VARCHAR(500),
|
| 217 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 218 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 219 |
+
);
|
| 220 |
+
|
| 221 |
+
-- User interactions (likes, saves, shares)
|
| 222 |
+
CREATE TABLE user_interactions (
|
| 223 |
+
id SERIAL PRIMARY KEY,
|
| 224 |
+
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
| 225 |
+
target_type VARCHAR(50) NOT NULL, -- 'project', 'recording', 'test', etc.
|
| 226 |
+
target_id INTEGER NOT NULL,
|
| 227 |
+
interaction_type VARCHAR(50) NOT NULL, -- 'like', 'save', 'share', 'comment'
|
| 228 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 229 |
+
UNIQUE(user_id, target_type, target_id, interaction_type)
|
| 230 |
+
);
|
| 231 |
+
|
| 232 |
+
-- System analytics
|
| 233 |
+
CREATE TABLE system_analytics (
|
| 234 |
+
id SERIAL PRIMARY KEY,
|
| 235 |
+
event_type VARCHAR(100) NOT NULL,
|
| 236 |
+
event_data JSONB,
|
| 237 |
+
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
| 238 |
+
session_id VARCHAR(255),
|
| 239 |
+
ip_address INET,
|
| 240 |
+
user_agent TEXT,
|
| 241 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 242 |
+
);
|
| 243 |
+
|
| 244 |
+
-- API usage tracking
|
| 245 |
+
CREATE TABLE api_usage (
|
| 246 |
+
id SERIAL PRIMARY KEY,
|
| 247 |
+
api_name VARCHAR(100) NOT NULL,
|
| 248 |
+
endpoint VARCHAR(200),
|
| 249 |
+
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
| 250 |
+
request_count INTEGER DEFAULT 1,
|
| 251 |
+
response_time_ms INTEGER,
|
| 252 |
+
status_code INTEGER,
|
| 253 |
+
error_message TEXT,
|
| 254 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 255 |
+
);
|
| 256 |
+
|
| 257 |
+
-- Create indexes for better performance
|
| 258 |
+
CREATE INDEX idx_users_email ON users(email);
|
| 259 |
+
CREATE INDEX idx_users_created_at ON users(created_at);
|
| 260 |
+
CREATE INDEX idx_environmental_data_user_id ON environmental_data(user_id);
|
| 261 |
+
CREATE INDEX idx_environmental_data_type ON environmental_data(data_type);
|
| 262 |
+
CREATE INDEX idx_environmental_data_location ON environmental_data(location_lat, location_lon);
|
| 263 |
+
CREATE INDEX idx_environmental_data_created_at ON environmental_data(created_at);
|
| 264 |
+
CREATE INDEX idx_ewaste_items_user_id ON ewaste_items(user_id);
|
| 265 |
+
CREATE INDEX idx_ewaste_items_device_type ON ewaste_items(device_type);
|
| 266 |
+
CREATE INDEX idx_upcycling_projects_user_id ON upcycling_projects(user_id);
|
| 267 |
+
CREATE INDEX idx_upcycling_projects_public ON upcycling_projects(is_public);
|
| 268 |
+
CREATE INDEX idx_biodiversity_recordings_user_id ON biodiversity_recordings(user_id);
|
| 269 |
+
CREATE INDEX idx_biodiversity_recordings_location ON biodiversity_recordings(location_lat, location_lon);
|
| 270 |
+
CREATE INDEX idx_water_quality_tests_user_id ON water_quality_tests(user_id);
|
| 271 |
+
CREATE INDEX idx_carbon_activities_user_id ON carbon_activities(user_id);
|
| 272 |
+
CREATE INDEX idx_carbon_activities_date ON carbon_activities(date_recorded);
|
| 273 |
+
CREATE INDEX idx_farming_data_user_id ON farming_data(user_id);
|
| 274 |
+
CREATE INDEX idx_air_quality_location ON air_quality_data(location_lat, location_lon);
|
| 275 |
+
CREATE INDEX idx_air_quality_created_at ON air_quality_data(created_at);
|
| 276 |
+
CREATE INDEX idx_user_achievements_user_id ON user_achievements(user_id);
|
| 277 |
+
CREATE INDEX idx_community_projects_status ON community_projects(status);
|
| 278 |
+
CREATE INDEX idx_user_interactions_user_id ON user_interactions(user_id);
|
| 279 |
+
CREATE INDEX idx_user_interactions_target ON user_interactions(target_type, target_id);
|
| 280 |
+
CREATE INDEX idx_system_analytics_event_type ON system_analytics(event_type);
|
| 281 |
+
CREATE INDEX idx_system_analytics_created_at ON system_analytics(created_at);
|
| 282 |
+
CREATE INDEX idx_api_usage_api_name ON api_usage(api_name);
|
| 283 |
+
CREATE INDEX idx_api_usage_created_at ON api_usage(created_at);
|
| 284 |
+
|
| 285 |
+
-- Create triggers for updated_at timestamps
|
| 286 |
+
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
| 287 |
+
RETURNS TRIGGER AS $$
|
| 288 |
+
BEGIN
|
| 289 |
+
NEW.updated_at = CURRENT_TIMESTAMP;
|
| 290 |
+
RETURN NEW;
|
| 291 |
+
END;
|
| 292 |
+
$$ language 'plpgsql';
|
| 293 |
+
|
| 294 |
+
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
|
| 295 |
+
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
| 296 |
+
|
| 297 |
+
CREATE TRIGGER update_community_projects_updated_at BEFORE UPDATE ON community_projects
|
| 298 |
+
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
web/backend/database/water_quality.db
ADDED
|
Binary file (28.7 kB). View file
|
|
|
web/backend/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
web/backend/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "ecospire-backend",
|
| 3 |
+
"version": "1.0.0",
|
| 4 |
+
"description": "Backend server for the EcoSpire Environmental Intelligence Platform",
|
| 5 |
+
"main": "server.js",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"start": "node server.js",
|
| 8 |
+
"dev": "nodemon server.js"
|
| 9 |
+
},
|
| 10 |
+
"keywords": [
|
| 11 |
+
"nodejs",
|
| 12 |
+
"express",
|
| 13 |
+
"environment",
|
| 14 |
+
"ai"
|
| 15 |
+
],
|
| 16 |
+
"author": "EcoSpire Team",
|
| 17 |
+
"license": "MIT",
|
| 18 |
+
"dependencies": {
|
| 19 |
+
"cors": "^2.8.5",
|
| 20 |
+
"dotenv": "^16.3.1",
|
| 21 |
+
"express": "^4.18.2",
|
| 22 |
+
"multer": "^1.4.5-lts.1",
|
| 23 |
+
"sharp": "^0.32.6",
|
| 24 |
+
"sqlite3": "^5.1.6",
|
| 25 |
+
"uuid": "^9.0.1"
|
| 26 |
+
}
|
| 27 |
+
}
|
web/backend/python/audio_analyzer.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import json
|
| 3 |
+
import time
|
| 4 |
+
import random
|
| 5 |
+
import librosa # The new library for real audio analysis
|
| 6 |
+
import numpy as np # The library for numerical operations
|
| 7 |
+
|
| 8 |
+
# --- This is our new "intelligence" factor ---
|
| 9 |
+
# We'll consider any audio with average energy below this threshold as silence.
|
| 10 |
+
# You can experiment with this value; lower values make it more sensitive to quiet sounds.
|
| 11 |
+
SILENCE_THRESHOLD = 0.001
|
| 12 |
+
|
| 13 |
+
def get_mock_species_data(species_name):
|
| 14 |
+
"""
|
| 15 |
+
Returns a rich, detailed data structure for a given species name.
|
| 16 |
+
This simulates a database lookup for species information.
|
| 17 |
+
"""
|
| 18 |
+
all_species = {
|
| 19 |
+
"European Robin": { "scientificName": "Erithacus rubecula", "icon": "🐦", "conservationStatus": "Least Concern", "description": "A small insectivorous passerine bird. Known for its bright orange-red breast.", "habitat": "Woodlands, parks, gardens", "frequency": "2-6 kHz", "callType": "Melodic song", "sound": "Clear, warbling notes" },
|
| 20 |
+
"Great Tit": { "scientificName": "Parus major", "icon": "🐦", "conservationStatus": "Least Concern", "description": "A distinctive bird with a black head and neck, prominent white cheeks, and a black stripe down its yellow front.", "habitat": "Deciduous woodland, gardens", "frequency": "3-7 kHz", "callType": "Repetitive two-note song", "sound": "'Teacher-teacher' sound" },
|
| 21 |
+
"Common Nightingale": { "scientificName": "Luscinia megarhynchos", "icon": "🎶", "conservationStatus": "Least Concern", "description": "A small passerine bird best known for its powerful and beautiful song.", "habitat": "Dense scrub and woodland", "frequency": "1-8 kHz", "callType": "Complex, rich song", "sound": "Crescendo of notes" },
|
| 22 |
+
"Red-winged Blackbird": { "scientificName": "Agelaius phoeniceus", "icon": "⚫", "conservationStatus": "Least Concern", "description": "A passerine bird of the family Icteridae. Males are black with a red and yellow shoulder patch.", "habitat": "Marshes, wetlands", "frequency": "2-4 kHz", "callType": "Gurgling song", "sound": "'Conk-la-ree' sound" }
|
| 23 |
+
}
|
| 24 |
+
return all_species.get(species_name, { "scientificName": "Unknown", "icon": "❓", "conservationStatus": "Unknown", "description": "Could not identify species.", "habitat": "Unknown", "frequency": "N/A", "callType": "N/A", "sound": "N/A" })
|
| 25 |
+
|
| 26 |
+
def analyze_audio_file(file_path):
|
| 27 |
+
"""
|
| 28 |
+
A smarter simulation of AI audio analysis.
|
| 29 |
+
It now performs REAL energy analysis to detect silence.
|
| 30 |
+
"""
|
| 31 |
+
# --- REAL ANALYSIS STEP ---
|
| 32 |
+
# Load the audio file using librosa. This gives us the raw sound wave (y)
|
| 33 |
+
# and the sample rate (sr).
|
| 34 |
+
y, sr = librosa.load(file_path, sr=None, mono=True, res_type='kaiser_fast')
|
| 35 |
+
|
| 36 |
+
# Calculate the Root Mean Square (RMS) energy, a measure of average volume.
|
| 37 |
+
rms_energy = np.sqrt(np.mean(y**2))
|
| 38 |
+
|
| 39 |
+
# --- INTELLIGENT DECISION STEP ---
|
| 40 |
+
# If the energy is below our silence threshold, return a specific "silent" result.
|
| 41 |
+
if rms_energy < SILENCE_THRESHOLD:
|
| 42 |
+
return {
|
| 43 |
+
"confidence": 95,
|
| 44 |
+
"analysisQuality": "High",
|
| 45 |
+
"detectedSpecies": [], # Return an empty list of species
|
| 46 |
+
"biodiversityMetrics": {
|
| 47 |
+
"biodiversityScore": 0,
|
| 48 |
+
"shannonIndex": 0,
|
| 49 |
+
"ecosystemHealth": "Unknown (Silence)"
|
| 50 |
+
},
|
| 51 |
+
"acousticFeatures": { "duration": librosa.get_duration(y=y, sr=sr), "sampleRate": sr },
|
| 52 |
+
"recommendations": [
|
| 53 |
+
"No significant audio was detected in this recording.",
|
| 54 |
+
"Try recording in a location with more natural sounds.",
|
| 55 |
+
"Ensure your microphone is working and not covered."
|
| 56 |
+
]
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
# --- SIMULATION STEP (if sound is detected) ---
|
| 60 |
+
# If the audio is NOT silent, we proceed with our previous simulation.
|
| 61 |
+
time.sleep(random.uniform(1.5, 2.5)) # Simulate processing time
|
| 62 |
+
|
| 63 |
+
possible_species = ["European Robin", "Great Tit", "Common Nightingale", "Red-winged Blackbird"]
|
| 64 |
+
num_detected = random.randint(1, 3)
|
| 65 |
+
detected_species_names = random.sample(possible_species, num_detected)
|
| 66 |
+
|
| 67 |
+
detected_species_results = []
|
| 68 |
+
for species_name in detected_species_names:
|
| 69 |
+
species_data = get_mock_species_data(species_name)
|
| 70 |
+
species_data["name"] = species_name
|
| 71 |
+
species_data["confidence"] = random.randint(75, 98)
|
| 72 |
+
detected_species_results.append(species_data)
|
| 73 |
+
|
| 74 |
+
biodiversity_score = 60 + len(detected_species_results) * 15 + random.randint(-5, 5)
|
| 75 |
+
ecosystem_health = "Excellent" if biodiversity_score > 85 else "Good" if biodiversity_score > 70 else "Fair"
|
| 76 |
+
shannon_index = round(1.2 + len(detected_species_results) * 0.2 + random.uniform(-0.1, 0.1), 2)
|
| 77 |
+
|
| 78 |
+
return {
|
| 79 |
+
"confidence": random.randint(85, 99),
|
| 80 |
+
"analysisQuality": "High",
|
| 81 |
+
"detectedSpecies": detected_species_results,
|
| 82 |
+
"biodiversityMetrics": {
|
| 83 |
+
"biodiversityScore": biodiversity_score,
|
| 84 |
+
"shannonIndex": shannon_index,
|
| 85 |
+
"ecosystemHealth": ecosystem_health
|
| 86 |
+
},
|
| 87 |
+
"acousticFeatures": { "duration": librosa.get_duration(y=y, sr=sr), "sampleRate": sr },
|
| 88 |
+
"recommendations": [
|
| 89 |
+
"This area shows healthy species diversity.",
|
| 90 |
+
"Consider conservation efforts for nearby wetlands.",
|
| 91 |
+
"Continue monitoring during migratory seasons."
|
| 92 |
+
]
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
if __name__ == "__main__":
|
| 96 |
+
try:
|
| 97 |
+
audio_file_path = sys.argv[1]
|
| 98 |
+
analysis_data = analyze_audio_file(audio_file_path)
|
| 99 |
+
print(json.dumps(analysis_data, indent=4))
|
| 100 |
+
except Exception as e:
|
| 101 |
+
print(f"Error in Python script: {e}", file=sys.stderr)
|
| 102 |
+
sys.exit(1)
|
web/backend/python/blast_db/biostream_db.ndb
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bbe8194a25836998c96b70c10f49657c14706ad0044289b3e56ac7f983ab180c
|
| 3 |
+
size 500000
|
web/backend/python/blast_db/biostream_db.nhr
ADDED
|
Binary file (313 Bytes). View file
|
|
|
web/backend/python/blast_db/biostream_db.nin
ADDED
|
Binary file (136 Bytes). View file
|
|
|
web/backend/python/blast_db/biostream_db.njs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": "1.2",
|
| 3 |
+
"dbname": "biostream_db",
|
| 4 |
+
"dbtype": "Nucleotide",
|
| 5 |
+
"db-version": 5,
|
| 6 |
+
"description": "custom_database.fasta",
|
| 7 |
+
"number-of-letters": 2228,
|
| 8 |
+
"number-of-sequences": 2,
|
| 9 |
+
"last-updated": "2025-08-21T21:16:00",
|
| 10 |
+
"number-of-volumes": 1,
|
| 11 |
+
"bytes-total": 1001052,
|
| 12 |
+
"bytes-to-cache": 695,
|
| 13 |
+
"files": [
|
| 14 |
+
"biostream_db.ndb",
|
| 15 |
+
"biostream_db.nhr",
|
| 16 |
+
"biostream_db.nin",
|
| 17 |
+
"biostream_db.not",
|
| 18 |
+
"biostream_db.nsq",
|
| 19 |
+
"biostream_db.ntf",
|
| 20 |
+
"biostream_db.nto"
|
| 21 |
+
]
|
| 22 |
+
}
|
web/backend/python/blast_db/biostream_db.not
ADDED
|
Binary file (32 Bytes). View file
|
|
|
web/backend/python/blast_db/biostream_db.nsq
ADDED
|
Binary file (559 Bytes). View file
|
|
|
web/backend/python/blast_db/biostream_db.ntf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:811fcc1c59308d9bfc396b93a0d861101454290622bd4ce6f8abc450cf05593f
|
| 3 |
+
size 500000
|
web/backend/python/blast_db/biostream_db.nto
ADDED
|
Binary file (12 Bytes). View file
|
|
|
web/backend/python/dna_analyzer.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# FILE: web/backend/python/dna_analyzer.py (FINAL VERSION - Shows ID and Name)
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
import json
|
| 5 |
+
import subprocess
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
def get_species_details(blast_id):
|
| 9 |
+
"""
|
| 10 |
+
Looks up the ugly BLAST ID and returns a rich data structure
|
| 11 |
+
containing the pretty name and other info.
|
| 12 |
+
"""
|
| 13 |
+
species_info_db = {
|
| 14 |
+
"KY045437.1": {
|
| 15 |
+
"species": "Salmo trutta",
|
| 16 |
+
"commonName": "Brown Trout",
|
| 17 |
+
"kingdom": "Animalia",
|
| 18 |
+
"phylum": "Chordata",
|
| 19 |
+
"ecologicalRole": "Top predator",
|
| 20 |
+
"conservationStatus": "Least Concern",
|
| 21 |
+
"indicators": ["Healthy fish population", "Good water quality"]
|
| 22 |
+
},
|
| 23 |
+
"LC143821.1": {
|
| 24 |
+
"species": "Escherichia coli",
|
| 25 |
+
"commonName": "E. coli",
|
| 26 |
+
"kingdom": "Bacteria",
|
| 27 |
+
"phylum": "Proteobacteria",
|
| 28 |
+
"ecologicalRole": "Decomposer",
|
| 29 |
+
"conservationStatus": "Pathogen Indicator",
|
| 30 |
+
"indicators": ["Fecal contamination", "Health risk"]
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
for known_id, data in species_info_db.items():
|
| 35 |
+
if known_id in blast_id:
|
| 36 |
+
# --- THIS IS THE CHANGE ---
|
| 37 |
+
# We add the raw blast_id to the data we return.
|
| 38 |
+
data['blastId'] = blast_id
|
| 39 |
+
return data
|
| 40 |
+
|
| 41 |
+
# If we don't find a match, we still return the ID.
|
| 42 |
+
return {"species": blast_id, "commonName": "Unknown Species", "blastId": blast_id}
|
| 43 |
+
|
| 44 |
+
def run_real_dna_analysis(file_path):
|
| 45 |
+
try:
|
| 46 |
+
script_dir = os.path.dirname(__file__)
|
| 47 |
+
blast_db_dir = os.path.join(script_dir, 'blast_db')
|
| 48 |
+
db_name = 'biostream_db'
|
| 49 |
+
output_path = os.path.join(script_dir, '..', 'temp', f"{os.path.basename(file_path)}.csv")
|
| 50 |
+
absolute_input_path = os.path.abspath(file_path)
|
| 51 |
+
|
| 52 |
+
blast_command = [
|
| 53 |
+
'blastn', '-query', absolute_input_path, '-db', db_name,
|
| 54 |
+
'-out', output_path, '-outfmt', "10 sseqid pident", '-subject_besthit'
|
| 55 |
+
]
|
| 56 |
+
|
| 57 |
+
process = subprocess.run(
|
| 58 |
+
blast_command, check=True, capture_output=True, text=True, cwd=blast_db_dir
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
species_hits = {}
|
| 62 |
+
if not os.path.exists(output_path):
|
| 63 |
+
return {"error": "BLAST did not produce an output file."}
|
| 64 |
+
|
| 65 |
+
with open(output_path, 'r') as f:
|
| 66 |
+
for line in f:
|
| 67 |
+
parts = line.strip().split(',')
|
| 68 |
+
if len(parts) < 2: continue
|
| 69 |
+
species_id, identity = parts[0], float(parts[1])
|
| 70 |
+
if species_id not in species_hits:
|
| 71 |
+
species_hits[species_id] = {'count': 0, 'total_identity': 0}
|
| 72 |
+
species_hits[species_id]['count'] += 1
|
| 73 |
+
species_hits[species_id]['total_identity'] += identity
|
| 74 |
+
|
| 75 |
+
if not species_hits:
|
| 76 |
+
return { "detectedSpecies": [], "biodiversityMetrics": { "biodiversityScore": 0, "ecosystemHealth": "No Match Found" }}
|
| 77 |
+
|
| 78 |
+
detected_species_list = []
|
| 79 |
+
for species_id, data in species_hits.items():
|
| 80 |
+
details = get_species_details(species_id)
|
| 81 |
+
avg_identity = data['total_identity'] / data['count']
|
| 82 |
+
|
| 83 |
+
details['confidence'] = round(avg_identity, 2)
|
| 84 |
+
details['abundance'] = "Medium"
|
| 85 |
+
details['dnaFragments'] = data['count']
|
| 86 |
+
detected_species_list.append(details)
|
| 87 |
+
|
| 88 |
+
os.remove(output_path)
|
| 89 |
+
|
| 90 |
+
return {
|
| 91 |
+
"detectedSpecies": detected_species_list,
|
| 92 |
+
"biodiversityMetrics": { "speciesRichness": len(detected_species_list), "biodiversityScore": 85, "ecosystemHealth": "Good"},
|
| 93 |
+
"waterQualityAssessment": { "overallQuality": "Good", "recommendations": ["Analysis complete."] }
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
except subprocess.CalledProcessError as e:
|
| 97 |
+
return {"error": f"BLAST analysis failed. Details: {e.stderr}"}
|
| 98 |
+
except Exception as e:
|
| 99 |
+
return {"error": f"An error occurred in Python: {str(e)}"}
|
| 100 |
+
|
| 101 |
+
if __name__ == "__main__":
|
| 102 |
+
try:
|
| 103 |
+
dna_file_path = sys.argv[1]
|
| 104 |
+
analysis_report = run_real_dna_analysis(dna_file_path)
|
| 105 |
+
print(json.dumps(analysis_report))
|
| 106 |
+
except Exception as e:
|
| 107 |
+
print(json.dumps({"error": str(e)}), file=sys.stderr)
|
| 108 |
+
sys.exit(1)
|
web/backend/python/ewaste_analyzer.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# FILE: web/backend/python/ewaste_analyzer.py
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
import json
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
|
| 7 |
+
# --- FULL DATABASE AND MULTIPLIERS (FROM YOUR JS UTILITY) ---
|
| 8 |
+
device_database = {
|
| 9 |
+
'smartphones': {
|
| 10 |
+
'brands': {
|
| 11 |
+
'Apple': {
|
| 12 |
+
'models': {
|
| 13 |
+
'iPhone 15 Pro Max': {'basePrice': 800, 'releaseYear': 2023},
|
| 14 |
+
'iPhone 15 Pro': {'basePrice': 700, 'releaseYear': 2023},
|
| 15 |
+
'iPhone 15': {'basePrice': 500, 'releaseYear': 2023},
|
| 16 |
+
'iPhone 14 Pro Max': {'basePrice': 650, 'releaseYear': 2022},
|
| 17 |
+
'iPhone 14 Pro': {'basePrice': 550, 'releaseYear': 2022},
|
| 18 |
+
'iPhone 14': {'basePrice': 400, 'releaseYear': 2022},
|
| 19 |
+
'iPhone 13 Pro Max': {'basePrice': 500, 'releaseYear': 2021},
|
| 20 |
+
'iPhone 13 Pro': {'basePrice': 450, 'releaseYear': 2021},
|
| 21 |
+
'iPhone 13': {'basePrice': 350, 'releaseYear': 2021},
|
| 22 |
+
'iPhone 12 Pro Max': {'basePrice': 400, 'releaseYear': 2020},
|
| 23 |
+
'iPhone 12 Pro': {'basePrice': 350, 'releaseYear': 2020},
|
| 24 |
+
'iPhone 12': {'basePrice': 280, 'releaseYear': 2020},
|
| 25 |
+
'iPhone 11 Pro Max': {'basePrice': 300, 'releaseYear': 2019},
|
| 26 |
+
'iPhone 11 Pro': {'basePrice': 250, 'releaseYear': 2019},
|
| 27 |
+
'iPhone 11': {'basePrice': 200, 'releaseYear': 2019},
|
| 28 |
+
'iPhone XS Max': {'basePrice': 200, 'releaseYear': 2018},
|
| 29 |
+
'iPhone XS': {'basePrice': 180, 'releaseYear': 2018},
|
| 30 |
+
'iPhone XR': {'basePrice': 150, 'releaseYear': 2018},
|
| 31 |
+
'iPhone X': {'basePrice': 120, 'releaseYear': 2017},
|
| 32 |
+
'iPhone 8 Plus': {'basePrice': 100, 'releaseYear': 2017},
|
| 33 |
+
'iPhone 8': {'basePrice': 80, 'releaseYear': 2017},
|
| 34 |
+
'iPhone 7 Plus': {'basePrice': 70, 'releaseYear': 2016},
|
| 35 |
+
'iPhone 7': {'basePrice': 50, 'releaseYear': 2016}
|
| 36 |
+
}
|
| 37 |
+
},
|
| 38 |
+
'Samsung': {
|
| 39 |
+
'models': {
|
| 40 |
+
'Galaxy S24 Ultra': {'basePrice': 600, 'releaseYear': 2024},
|
| 41 |
+
'Galaxy S24+': {'basePrice': 500, 'releaseYear': 2024},
|
| 42 |
+
'Galaxy S24': {'basePrice': 400, 'releaseYear': 2024},
|
| 43 |
+
'Galaxy S23 Ultra': {'basePrice': 500, 'releaseYear': 2023},
|
| 44 |
+
'Galaxy S23+': {'basePrice': 400, 'releaseYear': 2023},
|
| 45 |
+
'Galaxy S23': {'basePrice': 320, 'releaseYear': 2023},
|
| 46 |
+
'Galaxy S22 Ultra': {'basePrice': 400, 'releaseYear': 2022},
|
| 47 |
+
'Galaxy S22+': {'basePrice': 320, 'releaseYear': 2022},
|
| 48 |
+
'Galaxy S22': {'basePrice': 250, 'releaseYear': 2022},
|
| 49 |
+
'Galaxy S21 Ultra': {'basePrice': 350, 'releaseYear': 2021},
|
| 50 |
+
'Galaxy S21+': {'basePrice': 280, 'releaseYear': 2021},
|
| 51 |
+
'Galaxy S21': {'basePrice': 220, 'releaseYear': 2021},
|
| 52 |
+
'Galaxy Note 20 Ultra': {'basePrice': 300, 'releaseYear': 2020},
|
| 53 |
+
'Galaxy Note 20': {'basePrice': 250, 'releaseYear': 2020},
|
| 54 |
+
'Galaxy S20 Ultra': {'basePrice': 280, 'releaseYear': 2020},
|
| 55 |
+
'Galaxy S20+': {'basePrice': 220, 'releaseYear': 2020},
|
| 56 |
+
'Galaxy S20': {'basePrice': 180, 'releaseYear': 2020},
|
| 57 |
+
'Galaxy Note 10+': {'basePrice': 200, 'releaseYear': 2019},
|
| 58 |
+
'Galaxy Note 10': {'basePrice': 170, 'releaseYear': 2019},
|
| 59 |
+
'Galaxy S10+': {'basePrice': 150, 'releaseYear': 2019},
|
| 60 |
+
'Galaxy S10': {'basePrice': 120, 'releaseYear': 2019}
|
| 61 |
+
}
|
| 62 |
+
},
|
| 63 |
+
'Google': {
|
| 64 |
+
'models': {
|
| 65 |
+
'Pixel 8 Pro': {'basePrice': 450, 'releaseYear': 2023},
|
| 66 |
+
'Pixel 8': {'basePrice': 350, 'releaseYear': 2023},
|
| 67 |
+
'Pixel 7 Pro': {'basePrice': 350, 'releaseYear': 2022},
|
| 68 |
+
'Pixel 7': {'basePrice': 280, 'releaseYear': 2022},
|
| 69 |
+
'Pixel 6 Pro': {'basePrice': 280, 'releaseYear': 2021},
|
| 70 |
+
'Pixel 6': {'basePrice': 220, 'releaseYear': 2021},
|
| 71 |
+
'Pixel 5': {'basePrice': 150, 'releaseYear': 2020},
|
| 72 |
+
'Pixel 4 XL': {'basePrice': 120, 'releaseYear': 2019},
|
| 73 |
+
'Pixel 4': {'basePrice': 100, 'releaseYear': 2019}
|
| 74 |
+
}
|
| 75 |
+
},
|
| 76 |
+
'OnePlus': {
|
| 77 |
+
'models': {
|
| 78 |
+
'OnePlus 12': {'basePrice': 400, 'releaseYear': 2024},
|
| 79 |
+
'OnePlus 11': {'basePrice': 320, 'releaseYear': 2023},
|
| 80 |
+
'OnePlus 10 Pro': {'basePrice': 280, 'releaseYear': 2022},
|
| 81 |
+
'OnePlus 9 Pro': {'basePrice': 220, 'releaseYear': 2021},
|
| 82 |
+
'OnePlus 9': {'basePrice': 180, 'releaseYear': 2021},
|
| 83 |
+
'OnePlus 8 Pro': {'basePrice': 150, 'releaseYear': 2020},
|
| 84 |
+
'OnePlus 8': {'basePrice': 120, 'releaseYear': 2020}
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
},
|
| 89 |
+
'laptops': {
|
| 90 |
+
'brands': {
|
| 91 |
+
'Apple': {
|
| 92 |
+
'models': {
|
| 93 |
+
'MacBook Pro 16" M3': {'basePrice': 1800, 'releaseYear': 2023},
|
| 94 |
+
'MacBook Pro 14" M3': {'basePrice': 1400, 'releaseYear': 2023},
|
| 95 |
+
'MacBook Air M3': {'basePrice': 900, 'releaseYear': 2024},
|
| 96 |
+
'MacBook Pro 16" M2': {'basePrice': 1500, 'releaseYear': 2022},
|
| 97 |
+
'MacBook Pro 14" M2': {'basePrice': 1200, 'releaseYear': 2022},
|
| 98 |
+
'MacBook Air M2': {'basePrice': 800, 'releaseYear': 2022},
|
| 99 |
+
'MacBook Pro 16" M1': {'basePrice': 1200, 'releaseYear': 2021},
|
| 100 |
+
'MacBook Pro 14" M1': {'basePrice': 1000, 'releaseYear': 2021},
|
| 101 |
+
'MacBook Air M1': {'basePrice': 650, 'releaseYear': 2020},
|
| 102 |
+
'MacBook Pro 16" Intel': {'basePrice': 800, 'releaseYear': 2019},
|
| 103 |
+
'MacBook Pro 13" Intel': {'basePrice': 600, 'releaseYear': 2020},
|
| 104 |
+
'MacBook Air Intel': {'basePrice': 400, 'releaseYear': 2020}
|
| 105 |
+
}
|
| 106 |
+
},
|
| 107 |
+
'Dell': {
|
| 108 |
+
'models': {
|
| 109 |
+
'XPS 15 (2024)': {'basePrice': 1000, 'releaseYear': 2024},
|
| 110 |
+
'XPS 13 (2024)': {'basePrice': 800, 'releaseYear': 2024},
|
| 111 |
+
'XPS 15 (2023)': {'basePrice': 900, 'releaseYear': 2023},
|
| 112 |
+
'XPS 13 (2023)': {'basePrice': 700, 'releaseYear': 2023},
|
| 113 |
+
'XPS 15 (2022)': {'basePrice': 800, 'releaseYear': 2022},
|
| 114 |
+
'XPS 13 (2022)': {'basePrice': 600, 'releaseYear': 2022},
|
| 115 |
+
'Inspiron 15 7000': {'basePrice': 400, 'releaseYear': 2023},
|
| 116 |
+
'Inspiron 14 5000': {'basePrice': 300, 'releaseYear': 2023},
|
| 117 |
+
'Latitude 7420': {'basePrice': 500, 'releaseYear': 2021},
|
| 118 |
+
'Latitude 5520': {'basePrice': 350, 'releaseYear': 2021}
|
| 119 |
+
}
|
| 120 |
+
},
|
| 121 |
+
'HP': {
|
| 122 |
+
'models': {
|
| 123 |
+
'Spectre x360 16': {'basePrice': 900, 'releaseYear': 2023},
|
| 124 |
+
'Spectre x360 14': {'basePrice': 700, 'releaseYear': 2023},
|
| 125 |
+
'EliteBook 850 G9': {'basePrice': 600, 'releaseYear': 2022},
|
| 126 |
+
'Pavilion 15': {'basePrice': 350, 'releaseYear': 2023},
|
| 127 |
+
'Envy 13': {'basePrice': 450, 'releaseYear': 2022},
|
| 128 |
+
'ProBook 450 G9': {'basePrice': 400, 'releaseYear': 2022}
|
| 129 |
+
}
|
| 130 |
+
},
|
| 131 |
+
'Lenovo': {
|
| 132 |
+
'models': {
|
| 133 |
+
'ThinkPad X1 Carbon Gen 11': {'basePrice': 1000, 'releaseYear': 2023},
|
| 134 |
+
'ThinkPad X1 Carbon Gen 10': {'basePrice': 850, 'releaseYear': 2022},
|
| 135 |
+
'ThinkPad T14 Gen 4': {'basePrice': 600, 'releaseYear': 2023},
|
| 136 |
+
'ThinkPad T14 Gen 3': {'basePrice': 500, 'releaseYear': 2022},
|
| 137 |
+
'IdeaPad 5 Pro': {'basePrice': 400, 'releaseYear': 2023},
|
| 138 |
+
'Legion 5 Pro': {'basePrice': 800, 'releaseYear': 2023},
|
| 139 |
+
'Yoga 9i': {'basePrice': 700, 'releaseYear': 2023}
|
| 140 |
+
}
|
| 141 |
+
},
|
| 142 |
+
'ASUS': {
|
| 143 |
+
'models': {
|
| 144 |
+
'ZenBook Pro 16X': {'basePrice': 1200, 'releaseYear': 2023},
|
| 145 |
+
'ZenBook 14': {'basePrice': 600, 'releaseYear': 2023},
|
| 146 |
+
'ROG Zephyrus G15': {'basePrice': 900, 'releaseYear': 2023},
|
| 147 |
+
'VivoBook S15': {'basePrice': 400, 'releaseYear': 2023},
|
| 148 |
+
'TUF Gaming A15': {'basePrice': 500, 'releaseYear': 2023}
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
},
|
| 153 |
+
'tablets': {
|
| 154 |
+
'brands': {
|
| 155 |
+
'Apple': {
|
| 156 |
+
'models': {
|
| 157 |
+
'iPad Pro 12.9" M4': {'basePrice': 800, 'releaseYear': 2024},
|
| 158 |
+
'iPad Pro 11" M4': {'basePrice': 650, 'releaseYear': 2024},
|
| 159 |
+
'iPad Air M2': {'basePrice': 450, 'releaseYear': 2024},
|
| 160 |
+
'iPad Pro 12.9" M2': {'basePrice': 700, 'releaseYear': 2022},
|
| 161 |
+
'iPad Pro 11" M2': {'basePrice': 550, 'releaseYear': 2022},
|
| 162 |
+
'iPad Air M1': {'basePrice': 400, 'releaseYear': 2022},
|
| 163 |
+
'iPad 10th Gen': {'basePrice': 250, 'releaseYear': 2022},
|
| 164 |
+
'iPad 9th Gen': {'basePrice': 200, 'releaseYear': 2021},
|
| 165 |
+
'iPad mini 6': {'basePrice': 350, 'releaseYear': 2021}
|
| 166 |
+
}
|
| 167 |
+
},
|
| 168 |
+
'Samsung': {
|
| 169 |
+
'models': {
|
| 170 |
+
'Galaxy Tab S9 Ultra': {'basePrice': 700, 'releaseYear': 2023},
|
| 171 |
+
'Galaxy Tab S9+': {'basePrice': 550, 'releaseYear': 2023},
|
| 172 |
+
'Galaxy Tab S9': {'basePrice': 450, 'releaseYear': 2023},
|
| 173 |
+
'Galaxy Tab S8 Ultra': {'basePrice': 600, 'releaseYear': 2022},
|
| 174 |
+
'Galaxy Tab S8+': {'basePrice': 450, 'releaseYear': 2022},
|
| 175 |
+
'Galaxy Tab S8': {'basePrice': 350, 'releaseYear': 2022},
|
| 176 |
+
'Galaxy Tab A8': {'basePrice': 150, 'releaseYear': 2022}
|
| 177 |
+
}
|
| 178 |
+
},
|
| 179 |
+
'Microsoft': {
|
| 180 |
+
'models': {
|
| 181 |
+
'Surface Pro 10': {'basePrice': 800, 'releaseYear': 2024},
|
| 182 |
+
'Surface Pro 9': {'basePrice': 650, 'releaseYear': 2022},
|
| 183 |
+
'Surface Pro 8': {'basePrice': 550, 'releaseYear': 2021},
|
| 184 |
+
'Surface Go 4': {'basePrice': 300, 'releaseYear': 2023},
|
| 185 |
+
'Surface Go 3': {'basePrice': 250, 'releaseYear': 2021}
|
| 186 |
+
}
|
| 187 |
+
}
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
condition_multipliers = {
|
| 193 |
+
'Like New': 0.9, 'Excellent': 0.8, 'Good': 0.65,
|
| 194 |
+
'Fair': 0.45, 'Poor': 0.25, 'For Parts': 0.15
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
storage_multipliers = {
|
| 198 |
+
'16GB': 0.7, '32GB': 0.8, '64GB': 0.9, '128GB': 1.0,
|
| 199 |
+
'256GB': 1.15, '512GB': 1.3, '1TB': 1.5, '2TB': 1.8
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
def calculate_price(device_type, brand, model, condition, storage, accessories):
|
| 203 |
+
device = device_database.get(device_type, {}).get('brands', {}).get(brand, {}).get('models', {}).get(model)
|
| 204 |
+
if not device:
|
| 205 |
+
raise ValueError(f"Device not found in database: {brand} {model}")
|
| 206 |
+
|
| 207 |
+
base_price = device['basePrice']
|
| 208 |
+
current_year = datetime.now().year
|
| 209 |
+
device_age = current_year - device['releaseYear']
|
| 210 |
+
|
| 211 |
+
if device_age <= 3:
|
| 212 |
+
age_multiplier = max(0.4, 1 - (device_age * 0.1))
|
| 213 |
+
else:
|
| 214 |
+
age_multiplier = max(0.2, 0.7 - ((device_age - 3) * 0.05))
|
| 215 |
+
|
| 216 |
+
condition_multiplier = condition_multipliers.get(condition, 0.5)
|
| 217 |
+
storage_multiplier = storage_multipliers.get(storage, 1.0)
|
| 218 |
+
|
| 219 |
+
estimated_value = base_price * age_multiplier * condition_multiplier * storage_multiplier
|
| 220 |
+
|
| 221 |
+
accessory_bonus = 0
|
| 222 |
+
if 'Original Box' in accessories: accessory_bonus += estimated_value * 0.05
|
| 223 |
+
if 'Charger' in accessories: accessory_bonus += estimated_value * 0.03
|
| 224 |
+
if 'Cables' in accessories: accessory_bonus += estimated_value * 0.02
|
| 225 |
+
if 'Manual' in accessories: accessory_bonus += estimated_value * 0.01
|
| 226 |
+
if 'Case/Cover' in accessories: accessory_bonus += estimated_value * 0.02
|
| 227 |
+
estimated_value += accessory_bonus
|
| 228 |
+
|
| 229 |
+
return {
|
| 230 |
+
'minPrice': round(estimated_value * 0.85),
|
| 231 |
+
'maxPrice': round(estimated_value * 1.15),
|
| 232 |
+
'estimatedValue': round(estimated_value)
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
def get_full_analysis(form_data):
|
| 236 |
+
price_result = calculate_price(
|
| 237 |
+
form_data.get('deviceType'),
|
| 238 |
+
form_data.get('brand'),
|
| 239 |
+
form_data.get('model'),
|
| 240 |
+
form_data.get('condition'),
|
| 241 |
+
form_data.get('storage', '128GB'),
|
| 242 |
+
form_data.get('accessories', [])
|
| 243 |
+
)
|
| 244 |
+
return {"priceAnalysis": price_result}
|
| 245 |
+
|
| 246 |
+
if __name__ == "__main__":
|
| 247 |
+
try:
|
| 248 |
+
form_data = json.load(sys.stdin)
|
| 249 |
+
analysis_data = get_full_analysis(form_data)
|
| 250 |
+
print(json.dumps(analysis_data))
|
| 251 |
+
except Exception as e:
|
| 252 |
+
print(json.dumps({"error": str(e)}), file=sys.stderr)
|
| 253 |
+
sys.exit(1)
|
web/backend/python/phantom_footprint_analyzer.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# FILE: web/backend/python/phantom_footprint_analyzer.py (UPGRADED with better AI)
|
| 2 |
+
|
| 3 |
+
import sys
|
| 4 |
+
import json
|
| 5 |
+
import random
|
| 6 |
+
|
| 7 |
+
# --- UPGRADED KNOWLEDGE BASE ---
|
| 8 |
+
# We've added more categories and more specific data
|
| 9 |
+
product_category_db = {
|
| 10 |
+
"small_electronics": {
|
| 11 |
+
"return_rate_percent": (20, 35), "packaging_waste_grams": (300, 800),
|
| 12 |
+
"carbon_footprint_kg": (15, 40), "water_usage_liters": (3000, 12000),
|
| 13 |
+
"base_weight_kg": 0.3,
|
| 14 |
+
"recommendations": [
|
| 15 |
+
"Look for brands with strong repairability scores to extend the product's life.",
|
| 16 |
+
"Consider purchasing refurbished electronics to reduce manufacturing demand.",
|
| 17 |
+
"Check for certifications like EPEAT or Energy Star for efficiency."
|
| 18 |
+
]
|
| 19 |
+
},
|
| 20 |
+
"fashion": {
|
| 21 |
+
"return_rate_percent": (25, 45), "packaging_waste_grams": (100, 400),
|
| 22 |
+
"carbon_footprint_kg": (10, 30), "water_usage_liters": (2500, 8000),
|
| 23 |
+
"base_weight_kg": 0.8,
|
| 24 |
+
"recommendations": [
|
| 25 |
+
"High return rates often indicate poor sizing. Check user reviews for sizing accuracy.",
|
| 26 |
+
"Choose natural, sustainably sourced fibers like organic cotton or Tencel.",
|
| 27 |
+
"Wash clothes in cold water to save energy and extend their lifespan."
|
| 28 |
+
]
|
| 29 |
+
},
|
| 30 |
+
"small_appliance": {
|
| 31 |
+
"return_rate_percent": (10, 20), "packaging_waste_grams": (800, 2500),
|
| 32 |
+
"carbon_footprint_kg": (20, 60), "water_usage_liters": (1000, 5000),
|
| 33 |
+
"base_weight_kg": 2.5,
|
| 34 |
+
"recommendations": [
|
| 35 |
+
"Prioritize energy and water efficiency ratings to save on long-term running costs.",
|
| 36 |
+
"Measure your space carefully before ordering to avoid return shipping.",
|
| 37 |
+
"Check if the manufacturer has a take-back program for your old appliance."
|
| 38 |
+
]
|
| 39 |
+
},
|
| 40 |
+
# --- NEW, MORE ACCURATE CATEGORY ---
|
| 41 |
+
"large_appliance": {
|
| 42 |
+
"return_rate_percent": (5, 15), "packaging_waste_grams": (3000, 8000),
|
| 43 |
+
"carbon_footprint_kg": (80, 250), "water_usage_liters": (4000, 10000),
|
| 44 |
+
"base_weight_kg": 8.0, # Much heavier
|
| 45 |
+
"recommendations": [
|
| 46 |
+
"For large appliances, repair is almost always the most eco-friendly option.",
|
| 47 |
+
"Ensure the product has a high energy-efficiency rating as its lifetime energy use is a major impact.",
|
| 48 |
+
"Confirm the retailer will recycle your old appliance upon delivery."
|
| 49 |
+
]
|
| 50 |
+
},
|
| 51 |
+
"default": { # Fallback for unknown items
|
| 52 |
+
"return_rate_percent": (15, 30), "packaging_waste_grams": (200, 600),
|
| 53 |
+
"carbon_footprint_kg": (20, 50), "water_usage_liters": (2000, 6000),
|
| 54 |
+
"base_weight_kg": 1.0,
|
| 55 |
+
"recommendations": ["Consider buying from local retailers to reduce shipping emissions.", "Look for minimal packaging options."]
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
transport_co2_kg_per_km_per_kg = 0.00018
|
| 60 |
+
shipping_distances_km = { "China": 12000, "Vietnam": 13500, "India": 14000, "Colombia": 4500, "USA": 1500, "Germany": 8000 }
|
| 61 |
+
|
| 62 |
+
# --- UPGRADED "AI" - More Keywords, Better Logic ---
|
| 63 |
+
def extract_info_from_url(url):
|
| 64 |
+
"""
|
| 65 |
+
A smarter simulation to identify product, category, and origin from a URL.
|
| 66 |
+
"""
|
| 67 |
+
url_lower = url.lower() # Convert URL to lowercase for easier searching
|
| 68 |
+
|
| 69 |
+
# Check for specific, high-impact items first
|
| 70 |
+
if "vacuum" in url_lower or "navigator" in url_lower or "cleaner" in url_lower:
|
| 71 |
+
return "Vacuum Cleaner", "large_appliance", "China"
|
| 72 |
+
|
| 73 |
+
# Check for other categories
|
| 74 |
+
if "headphone" in url_lower or "speaker" in url_lower or "case" in url_lower or "tracker" in url_lower:
|
| 75 |
+
return "Electronic Accessory", "small_electronics", "China"
|
| 76 |
+
if "shoe" in url_lower or "backpack" in url_lower or "shirt" in url_lower:
|
| 77 |
+
return "Fashion Apparel", "fashion", "Vietnam"
|
| 78 |
+
if "coffee" in url_lower or "lamp" in url_lower or "blender" in url_lower:
|
| 79 |
+
return "Small Home Appliance", "small_appliance", "Germany"
|
| 80 |
+
|
| 81 |
+
# A better default fallback
|
| 82 |
+
return "General Product", "default", "China"
|
| 83 |
+
|
| 84 |
+
# --- The rest of the file (analyze_phantom_footprint and the main block) remains the same ---
|
| 85 |
+
def analyze_phantom_footprint(url):
|
| 86 |
+
product_name, category, origin_country = extract_info_from_url(url)
|
| 87 |
+
category_data = product_category_db[category]
|
| 88 |
+
distance = shipping_distances_km.get(origin_country, 8000)
|
| 89 |
+
transport_co2 = distance * category_data['base_weight_kg'] * transport_co2_kg_per_km_per_kg
|
| 90 |
+
manufacturing_co2 = random.randint(*category_data["carbon_footprint_kg"])
|
| 91 |
+
total_co2_footprint = manufacturing_co2 + transport_co2
|
| 92 |
+
hidden_impacts = {
|
| 93 |
+
"returnRate": random.randint(*category_data["return_rate_percent"]),
|
| 94 |
+
"packagingWaste": random.randint(*category_data["packaging_waste_grams"]),
|
| 95 |
+
"carbonFootprint": manufacturing_co2,
|
| 96 |
+
"waterUsage": random.randint(*category_data["water_usage_liters"])
|
| 97 |
+
}
|
| 98 |
+
score = (hidden_impacts["returnRate"] * 0.5 + total_co2_footprint * 0.5 + hidden_impacts["packagingWaste"] / 100)
|
| 99 |
+
report = {
|
| 100 |
+
"productName": product_name, "originCountry": origin_country,
|
| 101 |
+
"impactScore": min(99, int(score)),
|
| 102 |
+
"phantomFootprint": {
|
| 103 |
+
"totalCO2EquivalentKg": round(total_co2_footprint, 2),
|
| 104 |
+
"breakdown": {"manufacturingCO2Kg": manufacturing_co2, "transportCO2Kg": round(transport_co2, 2)},
|
| 105 |
+
"hiddenWaterUsageLiters": hidden_impacts['waterUsage'],
|
| 106 |
+
"productionWasteKg": round(hidden_impacts['packagingWaste'] / 1000, 2)
|
| 107 |
+
},
|
| 108 |
+
"insights": category_data["recommendations"]
|
| 109 |
+
}
|
| 110 |
+
return report
|
| 111 |
+
|
| 112 |
+
if __name__ == "__main__":
|
| 113 |
+
try:
|
| 114 |
+
input_data = json.load(sys.stdin)
|
| 115 |
+
url = input_data.get('url')
|
| 116 |
+
if not url: raise ValueError("Missing product URL.")
|
| 117 |
+
analysis_report = analyze_phantom_footprint(url)
|
| 118 |
+
print(json.dumps(analysis_report))
|
| 119 |
+
except Exception as e:
|
| 120 |
+
print(json.dumps({"error": str(e)}), file=sys.stderr)
|
| 121 |
+
sys.exit(1)
|
web/backend/python/requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
opencv-python==4.8.1.78
|
| 2 |
+
numpy==1.24.3
|
| 3 |
+
Pillow==10.0.1
|
| 4 |
+
Flask==2.3.3
|
| 5 |
+
Flask-CORS==4.0.0
|
| 6 |
+
scikit-image==0.21.0
|
| 7 |
+
scipy==1.11.3
|
| 8 |
+
biopython==1.83
|
| 9 |
+
librosa==0.10.1
|
web/backend/python/water_analysis.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import json
|
| 3 |
+
import cv2
|
| 4 |
+
import numpy as np
|
| 5 |
+
from skimage import color as skimage_color
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
class WaterQualityAnalyzer:
|
| 9 |
+
def __init__(self):
|
| 10 |
+
self.lab_calibration = {
|
| 11 |
+
'ph': [
|
| 12 |
+
((54, 81, 69), 4.0),
|
| 13 |
+
((63, 60, 59), 5.0),
|
| 14 |
+
((75, 24, 79), 6.0),
|
| 15 |
+
((88, -8, 86), 6.5),
|
| 16 |
+
((97, -15, 94), 7.0),
|
| 17 |
+
((91, -26, 85), 7.5),
|
| 18 |
+
((88, -76, 81), 8.0),
|
| 19 |
+
((91, -48, -14), 8.5),
|
| 20 |
+
((54, 57, -100), 9.0),
|
| 21 |
+
],
|
| 22 |
+
'chlorine': [
|
| 23 |
+
((100, 0, 0), 0.0),
|
| 24 |
+
((97, 5, 2), 0.5),
|
| 25 |
+
((91, 15, 5), 1.0),
|
| 26 |
+
((76, 34, 4), 2.0),
|
| 27 |
+
((60, 49, -4), 3.0),
|
| 28 |
+
((54, 69, 36), 4.0),
|
| 29 |
+
],
|
| 30 |
+
'nitrates': [
|
| 31 |
+
((100, 0, 0), 0),
|
| 32 |
+
((97, 6, 4), 5),
|
| 33 |
+
((92, 14, 6), 10),
|
| 34 |
+
((76, 33, 5), 25),
|
| 35 |
+
((63, 60, 58), 50),
|
| 36 |
+
],
|
| 37 |
+
'hardness': [
|
| 38 |
+
((100, 0, 0), 0),
|
| 39 |
+
((98, -3, 3), 50),
|
| 40 |
+
((91, -21, 20), 100),
|
| 41 |
+
((88, -76, 81), 150),
|
| 42 |
+
((54, -39, 36), 200),
|
| 43 |
+
((46, -51, 49), 300),
|
| 44 |
+
],
|
| 45 |
+
'alkalinity': [
|
| 46 |
+
((100, 0, 0), 0),
|
| 47 |
+
((98, -9, 0), 40),
|
| 48 |
+
((91, -16, -11), 80),
|
| 49 |
+
((91, -48, -14), 120),
|
| 50 |
+
((87, -42, -15), 160),
|
| 51 |
+
((60, -29, -29), 240),
|
| 52 |
+
],
|
| 53 |
+
'bacteria': [
|
| 54 |
+
((100, 0, 0), 0),
|
| 55 |
+
((97, -1, 12), 0.5),
|
| 56 |
+
((97, -15, 94), 1),
|
| 57 |
+
],
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _color_distance_lab(self, lab1, lab2):
|
| 63 |
+
return np.sqrt(np.sum((np.array(lab1) - np.array(lab2)) ** 2))
|
| 64 |
+
|
| 65 |
+
def analyze_parameter(self, avg_lab_color, parameter):
|
| 66 |
+
if parameter not in self.lab_calibration: return 0, 0
|
| 67 |
+
calibration_data = self.lab_calibration[parameter]
|
| 68 |
+
if not calibration_data: return 0, 0
|
| 69 |
+
|
| 70 |
+
distances = [(self._color_distance_lab(avg_lab_color, cal_lab), value) for cal_lab, value in calibration_data]
|
| 71 |
+
distances.sort()
|
| 72 |
+
|
| 73 |
+
closest_dist, best_value = distances[0]
|
| 74 |
+
confidence = max(0, 100 - (closest_dist * 2.5))
|
| 75 |
+
|
| 76 |
+
if len(distances) >= 2:
|
| 77 |
+
d1, v1 = distances[0]
|
| 78 |
+
d2, v2 = distances[1]
|
| 79 |
+
if (d1 + d2) == 0: return v1, confidence
|
| 80 |
+
weight1 = d2 / (d1 + d2)
|
| 81 |
+
weight2 = d1 / (d1 + d2)
|
| 82 |
+
interpolated_value = v1 * weight1 + v2 * weight2
|
| 83 |
+
return interpolated_value, confidence
|
| 84 |
+
|
| 85 |
+
return best_value, confidence
|
| 86 |
+
|
| 87 |
+
def analyze_water_quality(self, image_path, water_source='unknown'):
|
| 88 |
+
image = cv2.imread(image_path)
|
| 89 |
+
if image is None: raise ValueError(f"Could not load image: {image_path}")
|
| 90 |
+
|
| 91 |
+
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
| 92 |
+
height = image_rgb.shape[0]
|
| 93 |
+
pad_height = height // 6
|
| 94 |
+
regions_of_interest = [image_rgb[i * pad_height:(i + 1) * pad_height, :] for i in range(6)]
|
| 95 |
+
|
| 96 |
+
parameter_names = ['ph', 'chlorine', 'nitrates', 'hardness', 'alkalinity', 'bacteria']
|
| 97 |
+
results, confidences = {}, {}
|
| 98 |
+
|
| 99 |
+
for i, param in enumerate(parameter_names):
|
| 100 |
+
roi_rgb = regions_of_interest[i]
|
| 101 |
+
roi_lab = skimage_color.rgb2lab(roi_rgb)
|
| 102 |
+
avg_lab_color = np.mean(roi_lab.reshape(-1, 3), axis=0)
|
| 103 |
+
value, confidence = self.analyze_parameter(avg_lab_color, param)
|
| 104 |
+
results[param] = value
|
| 105 |
+
confidences[param] = round(confidence)
|
| 106 |
+
|
| 107 |
+
overall_confidence = round(np.mean(list(confidences.values())))
|
| 108 |
+
|
| 109 |
+
return {
|
| 110 |
+
"ph": results.get('ph', 0), "chlorine": results.get('chlorine', 0),
|
| 111 |
+
"nitrates": results.get('nitrates', 0), "hardness": results.get('hardness', 0),
|
| 112 |
+
"alkalinity": results.get('alkalinity', 0), "bacteria": results.get('bacteria', 0),
|
| 113 |
+
"confidence": overall_confidence, "individualConfidences": confidences,
|
| 114 |
+
"processingMethod": "Python CV (LAB Space)",
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
def main():
|
| 118 |
+
if len(sys.argv) < 2:
|
| 119 |
+
print(json.dumps({"error": "No image path provided"}), file=sys.stderr)
|
| 120 |
+
sys.exit(1)
|
| 121 |
+
|
| 122 |
+
image_path = sys.argv[1]
|
| 123 |
+
water_source = sys.argv[2] if len(sys.argv) > 2 else 'unknown'
|
| 124 |
+
|
| 125 |
+
if not os.path.exists(image_path):
|
| 126 |
+
print(json.dumps({"error": f"Image file not found: {image_path}"}), file=sys.stderr)
|
| 127 |
+
sys.exit(1)
|
| 128 |
+
|
| 129 |
+
try:
|
| 130 |
+
analyzer = WaterQualityAnalyzer()
|
| 131 |
+
results = analyzer.analyze_water_quality(image_path, water_source)
|
| 132 |
+
print(json.dumps(results, indent=4))
|
| 133 |
+
except Exception as e:
|
| 134 |
+
print(json.dumps({"error": str(e)}), file=sys.stderr)
|
| 135 |
+
sys.exit(1)
|
| 136 |
+
|
| 137 |
+
if __name__ == "__main__":
|
| 138 |
+
main()
|
web/backend/results/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
web/backend/server.js
ADDED
|
File without changes
|
web/backend/uploads/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
web/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "greenplus-by-gxs-web",
|
| 3 |
+
"version": "2.0.0",
|
| 4 |
+
"description": "GreenPlus by GXS - Environmental Intelligence Hub with Enhanced UI/UX",
|
| 5 |
+
"private": true,
|
| 6 |
+
"dependencies": {
|
| 7 |
+
"idb": "^8.0.3",
|
| 8 |
+
"lucide-react": "^0.539.0",
|
| 9 |
+
"react": "^18.2.0",
|
| 10 |
+
"react-dom": "^18.2.0",
|
| 11 |
+
"react-scripts": "5.0.1",
|
| 12 |
+
"tone": "^15.1.22",
|
| 13 |
+
"web-vitals": "^3.3.2"
|
| 14 |
+
},
|
| 15 |
+
"devDependencies": {
|
| 16 |
+
"@testing-library/jest-dom": "^5.16.5",
|
| 17 |
+
"@testing-library/react": "^13.4.0",
|
| 18 |
+
"@testing-library/user-event": "^14.4.3"
|
| 19 |
+
},
|
| 20 |
+
"scripts": {
|
| 21 |
+
"start": "react-scripts start",
|
| 22 |
+
"build": "GENERATE_SOURCEMAP=false WASM=0 react-scripts build",
|
| 23 |
+
"test": "react-scripts test",
|
| 24 |
+
"eject": "react-scripts eject"
|
| 25 |
+
},
|
| 26 |
+
"eslintConfig": {
|
| 27 |
+
"extends": [
|
| 28 |
+
"react-app"
|
| 29 |
+
]
|
| 30 |
+
},
|
| 31 |
+
"browserslist": {
|
| 32 |
+
"production": [
|
| 33 |
+
">0.2%",
|
| 34 |
+
"not dead",
|
| 35 |
+
"not op_mini all"
|
| 36 |
+
],
|
| 37 |
+
"development": [
|
| 38 |
+
"last 1 chrome version",
|
| 39 |
+
"last 1 firefox version",
|
| 40 |
+
"last 1 safari version"
|
| 41 |
+
]
|
| 42 |
+
}
|
| 43 |
+
}
|
web/public/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<meta name="theme-color" content="#2E7D32" />
|
| 7 |
+
<meta name="description" content="GreenPlus by GXS - Your environmental companion for a sustainable future" />
|
| 8 |
+
<title>GreenPlus by GXS - Save the Planet</title>
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<div id="root"></div>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
web/public/manifest.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"short_name": "GreenPlus by GXS",
|
| 3 |
+
"name": "GreenPlus by GXS - Environmental Action Platform",
|
| 4 |
+
"icons": [
|
| 5 |
+
{
|
| 6 |
+
"src": "favicon.ico",
|
| 7 |
+
"sizes": "64x64 32x32 24x24 16x16",
|
| 8 |
+
"type": "image/x-icon"
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"src": "logo192.png",
|
| 12 |
+
"type": "image/png",
|
| 13 |
+
"sizes": "192x192"
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"src": "logo512.png",
|
| 17 |
+
"type": "image/png",
|
| 18 |
+
"sizes": "512x512"
|
| 19 |
+
}
|
| 20 |
+
],
|
| 21 |
+
"start_url": ".",
|
| 22 |
+
"display": "standalone",
|
| 23 |
+
"theme_color": "#2E7D32",
|
| 24 |
+
"background_color": "#F5F5F5"
|
| 25 |
+
}
|
web/public/sounds/bird-call.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f080cd3ef7eeeb5af924169721554d0dbfb05faf5019ba90b019fdc60643d56f
|
| 3 |
+
size 6815242
|
web/public/sounds/forest-ambience.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:56f7a425f4d45356641586642a9f51ba6bba4b46208dd4ff5ed76e915b696fef
|
| 3 |
+
size 2413440
|
web/public/sounds/industrial-hum.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0efc2ee25714b10ce986fb54c48fc571d16d5ae5c631caf185e00daf72b229ab
|
| 3 |
+
size 632160
|
web/public/sounds/river.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:11ecd37d5a586689f387c83d91ea362bbc4207ac7e68d5457c9c0cfdef09e32a
|
| 3 |
+
size 3843552
|
web/src/App.css
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Grid Layouts for Dashboard */
|
| 2 |
+
.grid {
|
| 3 |
+
display: grid;
|
| 4 |
+
gap: 20px;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
.grid-3 {
|
| 8 |
+
display: grid;
|
| 9 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
| 10 |
+
gap: 20px;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
.grid-4 {
|
| 14 |
+
display: grid;
|
| 15 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 16 |
+
gap: 20px;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
/* Card Component */
|
| 20 |
+
.card {
|
| 21 |
+
background: white;
|
| 22 |
+
border-radius: 12px;
|
| 23 |
+
padding: 24px;
|
| 24 |
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
| 25 |
+
border: 1px solid rgba(0, 0, 0, 0.1);
|
| 26 |
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
| 27 |
+
display: flex;
|
| 28 |
+
flex-direction: column;
|
| 29 |
+
height: 100%;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
.card:hover {
|
| 33 |
+
transform: translateY(-2px);
|
| 34 |
+
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/* Card content that grows to push button to bottom */
|
| 38 |
+
.card-content {
|
| 39 |
+
flex: 1;
|
| 40 |
+
display: flex;
|
| 41 |
+
flex-direction: column;
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.card-description {
|
| 45 |
+
flex: 1;
|
| 46 |
+
margin-bottom: 20px;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.card-button {
|
| 50 |
+
margin-top: auto;
|
| 51 |
+
transition: all 0.2s ease;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.card-button:hover {
|
| 55 |
+
transform: translateY(-1px);
|
| 56 |
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
/* Responsive adjustments */
|
| 60 |
+
@media (max-width: 768px) {
|
| 61 |
+
.grid-3, .grid-4 {
|
| 62 |
+
grid-template-columns: 1fr;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.card {
|
| 66 |
+
padding: 16px;
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
/* Main App Container */
|
| 71 |
+
.terra-app {
|
| 72 |
+
display: flex;
|
| 73 |
+
height: 100vh;
|
| 74 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/* Sidebar */
|
| 78 |
+
.terra-sidebar {
|
| 79 |
+
background: rgba(255, 255, 255, 0.95);
|
| 80 |
+
backdrop-filter: blur(10px);
|
| 81 |
+
color: #333;
|
| 82 |
+
transition: width 0.3s ease;
|
| 83 |
+
display: flex;
|
| 84 |
+
flex-direction: column;
|
| 85 |
+
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
|
| 86 |
+
position: relative;
|
| 87 |
+
z-index: 100;
|
| 88 |
+
border-right: 1px solid rgba(0, 0, 0, 0.1);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
.terra-sidebar.open {
|
| 92 |
+
width: 280px;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.terra-sidebar.closed {
|
| 96 |
+
width: 70px;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
/* Logo Section */
|
| 100 |
+
.terra-logo {
|
| 101 |
+
padding: 24px 20px;
|
| 102 |
+
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
| 103 |
+
display: flex;
|
| 104 |
+
align-items: center;
|
| 105 |
+
min-height: 90px;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.logo-container {
|
| 109 |
+
display: flex;
|
| 110 |
+
align-items: center;
|
| 111 |
+
gap: 16px;
|
| 112 |
+
flex: 1;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.logo-icon {
|
| 116 |
+
font-size: 2.5rem;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.logo-text h1 {
|
| 120 |
+
font-size: 1.6rem;
|
| 121 |
+
font-weight: 700;
|
| 122 |
+
margin-bottom: 4px;
|
| 123 |
+
color: #2E7D32;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.logo-text p {
|
| 127 |
+
font-size: 0.8rem;
|
| 128 |
+
color: #666;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
/* Navigation */
|
| 132 |
+
.terra-nav {
|
| 133 |
+
flex: 1;
|
| 134 |
+
padding: 16px 0;
|
| 135 |
+
overflow-y: auto;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.terra-nav::-webkit-scrollbar {
|
| 139 |
+
width: 4px;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.terra-nav::-webkit-scrollbar-track {
|
| 143 |
+
background: transparent;
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.terra-nav::-webkit-scrollbar-thumb {
|
| 147 |
+
background: rgba(0, 0, 0, 0.2);
|
| 148 |
+
border-radius: 2px;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.nav-item {
|
| 152 |
+
display: flex;
|
| 153 |
+
align-items: center;
|
| 154 |
+
padding: 14px 20px;
|
| 155 |
+
margin: 3px 12px;
|
| 156 |
+
border-radius: 8px;
|
| 157 |
+
cursor: pointer;
|
| 158 |
+
transition: all 0.3s ease;
|
| 159 |
+
position: relative;
|
| 160 |
+
color: #333;
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.nav-item:hover {
|
| 164 |
+
background: rgba(46, 125, 50, 0.1);
|
| 165 |
+
transform: translateX(2px);
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.nav-item.active {
|
| 169 |
+
background: rgba(46, 125, 50, 0.15);
|
| 170 |
+
border-left: 3px solid #2E7D32;
|
| 171 |
+
color: #2E7D32;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.nav-icon {
|
| 175 |
+
font-size: 1.5rem;
|
| 176 |
+
margin-right: 14px;
|
| 177 |
+
min-width: 28px;
|
| 178 |
+
text-align: center;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.nav-content {
|
| 182 |
+
flex: 1;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.nav-name {
|
| 186 |
+
font-weight: 600;
|
| 187 |
+
font-size: 0.95rem;
|
| 188 |
+
display: block;
|
| 189 |
+
margin-bottom: 3px;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.nav-desc {
|
| 193 |
+
font-size: 0.75rem;
|
| 194 |
+
opacity: 0.7;
|
| 195 |
+
color: #666;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
/* Sidebar Footer */
|
| 199 |
+
.sidebar-footer {
|
| 200 |
+
padding: 20px;
|
| 201 |
+
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.footer-stats {
|
| 205 |
+
display: flex;
|
| 206 |
+
gap: 16px;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
.stat {
|
| 210 |
+
display: flex;
|
| 211 |
+
align-items: center;
|
| 212 |
+
gap: 10px;
|
| 213 |
+
flex: 1;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
.stat-icon {
|
| 217 |
+
font-size: 1.3rem;
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
.stat-value {
|
| 221 |
+
font-weight: 700;
|
| 222 |
+
font-size: 0.95rem;
|
| 223 |
+
color: #2E7D32;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
.stat-label {
|
| 227 |
+
font-size: 0.7rem;
|
| 228 |
+
color: #666;
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
/* Main Content */
|
| 232 |
+
.terra-main {
|
| 233 |
+
flex: 1;
|
| 234 |
+
display: flex;
|
| 235 |
+
flex-direction: column;
|
| 236 |
+
background: linear-gradient(135deg, #f8fdf8 0%, #f0f9f0 100%);
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
/* Header */
|
| 240 |
+
.terra-header {
|
| 241 |
+
background: rgba(255, 255, 255, 0.95);
|
| 242 |
+
backdrop-filter: blur(10px);
|
| 243 |
+
border-bottom: 1px solid rgba(76, 175, 80, 0.1);
|
| 244 |
+
padding: 24px 32px;
|
| 245 |
+
display: flex;
|
| 246 |
+
justify-content: space-between;
|
| 247 |
+
align-items: center;
|
| 248 |
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
.header-left {
|
| 252 |
+
flex: 1;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
.page-title {
|
| 256 |
+
display: flex;
|
| 257 |
+
align-items: center;
|
| 258 |
+
gap: 16px;
|
| 259 |
+
margin: 0 0 8px 0;
|
| 260 |
+
color: #2d5016;
|
| 261 |
+
font-size: 2rem;
|
| 262 |
+
font-weight: 700;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
.title-icon {
|
| 266 |
+
font-size: 2.2rem;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
.page-subtitle {
|
| 270 |
+
margin: 0;
|
| 271 |
+
color: #4a7c23;
|
| 272 |
+
font-size: 1rem;
|
| 273 |
+
font-weight: 500;
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
.header-right {
|
| 277 |
+
display: flex;
|
| 278 |
+
align-items: center;
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
.header-stats {
|
| 282 |
+
display: flex;
|
| 283 |
+
gap: 20px;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
.header-stat {
|
| 287 |
+
display: flex;
|
| 288 |
+
align-items: center;
|
| 289 |
+
gap: 12px;
|
| 290 |
+
background: rgba(76, 175, 80, 0.1);
|
| 291 |
+
padding: 16px 20px;
|
| 292 |
+
border-radius: 12px;
|
| 293 |
+
border: 1px solid rgba(76, 175, 80, 0.2);
|
| 294 |
+
transition: all 0.3s ease;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
.header-stat:hover {
|
| 298 |
+
background: rgba(76, 175, 80, 0.15);
|
| 299 |
+
transform: translateY(-2px);
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
.header-stat span {
|
| 303 |
+
font-size: 1.6rem;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
.header-stat > div > div:first-child {
|
| 307 |
+
font-weight: 700;
|
| 308 |
+
font-size: 1.2rem;
|
| 309 |
+
color: #2d5016;
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
.header-stat > div > div:last-child {
|
| 313 |
+
font-size: 0.8rem;
|
| 314 |
+
color: #4a7c23;
|
| 315 |
+
font-weight: 500;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
/* Content Area */
|
| 319 |
+
.terra-content {
|
| 320 |
+
flex: 1;
|
| 321 |
+
padding: 32px;
|
| 322 |
+
overflow-y: auto;
|
| 323 |
+
background: transparent;
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
.terra-content::-webkit-scrollbar {
|
| 327 |
+
width: 8px;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
.terra-content::-webkit-scrollbar-track {
|
| 331 |
+
background: rgba(76, 175, 80, 0.1);
|
| 332 |
+
border-radius: 4px;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
.terra-content::-webkit-scrollbar-thumb {
|
| 336 |
+
background: rgba(76, 175, 80, 0.3);
|
| 337 |
+
border-radius: 4px;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
.terra-content::-webkit-scrollbar-thumb:hover {
|
| 341 |
+
background: rgba(76, 175, 80, 0.5);
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
/* Page Transition Animation */
|
| 345 |
+
.terra-content > * {
|
| 346 |
+
animation: fadeInUp 0.5s ease-out;
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
@keyframes fadeInUp {
|
| 350 |
+
from {
|
| 351 |
+
opacity: 0;
|
| 352 |
+
transform: translateY(20px);
|
| 353 |
+
}
|
| 354 |
+
to {
|
| 355 |
+
opacity: 1;
|
| 356 |
+
transform: translateY(0);
|
| 357 |
+
}
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
/* Responsive Design */
|
| 361 |
+
@media (max-width: 1024px) {
|
| 362 |
+
.terra-sidebar.open {
|
| 363 |
+
width: 260px;
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
.header-stats {
|
| 367 |
+
gap: 16px;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
.header-stat {
|
| 371 |
+
padding: 12px 16px;
|
| 372 |
+
}
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
@media (max-width: 768px) {
|
| 376 |
+
.terra-sidebar {
|
| 377 |
+
position: fixed;
|
| 378 |
+
left: 0;
|
| 379 |
+
top: 0;
|
| 380 |
+
height: 100vh;
|
| 381 |
+
z-index: 1000;
|
| 382 |
+
transform: translateX(-100%);
|
| 383 |
+
transition: transform 0.3s ease;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
.terra-sidebar.open {
|
| 387 |
+
transform: translateX(0);
|
| 388 |
+
width: 280px;
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
.terra-sidebar.closed {
|
| 392 |
+
transform: translateX(-100%);
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
.terra-header {
|
| 396 |
+
padding: 20px 24px;
|
| 397 |
+
flex-direction: column;
|
| 398 |
+
align-items: flex-start;
|
| 399 |
+
gap: 16px;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.header-stats {
|
| 403 |
+
width: 100%;
|
| 404 |
+
justify-content: space-between;
|
| 405 |
+
flex-wrap: wrap;
|
| 406 |
+
gap: 12px;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
.header-stat {
|
| 410 |
+
flex: 1;
|
| 411 |
+
min-width: 140px;
|
| 412 |
+
padding: 12px 16px;
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
.terra-content {
|
| 416 |
+
padding: 24px 20px;
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
.page-title {
|
| 420 |
+
font-size: 1.6rem;
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
.title-icon {
|
| 424 |
+
font-size: 1.8rem;
|
| 425 |
+
}
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
@media (max-width: 480px) {
|
| 429 |
+
.terra-logo {
|
| 430 |
+
padding: 20px 16px;
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
.logo-text h1 {
|
| 434 |
+
font-size: 1.3rem;
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
.nav-item {
|
| 438 |
+
padding: 12px 16px;
|
| 439 |
+
margin: 2px 8px;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
.nav-name {
|
| 443 |
+
font-size: 0.9rem;
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
.nav-desc {
|
| 447 |
+
font-size: 0.7rem;
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
.header-stats {
|
| 451 |
+
flex-direction: column;
|
| 452 |
+
width: 100%;
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
.header-stat {
|
| 456 |
+
width: 100%;
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
.terra-content {
|
| 460 |
+
padding: 20px 16px;
|
| 461 |
+
}
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
/* Focus States for Accessibility */
|
| 465 |
+
.nav-item:focus,
|
| 466 |
+
.sidebar-toggle:focus,
|
| 467 |
+
.header-stat:focus {
|
| 468 |
+
outline: 2px solid #a8e6a3;
|
| 469 |
+
outline-offset: 2px;
|
| 470 |
+
}
|
| 471 |
+
|
| 472 |
+
/* High Contrast Mode */
|
| 473 |
+
@media (prefers-contrast: high) {
|
| 474 |
+
.terra-sidebar {
|
| 475 |
+
background: #2d5016;
|
| 476 |
+
}
|
| 477 |
+
|
| 478 |
+
.nav-item.active {
|
| 479 |
+
background: rgba(255, 255, 255, 0.3);
|
| 480 |
+
}
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
/* Reduced Motion */
|
| 484 |
+
@media (prefers-reduced-motion: reduce) {
|
| 485 |
+
* {
|
| 486 |
+
animation-duration: 0.01ms !important;
|
| 487 |
+
animation-iteration-count: 1 !important;
|
| 488 |
+
transition-duration: 0.01ms !important;
|
| 489 |
+
}
|
| 490 |
+
}
|
| 491 |
+
/*
|
| 492 |
+
Global Button Styles */
|
| 493 |
+
button {
|
| 494 |
+
transition: all 0.3s ease;
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
button:hover:not(:disabled) {
|
| 498 |
+
transform: translateY(-2px);
|
| 499 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
button:active:not(:disabled) {
|
| 503 |
+
transform: translateY(0);
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
button:disabled {
|
| 507 |
+
opacity: 0.6;
|
| 508 |
+
cursor: not-allowed !important;
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
/* Ensure all buttons have proper cursor */
|
| 512 |
+
button:not(:disabled) {
|
| 513 |
+
cursor: pointer;
|
| 514 |
+
}
|
web/src/App.js
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from 'react';
|
| 2 |
+
import './App.css';
|
| 3 |
+
import { systemInitializer } from './utils/systemInitializer.js';
|
| 4 |
+
import { authManager } from './utils/auth.js';
|
| 5 |
+
import LoadingScreen from './components/LoadingScreen';
|
| 6 |
+
|
| 7 |
+
// Import all your pages
|
| 8 |
+
import Dashboard from './pages/Dashboard';
|
| 9 |
+
import AquaLens from './pages/AquaLens';
|
| 10 |
+
import BiodiversityEar from './pages/BiodiversityEar';
|
| 11 |
+
import CarbonOptimizer from './pages/CarbonOptimizer';
|
| 12 |
+
import SmartFarming from './pages/SmartFarming';
|
| 13 |
+
import EWasteRecycling from './pages/EWasteRecycling';
|
| 14 |
+
import AirQuality from './pages/AirQuality';
|
| 15 |
+
import FloraShield from './pages/FloraShield';
|
| 16 |
+
import FoodWasteReduction from './pages/FoodWasteReduction';
|
| 17 |
+
import EnvironmentalJustice from './pages/EnvironmentalJustice';
|
| 18 |
+
import EcoSonification from './pages/EcoSonification';
|
| 19 |
+
import PhantomFootprint from './pages/PhantomFootprint';
|
| 20 |
+
import UpcyclingAgent from './pages/UpcyclingAgent';
|
| 21 |
+
import PackagingDesigner from './pages/PackagingDesigner';
|
| 22 |
+
import Impact from './pages/Impact';
|
| 23 |
+
import EcoTasks from './pages/EcoTasks';
|
| 24 |
+
import Learn from './pages/Learn';
|
| 25 |
+
import Startups from './pages/Startups';
|
| 26 |
+
import Community from './pages/Community';
|
| 27 |
+
import Profile from './pages/Profile';
|
| 28 |
+
import Login from './pages/Login';
|
| 29 |
+
import DigitalQuarry from './pages/DigitalQuarry';
|
| 30 |
+
import BioStreamAI from './pages/BioStreamAI';
|
| 31 |
+
import EWasteProspector from './pages/EWasteProspector';
|
| 32 |
+
import GeneticResilience from './pages/GeneticResilience';
|
| 33 |
+
|
| 34 |
+
function App() {
|
| 35 |
+
const [activePage, setActivePage] = useState('Dashboard');
|
| 36 |
+
const [userStats, setUserStats] = useState({
|
| 37 |
+
carbonSaved: 0,
|
| 38 |
+
waterTests: 0,
|
| 39 |
+
biodiversityScans: 0,
|
| 40 |
+
treesPlanted: 0,
|
| 41 |
+
wasteReduced: 0,
|
| 42 |
+
energySaved: 0
|
| 43 |
+
});
|
| 44 |
+
const [currentUser, setCurrentUser] = useState(null);
|
| 45 |
+
const [systemReady, setSystemReady] = useState(false);
|
| 46 |
+
const [systemStatus, setSystemStatus] = useState(null);
|
| 47 |
+
const [isLoading, setIsLoading] = useState(true);
|
| 48 |
+
|
| 49 |
+
// Initialize advanced systems and authentication on app start
|
| 50 |
+
useEffect(() => {
|
| 51 |
+
const initializeSystems = async () => {
|
| 52 |
+
console.log('🚀 GreenPlus by GXS: Initializing Advanced Environmental Systems...');
|
| 53 |
+
|
| 54 |
+
try {
|
| 55 |
+
const status = await systemInitializer.initialize();
|
| 56 |
+
setSystemStatus(status);
|
| 57 |
+
setSystemReady(status.overallReady);
|
| 58 |
+
|
| 59 |
+
// Initialize authentication
|
| 60 |
+
const user = authManager.getCurrentUser();
|
| 61 |
+
setCurrentUser(user);
|
| 62 |
+
|
| 63 |
+
if (status.overallReady) {
|
| 64 |
+
console.log('✅ GreenPlus by GXS: All systems operational!');
|
| 65 |
+
} else {
|
| 66 |
+
console.warn('⚠️ GreenPlus by GXS: Running with limited functionality');
|
| 67 |
+
}
|
| 68 |
+
} catch (error) {
|
| 69 |
+
console.error('❌ GreenPlus by GXS: System initialization failed:', error);
|
| 70 |
+
setSystemReady(false);
|
| 71 |
+
}
|
| 72 |
+
};
|
| 73 |
+
|
| 74 |
+
initializeSystems();
|
| 75 |
+
|
| 76 |
+
// Show loading screen for 2.5 seconds
|
| 77 |
+
const loadingTimer = setTimeout(() => {
|
| 78 |
+
setIsLoading(false);
|
| 79 |
+
}, 2500);
|
| 80 |
+
|
| 81 |
+
return () => clearTimeout(loadingTimer);
|
| 82 |
+
}, []);
|
| 83 |
+
|
| 84 |
+
// Handle authentication changes
|
| 85 |
+
const handleAuthChange = (user) => {
|
| 86 |
+
setCurrentUser(user);
|
| 87 |
+
updateUserStats();
|
| 88 |
+
};
|
| 89 |
+
|
| 90 |
+
// Update user stats from auth manager
|
| 91 |
+
const updateUserStats = () => {
|
| 92 |
+
const stats = authManager.getUserStats();
|
| 93 |
+
|
| 94 |
+
// Only show demo stats for actual guest users, not logged-in users
|
| 95 |
+
const isGuest = currentUser?.isGuest === true;
|
| 96 |
+
const isNewGuest = isGuest && !stats.carbonSaved && !stats.waterTests && !stats.biodiversityScans;
|
| 97 |
+
|
| 98 |
+
console.log('🔄 App.js updateUserStats called');
|
| 99 |
+
console.log('📊 Raw stats from authManager:', stats);
|
| 100 |
+
console.log('👤 Current user:', currentUser);
|
| 101 |
+
console.log('🆕 Is new guest:', isNewGuest);
|
| 102 |
+
|
| 103 |
+
const newStats = {
|
| 104 |
+
carbonSaved: stats.carbonSaved || (isNewGuest ? 12 : 0),
|
| 105 |
+
waterTests: stats.waterTests || (isNewGuest ? 3 : 0),
|
| 106 |
+
biodiversityScans: stats.biodiversityScans || (isNewGuest ? 2 : 0),
|
| 107 |
+
treesPlanted: stats.treesPlanted || 0,
|
| 108 |
+
wasteReduced: stats.wasteReduced || 0,
|
| 109 |
+
energySaved: stats.energySaved || 0
|
| 110 |
+
};
|
| 111 |
+
|
| 112 |
+
console.log('📈 Setting userStats to:', newStats);
|
| 113 |
+
setUserStats(newStats);
|
| 114 |
+
};
|
| 115 |
+
|
| 116 |
+
// Set up periodic stats refresh
|
| 117 |
+
useEffect(() => {
|
| 118 |
+
// Initial stats load
|
| 119 |
+
updateUserStats();
|
| 120 |
+
|
| 121 |
+
// Refresh stats every 10 seconds (less frequent to reduce flickering)
|
| 122 |
+
const statsInterval = setInterval(updateUserStats, 10000);
|
| 123 |
+
|
| 124 |
+
return () => clearInterval(statsInterval);
|
| 125 |
+
}, [currentUser]);
|
| 126 |
+
|
| 127 |
+
const pages = [
|
| 128 |
+
{
|
| 129 |
+
id: 'Dashboard',
|
| 130 |
+
name: 'Dashboard',
|
| 131 |
+
icon: '🏠',
|
| 132 |
+
component: Dashboard,
|
| 133 |
+
description: 'Overview & Analytics'
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
id: 'AquaLens',
|
| 137 |
+
name: 'AquaLens',
|
| 138 |
+
icon: '💧',
|
| 139 |
+
component: AquaLens,
|
| 140 |
+
description: 'Water Quality Analysis'
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
id: 'BiodiversityEar',
|
| 144 |
+
name: 'BiodiversityEar',
|
| 145 |
+
icon: '🦜',
|
| 146 |
+
component: BiodiversityEar,
|
| 147 |
+
description: 'Ecosystem Monitoring'
|
| 148 |
+
},
|
| 149 |
+
{
|
| 150 |
+
id: 'CarbonOptimizer',
|
| 151 |
+
name: 'Carbon Optimizer',
|
| 152 |
+
icon: '🌱',
|
| 153 |
+
component: CarbonOptimizer,
|
| 154 |
+
description: 'Carbon Footprint Tracking'
|
| 155 |
+
},
|
| 156 |
+
{
|
| 157 |
+
id: 'SmartFarming',
|
| 158 |
+
name: 'Smart Farming',
|
| 159 |
+
icon: '🌾',
|
| 160 |
+
component: SmartFarming,
|
| 161 |
+
description: 'Agricultural Intelligence'
|
| 162 |
+
},
|
| 163 |
+
{
|
| 164 |
+
id: 'EWasteRecycling',
|
| 165 |
+
name: 'E-Waste Recycling',
|
| 166 |
+
icon: '♻️',
|
| 167 |
+
component: EWasteRecycling,
|
| 168 |
+
description: 'Electronic Waste Management'
|
| 169 |
+
},
|
| 170 |
+
{
|
| 171 |
+
id: 'AirQuality',
|
| 172 |
+
name: 'Air Quality',
|
| 173 |
+
icon: '🌬️',
|
| 174 |
+
component: AirQuality,
|
| 175 |
+
description: 'Air Pollution Monitoring'
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
id: 'FloraShield',
|
| 179 |
+
name: 'FloraShield',
|
| 180 |
+
icon: '🛡️',
|
| 181 |
+
component: FloraShield,
|
| 182 |
+
description: 'Plant Disease Detection'
|
| 183 |
+
},
|
| 184 |
+
{
|
| 185 |
+
id: 'FoodWasteReduction',
|
| 186 |
+
name: 'Food Waste Reduction',
|
| 187 |
+
icon: '🍎',
|
| 188 |
+
component: FoodWasteReduction,
|
| 189 |
+
description: 'Food Rescue Network'
|
| 190 |
+
},
|
| 191 |
+
{
|
| 192 |
+
id: 'EnvironmentalJustice',
|
| 193 |
+
name: 'Environmental Justice',
|
| 194 |
+
icon: '⚖️',
|
| 195 |
+
component: EnvironmentalJustice,
|
| 196 |
+
description: 'Equity & Justice'
|
| 197 |
+
},
|
| 198 |
+
{
|
| 199 |
+
id: 'EcoSonification',
|
| 200 |
+
name: 'EcoSonification',
|
| 201 |
+
icon: '🎵',
|
| 202 |
+
component: EcoSonification,
|
| 203 |
+
description: 'Environmental Sound Art'
|
| 204 |
+
},
|
| 205 |
+
{
|
| 206 |
+
id: 'PhantomFootprint',
|
| 207 |
+
name: 'Phantom Footprint',
|
| 208 |
+
icon: '👻',
|
| 209 |
+
component: PhantomFootprint,
|
| 210 |
+
description: 'Hidden Impact Tracker'
|
| 211 |
+
},
|
| 212 |
+
{
|
| 213 |
+
id: 'UpcyclingAgent',
|
| 214 |
+
name: 'Upcycling Agent',
|
| 215 |
+
icon: '🔄',
|
| 216 |
+
component: UpcyclingAgent,
|
| 217 |
+
description: 'Creative Reuse Assistant'
|
| 218 |
+
},
|
| 219 |
+
{
|
| 220 |
+
id: 'PackagingDesigner',
|
| 221 |
+
name: 'Packaging Designer',
|
| 222 |
+
icon: '📦',
|
| 223 |
+
component: PackagingDesigner,
|
| 224 |
+
description: 'Sustainable Packaging'
|
| 225 |
+
},
|
| 226 |
+
{
|
| 227 |
+
id: 'DigitalQuarry',
|
| 228 |
+
name: 'Digital Quarry',
|
| 229 |
+
icon: '🏗️',
|
| 230 |
+
component: DigitalQuarry,
|
| 231 |
+
description: 'Construction Waste Marketplace'
|
| 232 |
+
},
|
| 233 |
+
{
|
| 234 |
+
id: 'BioStreamAI',
|
| 235 |
+
name: 'Bio-Stream AI',
|
| 236 |
+
icon: '🧬',
|
| 237 |
+
component: BioStreamAI,
|
| 238 |
+
description: 'Environmental DNA Analysis'
|
| 239 |
+
},
|
| 240 |
+
{
|
| 241 |
+
id: 'EWasteProspector',
|
| 242 |
+
name: 'E-Waste Prospector',
|
| 243 |
+
icon: '⚡',
|
| 244 |
+
component: EWasteProspector,
|
| 245 |
+
description: 'Critical Mineral Recovery'
|
| 246 |
+
},
|
| 247 |
+
{
|
| 248 |
+
id: 'GeneticResilience',
|
| 249 |
+
name: 'Genetic Resilience',
|
| 250 |
+
icon: '🌾',
|
| 251 |
+
component: GeneticResilience,
|
| 252 |
+
description: 'Climate Crop Analysis'
|
| 253 |
+
},
|
| 254 |
+
{
|
| 255 |
+
id: 'Impact',
|
| 256 |
+
name: 'Global Impact',
|
| 257 |
+
icon: '🌍',
|
| 258 |
+
component: Impact,
|
| 259 |
+
description: 'Environmental Impact Data'
|
| 260 |
+
},
|
| 261 |
+
{
|
| 262 |
+
id: 'EcoTasks',
|
| 263 |
+
name: 'Eco Tasks',
|
| 264 |
+
icon: '✅',
|
| 265 |
+
component: EcoTasks,
|
| 266 |
+
description: 'Daily Green Actions'
|
| 267 |
+
},
|
| 268 |
+
{
|
| 269 |
+
id: 'Learn',
|
| 270 |
+
name: 'Learn',
|
| 271 |
+
icon: '📚',
|
| 272 |
+
component: Learn,
|
| 273 |
+
description: 'Environmental Education'
|
| 274 |
+
},
|
| 275 |
+
{
|
| 276 |
+
id: 'Startups',
|
| 277 |
+
name: 'Green Startups',
|
| 278 |
+
icon: '🚀',
|
| 279 |
+
component: Startups,
|
| 280 |
+
description: 'Sustainable Innovation'
|
| 281 |
+
},
|
| 282 |
+
{
|
| 283 |
+
id: 'Community',
|
| 284 |
+
name: 'Community',
|
| 285 |
+
icon: '👥',
|
| 286 |
+
component: Community,
|
| 287 |
+
description: 'Connect with eco champions'
|
| 288 |
+
},
|
| 289 |
+
{
|
| 290 |
+
id: 'Profile',
|
| 291 |
+
name: 'My Profile',
|
| 292 |
+
icon: '👤',
|
| 293 |
+
component: Profile,
|
| 294 |
+
description: 'View your environmental impact'
|
| 295 |
+
},
|
| 296 |
+
{
|
| 297 |
+
id: 'Login',
|
| 298 |
+
name: 'Login',
|
| 299 |
+
icon: '🔑',
|
| 300 |
+
component: Login,
|
| 301 |
+
description: 'Sign in or create account'
|
| 302 |
+
}
|
| 303 |
+
];
|
| 304 |
+
|
| 305 |
+
// Initialize page from URL on app load
|
| 306 |
+
useEffect(() => {
|
| 307 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 308 |
+
const pageFromUrl = urlParams.get('page');
|
| 309 |
+
|
| 310 |
+
if (pageFromUrl && pages.some(page => page.id === pageFromUrl)) {
|
| 311 |
+
setActivePage(pageFromUrl);
|
| 312 |
+
} else {
|
| 313 |
+
// If no valid page in URL, set default and update URL
|
| 314 |
+
const defaultPage = 'Dashboard';
|
| 315 |
+
setActivePage(defaultPage);
|
| 316 |
+
updateURL(defaultPage);
|
| 317 |
+
}
|
| 318 |
+
}, []);
|
| 319 |
+
|
| 320 |
+
// Function to update URL without page reload
|
| 321 |
+
const updateURL = (pageId) => {
|
| 322 |
+
const newUrl = `${window.location.pathname}?page=${pageId}`;
|
| 323 |
+
window.history.pushState({ page: pageId }, '', newUrl);
|
| 324 |
+
};
|
| 325 |
+
|
| 326 |
+
// Handle browser back/forward buttons
|
| 327 |
+
useEffect(() => {
|
| 328 |
+
const handlePopState = (event) => {
|
| 329 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 330 |
+
const pageFromUrl = urlParams.get('page') || 'Dashboard';
|
| 331 |
+
|
| 332 |
+
if (pages.some(page => page.id === pageFromUrl)) {
|
| 333 |
+
setActivePage(pageFromUrl);
|
| 334 |
+
}
|
| 335 |
+
};
|
| 336 |
+
|
| 337 |
+
window.addEventListener('popstate', handlePopState);
|
| 338 |
+
return () => window.removeEventListener('popstate', handlePopState);
|
| 339 |
+
}, []);
|
| 340 |
+
|
| 341 |
+
const currentPage = pages.find(page => page.id === activePage);
|
| 342 |
+
const CurrentComponent = currentPage?.component || Dashboard;
|
| 343 |
+
|
| 344 |
+
const handlePageChange = (pageId) => {
|
| 345 |
+
setActivePage(pageId);
|
| 346 |
+
updateURL(pageId);
|
| 347 |
+
};
|
| 348 |
+
|
| 349 |
+
// Handle activity completion to update stats immediately
|
| 350 |
+
const handleActivityComplete = async (activity) => {
|
| 351 |
+
console.log('🎯 App.js handleActivityComplete called with:', activity);
|
| 352 |
+
|
| 353 |
+
// Log activity through auth manager if activity data is provided
|
| 354 |
+
if (activity && activity.type) {
|
| 355 |
+
console.log('📝 Logging activity through authManager...');
|
| 356 |
+
await authManager.logActivity(activity.description || 'Environmental action', {
|
| 357 |
+
type: activity.type,
|
| 358 |
+
amount: activity.amount,
|
| 359 |
+
points: activity.points || 10
|
| 360 |
+
});
|
| 361 |
+
console.log('✅ Activity logged successfully');
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
// Update stats display
|
| 365 |
+
console.log('🔄 Calling updateUserStats after activity completion...');
|
| 366 |
+
updateUserStats();
|
| 367 |
+
};
|
| 368 |
+
|
| 369 |
+
// Show loading screen first
|
| 370 |
+
if (isLoading) {
|
| 371 |
+
return <LoadingScreen />;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
return (
|
| 375 |
+
<div className="terra-app">
|
| 376 |
+
{/* Sidebar */}
|
| 377 |
+
<div className="terra-sidebar open">
|
| 378 |
+
{/* Logo */}
|
| 379 |
+
<div className="terra-logo">
|
| 380 |
+
<div className="logo-container">
|
| 381 |
+
<div className="logo-icon">🌿</div>
|
| 382 |
+
<div className="logo-text">
|
| 383 |
+
<h1>GreenPlus by GXS</h1>
|
| 384 |
+
<p>Environmental Intelligence Hub</p>
|
| 385 |
+
</div>
|
| 386 |
+
</div>
|
| 387 |
+
</div>
|
| 388 |
+
|
| 389 |
+
{/* Navigation */}
|
| 390 |
+
<div className="terra-nav">
|
| 391 |
+
{pages.map(page => (
|
| 392 |
+
<div
|
| 393 |
+
key={page.id}
|
| 394 |
+
className={`nav-item ${activePage === page.id ? 'active' : ''}`}
|
| 395 |
+
onClick={() => handlePageChange(page.id)}
|
| 396 |
+
>
|
| 397 |
+
<span className="nav-icon">{page.icon}</span>
|
| 398 |
+
<div className="nav-content">
|
| 399 |
+
<span className="nav-name">{page.name}</span>
|
| 400 |
+
<span className="nav-desc">{page.description}</span>
|
| 401 |
+
</div>
|
| 402 |
+
</div>
|
| 403 |
+
))}
|
| 404 |
+
</div>
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
</div>
|
| 408 |
+
|
| 409 |
+
{/* Main Content */}
|
| 410 |
+
<div className="terra-main">
|
| 411 |
+
{/* Header */}
|
| 412 |
+
<div className="terra-header">
|
| 413 |
+
<div className="header-left">
|
| 414 |
+
<h1 className="page-title">
|
| 415 |
+
<span className="title-icon">{currentPage?.icon}</span>
|
| 416 |
+
{currentPage?.name}
|
| 417 |
+
</h1>
|
| 418 |
+
<p className="page-subtitle">{currentPage?.description}</p>
|
| 419 |
+
</div>
|
| 420 |
+
<div className="header-right">
|
| 421 |
+
{/* User Info */}
|
| 422 |
+
{currentUser && (
|
| 423 |
+
<div style={{
|
| 424 |
+
display: 'flex',
|
| 425 |
+
alignItems: 'center',
|
| 426 |
+
gap: '15px',
|
| 427 |
+
marginRight: '20px'
|
| 428 |
+
}}>
|
| 429 |
+
<div
|
| 430 |
+
onClick={() => handlePageChange('Profile')}
|
| 431 |
+
style={{
|
| 432 |
+
display: 'flex',
|
| 433 |
+
alignItems: 'center',
|
| 434 |
+
gap: '8px',
|
| 435 |
+
background: 'rgba(255,255,255,0.1)',
|
| 436 |
+
padding: '8px 15px',
|
| 437 |
+
borderRadius: '20px',
|
| 438 |
+
cursor: 'pointer',
|
| 439 |
+
transition: 'background 0.2s'
|
| 440 |
+
}}
|
| 441 |
+
onMouseEnter={(e) => e.target.style.background = 'rgba(255,255,255,0.2)'}
|
| 442 |
+
onMouseLeave={(e) => e.target.style.background = 'rgba(255,255,255,0.1)'}
|
| 443 |
+
title="Click to view profile"
|
| 444 |
+
>
|
| 445 |
+
<span style={{ fontSize: '1.2rem' }}>{currentUser.avatar}</span>
|
| 446 |
+
<span style={{ color: 'white', fontWeight: 'bold' }}>
|
| 447 |
+
{currentUser.name}
|
| 448 |
+
</span>
|
| 449 |
+
{currentUser.isGuest && (
|
| 450 |
+
<span style={{
|
| 451 |
+
background: '#FF9800',
|
| 452 |
+
color: 'white',
|
| 453 |
+
padding: '2px 8px',
|
| 454 |
+
borderRadius: '10px',
|
| 455 |
+
fontSize: '0.7rem'
|
| 456 |
+
}}>
|
| 457 |
+
GUEST
|
| 458 |
+
</span>
|
| 459 |
+
)}
|
| 460 |
+
</div>
|
| 461 |
+
</div>
|
| 462 |
+
)}
|
| 463 |
+
|
| 464 |
+
<div className="header-stats">
|
| 465 |
+
<div className="header-stat">
|
| 466 |
+
<span>🌿</span>
|
| 467 |
+
<div>
|
| 468 |
+
<div>{Math.round(userStats.carbonSaved)}</div>
|
| 469 |
+
<div>CO₂ Saved (kg)</div>
|
| 470 |
+
</div>
|
| 471 |
+
</div>
|
| 472 |
+
<div className="header-stat">
|
| 473 |
+
<span>💧</span>
|
| 474 |
+
<div>
|
| 475 |
+
<div>{userStats.waterTests}</div>
|
| 476 |
+
<div>Water Tests</div>
|
| 477 |
+
</div>
|
| 478 |
+
</div>
|
| 479 |
+
<div className="header-stat">
|
| 480 |
+
<span>🦜</span>
|
| 481 |
+
<div>
|
| 482 |
+
<div>{userStats.biodiversityScans}</div>
|
| 483 |
+
<div>Bio Scans</div>
|
| 484 |
+
</div>
|
| 485 |
+
</div>
|
| 486 |
+
<div className="header-stat">
|
| 487 |
+
<span>🌳</span>
|
| 488 |
+
<div>
|
| 489 |
+
<div>{userStats.treesPlanted}</div>
|
| 490 |
+
<div>Trees Planted</div>
|
| 491 |
+
</div>
|
| 492 |
+
</div>
|
| 493 |
+
</div>
|
| 494 |
+
|
| 495 |
+
</div>
|
| 496 |
+
</div>
|
| 497 |
+
|
| 498 |
+
{/* Content */}
|
| 499 |
+
<div className="terra-content">
|
| 500 |
+
<CurrentComponent
|
| 501 |
+
onNavigate={handlePageChange}
|
| 502 |
+
onActivityComplete={handleActivityComplete}
|
| 503 |
+
onAuthChange={handleAuthChange}
|
| 504 |
+
userStats={userStats}
|
| 505 |
+
currentUser={currentUser}
|
| 506 |
+
isAuthenticated={currentUser && !currentUser.isGuest}
|
| 507 |
+
/>
|
| 508 |
+
</div>
|
| 509 |
+
</div>
|
| 510 |
+
</div>
|
| 511 |
+
);
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
export default App;
|
web/src/App.test.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { render, screen } from '@testing-library/react';
|
| 3 |
+
import '@testing-library/jest-dom';
|
| 4 |
+
import SimpleComponent from './SimpleComponent'; // We are importing our new simple component
|
| 5 |
+
|
| 6 |
+
// Test 1: Check if our simple component renders.
|
| 7 |
+
test('renders the simple component without crashing', () => {
|
| 8 |
+
render(<SimpleComponent />);
|
| 9 |
+
const titleElement = screen.getByText(/EcoSpire Test Passed/i);
|
| 10 |
+
expect(titleElement).toBeInTheDocument();
|
| 11 |
+
});
|
| 12 |
+
|
| 13 |
+
// Test 2: Check for the paragraph in our simple component.
|
| 14 |
+
test('shows the success message in the simple component', () => {
|
| 15 |
+
render(<SimpleComponent />);
|
| 16 |
+
const messageElement = screen.getByText(/Component rendered successfully/i);
|
| 17 |
+
expect(messageElement).toBeInTheDocument();
|
| 18 |
+
});
|
web/src/SimpleComponent.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
function SimpleComponent() {
|
| 4 |
+
return (
|
| 5 |
+
<div>
|
| 6 |
+
<h1>GreenPlus by GXS Test Passed</h1>
|
| 7 |
+
<p>Component rendered successfully.</p>
|
| 8 |
+
</div>
|
| 9 |
+
);
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export default SimpleComponent;
|
web/src/components/Analytics.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
function Analytics({ activities, goals }) {
|
| 4 |
+
// Calculate various analytics
|
| 5 |
+
const calculateAnalytics = () => {
|
| 6 |
+
if (activities.length === 0) return null;
|
| 7 |
+
|
| 8 |
+
const now = new Date();
|
| 9 |
+
const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
| 10 |
+
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
| 11 |
+
|
| 12 |
+
// Filter activities by time period
|
| 13 |
+
const last30Days = activities.filter(activity =>
|
| 14 |
+
new Date(activity.timestamp || Date.now()) >= thirtyDaysAgo
|
| 15 |
+
);
|
| 16 |
+
const last7Days = activities.filter(activity =>
|
| 17 |
+
new Date(activity.timestamp || Date.now()) >= sevenDaysAgo
|
| 18 |
+
);
|
| 19 |
+
|
| 20 |
+
// Calculate totals by category
|
| 21 |
+
const categoryTotals = activities.reduce((acc, activity) => {
|
| 22 |
+
acc[activity.type] = (acc[activity.type] || 0) + activity.co2;
|
| 23 |
+
return acc;
|
| 24 |
+
}, {});
|
| 25 |
+
|
| 26 |
+
// Calculate monthly trend
|
| 27 |
+
const monthlyData = [];
|
| 28 |
+
for (let i = 29; i >= 0; i--) {
|
| 29 |
+
const date = new Date(now.getTime() - i * 24 * 60 * 60 * 1000);
|
| 30 |
+
const dayActivities = activities.filter(activity => {
|
| 31 |
+
const activityDate = new Date(activity.timestamp || Date.now());
|
| 32 |
+
return activityDate.toDateString() === date.toDateString();
|
| 33 |
+
});
|
| 34 |
+
const dayTotal = dayActivities.reduce((sum, activity) => sum + activity.co2, 0);
|
| 35 |
+
monthlyData.push({
|
| 36 |
+
date: date.getDate(),
|
| 37 |
+
co2: dayTotal,
|
| 38 |
+
day: date.toLocaleDateString('en-US', { weekday: 'short' })
|
| 39 |
+
});
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// Calculate averages
|
| 43 |
+
const dailyAverage = last30Days.reduce((sum, activity) => sum + activity.co2, 0) / 30;
|
| 44 |
+
const weeklyAverage = last7Days.reduce((sum, activity) => sum + activity.co2, 0) / 7;
|
| 45 |
+
|
| 46 |
+
// Calculate improvement
|
| 47 |
+
const firstHalf = last30Days.slice(0, 15).reduce((sum, activity) => sum + activity.co2, 0) / 15;
|
| 48 |
+
const secondHalf = last30Days.slice(15).reduce((sum, activity) => sum + activity.co2, 0) / 15;
|
| 49 |
+
const improvement = ((firstHalf - secondHalf) / firstHalf) * 100;
|
| 50 |
+
|
| 51 |
+
return {
|
| 52 |
+
categoryTotals,
|
| 53 |
+
monthlyData,
|
| 54 |
+
dailyAverage,
|
| 55 |
+
weeklyAverage,
|
| 56 |
+
improvement,
|
| 57 |
+
totalActivities: activities.length,
|
| 58 |
+
totalCO2: activities.reduce((sum, activity) => sum + activity.co2, 0),
|
| 59 |
+
last30DaysCO2: last30Days.reduce((sum, activity) => sum + activity.co2, 0),
|
| 60 |
+
last7DaysCO2: last7Days.reduce((sum, activity) => sum + activity.co2, 0)
|
| 61 |
+
};
|
| 62 |
+
};
|
| 63 |
+
|
| 64 |
+
const analytics = calculateAnalytics();
|
| 65 |
+
|
| 66 |
+
if (!analytics) {
|
| 67 |
+
return (
|
| 68 |
+
<div className="card">
|
| 69 |
+
<h3>📊 Analytics</h3>
|
| 70 |
+
<p style={{ textAlign: 'center', color: '#666', padding: '40px' }}>
|
| 71 |
+
Start tracking activities to see your analytics!
|
| 72 |
+
</p>
|
| 73 |
+
</div>
|
| 74 |
+
);
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
const categoryColors = {
|
| 78 |
+
transport: '#2196F3',
|
| 79 |
+
energy: '#FF9800',
|
| 80 |
+
food: '#4CAF50',
|
| 81 |
+
waste: '#9C27B0'
|
| 82 |
+
};
|
| 83 |
+
|
| 84 |
+
const categoryIcons = {
|
| 85 |
+
transport: '🚗',
|
| 86 |
+
energy: '⚡',
|
| 87 |
+
food: '🍽️',
|
| 88 |
+
waste: '🗑️'
|
| 89 |
+
};
|
| 90 |
+
|
| 91 |
+
return (
|
| 92 |
+
<div className="card">
|
| 93 |
+
<h3>📊 Your Environmental Analytics</h3>
|
| 94 |
+
|
| 95 |
+
{/* Key Metrics */}
|
| 96 |
+
<div className="grid grid-4" style={{ marginBottom: '30px' }}>
|
| 97 |
+
<div style={{ textAlign: 'center', padding: '15px', background: '#f8f9fa', borderRadius: '8px' }}>
|
| 98 |
+
<div style={{ fontSize: '2rem', color: '#2E7D32', fontWeight: 'bold' }}>
|
| 99 |
+
{analytics.totalCO2.toFixed(1)}
|
| 100 |
+
</div>
|
| 101 |
+
<div style={{ fontSize: '0.9rem', color: '#666' }}>Total CO₂ (kg)</div>
|
| 102 |
+
</div>
|
| 103 |
+
<div style={{ textAlign: 'center', padding: '15px', background: '#f8f9fa', borderRadius: '8px' }}>
|
| 104 |
+
<div style={{ fontSize: '2rem', color: '#2196F3', fontWeight: 'bold' }}>
|
| 105 |
+
{analytics.dailyAverage.toFixed(1)}
|
| 106 |
+
</div>
|
| 107 |
+
<div style={{ fontSize: '0.9rem', color: '#666' }}>Daily Average</div>
|
| 108 |
+
</div>
|
| 109 |
+
<div style={{ textAlign: 'center', padding: '15px', background: '#f8f9fa', borderRadius: '8px' }}>
|
| 110 |
+
<div style={{ fontSize: '2rem', color: analytics.improvement > 0 ? '#4CAF50' : '#f44336', fontWeight: 'bold' }}>
|
| 111 |
+
{analytics.improvement > 0 ? '-' : '+'}{Math.abs(analytics.improvement).toFixed(1)}%
|
| 112 |
+
</div>
|
| 113 |
+
<div style={{ fontSize: '0.9rem', color: '#666' }}>30-Day Trend</div>
|
| 114 |
+
</div>
|
| 115 |
+
<div style={{ textAlign: 'center', padding: '15px', background: '#f8f9fa', borderRadius: '8px' }}>
|
| 116 |
+
<div style={{ fontSize: '2rem', color: '#FF9800', fontWeight: 'bold' }}>
|
| 117 |
+
{analytics.totalActivities}
|
| 118 |
+
</div>
|
| 119 |
+
<div style={{ fontSize: '0.9rem', color: '#666' }}>Activities Logged</div>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
|
| 123 |
+
{/* Category Breakdown */}
|
| 124 |
+
<div style={{ marginBottom: '30px' }}>
|
| 125 |
+
<h4 style={{ marginBottom: '15px' }}>🎯 Impact by Category</h4>
|
| 126 |
+
<div className="grid grid-2">
|
| 127 |
+
{Object.entries(analytics.categoryTotals).map(([category, total]) => (
|
| 128 |
+
<div key={category} style={{
|
| 129 |
+
display: 'flex',
|
| 130 |
+
alignItems: 'center',
|
| 131 |
+
padding: '15px',
|
| 132 |
+
border: '1px solid #eee',
|
| 133 |
+
borderRadius: '8px',
|
| 134 |
+
marginBottom: '10px'
|
| 135 |
+
}}>
|
| 136 |
+
<div style={{
|
| 137 |
+
fontSize: '2rem',
|
| 138 |
+
marginRight: '15px',
|
| 139 |
+
width: '50px',
|
| 140 |
+
textAlign: 'center'
|
| 141 |
+
}}>
|
| 142 |
+
{categoryIcons[category]}
|
| 143 |
+
</div>
|
| 144 |
+
<div style={{ flex: 1 }}>
|
| 145 |
+
<div style={{
|
| 146 |
+
display: 'flex',
|
| 147 |
+
justifyContent: 'space-between',
|
| 148 |
+
alignItems: 'center',
|
| 149 |
+
marginBottom: '8px'
|
| 150 |
+
}}>
|
| 151 |
+
<span style={{ fontWeight: 'bold', textTransform: 'capitalize' }}>
|
| 152 |
+
{category}
|
| 153 |
+
</span>
|
| 154 |
+
<span style={{ color: categoryColors[category], fontWeight: 'bold' }}>
|
| 155 |
+
{total.toFixed(1)} kg CO₂
|
| 156 |
+
</span>
|
| 157 |
+
</div>
|
| 158 |
+
<div style={{
|
| 159 |
+
width: '100%',
|
| 160 |
+
height: '6px',
|
| 161 |
+
background: '#eee',
|
| 162 |
+
borderRadius: '3px',
|
| 163 |
+
overflow: 'hidden'
|
| 164 |
+
}}>
|
| 165 |
+
<div style={{
|
| 166 |
+
width: `${(total / analytics.totalCO2) * 100}%`,
|
| 167 |
+
height: '100%',
|
| 168 |
+
background: categoryColors[category]
|
| 169 |
+
}}></div>
|
| 170 |
+
</div>
|
| 171 |
+
<div style={{
|
| 172 |
+
fontSize: '0.8rem',
|
| 173 |
+
color: '#666',
|
| 174 |
+
marginTop: '5px'
|
| 175 |
+
}}>
|
| 176 |
+
{((total / analytics.totalCO2) * 100).toFixed(1)}% of total impact
|
| 177 |
+
</div>
|
| 178 |
+
</div>
|
| 179 |
+
</div>
|
| 180 |
+
))}
|
| 181 |
+
</div>
|
| 182 |
+
</div>
|
| 183 |
+
|
| 184 |
+
{/* Environmental Impact Comparison */}
|
| 185 |
+
<div style={{
|
| 186 |
+
background: 'linear-gradient(135deg, #e8f5e8 0%, #f1f8e9 100%)',
|
| 187 |
+
padding: '20px',
|
| 188 |
+
borderRadius: '12px'
|
| 189 |
+
}}>
|
| 190 |
+
<h4 style={{ color: '#2E7D32', marginBottom: '15px' }}>🌍 Your Environmental Impact</h4>
|
| 191 |
+
<div className="grid grid-3">
|
| 192 |
+
<div style={{ textAlign: 'center' }}>
|
| 193 |
+
<div style={{ fontSize: '1.5rem', marginBottom: '5px' }}>🌳</div>
|
| 194 |
+
<div style={{ fontWeight: 'bold', color: '#2E7D32' }}>
|
| 195 |
+
{(analytics.totalCO2 / 22).toFixed(1)} trees
|
| 196 |
+
</div>
|
| 197 |
+
<div style={{ fontSize: '0.8rem', color: '#666' }}>needed to offset</div>
|
| 198 |
+
</div>
|
| 199 |
+
<div style={{ textAlign: 'center' }}>
|
| 200 |
+
<div style={{ fontSize: '1.5rem', marginBottom: '5px' }}>🚗</div>
|
| 201 |
+
<div style={{ fontWeight: 'bold', color: '#2E7D32' }}>
|
| 202 |
+
{(analytics.totalCO2 / 0.21).toFixed(0)} km
|
| 203 |
+
</div>
|
| 204 |
+
<div style={{ fontSize: '0.8rem', color: '#666' }}>car driving equivalent</div>
|
| 205 |
+
</div>
|
| 206 |
+
<div style={{ textAlign: 'center' }}>
|
| 207 |
+
<div style={{ fontSize: '1.5rem', marginBottom: '5px' }}>⚡</div>
|
| 208 |
+
<div style={{ fontWeight: 'bold', color: '#2E7D32' }}>
|
| 209 |
+
{(analytics.totalCO2 / 0.5).toFixed(0)} kWh
|
| 210 |
+
</div>
|
| 211 |
+
<div style={{ fontSize: '0.8rem', color: '#666' }}>electricity equivalent</div>
|
| 212 |
+
</div>
|
| 213 |
+
</div>
|
| 214 |
+
</div>
|
| 215 |
+
</div>
|
| 216 |
+
);
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
export default Analytics;
|
web/src/components/Header.css
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.header {
|
| 2 |
+
background: linear-gradient(135deg, #2E7D32 0%, #4CAF50 100%);
|
| 3 |
+
color: white;
|
| 4 |
+
padding: 20px 0;
|
| 5 |
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
.header-content {
|
| 9 |
+
display: flex;
|
| 10 |
+
justify-content: space-between;
|
| 11 |
+
align-items: center;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
.logo {
|
| 15 |
+
display: flex;
|
| 16 |
+
align-items: center;
|
| 17 |
+
gap: 12px;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
.logo-icon {
|
| 21 |
+
font-size: 2.5rem;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.logo h1 {
|
| 25 |
+
font-size: 2rem;
|
| 26 |
+
font-weight: 700;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
.header-subtitle {
|
| 30 |
+
font-size: 1rem;
|
| 31 |
+
opacity: 0.9;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
@media (max-width: 768px) {
|
| 35 |
+
.header-content {
|
| 36 |
+
flex-direction: column;
|
| 37 |
+
gap: 10px;
|
| 38 |
+
text-align: center;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
.logo h1 {
|
| 42 |
+
font-size: 1.5rem;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.header-subtitle {
|
| 46 |
+
font-size: 0.9rem;
|
| 47 |
+
}
|
| 48 |
+
}
|
web/src/components/Header.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
import { authManager } from '../utils/auth';
|
| 3 |
+
|
| 4 |
+
const Header = ({ currentUser, onNavigate, onAuthChange }) => {
|
| 5 |
+
const [showUserMenu, setShowUserMenu] = useState(false);
|
| 6 |
+
const [isLoggingOut, setIsLoggingOut] = useState(false);
|
| 7 |
+
|
| 8 |
+
const handleLogout = async () => {
|
| 9 |
+
setIsLoggingOut(true);
|
| 10 |
+
try {
|
| 11 |
+
await authManager.logout();
|
| 12 |
+
const guestUser = authManager.getCurrentUser();
|
| 13 |
+
onAuthChange && onAuthChange(guestUser);
|
| 14 |
+
setShowUserMenu(false);
|
| 15 |
+
onNavigate && onNavigate('Dashboard');
|
| 16 |
+
} catch (error) {
|
| 17 |
+
console.error('Logout failed:', error);
|
| 18 |
+
} finally {
|
| 19 |
+
setIsLoggingOut(false);
|
| 20 |
+
}
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
return (
|
| 24 |
+
<header style={{
|
| 25 |
+
background: 'linear-gradient(135deg, #2c3e50 0%, #34495e 100%)',
|
| 26 |
+
color: 'white',
|
| 27 |
+
padding: '15px 20px',
|
| 28 |
+
display: 'flex',
|
| 29 |
+
justifyContent: 'space-between',
|
| 30 |
+
alignItems: 'center',
|
| 31 |
+
boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
|
| 32 |
+
position: 'sticky',
|
| 33 |
+
top: 0,
|
| 34 |
+
zIndex: 1000
|
| 35 |
+
}}>
|
| 36 |
+
{/* Logo */}
|
| 37 |
+
<div
|
| 38 |
+
onClick={() => onNavigate && onNavigate('Dashboard')}
|
| 39 |
+
style={{
|
| 40 |
+
display: 'flex',
|
| 41 |
+
alignItems: 'center',
|
| 42 |
+
gap: '10px',
|
| 43 |
+
cursor: 'pointer',
|
| 44 |
+
fontSize: '1.5rem',
|
| 45 |
+
fontWeight: 'bold'
|
| 46 |
+
}}
|
| 47 |
+
>
|
| 48 |
+
<span style={{ fontSize: '2rem' }}>🌿</span>
|
| 49 |
+
GreenPlus by GXS
|
| 50 |
+
</div>
|
| 51 |
+
|
| 52 |
+
{/* User Section */}
|
| 53 |
+
<div style={{ position: 'relative' }}>
|
| 54 |
+
{currentUser ? (
|
| 55 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
|
| 56 |
+
{/* User Stats */}
|
| 57 |
+
<div style={{
|
| 58 |
+
display: 'flex',
|
| 59 |
+
alignItems: 'center',
|
| 60 |
+
gap: '15px',
|
| 61 |
+
fontSize: '0.9rem',
|
| 62 |
+
opacity: 0.9
|
| 63 |
+
}}>
|
| 64 |
+
<span>⭐ Level {authManager.getUserStats().level}</span>
|
| 65 |
+
<span>🏆 {authManager.getUserStats().points} pts</span>
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
{/* User Menu */}
|
| 69 |
+
<div
|
| 70 |
+
onClick={() => setShowUserMenu(!showUserMenu)}
|
| 71 |
+
style={{
|
| 72 |
+
display: 'flex',
|
| 73 |
+
alignItems: 'center',
|
| 74 |
+
gap: '8px',
|
| 75 |
+
cursor: 'pointer',
|
| 76 |
+
padding: '8px 12px',
|
| 77 |
+
borderRadius: '20px',
|
| 78 |
+
background: 'rgba(255,255,255,0.1)',
|
| 79 |
+
transition: 'background 0.2s'
|
| 80 |
+
}}
|
| 81 |
+
>
|
| 82 |
+
<span style={{ fontSize: '1.2rem' }}>{currentUser.avatar}</span>
|
| 83 |
+
<span style={{ fontWeight: '500' }}>{currentUser.name}</span>
|
| 84 |
+
<span style={{ fontSize: '0.8rem' }}>▼</span>
|
| 85 |
+
</div>
|
| 86 |
+
|
| 87 |
+
{/* Dropdown Menu */}
|
| 88 |
+
{showUserMenu && (
|
| 89 |
+
<div style={{
|
| 90 |
+
position: 'absolute',
|
| 91 |
+
top: '100%',
|
| 92 |
+
right: 0,
|
| 93 |
+
marginTop: '10px',
|
| 94 |
+
background: 'white',
|
| 95 |
+
borderRadius: '12px',
|
| 96 |
+
boxShadow: '0 4px 20px rgba(0,0,0,0.15)',
|
| 97 |
+
minWidth: '200px',
|
| 98 |
+
overflow: 'hidden',
|
| 99 |
+
zIndex: 1001
|
| 100 |
+
}}>
|
| 101 |
+
<div style={{
|
| 102 |
+
padding: '15px',
|
| 103 |
+
borderBottom: '1px solid #ecf0f1',
|
| 104 |
+
background: '#f8f9fa'
|
| 105 |
+
}}>
|
| 106 |
+
<div style={{ fontWeight: 'bold', color: '#2c3e50' }}>
|
| 107 |
+
{currentUser.name}
|
| 108 |
+
</div>
|
| 109 |
+
<div style={{ fontSize: '0.9rem', color: '#7f8c8d' }}>
|
| 110 |
+
{currentUser.email}
|
| 111 |
+
</div>
|
| 112 |
+
{currentUser.isGuest && (
|
| 113 |
+
<div style={{
|
| 114 |
+
fontSize: '0.8rem',
|
| 115 |
+
color: '#e67e22',
|
| 116 |
+
marginTop: '5px',
|
| 117 |
+
fontWeight: '500'
|
| 118 |
+
}}>
|
| 119 |
+
🚀 Guest Mode
|
| 120 |
+
</div>
|
| 121 |
+
)}
|
| 122 |
+
</div>
|
| 123 |
+
|
| 124 |
+
<div style={{ padding: '10px 0' }}>
|
| 125 |
+
<button
|
| 126 |
+
onClick={() => {
|
| 127 |
+
setShowUserMenu(false);
|
| 128 |
+
onNavigate && onNavigate('Profile');
|
| 129 |
+
}}
|
| 130 |
+
style={{
|
| 131 |
+
width: '100%',
|
| 132 |
+
padding: '12px 20px',
|
| 133 |
+
background: 'none',
|
| 134 |
+
border: 'none',
|
| 135 |
+
textAlign: 'left',
|
| 136 |
+
cursor: 'pointer',
|
| 137 |
+
color: '#2c3e50',
|
| 138 |
+
display: 'flex',
|
| 139 |
+
alignItems: 'center',
|
| 140 |
+
gap: '10px',
|
| 141 |
+
fontSize: '14px'
|
| 142 |
+
}}
|
| 143 |
+
onMouseEnter={(e) => e.target.style.background = '#f8f9fa'}
|
| 144 |
+
onMouseLeave={(e) => e.target.style.background = 'none'}
|
| 145 |
+
>
|
| 146 |
+
👤 View Profile
|
| 147 |
+
</button>
|
| 148 |
+
|
| 149 |
+
<button
|
| 150 |
+
onClick={() => {
|
| 151 |
+
setShowUserMenu(false);
|
| 152 |
+
onNavigate && onNavigate('Community');
|
| 153 |
+
}}
|
| 154 |
+
style={{
|
| 155 |
+
width: '100%',
|
| 156 |
+
padding: '12px 20px',
|
| 157 |
+
background: 'none',
|
| 158 |
+
border: 'none',
|
| 159 |
+
textAlign: 'left',
|
| 160 |
+
cursor: 'pointer',
|
| 161 |
+
color: '#2c3e50',
|
| 162 |
+
display: 'flex',
|
| 163 |
+
alignItems: 'center',
|
| 164 |
+
gap: '10px',
|
| 165 |
+
fontSize: '14px'
|
| 166 |
+
}}
|
| 167 |
+
onMouseEnter={(e) => e.target.style.background = '#f8f9fa'}
|
| 168 |
+
onMouseLeave={(e) => e.target.style.background = 'none'}
|
| 169 |
+
>
|
| 170 |
+
👥 Community
|
| 171 |
+
</button>
|
| 172 |
+
|
| 173 |
+
{currentUser.isGuest ? (
|
| 174 |
+
<button
|
| 175 |
+
onClick={() => {
|
| 176 |
+
setShowUserMenu(false);
|
| 177 |
+
onNavigate && onNavigate('Login');
|
| 178 |
+
}}
|
| 179 |
+
style={{
|
| 180 |
+
width: '100%',
|
| 181 |
+
padding: '12px 20px',
|
| 182 |
+
background: 'none',
|
| 183 |
+
border: 'none',
|
| 184 |
+
textAlign: 'left',
|
| 185 |
+
cursor: 'pointer',
|
| 186 |
+
color: '#27ae60',
|
| 187 |
+
display: 'flex',
|
| 188 |
+
alignItems: 'center',
|
| 189 |
+
gap: '10px',
|
| 190 |
+
fontSize: '14px',
|
| 191 |
+
fontWeight: '500'
|
| 192 |
+
}}
|
| 193 |
+
onMouseEnter={(e) => e.target.style.background = '#f8f9fa'}
|
| 194 |
+
onMouseLeave={(e) => e.target.style.background = 'none'}
|
| 195 |
+
>
|
| 196 |
+
🚀 Create Account
|
| 197 |
+
</button>
|
| 198 |
+
) : (
|
| 199 |
+
<button
|
| 200 |
+
onClick={handleLogout}
|
| 201 |
+
disabled={isLoggingOut}
|
| 202 |
+
style={{
|
| 203 |
+
width: '100%',
|
| 204 |
+
padding: '12px 20px',
|
| 205 |
+
background: 'none',
|
| 206 |
+
border: 'none',
|
| 207 |
+
textAlign: 'left',
|
| 208 |
+
cursor: isLoggingOut ? 'not-allowed' : 'pointer',
|
| 209 |
+
color: '#e74c3c',
|
| 210 |
+
display: 'flex',
|
| 211 |
+
alignItems: 'center',
|
| 212 |
+
gap: '10px',
|
| 213 |
+
fontSize: '14px',
|
| 214 |
+
opacity: isLoggingOut ? 0.6 : 1
|
| 215 |
+
}}
|
| 216 |
+
onMouseEnter={(e) => !isLoggingOut && (e.target.style.background = '#f8f9fa')}
|
| 217 |
+
onMouseLeave={(e) => e.target.style.background = 'none'}
|
| 218 |
+
>
|
| 219 |
+
{isLoggingOut ? '⏳ Logging out...' : '🚪 Logout'}
|
| 220 |
+
</button>
|
| 221 |
+
)}
|
| 222 |
+
</div>
|
| 223 |
+
</div>
|
| 224 |
+
)}
|
| 225 |
+
</div>
|
| 226 |
+
) : (
|
| 227 |
+
<button
|
| 228 |
+
onClick={() => onNavigate && onNavigate('Login')}
|
| 229 |
+
style={{
|
| 230 |
+
padding: '10px 20px',
|
| 231 |
+
background: 'linear-gradient(135deg, #27ae60 0%, #2ecc71 100%)',
|
| 232 |
+
color: 'white',
|
| 233 |
+
border: 'none',
|
| 234 |
+
borderRadius: '20px',
|
| 235 |
+
cursor: 'pointer',
|
| 236 |
+
fontWeight: '500',
|
| 237 |
+
fontSize: '14px'
|
| 238 |
+
}}
|
| 239 |
+
>
|
| 240 |
+
🚀 Sign In
|
| 241 |
+
</button>
|
| 242 |
+
)}
|
| 243 |
+
</div>
|
| 244 |
+
|
| 245 |
+
{/* Click outside to close menu */}
|
| 246 |
+
{showUserMenu && (
|
| 247 |
+
<div
|
| 248 |
+
onClick={() => setShowUserMenu(false)}
|
| 249 |
+
style={{
|
| 250 |
+
position: 'fixed',
|
| 251 |
+
top: 0,
|
| 252 |
+
left: 0,
|
| 253 |
+
right: 0,
|
| 254 |
+
bottom: 0,
|
| 255 |
+
zIndex: 999
|
| 256 |
+
}}
|
| 257 |
+
/>
|
| 258 |
+
)}
|
| 259 |
+
</header>
|
| 260 |
+
);
|
| 261 |
+
};
|
| 262 |
+
|
| 263 |
+
export default Header;
|
web/src/components/LoadingScreen.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
const LoadingScreen = () => {
|
| 4 |
+
return (
|
| 5 |
+
<div style={{
|
| 6 |
+
position: 'fixed',
|
| 7 |
+
top: 0,
|
| 8 |
+
left: 0,
|
| 9 |
+
width: '100vw',
|
| 10 |
+
height: '100vh',
|
| 11 |
+
background: 'linear-gradient(135deg, #1B5E20 0%, #2E7D32 50%, #4CAF50 100%)',
|
| 12 |
+
display: 'flex',
|
| 13 |
+
flexDirection: 'column',
|
| 14 |
+
justifyContent: 'center',
|
| 15 |
+
alignItems: 'center',
|
| 16 |
+
zIndex: 9999
|
| 17 |
+
}}>
|
| 18 |
+
{/* EcoSpire Logo */}
|
| 19 |
+
<div style={{
|
| 20 |
+
textAlign: 'center',
|
| 21 |
+
marginBottom: '40px'
|
| 22 |
+
}}>
|
| 23 |
+
<div style={{
|
| 24 |
+
fontSize: '6rem',
|
| 25 |
+
marginBottom: '20px',
|
| 26 |
+
animation: 'pulse 2s ease-in-out infinite'
|
| 27 |
+
}}>
|
| 28 |
+
🌱
|
| 29 |
+
</div>
|
| 30 |
+
<h1 style={{
|
| 31 |
+
fontSize: '4rem',
|
| 32 |
+
color: 'white',
|
| 33 |
+
margin: 0,
|
| 34 |
+
fontWeight: 'bold',
|
| 35 |
+
textShadow: '2px 2px 4px rgba(0,0,0,0.3)',
|
| 36 |
+
animation: 'fadeInUp 1s ease-out'
|
| 37 |
+
}}>
|
| 38 |
+
GreenPlus by GXS
|
| 39 |
+
</h1>
|
| 40 |
+
<p style={{
|
| 41 |
+
fontSize: '1.5rem',
|
| 42 |
+
color: '#E8F5E9',
|
| 43 |
+
margin: '10px 0 0 0',
|
| 44 |
+
fontWeight: '300',
|
| 45 |
+
animation: 'fadeInUp 1s ease-out 0.3s both'
|
| 46 |
+
}}>
|
| 47 |
+
AI-Powered Environmental Intelligence
|
| 48 |
+
</p>
|
| 49 |
+
</div>
|
| 50 |
+
|
| 51 |
+
{/* Loading Animation */}
|
| 52 |
+
<div style={{
|
| 53 |
+
display: 'flex',
|
| 54 |
+
gap: '8px',
|
| 55 |
+
marginTop: '20px'
|
| 56 |
+
}}>
|
| 57 |
+
<div style={{
|
| 58 |
+
width: '12px',
|
| 59 |
+
height: '12px',
|
| 60 |
+
borderRadius: '50%',
|
| 61 |
+
background: 'white',
|
| 62 |
+
animation: 'bounce 1.4s ease-in-out infinite both',
|
| 63 |
+
animationDelay: '0s'
|
| 64 |
+
}}></div>
|
| 65 |
+
<div style={{
|
| 66 |
+
width: '12px',
|
| 67 |
+
height: '12px',
|
| 68 |
+
borderRadius: '50%',
|
| 69 |
+
background: 'white',
|
| 70 |
+
animation: 'bounce 1.4s ease-in-out infinite both',
|
| 71 |
+
animationDelay: '0.16s'
|
| 72 |
+
}}></div>
|
| 73 |
+
<div style={{
|
| 74 |
+
width: '12px',
|
| 75 |
+
height: '12px',
|
| 76 |
+
borderRadius: '50%',
|
| 77 |
+
background: 'white',
|
| 78 |
+
animation: 'bounce 1.4s ease-in-out infinite both',
|
| 79 |
+
animationDelay: '0.32s'
|
| 80 |
+
}}></div>
|
| 81 |
+
</div>
|
| 82 |
+
|
| 83 |
+
{/* Loading Text */}
|
| 84 |
+
<p style={{
|
| 85 |
+
color: 'white',
|
| 86 |
+
fontSize: '1.1rem',
|
| 87 |
+
marginTop: '30px',
|
| 88 |
+
opacity: 0.9,
|
| 89 |
+
animation: 'fadeIn 2s ease-in-out'
|
| 90 |
+
}}>
|
| 91 |
+
Initializing Environmental Tools...
|
| 92 |
+
</p>
|
| 93 |
+
|
| 94 |
+
<style jsx>{`
|
| 95 |
+
@keyframes pulse {
|
| 96 |
+
0%, 100% {
|
| 97 |
+
transform: scale(1);
|
| 98 |
+
}
|
| 99 |
+
50% {
|
| 100 |
+
transform: scale(1.1);
|
| 101 |
+
}
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
@keyframes fadeInUp {
|
| 105 |
+
from {
|
| 106 |
+
opacity: 0;
|
| 107 |
+
transform: translateY(30px);
|
| 108 |
+
}
|
| 109 |
+
to {
|
| 110 |
+
opacity: 1;
|
| 111 |
+
transform: translateY(0);
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
@keyframes fadeIn {
|
| 116 |
+
from {
|
| 117 |
+
opacity: 0;
|
| 118 |
+
}
|
| 119 |
+
to {
|
| 120 |
+
opacity: 1;
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
@keyframes bounce {
|
| 125 |
+
0%, 80%, 100% {
|
| 126 |
+
transform: scale(0);
|
| 127 |
+
}
|
| 128 |
+
40% {
|
| 129 |
+
transform: scale(1);
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
`}</style>
|
| 133 |
+
</div>
|
| 134 |
+
);
|
| 135 |
+
};
|
| 136 |
+
|
| 137 |
+
export default LoadingScreen;
|
web/src/components/Navigation.css
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.navigation {
|
| 2 |
+
width: 280px;
|
| 3 |
+
background: white;
|
| 4 |
+
box-shadow: 2px 0 8px rgba(0,0,0,0.1);
|
| 5 |
+
padding: 30px 0;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
.nav-list {
|
| 9 |
+
list-style: none;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
.nav-item {
|
| 13 |
+
margin: 8px 0;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
.nav-button {
|
| 17 |
+
width: 100%;
|
| 18 |
+
padding: 16px 30px;
|
| 19 |
+
border: none;
|
| 20 |
+
background: none;
|
| 21 |
+
text-align: left;
|
| 22 |
+
font-size: 1rem;
|
| 23 |
+
cursor: pointer;
|
| 24 |
+
transition: all 0.3s ease;
|
| 25 |
+
border-left: 4px solid transparent;
|
| 26 |
+
display: flex;
|
| 27 |
+
align-items: center;
|
| 28 |
+
gap: 12px;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
.nav-button:hover {
|
| 32 |
+
background: #f5f5f5;
|
| 33 |
+
border-left-color: #2E7D32;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.nav-item.active .nav-button {
|
| 37 |
+
background: #e8f5e8;
|
| 38 |
+
border-left-color: #2E7D32;
|
| 39 |
+
color: #2E7D32;
|
| 40 |
+
font-weight: 600;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
.nav-icon {
|
| 44 |
+
font-size: 1.2rem;
|
| 45 |
+
width: 24px;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
.nav-label {
|
| 49 |
+
flex: 1;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
@media (max-width: 768px) {
|
| 53 |
+
.navigation {
|
| 54 |
+
width: 100%;
|
| 55 |
+
padding: 20px 0;
|
| 56 |
+
order: 2;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.nav-list {
|
| 60 |
+
display: flex;
|
| 61 |
+
overflow-x: auto;
|
| 62 |
+
padding: 0 15px;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.nav-item {
|
| 66 |
+
flex-shrink: 0;
|
| 67 |
+
margin: 0 4px;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
.nav-button {
|
| 71 |
+
padding: 12px 16px;
|
| 72 |
+
border-left: none;
|
| 73 |
+
border-bottom: 3px solid transparent;
|
| 74 |
+
flex-direction: column;
|
| 75 |
+
gap: 4px;
|
| 76 |
+
min-width: 80px;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.nav-button:hover {
|
| 80 |
+
border-left: none;
|
| 81 |
+
border-bottom-color: #2E7D32;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.nav-item.active .nav-button {
|
| 85 |
+
border-left: none;
|
| 86 |
+
border-bottom-color: #2E7D32;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.nav-label {
|
| 90 |
+
font-size: 0.8rem;
|
| 91 |
+
}
|
| 92 |
+
}
|
web/src/components/layout/Header.css
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Header Component Styles */
|
| 2 |
+
.terra-header {
|
| 3 |
+
height: 80px;
|
| 4 |
+
background: rgba(255, 255, 255, 0.95);
|
| 5 |
+
backdrop-filter: blur(20px);
|
| 6 |
+
border-bottom: 1px solid rgba(74, 124, 35, 0.1);
|
| 7 |
+
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.05);
|
| 8 |
+
display: flex;
|
| 9 |
+
align-items: center;
|
| 10 |
+
justify-content: space-between;
|
| 11 |
+
padding: 0 var(--spacing-xl);
|
| 12 |
+
position: sticky;
|
| 13 |
+
top: 0;
|
| 14 |
+
z-index: var(--z-sticky);
|
| 15 |
+
transition: all var(--transition-normal);
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
.terra-header__left {
|
| 19 |
+
display: flex;
|
| 20 |
+
align-items: center;
|
| 21 |
+
gap: var(--spacing-lg);
|
| 22 |
+
flex: 1;
|
| 23 |
+
min-width: 0;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.terra-header__right {
|
| 27 |
+
display: flex;
|
| 28 |
+
align-items: center;
|
| 29 |
+
gap: var(--spacing-lg);
|
| 30 |
+
flex-shrink: 0;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/* Mobile Menu Button */
|
| 34 |
+
.terra-header__menu-btn {
|
| 35 |
+
display: none;
|
| 36 |
+
background: none;
|
| 37 |
+
border: none;
|
| 38 |
+
cursor: pointer;
|
| 39 |
+
padding: var(--spacing-sm);
|
| 40 |
+
border-radius: var(--radius-sm);
|
| 41 |
+
transition: all var(--transition-normal);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.terra-header__menu-btn:hover {
|
| 45 |
+
background: rgba(74, 124, 35, 0.1);
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
.terra-header__menu-btn:focus {
|
| 49 |
+
outline: none;
|
| 50 |
+
box-shadow: 0 0 0 2px rgba(74, 124, 35, 0.3);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.terra-header__menu-icon {
|
| 54 |
+
display: flex;
|
| 55 |
+
flex-direction: column;
|
| 56 |
+
width: 24px;
|
| 57 |
+
height: 18px;
|
| 58 |
+
justify-content: space-between;
|
| 59 |
+
transition: all var(--transition-normal);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.terra-header__menu-icon span {
|
| 63 |
+
display: block;
|
| 64 |
+
height: 2px;
|
| 65 |
+
width: 100%;
|
| 66 |
+
background: var(--color-forest-primary);
|
| 67 |
+
border-radius: 1px;
|
| 68 |
+
transition: all var(--transition-normal);
|
| 69 |
+
transform-origin: center;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.terra-header__menu-icon--open span:nth-child(1) {
|
| 73 |
+
transform: rotate(45deg) translate(6px, 6px);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.terra-header__menu-icon--open span:nth-child(2) {
|
| 77 |
+
opacity: 0;
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.terra-header__menu-icon--open span:nth-child(3) {
|
| 81 |
+
transform: rotate(-45deg) translate(6px, -6px);
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
/* Page Info */
|
| 85 |
+
.terra-header__page-info {
|
| 86 |
+
display: flex;
|
| 87 |
+
flex-direction: column;
|
| 88 |
+
gap: var(--spacing-xs);
|
| 89 |
+
min-width: 0;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.terra-header__page-title {
|
| 93 |
+
display: flex;
|
| 94 |
+
align-items: center;
|
| 95 |
+
gap: var(--spacing-md);
|
| 96 |
+
margin: 0;
|
| 97 |
+
color: var(--color-forest-primary);
|
| 98 |
+
font-size: var(--font-size-2xl);
|
| 99 |
+
font-weight: var(--font-weight-bold);
|
| 100 |
+
line-height: 1.2;
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
.terra-header__page-icon {
|
| 104 |
+
font-size: 2.2rem;
|
| 105 |
+
flex-shrink: 0;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.terra-header__page-name {
|
| 109 |
+
white-space: nowrap;
|
| 110 |
+
overflow: hidden;
|
| 111 |
+
text-overflow: ellipsis;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.terra-header__page-subtitle {
|
| 115 |
+
margin: 0;
|
| 116 |
+
color: var(--color-nature-green);
|
| 117 |
+
font-size: var(--font-size-base);
|
| 118 |
+
font-weight: var(--font-weight-medium);
|
| 119 |
+
white-space: nowrap;
|
| 120 |
+
overflow: hidden;
|
| 121 |
+
text-overflow: ellipsis;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
/* Environmental Stats */
|
| 125 |
+
.terra-header__stats {
|
| 126 |
+
display: flex;
|
| 127 |
+
gap: var(--spacing-lg);
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.terra-header__stat {
|
| 131 |
+
display: flex;
|
| 132 |
+
align-items: center;
|
| 133 |
+
gap: var(--spacing-md);
|
| 134 |
+
background: rgba(76, 175, 80, 0.1);
|
| 135 |
+
padding: var(--spacing-md) var(--spacing-lg);
|
| 136 |
+
border-radius: var(--radius-md);
|
| 137 |
+
border: 1px solid rgba(76, 175, 80, 0.2);
|
| 138 |
+
transition: all var(--transition-normal);
|
| 139 |
+
cursor: pointer;
|
| 140 |
+
min-width: 120px;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.terra-header__stat:hover {
|
| 144 |
+
background: rgba(76, 175, 80, 0.15);
|
| 145 |
+
transform: translateY(-2px);
|
| 146 |
+
box-shadow: var(--shadow-md);
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.terra-header__stat-icon {
|
| 150 |
+
font-size: 1.6rem;
|
| 151 |
+
flex-shrink: 0;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.terra-header__stat-content {
|
| 155 |
+
display: flex;
|
| 156 |
+
flex-direction: column;
|
| 157 |
+
gap: var(--spacing-xs);
|
| 158 |
+
min-width: 0;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.terra-header__stat-value {
|
| 162 |
+
font-weight: var(--font-weight-bold);
|
| 163 |
+
font-size: var(--font-size-lg);
|
| 164 |
+
color: var(--color-forest-primary);
|
| 165 |
+
line-height: 1;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.terra-header__stat-label {
|
| 169 |
+
font-size: var(--font-size-xs);
|
| 170 |
+
color: var(--color-nature-green);
|
| 171 |
+
font-weight: var(--font-weight-medium);
|
| 172 |
+
white-space: nowrap;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
/* User Actions */
|
| 176 |
+
.terra-header__actions {
|
| 177 |
+
display: flex;
|
| 178 |
+
align-items: center;
|
| 179 |
+
gap: var(--spacing-lg);
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
/* Notifications */
|
| 183 |
+
.terra-header__notifications {
|
| 184 |
+
position: relative;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.terra-header__notification-btn {
|
| 188 |
+
background: rgba(74, 124, 35, 0.1);
|
| 189 |
+
border: 1px solid rgba(74, 124, 35, 0.2);
|
| 190 |
+
border-radius: var(--radius-full);
|
| 191 |
+
width: 48px;
|
| 192 |
+
height: 48px;
|
| 193 |
+
display: flex;
|
| 194 |
+
align-items: center;
|
| 195 |
+
justify-content: center;
|
| 196 |
+
cursor: pointer;
|
| 197 |
+
transition: all var(--transition-normal);
|
| 198 |
+
position: relative;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
.terra-header__notification-btn:hover {
|
| 202 |
+
background: rgba(74, 124, 35, 0.15);
|
| 203 |
+
transform: scale(1.05);
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.terra-header__notification-btn:focus {
|
| 207 |
+
outline: none;
|
| 208 |
+
box-shadow: 0 0 0 2px rgba(74, 124, 35, 0.3);
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
.terra-header__notification-icon {
|
| 212 |
+
font-size: 1.4rem;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.terra-header__notification-badge {
|
| 216 |
+
position: absolute;
|
| 217 |
+
top: -4px;
|
| 218 |
+
right: -4px;
|
| 219 |
+
background: var(--color-warning-red);
|
| 220 |
+
color: var(--color-white);
|
| 221 |
+
font-size: var(--font-size-xs);
|
| 222 |
+
font-weight: var(--font-weight-bold);
|
| 223 |
+
padding: 2px 6px;
|
| 224 |
+
border-radius: var(--radius-full);
|
| 225 |
+
min-width: 18px;
|
| 226 |
+
height: 18px;
|
| 227 |
+
display: flex;
|
| 228 |
+
align-items: center;
|
| 229 |
+
justify-content: center;
|
| 230 |
+
animation: pulse 2s infinite;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
/* User Profile */
|
| 234 |
+
.terra-header__user {
|
| 235 |
+
display: flex;
|
| 236 |
+
align-items: center;
|
| 237 |
+
gap: var(--spacing-md);
|
| 238 |
+
padding: var(--spacing-sm) var(--spacing-md);
|
| 239 |
+
background: rgba(74, 124, 35, 0.05);
|
| 240 |
+
border-radius: var(--radius-xl);
|
| 241 |
+
border: 1px solid rgba(74, 124, 35, 0.1);
|
| 242 |
+
cursor: pointer;
|
| 243 |
+
transition: all var(--transition-normal);
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.terra-header__user:hover {
|
| 247 |
+
background: rgba(74, 124, 35, 0.1);
|
| 248 |
+
transform: translateY(-1px);
|
| 249 |
+
box-shadow: var(--shadow-sm);
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
.terra-header__user-info {
|
| 253 |
+
display: flex;
|
| 254 |
+
flex-direction: column;
|
| 255 |
+
gap: var(--spacing-xs);
|
| 256 |
+
text-align: right;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
.terra-header__user-greeting {
|
| 260 |
+
font-size: var(--font-size-xs);
|
| 261 |
+
color: var(--color-gray-500);
|
| 262 |
+
font-weight: var(--font-weight-medium);
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
.terra-header__user-name {
|
| 266 |
+
font-size: var(--font-size-sm);
|
| 267 |
+
color: var(--color-forest-primary);
|
| 268 |
+
font-weight: var(--font-weight-semibold);
|
| 269 |
+
white-space: nowrap;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.terra-header__user-avatar {
|
| 273 |
+
width: 40px;
|
| 274 |
+
height: 40px;
|
| 275 |
+
background: linear-gradient(135deg, var(--color-nature-green) 0%, var(--color-sage-green) 100%);
|
| 276 |
+
border-radius: var(--radius-full);
|
| 277 |
+
display: flex;
|
| 278 |
+
align-items: center;
|
| 279 |
+
justify-content: center;
|
| 280 |
+
box-shadow: var(--shadow-sm);
|
| 281 |
+
flex-shrink: 0;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
.terra-header__avatar-icon {
|
| 285 |
+
font-size: 1.2rem;
|
| 286 |
+
color: var(--color-white);
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
/* Responsive Design */
|
| 290 |
+
@media (max-width: 1200px) {
|
| 291 |
+
.terra-header__stats {
|
| 292 |
+
gap: var(--spacing-md);
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
.terra-header__stat {
|
| 296 |
+
padding: var(--spacing-sm) var(--spacing-md);
|
| 297 |
+
min-width: 100px;
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
.terra-header__stat-value {
|
| 301 |
+
font-size: var(--font-size-base);
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
@media (max-width: 1024px) {
|
| 306 |
+
.terra-header {
|
| 307 |
+
padding: 0 var(--spacing-lg);
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
.terra-header__menu-btn {
|
| 311 |
+
display: flex;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
.terra-header__stats {
|
| 315 |
+
display: none;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
.terra-header__user-info {
|
| 319 |
+
display: none;
|
| 320 |
+
}
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
@media (max-width: 768px) {
|
| 324 |
+
.terra-header {
|
| 325 |
+
padding: 0 var(--spacing-md);
|
| 326 |
+
height: 70px;
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
.terra-header__page-title {
|
| 330 |
+
font-size: var(--font-size-xl);
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.terra-header__page-icon {
|
| 334 |
+
font-size: 1.8rem;
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
.terra-header__page-subtitle {
|
| 338 |
+
font-size: var(--font-size-sm);
|
| 339 |
+
}
|
| 340 |
+
|
| 341 |
+
.terra-header__actions {
|
| 342 |
+
gap: var(--spacing-md);
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
.terra-header__notification-btn {
|
| 346 |
+
width: 40px;
|
| 347 |
+
height: 40px;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
.terra-header__user-avatar {
|
| 351 |
+
width: 36px;
|
| 352 |
+
height: 36px;
|
| 353 |
+
}
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
@media (max-width: 640px) {
|
| 357 |
+
.terra-header__left {
|
| 358 |
+
gap: var(--spacing-md);
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
.terra-header__page-info {
|
| 362 |
+
min-width: 0;
|
| 363 |
+
flex: 1;
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
.terra-header__page-title {
|
| 367 |
+
font-size: var(--font-size-lg);
|
| 368 |
+
gap: var(--spacing-sm);
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
.terra-header__page-icon {
|
| 372 |
+
font-size: 1.5rem;
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
.terra-header__page-subtitle {
|
| 376 |
+
display: none;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
.terra-header__actions {
|
| 380 |
+
gap: var(--spacing-sm);
|
| 381 |
+
}
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
/* High Contrast Mode */
|
| 385 |
+
@media (prefers-contrast: high) {
|
| 386 |
+
.terra-header {
|
| 387 |
+
border-bottom-width: 2px;
|
| 388 |
+
border-bottom-color: var(--color-forest-primary);
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
.terra-header__stat {
|
| 392 |
+
border-width: 2px;
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
.terra-header__notification-btn,
|
| 396 |
+
.terra-header__user {
|
| 397 |
+
border-width: 2px;
|
| 398 |
+
}
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
/* Reduced Motion */
|
| 402 |
+
@media (prefers-reduced-motion: reduce) {
|
| 403 |
+
.terra-header,
|
| 404 |
+
.terra-header__menu-icon,
|
| 405 |
+
.terra-header__menu-icon span,
|
| 406 |
+
.terra-header__stat,
|
| 407 |
+
.terra-header__notification-btn,
|
| 408 |
+
.terra-header__user {
|
| 409 |
+
transition: none;
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
.terra-header__notification-badge {
|
| 413 |
+
animation: none;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
.terra-header__stat:hover,
|
| 417 |
+
.terra-header__notification-btn:hover,
|
| 418 |
+
.terra-header__user:hover {
|
| 419 |
+
transform: none;
|
| 420 |
+
}
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
/* Focus Management */
|
| 424 |
+
.terra-header__notification-btn:focus-visible,
|
| 425 |
+
.terra-header__user:focus-visible {
|
| 426 |
+
outline: 2px solid var(--color-nature-green);
|
| 427 |
+
outline-offset: 2px;
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
/* Loading State */
|
| 431 |
+
.terra-header--loading .terra-header__stat {
|
| 432 |
+
opacity: 0.6;
|
| 433 |
+
pointer-events: none;
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
.terra-header--loading .terra-header__stat-value {
|
| 437 |
+
background: var(--color-gray-200);
|
| 438 |
+
color: transparent;
|
| 439 |
+
border-radius: var(--radius-sm);
|
| 440 |
+
animation: shimmer 1.5s infinite;
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
@keyframes shimmer {
|
| 444 |
+
0% {
|
| 445 |
+
background-position: -200px 0;
|
| 446 |
+
}
|
| 447 |
+
100% {
|
| 448 |
+
background-position: calc(200px + 100%) 0;
|
| 449 |
+
}
|
| 450 |
+
}
|
| 451 |
+
/* Refr
|
| 452 |
+
esh Button */
|
| 453 |
+
.terra-header__refresh-btn {
|
| 454 |
+
background: rgba(255, 255, 255, 0.1);
|
| 455 |
+
backdrop-filter: blur(10px);
|
| 456 |
+
border: 2px solid rgba(255, 255, 255, 0.3);
|
| 457 |
+
border-radius: 50%;
|
| 458 |
+
width: 40px;
|
| 459 |
+
height: 40px;
|
| 460 |
+
display: flex;
|
| 461 |
+
align-items: center;
|
| 462 |
+
justify-content: center;
|
| 463 |
+
color: white;
|
| 464 |
+
font-size: 16px;
|
| 465 |
+
cursor: pointer;
|
| 466 |
+
transition: all 0.2s ease;
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
.terra-header__refresh-btn:hover {
|
| 470 |
+
background: rgba(255, 255, 255, 0.2);
|
| 471 |
+
border-color: rgba(255, 255, 255, 0.6);
|
| 472 |
+
transform: rotate(180deg);
|
| 473 |
+
}
|
web/src/components/layout/Header.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
import './Header.css';
|
| 3 |
+
import ProfileDropdown from '../ui/ProfileDropdown';
|
| 4 |
+
|
| 5 |
+
const Header = ({
|
| 6 |
+
currentPage,
|
| 7 |
+
sidebarOpen,
|
| 8 |
+
onSidebarToggle,
|
| 9 |
+
userStats = {},
|
| 10 |
+
notifications = [],
|
| 11 |
+
isGuest = false,
|
| 12 |
+
user = null,
|
| 13 |
+
onLogin,
|
| 14 |
+
onLogout,
|
| 15 |
+
onProfile
|
| 16 |
+
}) => {
|
| 17 |
+
const [showRefresh, setShowRefresh] = useState(false);
|
| 18 |
+
const formatNumber = (num) => {
|
| 19 |
+
if (num >= 1000000) {
|
| 20 |
+
return (num / 1000000).toFixed(1) + 'M';
|
| 21 |
+
}
|
| 22 |
+
if (num >= 1000) {
|
| 23 |
+
return (num / 1000).toFixed(1) + 'K';
|
| 24 |
+
}
|
| 25 |
+
return num?.toString() || '0';
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
const getTimeGreeting = () => {
|
| 29 |
+
const hour = new Date().getHours();
|
| 30 |
+
if (hour < 12) return 'Good morning';
|
| 31 |
+
if (hour < 17) return 'Good afternoon';
|
| 32 |
+
return 'Good evening';
|
| 33 |
+
};
|
| 34 |
+
|
| 35 |
+
return (
|
| 36 |
+
<header className="terra-header">
|
| 37 |
+
<div className="terra-header__left">
|
| 38 |
+
{/* Mobile menu button */}
|
| 39 |
+
<button
|
| 40 |
+
className="terra-header__menu-btn"
|
| 41 |
+
onClick={onSidebarToggle}
|
| 42 |
+
aria-label="Toggle navigation menu"
|
| 43 |
+
>
|
| 44 |
+
<span className={`terra-header__menu-icon ${sidebarOpen ? 'terra-header__menu-icon--open' : ''}`}>
|
| 45 |
+
<span></span>
|
| 46 |
+
<span></span>
|
| 47 |
+
<span></span>
|
| 48 |
+
</span>
|
| 49 |
+
</button>
|
| 50 |
+
|
| 51 |
+
{/* Page info */}
|
| 52 |
+
<div className="terra-header__page-info">
|
| 53 |
+
<h1 className="terra-header__page-title">
|
| 54 |
+
<span className="terra-header__page-icon">{currentPage?.icon}</span>
|
| 55 |
+
<span className="terra-header__page-name">{currentPage?.name}</span>
|
| 56 |
+
</h1>
|
| 57 |
+
<p className="terra-header__page-subtitle">{currentPage?.description}</p>
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
|
| 61 |
+
<div className="terra-header__right">
|
| 62 |
+
{/* Environmental Stats */}
|
| 63 |
+
<div className="terra-header__stats">
|
| 64 |
+
<div className="terra-header__stat" title="CO₂ Saved">
|
| 65 |
+
<span className="terra-header__stat-icon">🌿</span>
|
| 66 |
+
<div className="terra-header__stat-content">
|
| 67 |
+
<div className="terra-header__stat-value">
|
| 68 |
+
{formatNumber(userStats.co2Saved || (isGuest ? 0 : 0))}
|
| 69 |
+
</div>
|
| 70 |
+
<div className="terra-header__stat-label">CO₂ Saved (kg)</div>
|
| 71 |
+
</div>
|
| 72 |
+
</div>
|
| 73 |
+
|
| 74 |
+
<div className="terra-header__stat" title="Water Tests Completed">
|
| 75 |
+
<span className="terra-header__stat-icon">💧</span>
|
| 76 |
+
<div className="terra-header__stat-content">
|
| 77 |
+
<div className="terra-header__stat-value">
|
| 78 |
+
{formatNumber(userStats.waterTests || (isGuest ? 0 : 0))}
|
| 79 |
+
</div>
|
| 80 |
+
<div className="terra-header__stat-label">Water Tests</div>
|
| 81 |
+
</div>
|
| 82 |
+
</div>
|
| 83 |
+
|
| 84 |
+
<div className="terra-header__stat" title="Biodiversity Scans">
|
| 85 |
+
<span className="terra-header__stat-icon">🦜</span>
|
| 86 |
+
<div className="terra-header__stat-content">
|
| 87 |
+
<div className="terra-header__stat-value">
|
| 88 |
+
{formatNumber(userStats.bioScans || (isGuest ? 0 : 0))}
|
| 89 |
+
</div>
|
| 90 |
+
<div className="terra-header__stat-label">Bio Scans</div>
|
| 91 |
+
</div>
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
|
| 95 |
+
{/* User Actions */}
|
| 96 |
+
<div className="terra-header__actions">
|
| 97 |
+
{/* Notifications */}
|
| 98 |
+
<div className="terra-header__notifications">
|
| 99 |
+
<button
|
| 100 |
+
className="terra-header__notification-btn"
|
| 101 |
+
aria-label="View notifications"
|
| 102 |
+
>
|
| 103 |
+
<span className="terra-header__notification-icon">🔔</span>
|
| 104 |
+
{notifications.length > 0 && (
|
| 105 |
+
<span className="terra-header__notification-badge">
|
| 106 |
+
{notifications.length > 9 ? '9+' : notifications.length}
|
| 107 |
+
</span>
|
| 108 |
+
)}
|
| 109 |
+
</button>
|
| 110 |
+
</div>
|
| 111 |
+
|
| 112 |
+
{/* Refresh Button (when needed) */}
|
| 113 |
+
{showRefresh && (
|
| 114 |
+
<button
|
| 115 |
+
className="terra-header__refresh-btn"
|
| 116 |
+
onClick={() => window.location.reload()}
|
| 117 |
+
title="Refresh page"
|
| 118 |
+
>
|
| 119 |
+
🔄
|
| 120 |
+
</button>
|
| 121 |
+
)}
|
| 122 |
+
|
| 123 |
+
{/* Profile Dropdown */}
|
| 124 |
+
<ProfileDropdown
|
| 125 |
+
user={user}
|
| 126 |
+
onLogin={onLogin}
|
| 127 |
+
onLogout={onLogout}
|
| 128 |
+
onProfile={onProfile}
|
| 129 |
+
/>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
</header>
|
| 133 |
+
);
|
| 134 |
+
};
|
| 135 |
+
|
| 136 |
+
export default Header;
|
web/src/components/layout/Sidebar.css
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Sidebar Component Styles */
|
| 2 |
+
.terra-sidebar {
|
| 3 |
+
position: fixed;
|
| 4 |
+
left: 0;
|
| 5 |
+
top: 0;
|
| 6 |
+
height: 100vh;
|
| 7 |
+
background: #2E7D32;
|
| 8 |
+
color: white;
|
| 9 |
+
transition: width 0.3s ease;
|
| 10 |
+
display: flex;
|
| 11 |
+
flex-direction: column;
|
| 12 |
+
z-index: 1000;
|
| 13 |
+
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
|
| 14 |
+
overflow: hidden;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
.terra-sidebar--open {
|
| 18 |
+
width: 280px;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.terra-sidebar--closed {
|
| 22 |
+
width: 70px;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
/* Header Section */
|
| 26 |
+
.terra-sidebar__header {
|
| 27 |
+
padding: 20px;
|
| 28 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
| 29 |
+
display: flex;
|
| 30 |
+
align-items: center;
|
| 31 |
+
justify-content: space-between;
|
| 32 |
+
min-height: 80px;
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
.terra-sidebar__logo {
|
| 36 |
+
display: flex;
|
| 37 |
+
align-items: center;
|
| 38 |
+
gap: var(--spacing-md);
|
| 39 |
+
flex: 1;
|
| 40 |
+
min-width: 0;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
.terra-sidebar__logo-icon {
|
| 44 |
+
font-size: 2.5rem;
|
| 45 |
+
animation: gentle-pulse 3s ease-in-out infinite;
|
| 46 |
+
flex-shrink: 0;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
@keyframes gentle-pulse {
|
| 50 |
+
0%, 100% {
|
| 51 |
+
transform: scale(1);
|
| 52 |
+
}
|
| 53 |
+
50% {
|
| 54 |
+
transform: scale(1.05);
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.terra-sidebar__logo-text {
|
| 59 |
+
min-width: 0;
|
| 60 |
+
opacity: 1;
|
| 61 |
+
transition: opacity var(--transition-normal);
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.terra-sidebar--closed .terra-sidebar__logo-text {
|
| 65 |
+
opacity: 0;
|
| 66 |
+
pointer-events: none;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.terra-sidebar__logo-title {
|
| 70 |
+
font-size: 1.4rem;
|
| 71 |
+
font-weight: 700;
|
| 72 |
+
margin: 0 0 4px 0;
|
| 73 |
+
color: white;
|
| 74 |
+
white-space: nowrap;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.terra-sidebar__logo-subtitle {
|
| 78 |
+
font-size: 0.8rem;
|
| 79 |
+
color: rgba(255, 255, 255, 0.8);
|
| 80 |
+
margin: 0;
|
| 81 |
+
white-space: nowrap;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
.terra-sidebar__toggle {
|
| 85 |
+
background: rgba(255, 255, 255, 0.2);
|
| 86 |
+
border: none;
|
| 87 |
+
color: white;
|
| 88 |
+
width: 32px;
|
| 89 |
+
height: 32px;
|
| 90 |
+
border-radius: 6px;
|
| 91 |
+
cursor: pointer;
|
| 92 |
+
display: flex;
|
| 93 |
+
align-items: center;
|
| 94 |
+
justify-content: center;
|
| 95 |
+
transition: all 0.3s ease;
|
| 96 |
+
font-size: 0.9rem;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.terra-sidebar__toggle:hover {
|
| 100 |
+
background: rgba(255, 255, 255, 0.25);
|
| 101 |
+
transform: scale(1.05);
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.terra-sidebar__toggle:focus {
|
| 105 |
+
outline: none;
|
| 106 |
+
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.3);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.terra-sidebar__toggle-icon {
|
| 110 |
+
transition: transform var(--transition-normal);
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
.terra-sidebar__toggle-icon--open {
|
| 114 |
+
transform: rotate(180deg);
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
/* Search Section */
|
| 118 |
+
.terra-sidebar__search {
|
| 119 |
+
padding: 0 var(--spacing-lg) var(--spacing-lg);
|
| 120 |
+
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.terra-sidebar__search-container {
|
| 124 |
+
position: relative;
|
| 125 |
+
display: flex;
|
| 126 |
+
align-items: center;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.terra-sidebar__search-input {
|
| 130 |
+
width: 100%;
|
| 131 |
+
padding: var(--spacing-sm) var(--spacing-md);
|
| 132 |
+
padding-right: 2.5rem;
|
| 133 |
+
background: rgba(255, 255, 255, 0.1);
|
| 134 |
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
| 135 |
+
border-radius: var(--radius-md);
|
| 136 |
+
color: var(--color-white);
|
| 137 |
+
font-size: var(--font-size-sm);
|
| 138 |
+
transition: all var(--transition-normal);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
.terra-sidebar__search-input::placeholder {
|
| 142 |
+
color: rgba(255, 255, 255, 0.6);
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
.terra-sidebar__search-input:focus {
|
| 146 |
+
outline: none;
|
| 147 |
+
background: rgba(255, 255, 255, 0.15);
|
| 148 |
+
border-color: var(--color-mint-light);
|
| 149 |
+
box-shadow: 0 0 0 2px rgba(168, 230, 163, 0.3);
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.terra-sidebar__search-icon {
|
| 153 |
+
position: absolute;
|
| 154 |
+
right: var(--spacing-sm);
|
| 155 |
+
color: rgba(255, 255, 255, 0.6);
|
| 156 |
+
font-size: var(--font-size-sm);
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.terra-sidebar__search-clear {
|
| 160 |
+
background: none;
|
| 161 |
+
border: none;
|
| 162 |
+
color: rgba(255, 255, 255, 0.6);
|
| 163 |
+
cursor: pointer;
|
| 164 |
+
padding: var(--spacing-xs);
|
| 165 |
+
border-radius: var(--radius-sm);
|
| 166 |
+
transition: all var(--transition-normal);
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.terra-sidebar__search-clear:hover {
|
| 170 |
+
color: var(--color-white);
|
| 171 |
+
background: rgba(255, 255, 255, 0.1);
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
/* Navigation Section */
|
| 175 |
+
.terra-sidebar__nav {
|
| 176 |
+
flex: 1;
|
| 177 |
+
overflow-y: auto;
|
| 178 |
+
padding: var(--spacing-md) 0;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
.terra-sidebar__nav::-webkit-scrollbar {
|
| 182 |
+
width: 4px;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.terra-sidebar__nav::-webkit-scrollbar-track {
|
| 186 |
+
background: transparent;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
.terra-sidebar__nav::-webkit-scrollbar-thumb {
|
| 190 |
+
background: rgba(255, 255, 255, 0.3);
|
| 191 |
+
border-radius: 2px;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.terra-sidebar__nav::-webkit-scrollbar-thumb:hover {
|
| 195 |
+
background: rgba(255, 255, 255, 0.5);
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.terra-sidebar__nav-content {
|
| 199 |
+
display: flex;
|
| 200 |
+
flex-direction: column;
|
| 201 |
+
gap: var(--spacing-lg);
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
.terra-sidebar__category {
|
| 205 |
+
display: flex;
|
| 206 |
+
flex-direction: column;
|
| 207 |
+
gap: var(--spacing-xs);
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.terra-sidebar__category-title {
|
| 211 |
+
font-size: var(--font-size-xs);
|
| 212 |
+
font-weight: var(--font-weight-semibold);
|
| 213 |
+
color: rgba(255, 255, 255, 0.7);
|
| 214 |
+
text-transform: uppercase;
|
| 215 |
+
letter-spacing: 0.5px;
|
| 216 |
+
margin: 0;
|
| 217 |
+
padding: 0 var(--spacing-lg);
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
.terra-sidebar__category-items {
|
| 221 |
+
display: flex;
|
| 222 |
+
flex-direction: column;
|
| 223 |
+
gap: var(--spacing-xs);
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
.terra-sidebar__nav-item {
|
| 227 |
+
display: flex;
|
| 228 |
+
align-items: center;
|
| 229 |
+
padding: 12px 16px;
|
| 230 |
+
margin: 2px 12px;
|
| 231 |
+
border-radius: 8px;
|
| 232 |
+
cursor: pointer;
|
| 233 |
+
transition: all 0.3s ease;
|
| 234 |
+
position: relative;
|
| 235 |
+
min-height: 48px;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
.terra-sidebar__nav-item:hover {
|
| 239 |
+
background: rgba(255, 255, 255, 0.1);
|
| 240 |
+
transform: translateX(4px);
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.terra-sidebar__nav-item:focus {
|
| 244 |
+
outline: none;
|
| 245 |
+
background: rgba(255, 255, 255, 0.15);
|
| 246 |
+
box-shadow: 0 0 0 2px rgba(168, 230, 163, 0.3);
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
.terra-sidebar__nav-item--active {
|
| 250 |
+
background: rgba(255, 255, 255, 0.2);
|
| 251 |
+
transform: translateX(4px);
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
.terra-sidebar__nav-item--active::before {
|
| 255 |
+
content: '';
|
| 256 |
+
position: absolute;
|
| 257 |
+
left: -12px;
|
| 258 |
+
top: 0;
|
| 259 |
+
bottom: 0;
|
| 260 |
+
width: 4px;
|
| 261 |
+
background: #4CAF50;
|
| 262 |
+
border-radius: 0 2px 2px 0;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
.terra-sidebar__nav-icon {
|
| 266 |
+
font-size: 1.5rem;
|
| 267 |
+
margin-right: var(--spacing-md);
|
| 268 |
+
min-width: 28px;
|
| 269 |
+
text-align: center;
|
| 270 |
+
flex-shrink: 0;
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
.terra-sidebar__nav-content {
|
| 274 |
+
flex: 1;
|
| 275 |
+
min-width: 0;
|
| 276 |
+
opacity: 1;
|
| 277 |
+
transition: opacity var(--transition-normal);
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
.terra-sidebar--closed .terra-sidebar__nav-content {
|
| 281 |
+
opacity: 0;
|
| 282 |
+
pointer-events: none;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
.terra-sidebar__nav-name {
|
| 286 |
+
font-weight: var(--font-weight-semibold);
|
| 287 |
+
font-size: var(--font-size-sm);
|
| 288 |
+
display: block;
|
| 289 |
+
margin-bottom: var(--spacing-xs);
|
| 290 |
+
white-space: nowrap;
|
| 291 |
+
overflow: hidden;
|
| 292 |
+
text-overflow: ellipsis;
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
.terra-sidebar__nav-desc {
|
| 296 |
+
font-size: var(--font-size-xs);
|
| 297 |
+
opacity: 0.8;
|
| 298 |
+
color: #b8e6b3;
|
| 299 |
+
white-space: nowrap;
|
| 300 |
+
overflow: hidden;
|
| 301 |
+
text-overflow: ellipsis;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
.terra-sidebar__nav-indicator {
|
| 305 |
+
position: absolute;
|
| 306 |
+
right: var(--spacing-md);
|
| 307 |
+
width: 6px;
|
| 308 |
+
height: 6px;
|
| 309 |
+
background: var(--color-mint-light);
|
| 310 |
+
border-radius: 50%;
|
| 311 |
+
animation: pulse 2s infinite;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
/* Footer Section */
|
| 315 |
+
.terra-sidebar__footer {
|
| 316 |
+
padding: var(--spacing-lg);
|
| 317 |
+
border-top: 1px solid rgba(255, 255, 255, 0.15);
|
| 318 |
+
display: flex;
|
| 319 |
+
flex-direction: column;
|
| 320 |
+
gap: var(--spacing-md);
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
.terra-sidebar__stats {
|
| 324 |
+
display: flex;
|
| 325 |
+
gap: var(--spacing-md);
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
.terra-sidebar__stat {
|
| 329 |
+
display: flex;
|
| 330 |
+
align-items: center;
|
| 331 |
+
gap: var(--spacing-sm);
|
| 332 |
+
flex: 1;
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
.terra-sidebar__stat-icon {
|
| 336 |
+
font-size: 1.3rem;
|
| 337 |
+
flex-shrink: 0;
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
.terra-sidebar__stat-content {
|
| 341 |
+
min-width: 0;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
.terra-sidebar__stat-value {
|
| 345 |
+
font-weight: var(--font-weight-bold);
|
| 346 |
+
font-size: var(--font-size-sm);
|
| 347 |
+
white-space: nowrap;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
.terra-sidebar__stat-label {
|
| 351 |
+
font-size: var(--font-size-xs);
|
| 352 |
+
opacity: 0.8;
|
| 353 |
+
white-space: nowrap;
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
.terra-sidebar__version {
|
| 357 |
+
text-align: center;
|
| 358 |
+
font-size: var(--font-size-xs);
|
| 359 |
+
opacity: 0.6;
|
| 360 |
+
color: #b8e6b3;
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
/* Tooltip for collapsed state */
|
| 364 |
+
.terra-sidebar__tooltip {
|
| 365 |
+
position: fixed;
|
| 366 |
+
left: 80px;
|
| 367 |
+
background: var(--color-gray-800);
|
| 368 |
+
color: var(--color-white);
|
| 369 |
+
padding: var(--spacing-sm) var(--spacing-md);
|
| 370 |
+
border-radius: var(--radius-sm);
|
| 371 |
+
font-size: var(--font-size-sm);
|
| 372 |
+
white-space: nowrap;
|
| 373 |
+
opacity: 0;
|
| 374 |
+
pointer-events: none;
|
| 375 |
+
transition: opacity var(--transition-normal);
|
| 376 |
+
z-index: var(--z-tooltip);
|
| 377 |
+
box-shadow: var(--shadow-lg);
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
.terra-sidebar__tooltip-arrow {
|
| 381 |
+
position: absolute;
|
| 382 |
+
left: -4px;
|
| 383 |
+
top: 50%;
|
| 384 |
+
transform: translateY(-50%);
|
| 385 |
+
width: 0;
|
| 386 |
+
height: 0;
|
| 387 |
+
border-top: 4px solid transparent;
|
| 388 |
+
border-bottom: 4px solid transparent;
|
| 389 |
+
border-right: 4px solid var(--color-gray-800);
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
/* Responsive Design */
|
| 393 |
+
@media (max-width: 1024px) {
|
| 394 |
+
.terra-sidebar {
|
| 395 |
+
transform: translateX(-100%);
|
| 396 |
+
transition: transform var(--transition-bounce);
|
| 397 |
+
border-radius: 0;
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
.terra-sidebar--open {
|
| 401 |
+
transform: translateX(0);
|
| 402 |
+
width: 280px;
|
| 403 |
+
}
|
| 404 |
+
|
| 405 |
+
.terra-sidebar--closed {
|
| 406 |
+
transform: translateX(-100%);
|
| 407 |
+
}
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
@media (max-width: 640px) {
|
| 411 |
+
.terra-sidebar--open {
|
| 412 |
+
width: 100vw;
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
.terra-sidebar__header {
|
| 416 |
+
padding: var(--spacing-md);
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
.terra-sidebar__search {
|
| 420 |
+
padding: 0 var(--spacing-md) var(--spacing-md);
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
.terra-sidebar__nav-item {
|
| 424 |
+
padding: var(--spacing-md);
|
| 425 |
+
margin: 0 var(--spacing-sm);
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
.terra-sidebar__footer {
|
| 429 |
+
padding: var(--spacing-md);
|
| 430 |
+
}
|
| 431 |
+
}
|
| 432 |
+
|
| 433 |
+
/* High Contrast Mode */
|
| 434 |
+
@media (prefers-contrast: high) {
|
| 435 |
+
.terra-sidebar {
|
| 436 |
+
background: var(--color-forest-deep);
|
| 437 |
+
border-right: 2px solid var(--color-white);
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
.terra-sidebar__nav-item--active {
|
| 441 |
+
background: rgba(255, 255, 255, 0.3);
|
| 442 |
+
border: 1px solid var(--color-white);
|
| 443 |
+
}
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
/* Reduced Motion */
|
| 447 |
+
@media (prefers-reduced-motion: reduce) {
|
| 448 |
+
.terra-sidebar,
|
| 449 |
+
.terra-sidebar__toggle-icon,
|
| 450 |
+
.terra-sidebar__nav-item,
|
| 451 |
+
.terra-sidebar__logo-text,
|
| 452 |
+
.terra-sidebar__nav-content {
|
| 453 |
+
transition: none;
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
.terra-sidebar__logo-icon {
|
| 457 |
+
animation: none;
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
.terra-sidebar__nav-indicator {
|
| 461 |
+
animation: none;
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
.terra-sidebar__nav-item:hover {
|
| 465 |
+
transform: none;
|
| 466 |
+
}
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
/* Focus Management */
|
| 470 |
+
.terra-sidebar__nav-item:focus-visible {
|
| 471 |
+
outline: 2px solid var(--color-mint-light);
|
| 472 |
+
outline-offset: 2px;
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
/* Animation for nav items */
|
| 476 |
+
.terra-sidebar__nav-item {
|
| 477 |
+
animation: slideInLeft 0.3s ease-out;
|
| 478 |
+
animation-fill-mode: both;
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
.terra-sidebar__nav-item:nth-child(1) { animation-delay: 0.1s; }
|
| 482 |
+
.terra-sidebar__nav-item:nth-child(2) { animation-delay: 0.15s; }
|
| 483 |
+
.terra-sidebar__nav-item:nth-child(3) { animation-delay: 0.2s; }
|
| 484 |
+
.terra-sidebar__nav-item:nth-child(4) { animation-delay: 0.25s; }
|
| 485 |
+
.terra-sidebar__nav-item:nth-child(5) { animation-delay: 0.3s; }
|
| 486 |
+
|
| 487 |
+
@keyframes slideInLeft {
|
| 488 |
+
from {
|
| 489 |
+
opacity: 0;
|
| 490 |
+
transform: translateX(-20px);
|
| 491 |
+
}
|
| 492 |
+
to {
|
| 493 |
+
opacity: 1;
|
| 494 |
+
transform: translateX(0);
|
| 495 |
+
}
|
| 496 |
+
}
|
web/src/components/layout/Sidebar.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from 'react';
|
| 2 |
+
import './Sidebar.css';
|
| 3 |
+
|
| 4 |
+
const Sidebar = ({
|
| 5 |
+
isOpen,
|
| 6 |
+
onToggle,
|
| 7 |
+
activePage,
|
| 8 |
+
onPageChange,
|
| 9 |
+
pages = [],
|
| 10 |
+
userStats = {}
|
| 11 |
+
}) => {
|
| 12 |
+
const [searchTerm, setSearchTerm] = useState('');
|
| 13 |
+
const [filteredPages, setFilteredPages] = useState(pages);
|
| 14 |
+
|
| 15 |
+
useEffect(() => {
|
| 16 |
+
if (searchTerm) {
|
| 17 |
+
const filtered = pages.filter(page =>
|
| 18 |
+
page.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
| 19 |
+
page.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
| 20 |
+
page.category?.toLowerCase().includes(searchTerm.toLowerCase())
|
| 21 |
+
);
|
| 22 |
+
setFilteredPages(filtered);
|
| 23 |
+
} else {
|
| 24 |
+
setFilteredPages(pages);
|
| 25 |
+
}
|
| 26 |
+
}, [searchTerm, pages]);
|
| 27 |
+
|
| 28 |
+
const groupedPages = filteredPages.reduce((groups, page) => {
|
| 29 |
+
const category = page.category || 'Tools';
|
| 30 |
+
if (!groups[category]) {
|
| 31 |
+
groups[category] = [];
|
| 32 |
+
}
|
| 33 |
+
groups[category].push(page);
|
| 34 |
+
return groups;
|
| 35 |
+
}, {});
|
| 36 |
+
|
| 37 |
+
const handlePageClick = (pageId) => {
|
| 38 |
+
onPageChange(pageId);
|
| 39 |
+
};
|
| 40 |
+
|
| 41 |
+
const clearSearch = () => {
|
| 42 |
+
setSearchTerm('');
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
return (
|
| 46 |
+
<div className={`terra-sidebar ${isOpen ? 'terra-sidebar--open' : 'terra-sidebar--closed'}`}>
|
| 47 |
+
{/* Logo Section */}
|
| 48 |
+
<div className="terra-sidebar__header">
|
| 49 |
+
<div className="terra-sidebar__logo">
|
| 50 |
+
<div className="terra-sidebar__logo-icon">
|
| 51 |
+
🌿
|
| 52 |
+
</div>
|
| 53 |
+
{isOpen && (
|
| 54 |
+
<div className="terra-sidebar__logo-text">
|
| 55 |
+
<h1 className="terra-sidebar__logo-title">GreenPlus by GXS</h1>
|
| 56 |
+
<p className="terra-sidebar__logo-subtitle">Environmental Intelligence Hub</p>
|
| 57 |
+
</div>
|
| 58 |
+
)}
|
| 59 |
+
</div>
|
| 60 |
+
<button
|
| 61 |
+
className="terra-sidebar__toggle"
|
| 62 |
+
onClick={onToggle}
|
| 63 |
+
aria-label={isOpen ? 'Collapse sidebar' : 'Expand sidebar'}
|
| 64 |
+
>
|
| 65 |
+
<span className={`terra-sidebar__toggle-icon ${isOpen ? 'terra-sidebar__toggle-icon--open' : ''}`}>
|
| 66 |
+
▶
|
| 67 |
+
</span>
|
| 68 |
+
</button>
|
| 69 |
+
</div>
|
| 70 |
+
|
| 71 |
+
{/* Search Section */}
|
| 72 |
+
{isOpen && (
|
| 73 |
+
<div className="terra-sidebar__search">
|
| 74 |
+
<div className="terra-sidebar__search-container">
|
| 75 |
+
<input
|
| 76 |
+
type="text"
|
| 77 |
+
placeholder="Search tools..."
|
| 78 |
+
value={searchTerm}
|
| 79 |
+
onChange={(e) => setSearchTerm(e.target.value)}
|
| 80 |
+
className="terra-sidebar__search-input"
|
| 81 |
+
/>
|
| 82 |
+
<div className="terra-sidebar__search-icon">
|
| 83 |
+
{searchTerm ? (
|
| 84 |
+
<button
|
| 85 |
+
onClick={clearSearch}
|
| 86 |
+
className="terra-sidebar__search-clear"
|
| 87 |
+
aria-label="Clear search"
|
| 88 |
+
>
|
| 89 |
+
✕
|
| 90 |
+
</button>
|
| 91 |
+
) : (
|
| 92 |
+
<span>🔍</span>
|
| 93 |
+
)}
|
| 94 |
+
</div>
|
| 95 |
+
</div>
|
| 96 |
+
</div>
|
| 97 |
+
)}
|
| 98 |
+
|
| 99 |
+
{/* Navigation Section */}
|
| 100 |
+
<nav className="terra-sidebar__nav">
|
| 101 |
+
<div className="terra-sidebar__nav-content">
|
| 102 |
+
{Object.entries(groupedPages).map(([category, categoryPages]) => (
|
| 103 |
+
<div key={category} className="terra-sidebar__category">
|
| 104 |
+
{isOpen && (
|
| 105 |
+
<h3 className="terra-sidebar__category-title">{category}</h3>
|
| 106 |
+
)}
|
| 107 |
+
<div className="terra-sidebar__category-items">
|
| 108 |
+
{categoryPages.map(page => (
|
| 109 |
+
<div
|
| 110 |
+
key={page.id}
|
| 111 |
+
className={`terra-sidebar__nav-item ${
|
| 112 |
+
activePage === page.id ? 'terra-sidebar__nav-item--active' : ''
|
| 113 |
+
}`}
|
| 114 |
+
onClick={() => handlePageClick(page.id)}
|
| 115 |
+
role="button"
|
| 116 |
+
tabIndex={0}
|
| 117 |
+
onKeyDown={(e) => {
|
| 118 |
+
if (e.key === 'Enter' || e.key === ' ') {
|
| 119 |
+
e.preventDefault();
|
| 120 |
+
handlePageClick(page.id);
|
| 121 |
+
}
|
| 122 |
+
}}
|
| 123 |
+
>
|
| 124 |
+
<div className="terra-sidebar__nav-icon">
|
| 125 |
+
{page.icon}
|
| 126 |
+
</div>
|
| 127 |
+
{isOpen && (
|
| 128 |
+
<div className="terra-sidebar__nav-content">
|
| 129 |
+
<span className="terra-sidebar__nav-name">{page.name}</span>
|
| 130 |
+
<span className="terra-sidebar__nav-desc">{page.description}</span>
|
| 131 |
+
</div>
|
| 132 |
+
)}
|
| 133 |
+
{activePage === page.id && (
|
| 134 |
+
<div className="terra-sidebar__nav-indicator" />
|
| 135 |
+
)}
|
| 136 |
+
</div>
|
| 137 |
+
))}
|
| 138 |
+
</div>
|
| 139 |
+
</div>
|
| 140 |
+
))}
|
| 141 |
+
</div>
|
| 142 |
+
</nav>
|
| 143 |
+
|
| 144 |
+
{/* Stats Footer */}
|
| 145 |
+
{isOpen && (
|
| 146 |
+
<div className="terra-sidebar__footer">
|
| 147 |
+
<div className="terra-sidebar__stats">
|
| 148 |
+
<div className="terra-sidebar__stat">
|
| 149 |
+
<span className="terra-sidebar__stat-icon">🌱</span>
|
| 150 |
+
<div className="terra-sidebar__stat-content">
|
| 151 |
+
<div className="terra-sidebar__stat-value">
|
| 152 |
+
{userStats.toolsUsed || pages.length}
|
| 153 |
+
</div>
|
| 154 |
+
<div className="terra-sidebar__stat-label">Tools</div>
|
| 155 |
+
</div>
|
| 156 |
+
</div>
|
| 157 |
+
<div className="terra-sidebar__stat">
|
| 158 |
+
<span className="terra-sidebar__stat-icon">🌍</span>
|
| 159 |
+
<div className="terra-sidebar__stat-content">
|
| 160 |
+
<div className="terra-sidebar__stat-value">
|
| 161 |
+
{userStats.impactScore || '2.5M+'}
|
| 162 |
+
</div>
|
| 163 |
+
<div className="terra-sidebar__stat-label">Impact</div>
|
| 164 |
+
</div>
|
| 165 |
+
</div>
|
| 166 |
+
</div>
|
| 167 |
+
<div className="terra-sidebar__version">
|
| 168 |
+
<span>GreenPlus by GXS v2.0</span>
|
| 169 |
+
</div>
|
| 170 |
+
</div>
|
| 171 |
+
)}
|
| 172 |
+
|
| 173 |
+
{/* Tooltip for collapsed state */}
|
| 174 |
+
{!isOpen && (
|
| 175 |
+
<div className="terra-sidebar__tooltip" id="sidebar-tooltip" role="tooltip">
|
| 176 |
+
<div className="terra-sidebar__tooltip-content"></div>
|
| 177 |
+
<div className="terra-sidebar__tooltip-arrow"></div>
|
| 178 |
+
</div>
|
| 179 |
+
)}
|
| 180 |
+
</div>
|
| 181 |
+
);
|
| 182 |
+
};
|
| 183 |
+
|
| 184 |
+
export default Sidebar;
|
web/src/components/layout/index.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Layout Components Export
|
| 2 |
+
export { default as Sidebar } from './Sidebar';
|
| 3 |
+
export { default as Header } from './Header';
|
web/src/components/ui/ActionButton.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
const ActionButton = ({
|
| 4 |
+
children,
|
| 5 |
+
onClick,
|
| 6 |
+
variant = 'primary',
|
| 7 |
+
size = 'medium',
|
| 8 |
+
icon = null,
|
| 9 |
+
disabled = false,
|
| 10 |
+
loading = false,
|
| 11 |
+
fullWidth = false,
|
| 12 |
+
style = {}
|
| 13 |
+
}) => {
|
| 14 |
+
const variants = {
|
| 15 |
+
primary: {
|
| 16 |
+
background: 'linear-gradient(135deg, #2E7D32 0%, #4CAF50 100%)',
|
| 17 |
+
color: 'white',
|
| 18 |
+
border: 'none',
|
| 19 |
+
hoverTransform: 'translateY(-2px)',
|
| 20 |
+
hoverShadow: '0 8px 25px rgba(46, 125, 50, 0.4)'
|
| 21 |
+
},
|
| 22 |
+
secondary: {
|
| 23 |
+
background: 'linear-gradient(135deg, #1976D2 0%, #2196F3 100%)',
|
| 24 |
+
color: 'white',
|
| 25 |
+
border: 'none',
|
| 26 |
+
hoverTransform: 'translateY(-2px)',
|
| 27 |
+
hoverShadow: '0 8px 25px rgba(25, 118, 210, 0.4)'
|
| 28 |
+
},
|
| 29 |
+
warning: {
|
| 30 |
+
background: 'linear-gradient(135deg, #F57C00 0%, #FF9800 100%)',
|
| 31 |
+
color: 'white',
|
| 32 |
+
border: 'none',
|
| 33 |
+
hoverTransform: 'translateY(-2px)',
|
| 34 |
+
hoverShadow: '0 8px 25px rgba(245, 124, 0, 0.4)'
|
| 35 |
+
},
|
| 36 |
+
danger: {
|
| 37 |
+
background: 'linear-gradient(135deg, #D32F2F 0%, #f44336 100%)',
|
| 38 |
+
color: 'white',
|
| 39 |
+
border: 'none',
|
| 40 |
+
hoverTransform: 'translateY(-2px)',
|
| 41 |
+
hoverShadow: '0 8px 25px rgba(211, 47, 47, 0.4)'
|
| 42 |
+
},
|
| 43 |
+
outline: {
|
| 44 |
+
background: 'transparent',
|
| 45 |
+
color: '#2E7D32',
|
| 46 |
+
border: '2px solid #2E7D32',
|
| 47 |
+
hoverTransform: 'translateY(-2px)',
|
| 48 |
+
hoverShadow: '0 8px 25px rgba(46, 125, 50, 0.2)',
|
| 49 |
+
hoverBackground: '#2E7D32',
|
| 50 |
+
hoverColor: 'white'
|
| 51 |
+
}
|
| 52 |
+
};
|
| 53 |
+
|
| 54 |
+
const sizes = {
|
| 55 |
+
small: { padding: '8px 16px', fontSize: '0.9rem' },
|
| 56 |
+
medium: { padding: '12px 24px', fontSize: '1rem' },
|
| 57 |
+
large: { padding: '15px 30px', fontSize: '1.1rem' }
|
| 58 |
+
};
|
| 59 |
+
|
| 60 |
+
const variantStyle = variants[variant] || variants.primary;
|
| 61 |
+
const sizeStyle = sizes[size] || sizes.medium;
|
| 62 |
+
|
| 63 |
+
const buttonStyle = {
|
| 64 |
+
...sizeStyle,
|
| 65 |
+
...variantStyle,
|
| 66 |
+
borderRadius: '8px',
|
| 67 |
+
fontWeight: 'bold',
|
| 68 |
+
cursor: disabled || loading ? 'not-allowed' : 'pointer',
|
| 69 |
+
transition: 'all 0.3s ease',
|
| 70 |
+
display: 'flex',
|
| 71 |
+
alignItems: 'center',
|
| 72 |
+
justifyContent: 'center',
|
| 73 |
+
gap: icon ? '8px' : '0',
|
| 74 |
+
width: fullWidth ? '100%' : 'auto',
|
| 75 |
+
opacity: disabled || loading ? 0.6 : 1,
|
| 76 |
+
position: 'relative',
|
| 77 |
+
overflow: 'hidden',
|
| 78 |
+
...style
|
| 79 |
+
};
|
| 80 |
+
|
| 81 |
+
const handleMouseEnter = (e) => {
|
| 82 |
+
if (disabled || loading) return;
|
| 83 |
+
e.currentTarget.style.transform = variantStyle.hoverTransform;
|
| 84 |
+
e.currentTarget.style.boxShadow = variantStyle.hoverShadow;
|
| 85 |
+
if (variantStyle.hoverBackground) {
|
| 86 |
+
e.currentTarget.style.background = variantStyle.hoverBackground;
|
| 87 |
+
e.currentTarget.style.color = variantStyle.hoverColor;
|
| 88 |
+
}
|
| 89 |
+
};
|
| 90 |
+
|
| 91 |
+
const handleMouseLeave = (e) => {
|
| 92 |
+
if (disabled || loading) return;
|
| 93 |
+
e.currentTarget.style.transform = 'translateY(0)';
|
| 94 |
+
e.currentTarget.style.boxShadow = 'none';
|
| 95 |
+
if (variantStyle.hoverBackground) {
|
| 96 |
+
e.currentTarget.style.background = variantStyle.background;
|
| 97 |
+
e.currentTarget.style.color = variantStyle.color;
|
| 98 |
+
}
|
| 99 |
+
};
|
| 100 |
+
|
| 101 |
+
return (
|
| 102 |
+
<button
|
| 103 |
+
style={buttonStyle}
|
| 104 |
+
onClick={disabled || loading ? undefined : onClick}
|
| 105 |
+
onMouseEnter={handleMouseEnter}
|
| 106 |
+
onMouseLeave={handleMouseLeave}
|
| 107 |
+
disabled={disabled || loading}
|
| 108 |
+
>
|
| 109 |
+
{loading && (
|
| 110 |
+
<div style={{
|
| 111 |
+
width: '16px',
|
| 112 |
+
height: '16px',
|
| 113 |
+
border: '2px solid transparent',
|
| 114 |
+
borderTop: '2px solid currentColor',
|
| 115 |
+
borderRadius: '50%',
|
| 116 |
+
animation: 'spin 1s linear infinite'
|
| 117 |
+
}} />
|
| 118 |
+
)}
|
| 119 |
+
{!loading && icon && <span>{icon}</span>}
|
| 120 |
+
{!loading && children}
|
| 121 |
+
|
| 122 |
+
<style jsx>{`
|
| 123 |
+
@keyframes spin {
|
| 124 |
+
0% { transform: rotate(0deg); }
|
| 125 |
+
100% { transform: rotate(360deg); }
|
| 126 |
+
}
|
| 127 |
+
`}</style>
|
| 128 |
+
</button>
|
| 129 |
+
);
|
| 130 |
+
};
|
| 131 |
+
|
| 132 |
+
export default ActionButton;
|
web/src/components/ui/Button.css
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Button Component Styles */
|
| 2 |
+
.terra-button {
|
| 3 |
+
display: inline-flex;
|
| 4 |
+
align-items: center;
|
| 5 |
+
justify-content: center;
|
| 6 |
+
gap: var(--spacing-sm);
|
| 7 |
+
font-family: var(--font-family-primary);
|
| 8 |
+
font-weight: var(--font-weight-semibold);
|
| 9 |
+
border: none;
|
| 10 |
+
border-radius: var(--radius-md);
|
| 11 |
+
cursor: pointer;
|
| 12 |
+
transition: all var(--transition-bounce);
|
| 13 |
+
text-decoration: none;
|
| 14 |
+
white-space: nowrap;
|
| 15 |
+
position: relative;
|
| 16 |
+
overflow: hidden;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
.terra-button:focus {
|
| 20 |
+
outline: none;
|
| 21 |
+
box-shadow: 0 0 0 3px rgba(74, 124, 35, 0.2);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
.terra-button:disabled {
|
| 25 |
+
cursor: not-allowed;
|
| 26 |
+
opacity: 0.6;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
/* Button Variants */
|
| 30 |
+
.terra-button--primary {
|
| 31 |
+
background: linear-gradient(135deg, var(--color-forest-primary) 0%, var(--color-nature-green) 100%);
|
| 32 |
+
color: var(--color-white);
|
| 33 |
+
box-shadow: var(--shadow-md);
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.terra-button--primary:hover:not(:disabled) {
|
| 37 |
+
transform: translateY(-2px);
|
| 38 |
+
box-shadow: var(--shadow-lg);
|
| 39 |
+
background: linear-gradient(135deg, var(--color-forest-deep) 0%, var(--color-forest-primary) 100%);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.terra-button--primary:active:not(:disabled) {
|
| 43 |
+
transform: translateY(0);
|
| 44 |
+
box-shadow: var(--shadow-sm);
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
.terra-button--secondary {
|
| 48 |
+
background: rgba(168, 230, 163, 0.1);
|
| 49 |
+
color: var(--color-forest-primary);
|
| 50 |
+
border: 2px solid var(--color-mint-light);
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.terra-button--secondary:hover:not(:disabled) {
|
| 54 |
+
background: rgba(168, 230, 163, 0.2);
|
| 55 |
+
border-color: var(--color-nature-green);
|
| 56 |
+
transform: scale(1.02);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.terra-button--outline {
|
| 60 |
+
background: transparent;
|
| 61 |
+
color: var(--color-nature-green);
|
| 62 |
+
border: 2px solid var(--color-nature-green);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
.terra-button--outline:hover:not(:disabled) {
|
| 66 |
+
background: var(--color-nature-green);
|
| 67 |
+
color: var(--color-white);
|
| 68 |
+
transform: translateY(-1px);
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.terra-button--ghost {
|
| 72 |
+
background: transparent;
|
| 73 |
+
color: var(--color-nature-green);
|
| 74 |
+
border: none;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.terra-button--ghost:hover:not(:disabled) {
|
| 78 |
+
background: rgba(168, 230, 163, 0.1);
|
| 79 |
+
transform: scale(1.05);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
.terra-button--danger {
|
| 83 |
+
background: linear-gradient(135deg, var(--color-warning-red) 0%, #d32f2f 100%);
|
| 84 |
+
color: var(--color-white);
|
| 85 |
+
box-shadow: var(--shadow-md);
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.terra-button--danger:hover:not(:disabled) {
|
| 89 |
+
transform: translateY(-2px);
|
| 90 |
+
box-shadow: var(--shadow-lg);
|
| 91 |
+
background: linear-gradient(135deg, #d32f2f 0%, #b71c1c 100%);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
.terra-button--success {
|
| 95 |
+
background: linear-gradient(135deg, var(--color-success-green) 0%, #2E7D32 100%);
|
| 96 |
+
color: var(--color-white);
|
| 97 |
+
box-shadow: var(--shadow-md);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
.terra-button--success:hover:not(:disabled) {
|
| 101 |
+
transform: translateY(-2px);
|
| 102 |
+
box-shadow: var(--shadow-lg);
|
| 103 |
+
background: linear-gradient(135deg, #2E7D32 0%, #1B5E20 100%);
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.terra-button--warning {
|
| 107 |
+
background: linear-gradient(135deg, var(--color-sunset-orange) 0%, #F57C00 100%);
|
| 108 |
+
color: var(--color-white);
|
| 109 |
+
box-shadow: var(--shadow-md);
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.terra-button--warning:hover:not(:disabled) {
|
| 113 |
+
transform: translateY(-2px);
|
| 114 |
+
box-shadow: var(--shadow-lg);
|
| 115 |
+
background: linear-gradient(135deg, #F57C00 0%, #E65100 100%);
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
/* Button Sizes */
|
| 119 |
+
.terra-button--xs {
|
| 120 |
+
padding: var(--spacing-xs) var(--spacing-sm);
|
| 121 |
+
font-size: var(--font-size-xs);
|
| 122 |
+
border-radius: var(--radius-sm);
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.terra-button--sm {
|
| 126 |
+
padding: var(--spacing-sm) var(--spacing-md);
|
| 127 |
+
font-size: var(--font-size-sm);
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.terra-button--md {
|
| 131 |
+
padding: var(--spacing-md) var(--spacing-lg);
|
| 132 |
+
font-size: var(--font-size-base);
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.terra-button--lg {
|
| 136 |
+
padding: var(--spacing-lg) var(--spacing-xl);
|
| 137 |
+
font-size: var(--font-size-lg);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.terra-button--xl {
|
| 141 |
+
padding: var(--spacing-xl) var(--spacing-2xl);
|
| 142 |
+
font-size: var(--font-size-xl);
|
| 143 |
+
border-radius: var(--radius-xl);
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
/* Button States */
|
| 147 |
+
.terra-button--loading {
|
| 148 |
+
pointer-events: none;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.terra-button--loading .terra-button__text {
|
| 152 |
+
opacity: 0.7;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.terra-button__spinner {
|
| 156 |
+
display: flex;
|
| 157 |
+
align-items: center;
|
| 158 |
+
justify-content: center;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.terra-button__icon {
|
| 162 |
+
display: flex;
|
| 163 |
+
align-items: center;
|
| 164 |
+
justify-content: center;
|
| 165 |
+
font-size: 1.2em;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.terra-button__text {
|
| 169 |
+
display: flex;
|
| 170 |
+
align-items: center;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
/* Icon-only buttons */
|
| 174 |
+
.terra-button--icon-only {
|
| 175 |
+
padding: var(--spacing-md);
|
| 176 |
+
aspect-ratio: 1;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.terra-button--icon-only .terra-button__text {
|
| 180 |
+
display: none;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
/* Full width button */
|
| 184 |
+
.terra-button--full {
|
| 185 |
+
width: 100%;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
/* Floating Action Button */
|
| 189 |
+
.terra-button--fab {
|
| 190 |
+
border-radius: var(--radius-full);
|
| 191 |
+
padding: var(--spacing-lg);
|
| 192 |
+
aspect-ratio: 1;
|
| 193 |
+
box-shadow: var(--shadow-xl);
|
| 194 |
+
position: fixed;
|
| 195 |
+
bottom: var(--spacing-xl);
|
| 196 |
+
right: var(--spacing-xl);
|
| 197 |
+
z-index: var(--z-fixed);
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.terra-button--fab:hover:not(:disabled) {
|
| 201 |
+
transform: scale(1.1);
|
| 202 |
+
box-shadow: var(--shadow-2xl);
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
/* Button Group */
|
| 206 |
+
.terra-button-group {
|
| 207 |
+
display: inline-flex;
|
| 208 |
+
border-radius: var(--radius-md);
|
| 209 |
+
overflow: hidden;
|
| 210 |
+
box-shadow: var(--shadow-sm);
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
.terra-button-group .terra-button {
|
| 214 |
+
border-radius: 0;
|
| 215 |
+
border-right: 1px solid rgba(255, 255, 255, 0.2);
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
.terra-button-group .terra-button:first-child {
|
| 219 |
+
border-top-left-radius: var(--radius-md);
|
| 220 |
+
border-bottom-left-radius: var(--radius-md);
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.terra-button-group .terra-button:last-child {
|
| 224 |
+
border-top-right-radius: var(--radius-md);
|
| 225 |
+
border-bottom-right-radius: var(--radius-md);
|
| 226 |
+
border-right: none;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.terra-button-group .terra-button:hover:not(:disabled) {
|
| 230 |
+
transform: none;
|
| 231 |
+
z-index: 1;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
/* Responsive Design */
|
| 235 |
+
@media (max-width: 640px) {
|
| 236 |
+
.terra-button--lg {
|
| 237 |
+
padding: var(--spacing-md) var(--spacing-lg);
|
| 238 |
+
font-size: var(--font-size-base);
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
.terra-button--xl {
|
| 242 |
+
padding: var(--spacing-lg) var(--spacing-xl);
|
| 243 |
+
font-size: var(--font-size-lg);
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
.terra-button--fab {
|
| 247 |
+
bottom: var(--spacing-lg);
|
| 248 |
+
right: var(--spacing-lg);
|
| 249 |
+
padding: var(--spacing-md);
|
| 250 |
+
}
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
/* High Contrast Mode */
|
| 254 |
+
@media (prefers-contrast: high) {
|
| 255 |
+
.terra-button--primary {
|
| 256 |
+
background: var(--color-forest-deep);
|
| 257 |
+
border: 2px solid var(--color-white);
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
.terra-button--secondary {
|
| 261 |
+
border-width: 3px;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
.terra-button--outline {
|
| 265 |
+
border-width: 3px;
|
| 266 |
+
}
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
/* Reduced Motion */
|
| 270 |
+
@media (prefers-reduced-motion: reduce) {
|
| 271 |
+
.terra-button {
|
| 272 |
+
transition: none;
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
.terra-button:hover:not(:disabled) {
|
| 276 |
+
transform: none;
|
| 277 |
+
}
|
| 278 |
+
}
|
web/src/components/ui/Button.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import './Button.css';
|
| 3 |
+
|
| 4 |
+
const Button = ({
|
| 5 |
+
children,
|
| 6 |
+
variant = 'primary',
|
| 7 |
+
size = 'md',
|
| 8 |
+
disabled = false,
|
| 9 |
+
loading = false,
|
| 10 |
+
icon = null,
|
| 11 |
+
onClick,
|
| 12 |
+
className = '',
|
| 13 |
+
type = 'button',
|
| 14 |
+
...props
|
| 15 |
+
}) => {
|
| 16 |
+
const baseClasses = 'terra-button';
|
| 17 |
+
const variantClasses = `terra-button--${variant}`;
|
| 18 |
+
const sizeClasses = `terra-button--${size}`;
|
| 19 |
+
const stateClasses = [
|
| 20 |
+
disabled && 'terra-button--disabled',
|
| 21 |
+
loading && 'terra-button--loading'
|
| 22 |
+
].filter(Boolean).join(' ');
|
| 23 |
+
|
| 24 |
+
const buttonClasses = [
|
| 25 |
+
baseClasses,
|
| 26 |
+
variantClasses,
|
| 27 |
+
sizeClasses,
|
| 28 |
+
stateClasses,
|
| 29 |
+
className
|
| 30 |
+
].filter(Boolean).join(' ');
|
| 31 |
+
|
| 32 |
+
return (
|
| 33 |
+
<button
|
| 34 |
+
type={type}
|
| 35 |
+
className={buttonClasses}
|
| 36 |
+
disabled={disabled || loading}
|
| 37 |
+
onClick={onClick}
|
| 38 |
+
{...props}
|
| 39 |
+
>
|
| 40 |
+
{loading && (
|
| 41 |
+
<span className="terra-button__spinner">
|
| 42 |
+
<svg className="animate-spin" width="16" height="16" viewBox="0 0 24 24" fill="none">
|
| 43 |
+
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" strokeOpacity="0.3"/>
|
| 44 |
+
<path d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" fill="currentColor"/>
|
| 45 |
+
</svg>
|
| 46 |
+
</span>
|
| 47 |
+
)}
|
| 48 |
+
{icon && !loading && (
|
| 49 |
+
<span className="terra-button__icon">
|
| 50 |
+
{icon}
|
| 51 |
+
</span>
|
| 52 |
+
)}
|
| 53 |
+
<span className="terra-button__text">
|
| 54 |
+
{children}
|
| 55 |
+
</span>
|
| 56 |
+
</button>
|
| 57 |
+
);
|
| 58 |
+
};
|
| 59 |
+
|
| 60 |
+
export default Button;
|