demeter
Autonomous Hydroponic Intelligencegit clone https://github.com/maydayv7/demeter
frontend/src/pages/LandingPage.jsx (18658B)
1import React, { useEffect, useState } from "react";
2import { useNavigate } from "react-router-dom";
3import {
4 Leaf,
5 ArrowRight,
6 Cpu,
7 Database,
8 Eye,
9 Zap,
10 Activity,
11 Sparkles,
12} from "lucide-react";
13import { useFarmData } from "../hooks/useFarmData";
14import { extractSensors, deriveCropStatus } from "../utils/dataUtils";
15import { useT } from "../hooks/useTranslation";
16
17function computeFleetStats(dashData) {
18 if (!dashData?.length) return null;
19 const sensors = dashData.map((d) => extractSensors(d.payload));
20 const avg = (arr) =>
21 arr.reduce((s, v) => s + parseFloat(v || 0), 0) / arr.length;
22
23 const ph = avg(sensors.map((s) => s.ph));
24 const ec = avg(sensors.map((s) => s.ec));
25 const temp = avg(sensors.map((s) => s.temp));
26 const humidity = avg(sensors.map((s) => s.humidity));
27
28 const totalSeqs = dashData.reduce(
29 (s, d) => s + (d.payload?.sequence_number || 0),
30 0,
31 );
32 const cropTypes = [
33 ...new Set(dashData.map((d) => d.payload?.crop).filter(Boolean)),
34 ];
35
36 const alerts = dashData.filter((d) => {
37 const status = deriveCropStatus(d.payload);
38 return status === "Critical" || status === "Attention";
39 }).length;
40
41 return {
42 ph: ph.toFixed(2),
43 ec: ec.toFixed(2),
44 temp: temp.toFixed(1),
45 humidity: humidity.toFixed(1),
46 cropCount: dashData.length,
47 totalSeqs,
48 cropTypes,
49 alerts,
50 };
51}
52
53function buildActivityLog(historyData) {
54 if (!historyData?.length) return [];
55 return historyData
56 .slice(-5)
57 .reverse()
58 .map((d, i) => {
59 const p = d.payload || {};
60 const agent = (p.strategic_intent || "SUPERVISOR")
61 .replace(/_/g, " ")
62 .split(" ")[0];
63 const msg = p.strategic_intent
64 ? `Strategy: ${p.strategic_intent.replace(/_/g, " ")}`
65 : `Monitoring ${p.crop || "crop"} - seq #${p.sequence_number || 1}`;
66 const ts = p.timestamp
67 ? new Date(p.timestamp).toLocaleTimeString([], {
68 hour: "2-digit",
69 minute: "2-digit",
70 })
71 : `0${i}:0${i}`;
72 return { agent, msg, time: ts };
73 });
74}
75
76export default function LandingPage() {
77 const navigate = useNavigate();
78 const { t, td } = useT();
79 const [mounted, setMounted] = useState(false);
80 const { dashboard, history, loading } = useFarmData();
81
82 const [stats, setStats] = useState(null);
83 const [log, setLog] = useState([]);
84
85 useEffect(() => {
86 setMounted(true);
87 }, []);
88
89 useEffect(() => {
90 if (!loading) {
91 setStats(computeFleetStats(dashboard));
92 setLog(buildActivityLog(history));
93 }
94 }, [dashboard, history, loading]);
95
96 const FEATURES = [
97 {
98 icon: Cpu,
99 label: t("landing_feature_rl"),
100 desc: t("landing_feature_rl_desc"),
101 },
102 {
103 icon: Eye,
104 label: t("landing_feature_cv"),
105 desc: t("landing_feature_cv_desc"),
106 },
107 {
108 icon: Database,
109 label: t("landing_feature_vector"),
110 desc: t("landing_feature_vector_desc"),
111 },
112 {
113 icon: Zap,
114 label: t("landing_feature_sim"),
115 desc: t("landing_feature_sim_desc"),
116 },
117 ];
118
119 const headlineStats = stats
120 ? [
121 { val: stats.cropCount.toString(), label: t("landing_active_crops") },
122 {
123 val: stats.cropTypes.length.toString(),
124 label: t("landing_crop_types"),
125 },
126 { val: stats.totalSeqs.toString(), label: t("landing_total_cycles") },
127 {
128 val: stats.alerts > 0 ? stats.alerts.toString() : "0",
129 label: t("landing_active_alerts"),
130 },
131 ]
132 : [
133 { val: "-", label: t("landing_active_crops") },
134 { val: "-", label: t("landing_crop_types") },
135 { val: "-", label: t("landing_total_cycles") },
136 { val: "-", label: t("landing_active_alerts") },
137 ];
138
139 // Live sensor readings
140 const readings = stats
141 ? [
142 {
143 label: t("analytics_avg_ph"),
144 value: stats.ph,
145 ok: parseFloat(stats.ph) >= 5.5 && parseFloat(stats.ph) <= 6.5,
146 },
147 {
148 label: t("analytics_avg_ec"),
149 value: `${stats.ec}`,
150 ok: parseFloat(stats.ec) <= 2.5,
151 },
152 {
153 label: t("sensor_temp").toUpperCase(),
154 value: `${stats.temp}°C`,
155 ok: parseFloat(stats.temp) >= 18 && parseFloat(stats.temp) <= 30,
156 },
157 {
158 label: t("sensor_humidity").toUpperCase(),
159 value: `${stats.humidity}%`,
160 ok:
161 parseFloat(stats.humidity) >= 40 &&
162 parseFloat(stats.humidity) <= 80,
163 },
164 ]
165 : [
166 { label: t("analytics_avg_ph"), value: "-", ok: true },
167 { label: t("analytics_avg_ec"), value: "-", ok: true },
168 { label: t("sensor_temp").toUpperCase(), value: "-", ok: true },
169 { label: t("sensor_humidity").toUpperCase(), value: "-", ok: true },
170 ];
171
172 return (
173 <div
174 className="min-h-screen grid-bg relative overflow-hidden"
175 style={{ background: "var(--bg)" }}
176 >
177 <div className="scanline" />
178
179 {/* Ambient glow */}
180 <div
181 className="absolute top-0 left-1/2 -translate-x-1/2 w-[800px] h-[400px] pointer-events-none"
182 style={{
183 background:
184 "radial-gradient(ellipse at center, rgba(74,222,128,0.06) 0%, transparent 70%)",
185 }}
186 />
187
188 {/* Nav */}
189 <nav
190 className="relative z-10 flex items-center justify-between px-8 py-5 border-b"
191 style={{ borderColor: "rgba(74,222,128,0.1)" }}
192 >
193 <div className="flex items-center gap-3">
194 <div
195 className="w-9 h-9 rounded-xl flex items-center justify-center"
196 style={{ background: "linear-gradient(135deg, #1a5c2d, #4ade80)" }}
197 >
198 <Leaf size={18} fill="white" color="white" />
199 </div>
200 <div>
201 <div
202 className="font-bold text-base tracking-tight"
203 style={{ color: "var(--text)" }}
204 >
205 DEMETER
206 </div>
207 <div
208 className="text-[9px] font-mono tracking-[0.2em]"
209 style={{ color: "var(--text-3)" }}
210 >
211 {td("AUTONOMOUS FARM INTELLIGENCE")}
212 </div>
213 </div>
214 </div>
215
216 <div className="flex items-center gap-4">
217 <div
218 className="flex items-center gap-2 text-[11px] font-mono px-3 py-1.5 rounded-full"
219 style={{
220 border: "1px solid rgba(74,222,128,0.3)",
221 color: "var(--green)",
222 background: "rgba(74,222,128,0.05)",
223 }}
224 >
225 <span
226 className="status-dot w-1.5 h-1.5 rounded-full"
227 style={{ background: "var(--green)" }}
228 />
229 {loading
230 ? t("sidebar_connecting")
231 : stats
232 ? t("sidebar_farm_online")
233 : t("sidebar_no_data")}
234 </div>
235 <button
236 onClick={() => navigate("/dashboard")}
237 className="px-4 py-2 rounded-lg text-sm font-semibold transition-all"
238 style={{
239 background: "var(--surface)",
240 color: "var(--text-2)",
241 border: "1px solid var(--border)",
242 }}
243 >
244 {t("landing_enter_dash")}
245 </button>
246 </div>
247 </nav>
248
249 {/* Hero */}
250 <div className="relative z-10 max-w-7xl mx-auto px-8 pt-24 pb-16">
251 <div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
252 {/* Left */}
253 <div
254 className={`space-y-8 ${mounted ? "animate-fade-up" : "opacity-0"}`}
255 >
256 <div
257 className="inline-flex items-center gap-2 text-[11px] font-mono px-3 py-1.5 rounded-full"
258 style={{
259 border: "1px solid rgba(245,158,11,0.4)",
260 color: "var(--amber)",
261 background: "rgba(245,158,11,0.06)",
262 }}
263 >
264 <Zap size={10} fill="currentColor" />
265 {td("MULTI-AGENT SYSTEM · LANGGRAPH · AZURE")}
266 </div>
267
268 <h1
269 className="text-6xl lg:text-7xl font-bold leading-[1.0] tracking-tight"
270 style={{ color: "var(--text)" }}
271 >
272 {t("landing_hero_1")}
273 <br />
274 <span
275 className="font-serif italic"
276 style={{ color: "var(--green)" }}
277 >
278 {t("landing_hero_2")}
279 </span>
280 <br />
281 {t("landing_hero_3")}
282 </h1>
283
284 <p
285 className="text-lg leading-relaxed max-w-md"
286 style={{ color: "var(--text-2)", fontWeight: 300 }}
287 >
288 {t("landing_hero_sub")}
289 </p>
290
291 <div className="flex gap-4">
292 <button
293 onClick={() => navigate("/dashboard")}
294 className="group flex items-center gap-3 px-7 py-3.5 rounded-xl font-semibold text-sm transition-all glow-green"
295 style={{ background: "var(--green)", color: "#0c1a0e" }}
296 >
297 {t("landing_enter_dash")}{" "}
298 <ArrowRight
299 size={16}
300 className="group-hover:translate-x-1 transition-transform"
301 />
302 </button>
303 <button
304 onClick={() => navigate("/intelligence")}
305 className="flex items-center gap-3 px-7 py-3.5 rounded-xl font-semibold text-sm transition-all"
306 style={{
307 border: "1px solid var(--border)",
308 color: "var(--text-2)",
309 background: "var(--surface)",
310 }}
311 >
312 <Sparkles size={16} /> {t("landing_intelligence")}
313 </button>
314 </div>
315
316 {/* Live headline stats from DB */}
317 <div
318 className="grid grid-cols-4 gap-4 pt-4 border-t"
319 style={{ borderColor: "var(--border)" }}
320 >
321 {headlineStats.map(({ val, label }) => (
322 <div key={label}>
323 <div
324 className="text-2xl font-bold font-mono"
325 style={{ color: "var(--green)" }}
326 >
327 {loading ? (
328 <span className="inline-block w-8 h-6 rounded shimmer" />
329 ) : (
330 val
331 )}
332 </div>
333 <div
334 className="text-[11px] mt-0.5"
335 style={{ color: "var(--text-3)" }}
336 >
337 {label}
338 </div>
339 </div>
340 ))}
341 </div>
342 </div>
343
344 {/* Right - live HUD */}
345 <div
346 className={`relative ${mounted ? "animate-fade-in" : "opacity-0"}`}
347 >
348 <div
349 className="rounded-2xl overflow-hidden"
350 style={{
351 border: "1px solid var(--border)",
352 background: "var(--bg-2)",
353 }}
354 >
355 {/* Terminal header */}
356 <div
357 className="flex items-center gap-2 px-4 py-3 border-b"
358 style={{
359 borderColor: "var(--border)",
360 background: "var(--bg-3)",
361 }}
362 >
363 <div
364 className="w-3 h-3 rounded-full"
365 style={{ background: "#ff5f57" }}
366 />
367 <div
368 className="w-3 h-3 rounded-full"
369 style={{ background: "#ffbd2e" }}
370 />
371 <div
372 className="w-3 h-3 rounded-full"
373 style={{ background: "#28ca41" }}
374 />
375 <span
376 className="ml-2 text-[11px] font-mono"
377 style={{ color: "var(--text-3)" }}
378 >
379 {t("landing_live_feed")}
380 </span>
381 <Activity
382 size={11}
383 className="ml-auto"
384 style={{ color: "var(--green)" }}
385 />
386 </div>
387
388 <div className="p-6 space-y-5">
389 {/* Live sensor grid */}
390 <div className="grid grid-cols-2 gap-3">
391 {readings.map(({ label, value, ok }) => (
392 <div
393 key={label}
394 className="rounded-xl p-4"
395 style={{
396 background: "var(--surface)",
397 border: "1px solid var(--border)",
398 }}
399 >
400 <div
401 className="text-[10px] font-mono mb-2"
402 style={{ color: "var(--text-3)" }}
403 >
404 {label}
405 </div>
406 {loading ? (
407 <div className="h-7 w-16 rounded shimmer" />
408 ) : (
409 <div
410 className="text-2xl font-mono font-bold"
411 style={{ color: ok ? "var(--green)" : "var(--red)" }}
412 >
413 {value}
414 </div>
415 )}
416 <div
417 className="text-[9px] font-mono mt-1"
418 style={{
419 color: ok
420 ? "rgba(74,222,128,0.6)"
421 : "rgba(248,113,113,0.6)",
422 }}
423 >
424 {ok ? t("landing_optimal") : t("landing_alert")}
425 </div>
426 </div>
427 ))}
428 </div>
429
430 {/* Agent activity feed */}
431 <div
432 className="rounded-xl p-4"
433 style={{
434 background: "var(--surface)",
435 border: "1px solid var(--border)",
436 }}
437 >
438 <div
439 className="text-[10px] font-mono mb-3"
440 style={{ color: "var(--text-3)" }}
441 >
442 {t("landing_recent_activity")}
443 </div>
444 <div className="space-y-2">
445 {loading ? (
446 [1, 2, 3].map((i) => (
447 <div key={i} className="h-4 rounded shimmer" />
448 ))
449 ) : log.length > 0 ? (
450 log.slice(0, 3).map(({ agent, msg, time }, i) => (
451 <div
452 key={i}
453 className="flex items-start gap-2 text-[11px] font-mono"
454 >
455 <span style={{ color: "var(--green)", opacity: 0.6 }}>
456 {time}
457 </span>
458 <span
459 className="px-1.5 py-0.5 rounded text-[9px]"
460 style={{
461 background: "rgba(74,222,128,0.12)",
462 color: "var(--green)",
463 whiteSpace: "nowrap",
464 }}
465 >
466 {td(agent.substring(0, 12))}
467 </span>
468 <span style={{ color: "var(--text-2)" }}>
469 {td(msg.substring(0, 40))}
470 {msg.length > 40 ? "…" : ""}
471 </span>
472 </div>
473 ))
474 ) : (
475 <div
476 className="text-[11px] font-mono"
477 style={{ color: "var(--text-3)" }}
478 >
479 {t("landing_no_cycles")}
480 </div>
481 )}
482 <div
483 className="flex items-center gap-1 text-[11px] font-mono cursor-blink"
484 style={{ color: "var(--green)" }}
485 >
486 <span style={{ opacity: 0.4 }}>→ </span>
487 </div>
488 </div>
489 </div>
490 </div>
491 </div>
492
493 {/* Live crop count chip */}
494 {stats && (
495 <div
496 className="absolute -top-4 -right-4 px-3 py-2 rounded-lg text-[11px] font-mono"
497 style={{
498 background: "var(--surface-2)",
499 border: "1px solid var(--border)",
500 color: "var(--amber)",
501 }}
502 >
503 ⬆ {stats.cropCount} {t("common_crop").toLowerCase()}
504 {stats.cropCount !== 1 ? "s" : ""}
505 </div>
506 )}
507 </div>
508 </div>
509
510 {/* Feature grid */}
511 <div
512 className="mt-24 pt-12 border-t"
513 style={{ borderColor: "var(--border)" }}
514 >
515 <div
516 className="text-[11px] font-mono mb-8"
517 style={{ color: "var(--text-3)" }}
518 >
519 {t("landing_capabilities")}
520 </div>
521 <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
522 {FEATURES.map(({ icon: Icon, label, desc }) => (
523 <div
524 key={label}
525 className="p-5 rounded-xl card-hover"
526 style={{
527 background: "var(--surface)",
528 border: "1px solid var(--border)",
529 }}
530 >
531 <div
532 className="w-9 h-9 rounded-lg flex items-center justify-center mb-4"
533 style={{
534 background: "rgba(74,222,128,0.1)",
535 border: "1px solid rgba(74,222,128,0.2)",
536 }}
537 >
538 <Icon size={18} style={{ color: "var(--green)" }} />
539 </div>
540 <div
541 className="font-semibold text-sm mb-1"
542 style={{ color: "var(--text)" }}
543 >
544 {label}
545 </div>
546 <div
547 className="text-xs leading-relaxed"
548 style={{ color: "var(--text-3)" }}
549 >
550 {desc}
551 </div>
552 </div>
553 ))}
554 </div>
555 </div>
556 </div>
557 </div>
558 );
559}