datamk commited on
Commit
0d8a28a
·
verified ·
1 Parent(s): 57da3ff

Upload 63 files

Browse files
index.html CHANGED
@@ -3,11 +3,54 @@
3
  <head>
4
  <meta charset="UTF-8" />
5
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 
 
7
  <title>Meri Mandi</title>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  </head>
9
  <body>
 
 
 
 
10
  <div id="root"></div>
11
  <script type="module" src="/src/main.jsx"></script>
 
 
 
 
 
 
 
 
 
12
  </body>
13
  </html>
 
3
  <head>
4
  <meta charset="UTF-8" />
5
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0" />
7
+ <meta name="theme-color" content="#10b981" />
8
+ <meta name="description" content="Meri Mandi - Digital Marketplace for Farmers" />
9
  <title>Meri Mandi</title>
10
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
11
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
12
+ <style>
13
+ #initial-loader {
14
+ position: fixed;
15
+ top: 0;
16
+ left: 0;
17
+ width: 100%;
18
+ height: 100%;
19
+ display: flex;
20
+ flex-direction: column;
21
+ justify-content: center;
22
+ align-items: center;
23
+ background: #0f172a;
24
+ z-index: 9999;
25
+ transition: opacity 0.5s ease-out;
26
+ }
27
+ .spinner {
28
+ width: 40px;
29
+ height: 40px;
30
+ border: 4px solid rgba(16, 185, 129, 0.1);
31
+ border-top: 4px solid #10b981;
32
+ border-radius: 50%;
33
+ animation: spin 1s linear infinite;
34
+ }
35
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
36
+ .loading-text { color: #10b981; margin-top: 15px; font-family: sans-serif; font-size: 14px; font-weight: 500; }
37
+ </style>
38
  </head>
39
  <body>
40
+ <div id="initial-loader">
41
+ <div class="spinner"></div>
42
+ <div class="loading-text">🌾 Meri Mandi...</div>
43
+ </div>
44
  <div id="root"></div>
45
  <script type="module" src="/src/main.jsx"></script>
46
+ <script>
47
+ window.addEventListener('load', () => {
48
+ const loader = document.getElementById('initial-loader');
49
+ if (loader) {
50
+ loader.style.opacity = '0';
51
+ setTimeout(() => loader.remove(), 500);
52
+ }
53
+ });
54
+ </script>
55
  </body>
56
  </html>
src/App.jsx CHANGED
@@ -1,22 +1,30 @@
1
  import { Routes, Route, Navigate, useNavigate } from 'react-router-dom';
2
  import { useStore } from './store';
3
  import { Auth } from './components/Auth';
4
- import { BuyerDashboard } from './components/BuyerDashboard';
5
- import { SellerDashboard } from './components/SellerDashboard';
6
- import { AddListing } from './components/AddListing';
7
- import { ListingDetails } from './components/ListingDetails';
8
- import { CallOverlay } from './components/CallOverlay';
9
- import { HelpOverlay } from './components/HelpOverlay';
10
- import { ChatOverlay } from './components/ChatOverlay';
11
- import { InboxOverlay } from './components/InboxOverlay';
12
- import { AdminLogin } from './components/admin/AdminLogin';
13
- import { AdminDashboard } from './components/admin/AdminDashboard';
14
  import { ProtectedRoute } from './components/ProtectedRoute';
15
- import { LogOut, Home, KeyRound, Heart, X } from 'lucide-react';
16
- import { useEffect, useState } from 'react';
17
  import { socket } from './socket';
18
  import { useLocation } from 'react-router-dom';
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  export default function App() {
21
  const { currentUser, initDevice, logout } = useStore();
22
  const [showQR, setShowQR] = useState(false);
@@ -25,18 +33,16 @@ export default function App() {
25
  const isAdminRoute = location.pathname.startsWith('/admin');
26
 
27
  useEffect(() => {
28
- // Request native OS notification permissions
29
  if ("Notification" in window && Notification.permission !== "granted" && Notification.permission !== "denied") {
30
  Notification.requestPermission();
31
  }
32
 
33
- const id = initDevice();
34
- if (id) {
35
  socket.connect();
36
- socket.emit('join-network', id);
37
  }
38
 
39
- // Listen for global listing updates (e.g., marked sold)
40
  socket.on('listing-updated', (data) => {
41
  useStore.getState().updateListingStatus(data.id, data.status);
42
  });
@@ -52,7 +58,7 @@ export default function App() {
52
  if ("Notification" in window && Notification.permission === "granted") {
53
  new Notification("New Message - Meri Mandi", {
54
  body: data.message,
55
- icon: '/upiqr.jpeg' // Generic icon for app push
56
  });
57
  }
58
  }
@@ -79,111 +85,111 @@ export default function App() {
79
 
80
  return (
81
  <div className={isAdminRoute ? "admin-container" : "app-container"}>
82
- {/* Main Content Area */}
83
- {isAdminRoute ? (
84
- <Routes>
85
- <Route path="/admin" element={<AdminLogin />} />
86
- <Route path="/admin/dashboard" element={<AdminDashboard />} />
87
- </Routes>
88
- ) : (
89
- <>
90
- <header style={{
91
- padding: '20px',
92
- display: 'flex',
93
- justifyContent: 'space-between',
94
- alignItems: 'center',
95
- borderBottom: '1px solid var(--border-color)',
96
- backdropFilter: 'blur(10px)',
97
- position: 'sticky',
98
- top: 0,
99
- zIndex: 10
100
- }}>
101
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
102
- <h2 className="text-gradient" onClick={() => navigate('/')} style={{ cursor: 'pointer', margin: 0 }}>🌾 Meri Mandi</h2>
103
- <button
104
- onClick={() => setShowQR(true)}
105
- style={{ background: 'linear-gradient(135deg, #ec4899, #be185d)', color: 'white', border: 'none', borderRadius: '12px', padding: '4px 10px', fontSize: '11px', display: 'flex', alignItems: 'center', gap: '4px', cursor: 'pointer', fontWeight: 600, marginLeft: '8px' }}
106
- >
107
- <Heart size={12} fill="white" /> Support App
108
- </button>
109
- </div>
110
- {currentUser && (
111
- <div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
112
- <span style={{ fontSize: '14px', color: 'var(--text-muted)', textTransform: 'capitalize' }}>
113
- {currentUser.name} ({currentUser.role})
114
- </span>
115
  <button
116
- onClick={handleSwitchProfile}
117
- className="btn-secondary"
118
- style={{ fontSize: '12px', padding: '6px 12px', display: 'flex', alignItems: 'center', gap: '6px' }}
119
  >
120
- <KeyRound size={14} /> Switch
121
  </button>
122
  </div>
123
- )}
124
- </header>
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
- <main style={{ flex: 1, padding: '20px', overflowY: 'auto' }}>
127
- <Routes>
128
- <Route path="/" element={
129
- !currentUser ? <Auth /> : (
130
- currentUser.role === 'buyer' ? <Navigate to="/buyer" /> : <Navigate to="/seller" />
131
- )
132
- } />
133
-
134
- <Route path="/buyer" element={
135
- <ProtectedRoute requiredRole="buyer">
136
- <BuyerDashboard />
137
- </ProtectedRoute>
138
- } />
139
- <Route path="/buyer/listing/:id" element={
140
- <ProtectedRoute requiredRole="buyer">
141
- <ListingDetails />
142
- </ProtectedRoute>
143
- } />
144
-
145
- <Route path="/seller" element={
146
- <ProtectedRoute requiredRole="seller">
147
- <SellerDashboard />
148
- </ProtectedRoute>
149
- } />
150
- <Route path="/seller/add" element={
151
- <ProtectedRoute requiredRole="seller">
152
- <AddListing />
153
- </ProtectedRoute>
154
- } />
155
- </Routes>
156
- </main>
157
 
158
- {/* Floating Global Overlays */}
159
- <CallOverlay />
160
- <HelpOverlay />
161
-
162
- {showQR && (
163
- <div style={{
164
- position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, zIndex: 9999,
165
- background: 'rgba(0,0,0,0.8)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '20px'
166
- }}>
167
- <div className="glass-panel" style={{ position: 'relative', width: '100%', maxWidth: '350px', padding: '30px', textAlign: 'center' }}>
168
- <button
169
- onClick={() => setShowQR(false)}
170
- style={{ position: 'absolute', top: '15px', right: '15px', background: 'var(--danger-glass)', border: 'none', color: 'var(--danger-color)', width: '30px', height: '30px', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}
171
- >
172
- <X size={16} />
173
- </button>
174
- <Heart size={40} color="#ec4899" fill="#ec4899" style={{ marginBottom: '16px' }} />
175
- <h3 style={{ fontSize: '20px', marginBottom: '8px' }}>Support Meri Mandi</h3>
176
- <p style={{ color: 'var(--text-muted)', fontSize: '14px', marginBottom: '24px' }}>Scan the QR code below through any UPI app to fund our servers and keep Meri Mandi free for farmers!</p>
177
- <div style={{ background: 'white', padding: '10px', borderRadius: '16px', display: 'inline-block' }}>
178
- <img src="/upiqr.jpeg" alt="UPI QR Code" style={{ width: '200px', height: '200px', objectFit: 'contain', display: 'block' }} />
179
  </div>
180
  </div>
181
- </div>
182
- )}
183
- <InboxOverlay />
184
- <ChatOverlay />
185
- </>
186
- )}
187
  </div>
188
  );
189
  }
 
1
  import { Routes, Route, Navigate, useNavigate } from 'react-router-dom';
2
  import { useStore } from './store';
3
  import { Auth } from './components/Auth';
 
 
 
 
 
 
 
 
 
 
4
  import { ProtectedRoute } from './components/ProtectedRoute';
5
+ import { KeyRound, Heart, X } from 'lucide-react';
6
+ import { useEffect, useState, lazy, Suspense } from 'react';
7
  import { socket } from './socket';
8
  import { useLocation } from 'react-router-dom';
9
 
10
+ // Lazy load heavy dashboard and overlay components
11
+ const BuyerDashboard = lazy(() => import('./components/BuyerDashboard').then(m => ({ default: m.BuyerDashboard })));
12
+ const SellerDashboard = lazy(() => import('./components/SellerDashboard').then(m => ({ default: m.SellerDashboard })));
13
+ const AddListing = lazy(() => import('./components/AddListing').then(m => ({ default: m.AddListing })));
14
+ const ListingDetails = lazy(() => import('./components/ListingDetails').then(m => ({ default: m.ListingDetails })));
15
+ const CallOverlay = lazy(() => import('./components/CallOverlay').then(m => ({ default: m.CallOverlay })));
16
+ const HelpOverlay = lazy(() => import('./components/HelpOverlay').then(m => ({ default: m.HelpOverlay })));
17
+ const ChatOverlay = lazy(() => import('./components/ChatOverlay').then(m => ({ default: m.ChatOverlay })));
18
+ const InboxOverlay = lazy(() => import('./components/InboxOverlay').then(m => ({ default: m.InboxOverlay })));
19
+ const AdminLogin = lazy(() => import('./components/admin/AdminLogin').then(m => ({ default: m.AdminLogin })));
20
+ const AdminDashboard = lazy(() => import('./components/admin/AdminDashboard').then(m => ({ default: m.AdminDashboard })));
21
+
22
+ const LoadingFallback = () => (
23
+ <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100px', color: 'var(--text-muted)' }}>
24
+ <div className="loading-dots">Loading...</div>
25
+ </div>
26
+ );
27
+
28
  export default function App() {
29
  const { currentUser, initDevice, logout } = useStore();
30
  const [showQR, setShowQR] = useState(false);
 
33
  const isAdminRoute = location.pathname.startsWith('/admin');
34
 
35
  useEffect(() => {
 
36
  if ("Notification" in window && Notification.permission !== "granted" && Notification.permission !== "denied") {
37
  Notification.requestPermission();
38
  }
39
 
40
+ const { deviceId } = initDevice();
41
+ if (deviceId) {
42
  socket.connect();
43
+ socket.emit('join-network', deviceId);
44
  }
45
 
 
46
  socket.on('listing-updated', (data) => {
47
  useStore.getState().updateListingStatus(data.id, data.status);
48
  });
 
58
  if ("Notification" in window && Notification.permission === "granted") {
59
  new Notification("New Message - Meri Mandi", {
60
  body: data.message,
61
+ icon: '/upiqr.jpeg'
62
  });
63
  }
64
  }
 
85
 
86
  return (
87
  <div className={isAdminRoute ? "admin-container" : "app-container"}>
88
+ <Suspense fallback={<LoadingFallback />}>
89
+ {isAdminRoute ? (
90
+ <Routes>
91
+ <Route path="/admin" element={<AdminLogin />} />
92
+ <Route path="/admin/dashboard" element={<AdminDashboard />} />
93
+ </Routes>
94
+ ) : (
95
+ <>
96
+ <header style={{
97
+ padding: '15px 20px',
98
+ display: 'flex',
99
+ justifyContent: 'space-between',
100
+ alignItems: 'center',
101
+ borderBottom: '1px solid var(--border-color)',
102
+ backdropFilter: 'blur(10px)',
103
+ position: 'sticky',
104
+ top: 0,
105
+ zIndex: 10
106
+ }}>
107
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
108
+ <h2 className="text-gradient" onClick={() => navigate('/')} style={{ cursor: 'pointer', margin: 0, fontSize: '20px' }}>🌾 Meri Mandi</h2>
 
 
 
 
 
 
 
 
 
 
 
 
109
  <button
110
+ onClick={() => setShowQR(true)}
111
+ style={{ background: 'linear-gradient(135deg, #ec4899, #be185d)', color: 'white', border: 'none', borderRadius: '12px', padding: '4px 10px', fontSize: '11px', display: 'flex', alignItems: 'center', gap: '4px', cursor: 'pointer', fontWeight: 600, marginLeft: '8px' }}
 
112
  >
113
+ <Heart size={12} fill="white" /> Support
114
  </button>
115
  </div>
116
+ {currentUser && (
117
+ <div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
118
+ <span style={{ fontSize: '12px', color: 'var(--text-muted)', textTransform: 'capitalize', display: 'none', '@media (min-width: 480px)': { display: 'inline' } }}>
119
+ {currentUser.name}
120
+ </span>
121
+ <button
122
+ onClick={handleSwitchProfile}
123
+ className="btn-secondary"
124
+ style={{ fontSize: '11px', padding: '5px 10px', display: 'flex', alignItems: 'center', gap: '6px' }}
125
+ >
126
+ <KeyRound size={13} /> Switch
127
+ </button>
128
+ </div>
129
+ )}
130
+ </header>
131
 
132
+ <main style={{ flex: 1, padding: '15px', overflowY: 'auto' }}>
133
+ <Routes>
134
+ <Route path="/" element={
135
+ !currentUser ? <Auth /> : (
136
+ currentUser.role === 'buyer' ? <Navigate to="/buyer" /> : <Navigate to="/seller" />
137
+ )
138
+ } />
139
+
140
+ <Route path="/buyer" element={
141
+ <ProtectedRoute requiredRole="buyer">
142
+ <BuyerDashboard />
143
+ </ProtectedRoute>
144
+ } />
145
+ <Route path="/buyer/listing/:id" element={
146
+ <ProtectedRoute requiredRole="buyer">
147
+ <ListingDetails />
148
+ </ProtectedRoute>
149
+ } />
150
+
151
+ <Route path="/seller" element={
152
+ <ProtectedRoute requiredRole="seller">
153
+ <SellerDashboard />
154
+ </ProtectedRoute>
155
+ } />
156
+ <Route path="/seller/add" element={
157
+ <ProtectedRoute requiredRole="seller">
158
+ <AddListing />
159
+ </ProtectedRoute>
160
+ } />
161
+ </Routes>
162
+ </main>
163
 
164
+ <CallOverlay />
165
+ <HelpOverlay />
166
+
167
+ {showQR && (
168
+ <div style={{
169
+ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, zIndex: 9999,
170
+ background: 'rgba(0,0,0,0.8)', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '20px'
171
+ }}>
172
+ <div className="glass-panel" style={{ position: 'relative', width: '100%', maxWidth: '350px', padding: '30px', textAlign: 'center' }}>
173
+ <button
174
+ onClick={() => setShowQR(false)}
175
+ style={{ position: 'absolute', top: '15px', right: '15px', background: 'var(--danger-glass)', border: 'none', color: 'var(--danger-color)', width: '30px', height: '30px', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}
176
+ >
177
+ <X size={16} />
178
+ </button>
179
+ <Heart size={40} color="#ec4899" fill="#ec4899" style={{ marginBottom: '16px' }} />
180
+ <h3 style={{ fontSize: '20px', marginBottom: '8px' }}>Support Meri Mandi</h3>
181
+ <p style={{ color: 'var(--text-muted)', fontSize: '14px', marginBottom: '24px' }}>Scan to keep Meri Mandi free for farmers!</p>
182
+ <div style={{ background: 'white', padding: '10px', borderRadius: '16px', display: 'inline-block' }}>
183
+ <img src="/upiqr.jpeg" alt="UPI QR Code" style={{ width: '200px', height: '200px', objectFit: 'contain', display: 'block' }} />
184
+ </div>
185
  </div>
186
  </div>
187
+ )}
188
+ <InboxOverlay />
189
+ <ChatOverlay />
190
+ </>
191
+ )}
192
+ </Suspense>
193
  </div>
194
  );
195
  }
src/components/BuyerDashboard.jsx CHANGED
@@ -115,6 +115,7 @@ export function BuyerDashboard() {
115
  <img
116
  src={listing.images[0]}
117
  alt={listing.cropName}
 
118
  style={{ width: '100%', height: '100%', objectFit: 'cover' }}
119
  />
120
  </div>
 
115
  <img
116
  src={listing.images[0]}
117
  alt={listing.cropName}
118
+ loading="lazy"
119
  style={{ width: '100%', height: '100%', objectFit: 'cover' }}
120
  />
121
  </div>
src/components/ListingDetails.jsx CHANGED
@@ -44,7 +44,7 @@ export function ListingDetails() {
44
  <div style={{ display: 'flex', overflowX: 'auto', gap: '16px', scrollSnapType: 'x mandatory', marginBottom: '24px' }}>
45
  {listing.images.map((img, idx) => (
46
  <div key={idx} style={{ minWidth: '100%', scrollSnapAlign: 'center', borderRadius: '16px', overflow: 'hidden', aspectRatio: '4/3' }}>
47
- <img src={img} alt={`Crop ${idx}`} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
48
  </div>
49
  ))}
50
  </div>
@@ -88,7 +88,7 @@ export function ListingDetails() {
88
  <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
89
  <div style={{ width: '50px', height: '50px', borderRadius: '50%', background: 'var(--surface-light)', display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }}>
90
  {listing.sellerSelfie ? (
91
- <img src={listing.sellerSelfie} alt={listing.sellerName} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
92
  ) : (
93
  <span style={{ fontSize: '20px', fontWeight: 600 }}>{listing.sellerName.charAt(0)}</span>
94
  )}
 
44
  <div style={{ display: 'flex', overflowX: 'auto', gap: '16px', scrollSnapType: 'x mandatory', marginBottom: '24px' }}>
45
  {listing.images.map((img, idx) => (
46
  <div key={idx} style={{ minWidth: '100%', scrollSnapAlign: 'center', borderRadius: '16px', overflow: 'hidden', aspectRatio: '4/3' }}>
47
+ <img src={img} alt={`Crop ${idx}`} loading="lazy" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
48
  </div>
49
  ))}
50
  </div>
 
88
  <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
89
  <div style={{ width: '50px', height: '50px', borderRadius: '50%', background: 'var(--surface-light)', display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'hidden' }}>
90
  {listing.sellerSelfie ? (
91
+ <img src={listing.sellerSelfie} alt={listing.sellerName} loading="lazy" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
92
  ) : (
93
  <span style={{ fontSize: '20px', fontWeight: 600 }}>{listing.sellerName.charAt(0)}</span>
94
  )}
src/index.css CHANGED
@@ -190,3 +190,23 @@ h1, h2, h3, h4, h5, h6 {
190
  padding: 20px;
191
  background: linear-gradient(to top, var(--surface-color) 70%, transparent);
192
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  padding: 20px;
191
  background: linear-gradient(to top, var(--surface-color) 70%, transparent);
192
  }
193
+
194
+ /* Skeleton & Pulse */
195
+ .animate-pulse {
196
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
197
+ }
198
+ @keyframes pulse {
199
+ 0%, 100% { opacity: 1; }
200
+ 50% { opacity: .5; }
201
+ }
202
+
203
+ .loading-dots:after {
204
+ content: ' .';
205
+ animation: dots 1.5s steps(5, end) infinite;
206
+ }
207
+ @keyframes dots {
208
+ 0%, 20% { content: ' .'; }
209
+ 40% { content: ' ..'; }
210
+ 60% { content: ' ...'; }
211
+ 80%, 100% { content: ''; }
212
+ }
vite.config.js CHANGED
@@ -6,6 +6,16 @@ export default defineConfig({
6
  build: {
7
  outDir: 'dist',
8
  emptyOutDir: true,
 
 
 
 
 
 
 
 
 
 
9
  },
10
  server: {
11
  proxy: {
 
6
  build: {
7
  outDir: 'dist',
8
  emptyOutDir: true,
9
+ minify: 'esbuild',
10
+ rollupOptions: {
11
+ output: {
12
+ manualChunks: {
13
+ 'vendor-react': ['react', 'react-dom', 'react-router-dom'],
14
+ 'vendor-ui': ['framer-motion', 'lucide-react'],
15
+ 'vendor-utils': ['socket.io-client', 'zustand', 'uuid'],
16
+ },
17
+ },
18
+ },
19
  },
20
  server: {
21
  proxy: {