"use client"; import { useState, useEffect, useCallback } from "react"; import { Plus, CheckCircle2, Circle, MoreHorizontal, Flame, Filter, Pencil, Trash2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { HabitCreateDialog } from "@/components/habits/habit-create-dialog"; import { HabitEditDialog } from "@/components/habits/habit-edit-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 { CreateItemDialog } from "@/components/create-item-dialog"; import { useCreateDialogStore } from "@/lib/stores/use-create-dialog-store"; import { toast } from "sonner"; interface Habit { id: string; name: string; description: string | null; domainId: string; frequency: 'daily' | 'weekly' | 'custom'; difficulty: 'easy' | 'medium' | 'hard'; goalPerPeriod: number; unit: string | null; streakCount: number; bestStreak: number; active: boolean; moodTracking: boolean; tags: { id: string; name: string; color: string | null }[]; } const difficultyColors: Record = { easy: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200", medium: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200", hard: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200", }; export default function HabitsPage() { const [habits, setHabits] = useState([]); const [domainId, setDomainId] = useState(null); const [domains, setDomains] = useState<{ id: string; name: string }[]>([]); const [createOpen, setCreateOpen] = useState(false); const { open: storeOpen, openCreate, closeCreate } = useCreateDialogStore(); const [editHabit, setEditHabit] = useState(null); const [completionHabit, setCompletionHabit] = useState(null); const [deleteHabit, setDeleteHabit] = useState(null); const [deleting, setDeleting] = useState(false); const [expandedHabit, setExpandedHabit] = useState(null); const [filter, setFilter] = useState('all'); const [loading, setLoading] = useState(true); // Fetch domains useEffect(() => { fetch('/api/domains?sort=sort_order') .then((res) => res.json()) .then((data) => { const items = data.items || []; setDomains(items); if (items.length > 0 && !domainId) { setDomainId(items[0].id); } }) .catch(() => {}); }, []); // eslint-disable-line react-hooks/exhaustive-deps // Fetch habits const fetchHabits = useCallback(async () => { if (!domainId) return; setLoading(true); try { const params = new URLSearchParams(); if (filter === 'active') params.set('active', 'true'); const res = await fetch(`/api/domains/${domainId}/habits?${params}`); const data = await res.json(); setHabits(data.items || []); } catch { toast.error('Failed to load habits'); } finally { setLoading(false); } }, [domainId, filter]); useEffect(() => { fetchHabits(); }, [fetchHabits]); // Complete a habit const handleComplete = async (habit: Habit, value?: number, mood?: number, notes?: string) => { try { const res = await fetch(`/api/domains/${domainId}/habits/${habit.id}/complete`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ value: value ?? 1, mood, notes }), }); if (!res.ok) throw new Error('Failed to complete'); toast.success(`"${habit.name}" logged!`); fetchHabits(); } catch { toast.error('Failed to complete habit'); } }; // Listen for custom event to open create dialog useEffect(() => { const handler = () => setCreateOpen(true); document.addEventListener('open-create-habit', handler); return () => document.removeEventListener('open-create-habit', handler); }, []); return (

Habits

Build streaks, track progress, stay consistent.

{domains.length > 1 && ( )}
{loading ? (
Loading habits...
) : habits.length === 0 ? (
) : (
{habits.map((habit) => (
{habit.name} {habit.difficulty} {habit.unit && ( per {habit.unit} )}
{habit.tags.length > 0 && (
{habit.tags.map((tag) => ( {tag.name} ))}
)}
setEditHabit(habit)}> Edit setDeleteHabit(habit)}> Delete
{expandedHabit === habit.id && (
)}
))}
)} {/* Analytics */}
{ setCreateOpen(o); if (!o) closeCreate(); }} domainId={domainId || ''} onCreated={fetchHabits} /> {/* Global CreateItemDialog from useCreateDialogStore — opened by topbar 'New habit' button */} { if (!o) closeCreate(); else openCreate('habit'); }} onCreated={fetchHabits} /> {editHabit && ( { if (!open) setEditHabit(null); }} habit={editHabit} domainId={domainId || ''} onUpdated={fetchHabits} /> )} {completionHabit && ( { if (!open) setCompletionHabit(null); }} habit={completionHabit} onComplete={(value, mood, notes) => { handleComplete(completionHabit, value, mood, notes); setCompletionHabit(null); }} /> )} { if (!open) setDeleteHabit(null); }}> Delete Habit Are you sure you want to delete "{deleteHabit?.name}"? This action cannot be undone. Cancel { if (!deleteHabit || !domainId) return; setDeleting(true); try { const res = await fetch(`/api/domains/${domainId}/habits/${deleteHabit.id}`, { method: 'DELETE', }); if (!res.ok) throw new Error('Failed to delete'); toast.success('Habit deleted'); setDeleteHabit(null); fetchHabits(); } catch { toast.error('Failed to delete habit'); } finally { setDeleting(false); } }} > {deleting ? 'Deleting...' : 'Delete'}
); }