import React, { useState, useEffect } from 'react'; import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'; import { useNavigate } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { Plus, Settings, TrendingUp, Flame, Calendar, Moon, Sun } from 'lucide-react'; import { Button } from '../components/ui/button'; import { useToast } from '../components/ui/use-toast'; import HabitCard from '../components/HabitCard'; import AnimatedCounter from '../components/AnimatedCounter'; import GitActivityGrid from '../components/GitActivityGrid'; import { getGitEnabled } from '../lib/git'; import { getHabits, updateHabit, syncLocalToRemoteIfNeeded, syncRemoteToLocal, getAuthUser } from '../lib/datastore'; const HomePage = () => { const navigate = useNavigate(); const { toast } = useToast(); const [habits, setHabits] = useState([]); const [collapsedGroups, setCollapsedGroups] = useState({}); const [gitEnabled, setGitEnabled] = useState(getGitEnabled()); const [darkMode, setDarkMode] = useState(() => { return localStorage.getItem('theme') === 'dark'; }); useEffect(() => { (async () => { // On login, pull remote habits into localStorage const user = await getAuthUser(); if (user) { await syncRemoteToLocal(); } await loadHabits(); setGitEnabled(getGitEnabled()); })(); // Background sync every 10s if logged in const interval = setInterval(() => { syncLocalToRemoteIfNeeded(); }, 10000); // Listen for remote sync event to reload habits const syncListener = () => loadHabits(); window.addEventListener('habitgrid-sync-updated', syncListener); return () => { clearInterval(interval); window.removeEventListener('habitgrid-sync-updated', syncListener); }; }, []); useEffect(() => { if (darkMode) { document.documentElement.classList.add('dark'); localStorage.setItem('theme', 'dark'); } else { document.documentElement.classList.remove('dark'); localStorage.setItem('theme', 'light'); } }, [darkMode]); const loadHabits = async () => { // Always read from local for instant UI const loadedHabits = JSON.parse(localStorage.getItem('habitgrid_data') || '[]'); loadedHabits.sort((a, b) => { if (a.sortOrder !== undefined && b.sortOrder !== undefined) return a.sortOrder - b.sortOrder; if (a.sortOrder !== undefined) return -1; if (b.sortOrder !== undefined) return 1; return new Date(a.createdAt || 0) - new Date(b.createdAt || 0); }); setHabits(loadedHabits); // Initialize collapsed state for new categories const categories = Array.from(new Set(loadedHabits.map(h => h.category || 'Uncategorized'))); setCollapsedGroups(prev => { const next = { ...prev }; categories.forEach(cat => { if (!(cat in next)) next[cat] = false; }); return next; }); }; const handleAddHabit = () => { navigate('/add'); }; return (
{/* Header */}
HabitGrid Logo

HabitGrid

Commit to yourself, one square at a time

{/* Stats Overview */} {habits.length > 0 && (
Active Habits

{habits.length}

Total Streaks

sum + (h.currentStreak || 0), 0)} duration={900} />

)} {/* Git Activity */} {gitEnabled && ( )} {/* Habits List */} {/* Grouped Habits by Category, collapsible, and uncategorized habits outside */} { if (!result.destination) return; const { source, destination } = result; // Get all habits grouped by category const uncategorized = habits.filter(h => !h.category); const categorized = habits.filter(h => h.category); const grouped = categorized.reduce((acc, habit) => { const cat = habit.category; if (!acc[cat]) acc[cat] = []; acc[cat].push(habit); return acc; }, {}); let newHabits = [...habits]; // If dropping into uncategorized, always unset category if (destination.droppableId === 'uncategorized') { let items, removed; if (source.droppableId === 'uncategorized') { // Reorder within uncategorized items = Array.from(uncategorized); [removed] = items.splice(source.index, 1); } else { // Move from category to uncategorized items = Array.from(uncategorized); const sourceItems = Array.from(grouped[source.droppableId]); [removed] = sourceItems.splice(source.index, 1); removed.category = ''; grouped[source.droppableId] = sourceItems; } // Always set category to '' removed.category = ''; items.splice(destination.index, 0, removed); items.forEach((h, i) => updateHabit(h.id, { sortOrder: i, category: '' })); newHabits = [ ...items, ...Object.values(grouped).flat() ]; } else if (source.droppableId === 'uncategorized' && grouped[destination.droppableId]) { // Move from uncategorized to category const items = Array.from(uncategorized); const [removed] = items.splice(source.index, 1); removed.category = destination.droppableId; const destItems = Array.from(grouped[destination.droppableId] || []); destItems.splice(destination.index, 0, removed); destItems.forEach((h, i) => updateHabit(h.id, { sortOrder: i, category: h.category })); newHabits = [ ...items, ...Object.values({ ...grouped, [destination.droppableId]: destItems }).flat() ]; } else if (grouped[source.droppableId] && grouped[destination.droppableId]) { // Move within or between categories const sourceItems = Array.from(grouped[source.droppableId]); const [removed] = sourceItems.splice(source.index, 1); if (source.droppableId === destination.droppableId) { // Reorder within same category sourceItems.splice(destination.index, 0, removed); sourceItems.forEach((h, i) => updateHabit(h.id, { sortOrder: i, category: h.category })); grouped[source.droppableId] = sourceItems; } else { // Move to another category const destItems = Array.from(grouped[destination.droppableId] || []); removed.category = destination.droppableId; destItems.splice(destination.index, 0, removed); destItems.forEach((h, i) => updateHabit(h.id, { sortOrder: i, category: h.category })); grouped[source.droppableId] = sourceItems; grouped[destination.droppableId] = destItems; } // Flatten newHabits = [ ...uncategorized, ...Object.values(grouped).flat() ]; } setTimeout(loadHabits, 0); // reload instantly after update }} >
{/* Uncategorized habits (no group panel) */} {(provided) => (
{habits.filter(h => !h.category).map((habit, index) => ( {(provided, snapshot) => (
)}
))} {provided.placeholder}
)}
{/* Group panels for named categories */} {Object.entries( habits.filter(h => h.category).reduce((acc, habit) => { const cat = habit.category; if (!acc[cat]) acc[cat] = []; acc[cat].push(habit); return acc; }, {}) ).map(([category, groupHabits], groupIdx) => (
{!collapsedGroups[category] && ( {(provided) => (
{groupHabits.map((habit, index) => ( {(provided, snapshot) => (
)}
))} {provided.placeholder}
)}
)}
))}
{/* Empty State */} {habits.length === 0 && (

Create your grid!

Create your first habit and watch your progress every day as you fill in the squares. Small steps lead to big changes!

)} {/* Add Button */} {habits.length > 0 && ( )}
); }; export default HomePage;