demeter

Autonomous Hydroponic Intelligence

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}