import { useState, useEffect, useMemo } from "react";
const DailyComponent = () => {
const storageKey = "daily-component-v1";
const pushUrl = "/api/v1/json_data";
const autosyncMs = 30000;
const [state, setState] = useState(() => {
try {
const saved = localStorage.getItem(storageKey);
return saved ? JSON.parse(saved) : { version: 1, items: initialItems(), lastSyncAt: null, lastSyncOk: null };
} catch {
return { version: 1, items: initialItems(), lastSyncAt: null, lastSyncOk: null };
}
});
const [showOnlyActive, setShowOnlyActive] = useState(false);
const [expanded, setExpanded] = useState({});
const [syncing, setSyncing] = useState(false);
const [syncError, setSyncError] = useState(null);
// persist to localStorage
useEffect(() => {
try {
localStorage.setItem(storageKey, JSON.stringify(state));
} catch {}
}, [state, storageKey]);
// autosync
useEffect(() => {
if (!autosyncMs || autosyncMs < 5000) return;
const t = setInterval(() => {
pushNow();
}, autosyncMs);
return () => clearInterval(t);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state]);
const toggleActive = (id) => {
setState((s) => ({
...s,
items: mapItemTree(s.items, id, (it) => ({ ...it, active: !it.active })),
}));
};
const incProgress = (id, delta) => {
setState((s) => ({
...s,
items: mapItemTree(s.items, id, (it) => ({
...it,
progress: clamp(it.progress + delta, 0, 100),
})),
}));
};
const updateNote = (id, note) => {
setState((s) => ({
...s,
items: mapItemTree(s.items, id, (it) => ({ ...it, note })),
}));
};
const toggleExpand = (id) => setExpanded((e) => ({ ...e, [id]: !(e[id] ?? true) }));
const pushNow = async () => {
setSyncing(true);
setSyncError(null);
try {
const payload = {
type: "daily-component/state",
at: new Date().toISOString(),
storageKey,
data: state,
stats: summarize(state.items),
};
const res = await fetch(pushUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
setState((s) => ({ ...s, lastSyncAt: new Date().toISOString(), lastSyncOk: true }));
} catch (e) {
setState((s) => ({ ...s, lastSyncAt: new Date().toISOString(), lastSyncOk: false }));
setSyncError(e.message || String(e));
} finally {
setSyncing(false);
}
};
const overall = useMemo(() => {
const flat = flatten(state.items).filter((x) => (showOnlyActive ? x.active : true));
const total = flat.reduce((a, it) => a + computeDisplayProgress(it), 0);
const pct = flat.length ? Math.round(total / flat.length) : 0;
const activeCount = flatten(state.items).filter((x) => x.active).length;
return { pct, count: flat.length, activeCount };
}, [state.items, showOnlyActive]);
return (
);
};
export default DailyComponent;
/* ---------- helpers ---------- */
const clamp = (v, lo = 0, hi = 100) => Math.max(lo, Math.min(hi, v));
const uid = () => Math.random().toString(36).slice(2, 9);
const initialItems = () => [
{ id: uid(), title: "CEC", active: true, note: "", progress: 0 },
{
id: uid(),
title: "Boarding Assistant",
active: true,
note: "",
progress: 0,
children: [
{ id: uid(), title: "Camera fix", active: true, note: "", progress: 0 },
{
id: uid(),
title: "Belt+Watch",
active: true,
note: "",
progress: 0,
children: [
{ id: uid(), title: "Image gen", active: true, note: "", progress: 0 },
{ id: uid(), title: "Backend update", active: true, note: "", progress: 0 },
],
},
],
},
{
id: uid(),
title: "Concierge",
active: true,
note: "",
progress: 0,
children: [
{
id: uid(),
title: "Microphone",
active: true,
note: "",
progress: 0,
children: [
{ id: uid(), title: "Look at reviews", active: true, note: "", progress: 0 },
{ id: uid(), title: "See what kind of support can there be", active: true, note: "", progress: 0 },
],
},
{ id: uid(), title: "Overview video removing", active: true, note: "", progress: 0 },
],
},
{
id: uid(),
title: "IrLive",
active: true,
note: "",
progress: 0,
children: [
{ id: uid(), title: "Unzip", active: true, note: "", progress: 0 },
{ id: uid(), title: "Detect faces", active: true, note: "", progress: 0 },
{ id: uid(), title: "Qwen experiment", active: true, note: "", progress: 0 },
],
},
{ id: uid(), title: "Misc", active: true, note: "", progress: 0 },
];
const mapItemTree = (items, id, fn) =>
items.map((it) => {
if (it.id === id) return fn({ ...it });
if (it.children?.length) return { ...it, children: mapItemTree(it.children, id, fn) };
return it;
});
const computeDisplayProgress = (it) => {
if (!it.children || it.children.length === 0) return it.progress;
const vals = it.children.map(computeDisplayProgress);
return Math.round(vals.reduce((a, b) => a + b, 0) / vals.length);
};
const flatten = (items, out = []) => {
items.forEach((it) => {
out.push(it);
if (it.children?.length) flatten(it.children, out);
});
return out;
};
const summarize = (items) => {
const flat = flatten(items);
const byTitle = {};
flat.forEach((it) => {
(byTitle[it.title] ||= []).push({ active: it.active, progress: it.progress });
});
const summary = Object.entries(byTitle).map(([title, arr]) => ({
title,
n: arr.length,
active: arr.filter((x) => x.active).length,
avg: Math.round(arr.reduce((a, b) => a + b.progress, 0) / arr.length),
}));
const avg = flat.length ? Math.round(flat.reduce((a, b) => a + b.progress, 0) / flat.length) : 0;
return { count: flat.length, avg, summary };
};
/* ---------- tree ---------- */
const Tree = ({ items, showOnlyActive, expanded, onToggleExpand, onToggleActive, onIncProgress, onUpdateNote }) => {
return (
{items.map((item) => (
))}
);
};
const TreeRow = ({
item,
depth,
showOnlyActive,
expanded,
onToggleExpand,
onToggleActive,
onIncProgress,
onUpdateNote,
}) => {
const hasChildren = !!(item.children && item.children.length);
const isExpanded = expanded[item.id] ?? true;
const displayPct = computeDisplayProgress(item);
if (showOnlyActive && !item.active) return null;
return (
{hasChildren ? (
) : (
)}
onUpdateNote(item.id, e.target.value)}
placeholder="Add a quick note…"
className="w-full text-sm px-2 py-1 border rounded-lg bg-white focus:outline-none"
/>
{hasChildren && isExpanded && (
{item.children.map((ch) => (
))}
)}
);
};