diff --git a/src/components/Calendar.tsx b/src/components/Calendar.tsx index 879e587..4de0509 100644 --- a/src/components/Calendar.tsx +++ b/src/components/Calendar.tsx @@ -39,7 +39,7 @@ export const Calendar: React.FC = ({ }) => { const { theme } = useThemeContext(); const isMobile = useIsMobile(); - + const daysOfWeek = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; const getFirstDayOfMonth = (year: number, month: number) => { @@ -81,7 +81,7 @@ export const Calendar: React.FC = ({ if (hideTimeoutRef.current) { clearTimeout(hideTimeoutRef.current); } - + const rect = e.currentTarget.getBoundingClientRect(); setTooltipData({ x: rect.left + rect.width / 2, @@ -118,10 +118,49 @@ export const Calendar: React.FC = ({ }; }, []); + // Update selected date data when habits change + const [selectedDate, setSelectedDate] = React.useState<{ + date: string; + completedHabits: Habit[]; + incompleteHabits: Habit[]; + } | null>(null); + + // Update the selected date data whenever habits change or a habit is toggled + React.useEffect(() => { + if (selectedDate) { + setSelectedDate({ + date: selectedDate.date, + completedHabits: getCompletedHabitsForDate(selectedDate.date), + incompleteHabits: habits.filter(habit => + !getCompletedHabitsForDate(selectedDate.date) + .map(h => h.id) + .includes(habit.id) + ) + }); + } + }, [habits, getCompletedHabitsForDate]); + + // Modified habit toggle handler for mobile view + const handleMobileHabitToggle = async (e: React.MouseEvent, habitId: number, date: string) => { + e.stopPropagation(); + await onToggleHabit(habitId, date); + + // Update the selected date data immediately after toggling + setSelectedDate({ + date, + completedHabits: getCompletedHabitsForDate(date), + incompleteHabits: habits.filter(habit => + !getCompletedHabitsForDate(date) + .map(h => h.id) + .includes(habit.id) + ) + }); + }; + return ( <>
@@ -152,21 +191,21 @@ export const Calendar: React.FC = ({
{daysOfWeek.map(day => (
{isMobile ? day.charAt(0) : day}
))} - + {(() => { const year = currentMonth.getFullYear(); const month = currentMonth.getMonth(); const firstDayOfMonth = getFirstDayOfMonth(year, month); const daysInMonth = getDaysInMonth(year, month); const daysInPrevMonth = getDaysInMonth(year, month - 1); - + const days = []; // Previous month days @@ -205,20 +244,29 @@ export const Calendar: React.FC = ({ const completedHabits = getCompletedHabitsForDate(date); const incompleteHabits = habits.filter(habit => !habit.completedDates.includes(date)); const isToday = date === todayStr; - + return (
{ + if (isMobile) { + setSelectedDate({ + date, + completedHabits, + incompleteHabits + }); + } + }} className={` border rounded-lg relative ${theme.border} ${isCurrentMonth ? theme.calendar.day.default : theme.calendar.day.otherMonth} ${isToday ? theme.calendar.day.today : ''} - ${isMobile ? 'p-1 min-h-[60px]' : 'p-3 min-h-[80px]'} + ${isMobile ? 'p-1 min-h-[60px] active:bg-gray-100 dark:active:bg-gray-800' : 'p-3 min-h-[80px]'} `} > = ({ absolute bottom-1 left-1/2 transform -translate-x-1/2 ${isMobile ? 'w-full px-1' : ''} `}> -
showTooltip(e, date, completedHabits, incompleteHabits), @@ -241,8 +289,8 @@ export const Calendar: React.FC = ({
0 - ? 'text-green-700 dark:text-green-300' + ${completedHabits.length > 0 + ? 'text-green-700 dark:text-green-300' : `${theme.text} opacity-75` } `}> @@ -257,15 +305,15 @@ export const Calendar: React.FC = ({
0 - ? 'bg-green-100 dark:bg-green-900/30 shadow-[0_2px_10px] shadow-green-900/20 dark:shadow-green-100/20' + ${completedHabits.length > 0 + ? 'bg-green-100 dark:bg-green-900/30 shadow-[0_2px_10px] shadow-green-900/20 dark:shadow-green-100/20' : `bg-gray-100 dark:bg-gray-800 shadow-sm` } `}> 0 - ? 'text-green-700 dark:text-green-300' + ${completedHabits.length > 0 + ? 'text-green-700 dark:text-green-300' : 'text-gray-600 dark:text-gray-400' } `}> @@ -284,7 +332,81 @@ export const Calendar: React.FC = ({
- {/* Tooltip portal - only render on desktop */} + {/* Mobile Selected Date Details */} + {isMobile && selectedDate && ( +
+
+

+ {new Date(selectedDate.date).toLocaleDateString('default', { + month: 'long', + day: 'numeric', + year: 'numeric' + })} +

+
+ + {selectedDate.completedHabits.length > 0 && ( +
+ + ✓ Completed + +
    + {selectedDate.completedHabits.map(habit => ( +
  • + {habit.name} + +
  • + ))} +
+
+ )} + + {selectedDate.incompleteHabits.length > 0 && ( +
+ + ○ Pending + +
    + {selectedDate.incompleteHabits.map(habit => ( +
  • + {habit.name} + +
  • + ))} +
+
+ )} + + {selectedDate.completedHabits.length === 0 && selectedDate.incompleteHabits.length === 0 && ( +

+ No habits tracked for this date +

+ )} +
+ )} + + {/* Desktop Tooltip Portal - unchanged */} {!isMobile && tooltipData && createPortal(
= ({ className={` fixed transition-all duration-150 ease-in-out - ${tooltipData.isVisible - ? 'opacity-100 translate-y-0' + ${tooltipData.isVisible + ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-1' } `} @@ -306,7 +428,7 @@ export const Calendar: React.FC = ({ zIndex: 100, }} > -
= ({ `} > {/* Updated Arrow */} -
= ({ border-t-0 border-l-0 `} /> - +
{tooltipData.completedHabits.length > 0 && (
@@ -339,8 +461,8 @@ export const Calendar: React.FC = ({
    {tooltipData.completedHabits.map(habit => ( -
  • {habit.name} @@ -362,8 +484,8 @@ export const Calendar: React.FC = ({
      {tooltipData.incompleteHabits.map(habit => ( -
    • {habit.name} @@ -385,4 +507,4 @@ export const Calendar: React.FC = ({ )} ); -}; \ No newline at end of file +}; diff --git a/src/components/HabitList.tsx b/src/components/HabitList.tsx index bbad450..8c9c4d4 100644 --- a/src/components/HabitList.tsx +++ b/src/components/HabitList.tsx @@ -60,7 +60,7 @@ export function HabitList({ {sortedHabits.map((habit) => { const { currentStreak } = calculateStreak(habit.completedDates); - + return ( diff --git a/src/components/SignUp.tsx b/src/components/SignUp.tsx index fc318d5..730a423 100644 --- a/src/components/SignUp.tsx +++ b/src/components/SignUp.tsx @@ -36,7 +36,7 @@ export function SignUp({ onSwitchToLogin }: { onSwitchToLogin: () => void }) { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - + // Validate password before submission const passwordValidationError = validatePassword(password); if (passwordValidationError) { @@ -48,8 +48,12 @@ export function SignUp({ onSwitchToLogin }: { onSwitchToLogin: () => void }) { setError(''); setPasswordError(null); await signUp(email, password); - } catch (err) { - setError('Failed to create an account'); + } catch (err: unknown) { + // Handle the error message + const errorMessage = err instanceof Error + ? err.message + : 'Failed to create an account'; + setError(errorMessage); } }; @@ -68,16 +72,16 @@ export function SignUp({ onSwitchToLogin }: { onSwitchToLogin: () => void }) {
      - @@ -103,30 +107,30 @@ export function SignUp({ onSwitchToLogin }: { onSwitchToLogin: () => void }) {
      - - @@ -134,7 +138,7 @@ export function SignUp({ onSwitchToLogin }: { onSwitchToLogin: () => void }) {
      -

      Sign Up

      {error && ( @@ -149,7 +153,7 @@ export function SignUp({ onSwitchToLogin }: { onSwitchToLogin: () => void }) { value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" - className={`w-full px-4 py-3 rounded-xl ${theme.input} transition-all duration-200 + className={`w-full px-4 py-3 rounded-xl ${theme.input} transition-all duration-200 focus:ring-2 focus:ring-black dark:focus:ring-white border-gray-200 dark:border-gray-700`} required /> @@ -160,8 +164,8 @@ export function SignUp({ onSwitchToLogin }: { onSwitchToLogin: () => void }) { value={password} onChange={handlePasswordChange} placeholder="Password" - className={`w-full px-4 py-3 rounded-xl ${theme.input} transition-all duration-200 - focus:ring-2 focus:ring-black dark:focus:ring-white + className={`w-full px-4 py-3 rounded-xl ${theme.input} transition-all duration-200 + focus:ring-2 focus:ring-black dark:focus:ring-white ${passwordError ? 'border-red-500' : 'border-gray-200 dark:border-gray-700'}`} required /> @@ -182,7 +186,7 @@ export function SignUp({ onSwitchToLogin }: { onSwitchToLogin: () => void }) {
      ); -} \ No newline at end of file +} diff --git a/src/hooks/useHabits.ts b/src/hooks/useHabits.ts index 26824f3..27e6b7a 100644 --- a/src/hooks/useHabits.ts +++ b/src/hooks/useHabits.ts @@ -11,53 +11,43 @@ export const useHabits = () => { const [error, setError] = useState(null); const { user } = useAuth(); - useEffect(() => { - if (user) { - fetchHabits(); - } else { - setHabits([]); - setLoading(false); - } - }, [user?.id]); - const fetchHabits = async () => { if (!user) { setHabits([]); setLoading(false); return; } - + try { setLoading(true); setError(null); - - const { data, error } = await supabase + + // Fetch habits + const { data: habitsData, error: habitsError } = await supabase .from('habits') - .select(` - id, - name, - created_at, - best_streak, - habit_completions ( - completion_date - ) - `) + .select('*') .eq('user_id', user.id) - .order('created_at', { ascending: false }); + .order('created_at', { ascending: true }); - if (error) throw error; + if (habitsError) throw habitsError; - const formattedHabits = data.map(habit => ({ - id: habit.id, - name: habit.name, - created_at: habit.created_at, - best_streak: habit.best_streak, - completedDates: habit.habit_completions?.map( - (completion: { completion_date: string }) => completion.completion_date - ) || [] + // Fetch all completions for user's habits + const { data: completionsData, error: completionsError } = await supabase + .from('habit_completions') + .select('*') + .eq('user_id', user.id); + + if (completionsError) throw completionsError; + + // Combine habits with their completions + const habitsWithCompletions = habitsData.map(habit => ({ + ...habit, + completedDates: completionsData + .filter(completion => completion.habit_id === habit.id) + .map(completion => completion.completion_date) })); - setHabits(formattedHabits); + setHabits(habitsWithCompletions); } catch (err) { console.error('Error fetching habits:', err); setError(err instanceof Error ? err.message : 'Failed to fetch habits'); @@ -66,20 +56,67 @@ export const useHabits = () => { } }; + // Set up real-time subscription for habits and completions + useEffect(() => { + if (!user) return; + + // Subscribe to habits changes + const habitsSubscription = supabase + .channel('habits-changes') + .on( + 'postgres_changes', + { + event: '*', + schema: 'public', + table: 'habits', + filter: `user_id=eq.${user.id}` + }, + () => { + fetchHabits(); + } + ) + .subscribe(); + + // Subscribe to habit completions changes + const completionsSubscription = supabase + .channel('completions-changes') + .on( + 'postgres_changes', + { + event: '*', + schema: 'public', + table: 'habit_completions', + filter: `user_id=eq.${user.id}` + }, + () => { + fetchHabits(); + } + ) + .subscribe(); + + // Initial fetch + fetchHabits(); + + // Cleanup subscriptions + return () => { + habitsSubscription.unsubscribe(); + completionsSubscription.unsubscribe(); + }; + }, [user]); + const addHabit = async (name: string): Promise => { if (!user || !name.trim()) return false; - + try { - const { data, error } = await supabase + const { error } = await supabase .from('habits') - .insert([{ - name: name.trim(), + .insert([{ + name: name.trim(), user_id: user.id, created_at: new Date().toISOString(), best_streak: 0 }]) - .select() - .single(); + .select(); if (error) throw error; @@ -94,11 +131,12 @@ export const useHabits = () => { const toggleHabit = async (id: number, date: string): Promise => { if (!user) return; - + try { - const isCompleted = habits - .find(h => h.id === id) - ?.completedDates.includes(date); + const habit = habits.find(h => h.id === id); + if (!habit) throw new Error('Habit not found'); + + const isCompleted = habit.completedDates.includes(date); if (isCompleted) { // Remove completion @@ -106,7 +144,8 @@ export const useHabits = () => { .from('habit_completions') .delete() .eq('habit_id', id) - .eq('completion_date', date); + .eq('completion_date', date) + .eq('user_id', user.id); if (error) throw error; } else { @@ -135,18 +174,17 @@ export const useHabits = () => { }) ); - // Fetch updated data to ensure consistency - await fetchHabits(); } catch (err) { console.error('Error toggling habit:', err); setError(err instanceof Error ? err.message : 'Failed to toggle habit'); + // Refresh habits to ensure consistency await fetchHabits(); } }; const updateHabit = async (id: number, name: string): Promise => { if (!user || !name.trim()) return; - + try { const { error } = await supabase .from('habits') @@ -172,7 +210,7 @@ export const useHabits = () => { const deleteHabit = async (id: number): Promise => { if (!user) return; - + try { const { error } = await supabase .from('habits') @@ -196,10 +234,10 @@ export const useHabits = () => { habits, loading, error, - fetchHabits, addHabit, toggleHabit, updateHabit, - deleteHabit + deleteHabit, + fetchHabits }; -}; \ No newline at end of file +}; diff --git a/src/utils/dateUtils.ts b/src/utils/dateUtils.ts index ce7dc36..5b980f2 100644 --- a/src/utils/dateUtils.ts +++ b/src/utils/dateUtils.ts @@ -5,21 +5,21 @@ export const formatDate = (date: Date): string => { const day = String(date.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }; - + // Get day index (0-6) where Monday is 0 and Sunday is 6 export const getDayIndex = (date: Date): number => { const day = date.getDay(); return day === 0 ? 6 : day - 1; }; - + // Get dates for current week starting from Monday export const getWeekDates = (baseDate: Date = new Date()): string[] => { const current = new Date(baseDate); const dayIndex = getDayIndex(current); - + // Adjust to Monday current.setDate(current.getDate() - dayIndex); - + // Generate dates starting from Monday const dates = []; for (let i = 0; i < 7; i++) { @@ -27,16 +27,16 @@ export const formatDate = (date: Date): string => { date.setDate(current.getDate() + i); dates.push(formatDate(date)); } - + return dates; }; - + // Get the first day of the month adjusted for Monday start export const getFirstDayOfMonth = (year: number, month: number): number => { const date = new Date(year, month, 1); return getDayIndex(date); }; - + // Check if two dates are the same day export const isSameDay = (date1: Date, date2: Date): boolean => { return ( @@ -44,4 +44,4 @@ export const formatDate = (date: Date): string => { date1.getMonth() === date2.getMonth() && date1.getFullYear() === date2.getFullYear() ); - }; \ No newline at end of file + };