"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 { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; 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 { HabitCompletionDialog } from "@/components/habits/habit-completion-dialog"; import { HabitCalendarHeatmap } from "@/components/habits/habit-calendar-heatmap"; 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 [completionHabit, setCompletionHabit] = useState(null); const [expandedHabit, setExpandedHabit] = useState(null); const [filter, setFilter] = useState('all'); const [loading, setLoading] = useState(true); const [editingHabit, setEditingHabit] = useState(null); const [editName, setEditName] = useState(""); const [editDescription, setEditDescription] = useState(""); const [editFrequency, setEditFrequency] = useState<"daily" | "weekly" | "custom">("daily"); const [editDifficulty, setEditDifficulty] = useState<"easy" | "medium" | "hard">("medium"); const [editGoalPerPeriod, setEditGoalPerPeriod] = useState(1); const [saving, setSaving] = useState(false); const [deleteHabitId, setDeleteHabitId] = useState(null); const [deleting, setDeleting] = useState(false); // 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'); } }; // Edit a habit const handleEdit = async () => { if (!editingHabit || !editName.trim()) return; setSaving(true); try { const res = await fetch(`/api/habits/${editingHabit.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: editName.trim(), description: editDescription || null, frequency: editFrequency, difficulty: editDifficulty, goalPerPeriod: editGoalPerPeriod, }), }); if (!res.ok) throw new Error('Failed to update'); toast.success('Habit updated'); setEditingHabit(null); fetchHabits(); } catch { toast.error('Failed to update habit'); } finally { setSaving(false); } }; // Delete a habit const handleDelete = async () => { if (!deleteHabitId) return; setDeleting(true); try { const res = await fetch(`/api/habits/${deleteHabitId}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Failed to delete'); toast.success('Habit deleted'); setDeleteHabitId(null); fetchHabits(); } catch { toast.error('Failed to delete habit'); } finally { setDeleting(false); } }; // Open edit dialog with habit data const openEdit = (habit: Habit) => { setEditName(habit.name); setEditDescription(habit.description || ""); setEditFrequency(habit.frequency); setEditDifficulty(habit.difficulty); setEditGoalPerPeriod(habit.goalPerPeriod || 1); setEditingHabit(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} ))}
)}
openEdit(habit)}> Edit setCompletionHabit(habit)}> Log details setDeleteHabitId(habit.id)}> Delete
{expandedHabit === habit.id && (
)}
))}
)} {completionHabit && ( { if (!open) setCompletionHabit(null); }} habit={completionHabit} onComplete={(value, mood, notes) => { handleComplete(completionHabit, value, mood, notes); setCompletionHabit(null); }} /> )} {/* Edit Habit Dialog */} { if (!open) setEditingHabit(null); }}> Edit habit Update your habit details.
setEditName(e.target.value)} autoFocus required />