import React, { useState, useEffect } from 'react'; import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'; // ...existing code... import { motion, AnimatePresence } from 'framer-motion'; import { Plus, Settings, TrendingUp, Flame, Calendar, Moon, Sun, Star } 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, hasEverLoggedIn } from '../lib/datastore'; import { useNavigate } from 'react-router-dom'; const HomePage = () => { const navigate = useNavigate(); const { toast } = useToast(); const [habits, setHabits] = useState([]); const [loading, setLoading] = useState(true); const [loggedIn, setLoggedIn] = useState(false); const [collapsedGroups, setCollapsedGroups] = useState({}); const [everLoggedIn, setEverLoggedIn] = useState(false); const [gitEnabled, setGitEnabled] = useState(getGitEnabled()); const [darkMode, setDarkMode] = useState(() => { return localStorage.getItem('theme') === 'dark'; }); useEffect(() => { (async () => { setLoading(true); // On login, pull remote habits into localStorage const user = await getAuthUser(); setLoggedIn(!!user); // Mark whether this browser has seen a login before try { setEverLoggedIn(hasEverLoggedIn()); } catch (e) { setEverLoggedIn(false); } if (user) { await syncRemoteToLocal(); } await loadHabits(); setGitEnabled(getGitEnabled()); setLoading(false); })(); // 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'); }; const handleLoginSync = () => { navigate('/login-providers'); }; const handleManualSync = async () => { await syncLocalToRemoteIfNeeded(); toast({ title: 'Synced!', description: 'Your habits have been synced to the cloud.' }); }; return (
{/* Header */}
HabitGrid Logo

HabitGrid

Commit to yourself, one square at a time

{/* Prompt previously-signed-in users to re-authenticate if currently logged out */} {!loggedIn && everLoggedIn && (
Looks like you were previously signed in. Sign in again to keep your habits synced across devices.
)} {/* 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]; // Helper to update local storage and UI instantly const updateLocalOrder = (habitsArr) => { localStorage.setItem('habitgrid_data', JSON.stringify(habitsArr)); setHabits(habitsArr); }; // Collect async remote updates to fire after local update let remoteUpdates = []; if (destination.droppableId === 'uncategorized') { let items, removed; if (source.droppableId === 'uncategorized') { items = Array.from(uncategorized); [removed] = items.splice(source.index, 1); } else { items = Array.from(uncategorized); const sourceItems = Array.from(grouped[source.droppableId]); [removed] = sourceItems.splice(source.index, 1); removed.category = ''; grouped[source.droppableId] = sourceItems; } removed.category = ''; items.splice(destination.index, 0, removed); items.forEach((h, i) => { h.sortOrder = i; h.category = ''; remoteUpdates.push(updateHabit(h.id, { sortOrder: i, category: '' })); }); newHabits = [ ...items, ...Object.values(grouped).flat() ]; updateLocalOrder(newHabits); } else if (source.droppableId === 'uncategorized' && grouped[destination.droppableId]) { 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) => { h.sortOrder = i; remoteUpdates.push(updateHabit(h.id, { sortOrder: i, category: h.category })); }); newHabits = [ ...items, ...Object.values({ ...grouped, [destination.droppableId]: destItems }).flat() ]; updateLocalOrder(newHabits); } else if (grouped[source.droppableId] && grouped[destination.droppableId]) { const sourceItems = Array.from(grouped[source.droppableId]); const [removed] = sourceItems.splice(source.index, 1); if (source.droppableId === destination.droppableId) { sourceItems.splice(destination.index, 0, removed); sourceItems.forEach((h, i) => { h.sortOrder = i; remoteUpdates.push(updateHabit(h.id, { sortOrder: i, category: h.category })); }); grouped[source.droppableId] = sourceItems; } else { const destItems = Array.from(grouped[destination.droppableId] || []); removed.category = destination.droppableId; destItems.splice(destination.index, 0, removed); destItems.forEach((h, i) => { h.sortOrder = i; remoteUpdates.push(updateHabit(h.id, { sortOrder: i, category: h.category })); }); grouped[source.droppableId] = sourceItems; grouped[destination.droppableId] = destItems; } newHabits = [ ...uncategorized, ...Object.values(grouped).flat() ]; updateLocalOrder(newHabits); } // Fire remote updates async, do not block UI Promise.allSettled(remoteUpdates); }} >
{/* 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 or Loading Buffer */} {habits.length === 0 && ( loading && loggedIn ? (

Loading your habits...

) : (

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!

{/* Call to Action for Login/Sync */}
{!loggedIn ? ( ) : ( )}
) )} {/* Add Button */} {habits.length > 0 && ( )}
); }; export default HomePage;