UI updates and bug fixes

This commit is contained in:
Harivansh Rathi 2024-11-21 18:02:18 -05:00
parent 20e91dae4c
commit 94c26c6b50
5 changed files with 274 additions and 244 deletions

View file

@ -30,7 +30,8 @@ function HabitTrackerContent() {
addHabit: addHabitApi, addHabit: addHabitApi,
toggleHabit, toggleHabit,
updateHabit, updateHabit,
deleteHabit deleteHabit,
error
} = useHabits(); } = useHabits();
const { const {
@ -88,26 +89,23 @@ function HabitTrackerContent() {
const renderHabitsView = () => ( const renderHabitsView = () => (
<div className="flex-1"> <div className="flex-1">
<div className="max-w-5xl mx-auto"> <div className="max-w-5xl mx-auto">
<div className="mb-8 flex items-center gap-4"> <div className="mb-8">
<form onSubmit={handleAddHabit} className="flex items-center gap-4">
<input <input
type="text" type="text"
value={newHabit} value={newHabit}
onChange={(e) => setNewHabit(e.target.value)} onChange={(e) => setNewHabit(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter' && newHabit.trim()) {
handleAddHabit(e);
}
}}
placeholder="Add a new habit" placeholder="Add a new habit"
className={`flex-1 px-4 py-2 rounded-lg ${theme.input}`} className={`flex-1 px-4 py-2 rounded-lg ${theme.input}`}
/> />
<button <button
onClick={handleAddHabit} type="submit"
disabled={!newHabit.trim()} disabled={!newHabit.trim()}
className={`px-4 py-2 rounded-lg ${theme.button.primary} disabled:opacity-50`} className={`px-4 py-2 rounded-lg ${theme.button.primary} disabled:opacity-50`}
> >
Add Habit Add Habit
</button> </button>
</form>
</div> </div>
<div className="mb-6"> <div className="mb-6">
@ -124,9 +122,7 @@ function HabitTrackerContent() {
<ChevronLeft className="h-5 w-5" /> <ChevronLeft className="h-5 w-5" />
</button> </button>
<button <button
onClick={() => { onClick={goToCurrentWeek}
setCurrentWeek(getCurrentWeekDates());
}}
className={`px-4 py-2 rounded-lg ${theme.button.secondary}`} className={`px-4 py-2 rounded-lg ${theme.button.secondary}`}
> >
Today Today
@ -141,6 +137,22 @@ function HabitTrackerContent() {
</div> </div>
</div> </div>
{loading ? (
<div className="flex justify-center items-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 dark:border-white"></div>
</div>
) : error ? (
<div className="text-red-500 text-center py-4">
{error}
<button
onClick={fetchHabits}
className="ml-2 text-blue-500 hover:underline"
>
Retry
</button>
</div>
) : (
<>
<HabitList <HabitList
habits={getSortedHabits()} habits={getSortedHabits()}
currentWeek={currentWeek} currentWeek={currentWeek}
@ -152,6 +164,8 @@ function HabitTrackerContent() {
<p className={`text-sm ${theme.mutedText} mt-4`}> <p className={`text-sm ${theme.mutedText} mt-4`}>
Keep up the good work! Consistency is key. Keep up the good work! Consistency is key.
</p> </p>
</>
)}
</div> </div>
</div> </div>
); );

View file

@ -3,15 +3,15 @@ import { Trash2 } from 'lucide-react';
import { Habit } from '../types'; import { Habit } from '../types';
import { useThemeContext } from '../contexts/ThemeContext'; import { useThemeContext } from '../contexts/ThemeContext';
import { calculateStreak } from '../utils/streakCalculator'; import { calculateStreak } from '../utils/streakCalculator';
import { getDayIndex } from '../utils/dateUtils';
interface HabitListProps { interface HabitListProps {
habits: Habit[]; habits: Habit[];
currentWeek: string[]; currentWeek: string[];
daysOfWeek: string[]; daysOfWeek: string[];
onToggleHabit: (id: number, date: string) => void; onToggleHabit: (id: number, date: string) => Promise<void>;
onUpdateHabit: (id: number, name: string) => void; onUpdateHabit: (id: number, name: string) => Promise<void>;
onDeleteHabit: (id: number) => void; onDeleteHabit: (id: number) => Promise<void>;
onUpdateStreak?: (id: number, newStreak: number) => Promise<void>;
} }
export function HabitList({ export function HabitList({
@ -22,21 +22,18 @@ export function HabitList({
onUpdateHabit, onUpdateHabit,
onDeleteHabit, onDeleteHabit,
}: HabitListProps) { }: HabitListProps) {
const { habitSort, showStreaks } = useThemeContext(); const { theme, habitSort, showStreaks } = useThemeContext();
const sortedHabits = useMemo(() => { const sortedHabits = useMemo(() => {
if (habitSort === 'alphabetical') { if (habitSort === 'alphabetical') {
return [...habits].sort((a, b) => a.name.localeCompare(b.name)); return [...habits].sort((a, b) => a.name.localeCompare(b.name));
} }
// Default to dateCreated sort
return habits; return habits;
}, [habits, habitSort]); }, [habits, habitSort]);
// Helper function to get day name
const getDayName = (dateStr: string) => { const getDayName = (dateStr: string) => {
const date = new Date(dateStr); const date = new Date(dateStr);
const dayIndex = date.getDay() === 0 ? 6 : date.getDay() - 1; return daysOfWeek[getDayIndex(date)];
return daysOfWeek[dayIndex];
}; };
return ( return (
@ -45,23 +42,26 @@ export function HabitList({
<thead> <thead>
<tr> <tr>
<th className="text-left px-4 py-2 dark:text-white">Habit</th> <th className="text-left px-4 py-2 dark:text-white">Habit</th>
{currentWeek.map((dateStr) => { {currentWeek.map((dateStr) => (
const date = new Date(dateStr);
return (
<th key={dateStr} className="px-4 py-2 text-center dark:text-white"> <th key={dateStr} className="px-4 py-2 text-center dark:text-white">
<div className="hidden md:block">{getDayName(dateStr)}</div> <div className="hidden md:block">{getDayName(dateStr)}</div>
<div className="md:hidden">{getDayName(dateStr).slice(0, 1)}</div> <div className="md:hidden">{getDayName(dateStr).slice(0, 1)}</div>
<div className="text-xs text-gray-500 dark:text-gray-400"> <div className="text-xs text-gray-500 dark:text-gray-400">
{date.getDate()} {new Date(dateStr).getDate()}
</div> </div>
</th> </th>
); ))}
})} {showStreaks && (
<th className="px-4 py-2 text-center dark:text-white">Streak</th>
)}
<th className="px-4 py-2 text-center dark:text-white">Actions</th> <th className="px-4 py-2 text-center dark:text-white">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{sortedHabits.map((habit) => ( {sortedHabits.map((habit) => {
const { currentStreak } = calculateStreak(habit.completedDates);
return (
<tr key={habit.id} className="border-t dark:border-gray-700"> <tr key={habit.id} className="border-t dark:border-gray-700">
<td className="px-4 py-2 dark:text-white"> <td className="px-4 py-2 dark:text-white">
<input <input
@ -106,6 +106,13 @@ export function HabitList({
</label> </label>
</td> </td>
))} ))}
{showStreaks && (
<td className="px-4 py-2 text-center dark:text-white">
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100">
{currentStreak} days
</span>
</td>
)}
<td className="px-4 py-2 text-center"> <td className="px-4 py-2 text-center">
<button <button
onClick={() => onDeleteHabit(habit.id)} onClick={() => onDeleteHabit(habit.id)}
@ -114,15 +121,9 @@ export function HabitList({
<Trash2 className="h-4 w-4" /> <Trash2 className="h-4 w-4" />
</button> </button>
</td> </td>
{showStreaks && (
<>
<td className="px-4 py-2 text-center">
{/* ... streak content ... */}
</td>
</>
)}
</tr> </tr>
))} );
})}
</tbody> </tbody>
</table> </table>
</div> </div>

View file

@ -3,27 +3,34 @@ import { supabase } from '../lib/supabase';
import { Habit } from '../types'; import { Habit } from '../types';
import { calculateStreak } from '../utils/streakCalculator'; import { calculateStreak } from '../utils/streakCalculator';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { formatDate } from '../utils/dateUtils';
export const useHabits = () => { export const useHabits = () => {
const [habits, setHabits] = useState<Habit[]>([]); const [habits, setHabits] = useState<Habit[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const { user } = useAuth(); const { user } = useAuth();
// Automatically fetch habits when user changes
useEffect(() => { useEffect(() => {
if (user) { if (user) {
fetchHabits(); fetchHabits();
} else { } 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 () => { const fetchHabits = async () => {
if (!user) { if (!user) {
setHabits([]); setHabits([]);
setLoading(false);
return; return;
} }
try { try {
setLoading(true);
setError(null);
const { data, error } = await supabase const { data, error } = await supabase
.from('habits') .from('habits')
.select(` .select(`
@ -40,7 +47,7 @@ export const useHabits = () => {
if (error) throw error; if (error) throw error;
const formattedHabits = (data || []).map(habit => ({ const formattedHabits = data.map(habit => ({
id: habit.id, id: habit.id,
name: habit.name, name: habit.name,
created_at: habit.created_at, created_at: habit.created_at,
@ -51,157 +58,144 @@ export const useHabits = () => {
})); }));
setHabits(formattedHabits); setHabits(formattedHabits);
} catch (error) { } catch (err) {
console.error('Error fetching habits:', error); console.error('Error fetching habits:', err);
setHabits([]); setError(err instanceof Error ? err.message : 'Failed to fetch habits');
} finally {
setLoading(false);
} }
}; };
const addHabit = async (name: string) => { const addHabit = async (name: string): Promise<boolean> => {
if (!user) return false; if (!user || !name.trim()) return false;
try { try {
const { data, error } = await supabase const { data, error } = await supabase
.from('habits') .from('habits')
.insert([{ .insert([{
name, name: name.trim(),
best_streak: 0, user_id: user.id,
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
user_id: user.id best_streak: 0
}]) }])
.select('id') .select()
.single(); .single();
if (error) throw error; if (error) throw error;
await fetchHabits(); await fetchHabits();
return true; return true;
} catch (error) { } catch (err) {
console.error('Error adding habit:', error); console.error('Error adding habit:', err);
setError(err instanceof Error ? err.message : 'Failed to add habit');
return false; return false;
} }
}; };
const toggleHabit = async (id: number, date: string) => { const toggleHabit = async (id: number, date: string): Promise<void> => {
if (!user) return; if (!user) return;
try { try {
// First verify this habit belongs to the user const isCompleted = habits
const { data: habitData, error: habitError } = await supabase .find(h => h.id === id)
.from('habits') ?.completedDates.includes(date);
.select('id')
.eq('id', id)
.eq('user_id', user.id)
.single();
if (habitError || !habitData) { if (isCompleted) {
throw new Error('Unauthorized access to habit'); // Remove completion
} const { error } = await supabase
// 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
.from('habit_completions') .from('habit_completions')
.delete() .delete()
.eq('habit_id', id) .eq('habit_id', id)
.eq('completion_date', formattedDate) .eq('completion_date', date);
.eq('user_id', user.id);
if (deleteError) throw deleteError; if (error) throw error;
} else { } else {
const { error: insertError } = await supabase // Add completion
const { error } = await supabase
.from('habit_completions') .from('habit_completions')
.insert([{ .insert({
habit_id: id, habit_id: id,
completion_date: formattedDate, completion_date: date,
user_id: user.id 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(); await fetchHabits();
} catch (error) {
console.error('Error toggling habit:', error);
throw error;
} }
}; };
const updateHabit = async (id: number, name: string) => { const updateHabit = async (id: number, name: string): Promise<void> => {
if (!user) return; if (!user || !name.trim()) return;
try { try {
// First verify this habit belongs to the user const { error } = await supabase
const { data: habitData, error: habitError } = await supabase
.from('habits') .from('habits')
.select('id') .update({ name: name.trim() })
.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 })
.eq('id', id) .eq('id', id)
.eq('user_id', user.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(); await fetchHabits();
} catch (error) {
console.error('Error updating habit:', error);
} }
}; };
const deleteHabit = async (id: number) => { const deleteHabit = async (id: number): Promise<void> => {
if (!user) return; if (!user) return;
try { try {
// First verify this habit belongs to the user const { error } = await supabase
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
.from('habits') .from('habits')
.delete() .delete()
.eq('id', id) .eq('id', id)
.eq('user_id', user.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(); await fetchHabits();
} catch (error) {
console.error('Error deleting habit:', error);
} }
}; };
return { return {
habits, habits,
loading,
error,
fetchHabits, fetchHabits,
addHabit, addHabit,
toggleHabit, toggleHabit,

View file

@ -1,43 +1,17 @@
import { useState } from 'react'; import { useState } from 'react';
import { getWeekDates, formatDate } from '../utils/dateUtils';
export const useWeek = () => { export const useWeek = () => {
const [currentWeek, setCurrentWeek] = useState<string[]>([]); const [currentWeek, setCurrentWeek] = useState<string[]>(getWeekDates());
const getCurrentWeekDates = () => { const getCurrentWeekDates = () => getWeekDates();
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 changeWeek = (direction: 'prev' | 'next') => { const changeWeek = (direction: 'prev' | 'next') => {
if (currentWeek.length === 0) return; setCurrentWeek(prevWeek => {
const firstDay = new Date(prevWeek[0]);
const firstDay = new Date(currentWeek[0]); firstDay.setDate(firstDay.getDate() + (direction === 'prev' ? -7 : 7));
firstDay.setHours(0, 0, 0, 0); return getWeekDates(firstDay);
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}`;
}));
}; };
return { return {

47
src/utils/dateUtils.ts Normal file
View file

@ -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()
);
};