demeter
Autonomous Hydroponic Intelligencegit clone https://github.com/maydayv7/demeter
frontend/src/pages/Settings.jsx (17664B)
1import { useState } from "react";
2import {
3 User,
4 Sun,
5 Moon,
6 Monitor,
7 Save,
8 RotateCcw,
9 Check,
10 Database,
11 Bell,
12 LayoutGrid,
13 Zap,
14 Globe,
15 HelpCircle,
16} from "lucide-react";
17import { PageShell, PageHeader, SectionCard } from "../components/ui";
18import { useSettings } from "../hooks/useSettings";
19import { useT } from "../hooks/useTranslation";
20import { USE_MOCK_DATA } from "../data/mockData";
21import Onboarding from "../components/Onboarding";
22
23function SectionHeader({ icon: Icon, title, sub }) {
24 return (
25 <div
26 style={{
27 display: "flex",
28 alignItems: "center",
29 gap: 12,
30 marginBottom: 20,
31 }}
32 >
33 <div
34 style={{
35 width: 36,
36 height: 36,
37 borderRadius: 10,
38 background: "rgba(74,222,128,0.1)",
39 border: "1px solid rgba(74,222,128,0.2)",
40 display: "flex",
41 alignItems: "center",
42 justifyContent: "center",
43 flexShrink: 0,
44 }}
45 >
46 <Icon size={16} style={{ color: "var(--green)" }} />
47 </div>
48 <div>
49 <div style={{ fontWeight: 700, fontSize: 15, color: "var(--text)" }}>
50 {title}
51 </div>
52 {sub && (
53 <div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 2 }}>
54 {sub}
55 </div>
56 )}
57 </div>
58 </div>
59 );
60}
61
62function FieldRow({ label, hint, children }) {
63 return (
64 <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
65 <label style={{ fontSize: 13, fontWeight: 600, color: "var(--text-2)" }}>
66 {label}
67 </label>
68 {hint && (
69 <div style={{ fontSize: 11, color: "var(--text-3)", lineHeight: 1.5 }}>
70 {hint}
71 </div>
72 )}
73 {children}
74 </div>
75 );
76}
77
78const inputStyle = {
79 width: "100%",
80 padding: "10px 14px",
81 borderRadius: 10,
82 fontSize: 14,
83 fontFamily: "DM Mono, monospace",
84 background: "var(--bg-3)",
85 border: "1px solid var(--border)",
86 color: "var(--text)",
87 outline: "none",
88 boxSizing: "border-box",
89};
90
91function Toggle({ value, onChange, enabledLabel, disabledLabel }) {
92 return (
93 <label
94 style={{
95 display: "flex",
96 alignItems: "center",
97 gap: 10,
98 cursor: "pointer",
99 marginTop: 6,
100 }}
101 >
102 <div
103 onClick={() => onChange(!value)}
104 style={{
105 width: 44,
106 height: 24,
107 borderRadius: 12,
108 background: value ? "var(--green)" : "var(--border)",
109 position: "relative",
110 cursor: "pointer",
111 transition: "background 0.2s",
112 flexShrink: 0,
113 }}
114 >
115 <div
116 style={{
117 position: "absolute",
118 top: 3,
119 left: value ? 23 : 3,
120 width: 18,
121 height: 18,
122 borderRadius: "50%",
123 background: "white",
124 transition: "left 0.2s",
125 boxShadow: "0 1px 4px rgba(0,0,0,0.3)",
126 }}
127 />
128 </div>
129 <span style={{ fontSize: 13, color: "var(--text-2)" }}>
130 {value ? enabledLabel : disabledLabel}
131 </span>
132 </label>
133 );
134}
135
136export default function SettingsPage() {
137 const { settings, update, reset } = useSettings();
138 const { t } = useT();
139 const [saved, setSaved] = useState(false);
140 const [showOnboarding, setShowOnboarding] = useState(false);
141
142 const [draft, setDraft] = useState({ ...settings });
143 const set = (key, val) => setDraft((d) => ({ ...d, [key]: val }));
144
145 const handleSave = () => {
146 Object.entries(draft).forEach(([k, v]) => update(k, v));
147 setSaved(true);
148 setTimeout(() => setSaved(false), 2000);
149 };
150
151 const handleReset = () => {
152 reset();
153 setDraft({ ...settings });
154 };
155
156 const ThemeButton = ({ value, label, Icon }) => (
157 <button
158 onClick={() => set("theme", value)}
159 style={{
160 flex: 1,
161 padding: "12px 0",
162 borderRadius: 12,
163 cursor: "pointer",
164 display: "flex",
165 flexDirection: "column",
166 alignItems: "center",
167 gap: 6,
168 background:
169 draft.theme === value ? "rgba(74,222,128,0.12)" : "var(--bg-3)",
170 border: `2px solid ${draft.theme === value ? "var(--green)" : "var(--border)"}`,
171 color: draft.theme === value ? "var(--green)" : "var(--text-3)",
172 transition: "all 0.15s",
173 }}
174 >
175 <Icon size={18} />
176 <span style={{ fontSize: 12, fontWeight: 600 }}>{label}</span>
177 </button>
178 );
179
180 const LangButton = ({ value, label }) => (
181 <button
182 onClick={() => set("language", value)}
183 style={{
184 flex: 1,
185 padding: "14px 12px",
186 borderRadius: 12,
187 cursor: "pointer",
188 display: "flex",
189 flexDirection: "column",
190 alignItems: "center",
191 gap: 4,
192 background:
193 draft.language === value ? "rgba(74,222,128,0.12)" : "var(--bg-3)",
194 border: `2px solid ${draft.language === value ? "var(--green)" : "var(--border)"}`,
195 color: draft.language === value ? "var(--green)" : "var(--text-3)",
196 transition: "all 0.15s",
197 }}
198 >
199 <Globe size={18} />
200 <span style={{ fontSize: 13, fontWeight: 600 }}>{label}</span>
201 <span
202 style={{
203 fontSize: 10,
204 fontFamily: "DM Mono, monospace",
205 opacity: 0.6,
206 }}
207 >
208 {value.toUpperCase()}
209 </span>
210 </button>
211 );
212
213 return (
214 <PageShell>
215 {showOnboarding && (
216 <Onboarding
217 onDone={() => {
218 update("onboardingDone", true);
219 setShowOnboarding(false);
220 }}
221 />
222 )}
223
224 <main
225 style={{
226 flex: 1,
227 display: "flex",
228 flexDirection: "column",
229 overflow: "hidden",
230 }}
231 >
232 {/* Header */}
233 <PageHeader
234 title={t("settings_title")}
235 subtitle={t("settings_subtitle")}
236 >
237 <div style={{ display: "flex", gap: 10, marginLeft: "auto" }}>
238 <button
239 onClick={handleReset}
240 style={{
241 display: "flex",
242 alignItems: "center",
243 gap: 6,
244 padding: "8px 16px",
245 borderRadius: 10,
246 fontSize: 13,
247 background: "var(--surface)",
248 border: "1px solid var(--border)",
249 color: "var(--text-3)",
250 cursor: "pointer",
251 }}
252 >
253 <RotateCcw size={13} /> {t("settings_reset")}
254 </button>
255 <button
256 onClick={handleSave}
257 style={{
258 display: "flex",
259 alignItems: "center",
260 gap: 6,
261 padding: "8px 20px",
262 borderRadius: 10,
263 fontSize: 13,
264 fontWeight: 600,
265 background: saved ? "rgba(74,222,128,0.2)" : "var(--green)",
266 border: saved ? "1px solid var(--green)" : "none",
267 color: saved ? "var(--green)" : "var(--btn-on-green)",
268 cursor: "pointer",
269 transition: "all 0.2s",
270 }}
271 >
272 {saved ? (
273 <>
274 <Check size={13} /> {t("settings_saved")}
275 </>
276 ) : (
277 <>
278 <Save size={13} /> {t("settings_save")}
279 </>
280 )}
281 </button>
282 </div>
283 </PageHeader>
284
285 <div style={{ flex: 1, overflowY: "auto", padding: 28 }}>
286 <div
287 style={{
288 display: "flex",
289 flexDirection: "column",
290 gap: 24,
291 }}
292 >
293 {/* Profile */}
294 <SectionCard>
295 <SectionHeader
296 icon={User}
297 title={t("settings_profile")}
298 sub={t("settings_profile_sub")}
299 />
300 <div
301 style={{
302 display: "grid",
303 gridTemplateColumns: "1fr 1fr",
304 gap: 16,
305 }}
306 >
307 <FieldRow label={t("settings_display_name")}>
308 <input
309 style={inputStyle}
310 value={draft.userName}
311 onChange={(e) => set("userName", e.target.value)}
312 placeholder="Your name"
313 />
314 </FieldRow>
315 <FieldRow label={t("settings_designation")}>
316 <input
317 style={inputStyle}
318 value={draft.userDesignation}
319 onChange={(e) => set("userDesignation", e.target.value)}
320 placeholder="Eg. Farm Owner"
321 />
322 </FieldRow>
323 <FieldRow
324 label={t("settings_initials")}
325 hint={t("settings_initials_hint")}
326 >
327 <input
328 style={{ ...inputStyle, maxWidth: 100 }}
329 value={draft.userInitials}
330 onChange={(e) =>
331 set(
332 "userInitials",
333 e.target.value.toUpperCase().slice(0, 2),
334 )
335 }
336 placeholder="RR"
337 maxLength={2}
338 />
339 </FieldRow>
340 </div>
341 </SectionCard>
342
343 {/* Appearance */}
344 <SectionCard>
345 <SectionHeader
346 icon={Sun}
347 title={t("settings_appearance")}
348 sub={t("settings_appearance_sub")}
349 />
350 <FieldRow
351 label={t("settings_theme")}
352 hint={t("settings_theme_hint")}
353 >
354 <div style={{ display: "flex", gap: 10, marginTop: 4 }}>
355 <ThemeButton
356 value="dark"
357 label={t("settings_dark")}
358 Icon={Moon}
359 />
360 <ThemeButton
361 value="light"
362 label={t("settings_light")}
363 Icon={Sun}
364 />
365 <ThemeButton
366 value="auto"
367 label={t("settings_system")}
368 Icon={Monitor}
369 />
370 </div>
371 </FieldRow>
372
373 <div style={{ marginTop: 20 }}>
374 <FieldRow
375 label={t("settings_compact")}
376 hint={t("settings_compact_hint")}
377 >
378 <Toggle
379 value={draft.compactMode}
380 onChange={(v) => set("compactMode", v)}
381 enabledLabel={t("common_enabled")}
382 disabledLabel={t("common_disabled")}
383 />
384 </FieldRow>
385 </div>
386 </SectionCard>
387
388 {/* Language */}
389 <SectionCard>
390 <SectionHeader
391 icon={Globe}
392 title={t("settings_language")}
393 sub={t("settings_language_sub")}
394 />
395 <div style={{ display: "flex", gap: 12 }}>
396 <LangButton value="en" label={t("settings_lang_en")} />
397 <LangButton value="hi" label={t("settings_lang_hi")} />
398 </div>
399 {draft.language === "hi" && (
400 <div
401 style={{
402 marginTop: 12,
403 padding: "10px 14px",
404 borderRadius: 10,
405 background: "rgba(74,222,128,0.07)",
406 border: "1px solid rgba(74,222,128,0.2)",
407 fontSize: 12,
408 color: "var(--text-2)",
409 fontFamily: "Noto Sans Devanagari, sans-serif",
410 }}
411 >
412 हिंदी भाषा चुनी गई है। सहेजने के बाद पूरा ऐप हिंदी में दिखेगा।
413 </div>
414 )}
415 </SectionCard>
416
417 {/* Help & Onboarding */}
418 <SectionCard>
419 <SectionHeader
420 icon={HelpCircle}
421 title={t("settings_onboarding")}
422 sub={t("settings_onboarding_sub")}
423 />
424 <button
425 onClick={() => {
426 update("onboardingDone", false);
427 setShowOnboarding(true);
428 }}
429 style={{
430 display: "flex",
431 alignItems: "center",
432 gap: 8,
433 padding: "10px 20px",
434 borderRadius: 10,
435 fontSize: 13,
436 fontWeight: 600,
437 background: "rgba(74,222,128,0.1)",
438 border: "1px solid rgba(74,222,128,0.3)",
439 color: "var(--green)",
440 cursor: "pointer",
441 }}
442 >
443 <HelpCircle size={14} />
444 {t("settings_restart_onboarding")}
445 </button>
446 </SectionCard>
447
448 {/* Display */}
449 <SectionCard>
450 <SectionHeader
451 icon={LayoutGrid}
452 title={t("settings_display_section")}
453 sub={t("settings_display_sub")}
454 />
455 <div
456 style={{
457 display: "grid",
458 gridTemplateColumns: "1fr 1fr",
459 gap: 16,
460 }}
461 >
462 <FieldRow
463 label={t("settings_max_crops")}
464 hint={t("settings_max_crops_hint")}
465 >
466 <select
467 style={inputStyle}
468 value={draft.maxResultsPerPage}
469 onChange={(e) =>
470 set("maxResultsPerPage", parseInt(e.target.value))
471 }
472 >
473 {[6, 8, 12, 16, 24].map((n) => (
474 <option key={n} value={n}>
475 {n} {t("common_per_page")}
476 </option>
477 ))}
478 </select>
479 </FieldRow>
480 <FieldRow
481 label={t("settings_history_limit")}
482 hint={t("settings_history_hint")}
483 >
484 <select
485 style={inputStyle}
486 value={draft.historyLogLimit ?? 20}
487 onChange={(e) =>
488 set("historyLogLimit", parseInt(e.target.value))
489 }
490 >
491 {[10, 20, 50, 100].map((n) => (
492 <option key={n} value={n}>
493 {t("common_last")} {n} {t("common_entries")}
494 </option>
495 ))}
496 </select>
497 </FieldRow>
498 </div>
499 </SectionCard>
500
501 {/* Alerts */}
502 <SectionCard>
503 <SectionHeader
504 icon={Bell}
505 title={t("settings_alerts_section")}
506 sub={t("settings_alerts_sub")}
507 />
508 <FieldRow label={t("settings_show_acked")}>
509 <Toggle
510 value={draft.alertsShowAcked}
511 onChange={(v) => set("alertsShowAcked", v)}
512 enabledLabel={t("common_showing_all")}
513 disabledLabel={t("common_hiding_acked")}
514 />
515 </FieldRow>
516 </SectionCard>
517
518 {/* Data Source */}
519 <SectionCard>
520 <SectionHeader
521 icon={Database}
522 title={t("settings_data_source")}
523 sub={t("settings_data_sub")}
524 />
525 <div
526 style={{
527 padding: "14px 16px",
528 borderRadius: 12,
529 background: USE_MOCK_DATA
530 ? "rgba(245,158,11,0.08)"
531 : "rgba(74,222,128,0.08)",
532 border: `1px solid ${USE_MOCK_DATA ? "rgba(245,158,11,0.3)" : "rgba(74,222,128,0.3)"}`,
533 display: "flex",
534 alignItems: "center",
535 gap: 12,
536 }}
537 >
538 <Zap
539 size={16}
540 style={{
541 color: USE_MOCK_DATA ? "var(--amber)" : "var(--green)",
542 flexShrink: 0,
543 }}
544 />
545 <div>
546 <div
547 style={{
548 fontSize: 13,
549 fontWeight: 600,
550 color: "var(--text)",
551 }}
552 >
553 {USE_MOCK_DATA
554 ? t("settings_mock_mode")
555 : t("settings_live_mode")}
556 </div>
557 <div
558 style={{
559 fontSize: 12,
560 color: "var(--text-3)",
561 marginTop: 3,
562 }}
563 >
564 {USE_MOCK_DATA
565 ? t("settings_mock_hint")
566 : `Connected to ${process.env.REACT_APP_FARM_API_URL || "http://localhost:3001/api"}`}
567 </div>
568 </div>
569 </div>
570 </SectionCard>
571 </div>
572 </div>
573 </main>
574 </PageShell>
575 );
576}