Gankit12 commited on
Commit
75fda81
·
1 Parent(s): 71b2afb
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
- ENV DATABASE_URL=sqlite:///./farmhelp.db
 
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
- # Init DB from JSON data, then start server (PORT set by HF Spaces, default 7860)
66
- CMD ["sh", "-c", "python init_db.py && (python seed_data.py || true) && exec uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-7860}"]
 
 
 
 
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
- _ViTFeatureExtractor = None
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 _ViTFeatureExtractor, _ViTForImageClassification
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
- ViTFeatureExtractor,
57
  ViTForImageClassification,
58
  )
59
 
@@ -62,7 +62,7 @@ def _ensure_imports():
62
  _transforms = transforms
63
  _AutoImageProcessor = AutoImageProcessor
64
  _AutoModelForImageClassification = AutoModelForImageClassification
65
- _ViTFeatureExtractor = ViTFeatureExtractor
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 = _ViTFeatureExtractor.from_pretrained(VIT_MODEL_ID)
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.30.0
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
- # FarmHelp Frontend Environment Variables
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=FarmHelp
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="FarmHelp - 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,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>FarmHelp - Smart Agriculture Assistant</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, toggleLanguage, theme, toggleTheme } = useApp();
41
  const location = useLocation();
42
 
43
  return (
44
  <Layout
45
  language={language}
46
- onToggleLanguage={toggleLanguage}
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={item.label}
85
  >
86
  <IconComponent className="h-6 w-6" aria-hidden="true" />
87
- <span className="text-[10px] font-medium leading-tight">
88
- {item.label}
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
- AI-powered agricultural support for Indian farmers.
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} {APP_NAME}. Built for Indian farmers.
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 toggle, dark mode toggle, and user profile.
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, LANGUAGES, APP_NAME } from "@utils/constants";
22
  import { useAuth } from "@context/AuthContext";
 
23
 
