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
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;