gaialive commited on
Commit
19bd8b9
·
verified ·
1 Parent(s): 4853569

Upload 136 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +6 -0
  2. web/backend/SETUP.md +403 -0
  3. web/backend/cpp/Makefile +79 -0
  4. web/backend/cpp/image_processor.cpp +243 -0
  5. web/backend/database/init.sql +224 -0
  6. web/backend/database/schema.sql +298 -0
  7. web/backend/database/water_quality.db +0 -0
  8. web/backend/package-lock.json +0 -0
  9. web/backend/package.json +27 -0
  10. web/backend/python/audio_analyzer.py +102 -0
  11. web/backend/python/blast_db/biostream_db.ndb +3 -0
  12. web/backend/python/blast_db/biostream_db.nhr +0 -0
  13. web/backend/python/blast_db/biostream_db.nin +0 -0
  14. web/backend/python/blast_db/biostream_db.njs +22 -0
  15. web/backend/python/blast_db/biostream_db.not +0 -0
  16. web/backend/python/blast_db/biostream_db.nsq +0 -0
  17. web/backend/python/blast_db/biostream_db.ntf +3 -0
  18. web/backend/python/blast_db/biostream_db.nto +0 -0
  19. web/backend/python/dna_analyzer.py +108 -0
  20. web/backend/python/ewaste_analyzer.py +253 -0
  21. web/backend/python/phantom_footprint_analyzer.py +121 -0
  22. web/backend/python/requirements.txt +9 -0
  23. web/backend/python/water_analysis.py +138 -0
  24. web/backend/results/.gitkeep +1 -0
  25. web/backend/server.js +0 -0
  26. web/backend/uploads/.gitkeep +1 -0
  27. web/package.json +43 -0
  28. web/public/index.html +13 -0
  29. web/public/manifest.json +25 -0
  30. web/public/sounds/bird-call.mp3 +3 -0
  31. web/public/sounds/forest-ambience.mp3 +3 -0
  32. web/public/sounds/industrial-hum.mp3 +3 -0
  33. web/public/sounds/river.mp3 +3 -0
  34. web/src/App.css +514 -0
  35. web/src/App.js +514 -0
  36. web/src/App.test.js +18 -0
  37. web/src/SimpleComponent.js +12 -0
  38. web/src/components/Analytics.js +219 -0
  39. web/src/components/Header.css +48 -0
  40. web/src/components/Header.js +263 -0
  41. web/src/components/LoadingScreen.js +137 -0
  42. web/src/components/Navigation.css +92 -0
  43. web/src/components/layout/Header.css +473 -0
  44. web/src/components/layout/Header.js +136 -0
  45. web/src/components/layout/Sidebar.css +496 -0
  46. web/src/components/layout/Sidebar.js +184 -0
  47. web/src/components/layout/index.js +3 -0
  48. web/src/components/ui/ActionButton.js +132 -0
  49. web/src/components/ui/Button.css +278 -0
  50. 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;