Spaces:
Sleeping
Sleeping
Res
Browse files- Dockerfile +8 -4
- backend/app/services/plant_disease_model.py +5 -5
- backend/requirements.txt +1 -1
- backend/start.sh +29 -0
- frontend/.env.example +2 -2
- frontend/index.html +2 -2
- frontend/package-lock.json +97 -1
- frontend/package.json +3 -0
- frontend/src/App.jsx +7 -2
- frontend/src/components/common/BottomNav.jsx +43 -39
- frontend/src/components/common/Footer.jsx +16 -13
- frontend/src/components/common/Header.jsx +77 -50
- frontend/src/components/common/LanguageSelector.jsx +137 -0
- frontend/src/components/common/Layout.jsx +3 -3
- frontend/src/components/common/index.js +1 -0
- frontend/src/context/AppContext.jsx +11 -5
- frontend/src/i18n/index.js +53 -0
- frontend/src/locales/en/translation.json +200 -0
- frontend/src/locales/gu/translation.json +200 -0
- frontend/src/locales/hi/translation.json +200 -0
- frontend/src/pages/FarmerDashboardPage.jsx +591 -0
- frontend/src/pages/FarmerProfilePage.jsx +172 -620
- frontend/src/pages/HomePage.jsx +85 -77
- frontend/src/utils/constants.js +9 -1
- frontend/vite.config.js +2 -0
Dockerfile
CHANGED
|
@@ -46,9 +46,10 @@ COPY --from=frontend /app/frontend/dist /app/static
|
|
| 46 |
ENV PORT=7860
|
| 47 |
ENV ENVIRONMENT=production
|
| 48 |
ENV STATIC_DIR=/app/static
|
| 49 |
-
|
|
|
|
| 50 |
ENV LOG_LEVEL=INFO
|
| 51 |
-
ENV LOG_FILE=logs/app.log
|
| 52 |
ENV RATE_LIMIT_PER_MINUTE=60
|
| 53 |
ENV WEATHER_CACHE_HOURS=6
|
| 54 |
ENV MANDI_CACHE_HOURS=24
|
|
@@ -62,5 +63,8 @@ ENV APP_VERSION=1.0.0
|
|
| 62 |
|
| 63 |
EXPOSE 7860
|
| 64 |
|
| 65 |
-
#
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
ENV PORT=7860
|
| 47 |
ENV ENVIRONMENT=production
|
| 48 |
ENV STATIC_DIR=/app/static
|
| 49 |
+
# Database URL will be set by start.sh based on available storage
|
| 50 |
+
ENV DATABASE_URL=sqlite:////app/backend/farmhelp.db
|
| 51 |
ENV LOG_LEVEL=INFO
|
| 52 |
+
ENV LOG_FILE=/app/backend/logs/app.log
|
| 53 |
ENV RATE_LIMIT_PER_MINUTE=60
|
| 54 |
ENV WEATHER_CACHE_HOURS=6
|
| 55 |
ENV MANDI_CACHE_HOURS=24
|
|
|
|
| 63 |
|
| 64 |
EXPOSE 7860
|
| 65 |
|
| 66 |
+
# Make startup script executable (already copied with backend/)
|
| 67 |
+
RUN chmod +x /app/backend/start.sh
|
| 68 |
+
|
| 69 |
+
# Run startup script
|
| 70 |
+
CMD ["/app/backend/start.sh"]
|
backend/app/services/plant_disease_model.py
CHANGED
|
@@ -34,7 +34,7 @@ _Image = None
|
|
| 34 |
_transforms = None
|
| 35 |
_AutoImageProcessor = None
|
| 36 |
_AutoModelForImageClassification = None
|
| 37 |
-
|
| 38 |
_ViTForImageClassification = None
|
| 39 |
|
| 40 |
|
|
@@ -42,7 +42,7 @@ def _ensure_imports():
|
|
| 42 |
"""Import heavy ML libraries on first use."""
|
| 43 |
global _torch, _Image, _transforms
|
| 44 |
global _AutoImageProcessor, _AutoModelForImageClassification
|
| 45 |
-
global
|
| 46 |
|
| 47 |
if _torch is not None:
|
| 48 |
return
|
|
@@ -53,7 +53,7 @@ def _ensure_imports():
|
|
| 53 |
from transformers import (
|
| 54 |
AutoImageProcessor,
|
| 55 |
AutoModelForImageClassification,
|
| 56 |
-
|
| 57 |
ViTForImageClassification,
|
| 58 |
)
|
| 59 |
|
|
@@ -62,7 +62,7 @@ def _ensure_imports():
|
|
| 62 |
_transforms = transforms
|
| 63 |
_AutoImageProcessor = AutoImageProcessor
|
| 64 |
_AutoModelForImageClassification = AutoModelForImageClassification
|
| 65 |
-
|
| 66 |
_ViTForImageClassification = ViTForImageClassification
|
| 67 |
|
| 68 |
|
|
@@ -206,7 +206,7 @@ def _load_vit():
|
|
| 206 |
logger.info("Loading ViT model: %s", VIT_MODEL_ID)
|
| 207 |
start = time.perf_counter()
|
| 208 |
try:
|
| 209 |
-
_vit_processor =
|
| 210 |
_vit_model = _ViTForImageClassification.from_pretrained(
|
| 211 |
VIT_MODEL_ID, ignore_mismatched_sizes=True
|
| 212 |
)
|
|
|
|
| 34 |
_transforms = None
|
| 35 |
_AutoImageProcessor = None
|
| 36 |
_AutoModelForImageClassification = None
|
| 37 |
+
_ViTImageProcessor = None
|
| 38 |
_ViTForImageClassification = None
|
| 39 |
|
| 40 |
|
|
|
|
| 42 |
"""Import heavy ML libraries on first use."""
|
| 43 |
global _torch, _Image, _transforms
|
| 44 |
global _AutoImageProcessor, _AutoModelForImageClassification
|
| 45 |
+
global _ViTImageProcessor, _ViTForImageClassification
|
| 46 |
|
| 47 |
if _torch is not None:
|
| 48 |
return
|
|
|
|
| 53 |
from transformers import (
|
| 54 |
AutoImageProcessor,
|
| 55 |
AutoModelForImageClassification,
|
| 56 |
+
ViTImageProcessor,
|
| 57 |
ViTForImageClassification,
|
| 58 |
)
|
| 59 |
|
|
|
|
| 62 |
_transforms = transforms
|
| 63 |
_AutoImageProcessor = AutoImageProcessor
|
| 64 |
_AutoModelForImageClassification = AutoModelForImageClassification
|
| 65 |
+
_ViTImageProcessor = ViTImageProcessor
|
| 66 |
_ViTForImageClassification = ViTForImageClassification
|
| 67 |
|
| 68 |
|
|
|
|
| 206 |
logger.info("Loading ViT model: %s", VIT_MODEL_ID)
|
| 207 |
start = time.perf_counter()
|
| 208 |
try:
|
| 209 |
+
_vit_processor = _ViTImageProcessor.from_pretrained(VIT_MODEL_ID)
|
| 210 |
_vit_model = _ViTForImageClassification.from_pretrained(
|
| 211 |
VIT_MODEL_ID, ignore_mismatched_sizes=True
|
| 212 |
)
|
backend/requirements.txt
CHANGED
|
@@ -12,5 +12,5 @@ pytest-cov==4.1.0
|
|
| 12 |
httpx==0.26.0
|
| 13 |
torch>=2.0.0
|
| 14 |
torchvision>=0.15.0
|
| 15 |
-
transformers>=4.
|
| 16 |
Pillow>=10.0.0
|
|
|
|
| 12 |
httpx==0.26.0
|
| 13 |
torch>=2.0.0
|
| 14 |
torchvision>=0.15.0
|
| 15 |
+
transformers>=4.36.0,<5.0.0
|
| 16 |
Pillow>=10.0.0
|
backend/start.sh
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/sh
|
| 2 |
+
set -e
|
| 3 |
+
|
| 4 |
+
# Create persistent directories (HF Spaces provides /data as persistent volume)
|
| 5 |
+
# Fall back to local directory if /data is not available
|
| 6 |
+
if [ -d "/data" ] && [ -w "/data" ]; then
|
| 7 |
+
echo "Using persistent storage at /data"
|
| 8 |
+
mkdir -p /data/logs
|
| 9 |
+
export DATABASE_URL="sqlite:////data/farmhelp.db"
|
| 10 |
+
export LOG_FILE="/data/logs/app.log"
|
| 11 |
+
else
|
| 12 |
+
echo "Persistent storage not available, using ephemeral storage"
|
| 13 |
+
mkdir -p /app/backend/logs
|
| 14 |
+
export DATABASE_URL="sqlite:////app/backend/farmhelp.db"
|
| 15 |
+
export LOG_FILE="/app/backend/logs/app.log"
|
| 16 |
+
fi
|
| 17 |
+
|
| 18 |
+
# Initialize database and load data
|
| 19 |
+
echo "Initializing database..."
|
| 20 |
+
python init_db.py
|
| 21 |
+
|
| 22 |
+
echo "Loading government schemes..."
|
| 23 |
+
python -m app.scripts.populate_schemes
|
| 24 |
+
|
| 25 |
+
echo "Seeding additional data..."
|
| 26 |
+
python seed_data.py || true
|
| 27 |
+
|
| 28 |
+
echo "Starting server on port ${PORT:-7860}..."
|
| 29 |
+
exec uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-7860}
|
frontend/.env.example
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
#
|
| 2 |
# Copy this file to .env and update the values as needed.
|
| 3 |
|
| 4 |
# Backend API base URL
|
|
@@ -8,7 +8,7 @@ VITE_API_BASE_URL=http://127.0.0.1:8000
|
|
| 8 |
VITE_MODEL_PATH=/models
|
| 9 |
|
| 10 |
# Application metadata
|
| 11 |
-
VITE_APP_NAME=
|
| 12 |
VITE_APP_VERSION=1.0.0
|
| 13 |
|
| 14 |
# Feature flags
|
|
|
|
| 1 |
+
# KrishiNiti Frontend Environment Variables
|
| 2 |
# Copy this file to .env and update the values as needed.
|
| 3 |
|
| 4 |
# Backend API base URL
|
|
|
|
| 8 |
VITE_MODEL_PATH=/models
|
| 9 |
|
| 10 |
# Application metadata
|
| 11 |
+
VITE_APP_NAME=KrishiNiti
|
| 12 |
VITE_APP_VERSION=1.0.0
|
| 13 |
|
| 14 |
# Feature flags
|
frontend/index.html
CHANGED
|
@@ -6,7 +6,7 @@
|
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
<meta
|
| 8 |
name="description"
|
| 9 |
-
content="
|
| 10 |
/>
|
| 11 |
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 12 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
@@ -14,7 +14,7 @@
|
|
| 14 |
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Poppins:wght@500;600;700&display=swap"
|
| 15 |
rel="stylesheet"
|
| 16 |
/>
|
| 17 |
-
<title>
|
| 18 |
</head>
|
| 19 |
<body>
|
| 20 |
<div id="root"></div>
|
|
|
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
<meta
|
| 8 |
name="description"
|
| 9 |
+
content="KrishiNiti - AI-powered crop disease detection, weather forecasting, and market price tracking for Indian farmers."
|
| 10 |
/>
|
| 11 |
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 12 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
|
|
| 14 |
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Poppins:wght@500;600;700&display=swap"
|
| 15 |
rel="stylesheet"
|
| 16 |
/>
|
| 17 |
+
<title>KrishiNiti - Smart Agriculture Assistant</title>
|
| 18 |
</head>
|
| 19 |
<body>
|
| 20 |
<div id="root"></div>
|
frontend/package-lock.json
CHANGED
|
@@ -12,10 +12,13 @@
|
|
| 12 |
"@tensorflow/tfjs": "^4.22.0",
|
| 13 |
"axios": "^1.13.4",
|
| 14 |
"framer-motion": "^12.33.0",
|
|
|
|
|
|
|
| 15 |
"prop-types": "^15.8.1",
|
| 16 |
"react": "^19.2.0",
|
| 17 |
"react-dom": "^19.2.0",
|
| 18 |
"react-hot-toast": "^2.6.0",
|
|
|
|
| 19 |
"react-router-dom": "^7.13.0"
|
| 20 |
},
|
| 21 |
"devDependencies": {
|
|
@@ -342,7 +345,6 @@
|
|
| 342 |
"version": "7.28.6",
|
| 343 |
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
| 344 |
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
| 345 |
-
"dev": true,
|
| 346 |
"license": "MIT",
|
| 347 |
"engines": {
|
| 348 |
"node": ">=6.9.0"
|
|
@@ -3771,6 +3773,15 @@
|
|
| 3771 |
"node": ">=18"
|
| 3772 |
}
|
| 3773 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3774 |
"node_modules/http-proxy-agent": {
|
| 3775 |
"version": "7.0.2",
|
| 3776 |
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
|
@@ -3799,6 +3810,46 @@
|
|
| 3799 |
"node": ">= 14"
|
| 3800 |
}
|
| 3801 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3802 |
"node_modules/iconv-lite": {
|
| 3803 |
"version": "0.6.3",
|
| 3804 |
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
|
@@ -4871,6 +4922,33 @@
|
|
| 4871 |
"react-dom": ">=16"
|
| 4872 |
}
|
| 4873 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4874 |
"node_modules/react-is": {
|
| 4875 |
"version": "16.13.1",
|
| 4876 |
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
|
@@ -5460,6 +5538,15 @@
|
|
| 5460 |
"punycode": "^2.1.0"
|
| 5461 |
}
|
| 5462 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5463 |
"node_modules/vite": {
|
| 5464 |
"version": "7.3.1",
|
| 5465 |
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
|
@@ -5613,6 +5700,15 @@
|
|
| 5613 |
}
|
| 5614 |
}
|
| 5615 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5616 |
"node_modules/w3c-xmlserializer": {
|
| 5617 |
"version": "5.0.0",
|
| 5618 |
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
|
|
|
| 12 |
"@tensorflow/tfjs": "^4.22.0",
|
| 13 |
"axios": "^1.13.4",
|
| 14 |
"framer-motion": "^12.33.0",
|
| 15 |
+
"i18next": "^25.8.4",
|
| 16 |
+
"i18next-browser-languagedetector": "^8.2.0",
|
| 17 |
"prop-types": "^15.8.1",
|
| 18 |
"react": "^19.2.0",
|
| 19 |
"react-dom": "^19.2.0",
|
| 20 |
"react-hot-toast": "^2.6.0",
|
| 21 |
+
"react-i18next": "^16.5.4",
|
| 22 |
"react-router-dom": "^7.13.0"
|
| 23 |
},
|
| 24 |
"devDependencies": {
|
|
|
|
| 345 |
"version": "7.28.6",
|
| 346 |
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
| 347 |
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
|
|
|
| 348 |
"license": "MIT",
|
| 349 |
"engines": {
|
| 350 |
"node": ">=6.9.0"
|
|
|
|
| 3773 |
"node": ">=18"
|
| 3774 |
}
|
| 3775 |
},
|
| 3776 |
+
"node_modules/html-parse-stringify": {
|
| 3777 |
+
"version": "3.0.1",
|
| 3778 |
+
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
| 3779 |
+
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
| 3780 |
+
"license": "MIT",
|
| 3781 |
+
"dependencies": {
|
| 3782 |
+
"void-elements": "3.1.0"
|
| 3783 |
+
}
|
| 3784 |
+
},
|
| 3785 |
"node_modules/http-proxy-agent": {
|
| 3786 |
"version": "7.0.2",
|
| 3787 |
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
|
|
|
| 3810 |
"node": ">= 14"
|
| 3811 |
}
|
| 3812 |
},
|
| 3813 |
+
"node_modules/i18next": {
|
| 3814 |
+
"version": "25.8.4",
|
| 3815 |
+
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.4.tgz",
|
| 3816 |
+
"integrity": "sha512-a9A0MnUjKvzjEN/26ZY1okpra9kA8MEwzYEz1BNm+IyxUKPRH6ihf0p7vj8YvULwZHKHl3zkJ6KOt4hewxBecQ==",
|
| 3817 |
+
"funding": [
|
| 3818 |
+
{
|
| 3819 |
+
"type": "individual",
|
| 3820 |
+
"url": "https://locize.com"
|
| 3821 |
+
},
|
| 3822 |
+
{
|
| 3823 |
+
"type": "individual",
|
| 3824 |
+
"url": "https://locize.com/i18next.html"
|
| 3825 |
+
},
|
| 3826 |
+
{
|
| 3827 |
+
"type": "individual",
|
| 3828 |
+
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
| 3829 |
+
}
|
| 3830 |
+
],
|
| 3831 |
+
"license": "MIT",
|
| 3832 |
+
"dependencies": {
|
| 3833 |
+
"@babel/runtime": "^7.28.4"
|
| 3834 |
+
},
|
| 3835 |
+
"peerDependencies": {
|
| 3836 |
+
"typescript": "^5"
|
| 3837 |
+
},
|
| 3838 |
+
"peerDependenciesMeta": {
|
| 3839 |
+
"typescript": {
|
| 3840 |
+
"optional": true
|
| 3841 |
+
}
|
| 3842 |
+
}
|
| 3843 |
+
},
|
| 3844 |
+
"node_modules/i18next-browser-languagedetector": {
|
| 3845 |
+
"version": "8.2.0",
|
| 3846 |
+
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz",
|
| 3847 |
+
"integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==",
|
| 3848 |
+
"license": "MIT",
|
| 3849 |
+
"dependencies": {
|
| 3850 |
+
"@babel/runtime": "^7.23.2"
|
| 3851 |
+
}
|
| 3852 |
+
},
|
| 3853 |
"node_modules/iconv-lite": {
|
| 3854 |
"version": "0.6.3",
|
| 3855 |
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
|
|
|
| 4922 |
"react-dom": ">=16"
|
| 4923 |
}
|
| 4924 |
},
|
| 4925 |
+
"node_modules/react-i18next": {
|
| 4926 |
+
"version": "16.5.4",
|
| 4927 |
+
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.4.tgz",
|
| 4928 |
+
"integrity": "sha512-6yj+dcfMncEC21QPhOTsW8mOSO+pzFmT6uvU7XXdvM/Cp38zJkmTeMeKmTrmCMD5ToT79FmiE/mRWiYWcJYW4g==",
|
| 4929 |
+
"license": "MIT",
|
| 4930 |
+
"dependencies": {
|
| 4931 |
+
"@babel/runtime": "^7.28.4",
|
| 4932 |
+
"html-parse-stringify": "^3.0.1",
|
| 4933 |
+
"use-sync-external-store": "^1.6.0"
|
| 4934 |
+
},
|
| 4935 |
+
"peerDependencies": {
|
| 4936 |
+
"i18next": ">= 25.6.2",
|
| 4937 |
+
"react": ">= 16.8.0",
|
| 4938 |
+
"typescript": "^5"
|
| 4939 |
+
},
|
| 4940 |
+
"peerDependenciesMeta": {
|
| 4941 |
+
"react-dom": {
|
| 4942 |
+
"optional": true
|
| 4943 |
+
},
|
| 4944 |
+
"react-native": {
|
| 4945 |
+
"optional": true
|
| 4946 |
+
},
|
| 4947 |
+
"typescript": {
|
| 4948 |
+
"optional": true
|
| 4949 |
+
}
|
| 4950 |
+
}
|
| 4951 |
+
},
|
| 4952 |
"node_modules/react-is": {
|
| 4953 |
"version": "16.13.1",
|
| 4954 |
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
|
|
|
| 5538 |
"punycode": "^2.1.0"
|
| 5539 |
}
|
| 5540 |
},
|
| 5541 |
+
"node_modules/use-sync-external-store": {
|
| 5542 |
+
"version": "1.6.0",
|
| 5543 |
+
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
| 5544 |
+
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
| 5545 |
+
"license": "MIT",
|
| 5546 |
+
"peerDependencies": {
|
| 5547 |
+
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
| 5548 |
+
}
|
| 5549 |
+
},
|
| 5550 |
"node_modules/vite": {
|
| 5551 |
"version": "7.3.1",
|
| 5552 |
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
|
|
|
| 5700 |
}
|
| 5701 |
}
|
| 5702 |
},
|
| 5703 |
+
"node_modules/void-elements": {
|
| 5704 |
+
"version": "3.1.0",
|
| 5705 |
+
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
| 5706 |
+
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
| 5707 |
+
"license": "MIT",
|
| 5708 |
+
"engines": {
|
| 5709 |
+
"node": ">=0.10.0"
|
| 5710 |
+
}
|
| 5711 |
+
},
|
| 5712 |
"node_modules/w3c-xmlserializer": {
|
| 5713 |
"version": "5.0.0",
|
| 5714 |
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
frontend/package.json
CHANGED
|
@@ -20,10 +20,13 @@
|
|
| 20 |
"@tensorflow/tfjs": "^4.22.0",
|
| 21 |
"axios": "^1.13.4",
|
| 22 |
"framer-motion": "^12.33.0",
|
|
|
|
|
|
|
| 23 |
"prop-types": "^15.8.1",
|
| 24 |
"react": "^19.2.0",
|
| 25 |
"react-dom": "^19.2.0",
|
| 26 |
"react-hot-toast": "^2.6.0",
|
|
|
|
| 27 |
"react-router-dom": "^7.13.0"
|
| 28 |
},
|
| 29 |
"devDependencies": {
|
|
|
|
| 20 |
"@tensorflow/tfjs": "^4.22.0",
|
| 21 |
"axios": "^1.13.4",
|
| 22 |
"framer-motion": "^12.33.0",
|
| 23 |
+
"i18next": "^25.8.4",
|
| 24 |
+
"i18next-browser-languagedetector": "^8.2.0",
|
| 25 |
"prop-types": "^15.8.1",
|
| 26 |
"react": "^19.2.0",
|
| 27 |
"react-dom": "^19.2.0",
|
| 28 |
"react-hot-toast": "^2.6.0",
|
| 29 |
+
"react-i18next": "^16.5.4",
|
| 30 |
"react-router-dom": "^7.13.0"
|
| 31 |
},
|
| 32 |
"devDependencies": {
|
frontend/src/App.jsx
CHANGED
|
@@ -8,6 +8,9 @@ import { AuthProvider } from "@context/AuthContext";
|
|
| 8 |
import { Layout, PageTransition, LoadingSpinner, ErrorBoundary } from "@components/common";
|
| 9 |
import useApp from "@hooks/useApp";
|
| 10 |
|
|
|
|
|
|
|
|
|
|
| 11 |
// ---------------------------------------------------------------------------
|
| 12 |
// Lazy-loaded page components (code splitting)
|
| 13 |
// ---------------------------------------------------------------------------
|
|
@@ -16,6 +19,7 @@ const HomePage = lazy(() => import("@pages/HomePage"));
|
|
| 16 |
const LoginPage = lazy(() => import("@pages/LoginPage"));
|
| 17 |
const SignupPage = lazy(() => import("@pages/SignupPage"));
|
| 18 |
const FarmerProfilePage = lazy(() => import("@pages/FarmerProfilePage"));
|
|
|
|
| 19 |
const DiseaseDetectionPage = lazy(() => import("@pages/DiseaseDetectionPage"));
|
| 20 |
const WeatherPage = lazy(() => import("@pages/WeatherPage"));
|
| 21 |
const APMCPricePage = lazy(() => import("@pages/APMCPricePage"));
|
|
@@ -37,13 +41,13 @@ function PageLoader() {
|
|
| 37 |
* Separated so that useApp can access the provider above it.
|
| 38 |
*/
|
| 39 |
function AppShell() {
|
| 40 |
-
const { language,
|
| 41 |
const location = useLocation();
|
| 42 |
|
| 43 |
return (
|
| 44 |
<Layout
|
| 45 |
language={language}
|
| 46 |
-
|
| 47 |
theme={theme}
|
| 48 |
onToggleTheme={toggleTheme}
|
| 49 |
>
|
|
@@ -56,6 +60,7 @@ function AppShell() {
|
|
| 56 |
<Route path="/login" element={<LoginPage />} />
|
| 57 |
<Route path="/signup" element={<SignupPage />} />
|
| 58 |
<Route path="/profile" element={<FarmerProfilePage />} />
|
|
|
|
| 59 |
<Route path="/disease" element={<DiseaseDetectionPage />} />
|
| 60 |
<Route path="/weather" element={<WeatherPage />} />
|
| 61 |
<Route path="/apmc" element={<APMCPricePage />} />
|
|
|
|
| 8 |
import { Layout, PageTransition, LoadingSpinner, ErrorBoundary } from "@components/common";
|
| 9 |
import useApp from "@hooks/useApp";
|
| 10 |
|
| 11 |
+
// Initialize i18n
|
| 12 |
+
import "@i18n";
|
| 13 |
+
|
| 14 |
// ---------------------------------------------------------------------------
|
| 15 |
// Lazy-loaded page components (code splitting)
|
| 16 |
// ---------------------------------------------------------------------------
|
|
|
|
| 19 |
const LoginPage = lazy(() => import("@pages/LoginPage"));
|
| 20 |
const SignupPage = lazy(() => import("@pages/SignupPage"));
|
| 21 |
const FarmerProfilePage = lazy(() => import("@pages/FarmerProfilePage"));
|
| 22 |
+
const FarmerDashboardPage = lazy(() => import("@pages/FarmerDashboardPage"));
|
| 23 |
const DiseaseDetectionPage = lazy(() => import("@pages/DiseaseDetectionPage"));
|
| 24 |
const WeatherPage = lazy(() => import("@pages/WeatherPage"));
|
| 25 |
const APMCPricePage = lazy(() => import("@pages/APMCPricePage"));
|
|
|
|
| 41 |
* Separated so that useApp can access the provider above it.
|
| 42 |
*/
|
| 43 |
function AppShell() {
|
| 44 |
+
const { language, setLanguage, theme, toggleTheme } = useApp();
|
| 45 |
const location = useLocation();
|
| 46 |
|
| 47 |
return (
|
| 48 |
<Layout
|
| 49 |
language={language}
|
| 50 |
+
onLanguageChange={setLanguage}
|
| 51 |
theme={theme}
|
| 52 |
onToggleTheme={toggleTheme}
|
| 53 |
>
|
|
|
|
| 60 |
<Route path="/login" element={<LoginPage />} />
|
| 61 |
<Route path="/signup" element={<SignupPage />} />
|
| 62 |
<Route path="/profile" element={<FarmerProfilePage />} />
|
| 63 |
+
<Route path="/my-dashboard" element={<FarmerDashboardPage />} />
|
| 64 |
<Route path="/disease" element={<DiseaseDetectionPage />} />
|
| 65 |
<Route path="/weather" element={<WeatherPage />} />
|
| 66 |
<Route path="/apmc" element={<APMCPricePage />} />
|
frontend/src/components/common/BottomNav.jsx
CHANGED
|
@@ -5,7 +5,9 @@
|
|
| 5 |
* Hidden on desktop via className prop (default usage in Layout).
|
| 6 |
*/
|
| 7 |
|
|
|
|
| 8 |
import { Link, useLocation } from "react-router-dom";
|
|
|
|
| 9 |
import PropTypes from "prop-types";
|
| 10 |
import {
|
| 11 |
HomeIcon,
|
|
@@ -23,51 +25,53 @@ import {
|
|
| 23 |
} from "@heroicons/react/24/solid";
|
| 24 |
import { ROUTES } from "@utils/constants";
|
| 25 |
|
| 26 |
-
const NAV_ITEMS = [
|
| 27 |
-
{
|
| 28 |
-
label: "Home",
|
| 29 |
-
path: ROUTES.HOME,
|
| 30 |
-
Icon: HomeIcon,
|
| 31 |
-
ActiveIcon: HomeIconSolid,
|
| 32 |
-
},
|
| 33 |
-
{
|
| 34 |
-
label: "Disease",
|
| 35 |
-
path: ROUTES.DISEASE_DETECTION,
|
| 36 |
-
Icon: CameraIcon,
|
| 37 |
-
ActiveIcon: CameraIconSolid,
|
| 38 |
-
},
|
| 39 |
-
{
|
| 40 |
-
label: "Weather",
|
| 41 |
-
path: ROUTES.WEATHER,
|
| 42 |
-
Icon: CloudIcon,
|
| 43 |
-
ActiveIcon: CloudIconSolid,
|
| 44 |
-
},
|
| 45 |
-
{
|
| 46 |
-
label: "APMC",
|
| 47 |
-
path: ROUTES.APMC,
|
| 48 |
-
Icon: CurrencyRupeeIcon,
|
| 49 |
-
ActiveIcon: CurrencyRupeeIconSolid,
|
| 50 |
-
},
|
| 51 |
-
{
|
| 52 |
-
label: "Schemes",
|
| 53 |
-
path: ROUTES.SCHEMES,
|
| 54 |
-
Icon: DocumentTextIcon,
|
| 55 |
-
ActiveIcon: DocumentTextIconSolid,
|
| 56 |
-
},
|
| 57 |
-
];
|
| 58 |
-
|
| 59 |
function BottomNav({ className = "" }) {
|
|
|
|
| 60 |
const location = useLocation();
|
| 61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
return (
|
| 63 |
<nav
|
| 64 |
-
className={`fixed bottom-0 inset-x-0 z-30 border-t border-neutral-200 bg-white/95 backdrop-blur-sm safe-area-bottom ${className}`}
|
| 65 |
aria-label="Bottom navigation"
|
| 66 |
>
|
| 67 |
<div className="flex items-center justify-around h-16">
|
| 68 |
{NAV_ITEMS.map((item) => {
|
| 69 |
const isActive = location.pathname === item.path;
|
| 70 |
const IconComponent = isActive ? item.ActiveIcon : item.Icon;
|
|
|
|
| 71 |
|
| 72 |
return (
|
| 73 |
<Link
|
|
@@ -77,15 +81,15 @@ function BottomNav({ className = "" }) {
|
|
| 77 |
"flex flex-col items-center justify-center gap-0.5 px-3 py-1 rounded-lg min-w-[4rem]",
|
| 78 |
"transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500",
|
| 79 |
isActive
|
| 80 |
-
? "text-primary-700"
|
| 81 |
-
: "text-neutral-500 hover:text-neutral-700",
|
| 82 |
].join(" ")}
|
| 83 |
aria-current={isActive ? "page" : undefined}
|
| 84 |
-
aria-label={
|
| 85 |
>
|
| 86 |
<IconComponent className="h-6 w-6" aria-hidden="true" />
|
| 87 |
-
<span className="text-[10px] font-medium leading-tight">
|
| 88 |
-
{
|
| 89 |
</span>
|
| 90 |
</Link>
|
| 91 |
);
|
|
|
|
| 5 |
* Hidden on desktop via className prop (default usage in Layout).
|
| 6 |
*/
|
| 7 |
|
| 8 |
+
import { useMemo } from "react";
|
| 9 |
import { Link, useLocation } from "react-router-dom";
|
| 10 |
+
import { useTranslation } from "react-i18next";
|
| 11 |
import PropTypes from "prop-types";
|
| 12 |
import {
|
| 13 |
HomeIcon,
|
|
|
|
| 25 |
} from "@heroicons/react/24/solid";
|
| 26 |
import { ROUTES } from "@utils/constants";
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
function BottomNav({ className = "" }) {
|
| 29 |
+
const { t } = useTranslation();
|
| 30 |
const location = useLocation();
|
| 31 |
|
| 32 |
+
const NAV_ITEMS = useMemo(() => [
|
| 33 |
+
{
|
| 34 |
+
labelKey: "nav.home",
|
| 35 |
+
path: ROUTES.HOME,
|
| 36 |
+
Icon: HomeIcon,
|
| 37 |
+
ActiveIcon: HomeIconSolid,
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
labelKey: "nav.diseaseDetection",
|
| 41 |
+
path: ROUTES.DISEASE_DETECTION,
|
| 42 |
+
Icon: CameraIcon,
|
| 43 |
+
ActiveIcon: CameraIconSolid,
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
labelKey: "nav.weather",
|
| 47 |
+
path: ROUTES.WEATHER,
|
| 48 |
+
Icon: CloudIcon,
|
| 49 |
+
ActiveIcon: CloudIconSolid,
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
labelKey: "nav.apmcPrice",
|
| 53 |
+
path: ROUTES.APMC,
|
| 54 |
+
Icon: CurrencyRupeeIcon,
|
| 55 |
+
ActiveIcon: CurrencyRupeeIconSolid,
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
labelKey: "nav.schemes",
|
| 59 |
+
path: ROUTES.SCHEMES,
|
| 60 |
+
Icon: DocumentTextIcon,
|
| 61 |
+
ActiveIcon: DocumentTextIconSolid,
|
| 62 |
+
},
|
| 63 |
+
], []);
|
| 64 |
+
|
| 65 |
return (
|
| 66 |
<nav
|
| 67 |
+
className={`fixed bottom-0 inset-x-0 z-30 border-t border-neutral-200 dark:border-neutral-700 bg-white/95 dark:bg-neutral-900/95 backdrop-blur-sm safe-area-bottom ${className}`}
|
| 68 |
aria-label="Bottom navigation"
|
| 69 |
>
|
| 70 |
<div className="flex items-center justify-around h-16">
|
| 71 |
{NAV_ITEMS.map((item) => {
|
| 72 |
const isActive = location.pathname === item.path;
|
| 73 |
const IconComponent = isActive ? item.ActiveIcon : item.Icon;
|
| 74 |
+
const label = t(item.labelKey);
|
| 75 |
|
| 76 |
return (
|
| 77 |
<Link
|
|
|
|
| 81 |
"flex flex-col items-center justify-center gap-0.5 px-3 py-1 rounded-lg min-w-[4rem]",
|
| 82 |
"transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500",
|
| 83 |
isActive
|
| 84 |
+
? "text-primary-700 dark:text-primary-400"
|
| 85 |
+
: "text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200",
|
| 86 |
].join(" ")}
|
| 87 |
aria-current={isActive ? "page" : undefined}
|
| 88 |
+
aria-label={label}
|
| 89 |
>
|
| 90 |
<IconComponent className="h-6 w-6" aria-hidden="true" />
|
| 91 |
+
<span className="text-[10px] font-medium leading-tight truncate max-w-[4rem]">
|
| 92 |
+
{label}
|
| 93 |
</span>
|
| 94 |
</Link>
|
| 95 |
);
|
frontend/src/components/common/Footer.jsx
CHANGED
|
@@ -5,22 +5,25 @@
|
|
| 5 |
* side-by-side on desktop.
|
| 6 |
*/
|
| 7 |
|
|
|
|
| 8 |
import { Link } from "react-router-dom";
|
|
|
|
| 9 |
import PropTypes from "prop-types";
|
| 10 |
import { ROUTES, APP_NAME } from "@utils/constants";
|
| 11 |
|
| 12 |
-
const FOOTER_LINKS = [
|
| 13 |
-
{ label: "Disease Detection", path: ROUTES.DISEASE_DETECTION },
|
| 14 |
-
{ label: "Weather", path: ROUTES.WEATHER },
|
| 15 |
-
{ label: "APMC Price", path: ROUTES.APMC },
|
| 16 |
-
];
|
| 17 |
-
|
| 18 |
function Footer({ className = "" }) {
|
|
|
|
| 19 |
const year = new Date().getFullYear();
|
| 20 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
return (
|
| 22 |
<footer
|
| 23 |
-
className={`border-t border-neutral-200 bg-white ${className}`}
|
| 24 |
role="contentinfo"
|
| 25 |
>
|
| 26 |
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-8">
|
|
@@ -34,12 +37,12 @@ function Footer({ className = "" }) {
|
|
| 34 |
<span className="inline-flex items-center justify-center h-7 w-7 rounded-lg bg-primary-600 text-white font-display font-bold text-xs">
|
| 35 |
FH
|
| 36 |
</span>
|
| 37 |
-
<span className="text-base font-display font-bold text-primary-700">
|
| 38 |
{APP_NAME}
|
| 39 |
</span>
|
| 40 |
</Link>
|
| 41 |
-
<p className="mt-2 text-sm text-neutral-500 max-w-xs">
|
| 42 |
-
|
| 43 |
</p>
|
| 44 |
</div>
|
| 45 |
|
|
@@ -50,7 +53,7 @@ function Footer({ className = "" }) {
|
|
| 50 |
<li key={link.path}>
|
| 51 |
<Link
|
| 52 |
to={link.path}
|
| 53 |
-
className="text-sm text-neutral-600 hover:text-primary-700 transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 rounded"
|
| 54 |
>
|
| 55 |
{link.label}
|
| 56 |
</Link>
|
|
@@ -61,9 +64,9 @@ function Footer({ className = "" }) {
|
|
| 61 |
</div>
|
| 62 |
|
| 63 |
{/* Copyright */}
|
| 64 |
-
<div className="mt-6 border-t border-neutral-100 pt-4">
|
| 65 |
<p className="text-xs text-neutral-400 text-center md:text-left">
|
| 66 |
-
{year} {
|
| 67 |
</p>
|
| 68 |
</div>
|
| 69 |
</div>
|
|
|
|
| 5 |
* side-by-side on desktop.
|
| 6 |
*/
|
| 7 |
|
| 8 |
+
import { useMemo } from "react";
|
| 9 |
import { Link } from "react-router-dom";
|
| 10 |
+
import { useTranslation } from "react-i18next";
|
| 11 |
import PropTypes from "prop-types";
|
| 12 |
import { ROUTES, APP_NAME } from "@utils/constants";
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
function Footer({ className = "" }) {
|
| 15 |
+
const { t } = useTranslation();
|
| 16 |
const year = new Date().getFullYear();
|
| 17 |
|
| 18 |
+
const FOOTER_LINKS = useMemo(() => [
|
| 19 |
+
{ label: t("nav.diseaseDetection"), path: ROUTES.DISEASE_DETECTION },
|
| 20 |
+
{ label: t("nav.weather"), path: ROUTES.WEATHER },
|
| 21 |
+
{ label: t("nav.apmcPrice"), path: ROUTES.APMC },
|
| 22 |
+
], [t]);
|
| 23 |
+
|
| 24 |
return (
|
| 25 |
<footer
|
| 26 |
+
className={`border-t border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 ${className}`}
|
| 27 |
role="contentinfo"
|
| 28 |
>
|
| 29 |
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-8">
|
|
|
|
| 37 |
<span className="inline-flex items-center justify-center h-7 w-7 rounded-lg bg-primary-600 text-white font-display font-bold text-xs">
|
| 38 |
FH
|
| 39 |
</span>
|
| 40 |
+
<span className="text-base font-display font-bold text-primary-700 dark:text-primary-400">
|
| 41 |
{APP_NAME}
|
| 42 |
</span>
|
| 43 |
</Link>
|
| 44 |
+
<p className="mt-2 text-sm text-neutral-500 dark:text-neutral-400 max-w-xs">
|
| 45 |
+
{t("home.tagline")}
|
| 46 |
</p>
|
| 47 |
</div>
|
| 48 |
|
|
|
|
| 53 |
<li key={link.path}>
|
| 54 |
<Link
|
| 55 |
to={link.path}
|
| 56 |
+
className="text-sm text-neutral-600 hover:text-primary-700 dark:text-neutral-400 dark:hover:text-primary-400 transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 rounded"
|
| 57 |
>
|
| 58 |
{link.label}
|
| 59 |
</Link>
|
|
|
|
| 64 |
</div>
|
| 65 |
|
| 66 |
{/* Copyright */}
|
| 67 |
+
<div className="mt-6 border-t border-neutral-100 dark:border-neutral-800 pt-4">
|
| 68 |
<p className="text-xs text-neutral-400 text-center md:text-left">
|
| 69 |
+
{year} {t("footer.copyright")} {t("footer.madeWith")}
|
| 70 |
</p>
|
| 71 |
</div>
|
| 72 |
</div>
|
frontend/src/components/common/Header.jsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
/**
|
| 2 |
* Header - Top navigation bar with logo, navigation links,
|
| 3 |
-
* language
|
| 4 |
*
|
| 5 |
* Features a subtle gradient background and responsive
|
| 6 |
* hamburger menu on mobile.
|
|
@@ -8,33 +8,36 @@
|
|
| 8 |
|
| 9 |
import { useState, useCallback } from "react";
|
| 10 |
import { Link, useLocation, useNavigate } from "react-router-dom";
|
|
|
|
| 11 |
import PropTypes from "prop-types";
|
| 12 |
import {
|
| 13 |
Bars3Icon,
|
| 14 |
XMarkIcon,
|
| 15 |
-
LanguageIcon,
|
| 16 |
SunIcon,
|
| 17 |
MoonIcon,
|
| 18 |
UserCircleIcon,
|
| 19 |
ArrowRightOnRectangleIcon,
|
|
|
|
| 20 |
} from "@heroicons/react/24/outline";
|
| 21 |
-
import { ROUTES,
|
| 22 |
import { useAuth } from "@context/AuthContext";
|
|
|
|
| 23 |
|
| 24 |
-
|
| 25 |
-
{
|
| 26 |
-
{ label: "Disease Detection", path: ROUTES.DISEASE_DETECTION },
|
| 27 |
-
{ label: "Weather", path: ROUTES.WEATHER },
|
| 28 |
-
{ label: "APMC Price", path: ROUTES.APMC },
|
| 29 |
-
{ label: "Schemes", path: ROUTES.SCHEMES },
|
| 30 |
-
];
|
| 31 |
-
|
| 32 |
-
function Header({ language, onToggleLanguage, theme, onToggleTheme }) {
|
| 33 |
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
| 34 |
const location = useLocation();
|
| 35 |
const navigate = useNavigate();
|
| 36 |
const { isAuthenticated, user, logout } = useAuth();
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
const toggleMobile = useCallback(() => {
|
| 39 |
setMobileMenuOpen((prev) => !prev);
|
| 40 |
}, []);
|
|
@@ -49,12 +52,11 @@ function Header({ language, onToggleLanguage, theme, onToggleTheme }) {
|
|
| 49 |
closeMobile();
|
| 50 |
}, [logout, navigate, closeMobile]);
|
| 51 |
|
| 52 |
-
const languageLabel = language === LANGUAGES.HI ? "EN" : "HI";
|
| 53 |
const isDark = theme === "dark";
|
| 54 |
|
| 55 |
return (
|
| 56 |
<header
|
| 57 |
-
className="sticky top-0 z-30 border-b border-neutral-200 bg-white/95 backdrop-blur-sm transition-colors duration-300"
|
| 58 |
id="app-header"
|
| 59 |
>
|
| 60 |
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
@@ -66,7 +68,7 @@ function Header({ language, onToggleLanguage, theme, onToggleTheme }) {
|
|
| 66 |
aria-label={`${APP_NAME} home`}
|
| 67 |
>
|
| 68 |
<span className="inline-flex items-center justify-center h-8 w-8 rounded-lg bg-gradient-header text-white font-display font-bold text-sm shadow-sm">
|
| 69 |
-
|
| 70 |
</span>
|
| 71 |
<span className="text-lg font-display font-bold text-primary-700 hidden sm:block">
|
| 72 |
{APP_NAME}
|
|
@@ -89,8 +91,8 @@ function Header({ language, onToggleLanguage, theme, onToggleTheme }) {
|
|
| 89 |
"px-3 py-2 rounded-lg text-sm font-medium transition-colors",
|
| 90 |
"focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500",
|
| 91 |
isActive
|
| 92 |
-
? "bg-primary-50 text-primary-700"
|
| 93 |
-
: "text-neutral-600 hover:text-neutral-900 hover:bg-neutral-100",
|
| 94 |
].join(" ")}
|
| 95 |
aria-current={isActive ? "page" : undefined}
|
| 96 |
>
|
|
@@ -107,8 +109,8 @@ function Header({ language, onToggleLanguage, theme, onToggleTheme }) {
|
|
| 107 |
<button
|
| 108 |
type="button"
|
| 109 |
onClick={onToggleTheme}
|
| 110 |
-
className="inline-flex items-center justify-center rounded-lg p-2 text-neutral-600 hover:bg-neutral-100 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
| 111 |
-
aria-label={
|
| 112 |
id="theme-toggle"
|
| 113 |
>
|
| 114 |
{isDark ? (
|
|
@@ -119,27 +121,39 @@ function Header({ language, onToggleLanguage, theme, onToggleTheme }) {
|
|
| 119 |
</button>
|
| 120 |
)}
|
| 121 |
|
| 122 |
-
{/* Language
|
| 123 |
-
{
|
| 124 |
-
<
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
aria-label={`Switch to ${languageLabel === "HI" ? "Hindi" : "English"}`}
|
| 129 |
-
id="lang-toggle"
|
| 130 |
-
>
|
| 131 |
-
<LanguageIcon className="h-4 w-4" aria-hidden="true" />
|
| 132 |
-
<span className="hidden xs:inline">{languageLabel}</span>
|
| 133 |
-
</button>
|
| 134 |
)}
|
| 135 |
|
| 136 |
-
{/* User Profile / Login */}
|
| 137 |
<div className="hidden md:flex items-center gap-1.5">
|
| 138 |
{isAuthenticated && user ? (
|
| 139 |
<>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
<Link
|
| 141 |
to={ROUTES.PROFILE}
|
| 142 |
-
className=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
>
|
| 144 |
<UserCircleIcon className="h-5 w-5" aria-hidden="true" />
|
| 145 |
<span className="max-w-[100px] truncate">{user.name?.split(" ")[0]}</span>
|
|
@@ -147,9 +161,9 @@ function Header({ language, onToggleLanguage, theme, onToggleTheme }) {
|
|
| 147 |
<button
|
| 148 |
type="button"
|
| 149 |
onClick={handleLogout}
|
| 150 |
-
className="inline-flex items-center justify-center rounded-lg p-2 text-neutral-500 hover:bg-neutral-100 hover:text-neutral-700 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
| 151 |
-
aria-label="
|
| 152 |
-
title="
|
| 153 |
>
|
| 154 |
<ArrowRightOnRectangleIcon className="h-5 w-5" aria-hidden="true" />
|
| 155 |
</button>
|
|
@@ -159,7 +173,7 @@ function Header({ language, onToggleLanguage, theme, onToggleTheme }) {
|
|
| 159 |
to={ROUTES.LOGIN}
|
| 160 |
className="inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium bg-primary-600 text-white hover:bg-primary-700 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
| 161 |
>
|
| 162 |
-
|
| 163 |
</Link>
|
| 164 |
)}
|
| 165 |
</div>
|
|
@@ -168,10 +182,10 @@ function Header({ language, onToggleLanguage, theme, onToggleTheme }) {
|
|
| 168 |
<button
|
| 169 |
type="button"
|
| 170 |
onClick={toggleMobile}
|
| 171 |
-
className="md:hidden inline-flex items-center justify-center rounded-lg p-2 text-neutral-600 hover:bg-neutral-100 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
| 172 |
aria-expanded={mobileMenuOpen}
|
| 173 |
aria-controls="mobile-menu"
|
| 174 |
-
aria-label="
|
| 175 |
>
|
| 176 |
{mobileMenuOpen ? (
|
| 177 |
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
|
@@ -187,7 +201,7 @@ function Header({ language, onToggleLanguage, theme, onToggleTheme }) {
|
|
| 187 |
{mobileMenuOpen && (
|
| 188 |
<nav
|
| 189 |
id="mobile-menu"
|
| 190 |
-
className="md:hidden border-t border-neutral-200 bg-white animate-slide-down"
|
| 191 |
aria-label="Mobile navigation"
|
| 192 |
>
|
| 193 |
<div className="px-4 py-3 space-y-1">
|
|
@@ -201,8 +215,8 @@ function Header({ language, onToggleLanguage, theme, onToggleTheme }) {
|
|
| 201 |
className={[
|
| 202 |
"block px-3 py-2 rounded-lg text-sm font-medium transition-colors",
|
| 203 |
isActive
|
| 204 |
-
? "bg-primary-50 text-primary-700"
|
| 205 |
-
: "text-neutral-600 hover:bg-neutral-100",
|
| 206 |
].join(" ")}
|
| 207 |
aria-current={isActive ? "page" : undefined}
|
| 208 |
>
|
|
@@ -212,29 +226,42 @@ function Header({ language, onToggleLanguage, theme, onToggleTheme }) {
|
|
| 212 |
})}
|
| 213 |
|
| 214 |
{/* Mobile Auth Links */}
|
| 215 |
-
<div className="border-t border-neutral-200 mt-2 pt-2">
|
| 216 |
{isAuthenticated && user ? (
|
| 217 |
<>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
<Link
|
| 219 |
to={ROUTES.PROFILE}
|
| 220 |
onClick={closeMobile}
|
| 221 |
className={[
|
| 222 |
"flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-colors",
|
| 223 |
location.pathname === ROUTES.PROFILE
|
| 224 |
-
? "bg-primary-50 text-primary-700"
|
| 225 |
-
: "text-neutral-600 hover:bg-neutral-100",
|
| 226 |
].join(" ")}
|
| 227 |
>
|
| 228 |
<UserCircleIcon className="h-5 w-5" />
|
| 229 |
-
|
| 230 |
</Link>
|
| 231 |
<button
|
| 232 |
type="button"
|
| 233 |
onClick={handleLogout}
|
| 234 |
-
className="flex items-center gap-2 w-full px-3 py-2 rounded-lg text-sm font-medium text-neutral-600 hover:bg-neutral-100 transition-colors"
|
| 235 |
>
|
| 236 |
<ArrowRightOnRectangleIcon className="h-5 w-5" />
|
| 237 |
-
|
| 238 |
</button>
|
| 239 |
</>
|
| 240 |
) : (
|
|
@@ -243,7 +270,7 @@ function Header({ language, onToggleLanguage, theme, onToggleTheme }) {
|
|
| 243 |
onClick={closeMobile}
|
| 244 |
className="block px-3 py-2 rounded-lg text-sm font-medium bg-primary-600 text-white text-center hover:bg-primary-700 transition-colors"
|
| 245 |
>
|
| 246 |
-
|
| 247 |
</Link>
|
| 248 |
)}
|
| 249 |
</div>
|
|
@@ -256,7 +283,7 @@ function Header({ language, onToggleLanguage, theme, onToggleTheme }) {
|
|
| 256 |
|
| 257 |
Header.propTypes = {
|
| 258 |
language: PropTypes.string,
|
| 259 |
-
|
| 260 |
theme: PropTypes.string,
|
| 261 |
onToggleTheme: PropTypes.func,
|
| 262 |
};
|
|
|
|
| 1 |
/**
|
| 2 |
* Header - Top navigation bar with logo, navigation links,
|
| 3 |
+
* language selector, dark mode toggle, and user profile.
|
| 4 |
*
|
| 5 |
* Features a subtle gradient background and responsive
|
| 6 |
* hamburger menu on mobile.
|
|
|
|
| 8 |
|
| 9 |
import { useState, useCallback } from "react";
|
| 10 |
import { Link, useLocation, useNavigate } from "react-router-dom";
|
| 11 |
+
import { useTranslation } from "react-i18next";
|
| 12 |
import PropTypes from "prop-types";
|
| 13 |
import {
|
| 14 |
Bars3Icon,
|
| 15 |
XMarkIcon,
|
|
|
|
| 16 |
SunIcon,
|
| 17 |
MoonIcon,
|
| 18 |
UserCircleIcon,
|
| 19 |
ArrowRightOnRectangleIcon,
|
| 20 |
+
Squares2X2Icon,
|
| 21 |
} from "@heroicons/react/24/outline";
|
| 22 |
+
import { ROUTES, APP_NAME } from "@utils/constants";
|
| 23 |
import { useAuth } from "@context/AuthContext";
|
| 24 |
+
import LanguageSelector from "./LanguageSelector";
|
| 25 |
|
| 26 |
+
function Header({ language, onLanguageChange, theme, onToggleTheme }) {
|
| 27 |
+
const { t } = useTranslation();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
| 29 |
const location = useLocation();
|
| 30 |
const navigate = useNavigate();
|
| 31 |
const { isAuthenticated, user, logout } = useAuth();
|
| 32 |
|
| 33 |
+
const NAV_ITEMS = [
|
| 34 |
+
{ label: t("nav.home"), path: ROUTES.HOME },
|
| 35 |
+
{ label: t("nav.diseaseDetection"), path: ROUTES.DISEASE_DETECTION },
|
| 36 |
+
{ label: t("nav.weather"), path: ROUTES.WEATHER },
|
| 37 |
+
{ label: t("nav.apmcPrice"), path: ROUTES.APMC },
|
| 38 |
+
{ label: t("nav.schemes"), path: ROUTES.SCHEMES },
|
| 39 |
+
];
|
| 40 |
+
|
| 41 |
const toggleMobile = useCallback(() => {
|
| 42 |
setMobileMenuOpen((prev) => !prev);
|
| 43 |
}, []);
|
|
|
|
| 52 |
closeMobile();
|
| 53 |
}, [logout, navigate, closeMobile]);
|
| 54 |
|
|
|
|
| 55 |
const isDark = theme === "dark";
|
| 56 |
|
| 57 |
return (
|
| 58 |
<header
|
| 59 |
+
className="sticky top-0 z-30 border-b border-neutral-200 dark:border-neutral-700 bg-white/95 dark:bg-neutral-900/95 backdrop-blur-sm transition-colors duration-300"
|
| 60 |
id="app-header"
|
| 61 |
>
|
| 62 |
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
|
|
| 68 |
aria-label={`${APP_NAME} home`}
|
| 69 |
>
|
| 70 |
<span className="inline-flex items-center justify-center h-8 w-8 rounded-lg bg-gradient-header text-white font-display font-bold text-sm shadow-sm">
|
| 71 |
+
KN
|
| 72 |
</span>
|
| 73 |
<span className="text-lg font-display font-bold text-primary-700 hidden sm:block">
|
| 74 |
{APP_NAME}
|
|
|
|
| 91 |
"px-3 py-2 rounded-lg text-sm font-medium transition-colors",
|
| 92 |
"focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500",
|
| 93 |
isActive
|
| 94 |
+
? "bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300"
|
| 95 |
+
: "text-neutral-600 hover:text-neutral-900 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:text-neutral-100 dark:hover:bg-neutral-700",
|
| 96 |
].join(" ")}
|
| 97 |
aria-current={isActive ? "page" : undefined}
|
| 98 |
>
|
|
|
|
| 109 |
<button
|
| 110 |
type="button"
|
| 111 |
onClick={onToggleTheme}
|
| 112 |
+
className="inline-flex items-center justify-center rounded-lg p-2 text-neutral-600 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-700 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
| 113 |
+
aria-label={t("header.switchTheme", { mode: isDark ? t("header.lightMode") : t("header.darkMode") })}
|
| 114 |
id="theme-toggle"
|
| 115 |
>
|
| 116 |
{isDark ? (
|
|
|
|
| 121 |
</button>
|
| 122 |
)}
|
| 123 |
|
| 124 |
+
{/* Language selector */}
|
| 125 |
+
{onLanguageChange && (
|
| 126 |
+
<LanguageSelector
|
| 127 |
+
currentLanguage={language}
|
| 128 |
+
onLanguageChange={onLanguageChange}
|
| 129 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
)}
|
| 131 |
|
| 132 |
+
{/* User Dashboard / Profile / Login */}
|
| 133 |
<div className="hidden md:flex items-center gap-1.5">
|
| 134 |
{isAuthenticated && user ? (
|
| 135 |
<>
|
| 136 |
+
<Link
|
| 137 |
+
to={ROUTES.DASHBOARD}
|
| 138 |
+
className={[
|
| 139 |
+
"inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500",
|
| 140 |
+
location.pathname === ROUTES.DASHBOARD
|
| 141 |
+
? "bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300"
|
| 142 |
+
: "text-neutral-600 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-700",
|
| 143 |
+
].join(" ")}
|
| 144 |
+
>
|
| 145 |
+
<Squares2X2Icon className="h-5 w-5" aria-hidden="true" />
|
| 146 |
+
{t("nav.dashboard")}
|
| 147 |
+
</Link>
|
| 148 |
<Link
|
| 149 |
to={ROUTES.PROFILE}
|
| 150 |
+
className={[
|
| 151 |
+
"inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500",
|
| 152 |
+
location.pathname === ROUTES.PROFILE
|
| 153 |
+
? "bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300"
|
| 154 |
+
: "text-neutral-600 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-700",
|
| 155 |
+
].join(" ")}
|
| 156 |
+
title={t("nav.profile")}
|
| 157 |
>
|
| 158 |
<UserCircleIcon className="h-5 w-5" aria-hidden="true" />
|
| 159 |
<span className="max-w-[100px] truncate">{user.name?.split(" ")[0]}</span>
|
|
|
|
| 161 |
<button
|
| 162 |
type="button"
|
| 163 |
onClick={handleLogout}
|
| 164 |
+
className="inline-flex items-center justify-center rounded-lg p-2 text-neutral-500 hover:bg-neutral-100 hover:text-neutral-700 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:hover:text-neutral-200 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
| 165 |
+
aria-label={t("nav.logout")}
|
| 166 |
+
title={t("nav.logout")}
|
| 167 |
>
|
| 168 |
<ArrowRightOnRectangleIcon className="h-5 w-5" aria-hidden="true" />
|
| 169 |
</button>
|
|
|
|
| 173 |
to={ROUTES.LOGIN}
|
| 174 |
className="inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium bg-primary-600 text-white hover:bg-primary-700 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
| 175 |
>
|
| 176 |
+
{t("nav.login")}
|
| 177 |
</Link>
|
| 178 |
)}
|
| 179 |
</div>
|
|
|
|
| 182 |
<button
|
| 183 |
type="button"
|
| 184 |
onClick={toggleMobile}
|
| 185 |
+
className="md:hidden inline-flex items-center justify-center rounded-lg p-2 text-neutral-600 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-700 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
| 186 |
aria-expanded={mobileMenuOpen}
|
| 187 |
aria-controls="mobile-menu"
|
| 188 |
+
aria-label={t("header.toggleMenu")}
|
| 189 |
>
|
| 190 |
{mobileMenuOpen ? (
|
| 191 |
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
|
|
|
| 201 |
{mobileMenuOpen && (
|
| 202 |
<nav
|
| 203 |
id="mobile-menu"
|
| 204 |
+
className="md:hidden border-t border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 animate-slide-down"
|
| 205 |
aria-label="Mobile navigation"
|
| 206 |
>
|
| 207 |
<div className="px-4 py-3 space-y-1">
|
|
|
|
| 215 |
className={[
|
| 216 |
"block px-3 py-2 rounded-lg text-sm font-medium transition-colors",
|
| 217 |
isActive
|
| 218 |
+
? "bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300"
|
| 219 |
+
: "text-neutral-600 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-700",
|
| 220 |
].join(" ")}
|
| 221 |
aria-current={isActive ? "page" : undefined}
|
| 222 |
>
|
|
|
|
| 226 |
})}
|
| 227 |
|
| 228 |
{/* Mobile Auth Links */}
|
| 229 |
+
<div className="border-t border-neutral-200 dark:border-neutral-700 mt-2 pt-2">
|
| 230 |
{isAuthenticated && user ? (
|
| 231 |
<>
|
| 232 |
+
<Link
|
| 233 |
+
to={ROUTES.DASHBOARD}
|
| 234 |
+
onClick={closeMobile}
|
| 235 |
+
className={[
|
| 236 |
+
"flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-colors",
|
| 237 |
+
location.pathname === ROUTES.DASHBOARD
|
| 238 |
+
? "bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300"
|
| 239 |
+
: "text-neutral-600 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-700",
|
| 240 |
+
].join(" ")}
|
| 241 |
+
>
|
| 242 |
+
<Squares2X2Icon className="h-5 w-5" />
|
| 243 |
+
{t("nav.dashboard")}
|
| 244 |
+
</Link>
|
| 245 |
<Link
|
| 246 |
to={ROUTES.PROFILE}
|
| 247 |
onClick={closeMobile}
|
| 248 |
className={[
|
| 249 |
"flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium transition-colors",
|
| 250 |
location.pathname === ROUTES.PROFILE
|
| 251 |
+
? "bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300"
|
| 252 |
+
: "text-neutral-600 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-700",
|
| 253 |
].join(" ")}
|
| 254 |
>
|
| 255 |
<UserCircleIcon className="h-5 w-5" />
|
| 256 |
+
{t("nav.profile")}
|
| 257 |
</Link>
|
| 258 |
<button
|
| 259 |
type="button"
|
| 260 |
onClick={handleLogout}
|
| 261 |
+
className="flex items-center gap-2 w-full px-3 py-2 rounded-lg text-sm font-medium text-neutral-600 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-700 transition-colors"
|
| 262 |
>
|
| 263 |
<ArrowRightOnRectangleIcon className="h-5 w-5" />
|
| 264 |
+
{t("nav.logout")}
|
| 265 |
</button>
|
| 266 |
</>
|
| 267 |
) : (
|
|
|
|
| 270 |
onClick={closeMobile}
|
| 271 |
className="block px-3 py-2 rounded-lg text-sm font-medium bg-primary-600 text-white text-center hover:bg-primary-700 transition-colors"
|
| 272 |
>
|
| 273 |
+
{t("nav.login")}
|
| 274 |
</Link>
|
| 275 |
)}
|
| 276 |
</div>
|
|
|
|
| 283 |
|
| 284 |
Header.propTypes = {
|
| 285 |
language: PropTypes.string,
|
| 286 |
+
onLanguageChange: PropTypes.func,
|
| 287 |
theme: PropTypes.string,
|
| 288 |
onToggleTheme: PropTypes.func,
|
| 289 |
};
|
frontend/src/components/common/LanguageSelector.jsx
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* LanguageSelector - Dropdown component for selecting UI language.
|
| 3 |
+
*
|
| 4 |
+
* Supports English, Hindi, and Gujarati.
|
| 5 |
+
* Persists selection to localStorage and updates i18n context.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import { useState, useRef, useEffect, useCallback } from "react";
|
| 9 |
+
import { useTranslation } from "react-i18next";
|
| 10 |
+
import PropTypes from "prop-types";
|
| 11 |
+
import { ChevronDownIcon, LanguageIcon, CheckIcon } from "@heroicons/react/24/outline";
|
| 12 |
+
import { LANGUAGES, LANGUAGE_NAMES } from "@utils/constants";
|
| 13 |
+
|
| 14 |
+
const LANGUAGE_OPTIONS = [
|
| 15 |
+
{ code: LANGUAGES.EN, name: LANGUAGE_NAMES[LANGUAGES.EN], nativeName: "English" },
|
| 16 |
+
{ code: LANGUAGES.HI, name: LANGUAGE_NAMES[LANGUAGES.HI], nativeName: "Hindi" },
|
| 17 |
+
{ code: LANGUAGES.GU, name: LANGUAGE_NAMES[LANGUAGES.GU], nativeName: "Gujarati" },
|
| 18 |
+
];
|
| 19 |
+
|
| 20 |
+
function LanguageSelector({ currentLanguage, onLanguageChange, compact = false }) {
|
| 21 |
+
const { t } = useTranslation();
|
| 22 |
+
const [isOpen, setIsOpen] = useState(false);
|
| 23 |
+
const dropdownRef = useRef(null);
|
| 24 |
+
|
| 25 |
+
const currentOption = LANGUAGE_OPTIONS.find((opt) => opt.code === currentLanguage) || LANGUAGE_OPTIONS[0];
|
| 26 |
+
|
| 27 |
+
const handleSelect = useCallback(
|
| 28 |
+
(langCode) => {
|
| 29 |
+
onLanguageChange(langCode);
|
| 30 |
+
setIsOpen(false);
|
| 31 |
+
},
|
| 32 |
+
[onLanguageChange]
|
| 33 |
+
);
|
| 34 |
+
|
| 35 |
+
const toggleDropdown = useCallback(() => {
|
| 36 |
+
setIsOpen((prev) => !prev);
|
| 37 |
+
}, []);
|
| 38 |
+
|
| 39 |
+
useEffect(() => {
|
| 40 |
+
function handleClickOutside(event) {
|
| 41 |
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
| 42 |
+
setIsOpen(false);
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
function handleEscape(event) {
|
| 47 |
+
if (event.key === "Escape") {
|
| 48 |
+
setIsOpen(false);
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
if (isOpen) {
|
| 53 |
+
document.addEventListener("mousedown", handleClickOutside);
|
| 54 |
+
document.addEventListener("keydown", handleEscape);
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
return () => {
|
| 58 |
+
document.removeEventListener("mousedown", handleClickOutside);
|
| 59 |
+
document.removeEventListener("keydown", handleEscape);
|
| 60 |
+
};
|
| 61 |
+
}, [isOpen]);
|
| 62 |
+
|
| 63 |
+
return (
|
| 64 |
+
<div className="relative" ref={dropdownRef}>
|
| 65 |
+
<button
|
| 66 |
+
type="button"
|
| 67 |
+
onClick={toggleDropdown}
|
| 68 |
+
className="inline-flex items-center gap-1.5 rounded-lg px-2.5 py-1.5 text-sm font-medium text-neutral-600 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-700 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
| 69 |
+
aria-expanded={isOpen}
|
| 70 |
+
aria-haspopup="listbox"
|
| 71 |
+
aria-label={t("common.selectLanguage")}
|
| 72 |
+
id="language-selector"
|
| 73 |
+
>
|
| 74 |
+
<LanguageIcon className="h-4 w-4" aria-hidden="true" />
|
| 75 |
+
{!compact && (
|
| 76 |
+
<>
|
| 77 |
+
<span className="hidden xs:inline">{currentOption.name}</span>
|
| 78 |
+
<ChevronDownIcon
|
| 79 |
+
className={`h-3.5 w-3.5 transition-transform duration-200 ${isOpen ? "rotate-180" : ""}`}
|
| 80 |
+
aria-hidden="true"
|
| 81 |
+
/>
|
| 82 |
+
</>
|
| 83 |
+
)}
|
| 84 |
+
{compact && <span className="uppercase text-xs">{currentOption.code}</span>}
|
| 85 |
+
</button>
|
| 86 |
+
|
| 87 |
+
{isOpen && (
|
| 88 |
+
<div
|
| 89 |
+
className="absolute right-0 z-50 mt-1 w-44 origin-top-right rounded-lg bg-white dark:bg-neutral-800 shadow-lg ring-1 ring-black/5 dark:ring-white/10 focus:outline-none animate-fade-in"
|
| 90 |
+
role="listbox"
|
| 91 |
+
aria-label={t("common.selectLanguage")}
|
| 92 |
+
>
|
| 93 |
+
<div className="py-1">
|
| 94 |
+
{LANGUAGE_OPTIONS.map((option) => {
|
| 95 |
+
const isSelected = option.code === currentLanguage;
|
| 96 |
+
return (
|
| 97 |
+
<button
|
| 98 |
+
key={option.code}
|
| 99 |
+
type="button"
|
| 100 |
+
onClick={() => handleSelect(option.code)}
|
| 101 |
+
className={`
|
| 102 |
+
w-full flex items-center justify-between px-3 py-2 text-sm transition-colors
|
| 103 |
+
${
|
| 104 |
+
isSelected
|
| 105 |
+
? "bg-primary-50 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300"
|
| 106 |
+
: "text-neutral-700 hover:bg-neutral-50 dark:text-neutral-200 dark:hover:bg-neutral-700"
|
| 107 |
+
}
|
| 108 |
+
`}
|
| 109 |
+
role="option"
|
| 110 |
+
aria-selected={isSelected}
|
| 111 |
+
>
|
| 112 |
+
<span className="flex flex-col items-start">
|
| 113 |
+
<span className="font-medium">{option.name}</span>
|
| 114 |
+
<span className="text-xs text-neutral-500 dark:text-neutral-400">
|
| 115 |
+
{option.nativeName}
|
| 116 |
+
</span>
|
| 117 |
+
</span>
|
| 118 |
+
{isSelected && (
|
| 119 |
+
<CheckIcon className="h-4 w-4 text-primary-600 dark:text-primary-400" aria-hidden="true" />
|
| 120 |
+
)}
|
| 121 |
+
</button>
|
| 122 |
+
);
|
| 123 |
+
})}
|
| 124 |
+
</div>
|
| 125 |
+
</div>
|
| 126 |
+
)}
|
| 127 |
+
</div>
|
| 128 |
+
);
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
LanguageSelector.propTypes = {
|
| 132 |
+
currentLanguage: PropTypes.oneOf([LANGUAGES.EN, LANGUAGES.HI, LANGUAGES.GU]).isRequired,
|
| 133 |
+
onLanguageChange: PropTypes.func.isRequired,
|
| 134 |
+
compact: PropTypes.bool,
|
| 135 |
+
};
|
| 136 |
+
|
| 137 |
+
export default LanguageSelector;
|
frontend/src/components/common/Layout.jsx
CHANGED
|
@@ -15,7 +15,7 @@ import { VoiceButton } from "@components/voice";
|
|
| 15 |
function Layout({
|
| 16 |
children,
|
| 17 |
language,
|
| 18 |
-
|
| 19 |
theme,
|
| 20 |
onToggleTheme,
|
| 21 |
hideHeader = false,
|
|
@@ -33,7 +33,7 @@ function Layout({
|
|
| 33 |
{!hideHeader && (
|
| 34 |
<Header
|
| 35 |
language={language}
|
| 36 |
-
|
| 37 |
theme={theme}
|
| 38 |
onToggleTheme={onToggleTheme}
|
| 39 |
/>
|
|
@@ -58,7 +58,7 @@ function Layout({
|
|
| 58 |
Layout.propTypes = {
|
| 59 |
children: PropTypes.node.isRequired,
|
| 60 |
language: PropTypes.string,
|
| 61 |
-
|
| 62 |
theme: PropTypes.string,
|
| 63 |
onToggleTheme: PropTypes.func,
|
| 64 |
hideHeader: PropTypes.bool,
|
|
|
|
| 15 |
function Layout({
|
| 16 |
children,
|
| 17 |
language,
|
| 18 |
+
onLanguageChange,
|
| 19 |
theme,
|
| 20 |
onToggleTheme,
|
| 21 |
hideHeader = false,
|
|
|
|
| 33 |
{!hideHeader && (
|
| 34 |
<Header
|
| 35 |
language={language}
|
| 36 |
+
onLanguageChange={onLanguageChange}
|
| 37 |
theme={theme}
|
| 38 |
onToggleTheme={onToggleTheme}
|
| 39 |
/>
|
|
|
|
| 58 |
Layout.propTypes = {
|
| 59 |
children: PropTypes.node.isRequired,
|
| 60 |
language: PropTypes.string,
|
| 61 |
+
onLanguageChange: PropTypes.func,
|
| 62 |
theme: PropTypes.string,
|
| 63 |
onToggleTheme: PropTypes.func,
|
| 64 |
hideHeader: PropTypes.bool,
|
frontend/src/components/common/index.js
CHANGED
|
@@ -17,6 +17,7 @@ export { default as FeatureTour } from "./FeatureTour";
|
|
| 17 |
export { default as Footer } from "./Footer";
|
| 18 |
export { default as Header } from "./Header";
|
| 19 |
export { default as Input } from "./Input";
|
|
|
|
| 20 |
export { default as Layout } from "./Layout";
|
| 21 |
export { default as LoadingSpinner } from "./LoadingSpinner";
|
| 22 |
export { default as Modal } from "./Modal";
|
|
|
|
| 17 |
export { default as Footer } from "./Footer";
|
| 18 |
export { default as Header } from "./Header";
|
| 19 |
export { default as Input } from "./Input";
|
| 20 |
+
export { default as LanguageSelector } from "./LanguageSelector";
|
| 21 |
export { default as Layout } from "./Layout";
|
| 22 |
export { default as LoadingSpinner } from "./LoadingSpinner";
|
| 23 |
export { default as Modal } from "./Modal";
|
frontend/src/context/AppContext.jsx
CHANGED
|
@@ -11,6 +11,7 @@
|
|
| 11 |
*/
|
| 12 |
|
| 13 |
import { createContext, useReducer, useEffect, useCallback } from "react";
|
|
|
|
| 14 |
import storage from "@utils/storage";
|
| 15 |
import {
|
| 16 |
STORAGE_KEYS,
|
|
@@ -127,12 +128,16 @@ export const AppContext = createContext(null);
|
|
| 127 |
|
| 128 |
export function AppProvider({ children }) {
|
| 129 |
const [state, dispatch] = useReducer(appReducer, null, buildInitialState);
|
|
|
|
| 130 |
|
| 131 |
-
// Persist language whenever it changes
|
| 132 |
useEffect(() => {
|
| 133 |
storage.set(STORAGE_KEYS.LANGUAGE, state.language);
|
| 134 |
document.documentElement.lang = state.language;
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
// Persist theme and apply dark class to <html>
|
| 138 |
useEffect(() => {
|
|
@@ -166,9 +171,10 @@ export function AppProvider({ children }) {
|
|
| 166 |
setLanguage,
|
| 167 |
|
| 168 |
toggleLanguage: () => {
|
| 169 |
-
const
|
| 170 |
-
|
| 171 |
-
|
|
|
|
| 172 |
},
|
| 173 |
|
| 174 |
setTheme: (theme) => {
|
|
|
|
| 11 |
*/
|
| 12 |
|
| 13 |
import { createContext, useReducer, useEffect, useCallback } from "react";
|
| 14 |
+
import { useTranslation } from "react-i18next";
|
| 15 |
import storage from "@utils/storage";
|
| 16 |
import {
|
| 17 |
STORAGE_KEYS,
|
|
|
|
| 128 |
|
| 129 |
export function AppProvider({ children }) {
|
| 130 |
const [state, dispatch] = useReducer(appReducer, null, buildInitialState);
|
| 131 |
+
const { i18n } = useTranslation();
|
| 132 |
|
| 133 |
+
// Persist language and sync with i18n whenever it changes
|
| 134 |
useEffect(() => {
|
| 135 |
storage.set(STORAGE_KEYS.LANGUAGE, state.language);
|
| 136 |
document.documentElement.lang = state.language;
|
| 137 |
+
if (i18n.language !== state.language) {
|
| 138 |
+
i18n.changeLanguage(state.language);
|
| 139 |
+
}
|
| 140 |
+
}, [state.language, i18n]);
|
| 141 |
|
| 142 |
// Persist theme and apply dark class to <html>
|
| 143 |
useEffect(() => {
|
|
|
|
| 171 |
setLanguage,
|
| 172 |
|
| 173 |
toggleLanguage: () => {
|
| 174 |
+
const langs = [LANGUAGES.EN, LANGUAGES.HI, LANGUAGES.GU];
|
| 175 |
+
const currentIndex = langs.indexOf(state.language);
|
| 176 |
+
const nextIndex = (currentIndex + 1) % langs.length;
|
| 177 |
+
dispatch({ type: ACTION_TYPES.SET_LANGUAGE, payload: langs[nextIndex] });
|
| 178 |
},
|
| 179 |
|
| 180 |
setTheme: (theme) => {
|
frontend/src/i18n/index.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* i18n Configuration
|
| 3 |
+
*
|
| 4 |
+
* Internationalization setup using i18next and react-i18next.
|
| 5 |
+
* Supports English, Hindi, and Gujarati languages.
|
| 6 |
+
*/
|
| 7 |
+
|
| 8 |
+
import i18n from "i18next";
|
| 9 |
+
import { initReactI18next } from "react-i18next";
|
| 10 |
+
import LanguageDetector from "i18next-browser-languagedetector";
|
| 11 |
+
|
| 12 |
+
import enTranslation from "@locales/en/translation.json";
|
| 13 |
+
import hiTranslation from "@locales/hi/translation.json";
|
| 14 |
+
import guTranslation from "@locales/gu/translation.json";
|
| 15 |
+
|
| 16 |
+
import { STORAGE_KEYS, DEFAULT_LANGUAGE } from "@utils/constants";
|
| 17 |
+
|
| 18 |
+
const resources = {
|
| 19 |
+
en: {
|
| 20 |
+
translation: enTranslation,
|
| 21 |
+
},
|
| 22 |
+
hi: {
|
| 23 |
+
translation: hiTranslation,
|
| 24 |
+
},
|
| 25 |
+
gu: {
|
| 26 |
+
translation: guTranslation,
|
| 27 |
+
},
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
i18n
|
| 31 |
+
.use(LanguageDetector)
|
| 32 |
+
.use(initReactI18next)
|
| 33 |
+
.init({
|
| 34 |
+
resources,
|
| 35 |
+
fallbackLng: DEFAULT_LANGUAGE,
|
| 36 |
+
debug: import.meta.env.DEV,
|
| 37 |
+
|
| 38 |
+
detection: {
|
| 39 |
+
order: ["localStorage", "navigator"],
|
| 40 |
+
lookupLocalStorage: STORAGE_KEYS.LANGUAGE,
|
| 41 |
+
caches: ["localStorage"],
|
| 42 |
+
},
|
| 43 |
+
|
| 44 |
+
interpolation: {
|
| 45 |
+
escapeValue: false,
|
| 46 |
+
},
|
| 47 |
+
|
| 48 |
+
react: {
|
| 49 |
+
useSuspense: true,
|
| 50 |
+
},
|
| 51 |
+
});
|
| 52 |
+
|
| 53 |
+
export default i18n;
|
frontend/src/locales/en/translation.json
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"common": {
|
| 3 |
+
"appName": "KrishiNiti",
|
| 4 |
+
"loading": "Loading...",
|
| 5 |
+
"error": "An error occurred",
|
| 6 |
+
"retry": "Retry",
|
| 7 |
+
"cancel": "Cancel",
|
| 8 |
+
"save": "Save",
|
| 9 |
+
"delete": "Delete",
|
| 10 |
+
"edit": "Edit",
|
| 11 |
+
"submit": "Submit",
|
| 12 |
+
"search": "Search",
|
| 13 |
+
"filter": "Filter",
|
| 14 |
+
"close": "Close",
|
| 15 |
+
"back": "Back",
|
| 16 |
+
"next": "Next",
|
| 17 |
+
"previous": "Previous",
|
| 18 |
+
"yes": "Yes",
|
| 19 |
+
"no": "No",
|
| 20 |
+
"ok": "OK",
|
| 21 |
+
"viewAll": "View All",
|
| 22 |
+
"seeMore": "See More",
|
| 23 |
+
"getStarted": "Get started",
|
| 24 |
+
"learnMore": "Learn More",
|
| 25 |
+
"noDataFound": "No data found",
|
| 26 |
+
"offline": "You are currently offline. Some features may be limited.",
|
| 27 |
+
"connected": "Connected",
|
| 28 |
+
"selectLanguage": "Select Language"
|
| 29 |
+
},
|
| 30 |
+
"languages": {
|
| 31 |
+
"en": "English",
|
| 32 |
+
"hi": "Hindi",
|
| 33 |
+
"gu": "Gujarati"
|
| 34 |
+
},
|
| 35 |
+
"nav": {
|
| 36 |
+
"home": "Home",
|
| 37 |
+
"diseaseDetection": "Disease Detection",
|
| 38 |
+
"weather": "Weather",
|
| 39 |
+
"apmcPrice": "APMC Price",
|
| 40 |
+
"schemes": "Schemes",
|
| 41 |
+
"profile": "Profile",
|
| 42 |
+
"dashboard": "My Dashboard",
|
| 43 |
+
"login": "Login",
|
| 44 |
+
"logout": "Logout",
|
| 45 |
+
"signup": "Sign Up"
|
| 46 |
+
},
|
| 47 |
+
"header": {
|
| 48 |
+
"switchTheme": "Switch to {{mode}} mode",
|
| 49 |
+
"lightMode": "light",
|
| 50 |
+
"darkMode": "dark",
|
| 51 |
+
"toggleMenu": "Toggle navigation menu"
|
| 52 |
+
},
|
| 53 |
+
"home": {
|
| 54 |
+
"greeting": {
|
| 55 |
+
"morning": "Good morning",
|
| 56 |
+
"afternoon": "Good afternoon",
|
| 57 |
+
"evening": "Good evening"
|
| 58 |
+
},
|
| 59 |
+
"tagline": "AI-powered crop disease detection, weather forecasting, and market price tracking for Indian farmers.",
|
| 60 |
+
"searchPlaceholder": "Search features... (e.g. crop disease, weather, APMC prices)",
|
| 61 |
+
"recentActivity": "Recent Activity",
|
| 62 |
+
"completeProfile": "Complete Your Profile",
|
| 63 |
+
"addCropsMessage": "Add your crops to get personalized APMC price alerts and farming recommendations.",
|
| 64 |
+
"addCropsNow": "Add Crops Now"
|
| 65 |
+
},
|
| 66 |
+
"features": {
|
| 67 |
+
"diseaseDetection": {
|
| 68 |
+
"title": "Disease Detection",
|
| 69 |
+
"description": "Upload a photo of your crop and get instant AI-powered disease identification with treatment recommendations.",
|
| 70 |
+
"aiPowered": "AI-powered"
|
| 71 |
+
},
|
| 72 |
+
"weather": {
|
| 73 |
+
"title": "Weather Forecast",
|
| 74 |
+
"description": "Get accurate 7-day weather forecasts, farming advisories, and severe weather alerts for your area.",
|
| 75 |
+
"sevenDay": "7-Day Forecast",
|
| 76 |
+
"pincodeBased": "Pincode-based"
|
| 77 |
+
},
|
| 78 |
+
"apmc": {
|
| 79 |
+
"title": "APMC Price",
|
| 80 |
+
"description": "Compare real-time commodity prices across APMCs, find the best selling market, and track price trends.",
|
| 81 |
+
"livePrices": "Live Prices"
|
| 82 |
+
},
|
| 83 |
+
"schemes": {
|
| 84 |
+
"title": "Government Schemes",
|
| 85 |
+
"description": "Explore national and state-specific schemes with complete details on benefits, eligibility, and application process."
|
| 86 |
+
}
|
| 87 |
+
},
|
| 88 |
+
"auth": {
|
| 89 |
+
"login": "Login",
|
| 90 |
+
"signup": "Sign Up",
|
| 91 |
+
"logout": "Logout",
|
| 92 |
+
"email": "Email",
|
| 93 |
+
"password": "Password",
|
| 94 |
+
"confirmPassword": "Confirm Password",
|
| 95 |
+
"name": "Full Name",
|
| 96 |
+
"phone": "Phone Number",
|
| 97 |
+
"forgotPassword": "Forgot Password?",
|
| 98 |
+
"noAccount": "Don't have an account?",
|
| 99 |
+
"hasAccount": "Already have an account?",
|
| 100 |
+
"loginSuccess": "Login successful",
|
| 101 |
+
"signupSuccess": "Account created successfully",
|
| 102 |
+
"logoutSuccess": "Logged out successfully"
|
| 103 |
+
},
|
| 104 |
+
"profile": {
|
| 105 |
+
"title": "My Profile",
|
| 106 |
+
"personalInfo": "Personal Information",
|
| 107 |
+
"farmDetails": "Farm Details",
|
| 108 |
+
"crops": "My Crops",
|
| 109 |
+
"location": "Location",
|
| 110 |
+
"pincode": "Pincode",
|
| 111 |
+
"state": "State",
|
| 112 |
+
"district": "District",
|
| 113 |
+
"landSize": "Land Size (in acres)",
|
| 114 |
+
"addCrop": "Add Crop",
|
| 115 |
+
"removeCrop": "Remove Crop",
|
| 116 |
+
"updateProfile": "Update Profile",
|
| 117 |
+
"profileUpdated": "Profile updated successfully"
|
| 118 |
+
},
|
| 119 |
+
"disease": {
|
| 120 |
+
"title": "Disease Detection",
|
| 121 |
+
"uploadImage": "Upload Image",
|
| 122 |
+
"takePhoto": "Take Photo",
|
| 123 |
+
"selectCrop": "Select Crop Type",
|
| 124 |
+
"analyzing": "Analyzing...",
|
| 125 |
+
"result": "Detection Result",
|
| 126 |
+
"disease": "Disease",
|
| 127 |
+
"confidence": "Confidence",
|
| 128 |
+
"treatment": "Treatment",
|
| 129 |
+
"prevention": "Prevention",
|
| 130 |
+
"noDisease": "No disease detected",
|
| 131 |
+
"healthyCrop": "Your crop appears to be healthy",
|
| 132 |
+
"uploadPrompt": "Upload a clear photo of the affected plant leaf for accurate detection"
|
| 133 |
+
},
|
| 134 |
+
"weather": {
|
| 135 |
+
"title": "Weather Forecast",
|
| 136 |
+
"currentWeather": "Current Weather",
|
| 137 |
+
"weeklyForecast": "Weekly Forecast",
|
| 138 |
+
"farmingAdvice": "Farming Advice",
|
| 139 |
+
"alerts": "Weather Alerts",
|
| 140 |
+
"temperature": "Temperature",
|
| 141 |
+
"humidity": "Humidity",
|
| 142 |
+
"wind": "Wind",
|
| 143 |
+
"rain": "Rain",
|
| 144 |
+
"sunrise": "Sunrise",
|
| 145 |
+
"sunset": "Sunset",
|
| 146 |
+
"feelsLike": "Feels like",
|
| 147 |
+
"enterPincode": "Enter your pincode",
|
| 148 |
+
"getWeather": "Get Weather",
|
| 149 |
+
"noAlerts": "No weather alerts"
|
| 150 |
+
},
|
| 151 |
+
"apmc": {
|
| 152 |
+
"title": "APMC Market Prices",
|
| 153 |
+
"selectCommodity": "Select Commodity",
|
| 154 |
+
"selectState": "Select State",
|
| 155 |
+
"selectMarket": "Select Market",
|
| 156 |
+
"minPrice": "Min Price",
|
| 157 |
+
"maxPrice": "Max Price",
|
| 158 |
+
"modalPrice": "Modal Price",
|
| 159 |
+
"pricePerQuintal": "Price per Quintal",
|
| 160 |
+
"bestMarket": "Best Market",
|
| 161 |
+
"priceAlerts": "Price Alerts",
|
| 162 |
+
"createAlert": "Create Alert",
|
| 163 |
+
"sellAdvisory": "Sell Advisory",
|
| 164 |
+
"noData": "No price data available"
|
| 165 |
+
},
|
| 166 |
+
"schemes": {
|
| 167 |
+
"title": "Government Schemes",
|
| 168 |
+
"nationalSchemes": "National Schemes",
|
| 169 |
+
"stateSchemes": "State Schemes",
|
| 170 |
+
"eligibility": "Eligibility",
|
| 171 |
+
"benefits": "Benefits",
|
| 172 |
+
"howToApply": "How to Apply",
|
| 173 |
+
"documents": "Required Documents",
|
| 174 |
+
"deadline": "Application Deadline",
|
| 175 |
+
"website": "Official Website",
|
| 176 |
+
"applyNow": "Apply Now",
|
| 177 |
+
"noSchemes": "No schemes found"
|
| 178 |
+
},
|
| 179 |
+
"voice": {
|
| 180 |
+
"title": "Voice Assistant",
|
| 181 |
+
"listening": "Listening...",
|
| 182 |
+
"processing": "Processing...",
|
| 183 |
+
"speak": "Speak now",
|
| 184 |
+
"tapToSpeak": "Tap to speak",
|
| 185 |
+
"voiceCommands": "Voice Commands",
|
| 186 |
+
"tutorial": "Voice Tutorial"
|
| 187 |
+
},
|
| 188 |
+
"errors": {
|
| 189 |
+
"networkError": "Network error. Please check your connection.",
|
| 190 |
+
"serverError": "Server error. Please try again later.",
|
| 191 |
+
"invalidInput": "Invalid input. Please check your entries.",
|
| 192 |
+
"sessionExpired": "Session expired. Please login again.",
|
| 193 |
+
"permissionDenied": "Permission denied.",
|
| 194 |
+
"notFound": "Resource not found."
|
| 195 |
+
},
|
| 196 |
+
"footer": {
|
| 197 |
+
"copyright": "KrishiNiti. All rights reserved.",
|
| 198 |
+
"madeWith": "Made with love for Indian farmers"
|
| 199 |
+
}
|
| 200 |
+
}
|
frontend/src/locales/gu/translation.json
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"common": {
|
| 3 |
+
"appName": "કૃષિનીતિ",
|
| 4 |
+
"loading": "લોડ થઈ રહ્યું છે...",
|
| 5 |
+
"error": "એક ભૂલ આવી",
|
| 6 |
+
"retry": "ફરીથી પ્રયાસ કરો",
|
| 7 |
+
"cancel": "રદ કરો",
|
| 8 |
+
"save": "સાચવો",
|
| 9 |
+
"delete": "કાઢી નાખો",
|
| 10 |
+
"edit": "સંપાદિત કરો",
|
| 11 |
+
"submit": "સબમિટ કરો",
|
| 12 |
+
"search": "શોધો",
|
| 13 |
+
"filter": "ફિલ્ટર",
|
| 14 |
+
"close": "બંધ કરો",
|
| 15 |
+
"back": "પાછા",
|
| 16 |
+
"next": "આગળ",
|
| 17 |
+
"previous": "પાછલું",
|
| 18 |
+
"yes": "હા",
|
| 19 |
+
"no": "ના",
|
| 20 |
+
"ok": "ઠીક છે",
|
| 21 |
+
"viewAll": "બધું જુઓ",
|
| 22 |
+
"seeMore": "વધુ જુઓ",
|
| 23 |
+
"getStarted": "શરૂ કરો",
|
| 24 |
+
"learnMore": "વધુ જાણો",
|
| 25 |
+
"noDataFound": "કોઈ ડેટા મળ્યો નથી",
|
| 26 |
+
"offline": "તમે હાલમાં ઑફલાઈન છો. કેટલીક સુવિધાઓ મર્યાદિત હોઈ શકે છે.",
|
| 27 |
+
"connected": "કનેક્ટેડ",
|
| 28 |
+
"selectLanguage": "ભાષા પસંદ કરો"
|
| 29 |
+
},
|
| 30 |
+
"languages": {
|
| 31 |
+
"en": "અંગ્રેજી",
|
| 32 |
+
"hi": "હિન્દી",
|
| 33 |
+
"gu": "ગુજરાતી"
|
| 34 |
+
},
|
| 35 |
+
"nav": {
|
| 36 |
+
"home": "હોમ",
|
| 37 |
+
"diseaseDetection": "રોગ શોધ",
|
| 38 |
+
"weather": "હવામાન",
|
| 39 |
+
"apmcPrice": "APMC ભાવ",
|
| 40 |
+
"schemes": "યોજનાઓ",
|
| 41 |
+
"profile": "પ્રોફાઇલ",
|
| 42 |
+
"dashboard": "મારું ડેશબોર્ડ",
|
| 43 |
+
"login": "લૉગિન",
|
| 44 |
+
"logout": "લૉગઆઉટ",
|
| 45 |
+
"signup": "સાઇન અપ"
|
| 46 |
+
},
|
| 47 |
+
"header": {
|
| 48 |
+
"switchTheme": "{{mode}} મોડમાં બદલો",
|
| 49 |
+
"lightMode": "લાઇટ",
|
| 50 |
+
"darkMode": "ડાર્ક",
|
| 51 |
+
"toggleMenu": "નેવિગેશન મેનૂ ટૉગલ કરો"
|
| 52 |
+
},
|
| 53 |
+
"home": {
|
| 54 |
+
"greeting": {
|
| 55 |
+
"morning": "સુપ્રભાત",
|
| 56 |
+
"afternoon": "નમસ્તે",
|
| 57 |
+
"evening": "શુભ સંધ્યા"
|
| 58 |
+
},
|
| 59 |
+
"tagline": "ભારતીય ખેડૂતો માટે AI-સંચાલિત પાક રોગ શોધ, હવામાન આગાહી અને બજાર ભાવ ટ્રેકિંગ.",
|
| 60 |
+
"searchPlaceholder": "સુવિધાઓ શોધો... (દા.ત. પાક રોગ, હવામાન, APMC ભાવ)",
|
| 61 |
+
"recentActivity": "તાજેતરની પ્રવૃત્તિ",
|
| 62 |
+
"completeProfile": "તમારી પ્રોફાઇલ પૂર્ણ કરો",
|
| 63 |
+
"addCropsMessage": "વ્યક્તિગત APMC ભાવ અલર્ટ અને ખેતીની ભલામણો મેળવવા માટે તમારા પાક ઉમેરો.",
|
| 64 |
+
"addCropsNow": "હવે પાક ઉમેરો"
|
| 65 |
+
},
|
| 66 |
+
"features": {
|
| 67 |
+
"diseaseDetection": {
|
| 68 |
+
"title": "રોગ શોધ",
|
| 69 |
+
"description": "તમારા પાકનો ફોટો અપલોડ કરો અને સારવારની ભલામણો સાથે તાત્કાલિક AI-સંચાલિત રોગ ઓળખ મેળવો.",
|
| 70 |
+
"aiPowered": "AI-સંચાલિત"
|
| 71 |
+
},
|
| 72 |
+
"weather": {
|
| 73 |
+
"title": "હવામાન આગાહી",
|
| 74 |
+
"description": "તમારા વિસ્તાર માટે ચોક્કસ 7-દિવસીય હવામાન આગાહી, ખેતી સલાહ અને ગંભીર હવામાન અલર્ટ મેળવો.",
|
| 75 |
+
"sevenDay": "7-દિવસીય આગાહી",
|
| 76 |
+
"pincodeBased": "પિનકોડ આધારિત"
|
| 77 |
+
},
|
| 78 |
+
"apmc": {
|
| 79 |
+
"title": "APMC ભાવ",
|
| 80 |
+
"description": "વિવિધ APMC માં રીઅલ-ટાઈમ કોમોડિટી ભાવોની સરખામણી કરો, શ્રેષ્ઠ વેચાણ બજાર શોધો અને ભાવ વલણો ટ્રેક કરો.",
|
| 81 |
+
"livePrices": "લાઇવ ભાવ"
|
| 82 |
+
},
|
| 83 |
+
"schemes": {
|
| 84 |
+
"title": "સરકારી યોજનાઓ",
|
| 85 |
+
"description": "લાભો, પાત્રતા અને અરજી પ્રક્રિયાની સંપૂર્ણ વિગતો સાથે રાષ્ટ્રીય અને રાજ્ય-વિશિષ્ટ યોજનાઓ શોધો."
|
| 86 |
+
}
|
| 87 |
+
},
|
| 88 |
+
"auth": {
|
| 89 |
+
"login": "લૉગિન",
|
| 90 |
+
"signup": "સાઇન અપ",
|
| 91 |
+
"logout": "લૉગઆઉટ",
|
| 92 |
+
"email": "ઈમેલ",
|
| 93 |
+
"password": "પાસવર્ડ",
|
| 94 |
+
"confirmPassword": "પાસવર્ડની પુષ્ટિ કરો",
|
| 95 |
+
"name": "પૂરું નામ",
|
| 96 |
+
"phone": "ફોન નંબર",
|
| 97 |
+
"forgotPassword": "પાસવર્ડ ભૂલી ગયા?",
|
| 98 |
+
"noAccount": "એકાઉન્ટ નથી?",
|
| 99 |
+
"hasAccount": "��હેલેથી એકાઉન્ટ છે?",
|
| 100 |
+
"loginSuccess": "લૉગિન સફળ",
|
| 101 |
+
"signupSuccess": "એકાઉન્ટ સફળતાપૂર્વક બનાવ્યું",
|
| 102 |
+
"logoutSuccess": "સફળતાપૂર્વક લૉગઆઉટ થયું"
|
| 103 |
+
},
|
| 104 |
+
"profile": {
|
| 105 |
+
"title": "મારી પ્રોફાઇલ",
|
| 106 |
+
"personalInfo": "વ્યક્તિગત માહિતી",
|
| 107 |
+
"farmDetails": "ખેતર વિગતો",
|
| 108 |
+
"crops": "મારા પાક",
|
| 109 |
+
"location": "સ્થાન",
|
| 110 |
+
"pincode": "પિનકોડ",
|
| 111 |
+
"state": "રાજ્ય",
|
| 112 |
+
"district": "જિલ્લો",
|
| 113 |
+
"landSize": "જમીનનું કદ (એકરમાં)",
|
| 114 |
+
"addCrop": "પાક ઉમેરો",
|
| 115 |
+
"removeCrop": "પાક દૂર કરો",
|
| 116 |
+
"updateProfile": "પ્રોફાઇલ અપડેટ કરો",
|
| 117 |
+
"profileUpdated": "પ્રોફાઇલ સફળતાપૂર્વક અપડેટ થઈ"
|
| 118 |
+
},
|
| 119 |
+
"disease": {
|
| 120 |
+
"title": "રોગ શોધ",
|
| 121 |
+
"uploadImage": "છબી અપલોડ કરો",
|
| 122 |
+
"takePhoto": "ફોટો લો",
|
| 123 |
+
"selectCrop": "પાક પ્રકાર પસંદ કરો",
|
| 124 |
+
"analyzing": "વિશ્લેષણ થઈ રહ્યું છે...",
|
| 125 |
+
"result": "શોધ પરિણામ",
|
| 126 |
+
"disease": "રોગ",
|
| 127 |
+
"confidence": "વિશ્વાસ સ્તર",
|
| 128 |
+
"treatment": "સારવાર",
|
| 129 |
+
"prevention": "નિવારણ",
|
| 130 |
+
"noDisease": "કોઈ રોગ મળ્યો નથી",
|
| 131 |
+
"healthyCrop": "તમારો પાક સ્વસ્થ દેખાય છે",
|
| 132 |
+
"uploadPrompt": "ચોક્કસ શોધ માટે અસરગ્રસ્ત છોડના પાંદડાનો સ્પષ્ટ ફોટો અપલોડ કરો"
|
| 133 |
+
},
|
| 134 |
+
"weather": {
|
| 135 |
+
"title": "હવામાન આગાહી",
|
| 136 |
+
"currentWeather": "વર્તમાન હવામાન",
|
| 137 |
+
"weeklyForecast": "સાપ્તાહિક આગાહી",
|
| 138 |
+
"farmingAdvice": "ખેતી સલાહ",
|
| 139 |
+
"alerts": "હવામાન અલર્ટ",
|
| 140 |
+
"temperature": "તાપમાન",
|
| 141 |
+
"humidity": "ભેજ",
|
| 142 |
+
"wind": "પવન",
|
| 143 |
+
"rain": "વરસાદ",
|
| 144 |
+
"sunrise": "સૂર્યોદય",
|
| 145 |
+
"sunset": "સૂર્યાસ્ત",
|
| 146 |
+
"feelsLike": "લાગે છે",
|
| 147 |
+
"enterPincode": "તમારો પિનકોડ દાખલ કરો",
|
| 148 |
+
"getWeather": "હવામાન જુઓ",
|
| 149 |
+
"noAlerts": "કોઈ હવામાન અલર્ટ નથી"
|
| 150 |
+
},
|
| 151 |
+
"apmc": {
|
| 152 |
+
"title": "APMC બજાર ભાવ",
|
| 153 |
+
"selectCommodity": "કોમોડિટી પસંદ કરો",
|
| 154 |
+
"selectState": "રાજ્ય પસંદ કરો",
|
| 155 |
+
"selectMarket": "બજાર પસંદ કરો",
|
| 156 |
+
"minPrice": "ન્યૂનતમ ભાવ",
|
| 157 |
+
"maxPrice": "મહત્તમ ભાવ",
|
| 158 |
+
"modalPrice": "મોડલ ભાવ",
|
| 159 |
+
"pricePerQuintal": "ક્વિન્ટલ દીઠ ભાવ",
|
| 160 |
+
"bestMarket": "શ્રેષ્ઠ બજાર",
|
| 161 |
+
"priceAlerts": "ભાવ અલર્ટ",
|
| 162 |
+
"createAlert": "અલર્ટ બનાવો",
|
| 163 |
+
"sellAdvisory": "વેચાણ સલાહ",
|
| 164 |
+
"noData": "કોઈ ભાવ ડેટા ઉપલબ્ધ નથી"
|
| 165 |
+
},
|
| 166 |
+
"schemes": {
|
| 167 |
+
"title": "સરકારી યોજનાઓ",
|
| 168 |
+
"nationalSchemes": "રાષ્ટ્રીય યોજનાઓ",
|
| 169 |
+
"stateSchemes": "રાજ્ય યોજનાઓ",
|
| 170 |
+
"eligibility": "પાત્રતા",
|
| 171 |
+
"benefits": "લાભો",
|
| 172 |
+
"howToApply": "અરજી કેવી રીતે કરવી",
|
| 173 |
+
"documents": "જરૂરી દસ્તાવેજો",
|
| 174 |
+
"deadline": "અરજીની છેલ્લી તારીખ",
|
| 175 |
+
"website": "સત્તાવાર વેબસાઇટ",
|
| 176 |
+
"applyNow": "હમણાં અરજી કરો",
|
| 177 |
+
"noSchemes": "કોઈ યોજના મળી નથી"
|
| 178 |
+
},
|
| 179 |
+
"voice": {
|
| 180 |
+
"title": "વૉઇસ આસિસ્ટન્ટ",
|
| 181 |
+
"listening": "સાંભળી રહ્યું છે...",
|
| 182 |
+
"processing": "પ્રોસેસ થઈ રહ્યું છે...",
|
| 183 |
+
"speak": "હવે બોલો",
|
| 184 |
+
"tapToSpeak": "બોલવા માટે ટેપ કરો",
|
| 185 |
+
"voiceCommands": "વૉઇસ કમાન્ડ",
|
| 186 |
+
"tutorial": "વૉઇસ ટ્યુટોરિયલ"
|
| 187 |
+
},
|
| 188 |
+
"errors": {
|
| 189 |
+
"networkError": "નેટવર્ક ભૂલ. કૃપા કરીને તમારું કનેક્શન તપાસો.",
|
| 190 |
+
"serverError": "સર્વર ભૂલ. કૃપા કરીને પછીથી ફરી પ્રયાસ કરો.",
|
| 191 |
+
"invalidInput": "અમાન્ય ઇનપુટ. કૃપા કરીને તમારી એન્ટ્રીઓ તપાસો.",
|
| 192 |
+
"sessionExpired": "સત્ર સમાપ્ત થયું. કૃપા કરીને ફરીથી લૉગિન કરો.",
|
| 193 |
+
"permissionDenied": "પરવાનગી નકારી.",
|
| 194 |
+
"notFound": "સંસાધન મળ્યું નથી."
|
| 195 |
+
},
|
| 196 |
+
"footer": {
|
| 197 |
+
"copyright": "કૃષિનીતિ. સર્વાધિકાર સુરક્ષિત.",
|
| 198 |
+
"madeWith": "ભારતીય ખેડૂતો માટે પ્રેમથી બનાવેલું"
|
| 199 |
+
}
|
| 200 |
+
}
|
frontend/src/locales/hi/translation.json
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"common": {
|
| 3 |
+
"appName": "कृषिनीति",
|
| 4 |
+
"loading": "लोड हो रहा है...",
|
| 5 |
+
"error": "एक त्रुटि हुई",
|
| 6 |
+
"retry": "पुनः प्रयास करें",
|
| 7 |
+
"cancel": "रद्द करें",
|
| 8 |
+
"save": "सहेजें",
|
| 9 |
+
"delete": "हटाएं",
|
| 10 |
+
"edit": "संपादित करें",
|
| 11 |
+
"submit": "जमा करें",
|
| 12 |
+
"search": "खोजें",
|
| 13 |
+
"filter": "फ़िल्टर",
|
| 14 |
+
"close": "बंद करें",
|
| 15 |
+
"back": "वापस",
|
| 16 |
+
"next": "आगे",
|
| 17 |
+
"previous": "पिछला",
|
| 18 |
+
"yes": "हां",
|
| 19 |
+
"no": "नहीं",
|
| 20 |
+
"ok": "ठीक है",
|
| 21 |
+
"viewAll": "सभी देखें",
|
| 22 |
+
"seeMore": "और देखें",
|
| 23 |
+
"getStarted": "शुरू करें",
|
| 24 |
+
"learnMore": "और जानें",
|
| 25 |
+
"noDataFound": "कोई डेटा नहीं मिला",
|
| 26 |
+
"offline": "आप वर्तमान में ऑफ़लाइन हैं। कुछ सुविधाएं सीमित हो सकती हैं।",
|
| 27 |
+
"connected": "कनेक्टेड",
|
| 28 |
+
"selectLanguage": "भाषा चुनें"
|
| 29 |
+
},
|
| 30 |
+
"languages": {
|
| 31 |
+
"en": "अंग्रेज़ी",
|
| 32 |
+
"hi": "हिंदी",
|
| 33 |
+
"gu": "गुजराती"
|
| 34 |
+
},
|
| 35 |
+
"nav": {
|
| 36 |
+
"home": "होम",
|
| 37 |
+
"diseaseDetection": "रोग पहचान",
|
| 38 |
+
"weather": "मौसम",
|
| 39 |
+
"apmcPrice": "एपीएमसी मूल्य",
|
| 40 |
+
"schemes": "योजनाएं",
|
| 41 |
+
"profile": "प्रोफ़ाइल",
|
| 42 |
+
"dashboard": "मेरा डैशबोर्ड",
|
| 43 |
+
"login": "लॉगिन",
|
| 44 |
+
"logout": "लॉगआउट",
|
| 45 |
+
"signup": "साइन अप"
|
| 46 |
+
},
|
| 47 |
+
"header": {
|
| 48 |
+
"switchTheme": "{{mode}} मोड में बदलें",
|
| 49 |
+
"lightMode": "लाइट",
|
| 50 |
+
"darkMode": "डार्क",
|
| 51 |
+
"toggleMenu": "नेविगेशन मेनू टॉगल करें"
|
| 52 |
+
},
|
| 53 |
+
"home": {
|
| 54 |
+
"greeting": {
|
| 55 |
+
"morning": "सुप्रभात",
|
| 56 |
+
"afternoon": "नमस्कार",
|
| 57 |
+
"evening": "शुभ संध्या"
|
| 58 |
+
},
|
| 59 |
+
"tagline": "भारतीय किसानों के लिए AI-संचालित फसल रोग पहचान, मौसम पूर्वानुमान और बाजार मूल्य ट्रैकिंग।",
|
| 60 |
+
"searchPlaceholder": "सुविधाएं खोजें... (जैसे फसल रोग, मौसम, एपीएमसी मूल्य)",
|
| 61 |
+
"recentActivity": "हालिया गतिविधि",
|
| 62 |
+
"completeProfile": "अपनी प्रोफ़ाइल पूरी करें",
|
| 63 |
+
"addCropsMessage": "व्यक्तिगत एपीएमसी मूल्य अलर्ट और खेती की सिफारिशें प्राप्त करने के लिए अपनी फसलें जोड़ें।",
|
| 64 |
+
"addCropsNow": "अभी फसलें जोड़ें"
|
| 65 |
+
},
|
| 66 |
+
"features": {
|
| 67 |
+
"diseaseDetection": {
|
| 68 |
+
"title": "रोग पहचान",
|
| 69 |
+
"description": "अपनी फसल की तस्वीर अपलोड करें और उपचार सिफारिशों के साथ तुरंत AI-संचालित रोग पहचान प्राप्त करें।",
|
| 70 |
+
"aiPowered": "AI-संचालित"
|
| 71 |
+
},
|
| 72 |
+
"weather": {
|
| 73 |
+
"title": "मौसम पूर्वानुमान",
|
| 74 |
+
"description": "अपने क्षेत्र के लिए सटीक 7-दिवसीय मौसम पूर्वानुमान, कृषि सलाह और गंभीर मौसम अलर्ट प्राप्त करें।",
|
| 75 |
+
"sevenDay": "7-दिवसीय पूर्वानुमान",
|
| 76 |
+
"pincodeBased": "पिनकोड आधारित"
|
| 77 |
+
},
|
| 78 |
+
"apmc": {
|
| 79 |
+
"title": "एपीएमसी मूल्य",
|
| 80 |
+
"description": "विभिन्न एपीएमसी में वास्तविक समय कमोडिटी मूल्यों की तुलना करें, सबसे अच्छा बिक्री बाजार खोजें और मूल्य रुझान ट्रैक करें।",
|
| 81 |
+
"livePrices": "लाइव मूल्य"
|
| 82 |
+
},
|
| 83 |
+
"schemes": {
|
| 84 |
+
"title": "सरकारी योजनाएं",
|
| 85 |
+
"description": "लाभ, पात्रता और आवेदन प्रक्रिया के पूर्ण विवरण के साथ राष्ट्रीय और राज्य-विशिष्ट योजनाओं का पता लगाएं।"
|
| 86 |
+
}
|
| 87 |
+
},
|
| 88 |
+
"auth": {
|
| 89 |
+
"login": "लॉगिन",
|
| 90 |
+
"signup": "साइन अप",
|
| 91 |
+
"logout": "लॉगआउट",
|
| 92 |
+
"email": "��मेल",
|
| 93 |
+
"password": "पासवर्ड",
|
| 94 |
+
"confirmPassword": "पासवर्ड की पुष्टि करें",
|
| 95 |
+
"name": "पूरा नाम",
|
| 96 |
+
"phone": "फोन नंबर",
|
| 97 |
+
"forgotPassword": "पासवर्ड भूल गए?",
|
| 98 |
+
"noAccount": "खाता नहीं है?",
|
| 99 |
+
"hasAccount": "पहले से खाता है?",
|
| 100 |
+
"loginSuccess": "लॉगिन सफल",
|
| 101 |
+
"signupSuccess": "खाता सफलतापूर्वक बनाया गया",
|
| 102 |
+
"logoutSuccess": "सफलतापूर्वक लॉगआउट किया गया"
|
| 103 |
+
},
|
| 104 |
+
"profile": {
|
| 105 |
+
"title": "मेरी प्रोफ़ाइल",
|
| 106 |
+
"personalInfo": "व्यक्तिगत जानकारी",
|
| 107 |
+
"farmDetails": "खेत विवरण",
|
| 108 |
+
"crops": "मेरी फसलें",
|
| 109 |
+
"location": "स्थान",
|
| 110 |
+
"pincode": "पिनकोड",
|
| 111 |
+
"state": "राज्य",
|
| 112 |
+
"district": "जिला",
|
| 113 |
+
"landSize": "भूमि का आकार (एकड़ में)",
|
| 114 |
+
"addCrop": "फसल जोड़ें",
|
| 115 |
+
"removeCrop": "फसल हटाएं",
|
| 116 |
+
"updateProfile": "प्रोफ़ाइल अपडेट करें",
|
| 117 |
+
"profileUpdated": "प्रोफ़ाइल सफलतापूर्वक अपडेट की गई"
|
| 118 |
+
},
|
| 119 |
+
"disease": {
|
| 120 |
+
"title": "रोग पहचान",
|
| 121 |
+
"uploadImage": "छवि अपलोड करें",
|
| 122 |
+
"takePhoto": "फोटो लें",
|
| 123 |
+
"selectCrop": "फसल प्रकार चुनें",
|
| 124 |
+
"analyzing": "विश्लेषण हो रहा है...",
|
| 125 |
+
"result": "पहचान परिणाम",
|
| 126 |
+
"disease": "रोग",
|
| 127 |
+
"confidence": "विश्वास स्तर",
|
| 128 |
+
"treatment": "उपचार",
|
| 129 |
+
"prevention": "रोकथाम",
|
| 130 |
+
"noDisease": "कोई रोग नहीं पाया गया",
|
| 131 |
+
"healthyCrop": "आपकी फसल स्वस्थ प्रतीत होती है",
|
| 132 |
+
"uploadPrompt": "सटीक पहचान के लिए प्रभावित पौधे की पत्ती की स्पष्ट तस्वीर अपलोड करें"
|
| 133 |
+
},
|
| 134 |
+
"weather": {
|
| 135 |
+
"title": "मौसम पूर्वानुमान",
|
| 136 |
+
"currentWeather": "वर्तमान मौसम",
|
| 137 |
+
"weeklyForecast": "साप्ताहिक पूर्वानुमान",
|
| 138 |
+
"farmingAdvice": "कृषि सलाह",
|
| 139 |
+
"alerts": "मौसम अलर्ट",
|
| 140 |
+
"temperature": "तापमान",
|
| 141 |
+
"humidity": "आर्द्रता",
|
| 142 |
+
"wind": "हवा",
|
| 143 |
+
"rain": "बारिश",
|
| 144 |
+
"sunrise": "सूर्योदय",
|
| 145 |
+
"sunset": "सूर्यास्त",
|
| 146 |
+
"feelsLike": "महसूस होता है",
|
| 147 |
+
"enterPincode": "अपना पिनकोड दर्ज करें",
|
| 148 |
+
"getWeather": "मौसम देखें",
|
| 149 |
+
"noAlerts": "कोई मौसम अलर्ट नहीं"
|
| 150 |
+
},
|
| 151 |
+
"apmc": {
|
| 152 |
+
"title": "एपीएमसी बाजार मूल्य",
|
| 153 |
+
"selectCommodity": "कमोडिटी चुनें",
|
| 154 |
+
"selectState": "राज्य चुनें",
|
| 155 |
+
"selectMarket": "बाजार चुनें",
|
| 156 |
+
"minPrice": "न्यूनतम मूल्य",
|
| 157 |
+
"maxPrice": "अधिकतम मूल्य",
|
| 158 |
+
"modalPrice": "मोडल मूल्य",
|
| 159 |
+
"pricePerQuintal": "प्रति क्विंटल मूल्य",
|
| 160 |
+
"bestMarket": "सर्वश्रेष्ठ बाजार",
|
| 161 |
+
"priceAlerts": "मूल्य अलर्ट",
|
| 162 |
+
"createAlert": "अलर्ट बनाएं",
|
| 163 |
+
"sellAdvisory": "बिक्री सलाह",
|
| 164 |
+
"noData": "कोई मूल्य डेटा उपलब्ध नहीं है"
|
| 165 |
+
},
|
| 166 |
+
"schemes": {
|
| 167 |
+
"title": "सरकारी योजनाएं",
|
| 168 |
+
"nationalSchemes": "राष्ट्रीय योजनाएं",
|
| 169 |
+
"stateSchemes": "राज्य योजनाएं",
|
| 170 |
+
"eligibility": "पात्रता",
|
| 171 |
+
"benefits": "लाभ",
|
| 172 |
+
"howToApply": "आवेदन कैसे करें",
|
| 173 |
+
"documents": "आवश्यक दस्तावेज़",
|
| 174 |
+
"deadline": "आवेदन की अंतिम तिथि",
|
| 175 |
+
"website": "आधिकारिक वेबसाइट",
|
| 176 |
+
"applyNow": "अभी आवेदन करें",
|
| 177 |
+
"noSchemes": "कोई योजना नहीं मिली"
|
| 178 |
+
},
|
| 179 |
+
"voice": {
|
| 180 |
+
"title": "वॉइस असिस्टेंट",
|
| 181 |
+
"listening": "सुन रहा है...",
|
| 182 |
+
"processing": "प्रोसेस हो रहा है...",
|
| 183 |
+
"speak": "अब बोलें",
|
| 184 |
+
"tapToSpeak": "बोलने के लिए टैप करें",
|
| 185 |
+
"voiceCommands": "वॉइस कमांड",
|
| 186 |
+
"tutorial": "वॉइस ट्यूटो��ियल"
|
| 187 |
+
},
|
| 188 |
+
"errors": {
|
| 189 |
+
"networkError": "नेटवर्क त्रुटि। कृपया अपना कनेक्शन जांचें।",
|
| 190 |
+
"serverError": "सर्वर त्रुटि। कृपया बाद में पुनः प्रयास करें।",
|
| 191 |
+
"invalidInput": "अमान्य इनपुट। कृपया अपनी प्रविष्टियां जांचें।",
|
| 192 |
+
"sessionExpired": "सत्र समाप्त हो गया। कृपया फिर से लॉगिन करें।",
|
| 193 |
+
"permissionDenied": "अनुमति अस्वीकृत।",
|
| 194 |
+
"notFound": "संसाधन नहीं मिला।"
|
| 195 |
+
},
|
| 196 |
+
"footer": {
|
| 197 |
+
"copyright": "कृषिनीति। सर्वाधिकार सुरक्षित।",
|
| 198 |
+
"madeWith": "भारतीय किसानों के लिए प्यार से बनाया गया"
|
| 199 |
+
}
|
| 200 |
+
}
|
frontend/src/pages/FarmerDashboardPage.jsx
ADDED
|
@@ -0,0 +1,591 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* FarmerDashboardPage - Personalized farmer dashboard.
|
| 3 |
+
*
|
| 4 |
+
* Displays:
|
| 5 |
+
* - Personalized weather info based on location
|
| 6 |
+
* - APMC prices for selected crops
|
| 7 |
+
* - Government schemes relevant to user's state
|
| 8 |
+
*
|
| 9 |
+
* Only accessible to logged-in users.
|
| 10 |
+
*/
|
| 11 |
+
|
| 12 |
+
import { useState, useEffect } from "react";
|
| 13 |
+
import { useNavigate, Link } from "react-router-dom";
|
| 14 |
+
import { motion } from "framer-motion";
|
| 15 |
+
import {
|
| 16 |
+
UserIcon,
|
| 17 |
+
PhoneIcon,
|
| 18 |
+
MapPinIcon,
|
| 19 |
+
CloudIcon,
|
| 20 |
+
CurrencyRupeeIcon,
|
| 21 |
+
DocumentTextIcon,
|
| 22 |
+
ArrowRightIcon,
|
| 23 |
+
ExclamationTriangleIcon,
|
| 24 |
+
SunIcon,
|
| 25 |
+
ArrowTrendingUpIcon,
|
| 26 |
+
ArrowTrendingDownIcon,
|
| 27 |
+
} from "@heroicons/react/24/outline";
|
| 28 |
+
|
| 29 |
+
import { Card, LoadingSpinner } from "@components/common";
|
| 30 |
+
import { useAuth } from "@context/AuthContext";
|
| 31 |
+
import { getPrices, getTrends } from "@services/apmcApi";
|
| 32 |
+
import { getForecast, getAlerts } from "@services/weatherApi";
|
| 33 |
+
import { getSchemesByState } from "@services/schemesApi";
|
| 34 |
+
import { ROUTES } from "@utils/constants";
|
| 35 |
+
|
| 36 |
+
// Animation variants
|
| 37 |
+
const containerVariants = {
|
| 38 |
+
hidden: {},
|
| 39 |
+
show: { transition: { staggerChildren: 0.1 } },
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
const itemVariants = {
|
| 43 |
+
hidden: { opacity: 0, y: 16 },
|
| 44 |
+
show: { opacity: 1, y: 0, transition: { duration: 0.35, ease: "easeOut" } },
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
// Sub-components
|
| 48 |
+
function WeatherCard({ weather, alerts, loading, error, location }) {
|
| 49 |
+
if (loading) {
|
| 50 |
+
return (
|
| 51 |
+
<Card className="p-6">
|
| 52 |
+
<div className="flex items-center gap-2 mb-4">
|
| 53 |
+
<CloudIcon className="h-5 w-5 text-accent-600" />
|
| 54 |
+
<h3 className="font-semibold text-neutral-900 dark:text-white">Weather</h3>
|
| 55 |
+
</div>
|
| 56 |
+
<div className="flex justify-center py-8">
|
| 57 |
+
<LoadingSpinner size="md" message="Loading weather..." />
|
| 58 |
+
</div>
|
| 59 |
+
</Card>
|
| 60 |
+
);
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
if (error) {
|
| 64 |
+
return (
|
| 65 |
+
<Card className="p-6">
|
| 66 |
+
<div className="flex items-center gap-2 mb-4">
|
| 67 |
+
<CloudIcon className="h-5 w-5 text-accent-600" />
|
| 68 |
+
<h3 className="font-semibold text-neutral-900 dark:text-white">Weather</h3>
|
| 69 |
+
</div>
|
| 70 |
+
<p className="text-sm text-neutral-500">{error}</p>
|
| 71 |
+
</Card>
|
| 72 |
+
);
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
if (!weather?.forecast?.daily) {
|
| 76 |
+
return (
|
| 77 |
+
<Card className="p-6">
|
| 78 |
+
<div className="flex items-center gap-2 mb-4">
|
| 79 |
+
<CloudIcon className="h-5 w-5 text-accent-600" />
|
| 80 |
+
<h3 className="font-semibold text-neutral-900 dark:text-white">Weather</h3>
|
| 81 |
+
</div>
|
| 82 |
+
<p className="text-sm text-neutral-500">No weather data available</p>
|
| 83 |
+
</Card>
|
| 84 |
+
);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
const daily = weather.forecast.daily;
|
| 88 |
+
const today = {
|
| 89 |
+
tempMax: daily.temperature_2m_max?.[0] ?? "--",
|
| 90 |
+
tempMin: daily.temperature_2m_min?.[0] ?? "--",
|
| 91 |
+
precipitation: daily.precipitation_sum?.[0] ?? 0,
|
| 92 |
+
};
|
| 93 |
+
|
| 94 |
+
return (
|
| 95 |
+
<Card className="p-6">
|
| 96 |
+
<div className="flex items-center justify-between mb-4">
|
| 97 |
+
<div className="flex items-center gap-2">
|
| 98 |
+
<CloudIcon className="h-5 w-5 text-accent-600" />
|
| 99 |
+
<h3 className="font-semibold text-neutral-900 dark:text-white">Weather</h3>
|
| 100 |
+
</div>
|
| 101 |
+
<Link
|
| 102 |
+
to={ROUTES.WEATHER}
|
| 103 |
+
className="text-sm text-primary-600 hover:text-primary-700 flex items-center gap-1"
|
| 104 |
+
>
|
| 105 |
+
View Full <ArrowRightIcon className="h-4 w-4" />
|
| 106 |
+
</Link>
|
| 107 |
+
</div>
|
| 108 |
+
|
| 109 |
+
<div className="flex items-center gap-2 text-xs text-neutral-500 mb-4">
|
| 110 |
+
<MapPinIcon className="h-4 w-4" />
|
| 111 |
+
<span>{location.taluka}, {location.district}</span>
|
| 112 |
+
</div>
|
| 113 |
+
|
| 114 |
+
<div className="flex items-center gap-4 mb-4">
|
| 115 |
+
<div className="flex-1">
|
| 116 |
+
<p className="text-3xl font-bold text-neutral-900 dark:text-white">
|
| 117 |
+
{Math.round(today.tempMax)}°C
|
| 118 |
+
</p>
|
| 119 |
+
<p className="text-sm text-neutral-500">
|
| 120 |
+
Low: {Math.round(today.tempMin)}°C
|
| 121 |
+
</p>
|
| 122 |
+
</div>
|
| 123 |
+
<div className="text-right">
|
| 124 |
+
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
| 125 |
+
Precipitation
|
| 126 |
+
</p>
|
| 127 |
+
<p className="text-lg font-semibold text-accent-600">
|
| 128 |
+
{today.precipitation} mm
|
| 129 |
+
</p>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
|
| 133 |
+
{/* 5-day forecast */}
|
| 134 |
+
<div className="border-t border-neutral-200 dark:border-neutral-700 pt-4">
|
| 135 |
+
<p className="text-xs font-medium text-neutral-500 mb-3">5-Day Forecast</p>
|
| 136 |
+
<div className="grid grid-cols-5 gap-2">
|
| 137 |
+
{daily.time?.slice(0, 5).map((date, idx) => {
|
| 138 |
+
const dayName = new Date(date).toLocaleDateString("en-IN", { weekday: "short" });
|
| 139 |
+
return (
|
| 140 |
+
<div key={date} className="text-center">
|
| 141 |
+
<p className="text-xs text-neutral-500">{dayName}</p>
|
| 142 |
+
<p className="text-sm font-semibold text-neutral-900 dark:text-white">
|
| 143 |
+
{Math.round(daily.temperature_2m_max?.[idx] ?? 0)}°
|
| 144 |
+
</p>
|
| 145 |
+
<p className="text-xs text-neutral-400">
|
| 146 |
+
{Math.round(daily.temperature_2m_min?.[idx] ?? 0)}°
|
| 147 |
+
</p>
|
| 148 |
+
</div>
|
| 149 |
+
);
|
| 150 |
+
})}
|
| 151 |
+
</div>
|
| 152 |
+
</div>
|
| 153 |
+
|
| 154 |
+
{/* Alerts */}
|
| 155 |
+
{alerts?.alerts?.length > 0 && (
|
| 156 |
+
<div className="mt-4 p-3 bg-amber-50 border border-amber-200 rounded-lg">
|
| 157 |
+
<div className="flex items-center gap-2 mb-1">
|
| 158 |
+
<ExclamationTriangleIcon className="h-4 w-4 text-amber-600" />
|
| 159 |
+
<span className="text-sm font-medium text-amber-800">Weather Alert</span>
|
| 160 |
+
</div>
|
| 161 |
+
<p className="text-xs text-amber-700">{alerts.alerts[0].message}</p>
|
| 162 |
+
</div>
|
| 163 |
+
)}
|
| 164 |
+
</Card>
|
| 165 |
+
);
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
function CropPriceCard({ crop, priceData, trends, loading }) {
|
| 169 |
+
if (loading) {
|
| 170 |
+
return (
|
| 171 |
+
<div className="p-4 bg-neutral-50 dark:bg-neutral-800 rounded-lg">
|
| 172 |
+
<LoadingSpinner size="sm" />
|
| 173 |
+
</div>
|
| 174 |
+
);
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
const avgPrice = priceData?.avg_price || priceData?.prices?.[0]?.price_per_quintal;
|
| 178 |
+
const trendDirection = trends?.trend?.direction;
|
| 179 |
+
const trendPercent = trends?.trend?.percent_change;
|
| 180 |
+
|
| 181 |
+
return (
|
| 182 |
+
<div className="p-4 bg-neutral-50 dark:bg-neutral-800 rounded-lg">
|
| 183 |
+
<div className="flex items-center justify-between mb-2">
|
| 184 |
+
<h4 className="font-medium text-neutral-900 dark:text-white">{crop}</h4>
|
| 185 |
+
{trendDirection && (
|
| 186 |
+
<div
|
| 187 |
+
className={`flex items-center gap-1 text-xs font-medium ${
|
| 188 |
+
trendDirection === "up"
|
| 189 |
+
? "text-green-600"
|
| 190 |
+
: trendDirection === "down"
|
| 191 |
+
? "text-red-600"
|
| 192 |
+
: "text-neutral-500"
|
| 193 |
+
}`}
|
| 194 |
+
>
|
| 195 |
+
{trendDirection === "up" ? (
|
| 196 |
+
<ArrowTrendingUpIcon className="h-4 w-4" />
|
| 197 |
+
) : trendDirection === "down" ? (
|
| 198 |
+
<ArrowTrendingDownIcon className="h-4 w-4" />
|
| 199 |
+
) : null}
|
| 200 |
+
{trendPercent ? `${Math.abs(trendPercent).toFixed(1)}%` : "Stable"}
|
| 201 |
+
</div>
|
| 202 |
+
)}
|
| 203 |
+
</div>
|
| 204 |
+
<p className="text-2xl font-bold text-primary-600">
|
| 205 |
+
{avgPrice ? `₹${Math.round(avgPrice).toLocaleString("en-IN")}` : "N/A"}
|
| 206 |
+
<span className="text-sm font-normal text-neutral-500">/qtl</span>
|
| 207 |
+
</p>
|
| 208 |
+
{priceData?.prices?.length > 0 && (
|
| 209 |
+
<p className="text-xs text-neutral-500 mt-1">
|
| 210 |
+
Best: {priceData.prices[0].mandi_name} ({priceData.prices[0].state})
|
| 211 |
+
</p>
|
| 212 |
+
)}
|
| 213 |
+
</div>
|
| 214 |
+
);
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
function APMCPricesCard({ crops, pricesData, trendsData, loading }) {
|
| 218 |
+
if (loading && Object.keys(pricesData).length === 0) {
|
| 219 |
+
return (
|
| 220 |
+
<Card className="p-6">
|
| 221 |
+
<div className="flex items-center gap-2 mb-4">
|
| 222 |
+
<CurrencyRupeeIcon className="h-5 w-5 text-secondary-600" />
|
| 223 |
+
<h3 className="font-semibold text-neutral-900 dark:text-white">APMC Prices</h3>
|
| 224 |
+
</div>
|
| 225 |
+
<div className="flex justify-center py-8">
|
| 226 |
+
<LoadingSpinner size="md" message="Loading prices..." />
|
| 227 |
+
</div>
|
| 228 |
+
</Card>
|
| 229 |
+
);
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
if (crops.length === 0) {
|
| 233 |
+
return (
|
| 234 |
+
<Card className="p-6">
|
| 235 |
+
<div className="flex items-center gap-2 mb-4">
|
| 236 |
+
<CurrencyRupeeIcon className="h-5 w-5 text-secondary-600" />
|
| 237 |
+
<h3 className="font-semibold text-neutral-900 dark:text-white">APMC Prices</h3>
|
| 238 |
+
</div>
|
| 239 |
+
<div className="text-center py-6">
|
| 240 |
+
<p className="text-sm text-neutral-500 mb-3">
|
| 241 |
+
Add your crops to see personalized APMC prices
|
| 242 |
+
</p>
|
| 243 |
+
<Link
|
| 244 |
+
to={ROUTES.PROFILE}
|
| 245 |
+
className="inline-flex items-center gap-1 text-sm font-medium text-primary-600 hover:text-primary-700"
|
| 246 |
+
>
|
| 247 |
+
Go to Profile <ArrowRightIcon className="h-4 w-4" />
|
| 248 |
+
</Link>
|
| 249 |
+
</div>
|
| 250 |
+
</Card>
|
| 251 |
+
);
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
return (
|
| 255 |
+
<Card className="p-6">
|
| 256 |
+
<div className="flex items-center justify-between mb-4">
|
| 257 |
+
<div className="flex items-center gap-2">
|
| 258 |
+
<CurrencyRupeeIcon className="h-5 w-5 text-secondary-600" />
|
| 259 |
+
<h3 className="font-semibold text-neutral-900 dark:text-white">APMC Prices</h3>
|
| 260 |
+
</div>
|
| 261 |
+
<Link
|
| 262 |
+
to={ROUTES.APMC}
|
| 263 |
+
className="text-sm text-primary-600 hover:text-primary-700 flex items-center gap-1"
|
| 264 |
+
>
|
| 265 |
+
View All <ArrowRightIcon className="h-4 w-4" />
|
| 266 |
+
</Link>
|
| 267 |
+
</div>
|
| 268 |
+
|
| 269 |
+
<p className="text-xs text-neutral-500 mb-4">Your selected crops</p>
|
| 270 |
+
|
| 271 |
+
<div className="space-y-3">
|
| 272 |
+
{crops.map((crop) => (
|
| 273 |
+
<CropPriceCard
|
| 274 |
+
key={crop}
|
| 275 |
+
crop={crop}
|
| 276 |
+
priceData={pricesData[crop]}
|
| 277 |
+
trends={trendsData[crop]}
|
| 278 |
+
loading={loading && !pricesData[crop]}
|
| 279 |
+
/>
|
| 280 |
+
))}
|
| 281 |
+
</div>
|
| 282 |
+
</Card>
|
| 283 |
+
);
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
function SchemesCard({ schemes, loading, error, state }) {
|
| 287 |
+
if (loading) {
|
| 288 |
+
return (
|
| 289 |
+
<Card className="p-6">
|
| 290 |
+
<div className="flex items-center gap-2 mb-4">
|
| 291 |
+
<DocumentTextIcon className="h-5 w-5 text-green-600" />
|
| 292 |
+
<h3 className="font-semibold text-neutral-900 dark:text-white">Government Schemes</h3>
|
| 293 |
+
</div>
|
| 294 |
+
<div className="flex justify-center py-8">
|
| 295 |
+
<LoadingSpinner size="md" message="Loading schemes..." />
|
| 296 |
+
</div>
|
| 297 |
+
</Card>
|
| 298 |
+
);
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
if (error || !schemes?.length) {
|
| 302 |
+
return (
|
| 303 |
+
<Card className="p-6">
|
| 304 |
+
<div className="flex items-center gap-2 mb-4">
|
| 305 |
+
<DocumentTextIcon className="h-5 w-5 text-green-600" />
|
| 306 |
+
<h3 className="font-semibold text-neutral-900 dark:text-white">Government Schemes</h3>
|
| 307 |
+
</div>
|
| 308 |
+
<p className="text-sm text-neutral-500">
|
| 309 |
+
{error || "No schemes available for your state"}
|
| 310 |
+
</p>
|
| 311 |
+
</Card>
|
| 312 |
+
);
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
const displaySchemes = schemes.slice(0, 4);
|
| 316 |
+
|
| 317 |
+
return (
|
| 318 |
+
<Card className="p-6">
|
| 319 |
+
<div className="flex items-center justify-between mb-4">
|
| 320 |
+
<div className="flex items-center gap-2">
|
| 321 |
+
<DocumentTextIcon className="h-5 w-5 text-green-600" />
|
| 322 |
+
<h3 className="font-semibold text-neutral-900 dark:text-white">Government Schemes</h3>
|
| 323 |
+
</div>
|
| 324 |
+
<Link
|
| 325 |
+
to={ROUTES.SCHEMES}
|
| 326 |
+
className="text-sm text-primary-600 hover:text-primary-700 flex items-center gap-1"
|
| 327 |
+
>
|
| 328 |
+
View All <ArrowRightIcon className="h-4 w-4" />
|
| 329 |
+
</Link>
|
| 330 |
+
</div>
|
| 331 |
+
|
| 332 |
+
<p className="text-xs text-neutral-500 mb-4">
|
| 333 |
+
Available for {state}
|
| 334 |
+
</p>
|
| 335 |
+
|
| 336 |
+
<div className="space-y-3">
|
| 337 |
+
{displaySchemes.map((scheme) => (
|
| 338 |
+
<div
|
| 339 |
+
key={scheme.id}
|
| 340 |
+
className="p-3 bg-neutral-50 dark:bg-neutral-800 rounded-lg"
|
| 341 |
+
>
|
| 342 |
+
<h4 className="text-sm font-medium text-neutral-900 dark:text-white mb-1 line-clamp-1">
|
| 343 |
+
{scheme.scheme_name}
|
| 344 |
+
</h4>
|
| 345 |
+
<p className="text-xs text-neutral-500 line-clamp-2">
|
| 346 |
+
{scheme.description}
|
| 347 |
+
</p>
|
| 348 |
+
{scheme.benefit_amount && (
|
| 349 |
+
<p className="text-xs font-medium text-green-600 mt-1">
|
| 350 |
+
Benefit: {scheme.benefit_amount}
|
| 351 |
+
</p>
|
| 352 |
+
)}
|
| 353 |
+
</div>
|
| 354 |
+
))}
|
| 355 |
+
</div>
|
| 356 |
+
|
| 357 |
+
{schemes.length > 4 && (
|
| 358 |
+
<p className="text-xs text-neutral-400 text-center mt-4">
|
| 359 |
+
+{schemes.length - 4} more schemes available
|
| 360 |
+
</p>
|
| 361 |
+
)}
|
| 362 |
+
</Card>
|
| 363 |
+
);
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
function NoCropsAlert() {
|
| 367 |
+
const { user } = useAuth();
|
| 368 |
+
|
| 369 |
+
if (user?.crops?.length > 0) return null;
|
| 370 |
+
|
| 371 |
+
return (
|
| 372 |
+
<motion.div
|
| 373 |
+
variants={itemVariants}
|
| 374 |
+
className="rounded-lg bg-amber-50 border border-amber-200 p-4 flex items-start gap-3"
|
| 375 |
+
>
|
| 376 |
+
<ExclamationTriangleIcon className="h-6 w-6 text-amber-600 shrink-0 mt-0.5" />
|
| 377 |
+
<div className="flex-1">
|
| 378 |
+
<h3 className="text-sm font-semibold text-amber-800">
|
| 379 |
+
Add Your Crops
|
| 380 |
+
</h3>
|
| 381 |
+
<p className="text-sm text-amber-700 mt-1">
|
| 382 |
+
Go to your profile and add crops to see personalized APMC price updates.
|
| 383 |
+
</p>
|
| 384 |
+
<Link
|
| 385 |
+
to={ROUTES.PROFILE}
|
| 386 |
+
className="inline-flex items-center gap-1 mt-2 text-sm font-medium text-amber-700 hover:text-amber-900"
|
| 387 |
+
>
|
| 388 |
+
Add Crops Now
|
| 389 |
+
<ArrowRightIcon className="h-4 w-4" />
|
| 390 |
+
</Link>
|
| 391 |
+
</div>
|
| 392 |
+
</motion.div>
|
| 393 |
+
);
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
// Main Component
|
| 397 |
+
function FarmerDashboardPage() {
|
| 398 |
+
const navigate = useNavigate();
|
| 399 |
+
const { user, isAuthenticated } = useAuth();
|
| 400 |
+
|
| 401 |
+
// Weather state
|
| 402 |
+
const [weather, setWeather] = useState(null);
|
| 403 |
+
const [weatherAlerts, setWeatherAlerts] = useState(null);
|
| 404 |
+
const [weatherLoading, setWeatherLoading] = useState(true);
|
| 405 |
+
const [weatherError, setWeatherError] = useState(null);
|
| 406 |
+
|
| 407 |
+
// APMC state
|
| 408 |
+
const [pricesData, setPricesData] = useState({});
|
| 409 |
+
const [trendsData, setTrendsData] = useState({});
|
| 410 |
+
const [pricesLoading, setPricesLoading] = useState(false);
|
| 411 |
+
|
| 412 |
+
// Schemes state
|
| 413 |
+
const [schemes, setSchemes] = useState([]);
|
| 414 |
+
const [schemesLoading, setSchemesLoading] = useState(true);
|
| 415 |
+
const [schemesError, setSchemesError] = useState(null);
|
| 416 |
+
|
| 417 |
+
// Redirect if not authenticated
|
| 418 |
+
useEffect(() => {
|
| 419 |
+
if (!isAuthenticated) {
|
| 420 |
+
navigate(ROUTES.LOGIN);
|
| 421 |
+
}
|
| 422 |
+
}, [isAuthenticated, navigate]);
|
| 423 |
+
|
| 424 |
+
// Fetch weather based on user location
|
| 425 |
+
useEffect(() => {
|
| 426 |
+
if (!user?.state || !user?.district || !user?.taluka) return;
|
| 427 |
+
|
| 428 |
+
async function fetchWeather() {
|
| 429 |
+
setWeatherLoading(true);
|
| 430 |
+
setWeatherError(null);
|
| 431 |
+
try {
|
| 432 |
+
const location = {
|
| 433 |
+
state: user.state,
|
| 434 |
+
district: user.district,
|
| 435 |
+
taluka: user.taluka,
|
| 436 |
+
};
|
| 437 |
+
const [forecastData, alertsData] = await Promise.all([
|
| 438 |
+
getForecast(location),
|
| 439 |
+
getAlerts(location).catch(() => null),
|
| 440 |
+
]);
|
| 441 |
+
setWeather(forecastData);
|
| 442 |
+
setWeatherAlerts(alertsData);
|
| 443 |
+
} catch (error) {
|
| 444 |
+
console.error("Failed to fetch weather:", error);
|
| 445 |
+
setWeatherError("Unable to load weather data");
|
| 446 |
+
} finally {
|
| 447 |
+
setWeatherLoading(false);
|
| 448 |
+
}
|
| 449 |
+
}
|
| 450 |
+
fetchWeather();
|
| 451 |
+
}, [user?.state, user?.district, user?.taluka]);
|
| 452 |
+
|
| 453 |
+
// Fetch APMC prices for user's crops
|
| 454 |
+
useEffect(() => {
|
| 455 |
+
const crops = user?.crops || [];
|
| 456 |
+
if (crops.length === 0) {
|
| 457 |
+
setPricesData({});
|
| 458 |
+
setTrendsData({});
|
| 459 |
+
return;
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
async function fetchPricesForCrops() {
|
| 463 |
+
setPricesLoading(true);
|
| 464 |
+
const newPrices = {};
|
| 465 |
+
const newTrends = {};
|
| 466 |
+
|
| 467 |
+
await Promise.all(
|
| 468 |
+
crops.map(async (crop) => {
|
| 469 |
+
try {
|
| 470 |
+
const [priceResult, trendResult] = await Promise.all([
|
| 471 |
+
getPrices({ commodity: crop, state: user.state, limit: 5 }),
|
| 472 |
+
getTrends(crop, { state: user.state, days: 7 }).catch(() => null),
|
| 473 |
+
]);
|
| 474 |
+
newPrices[crop] = priceResult;
|
| 475 |
+
if (trendResult) newTrends[crop] = trendResult;
|
| 476 |
+
} catch (error) {
|
| 477 |
+
console.error(`Failed to fetch prices for ${crop}:`, error);
|
| 478 |
+
}
|
| 479 |
+
})
|
| 480 |
+
);
|
| 481 |
+
|
| 482 |
+
setPricesData(newPrices);
|
| 483 |
+
setTrendsData(newTrends);
|
| 484 |
+
setPricesLoading(false);
|
| 485 |
+
}
|
| 486 |
+
fetchPricesForCrops();
|
| 487 |
+
}, [user?.crops, user?.state]);
|
| 488 |
+
|
| 489 |
+
// Fetch schemes for user's state
|
| 490 |
+
useEffect(() => {
|
| 491 |
+
if (!user?.state) return;
|
| 492 |
+
|
| 493 |
+
async function fetchSchemes() {
|
| 494 |
+
setSchemesLoading(true);
|
| 495 |
+
setSchemesError(null);
|
| 496 |
+
try {
|
| 497 |
+
const data = await getSchemesByState(user.state);
|
| 498 |
+
setSchemes(data.schemes || []);
|
| 499 |
+
} catch (error) {
|
| 500 |
+
console.error("Failed to fetch schemes:", error);
|
| 501 |
+
setSchemesError("Unable to load schemes");
|
| 502 |
+
} finally {
|
| 503 |
+
setSchemesLoading(false);
|
| 504 |
+
}
|
| 505 |
+
}
|
| 506 |
+
fetchSchemes();
|
| 507 |
+
}, [user?.state]);
|
| 508 |
+
|
| 509 |
+
if (!isAuthenticated || !user) {
|
| 510 |
+
return (
|
| 511 |
+
<div className="min-h-[80vh] flex items-center justify-center">
|
| 512 |
+
<LoadingSpinner size="lg" message="Loading..." />
|
| 513 |
+
</div>
|
| 514 |
+
);
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
return (
|
| 518 |
+
<motion.div
|
| 519 |
+
variants={containerVariants}
|
| 520 |
+
initial="hidden"
|
| 521 |
+
animate="show"
|
| 522 |
+
className="max-w-6xl mx-auto px-4 py-8"
|
| 523 |
+
>
|
| 524 |
+
{/* Header */}
|
| 525 |
+
<motion.div variants={itemVariants} className="mb-8">
|
| 526 |
+
<div className="flex items-center gap-4">
|
| 527 |
+
<div className="h-16 w-16 rounded-full bg-primary-100 flex items-center justify-center">
|
| 528 |
+
<UserIcon className="h-8 w-8 text-primary-600" />
|
| 529 |
+
</div>
|
| 530 |
+
<div>
|
| 531 |
+
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white">
|
| 532 |
+
Welcome, {user.name?.split(" ")[0]}
|
| 533 |
+
</h1>
|
| 534 |
+
<div className="flex items-center gap-4 mt-1">
|
| 535 |
+
<div className="flex items-center gap-1 text-neutral-500 text-sm">
|
| 536 |
+
<PhoneIcon className="h-4 w-4" />
|
| 537 |
+
<span>{user.mobile_number}</span>
|
| 538 |
+
</div>
|
| 539 |
+
<div className="flex items-center gap-1 text-neutral-500 text-sm">
|
| 540 |
+
<MapPinIcon className="h-4 w-4" />
|
| 541 |
+
<span>
|
| 542 |
+
{user.taluka}, {user.district}, {user.state}
|
| 543 |
+
</span>
|
| 544 |
+
</div>
|
| 545 |
+
</div>
|
| 546 |
+
</div>
|
| 547 |
+
</div>
|
| 548 |
+
</motion.div>
|
| 549 |
+
|
| 550 |
+
{/* Crops Alert */}
|
| 551 |
+
<NoCropsAlert />
|
| 552 |
+
|
| 553 |
+
{/* Dashboard Grid */}
|
| 554 |
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
|
| 555 |
+
<motion.div variants={itemVariants}>
|
| 556 |
+
<WeatherCard
|
| 557 |
+
weather={weather}
|
| 558 |
+
alerts={weatherAlerts}
|
| 559 |
+
loading={weatherLoading}
|
| 560 |
+
error={weatherError}
|
| 561 |
+
location={{
|
| 562 |
+
taluka: user.taluka,
|
| 563 |
+
district: user.district,
|
| 564 |
+
state: user.state,
|
| 565 |
+
}}
|
| 566 |
+
/>
|
| 567 |
+
</motion.div>
|
| 568 |
+
|
| 569 |
+
<motion.div variants={itemVariants}>
|
| 570 |
+
<APMCPricesCard
|
| 571 |
+
crops={user.crops || []}
|
| 572 |
+
pricesData={pricesData}
|
| 573 |
+
trendsData={trendsData}
|
| 574 |
+
loading={pricesLoading}
|
| 575 |
+
/>
|
| 576 |
+
</motion.div>
|
| 577 |
+
|
| 578 |
+
<motion.div variants={itemVariants} className="lg:col-span-2">
|
| 579 |
+
<SchemesCard
|
| 580 |
+
schemes={schemes}
|
| 581 |
+
loading={schemesLoading}
|
| 582 |
+
error={schemesError}
|
| 583 |
+
state={user.state}
|
| 584 |
+
/>
|
| 585 |
+
</motion.div>
|
| 586 |
+
</div>
|
| 587 |
+
</motion.div>
|
| 588 |
+
);
|
| 589 |
+
}
|
| 590 |
+
|
| 591 |
+
export default FarmerDashboardPage;
|
frontend/src/pages/FarmerProfilePage.jsx
CHANGED
|
@@ -1,11 +1,10 @@
|
|
| 1 |
/**
|
| 2 |
-
* FarmerProfilePage -
|
| 3 |
*
|
| 4 |
-
*
|
| 5 |
-
* -
|
| 6 |
-
* -
|
| 7 |
-
* -
|
| 8 |
-
* - Government schemes relevant to user's state
|
| 9 |
*
|
| 10 |
* Only accessible to logged-in users.
|
| 11 |
*/
|
|
@@ -20,23 +19,14 @@ import {
|
|
| 20 |
MapPinIcon,
|
| 21 |
CheckIcon,
|
| 22 |
XMarkIcon,
|
| 23 |
-
CloudIcon,
|
| 24 |
-
CurrencyRupeeIcon,
|
| 25 |
-
DocumentTextIcon,
|
| 26 |
-
ArrowRightIcon,
|
| 27 |
-
ExclamationTriangleIcon,
|
| 28 |
-
SunIcon,
|
| 29 |
-
ArrowTrendingUpIcon,
|
| 30 |
-
ArrowTrendingDownIcon,
|
| 31 |
PencilIcon,
|
|
|
|
| 32 |
} from "@heroicons/react/24/outline";
|
| 33 |
|
| 34 |
-
import { Input, Button, Card, Select, LoadingSpinner
|
| 35 |
import { useAuth } from "@context/AuthContext";
|
| 36 |
import { updateProfile } from "@services/authApi";
|
| 37 |
-
import { getCommodities
|
| 38 |
-
import { getForecast, getAlerts } from "@services/weatherApi";
|
| 39 |
-
import { getSchemesByState } from "@services/schemesApi";
|
| 40 |
import { ROUTES } from "@utils/constants";
|
| 41 |
|
| 42 |
// Animation variants
|
|
@@ -50,331 +40,8 @@ const itemVariants = {
|
|
| 50 |
show: { opacity: 1, y: 0, transition: { duration: 0.35, ease: "easeOut" } },
|
| 51 |
};
|
| 52 |
|
| 53 |
-
//
|
| 54 |
-
function
|
| 55 |
-
if (code <= 3) return SunIcon;
|
| 56 |
-
return CloudIcon;
|
| 57 |
-
}
|
| 58 |
-
|
| 59 |
-
// Sub-components
|
| 60 |
-
function WeatherCard({ weather, alerts, loading, error, location }) {
|
| 61 |
-
if (loading) {
|
| 62 |
-
return (
|
| 63 |
-
<Card className="p-6">
|
| 64 |
-
<div className="flex items-center gap-2 mb-4">
|
| 65 |
-
<CloudIcon className="h-5 w-5 text-accent-600" />
|
| 66 |
-
<h3 className="font-semibold text-neutral-900 dark:text-white">Weather</h3>
|
| 67 |
-
</div>
|
| 68 |
-
<div className="flex justify-center py-8">
|
| 69 |
-
<LoadingSpinner size="md" message="Loading weather..." />
|
| 70 |
-
</div>
|
| 71 |
-
</Card>
|
| 72 |
-
);
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
if (error) {
|
| 76 |
-
return (
|
| 77 |
-
<Card className="p-6">
|
| 78 |
-
<div className="flex items-center gap-2 mb-4">
|
| 79 |
-
<CloudIcon className="h-5 w-5 text-accent-600" />
|
| 80 |
-
<h3 className="font-semibold text-neutral-900 dark:text-white">Weather</h3>
|
| 81 |
-
</div>
|
| 82 |
-
<p className="text-sm text-neutral-500">{error}</p>
|
| 83 |
-
</Card>
|
| 84 |
-
);
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
if (!weather?.forecast?.daily) {
|
| 88 |
-
return (
|
| 89 |
-
<Card className="p-6">
|
| 90 |
-
<div className="flex items-center gap-2 mb-4">
|
| 91 |
-
<CloudIcon className="h-5 w-5 text-accent-600" />
|
| 92 |
-
<h3 className="font-semibold text-neutral-900 dark:text-white">Weather</h3>
|
| 93 |
-
</div>
|
| 94 |
-
<p className="text-sm text-neutral-500">No weather data available</p>
|
| 95 |
-
</Card>
|
| 96 |
-
);
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
const daily = weather.forecast.daily;
|
| 100 |
-
const today = {
|
| 101 |
-
tempMax: daily.temperature_2m_max?.[0] ?? "--",
|
| 102 |
-
tempMin: daily.temperature_2m_min?.[0] ?? "--",
|
| 103 |
-
precipitation: daily.precipitation_sum?.[0] ?? 0,
|
| 104 |
-
weatherCode: daily.weather_code?.[0] ?? 0,
|
| 105 |
-
};
|
| 106 |
-
|
| 107 |
-
return (
|
| 108 |
-
<Card className="p-6">
|
| 109 |
-
<div className="flex items-center justify-between mb-4">
|
| 110 |
-
<div className="flex items-center gap-2">
|
| 111 |
-
<CloudIcon className="h-5 w-5 text-accent-600" />
|
| 112 |
-
<h3 className="font-semibold text-neutral-900 dark:text-white">Weather</h3>
|
| 113 |
-
</div>
|
| 114 |
-
<Link
|
| 115 |
-
to={ROUTES.WEATHER}
|
| 116 |
-
className="text-sm text-primary-600 hover:text-primary-700 flex items-center gap-1"
|
| 117 |
-
>
|
| 118 |
-
View Full <ArrowRightIcon className="h-4 w-4" />
|
| 119 |
-
</Link>
|
| 120 |
-
</div>
|
| 121 |
-
|
| 122 |
-
<div className="flex items-center gap-2 text-xs text-neutral-500 mb-4">
|
| 123 |
-
<MapPinIcon className="h-4 w-4" />
|
| 124 |
-
<span>{location.taluka}, {location.district}</span>
|
| 125 |
-
</div>
|
| 126 |
-
|
| 127 |
-
<div className="flex items-center gap-4 mb-4">
|
| 128 |
-
<div className="flex-1">
|
| 129 |
-
<p className="text-3xl font-bold text-neutral-900 dark:text-white">
|
| 130 |
-
{Math.round(today.tempMax)}°C
|
| 131 |
-
</p>
|
| 132 |
-
<p className="text-sm text-neutral-500">
|
| 133 |
-
Low: {Math.round(today.tempMin)}°C
|
| 134 |
-
</p>
|
| 135 |
-
</div>
|
| 136 |
-
<div className="text-right">
|
| 137 |
-
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
| 138 |
-
Precipitation
|
| 139 |
-
</p>
|
| 140 |
-
<p className="text-lg font-semibold text-accent-600">
|
| 141 |
-
{today.precipitation} mm
|
| 142 |
-
</p>
|
| 143 |
-
</div>
|
| 144 |
-
</div>
|
| 145 |
-
|
| 146 |
-
{/* 5-day forecast */}
|
| 147 |
-
<div className="border-t border-neutral-200 dark:border-neutral-700 pt-4">
|
| 148 |
-
<p className="text-xs font-medium text-neutral-500 mb-3">5-Day Forecast</p>
|
| 149 |
-
<div className="grid grid-cols-5 gap-2">
|
| 150 |
-
{daily.time?.slice(0, 5).map((date, idx) => {
|
| 151 |
-
const dayName = new Date(date).toLocaleDateString("en-IN", { weekday: "short" });
|
| 152 |
-
return (
|
| 153 |
-
<div key={date} className="text-center">
|
| 154 |
-
<p className="text-xs text-neutral-500">{dayName}</p>
|
| 155 |
-
<p className="text-sm font-semibold text-neutral-900 dark:text-white">
|
| 156 |
-
{Math.round(daily.temperature_2m_max?.[idx] ?? 0)}°
|
| 157 |
-
</p>
|
| 158 |
-
<p className="text-xs text-neutral-400">
|
| 159 |
-
{Math.round(daily.temperature_2m_min?.[idx] ?? 0)}°
|
| 160 |
-
</p>
|
| 161 |
-
</div>
|
| 162 |
-
);
|
| 163 |
-
})}
|
| 164 |
-
</div>
|
| 165 |
-
</div>
|
| 166 |
-
|
| 167 |
-
{/* Alerts */}
|
| 168 |
-
{alerts?.alerts?.length > 0 && (
|
| 169 |
-
<div className="mt-4 p-3 bg-amber-50 border border-amber-200 rounded-lg">
|
| 170 |
-
<div className="flex items-center gap-2 mb-1">
|
| 171 |
-
<ExclamationTriangleIcon className="h-4 w-4 text-amber-600" />
|
| 172 |
-
<span className="text-sm font-medium text-amber-800">Weather Alert</span>
|
| 173 |
-
</div>
|
| 174 |
-
<p className="text-xs text-amber-700">{alerts.alerts[0].message}</p>
|
| 175 |
-
</div>
|
| 176 |
-
)}
|
| 177 |
-
</Card>
|
| 178 |
-
);
|
| 179 |
-
}
|
| 180 |
-
|
| 181 |
-
function CropPriceCard({ crop, priceData, trends, loading }) {
|
| 182 |
-
if (loading) {
|
| 183 |
-
return (
|
| 184 |
-
<div className="p-4 bg-neutral-50 dark:bg-neutral-800 rounded-lg">
|
| 185 |
-
<LoadingSpinner size="sm" />
|
| 186 |
-
</div>
|
| 187 |
-
);
|
| 188 |
-
}
|
| 189 |
-
|
| 190 |
-
const avgPrice = priceData?.avg_price || priceData?.prices?.[0]?.price_per_quintal;
|
| 191 |
-
const trendDirection = trends?.trend?.direction;
|
| 192 |
-
const trendPercent = trends?.trend?.percent_change;
|
| 193 |
-
|
| 194 |
-
return (
|
| 195 |
-
<div className="p-4 bg-neutral-50 dark:bg-neutral-800 rounded-lg">
|
| 196 |
-
<div className="flex items-center justify-between mb-2">
|
| 197 |
-
<h4 className="font-medium text-neutral-900 dark:text-white">{crop}</h4>
|
| 198 |
-
{trendDirection && (
|
| 199 |
-
<div
|
| 200 |
-
className={`flex items-center gap-1 text-xs font-medium ${
|
| 201 |
-
trendDirection === "up"
|
| 202 |
-
? "text-green-600"
|
| 203 |
-
: trendDirection === "down"
|
| 204 |
-
? "text-red-600"
|
| 205 |
-
: "text-neutral-500"
|
| 206 |
-
}`}
|
| 207 |
-
>
|
| 208 |
-
{trendDirection === "up" ? (
|
| 209 |
-
<ArrowTrendingUpIcon className="h-4 w-4" />
|
| 210 |
-
) : trendDirection === "down" ? (
|
| 211 |
-
<ArrowTrendingDownIcon className="h-4 w-4" />
|
| 212 |
-
) : null}
|
| 213 |
-
{trendPercent ? `${Math.abs(trendPercent).toFixed(1)}%` : "Stable"}
|
| 214 |
-
</div>
|
| 215 |
-
)}
|
| 216 |
-
</div>
|
| 217 |
-
<p className="text-2xl font-bold text-primary-600">
|
| 218 |
-
{avgPrice ? `₹${Math.round(avgPrice).toLocaleString("en-IN")}` : "N/A"}
|
| 219 |
-
<span className="text-sm font-normal text-neutral-500">/qtl</span>
|
| 220 |
-
</p>
|
| 221 |
-
{priceData?.prices?.length > 0 && (
|
| 222 |
-
<p className="text-xs text-neutral-500 mt-1">
|
| 223 |
-
Best: {priceData.prices[0].mandi_name} ({priceData.prices[0].state})
|
| 224 |
-
</p>
|
| 225 |
-
)}
|
| 226 |
-
</div>
|
| 227 |
-
);
|
| 228 |
-
}
|
| 229 |
-
|
| 230 |
-
function APMCPricesCard({ crops, pricesData, trendsData, loading, error }) {
|
| 231 |
-
if (loading && Object.keys(pricesData).length === 0) {
|
| 232 |
-
return (
|
| 233 |
-
<Card className="p-6">
|
| 234 |
-
<div className="flex items-center gap-2 mb-4">
|
| 235 |
-
<CurrencyRupeeIcon className="h-5 w-5 text-secondary-600" />
|
| 236 |
-
<h3 className="font-semibold text-neutral-900 dark:text-white">APMC Prices</h3>
|
| 237 |
-
</div>
|
| 238 |
-
<div className="flex justify-center py-8">
|
| 239 |
-
<LoadingSpinner size="md" message="Loading prices..." />
|
| 240 |
-
</div>
|
| 241 |
-
</Card>
|
| 242 |
-
);
|
| 243 |
-
}
|
| 244 |
-
|
| 245 |
-
if (crops.length === 0) {
|
| 246 |
-
return (
|
| 247 |
-
<Card className="p-6">
|
| 248 |
-
<div className="flex items-center gap-2 mb-4">
|
| 249 |
-
<CurrencyRupeeIcon className="h-5 w-5 text-secondary-600" />
|
| 250 |
-
<h3 className="font-semibold text-neutral-900 dark:text-white">APMC Prices</h3>
|
| 251 |
-
</div>
|
| 252 |
-
<div className="text-center py-6">
|
| 253 |
-
<p className="text-sm text-neutral-500 mb-3">
|
| 254 |
-
Add your crops to see personalized APMC prices
|
| 255 |
-
</p>
|
| 256 |
-
<p className="text-xs text-neutral-400">
|
| 257 |
-
Scroll down to add crops in your profile
|
| 258 |
-
</p>
|
| 259 |
-
</div>
|
| 260 |
-
</Card>
|
| 261 |
-
);
|
| 262 |
-
}
|
| 263 |
-
|
| 264 |
-
return (
|
| 265 |
-
<Card className="p-6">
|
| 266 |
-
<div className="flex items-center justify-between mb-4">
|
| 267 |
-
<div className="flex items-center gap-2">
|
| 268 |
-
<CurrencyRupeeIcon className="h-5 w-5 text-secondary-600" />
|
| 269 |
-
<h3 className="font-semibold text-neutral-900 dark:text-white">APMC Prices</h3>
|
| 270 |
-
</div>
|
| 271 |
-
<Link
|
| 272 |
-
to={ROUTES.APMC}
|
| 273 |
-
className="text-sm text-primary-600 hover:text-primary-700 flex items-center gap-1"
|
| 274 |
-
>
|
| 275 |
-
View All <ArrowRightIcon className="h-4 w-4" />
|
| 276 |
-
</Link>
|
| 277 |
-
</div>
|
| 278 |
-
|
| 279 |
-
<p className="text-xs text-neutral-500 mb-4">Your selected crops</p>
|
| 280 |
-
|
| 281 |
-
<div className="space-y-3">
|
| 282 |
-
{crops.map((crop) => (
|
| 283 |
-
<CropPriceCard
|
| 284 |
-
key={crop}
|
| 285 |
-
crop={crop}
|
| 286 |
-
priceData={pricesData[crop]}
|
| 287 |
-
trends={trendsData[crop]}
|
| 288 |
-
loading={loading && !pricesData[crop]}
|
| 289 |
-
/>
|
| 290 |
-
))}
|
| 291 |
-
</div>
|
| 292 |
-
</Card>
|
| 293 |
-
);
|
| 294 |
-
}
|
| 295 |
-
|
| 296 |
-
function SchemesCard({ schemes, loading, error, state }) {
|
| 297 |
-
if (loading) {
|
| 298 |
-
return (
|
| 299 |
-
<Card className="p-6">
|
| 300 |
-
<div className="flex items-center gap-2 mb-4">
|
| 301 |
-
<DocumentTextIcon className="h-5 w-5 text-green-600" />
|
| 302 |
-
<h3 className="font-semibold text-neutral-900 dark:text-white">Government Schemes</h3>
|
| 303 |
-
</div>
|
| 304 |
-
<div className="flex justify-center py-8">
|
| 305 |
-
<LoadingSpinner size="md" message="Loading schemes..." />
|
| 306 |
-
</div>
|
| 307 |
-
</Card>
|
| 308 |
-
);
|
| 309 |
-
}
|
| 310 |
-
|
| 311 |
-
if (error || !schemes?.length) {
|
| 312 |
-
return (
|
| 313 |
-
<Card className="p-6">
|
| 314 |
-
<div className="flex items-center gap-2 mb-4">
|
| 315 |
-
<DocumentTextIcon className="h-5 w-5 text-green-600" />
|
| 316 |
-
<h3 className="font-semibold text-neutral-900 dark:text-white">Government Schemes</h3>
|
| 317 |
-
</div>
|
| 318 |
-
<p className="text-sm text-neutral-500">
|
| 319 |
-
{error || "No schemes available for your state"}
|
| 320 |
-
</p>
|
| 321 |
-
</Card>
|
| 322 |
-
);
|
| 323 |
-
}
|
| 324 |
-
|
| 325 |
-
const displaySchemes = schemes.slice(0, 4);
|
| 326 |
-
|
| 327 |
-
return (
|
| 328 |
-
<Card className="p-6">
|
| 329 |
-
<div className="flex items-center justify-between mb-4">
|
| 330 |
-
<div className="flex items-center gap-2">
|
| 331 |
-
<DocumentTextIcon className="h-5 w-5 text-green-600" />
|
| 332 |
-
<h3 className="font-semibold text-neutral-900 dark:text-white">Government Schemes</h3>
|
| 333 |
-
</div>
|
| 334 |
-
<Link
|
| 335 |
-
to={ROUTES.SCHEMES}
|
| 336 |
-
className="text-sm text-primary-600 hover:text-primary-700 flex items-center gap-1"
|
| 337 |
-
>
|
| 338 |
-
View All <ArrowRightIcon className="h-4 w-4" />
|
| 339 |
-
</Link>
|
| 340 |
-
</div>
|
| 341 |
-
|
| 342 |
-
<p className="text-xs text-neutral-500 mb-4">
|
| 343 |
-
Available for {state}
|
| 344 |
-
</p>
|
| 345 |
-
|
| 346 |
-
<div className="space-y-3">
|
| 347 |
-
{displaySchemes.map((scheme) => (
|
| 348 |
-
<div
|
| 349 |
-
key={scheme.id}
|
| 350 |
-
className="p-3 bg-neutral-50 dark:bg-neutral-800 rounded-lg"
|
| 351 |
-
>
|
| 352 |
-
<h4 className="text-sm font-medium text-neutral-900 dark:text-white mb-1 line-clamp-1">
|
| 353 |
-
{scheme.scheme_name}
|
| 354 |
-
</h4>
|
| 355 |
-
<p className="text-xs text-neutral-500 line-clamp-2">
|
| 356 |
-
{scheme.description}
|
| 357 |
-
</p>
|
| 358 |
-
{scheme.benefit_amount && (
|
| 359 |
-
<p className="text-xs font-medium text-green-600 mt-1">
|
| 360 |
-
Benefit: {scheme.benefit_amount}
|
| 361 |
-
</p>
|
| 362 |
-
)}
|
| 363 |
-
</div>
|
| 364 |
-
))}
|
| 365 |
-
</div>
|
| 366 |
-
|
| 367 |
-
{schemes.length > 4 && (
|
| 368 |
-
<p className="text-xs text-neutral-400 text-center mt-4">
|
| 369 |
-
+{schemes.length - 4} more schemes available
|
| 370 |
-
</p>
|
| 371 |
-
)}
|
| 372 |
-
</Card>
|
| 373 |
-
);
|
| 374 |
-
}
|
| 375 |
-
|
| 376 |
-
function ProfileEditSection({
|
| 377 |
-
user,
|
| 378 |
formData,
|
| 379 |
setFormData,
|
| 380 |
errors,
|
|
@@ -401,97 +68,94 @@ function ProfileEditSection({
|
|
| 401 |
.map((crop) => ({ value: crop, label: crop }));
|
| 402 |
|
| 403 |
return (
|
| 404 |
-
<
|
| 405 |
-
<
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
/>
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
<
|
| 426 |
-
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">
|
| 427 |
-
My Crops (Max 2)
|
| 428 |
-
</label>
|
| 429 |
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
>
|
| 437 |
-
<
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
disabled={loading}
|
| 444 |
-
>
|
| 445 |
-
<XMarkIcon className="h-4 w-4" />
|
| 446 |
-
</button>
|
| 447 |
-
</div>
|
| 448 |
-
))}
|
| 449 |
-
</div>
|
| 450 |
-
)}
|
| 451 |
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
</div>
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
<Button
|
| 471 |
-
type="button"
|
| 472 |
-
variant="secondary"
|
| 473 |
-
onClick={onAddCrop}
|
| 474 |
-
disabled={loading || !selectedCrop}
|
| 475 |
-
>
|
| 476 |
-
Add
|
| 477 |
-
</Button>
|
| 478 |
-
</>
|
| 479 |
-
)}
|
| 480 |
-
</div>
|
| 481 |
-
)}
|
| 482 |
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
</Card>
|
| 495 |
);
|
| 496 |
}
|
| 497 |
|
|
@@ -510,22 +174,6 @@ function FarmerProfilePage() {
|
|
| 510 |
const [commodities, setCommodities] = useState([]);
|
| 511 |
const [commoditiesLoading, setCommoditiesLoading] = useState(true);
|
| 512 |
|
| 513 |
-
// Weather state
|
| 514 |
-
const [weather, setWeather] = useState(null);
|
| 515 |
-
const [weatherAlerts, setWeatherAlerts] = useState(null);
|
| 516 |
-
const [weatherLoading, setWeatherLoading] = useState(true);
|
| 517 |
-
const [weatherError, setWeatherError] = useState(null);
|
| 518 |
-
|
| 519 |
-
// APMC state
|
| 520 |
-
const [pricesData, setPricesData] = useState({});
|
| 521 |
-
const [trendsData, setTrendsData] = useState({});
|
| 522 |
-
const [pricesLoading, setPricesLoading] = useState(false);
|
| 523 |
-
|
| 524 |
-
// Schemes state
|
| 525 |
-
const [schemes, setSchemes] = useState([]);
|
| 526 |
-
const [schemesLoading, setSchemesLoading] = useState(true);
|
| 527 |
-
const [schemesError, setSchemesError] = useState(null);
|
| 528 |
-
|
| 529 |
// Redirect if not authenticated
|
| 530 |
useEffect(() => {
|
| 531 |
if (!isAuthenticated) {
|
|
@@ -559,91 +207,6 @@ function FarmerProfilePage() {
|
|
| 559 |
fetchCommodities();
|
| 560 |
}, []);
|
| 561 |
|
| 562 |
-
// Fetch weather based on user location
|
| 563 |
-
useEffect(() => {
|
| 564 |
-
if (!user?.state || !user?.district || !user?.taluka) return;
|
| 565 |
-
|
| 566 |
-
async function fetchWeather() {
|
| 567 |
-
setWeatherLoading(true);
|
| 568 |
-
setWeatherError(null);
|
| 569 |
-
try {
|
| 570 |
-
const location = {
|
| 571 |
-
state: user.state,
|
| 572 |
-
district: user.district,
|
| 573 |
-
taluka: user.taluka,
|
| 574 |
-
};
|
| 575 |
-
const [forecastData, alertsData] = await Promise.all([
|
| 576 |
-
getForecast(location),
|
| 577 |
-
getAlerts(location).catch(() => null),
|
| 578 |
-
]);
|
| 579 |
-
setWeather(forecastData);
|
| 580 |
-
setWeatherAlerts(alertsData);
|
| 581 |
-
} catch (error) {
|
| 582 |
-
console.error("Failed to fetch weather:", error);
|
| 583 |
-
setWeatherError("Unable to load weather data");
|
| 584 |
-
} finally {
|
| 585 |
-
setWeatherLoading(false);
|
| 586 |
-
}
|
| 587 |
-
}
|
| 588 |
-
fetchWeather();
|
| 589 |
-
}, [user?.state, user?.district, user?.taluka]);
|
| 590 |
-
|
| 591 |
-
// Fetch APMC prices for user's crops
|
| 592 |
-
useEffect(() => {
|
| 593 |
-
const crops = user?.crops || [];
|
| 594 |
-
if (crops.length === 0) {
|
| 595 |
-
setPricesData({});
|
| 596 |
-
setTrendsData({});
|
| 597 |
-
return;
|
| 598 |
-
}
|
| 599 |
-
|
| 600 |
-
async function fetchPricesForCrops() {
|
| 601 |
-
setPricesLoading(true);
|
| 602 |
-
const newPrices = {};
|
| 603 |
-
const newTrends = {};
|
| 604 |
-
|
| 605 |
-
await Promise.all(
|
| 606 |
-
crops.map(async (crop) => {
|
| 607 |
-
try {
|
| 608 |
-
const [priceResult, trendResult] = await Promise.all([
|
| 609 |
-
getPrices({ commodity: crop, state: user.state, limit: 5 }),
|
| 610 |
-
getTrends(crop, { state: user.state, days: 7 }).catch(() => null),
|
| 611 |
-
]);
|
| 612 |
-
newPrices[crop] = priceResult;
|
| 613 |
-
if (trendResult) newTrends[crop] = trendResult;
|
| 614 |
-
} catch (error) {
|
| 615 |
-
console.error(`Failed to fetch prices for ${crop}:`, error);
|
| 616 |
-
}
|
| 617 |
-
})
|
| 618 |
-
);
|
| 619 |
-
|
| 620 |
-
setPricesData(newPrices);
|
| 621 |
-
setTrendsData(newTrends);
|
| 622 |
-
setPricesLoading(false);
|
| 623 |
-
}
|
| 624 |
-
fetchPricesForCrops();
|
| 625 |
-
}, [user?.crops, user?.state]);
|
| 626 |
-
|
| 627 |
-
// Fetch schemes for user's state
|
| 628 |
-
useEffect(() => {
|
| 629 |
-
if (!user?.state) return;
|
| 630 |
-
|
| 631 |
-
async function fetchSchemes() {
|
| 632 |
-
setSchemesLoading(true);
|
| 633 |
-
setSchemesError(null);
|
| 634 |
-
try {
|
| 635 |
-
const data = await getSchemesByState(user.state);
|
| 636 |
-
setSchemes(data.schemes || []);
|
| 637 |
-
} catch (error) {
|
| 638 |
-
console.error("Failed to fetch schemes:", error);
|
| 639 |
-
setSchemesError("Unable to load schemes");
|
| 640 |
-
} finally {
|
| 641 |
-
setSchemesLoading(false);
|
| 642 |
-
}
|
| 643 |
-
}
|
| 644 |
-
fetchSchemes();
|
| 645 |
-
}, [user?.state]);
|
| 646 |
-
|
| 647 |
// Handlers
|
| 648 |
const handleAddCrop = useCallback(() => {
|
| 649 |
if (!selectedCrop) return;
|
|
@@ -711,106 +274,95 @@ function FarmerProfilePage() {
|
|
| 711 |
variants={containerVariants}
|
| 712 |
initial="hidden"
|
| 713 |
animate="show"
|
| 714 |
-
className="max-w-
|
| 715 |
>
|
| 716 |
-
{/* Header */}
|
| 717 |
<motion.div variants={itemVariants} className="mb-8">
|
| 718 |
-
<
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
<
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 736 |
</div>
|
| 737 |
</div>
|
| 738 |
</div>
|
| 739 |
-
</div>
|
| 740 |
-
</motion.div>
|
| 741 |
-
|
| 742 |
-
{/* Dashboard Grid */}
|
| 743 |
-
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
| 744 |
-
{/* Left Column - Weather & Prices */}
|
| 745 |
-
<div className="lg:col-span-2 space-y-6">
|
| 746 |
-
<motion.div variants={itemVariants}>
|
| 747 |
-
<WeatherCard
|
| 748 |
-
weather={weather}
|
| 749 |
-
alerts={weatherAlerts}
|
| 750 |
-
loading={weatherLoading}
|
| 751 |
-
error={weatherError}
|
| 752 |
-
location={{
|
| 753 |
-
taluka: user.taluka,
|
| 754 |
-
district: user.district,
|
| 755 |
-
state: user.state,
|
| 756 |
-
}}
|
| 757 |
-
/>
|
| 758 |
-
</motion.div>
|
| 759 |
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 768 |
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
onRemoveCrop={handleRemoveCrop}
|
| 796 |
-
/>
|
| 797 |
-
</motion.div>
|
| 798 |
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
</Card>
|
| 811 |
-
</motion.div>
|
| 812 |
-
</div>
|
| 813 |
-
</div>
|
| 814 |
</motion.div>
|
| 815 |
);
|
| 816 |
}
|
|
|
|
| 1 |
/**
|
| 2 |
+
* FarmerProfilePage - User profile management page.
|
| 3 |
*
|
| 4 |
+
* Allows the logged-in user to:
|
| 5 |
+
* - View their profile information
|
| 6 |
+
* - Edit their name
|
| 7 |
+
* - Add/remove crops (max 2)
|
|
|
|
| 8 |
*
|
| 9 |
* Only accessible to logged-in users.
|
| 10 |
*/
|
|
|
|
| 19 |
MapPinIcon,
|
| 20 |
CheckIcon,
|
| 21 |
XMarkIcon,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
PencilIcon,
|
| 23 |
+
ArrowRightIcon,
|
| 24 |
} from "@heroicons/react/24/outline";
|
| 25 |
|
| 26 |
+
import { Input, Button, Card, Select, LoadingSpinner } from "@components/common";
|
| 27 |
import { useAuth } from "@context/AuthContext";
|
| 28 |
import { updateProfile } from "@services/authApi";
|
| 29 |
+
import { getCommodities } from "@services/apmcApi";
|
|
|
|
|
|
|
| 30 |
import { ROUTES } from "@utils/constants";
|
| 31 |
|
| 32 |
// Animation variants
|
|
|
|
| 40 |
show: { opacity: 1, y: 0, transition: { duration: 0.35, ease: "easeOut" } },
|
| 41 |
};
|
| 42 |
|
| 43 |
+
// Profile Edit Form Component
|
| 44 |
+
function ProfileEditForm({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
formData,
|
| 46 |
setFormData,
|
| 47 |
errors,
|
|
|
|
| 68 |
.map((crop) => ({ value: crop, label: crop }));
|
| 69 |
|
| 70 |
return (
|
| 71 |
+
<form onSubmit={onSubmit} className="space-y-6">
|
| 72 |
+
<Input
|
| 73 |
+
name="name"
|
| 74 |
+
label="Full Name"
|
| 75 |
+
type="text"
|
| 76 |
+
placeholder="Enter your full name"
|
| 77 |
+
value={formData.name}
|
| 78 |
+
onChange={handleNameChange}
|
| 79 |
+
error={errors.name}
|
| 80 |
+
required
|
| 81 |
+
maxLength={100}
|
| 82 |
+
disabled={loading}
|
| 83 |
+
/>
|
| 84 |
+
|
| 85 |
+
{/* Crops Selection */}
|
| 86 |
+
<div>
|
| 87 |
+
<label className="block text-sm font-medium text-neutral-700 dark:text-neutral-300 mb-2">
|
| 88 |
+
My Crops (Max 2)
|
| 89 |
+
</label>
|
| 90 |
+
<p className="text-xs text-neutral-500 mb-3">
|
| 91 |
+
Add your crops to see personalized APMC prices on your dashboard
|
| 92 |
+
</p>
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
+
{formData.crops.length > 0 && (
|
| 95 |
+
<div className="flex flex-wrap gap-2 mb-3">
|
| 96 |
+
{formData.crops.map((crop) => (
|
| 97 |
+
<div
|
| 98 |
+
key={crop}
|
| 99 |
+
className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-primary-50 text-primary-700 rounded-full text-sm font-medium"
|
| 100 |
+
>
|
| 101 |
+
<CheckIcon className="h-4 w-4" />
|
| 102 |
+
{crop}
|
| 103 |
+
<button
|
| 104 |
+
type="button"
|
| 105 |
+
onClick={() => onRemoveCrop(crop)}
|
| 106 |
+
className="ml-1 hover:text-primary-900 focus:outline-none"
|
| 107 |
+
disabled={loading}
|
| 108 |
+
aria-label={`Remove ${crop}`}
|
| 109 |
>
|
| 110 |
+
<XMarkIcon className="h-4 w-4" />
|
| 111 |
+
</button>
|
| 112 |
+
</div>
|
| 113 |
+
))}
|
| 114 |
+
</div>
|
| 115 |
+
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
+
{formData.crops.length < 2 && (
|
| 118 |
+
<div className="flex gap-2">
|
| 119 |
+
{commoditiesLoading ? (
|
| 120 |
+
<div className="flex-1 flex items-center justify-center py-2">
|
| 121 |
+
<LoadingSpinner size="sm" message="Loading crops..." />
|
| 122 |
+
</div>
|
| 123 |
+
) : (
|
| 124 |
+
<>
|
| 125 |
+
<div className="flex-1">
|
| 126 |
+
<Select
|
| 127 |
+
name="selectedCrop"
|
| 128 |
+
placeholder="Select a crop to add"
|
| 129 |
+
options={availableCropOptions}
|
| 130 |
+
value={selectedCrop}
|
| 131 |
+
onChange={(e) => setSelectedCrop(e.target.value)}
|
| 132 |
+
disabled={loading || availableCropOptions.length === 0}
|
| 133 |
+
/>
|
| 134 |
</div>
|
| 135 |
+
<Button
|
| 136 |
+
type="button"
|
| 137 |
+
variant="secondary"
|
| 138 |
+
onClick={onAddCrop}
|
| 139 |
+
disabled={loading || !selectedCrop}
|
| 140 |
+
>
|
| 141 |
+
Add
|
| 142 |
+
</Button>
|
| 143 |
+
</>
|
| 144 |
+
)}
|
| 145 |
+
</div>
|
| 146 |
+
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
+
{formData.crops.length === 2 && (
|
| 149 |
+
<p className="text-xs text-green-600 mt-2">
|
| 150 |
+
Maximum crops added. Remove one to add a different crop.
|
| 151 |
+
</p>
|
| 152 |
+
)}
|
| 153 |
+
</div>
|
| 154 |
|
| 155 |
+
<Button type="submit" variant="primary" fullWidth loading={loading}>
|
| 156 |
+
Save Changes
|
| 157 |
+
</Button>
|
| 158 |
+
</form>
|
|
|
|
| 159 |
);
|
| 160 |
}
|
| 161 |
|
|
|
|
| 174 |
const [commodities, setCommodities] = useState([]);
|
| 175 |
const [commoditiesLoading, setCommoditiesLoading] = useState(true);
|
| 176 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
// Redirect if not authenticated
|
| 178 |
useEffect(() => {
|
| 179 |
if (!isAuthenticated) {
|
|
|
|
| 207 |
fetchCommodities();
|
| 208 |
}, []);
|
| 209 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
// Handlers
|
| 211 |
const handleAddCrop = useCallback(() => {
|
| 212 |
if (!selectedCrop) return;
|
|
|
|
| 274 |
variants={containerVariants}
|
| 275 |
initial="hidden"
|
| 276 |
animate="show"
|
| 277 |
+
className="max-w-2xl mx-auto px-4 py-8"
|
| 278 |
>
|
| 279 |
+
{/* Page Header */}
|
| 280 |
<motion.div variants={itemVariants} className="mb-8">
|
| 281 |
+
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-2">
|
| 282 |
+
My Profile
|
| 283 |
+
</h1>
|
| 284 |
+
<p className="text-neutral-500">
|
| 285 |
+
Manage your account information and crop preferences
|
| 286 |
+
</p>
|
| 287 |
+
</motion.div>
|
| 288 |
+
|
| 289 |
+
{/* Profile Info Card */}
|
| 290 |
+
<motion.div variants={itemVariants}>
|
| 291 |
+
<Card className="p-6 mb-6">
|
| 292 |
+
<div className="flex items-start gap-4 mb-6">
|
| 293 |
+
<div className="h-16 w-16 rounded-full bg-primary-100 flex items-center justify-center shrink-0">
|
| 294 |
+
<UserIcon className="h-8 w-8 text-primary-600" />
|
| 295 |
+
</div>
|
| 296 |
+
<div className="flex-1 min-w-0">
|
| 297 |
+
<h2 className="text-xl font-semibold text-neutral-900 dark:text-white">
|
| 298 |
+
{user.name}
|
| 299 |
+
</h2>
|
| 300 |
+
<div className="flex flex-wrap items-center gap-4 mt-2">
|
| 301 |
+
<div className="flex items-center gap-1 text-neutral-500 text-sm">
|
| 302 |
+
<PhoneIcon className="h-4 w-4 shrink-0" />
|
| 303 |
+
<span>{user.mobile_number}</span>
|
| 304 |
+
</div>
|
| 305 |
+
<div className="flex items-center gap-1 text-neutral-500 text-sm">
|
| 306 |
+
<MapPinIcon className="h-4 w-4 shrink-0" />
|
| 307 |
+
<span className="truncate">
|
| 308 |
+
{user.taluka}, {user.district}, {user.state}
|
| 309 |
+
</span>
|
| 310 |
+
</div>
|
| 311 |
</div>
|
| 312 |
</div>
|
| 313 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
|
| 315 |
+
{/* Link to Dashboard */}
|
| 316 |
+
<Link
|
| 317 |
+
to={ROUTES.DASHBOARD}
|
| 318 |
+
className="flex items-center justify-between p-3 bg-primary-50 hover:bg-primary-100 rounded-lg transition-colors"
|
| 319 |
+
>
|
| 320 |
+
<span className="text-sm font-medium text-primary-700">
|
| 321 |
+
View your personalized dashboard
|
| 322 |
+
</span>
|
| 323 |
+
<ArrowRightIcon className="h-4 w-4 text-primary-600" />
|
| 324 |
+
</Link>
|
| 325 |
+
</Card>
|
| 326 |
+
</motion.div>
|
| 327 |
|
| 328 |
+
{/* Edit Profile Card */}
|
| 329 |
+
<motion.div variants={itemVariants}>
|
| 330 |
+
<Card className="p-6">
|
| 331 |
+
<div className="flex items-center gap-2 mb-6">
|
| 332 |
+
<PencilIcon className="h-5 w-5 text-primary-600" />
|
| 333 |
+
<h3 className="font-semibold text-neutral-900 dark:text-white">
|
| 334 |
+
Edit Profile
|
| 335 |
+
</h3>
|
| 336 |
+
</div>
|
| 337 |
|
| 338 |
+
<ProfileEditForm
|
| 339 |
+
formData={formData}
|
| 340 |
+
setFormData={setFormData}
|
| 341 |
+
errors={errors}
|
| 342 |
+
setErrors={setErrors}
|
| 343 |
+
loading={loading}
|
| 344 |
+
commodities={commodities}
|
| 345 |
+
commoditiesLoading={commoditiesLoading}
|
| 346 |
+
selectedCrop={selectedCrop}
|
| 347 |
+
setSelectedCrop={setSelectedCrop}
|
| 348 |
+
onSubmit={handleSubmit}
|
| 349 |
+
onAddCrop={handleAddCrop}
|
| 350 |
+
onRemoveCrop={handleRemoveCrop}
|
| 351 |
+
/>
|
| 352 |
+
</Card>
|
| 353 |
+
</motion.div>
|
|
|
|
|
|
|
|
|
|
| 354 |
|
| 355 |
+
{/* Account Info */}
|
| 356 |
+
<motion.div variants={itemVariants} className="mt-6">
|
| 357 |
+
<p className="text-xs text-neutral-400 text-center">
|
| 358 |
+
Account created on{" "}
|
| 359 |
+
{new Date(user.created_at).toLocaleDateString("en-IN", {
|
| 360 |
+
year: "numeric",
|
| 361 |
+
month: "long",
|
| 362 |
+
day: "numeric",
|
| 363 |
+
})}
|
| 364 |
+
</p>
|
| 365 |
+
</motion.div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
</motion.div>
|
| 367 |
);
|
| 368 |
}
|
frontend/src/pages/HomePage.jsx
CHANGED
|
@@ -9,6 +9,7 @@
|
|
| 9 |
|
| 10 |
import { useState, useCallback, useMemo } from "react";
|
| 11 |
import { useNavigate, Link } from "react-router-dom";
|
|
|
|
| 12 |
import { motion } from "framer-motion";
|
| 13 |
import {
|
| 14 |
CameraIcon,
|
|
@@ -35,56 +36,55 @@ import PropTypes from "prop-types";
|
|
| 35 |
// Constants
|
| 36 |
// ---------------------------------------------------------------------------
|
| 37 |
|
| 38 |
-
|
| 39 |
-
{
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
];
|
| 88 |
|
| 89 |
const SEARCH_ROUTES = [
|
| 90 |
{ keywords: ["disease", "crop", "plant", "leaf", "detect", "photo", "scan"], route: ROUTES.DISEASE_DETECTION },
|
|
@@ -130,28 +130,29 @@ const itemVariants = {
|
|
| 130 |
// ---------------------------------------------------------------------------
|
| 131 |
|
| 132 |
function GreetingBanner() {
|
|
|
|
| 133 |
const { isAuthenticated, user } = useAuth();
|
| 134 |
const hour = new Date().getHours();
|
| 135 |
-
let greeting = "
|
| 136 |
-
if (hour >= 12 && hour < 17) greeting = "
|
| 137 |
-
else if (hour >= 17) greeting = "
|
| 138 |
|
| 139 |
const firstName = user?.name?.split(" ")[0];
|
| 140 |
|
| 141 |
return (
|
| 142 |
<motion.div variants={itemVariants}>
|
| 143 |
-
<h1 className="text-3xl sm:text-4xl font-display font-bold text-neutral-900 mb-2">
|
| 144 |
{greeting}{isAuthenticated && firstName ? `, ${firstName}` : ""}
|
| 145 |
</h1>
|
| 146 |
-
<p className="text-neutral-600 text-lg max-w-xl">
|
| 147 |
-
|
| 148 |
-
tracking for Indian farmers.
|
| 149 |
</p>
|
| 150 |
</motion.div>
|
| 151 |
);
|
| 152 |
}
|
| 153 |
|
| 154 |
function QuickSearch({ onSearch }) {
|
|
|
|
| 155 |
const [query, setQuery] = useState("");
|
| 156 |
|
| 157 |
const handleSubmit = useCallback(
|
|
@@ -170,16 +171,16 @@ function QuickSearch({ onSearch }) {
|
|
| 170 |
onSubmit={handleSubmit}
|
| 171 |
className="relative max-w-lg"
|
| 172 |
role="search"
|
| 173 |
-
aria-label="
|
| 174 |
>
|
| 175 |
<MagnifyingGlassIcon className="absolute left-3.5 top-1/2 -translate-y-1/2 h-5 w-5 text-neutral-400" />
|
| 176 |
<input
|
| 177 |
type="text"
|
| 178 |
value={query}
|
| 179 |
onChange={(e) => setQuery(e.target.value)}
|
| 180 |
-
placeholder="
|
| 181 |
-
className="w-full rounded-xl border border-neutral-200 bg-white py-3 pl-11 pr-4 text-sm text-neutral-900 placeholder:text-neutral-400 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-colors"
|
| 182 |
-
aria-label="
|
| 183 |
/>
|
| 184 |
</motion.form>
|
| 185 |
);
|
|
@@ -190,6 +191,7 @@ QuickSearch.propTypes = {
|
|
| 190 |
};
|
| 191 |
|
| 192 |
function FeatureCard({ feature }) {
|
|
|
|
| 193 |
const navigate = useNavigate();
|
| 194 |
const { Icon } = feature;
|
| 195 |
|
|
@@ -210,11 +212,11 @@ function FeatureCard({ feature }) {
|
|
| 210 |
<h3 className={`text-base font-semibold ${feature.textClass} mb-1`}>
|
| 211 |
{feature.title}
|
| 212 |
</h3>
|
| 213 |
-
<p className="text-sm text-neutral-600 leading-relaxed">
|
| 214 |
{feature.description}
|
| 215 |
</p>
|
| 216 |
-
<div className="mt-3 flex items-center gap-1 text-sm font-medium text-primary-600">
|
| 217 |
-
|
| 218 |
<ArrowRightIcon className="h-4 w-4" />
|
| 219 |
</div>
|
| 220 |
</div>
|
|
@@ -237,6 +239,8 @@ FeatureCard.propTypes = {
|
|
| 237 |
};
|
| 238 |
|
| 239 |
function NetworkBanner({ isOnline }) {
|
|
|
|
|
|
|
| 240 |
if (isOnline) return null;
|
| 241 |
|
| 242 |
return (
|
|
@@ -248,13 +252,14 @@ function NetworkBanner({ isOnline }) {
|
|
| 248 |
>
|
| 249 |
<SignalSlashIcon className="h-5 w-5 text-secondary-600 shrink-0" />
|
| 250 |
<p className="text-sm text-secondary-800">
|
| 251 |
-
|
| 252 |
</p>
|
| 253 |
</motion.div>
|
| 254 |
);
|
| 255 |
}
|
| 256 |
|
| 257 |
function CropsAlertBanner() {
|
|
|
|
| 258 |
const { isAuthenticated, user } = useAuth();
|
| 259 |
|
| 260 |
// Show alert only for logged-in users without crops
|
|
@@ -271,16 +276,16 @@ function CropsAlertBanner() {
|
|
| 271 |
<ExclamationTriangleIcon className="h-6 w-6 text-amber-600 shrink-0 mt-0.5" />
|
| 272 |
<div className="flex-1">
|
| 273 |
<h3 className="text-sm font-semibold text-amber-800">
|
| 274 |
-
|
| 275 |
</h3>
|
| 276 |
<p className="text-sm text-amber-700 mt-1">
|
| 277 |
-
|
| 278 |
</p>
|
| 279 |
<Link
|
| 280 |
to={ROUTES.PROFILE}
|
| 281 |
className="inline-flex items-center gap-1 mt-2 text-sm font-medium text-amber-700 hover:text-amber-900"
|
| 282 |
>
|
| 283 |
-
|
| 284 |
<ArrowRightIcon className="h-4 w-4" />
|
| 285 |
</Link>
|
| 286 |
</div>
|
|
@@ -293,6 +298,7 @@ NetworkBanner.propTypes = {
|
|
| 293 |
};
|
| 294 |
|
| 295 |
function RecentActivity() {
|
|
|
|
| 296 |
const activities = useMemo(() => getRecentActivities(8), []);
|
| 297 |
|
| 298 |
if (activities.length === 0) {
|
|
@@ -301,9 +307,9 @@ function RecentActivity() {
|
|
| 301 |
|
| 302 |
return (
|
| 303 |
<motion.div variants={itemVariants}>
|
| 304 |
-
<h2 className="text-lg font-semibold text-neutral-900 mb-4 flex items-center gap-2">
|
| 305 |
<ClockIcon className="h-5 w-5 text-neutral-500" />
|
| 306 |
-
|
| 307 |
</h2>
|
| 308 |
<div className="space-y-2">
|
| 309 |
{activities.map((activity) => {
|
|
@@ -338,19 +344,20 @@ function RecentActivity() {
|
|
| 338 |
}
|
| 339 |
|
| 340 |
function QuickStats() {
|
|
|
|
| 341 |
const isOnline = useNetworkStatus();
|
| 342 |
|
| 343 |
return (
|
| 344 |
<motion.div variants={itemVariants} className="grid grid-cols-3 gap-3">
|
| 345 |
<div className="rounded-xl bg-primary-50 border border-primary-100 p-4 text-center">
|
| 346 |
<CameraIcon className="h-6 w-6 text-primary-600 mx-auto mb-1" />
|
| 347 |
-
<p className="text-xs text-primary-700 font-medium">
|
| 348 |
-
<p className="text-xs text-primary-500 mt-0.5">
|
| 349 |
</div>
|
| 350 |
<div className="rounded-xl bg-accent-50 border border-accent-100 p-4 text-center">
|
| 351 |
<CloudIcon className="h-6 w-6 text-accent-600 mx-auto mb-1" />
|
| 352 |
-
<p className="text-xs text-accent-700 font-medium">
|
| 353 |
-
<p className="text-xs text-accent-500 mt-0.5">
|
| 354 |
</div>
|
| 355 |
<div className="rounded-xl bg-secondary-50 border border-secondary-100 p-4 text-center">
|
| 356 |
<div className="flex items-center justify-center gap-1 mb-1">
|
|
@@ -360,9 +367,9 @@ function QuickStats() {
|
|
| 360 |
<SignalSlashIcon className="h-6 w-6 text-secondary-400" />
|
| 361 |
)}
|
| 362 |
</div>
|
| 363 |
-
<p className="text-xs text-secondary-700 font-medium">
|
| 364 |
<p className="text-xs text-secondary-500 mt-0.5">
|
| 365 |
-
{isOnline ? "
|
| 366 |
</p>
|
| 367 |
</div>
|
| 368 |
</motion.div>
|
|
@@ -376,6 +383,7 @@ function QuickStats() {
|
|
| 376 |
function HomePage() {
|
| 377 |
const navigate = useNavigate();
|
| 378 |
const isOnline = useNetworkStatus();
|
|
|
|
| 379 |
|
| 380 |
const handleSearch = useCallback(
|
| 381 |
(query) => {
|
|
@@ -406,7 +414,7 @@ function HomePage() {
|
|
| 406 |
|
| 407 |
{/* Feature Cards */}
|
| 408 |
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-2 xl:grid-cols-4">
|
| 409 |
-
{
|
| 410 |
<FeatureCard key={feature.key} feature={feature} />
|
| 411 |
))}
|
| 412 |
</div>
|
|
|
|
| 9 |
|
| 10 |
import { useState, useCallback, useMemo } from "react";
|
| 11 |
import { useNavigate, Link } from "react-router-dom";
|
| 12 |
+
import { useTranslation } from "react-i18next";
|
| 13 |
import { motion } from "framer-motion";
|
| 14 |
import {
|
| 15 |
CameraIcon,
|
|
|
|
| 36 |
// Constants
|
| 37 |
// ---------------------------------------------------------------------------
|
| 38 |
|
| 39 |
+
function useFeatures() {
|
| 40 |
+
const { t } = useTranslation();
|
| 41 |
+
return useMemo(() => [
|
| 42 |
+
{
|
| 43 |
+
key: "disease",
|
| 44 |
+
title: t("features.diseaseDetection.title"),
|
| 45 |
+
description: t("features.diseaseDetection.description"),
|
| 46 |
+
route: ROUTES.DISEASE_DETECTION,
|
| 47 |
+
Icon: CameraIcon,
|
| 48 |
+
color: "primary",
|
| 49 |
+
bgClass: "bg-primary-50",
|
| 50 |
+
textClass: "text-primary-700",
|
| 51 |
+
iconBg: "bg-primary-100",
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
key: "weather",
|
| 55 |
+
title: t("features.weather.title"),
|
| 56 |
+
description: t("features.weather.description"),
|
| 57 |
+
route: ROUTES.WEATHER,
|
| 58 |
+
Icon: CloudIcon,
|
| 59 |
+
color: "accent",
|
| 60 |
+
bgClass: "bg-accent-50",
|
| 61 |
+
textClass: "text-accent-700",
|
| 62 |
+
iconBg: "bg-accent-100",
|
| 63 |
+
},
|
| 64 |
+
{
|
| 65 |
+
key: "apmc",
|
| 66 |
+
title: t("features.apmc.title"),
|
| 67 |
+
description: t("features.apmc.description"),
|
| 68 |
+
route: ROUTES.APMC,
|
| 69 |
+
Icon: CurrencyRupeeIcon,
|
| 70 |
+
color: "secondary",
|
| 71 |
+
bgClass: "bg-secondary-50",
|
| 72 |
+
textClass: "text-secondary-700",
|
| 73 |
+
iconBg: "bg-secondary-100",
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
key: "schemes",
|
| 77 |
+
title: t("features.schemes.title"),
|
| 78 |
+
description: t("features.schemes.description"),
|
| 79 |
+
route: ROUTES.SCHEMES,
|
| 80 |
+
Icon: DocumentTextIcon,
|
| 81 |
+
color: "success",
|
| 82 |
+
bgClass: "bg-green-50",
|
| 83 |
+
textClass: "text-green-700",
|
| 84 |
+
iconBg: "bg-green-100",
|
| 85 |
+
},
|
| 86 |
+
], [t]);
|
| 87 |
+
}
|
|
|
|
| 88 |
|
| 89 |
const SEARCH_ROUTES = [
|
| 90 |
{ keywords: ["disease", "crop", "plant", "leaf", "detect", "photo", "scan"], route: ROUTES.DISEASE_DETECTION },
|
|
|
|
| 130 |
// ---------------------------------------------------------------------------
|
| 131 |
|
| 132 |
function GreetingBanner() {
|
| 133 |
+
const { t } = useTranslation();
|
| 134 |
const { isAuthenticated, user } = useAuth();
|
| 135 |
const hour = new Date().getHours();
|
| 136 |
+
let greeting = t("home.greeting.morning");
|
| 137 |
+
if (hour >= 12 && hour < 17) greeting = t("home.greeting.afternoon");
|
| 138 |
+
else if (hour >= 17) greeting = t("home.greeting.evening");
|
| 139 |
|
| 140 |
const firstName = user?.name?.split(" ")[0];
|
| 141 |
|
| 142 |
return (
|
| 143 |
<motion.div variants={itemVariants}>
|
| 144 |
+
<h1 className="text-3xl sm:text-4xl font-display font-bold text-neutral-900 dark:text-neutral-100 mb-2">
|
| 145 |
{greeting}{isAuthenticated && firstName ? `, ${firstName}` : ""}
|
| 146 |
</h1>
|
| 147 |
+
<p className="text-neutral-600 dark:text-neutral-400 text-lg max-w-xl">
|
| 148 |
+
{t("home.tagline")}
|
|
|
|
| 149 |
</p>
|
| 150 |
</motion.div>
|
| 151 |
);
|
| 152 |
}
|
| 153 |
|
| 154 |
function QuickSearch({ onSearch }) {
|
| 155 |
+
const { t } = useTranslation();
|
| 156 |
const [query, setQuery] = useState("");
|
| 157 |
|
| 158 |
const handleSubmit = useCallback(
|
|
|
|
| 171 |
onSubmit={handleSubmit}
|
| 172 |
className="relative max-w-lg"
|
| 173 |
role="search"
|
| 174 |
+
aria-label={t("common.search")}
|
| 175 |
>
|
| 176 |
<MagnifyingGlassIcon className="absolute left-3.5 top-1/2 -translate-y-1/2 h-5 w-5 text-neutral-400" />
|
| 177 |
<input
|
| 178 |
type="text"
|
| 179 |
value={query}
|
| 180 |
onChange={(e) => setQuery(e.target.value)}
|
| 181 |
+
placeholder={t("home.searchPlaceholder")}
|
| 182 |
+
className="w-full rounded-xl border border-neutral-200 bg-white dark:bg-neutral-800 dark:border-neutral-700 py-3 pl-11 pr-4 text-sm text-neutral-900 dark:text-neutral-100 placeholder:text-neutral-400 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-colors"
|
| 183 |
+
aria-label={t("common.search")}
|
| 184 |
/>
|
| 185 |
</motion.form>
|
| 186 |
);
|
|
|
|
| 191 |
};
|
| 192 |
|
| 193 |
function FeatureCard({ feature }) {
|
| 194 |
+
const { t } = useTranslation();
|
| 195 |
const navigate = useNavigate();
|
| 196 |
const { Icon } = feature;
|
| 197 |
|
|
|
|
| 212 |
<h3 className={`text-base font-semibold ${feature.textClass} mb-1`}>
|
| 213 |
{feature.title}
|
| 214 |
</h3>
|
| 215 |
+
<p className="text-sm text-neutral-600 dark:text-neutral-400 leading-relaxed">
|
| 216 |
{feature.description}
|
| 217 |
</p>
|
| 218 |
+
<div className="mt-3 flex items-center gap-1 text-sm font-medium text-primary-600 dark:text-primary-400">
|
| 219 |
+
{t("common.getStarted")}
|
| 220 |
<ArrowRightIcon className="h-4 w-4" />
|
| 221 |
</div>
|
| 222 |
</div>
|
|
|
|
| 239 |
};
|
| 240 |
|
| 241 |
function NetworkBanner({ isOnline }) {
|
| 242 |
+
const { t } = useTranslation();
|
| 243 |
+
|
| 244 |
if (isOnline) return null;
|
| 245 |
|
| 246 |
return (
|
|
|
|
| 252 |
>
|
| 253 |
<SignalSlashIcon className="h-5 w-5 text-secondary-600 shrink-0" />
|
| 254 |
<p className="text-sm text-secondary-800">
|
| 255 |
+
{t("common.offline")}
|
| 256 |
</p>
|
| 257 |
</motion.div>
|
| 258 |
);
|
| 259 |
}
|
| 260 |
|
| 261 |
function CropsAlertBanner() {
|
| 262 |
+
const { t } = useTranslation();
|
| 263 |
const { isAuthenticated, user } = useAuth();
|
| 264 |
|
| 265 |
// Show alert only for logged-in users without crops
|
|
|
|
| 276 |
<ExclamationTriangleIcon className="h-6 w-6 text-amber-600 shrink-0 mt-0.5" />
|
| 277 |
<div className="flex-1">
|
| 278 |
<h3 className="text-sm font-semibold text-amber-800">
|
| 279 |
+
{t("home.completeProfile")}
|
| 280 |
</h3>
|
| 281 |
<p className="text-sm text-amber-700 mt-1">
|
| 282 |
+
{t("home.addCropsMessage")}
|
| 283 |
</p>
|
| 284 |
<Link
|
| 285 |
to={ROUTES.PROFILE}
|
| 286 |
className="inline-flex items-center gap-1 mt-2 text-sm font-medium text-amber-700 hover:text-amber-900"
|
| 287 |
>
|
| 288 |
+
{t("home.addCropsNow")}
|
| 289 |
<ArrowRightIcon className="h-4 w-4" />
|
| 290 |
</Link>
|
| 291 |
</div>
|
|
|
|
| 298 |
};
|
| 299 |
|
| 300 |
function RecentActivity() {
|
| 301 |
+
const { t } = useTranslation();
|
| 302 |
const activities = useMemo(() => getRecentActivities(8), []);
|
| 303 |
|
| 304 |
if (activities.length === 0) {
|
|
|
|
| 307 |
|
| 308 |
return (
|
| 309 |
<motion.div variants={itemVariants}>
|
| 310 |
+
<h2 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100 mb-4 flex items-center gap-2">
|
| 311 |
<ClockIcon className="h-5 w-5 text-neutral-500" />
|
| 312 |
+
{t("home.recentActivity")}
|
| 313 |
</h2>
|
| 314 |
<div className="space-y-2">
|
| 315 |
{activities.map((activity) => {
|
|
|
|
| 344 |
}
|
| 345 |
|
| 346 |
function QuickStats() {
|
| 347 |
+
const { t } = useTranslation();
|
| 348 |
const isOnline = useNetworkStatus();
|
| 349 |
|
| 350 |
return (
|
| 351 |
<motion.div variants={itemVariants} className="grid grid-cols-3 gap-3">
|
| 352 |
<div className="rounded-xl bg-primary-50 border border-primary-100 p-4 text-center">
|
| 353 |
<CameraIcon className="h-6 w-6 text-primary-600 mx-auto mb-1" />
|
| 354 |
+
<p className="text-xs text-primary-700 font-medium">{t("features.diseaseDetection.title")}</p>
|
| 355 |
+
<p className="text-xs text-primary-500 mt-0.5">{t("features.diseaseDetection.aiPowered")}</p>
|
| 356 |
</div>
|
| 357 |
<div className="rounded-xl bg-accent-50 border border-accent-100 p-4 text-center">
|
| 358 |
<CloudIcon className="h-6 w-6 text-accent-600 mx-auto mb-1" />
|
| 359 |
+
<p className="text-xs text-accent-700 font-medium">{t("features.weather.sevenDay")}</p>
|
| 360 |
+
<p className="text-xs text-accent-500 mt-0.5">{t("features.weather.pincodeBased")}</p>
|
| 361 |
</div>
|
| 362 |
<div className="rounded-xl bg-secondary-50 border border-secondary-100 p-4 text-center">
|
| 363 |
<div className="flex items-center justify-center gap-1 mb-1">
|
|
|
|
| 367 |
<SignalSlashIcon className="h-6 w-6 text-secondary-400" />
|
| 368 |
)}
|
| 369 |
</div>
|
| 370 |
+
<p className="text-xs text-secondary-700 font-medium">{t("features.apmc.livePrices")}</p>
|
| 371 |
<p className="text-xs text-secondary-500 mt-0.5">
|
| 372 |
+
{isOnline ? t("common.connected") : "Offline"}
|
| 373 |
</p>
|
| 374 |
</div>
|
| 375 |
</motion.div>
|
|
|
|
| 383 |
function HomePage() {
|
| 384 |
const navigate = useNavigate();
|
| 385 |
const isOnline = useNetworkStatus();
|
| 386 |
+
const features = useFeatures();
|
| 387 |
|
| 388 |
const handleSearch = useCallback(
|
| 389 |
(query) => {
|
|
|
|
| 414 |
|
| 415 |
{/* Feature Cards */}
|
| 416 |
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-2 xl:grid-cols-4">
|
| 417 |
+
{features.map((feature) => (
|
| 418 |
<FeatureCard key={feature.key} feature={feature} />
|
| 419 |
))}
|
| 420 |
</div>
|
frontend/src/utils/constants.js
CHANGED
|
@@ -25,7 +25,7 @@ export const API_RETRY_DELAY_MS = 1000;
|
|
| 25 |
// Application Metadata
|
| 26 |
// ---------------------------------------------------------------------------
|
| 27 |
|
| 28 |
-
export const APP_NAME = import.meta.env.VITE_APP_NAME || "
|
| 29 |
|
| 30 |
export const APP_VERSION = import.meta.env.VITE_APP_VERSION || "1.0.0";
|
| 31 |
|
|
@@ -116,6 +116,13 @@ export const MODEL_PATH = import.meta.env.VITE_MODEL_PATH || "/models";
|
|
| 116 |
export const LANGUAGES = {
|
| 117 |
EN: "en",
|
| 118 |
HI: "hi",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
};
|
| 120 |
|
| 121 |
export const DEFAULT_LANGUAGE = LANGUAGES.EN;
|
|
@@ -148,6 +155,7 @@ export const ROUTES = {
|
|
| 148 |
LOGIN: "/login",
|
| 149 |
SIGNUP: "/signup",
|
| 150 |
PROFILE: "/profile",
|
|
|
|
| 151 |
DISEASE_DETECTION: "/disease",
|
| 152 |
WEATHER: "/weather",
|
| 153 |
APMC: "/apmc",
|
|
|
|
| 25 |
// Application Metadata
|
| 26 |
// ---------------------------------------------------------------------------
|
| 27 |
|
| 28 |
+
export const APP_NAME = import.meta.env.VITE_APP_NAME || "KrishiNiti";
|
| 29 |
|
| 30 |
export const APP_VERSION = import.meta.env.VITE_APP_VERSION || "1.0.0";
|
| 31 |
|
|
|
|
| 116 |
export const LANGUAGES = {
|
| 117 |
EN: "en",
|
| 118 |
HI: "hi",
|
| 119 |
+
GU: "gu",
|
| 120 |
+
};
|
| 121 |
+
|
| 122 |
+
export const LANGUAGE_NAMES = {
|
| 123 |
+
[LANGUAGES.EN]: "English",
|
| 124 |
+
[LANGUAGES.HI]: "हिंदी",
|
| 125 |
+
[LANGUAGES.GU]: "ગુજરાતી",
|
| 126 |
};
|
| 127 |
|
| 128 |
export const DEFAULT_LANGUAGE = LANGUAGES.EN;
|
|
|
|
| 155 |
LOGIN: "/login",
|
| 156 |
SIGNUP: "/signup",
|
| 157 |
PROFILE: "/profile",
|
| 158 |
+
DASHBOARD: "/my-dashboard",
|
| 159 |
DISEASE_DETECTION: "/disease",
|
| 160 |
WEATHER: "/weather",
|
| 161 |
APMC: "/apmc",
|
frontend/vite.config.js
CHANGED
|
@@ -25,6 +25,8 @@ export default defineConfig(({ mode }) => {
|
|
| 25 |
"@pages": path.resolve(__dirname, "./src/pages"),
|
| 26 |
"@assets": path.resolve(__dirname, "./src/assets"),
|
| 27 |
"@styles": path.resolve(__dirname, "./src/styles"),
|
|
|
|
|
|
|
| 28 |
},
|
| 29 |
},
|
| 30 |
|
|
|
|
| 25 |
"@pages": path.resolve(__dirname, "./src/pages"),
|
| 26 |
"@assets": path.resolve(__dirname, "./src/assets"),
|
| 27 |
"@styles": path.resolve(__dirname, "./src/styles"),
|
| 28 |
+
"@locales": path.resolve(__dirname, "./src/locales"),
|
| 29 |
+
"@i18n": path.resolve(__dirname, "./src/i18n"),
|
| 30 |
},
|
| 31 |
},
|
| 32 |
|