demeter

Autonomous Hydroponic Intelligence

frontend/src/components/Sidebar.jsx (10853B)


  1import React, { useState, useMemo } from "react";
  2import { Link, useLocation } from "react-router-dom";
  3import {
  4  Leaf,
  5  LayoutGrid,
  6  BarChart3,
  7  Bell,
  8  Settings,
  9  Sparkles,
 10  HelpCircle,
 11  ChevronLeft,
 12  ChevronRight,
 13  Shield,
 14} from "lucide-react";
 15import { useFarmData } from "../hooks/useFarmData";
 16import { deriveCropStatus, isReadyToHarvest } from "../utils/dataUtils";
 17import { useSettings } from "../hooks/useSettings";
 18import { useT } from "../hooks/useTranslation";
 19
 20export default function Sidebar() {
 21  const [collapsed, setCollapsed] = useState(false);
 22  const loc = useLocation();
 23  const { dashboard } = useFarmData();
 24  const { settings } = useSettings();
 25  const { t } = useT();
 26
 27  const alertCount = useMemo(() => {
 28    if (!dashboard?.length) return 0;
 29    return dashboard.filter((d) => {
 30      const payload = d.payload || d;
 31      const status = deriveCropStatus(payload);
 32      return status === "Critical" || status === "Attention";
 33    }).length;
 34  }, [dashboard]);
 35
 36  const harvestCount = useMemo(() => {
 37    if (!dashboard?.length) return 0;
 38    return dashboard.filter((d) => {
 39      const payload = d.payload || d;
 40      return isReadyToHarvest(payload);
 41    }).length;
 42  }, [dashboard]);
 43
 44  const totalBadge = alertCount + harvestCount;
 45
 46  const NAV = [
 47    { labelKey: "nav_crops", icon: LayoutGrid, path: "/dashboard" },
 48    {
 49      labelKey: "nav_alerts",
 50      icon: Bell,
 51      path: "/alerts",
 52      badge: totalBadge || null,
 53      harvest: harvestCount,
 54    },
 55    { labelKey: "nav_analytics", icon: BarChart3, path: "/analytics" },
 56    { labelKey: "nav_intelligence", icon: Sparkles, path: "/intelligence" },
 57    { labelKey: "nav_help", icon: HelpCircle, path: "/help" },
 58    { labelKey: "nav_settings", icon: Settings, path: "/settings" },
 59  ];
 60
 61  const initials =
 62    settings.userInitials ||
 63    (settings.userName || "?")
 64      .split(" ")
 65      .map((w) => w[0])
 66      .join("")
 67      .toUpperCase()
 68      .slice(0, 2);
 69
 70  return (
 71    <aside
 72      style={{
 73        width: collapsed ? 64 : 220,
 74        background: "var(--bg-2)",
 75        borderRight: "1px solid var(--border)",
 76        flexShrink: 0,
 77        display: "flex",
 78        flexDirection: "column",
 79        position: "relative",
 80        transition: "width 300ms ease",
 81        overflow: "visible",
 82      }}
 83    >
 84      {/* Logo */}
 85      <div
 86        style={{
 87          display: "flex",
 88          alignItems: "center",
 89          gap: 12,
 90          padding: collapsed ? "20px 0" : "20px 16px",
 91          justifyContent: collapsed ? "center" : "flex-start",
 92          borderBottom: "1px solid var(--border)",
 93          height: 64,
 94          boxSizing: "border-box",
 95        }}
 96      >
 97        <div
 98          style={{
 99            width: 32,
100            height: 32,
101            borderRadius: 10,
102            flexShrink: 0,
103            background: "linear-gradient(135deg, #2d7a44, #4ade80)",
104            display: "flex",
105            alignItems: "center",
106            justifyContent: "center",
107          }}
108        >
109          <Leaf size={15} fill="white" color="white" />
110        </div>
111        {!collapsed && (
112          <div style={{ overflow: "hidden" }}>
113            <div
114              style={{
115                fontWeight: 700,
116                fontSize: 15,
117                color: "var(--text)",
118                lineHeight: 1.2,
119              }}
120            >
121              Demeter
122            </div>
123            <div
124              style={{
125                fontSize: 9,
126                fontFamily: "DM Mono, monospace",
127                color: "var(--text-3)",
128                letterSpacing: "0.1em",
129              }}
130            >
131              {t("sidebar_agri_ai")}
132            </div>
133          </div>
134        )}
135      </div>
136
137      {/* Collapse toggle */}
138      <button
139        onClick={() => setCollapsed(!collapsed)}
140        style={{
141          position: "absolute",
142          top: 20,
143          right: -12,
144          width: 24,
145          height: 24,
146          borderRadius: "50%",
147          background: "var(--bg-3)",
148          border: "1px solid var(--border)",
149          color: "var(--text-3)",
150          display: "flex",
151          alignItems: "center",
152          justifyContent: "center",
153          cursor: "pointer",
154          zIndex: 10,
155        }}
156      >
157        {collapsed ? <ChevronRight size={11} /> : <ChevronLeft size={11} />}
158      </button>
159
160      {/* System status pill */}
161      {!collapsed && (
162        <div style={{ padding: "12px 12px 0" }}>
163          <div
164            style={{
165              display: "flex",
166              alignItems: "center",
167              gap: 6,
168              padding: "6px 10px",
169              borderRadius: 8,
170              background: "rgba(74,222,128,0.08)",
171              border: "1px solid rgba(74,222,128,0.2)",
172            }}
173          >
174            <span
175              className="status-dot"
176              style={{
177                width: 6,
178                height: 6,
179                borderRadius: "50%",
180                background: "var(--green)",
181                flexShrink: 0,
182              }}
183            />
184            <span
185              style={{
186                fontSize: 10,
187                fontFamily: "DM Mono, monospace",
188                color: "var(--green)",
189              }}
190            >
191              {t("nav_system_online")}
192            </span>
193          </div>
194        </div>
195      )}
196
197      {/* Nav */}
198      <nav
199        style={{
200          flex: 1,
201          padding: "12px 8px",
202          display: "flex",
203          flexDirection: "column",
204          gap: 2,
205        }}
206      >
207        {NAV.map(({ labelKey, icon: Icon, path, badge, harvest }) => {
208          const active = loc.pathname === path;
209          const label = t(labelKey);
210          return (
211            <Link
212              key={labelKey}
213              to={path}
214              title={collapsed ? label : undefined}
215              style={{
216                display: "flex",
217                alignItems: "center",
218                gap: 10,
219                padding: collapsed ? "10px 0" : "10px 12px",
220                justifyContent: collapsed ? "center" : "flex-start",
221                borderRadius: 10,
222                background: active ? "rgba(74,222,128,0.12)" : "transparent",
223                color: active ? "var(--green)" : "var(--text-2)",
224                border: active
225                  ? "1px solid rgba(74,222,128,0.25)"
226                  : "1px solid transparent",
227                textDecoration: "none",
228                position: "relative",
229                transition: "all 0.15s",
230              }}
231            >
232              <Icon size={17} style={{ flexShrink: 0 }} />
233              {!collapsed && (
234                <span style={{ fontSize: 13, fontWeight: 500 }}>{label}</span>
235              )}
236
237              {/* Badge (expanded) */}
238              {badge && !collapsed && (
239                <div
240                  style={{
241                    marginLeft: "auto",
242                    display: "flex",
243                    alignItems: "center",
244                    gap: 4,
245                  }}
246                >
247                  {alertCount > 0 && (
248                    <span
249                      className="alert-pulse"
250                      style={{
251                        fontSize: 10,
252                        fontFamily: "DM Mono, monospace",
253                        padding: "1px 5px",
254                        borderRadius: 4,
255                        background: "rgba(248,113,113,0.2)",
256                        color: "var(--red)",
257                      }}
258                    >
259                      {alertCount}
260                    </span>
261                  )}
262                  {harvest > 0 && (
263                    <span
264                      className="harvest-pulse"
265                      style={{
266                        fontSize: 10,
267                        fontFamily: "DM Mono, monospace",
268                        padding: "1px 5px",
269                        borderRadius: 4,
270                        background: "rgba(245,158,11,0.2)",
271                        color: "var(--amber)",
272                      }}
273                    >
274                      🌾{harvest}
275                    </span>
276                  )}
277                </div>
278              )}
279
280              {badge && collapsed && (
281                <span
282                  className="alert-pulse"
283                  style={{
284                    position: "absolute",
285                    top: 6,
286                    right: 6,
287                    width: 8,
288                    height: 8,
289                    borderRadius: "50%",
290                    background: "var(--red)",
291                  }}
292                />
293              )}
294            </Link>
295          );
296        })}
297      </nav>
298
299      {/* User avatar */}
300      <div
301        style={{
302          borderTop: "1px solid var(--border)",
303          padding: collapsed ? "12px 0" : "12px",
304          display: "flex",
305          alignItems: "center",
306          gap: 10,
307          justifyContent: collapsed ? "center" : "flex-start",
308        }}
309      >
310        {/* Avatar */}
311        <div
312          style={{
313            width: 34,
314            height: 34,
315            borderRadius: "50%",
316            background: "linear-gradient(135deg, #2d7a44, #4ade80)",
317            display: "flex",
318            alignItems: "center",
319            justifyContent: "center",
320            flexShrink: 0,
321            fontSize: 12,
322            fontWeight: 700,
323            color: "#fff",
324            letterSpacing: "0.05em",
325            boxShadow: "0 2px 6px rgba(74,222,128,0.3)",
326          }}
327        >
328          {initials}
329        </div>
330
331        {!collapsed && (
332          <div
333            style={{
334              minWidth: 0,
335              flex: 1,
336              animation: "fadeSlideIn 200ms ease",
337            }}
338          >
339            <div
340              style={{
341                fontSize: 13,
342                fontWeight: 600,
343                color: "var(--text)",
344                whiteSpace: "nowrap",
345                overflow: "hidden",
346                textOverflow: "ellipsis",
347              }}
348            >
349              {settings.userName}
350            </div>
351            <div
352              style={{
353                display: "flex",
354                alignItems: "center",
355                gap: 4,
356                marginTop: 1,
357              }}
358            >
359              <Shield
360                size={9}
361                style={{ color: "var(--green)", flexShrink: 0 }}
362              />
363              <span
364                style={{
365                  fontSize: 10,
366                  fontFamily: "DM Mono, monospace",
367                  color: "var(--green)",
368                  letterSpacing: "0.04em",
369                }}
370              >
371                {settings.userDesignation}
372              </span>
373            </div>
374          </div>
375        )}
376      </div>
377    </aside>
378  );
379}