diff --git a/apps/web/app/(dashboard)/habits/page.tsx b/apps/web/app/(dashboard)/habits/page.tsx
index 198ee8b..3b4514b 100644
--- a/apps/web/app/(dashboard)/habits/page.tsx
+++ b/apps/web/app/(dashboard)/habits/page.tsx
@@ -7,6 +7,7 @@ import { Badge } from "@/components/ui/badge";
import { HabitCreateDialog } from "@/components/habits/habit-create-dialog";
import { HabitCompletionDialog } from "@/components/habits/habit-completion-dialog";
import { HabitCalendarHeatmap } from "@/components/habits/habit-calendar-heatmap";
+import { HabitAnalytics } from "@/components/habits/habit-analytics";
import { toast } from "sonner";
interface Habit {
@@ -214,6 +215,11 @@ export default function HabitsPage() {
)}
+ {/* Analytics */}
+
+
+
+
([]);
+ const [completions, setCompletions] = useState([]);
+ const [loading, setLoading] = useState(false);
+
+ useEffect(() => {
+ if (!open) return;
+ setLoading(true);
+
+ Promise.all([
+ fetch('/api/habits/streaks').then((r) => r.json()),
+ ...habits.map((h) =>
+ fetch(
+ '/api/domains/' + domainId + '/habits/' + h.id + '/completions?from=' + daysAgo(30) + '&order=asc&limit=365'
+ ).then((r) => r.json())
+ ),
+ ])
+ .then(([streaksData, ...completionsData]) => {
+ setStreaks((streaksData.streaks || []).slice(0, 5));
+
+ const dateMap = new Map();
+ for (const data of completionsData) {
+ for (const item of data.items || []) {
+ const d = item.date?.split('T')[0];
+ if (d) dateMap.set(d, (dateMap.get(d) || 0) + 1);
+ }
+ }
+ const sorted = Array.from(dateMap.entries())
+ .sort(([a], [b]) => a.localeCompare(b))
+ .map(([date, count]) => ({ date, count }));
+ setCompletions(sorted);
+ })
+ .catch(() => {})
+ .finally(() => setLoading(false));
+ }, [open, domainId, habits]);
+
+ if (!open) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
Analytics (30 days)
+
+
+
+ {loading ? (
+
Loading analytics...
+ ) : (
+
+
+
+
+
+ Daily Completions
+
+
+
+ {completions.length === 0 ? (
+ No data yet
+ ) : (
+
+
+
+ v.slice(5)} />
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+ Top Streaks
+
+
+
+ {streaks.length === 0 ? (
+ No streaks yet
+ ) : (
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+ )}
+
+ );
+}
+
+function daysAgo(n: number): string {
+ const d = new Date();
+ d.setDate(d.getDate() - n);
+ return d.toISOString().split('T')[0];
+}