24
- const NAV_ITEMS = [
25
- { label: "Home", path: ROUTES.HOME },
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
- FH
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={isDark ? "Switch to light mode" : "Switch to dark mode"}
112
  id="theme-toggle"
113
  >
114
  {isDark ? (
@@ -119,27 +121,39 @@ function Header({ language, onToggleLanguage, theme, onToggleTheme }) {
119
  </button>
120
  )}
121
 
122
- {/* Language toggle */}
123
- {onToggleLanguage && (
124
- <button
125
- type="button"
126
- onClick={onToggleLanguage}
127
- className="inline-flex items-center gap-1 rounded-lg px-2.5 py-1.5 text-sm font-medium text-neutral-600 hover:bg-neutral-100 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
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="inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-sm font-medium text-neutral-600 hover:bg-neutral-100 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
 
 
 
 
 
 
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="Logout"
152
- title="Logout"
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
- Login
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="Toggle navigation menu"
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
- My Profile
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
- Logout
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
- Login
247
  </Link>
248
  )}
249
  </div>
@@ -256,7 +283,7 @@ function Header({ language, onToggleLanguage, theme, onToggleTheme }) {
256
 
257
  Header.propTypes = {
258
  language: PropTypes.string,
259
- onToggleLanguage: PropTypes.func,
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
- onToggleLanguage,
19
  theme,
20
  onToggleTheme,
21
  hideHeader = false,
@@ -33,7 +33,7 @@ function Layout({
33
  {!hideHeader && (
34
  <Header
35
  language={language}
36
- onToggleLanguage={onToggleLanguage}
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
- onToggleLanguage: PropTypes.func,
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
- }, [state.language]);
 
 
 
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 nextLang =
170
- state.language === LANGUAGES.EN ? LANGUAGES.HI : LANGUAGES.EN;
171
- dispatch({ type: ACTION_TYPES.SET_LANGUAGE, payload: nextLang });
 
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 - Personalized farmer dashboard.
3
  *
4
- * Displays:
5
- * - User profile with edit capability
6
- * - Personalized weather info based on location
7
- * - APMC prices for selected crops
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, Tabs } from "@components/common";
35
  import { useAuth } from "@context/AuthContext";
36
  import { updateProfile } from "@services/authApi";
37
- import { getCommodities, getPrices, getTrends } from "@services/apmcApi";
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
- // Weather icon mapping
54
- function getWeatherIcon(code) {
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
- <Card className="p-6">
405
- <div className="flex items-center gap-2 mb-4">
406
- <PencilIcon className="h-5 w-5 text-primary-600" />
407
- <h3 className="font-semibold text-neutral-900 dark:text-white">Edit Profile</h3>
408
- </div>
409
-
410
- <form onSubmit={onSubmit} className="space-y-5">
411
- <Input
412
- name="name"
413
- label="Full Name"
414
- type="text"
415
- placeholder="Enter your full name"
416
- value={formData.name}
417
- onChange={handleNameChange}
418
- error={errors.name}
419
- required
420
- maxLength={100}
421
- disabled={loading}
422
- />
423
-
424
- {/* Crops Selection */}
425
- <div>
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
- {formData.crops.length > 0 && (
431
- <div className="flex flex-wrap gap-2 mb-3">
432
- {formData.crops.map((crop) => (
433
- <div
434
- key={crop}
435
- 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"
 
 
 
 
 
 
 
 
 
436
  >
437
- <CheckIcon className="h-4 w-4" />
438
- {crop}
439
- <button
440
- type="button"
441
- onClick={() => onRemoveCrop(crop)}
442
- className="ml-1 hover:text-primary-900 focus:outline-none"
443
- disabled={loading}
444
- >
445
- <XMarkIcon className="h-4 w-4" />
446
- </button>
447
- </div>
448
- ))}
449
- </div>
450
- )}
451
 
452
- {formData.crops.length < 2 && (
453
- <div className="flex gap-2">
454
- {commoditiesLoading ? (
455
- <div className="flex-1 flex items-center justify-center py-2">
456
- <LoadingSpinner size="sm" message="Loading crops..." />
 
 
 
 
 
 
 
 
 
 
 
 
457
  </div>
458
- ) : (
459
- <>
460
- <div className="flex-1">
461
- <Select
462
- name="selectedCrop"
463
- placeholder="Select a crop to add"
464
- options={availableCropOptions}
465
- value={selectedCrop}
466
- onChange={(e) => setSelectedCrop(e.target.value)}
467
- disabled={loading || availableCropOptions.length === 0}
468
- />
469
- </div>
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
- {formData.crops.length === 0 && (
484
- <p className="mt-2 text-sm text-neutral-500">
485
- Add crops to see personalized APMC prices above
486
- </p>
487
- )}
488
- </div>
489
 
490
- <Button type="submit" variant="primary" fullWidth loading={loading}>
491
- Save Changes
492
- </Button>
493
- </form>
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-6xl mx-auto px-4 py-8"
715
  >
716
- {/* Header */}
717
  <motion.div variants={itemVariants} className="mb-8">
718
- <div className="flex items-center gap-4">
719
- <div className="h-16 w-16 rounded-full bg-primary-100 flex items-center justify-center">
720
- <UserIcon className="h-8 w-8 text-primary-600" />
721
- </div>
722
- <div>
723
- <h1 className="text-2xl font-bold text-neutral-900 dark:text-white">
724
- Welcome, {user.name?.split(" ")[0]}
725
- </h1>
726
- <div className="flex items-center gap-4 mt-1">
727
- <div className="flex items-center gap-1 text-neutral-500 text-sm">
728
- <PhoneIcon className="h-4 w-4" />
729
- <span>{user.mobile_number}</span>
730
- </div>
731
- <div className="flex items-center gap-1 text-neutral-500 text-sm">
732
- <MapPinIcon className="h-4 w-4" />
733
- <span>
734
- {user.taluka}, {user.district}, {user.state}
735
- </span>
 
 
 
 
 
 
 
 
 
 
 
 
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
- <motion.div variants={itemVariants}>
761
- <APMCPricesCard
762
- crops={user.crops || []}
763
- pricesData={pricesData}
764
- trendsData={trendsData}
765
- loading={pricesLoading}
766
- />
767
- </motion.div>
 
 
 
 
768
 
769
- <motion.div variants={itemVariants}>
770
- <SchemesCard
771
- schemes={schemes}
772
- loading={schemesLoading}
773
- error={schemesError}
774
- state={user.state}
775
- />
776
- </motion.div>
777
- </div>
778
 
779
- {/* Right Column - Profile Edit */}
780
- <div className="space-y-6">
781
- <motion.div variants={itemVariants}>
782
- <ProfileEditSection
783
- user={user}
784
- formData={formData}
785
- setFormData={setFormData}
786
- errors={errors}
787
- setErrors={setErrors}
788
- loading={loading}
789
- commodities={commodities}
790
- commoditiesLoading={commoditiesLoading}
791
- selectedCrop={selectedCrop}
792
- setSelectedCrop={setSelectedCrop}
793
- onSubmit={handleSubmit}
794
- onAddCrop={handleAddCrop}
795
- onRemoveCrop={handleRemoveCrop}
796
- />
797
- </motion.div>
798
 
799
- {/* Account Info */}
800
- <motion.div variants={itemVariants}>
801
- <Card className="p-4">
802
- <p className="text-xs text-neutral-500 text-center">
803
- Account created on{" "}
804
- {new Date(user.created_at).toLocaleDateString("en-IN", {
805
- year: "numeric",
806
- month: "long",
807
- day: "numeric",
808
- })}
809
- </p>
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
- const FEATURES = [
39
- {
40
- key: "disease",
41
- title: "Disease Detection",
42
- description:
43
- "Upload a photo of your crop and get instant AI-powered disease identification with treatment recommendations.",
44
- route: ROUTES.DISEASE_DETECTION,
45
- Icon: CameraIcon,
46
- color: "primary",
47
- bgClass: "bg-primary-50",
48
- textClass: "text-primary-700",
49
- iconBg: "bg-primary-100",
50
- },
51
- {
52
- key: "weather",
53
- title: "Weather Forecast",
54
- description:
55
- "Get accurate 7-day weather forecasts, farming advisories, and severe weather alerts for your area.",
56
- route: ROUTES.WEATHER,
57
- Icon: CloudIcon,
58
- color: "accent",
59
- bgClass: "bg-accent-50",
60
- textClass: "text-accent-700",
61
- iconBg: "bg-accent-100",
62
- },
63
- {
64
- key: "apmc",
65
- title: "APMC Price",
66
- description:
67
- "Compare real-time commodity prices across APMCs, find the best selling market, and track price trends.",
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: "Government Schemes",
78
- description:
79
- "Explore national and state-specific schemes with complete details on benefits, eligibility, and application process.",
80
- route: ROUTES.SCHEMES,
81
- Icon: DocumentTextIcon,
82
- color: "success",
83
- bgClass: "bg-green-50",
84
- textClass: "text-green-700",
85
- iconBg: "bg-green-100",
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 = "Good morning";
136
- if (hour >= 12 && hour < 17) greeting = "Good afternoon";
137
- else if (hour >= 17) greeting = "Good evening";
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
- AI-powered crop disease detection, weather forecasting, and market price
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="Search features"
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="Search features... (e.g. crop disease, weather, APMC prices)"
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="Search"
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
- Get started
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
- You are currently offline. Some features may be limited.
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
- Complete Your Profile
275
  </h3>
276
  <p className="text-sm text-amber-700 mt-1">
277
- Add your crops to get personalized APMC price alerts and farming recommendations.
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
- Add Crops Now
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
- Recent Activity
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">Disease Detection</p>
348
- <p className="text-xs text-primary-500 mt-0.5">AI-powered</p>
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">7-Day Forecast</p>
353
- <p className="text-xs text-accent-500 mt-0.5">Pincode-based</p>
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">Live Prices</p>
364
  <p className="text-xs text-secondary-500 mt-0.5">
365
- {isOnline ? "Connected" : "Offline"}
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
- {FEATURES.map((feature) => (
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 || "FarmHelp";
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