// Test page to showcase plant watering times on a vertical, linear timeline, // where each column represents a plant and each dot on the timeline represents a watering event const PlantsPage = () => { const { Box, Typography, Paper, Grid, Tooltip, ToggleButton, ToggleButtonGroup, FormControl, InputLabel, Select, MenuItem, Stack, Chip, Divider, } = MaterialUI; // ----------------------------- // Demo data (replace with your API / DB) // ----------------------------- const plants = [ { id: "plant1", name: "Plant 1", events: [ { ts: "2026-01-06T18:10:00Z", notes: "Soil dry" }, { ts: "2025-12-29T20:05:00Z" }, { ts: "2025-12-22T21:15:00Z" }, { ts: "2025-12-13T19:00:00Z" }, { ts: "2025-11-28T18:30:00Z" }, { ts: "2025-11-10T17:45:00Z", notes: "Fertilized" }, { ts: "2025-10-25T18:00:00Z" }, { ts: "2025-10-05T18:00:00Z" }, { ts: "2025-09-15T18:00:00Z" }, { ts: "2025-08-25T18:00:00Z" }, { ts: "2025-08-01T18:00:00Z", notes: "Repotted" }, { ts: "2025-07-10T18:00:00Z" }, { ts: "2025-06-20T18:00:00Z" }, { ts: "2025-06-01T18:00:00Z" }, ], }, { id: "plant2", name: "Plant 2", events: [ { ts: "2025-12-30T19:10:00Z" }, { ts: "2025-12-03T19:10:00Z" }, { ts: "2025-11-02T18:55:00Z" }, { ts: "2025-10-10T18:00:00Z" }, { ts: "2025-09-15T18:00:00Z", notes: "Pruned" }, { ts: "2025-08-20T18:00:00Z" }, { ts: "2025-07-25T18:00:00Z" }, { ts: "2025-07-01T18:00:00Z" }, { ts: "2025-06-10T18:00:00Z" }, { ts: "2025-06-01T18:00:00Z" }, ], }, { id: "plant3", name: "Plant 3", events: [ { ts: "2026-01-07T17:30:00Z" }, { ts: "2025-12-28T17:30:00Z" }, { ts: "2025-12-18T17:30:00Z" }, { ts: "2025-12-08T17:30:00Z" }, { ts: "2025-11-18T17:30:00Z" }, { ts: "2025-10-28T17:30:00Z", notes: "Heavy watering" }, { ts: "2025-09-28T17:30:00Z" }, { ts: "2025-08-28T17:30:00Z" }, { ts: "2025-07-28T17:30:00Z" }, { ts: "2025-06-28T17:30:00Z" }, { ts: "2025-06-01T17:30:00Z" }, ], }, { id: "plant4", name: "Plant 4", events: [ { ts: "2025-12-20T22:00:00Z", notes: "Light watering" }, { ts: "2025-11-25T22:00:00Z" }, { ts: "2025-10-28T22:00:00Z" }, { ts: "2025-09-28T22:00:00Z" }, { ts: "2025-08-28T22:00:00Z" }, { ts: "2025-07-28T22:00:00Z", notes: "Fertilized" }, { ts: "2025-06-28T22:00:00Z" }, { ts: "2025-06-01T22:00:00Z" }, ], }, // Add more plants; columns will keep extending horizontally { id: "plant5", name: "Plant 5", events: [ { ts: "2025-12-25T16:00:00Z" }, { ts: "2025-10-30T16:00:00Z" }, { ts: "2025-08-30T16:00:00Z" }, { ts: "2025-06-01T16:00:00Z" }, ], }, ]; // ----------------------------- // Helpers // ----------------------------- const clamp01 = (x) => Math.min(1, Math.max(0, x)); const formatDateTimeLocal = (d) => { // Shows in user's local time (browser) const pad = (n) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad( d.getHours() )}:${pad(d.getMinutes())}`; }; const daysBetween = (a, b) => { const ms = Math.abs(a.getTime() - b.getTime()); return ms / (1000 * 60 * 60 * 24); }; // ----------------------------- // View controls // ----------------------------- const [rangeDays, setRangeDays] = React.useState(60); // timeline range const [sortMode, setSortMode] = React.useState("name"); // name | recent const [density, setDensity] = React.useState("comfortable"); // compact | comfortable const now = React.useMemo(() => new Date(), []); const rangeStart = React.useMemo( () => new Date(now.getTime() - rangeDays * 86400000), [now, rangeDays] ); // Flatten events to compute global min/max (within range) for consistent timeline scaling const allEventsInRange = React.useMemo(() => { const out = []; for (const p of plants) { for (const e of p.events || []) { const d = new Date(e.ts); if (!isFinite(d.getTime())) continue; if (d >= rangeStart && d <= now) out.push({ plantId: p.id, date: d, raw: e }); } } return out; }, [plants, rangeStart, now]); // Use consistent scale based on (rangeStart..now) const timeToYPercent = React.useCallback( (date) => { const t0 = rangeStart.getTime(); const t1 = now.getTime(); const td = date.getTime(); if (t1 === t0) return 0; // 0% at top = now, 100% at bottom = rangeStart (so recent events appear higher) const pct = 1 - (td - t0) / (t1 - t0); return clamp01(pct) * 100; }, [rangeStart, now] ); // Sort plants const sortedPlants = React.useMemo(() => { const copy = [...plants]; if (sortMode === "name") { copy.sort((a, b) => (a.name || "").localeCompare(b.name || "")); return copy; } if (sortMode === "recent") { const mostRecentTs = (p) => { const ds = (p.events || []) .map((e) => new Date(e.ts)) .filter((d) => isFinite(d.getTime())); if (!ds.length) return 0; return Math.max(...ds.map((d) => d.getTime())); }; copy.sort((a, b) => mostRecentTs(b) - mostRecentTs(a)); return copy; } return copy; }, [plants, sortMode]); // Summary (optional) const plantStats = React.useMemo(() => { const map = {}; for (const p of plants) { const ds = (p.events || []) .map((e) => new Date(e.ts)) .filter((d) => isFinite(d.getTime())); ds.sort((a, b) => b.getTime() - a.getTime()); const last = ds[0] || null; map[p.id] = { last, daysSinceLast: last ? daysBetween(now, last) : null, countInRange: (p.events || []).filter((e) => { const d = new Date(e.ts); return isFinite(d.getTime()) && d >= rangeStart && d <= now; }).length, }; } return map; }, [plants, now, rangeStart]); // ----------------------------- // Timeline layout constants // ----------------------------- const timelineHeight = density === "compact" ? 360 : 520; const lineTopPad = 18; const lineBottomPad = 18; const dotSize = density === "compact" ? 9 : 11; const ticks = React.useMemo(() => { // simple tick marks at 0, 25, 50, 75, 100% with labels in days ago const t0 = rangeStart.getTime(); const t1 = now.getTime(); const lerpTime = (u) => new Date(t0 + u * (t1 - t0)); const daysAgo = (d) => Math.round(daysBetween(now, d)); const points = [1, 0.75, 0.5, 0.25, 0].map((u) => { const d = lerpTime(u); return { yPct: (1 - u) * 100, label: `${daysAgo(d)}d` }; }); return points; }, [rangeStart, now]); return ( Plants {/* Controls */} Range Sort v && setDensity(v)} aria-label="density" > Comfortable Compact Watering events {/* Timeline scale and plant columns side by side */} {/* Timeline scale (left column) */} Timeline {ticks.map((t, idx) => ( {t.label} ago ))} {/* Plant columns (right, horizontally adjacent) */} {sortedPlants.map((plant) => { const stats = plantStats[plant.id] || {}; const eventsInRange = (plant.events || []) .map((e) => ({ ...e, date: new Date(e.ts) })) .filter((e) => isFinite(e.date.getTime()) && e.date >= rangeStart && e.date <= now) .sort((a, b) => b.date.getTime() - a.date.getTime()); // newest first return ( {plant.name} {/* The timeline line */} {/* Dots */} {eventsInRange.map((e, idx) => { const yPct = timeToYPercent(e.date); const topPx = (timelineHeight - lineTopPad - lineBottomPad) * (yPct / 100) + lineTopPad; const tooltip = `${formatDateTimeLocal(e.date)}${ e.notes ? ` — ${e.notes}` : "" }`; return ( ); })} {/* Empty state */} {eventsInRange.length === 0 && ( No events in range )} ); })} ); }; // Make component available globally window.PlantsPage = PlantsPage;