From 94c26c6b5026f8fb52a33648f23436461d6b2604 Mon Sep 17 00:00:00 2001 From: rathi Date: Thu, 21 Nov 2024 18:02:18 -0500 Subject: [PATCH] UI updates and bug fixes --- src/App.tsx | 84 +++++++++------- src/components/HabitList.tsx | 163 +++++++++++++++---------------- src/hooks/useHabits.ts | 182 +++++++++++++++++------------------ src/hooks/useWeek.ts | 42 ++------ src/utils/dateUtils.ts | 47 +++++++++ 5 files changed, 274 insertions(+), 244 deletions(-) create mode 100644 src/utils/dateUtils.ts diff --git a/src/App.tsx b/src/App.tsx index 6e784b5..05f25bc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -30,7 +30,8 @@ function HabitTrackerContent() { addHabit: addHabitApi, toggleHabit, updateHabit, - deleteHabit + deleteHabit, + error } = useHabits(); const { @@ -88,26 +89,23 @@ function HabitTrackerContent() { const renderHabitsView = () => (
-
- setNewHabit(e.target.value)} - onKeyPress={(e) => { - if (e.key === 'Enter' && newHabit.trim()) { - handleAddHabit(e); - } - }} - placeholder="Add a new habit" - className={`flex-1 px-4 py-2 rounded-lg ${theme.input}`} - /> - +
+
+ setNewHabit(e.target.value)} + placeholder="Add a new habit" + className={`flex-1 px-4 py-2 rounded-lg ${theme.input}`} + /> + +
@@ -124,9 +122,7 @@ function HabitTrackerContent() {
- -

- Keep up the good work! Consistency is key. -

+ {loading ? ( +
+
+
+ ) : error ? ( +
+ {error} + +
+ ) : ( + <> + +

+ Keep up the good work! Consistency is key. +

+ + )}
); diff --git a/src/components/HabitList.tsx b/src/components/HabitList.tsx index f786f9d..bbad450 100644 --- a/src/components/HabitList.tsx +++ b/src/components/HabitList.tsx @@ -3,15 +3,15 @@ import { Trash2 } from 'lucide-react'; import { Habit } from '../types'; import { useThemeContext } from '../contexts/ThemeContext'; import { calculateStreak } from '../utils/streakCalculator'; +import { getDayIndex } from '../utils/dateUtils'; interface HabitListProps { habits: Habit[]; currentWeek: string[]; daysOfWeek: string[]; - onToggleHabit: (id: number, date: string) => void; - onUpdateHabit: (id: number, name: string) => void; - onDeleteHabit: (id: number) => void; - onUpdateStreak?: (id: number, newStreak: number) => Promise; + onToggleHabit: (id: number, date: string) => Promise; + onUpdateHabit: (id: number, name: string) => Promise; + onDeleteHabit: (id: number) => Promise; } export function HabitList({ @@ -22,21 +22,18 @@ export function HabitList({ onUpdateHabit, onDeleteHabit, }: HabitListProps) { - const { habitSort, showStreaks } = useThemeContext(); + const { theme, habitSort, showStreaks } = useThemeContext(); const sortedHabits = useMemo(() => { if (habitSort === 'alphabetical') { return [...habits].sort((a, b) => a.name.localeCompare(b.name)); } - // Default to dateCreated sort return habits; }, [habits, habitSort]); - // Helper function to get day name const getDayName = (dateStr: string) => { const date = new Date(dateStr); - const dayIndex = date.getDay() === 0 ? 6 : date.getDay() - 1; - return daysOfWeek[dayIndex]; + return daysOfWeek[getDayIndex(date)]; }; return ( @@ -45,84 +42,88 @@ export function HabitList({ Habit - {currentWeek.map((dateStr) => { - const date = new Date(dateStr); - return ( - -
{getDayName(dateStr)}
-
{getDayName(dateStr).slice(0, 1)}
-
- {date.getDate()} -
- - ); - })} + {currentWeek.map((dateStr) => ( + +
{getDayName(dateStr)}
+
{getDayName(dateStr).slice(0, 1)}
+
+ {new Date(dateStr).getDate()} +
+ + ))} + {showStreaks && ( + Streak + )} Actions - {sortedHabits.map((habit) => ( - - - onUpdateHabit(habit.id, e.target.value)} - className="bg-transparent border-none focus:outline-none focus:ring-2 focus:ring-gray-300 rounded px-2 w-full" - /> - - {currentWeek.map((date) => ( - - + {sortedHabits.map((habit) => { + const { currentStreak } = calculateStreak(habit.completedDates); + + return ( + + + onUpdateHabit(habit.id, e.target.value)} + className="bg-transparent border-none focus:outline-none focus:ring-2 focus:ring-gray-300 rounded px-2 w-full" + /> - ))} - - - - {showStreaks && ( - <> - - {/* ... streak content ... */} + {currentWeek.map((date) => ( + + - - )} - - ))} + ))} + {showStreaks && ( + + + {currentStreak} days + + + )} + + + + + ); + })} diff --git a/src/hooks/useHabits.ts b/src/hooks/useHabits.ts index b98f6f6..26824f3 100644 --- a/src/hooks/useHabits.ts +++ b/src/hooks/useHabits.ts @@ -3,27 +3,34 @@ import { supabase } from '../lib/supabase'; import { Habit } from '../types'; import { calculateStreak } from '../utils/streakCalculator'; import { useAuth } from '../contexts/AuthContext'; +import { formatDate } from '../utils/dateUtils'; export const useHabits = () => { const [habits, setHabits] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); const { user } = useAuth(); - // Automatically fetch habits when user changes useEffect(() => { if (user) { fetchHabits(); } else { - setHabits([]); // Clear habits when user logs out + setHabits([]); + setLoading(false); } - }, [user?.id]); // Depend on user.id to prevent unnecessary rerenders + }, [user?.id]); const fetchHabits = async () => { if (!user) { setHabits([]); + setLoading(false); return; } try { + setLoading(true); + setError(null); + const { data, error } = await supabase .from('habits') .select(` @@ -40,7 +47,7 @@ export const useHabits = () => { if (error) throw error; - const formattedHabits = (data || []).map(habit => ({ + const formattedHabits = data.map(habit => ({ id: habit.id, name: habit.name, created_at: habit.created_at, @@ -51,157 +58,144 @@ export const useHabits = () => { })); setHabits(formattedHabits); - } catch (error) { - console.error('Error fetching habits:', error); - setHabits([]); + } catch (err) { + console.error('Error fetching habits:', err); + setError(err instanceof Error ? err.message : 'Failed to fetch habits'); + } finally { + setLoading(false); } }; - const addHabit = async (name: string) => { - if (!user) return false; + const addHabit = async (name: string): Promise => { + if (!user || !name.trim()) return false; try { const { data, error } = await supabase .from('habits') .insert([{ - name, - best_streak: 0, + name: name.trim(), + user_id: user.id, created_at: new Date().toISOString(), - user_id: user.id + best_streak: 0 }]) - .select('id') + .select() .single(); if (error) throw error; - + await fetchHabits(); return true; - } catch (error) { - console.error('Error adding habit:', error); + } catch (err) { + console.error('Error adding habit:', err); + setError(err instanceof Error ? err.message : 'Failed to add habit'); return false; } }; - const toggleHabit = async (id: number, date: string) => { + const toggleHabit = async (id: number, date: string): Promise => { if (!user) return; try { - // First verify this habit belongs to the user - const { data: habitData, error: habitError } = await supabase - .from('habits') - .select('id') - .eq('id', id) - .eq('user_id', user.id) - .single(); + const isCompleted = habits + .find(h => h.id === id) + ?.completedDates.includes(date); - if (habitError || !habitData) { - throw new Error('Unauthorized access to habit'); - } - - // Keep the original date string without any timezone conversion - const formattedDate = date; - - console.log('Toggling habit:', { id, date, formattedDate }); - - // Check for existing completion - const { data: existing, error: existingError } = await supabase - .from('habit_completions') - .select('*') - .eq('habit_id', id) - .eq('completion_date', formattedDate) - .eq('user_id', user.id) - .single(); - - if (existingError && existingError.code !== 'PGRST116') { - throw existingError; - } - - if (existing) { - const { error: deleteError } = await supabase + if (isCompleted) { + // Remove completion + const { error } = await supabase .from('habit_completions') .delete() .eq('habit_id', id) - .eq('completion_date', formattedDate) - .eq('user_id', user.id); + .eq('completion_date', date); - if (deleteError) throw deleteError; + if (error) throw error; } else { - const { error: insertError } = await supabase + // Add completion + const { error } = await supabase .from('habit_completions') - .insert([{ - habit_id: id, - completion_date: formattedDate, + .insert({ + habit_id: id, + completion_date: date, user_id: user.id - }]); + }); - if (insertError) throw insertError; + if (error) throw error; } + // Update local state optimistically + setHabits(prevHabits => + prevHabits.map(h => { + if (h.id !== id) return h; + return { + ...h, + completedDates: isCompleted + ? h.completedDates.filter(d => d !== date) + : [...h.completedDates, date] + }; + }) + ); + + // 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'); await fetchHabits(); - } catch (error) { - console.error('Error toggling habit:', error); - throw error; } }; - const updateHabit = async (id: number, name: string) => { - if (!user) return; + const updateHabit = async (id: number, name: string): Promise => { + if (!user || !name.trim()) return; try { - // First verify this habit belongs to the user - const { data: habitData, error: habitError } = await supabase + const { error } = await supabase .from('habits') - .select('id') - .eq('id', id) - .eq('user_id', user.id) - .single(); - - if (habitError || !habitData) { - throw new Error('Unauthorized access to habit'); - } - - await supabase - .from('habits') - .update({ name }) + .update({ name: name.trim() }) .eq('id', id) .eq('user_id', user.id); + if (error) throw error; + + // Update local state optimistically + setHabits(prevHabits => + prevHabits.map(h => + h.id === id ? { ...h, name: name.trim() } : h + ) + ); + } catch (err) { + console.error('Error updating habit:', err); + setError(err instanceof Error ? err.message : 'Failed to update habit'); + // Revert optimistic update on error await fetchHabits(); - } catch (error) { - console.error('Error updating habit:', error); } }; - const deleteHabit = async (id: number) => { + const deleteHabit = async (id: number): Promise => { if (!user) return; try { - // First verify this habit belongs to the user - const { data: habitData, error: habitError } = await supabase - .from('habits') - .select('id') - .eq('id', id) - .eq('user_id', user.id) - .single(); - - if (habitError || !habitData) { - throw new Error('Unauthorized access to habit'); - } - - await supabase + const { error } = await supabase .from('habits') .delete() .eq('id', id) .eq('user_id', user.id); + if (error) throw error; + + // Update local state optimistically + setHabits(prevHabits => prevHabits.filter(h => h.id !== id)); + } catch (err) { + console.error('Error deleting habit:', err); + setError(err instanceof Error ? err.message : 'Failed to delete habit'); + // Revert optimistic update on error await fetchHabits(); - } catch (error) { - console.error('Error deleting habit:', error); } }; return { habits, + loading, + error, fetchHabits, addHabit, toggleHabit, diff --git a/src/hooks/useWeek.ts b/src/hooks/useWeek.ts index cdc5855..7e954aa 100644 --- a/src/hooks/useWeek.ts +++ b/src/hooks/useWeek.ts @@ -1,43 +1,17 @@ import { useState } from 'react'; +import { getWeekDates, formatDate } from '../utils/dateUtils'; export const useWeek = () => { - const [currentWeek, setCurrentWeek] = useState([]); + const [currentWeek, setCurrentWeek] = useState(getWeekDates()); - const getCurrentWeekDates = () => { - const now = new Date(); - const currentDay = now.getDay(); - const diff = currentDay === 0 ? -6 : 1 - currentDay; - - const monday = new Date(now); - monday.setDate(now.getDate() + diff); - monday.setHours(0, 0, 0, 0); - - return Array.from({ length: 7 }, (_, i) => { - const date = new Date(monday); - date.setDate(monday.getDate() + i); - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - return `${year}-${month}-${day}`; - }); - }; + const getCurrentWeekDates = () => getWeekDates(); const changeWeek = (direction: 'prev' | 'next') => { - if (currentWeek.length === 0) return; - - const firstDay = new Date(currentWeek[0]); - firstDay.setHours(0, 0, 0, 0); - const newFirstDay = new Date(firstDay); - newFirstDay.setDate(firstDay.getDate() + (direction === 'prev' ? -7 : 7)); - - setCurrentWeek(Array.from({ length: 7 }, (_, i) => { - const date = new Date(newFirstDay); - date.setDate(newFirstDay.getDate() + i); - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); - return `${year}-${month}-${day}`; - })); + setCurrentWeek(prevWeek => { + const firstDay = new Date(prevWeek[0]); + firstDay.setDate(firstDay.getDate() + (direction === 'prev' ? -7 : 7)); + return getWeekDates(firstDay); + }); }; return { diff --git a/src/utils/dateUtils.ts b/src/utils/dateUtils.ts new file mode 100644 index 0000000..ce7dc36 --- /dev/null +++ b/src/utils/dateUtils.ts @@ -0,0 +1,47 @@ +// Simple date formatting without timezone complications +export const formatDate = (date: Date): string => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + 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++) { + const date = new Date(current); + 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 ( + date1.getDate() === date2.getDate() && + date1.getMonth() === date2.getMonth() && + date1.getFullYear() === date2.getFullYear() + ); + }; \ No newline at end of file