'use client'; import { useEffect, useState, Suspense } from 'react'; import { Flame, Plus } from 'lucide-react'; import dynamic from 'next/dynamic'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { HabitCard } from '@/components/habits/habit-card'; import { HabitCompletionDialog } from '@/components/habits/habit-completion-dialog'; import type { Habit } from '@project-e/shared'; import { CreateItemDialog } from '@/components/create-item-dialog'; import { toast } from 'sonner'; // Lazy load react-calendar-heatmap (~15KB) const HabitHeatmap = dynamic( () => import('@/components/habits/habit-heatmap').then((m) => m.HabitHeatmap), { ssr: false, loading: () => (
), } ); /** Extended habit with server-computed fields */ interface HabitWithMeta extends Habit { logged_today: boolean; } export default function HabitsPage() { const [habits, setHabits] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [selectedHabit, setSelectedHabit] = useState(null); const [completionDialogOpen, setCompletionDialogOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false); useEffect(() => { fetchHabits(); }, []); useEffect(() => { if (new URLSearchParams(window.location.search).get('new') === 'true') setCreateOpen(true); }, []); function handleCreateOpenChange(open: boolean) { setCreateOpen(open); if (!open && new URLSearchParams(window.location.search).get('new') === 'true') { window.history.replaceState(null, '', '/habits'); } } async function fetchHabits() { try { setError(null); const response = await fetch('/api/habits'); if (!response.ok) throw new Error('Unable to load habits.'); const data = await response.json(); setHabits(data.items || []); } catch (error) { console.error('Failed to fetch habits:', error); setError('Unable to load habits. Please try again.'); } finally { setLoading(false); } } function handleComplete(habit: HabitWithMeta) { if (habit.completion_mode === 'quick') { logHabitCompletion(habit.id, {}); } else { setSelectedHabit(habit); setCompletionDialogOpen(true); } } async function logHabitCompletion( habitId: string, data: { mood?: number; value?: number; notes?: string } ) { try { const response = await fetch(`/api/habits/${habitId}/logs`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (!response.ok) throw new Error('Unable to save habit completion.'); fetchHabits(); setCompletionDialogOpen(false); toast.success('Habit completed.'); } catch (error) { console.error('Failed to log habit:', error); toast.error('Unable to save habit completion. Please try again.'); } } const completedCount = habits.filter((h) => h.logged_today).length; const completionRate = habits.length > 0 ? Math.round((completedCount / habits.length) * 100) : 0; if (loading) { return

Loading habits...

; } return (

Habits

Small actions, visible momentum.

{error && (
{error}
)} {/* Summary banner */}

Today's progress

{completedCount} / {habits.length} habits

Completion rate

{completionRate}%

{/* Habit cards grid */}
{habits.length === 0 && !error ? (

No habits yet. Start with one small action.

) : habits.map((habit) => ( handleComplete(habit)} /> ))}
{/* Heatmap section */} } > {/* Completion dialog */} {selectedHabit && ( logHabitCompletion(selectedHabit.id, data)} /> )}
); }