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,
toggleHabit,
updateHabit,
deleteHabit
deleteHabit,
error
} = useHabits();
const {
@ -88,26 +89,23 @@ function HabitTrackerContent() {
const renderHabitsView = () => (
<div className="flex-1">
<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
type="text"
value={newHabit}
onChange={(e) => 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}`}
/>
<button
onClick={handleAddHabit}
type="submit"
disabled={!newHabit.trim()}
className={`px-4 py-2 rounded-lg ${theme.button.primary} disabled:opacity-50`}
>
Add Habit
</button>
</form>
</div>
<div className="mb-6">
@ -124,9 +122,7 @@ function HabitTrackerContent() {
<ChevronLeft className="h-5 w-5" />
</button>
<button
onClick={() => {
setCurrentWeek(getCurrentWeekDates());
}}
onClick={goToCurrentWeek}
className={`px-4 py-2 rounded-lg ${theme.button.secondary}`}
>
Today
@ -141,6 +137,22 @@ function HabitTrackerContent() {
</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
habits={getSortedHabits()}
currentWeek={currentWeek}
@ -152,6 +164,8 @@ function HabitTrackerContent() {
<p className={`text-sm ${theme.mutedText} mt-4`}>
Keep up the good work! Consistency is key.
</p>
</>
)}
</div>
</div>
);

View file

@ -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<void>;
onToggleHabit: (id: number, date: string) => Promise<void>;
onUpdateHabit: (id: number, name: string) => Promise<void>;
onDeleteHabit: (id: number) => Promise<void>;
}
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,23 +42,26 @@ export function HabitList({
<thead>
<tr>
<th className="text-left px-4 py-2 dark:text-white">Habit</th>
{currentWeek.map((dateStr) => {
const date = new Date(dateStr);
return (
{currentWeek.map((dateStr) => (
<th key={dateStr} className="px-4 py-2 text-center dark:text-white">
<div className="hidden md:block">{getDayName(dateStr)}</div>
<div className="md:hidden">{getDayName(dateStr).slice(0, 1)}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
{date.getDate()}
{new Date(dateStr).getDate()}
</div>
</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>
</tr>
</thead>
<tbody>
{sortedHabits.map((habit) => (
{sortedHabits.map((habit) => {
const { currentStreak } = calculateStreak(habit.completedDates);
return (
<tr key={habit.id} className="border-t dark:border-gray-700">
<td className="px-4 py-2 dark:text-white">
<input
@ -106,6 +106,13 @@ export function HabitList({
</label>
</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">
<button
onClick={() => onDeleteHabit(habit.id)}
@ -114,15 +121,9 @@ export function HabitList({
<Trash2 className="h-4 w-4" />
</button>
</td>
{showStreaks && (
<>
<td className="px-4 py-2 text-center">
{/* ... streak content ... */}
</td>
</>
)}
</tr>
))}
);
})}
</tbody>
</table>
</div>

View file

@ -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<Habit[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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<boolean> => {
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<void> => {
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([{
.insert({
habit_id: id,
completion_date: formattedDate,
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<void> => {
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<void> => {
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,

View file

@ -1,43 +1,17 @@
import { useState } from 'react';
import { getWeekDates, formatDate } from '../utils/dateUtils';
export const useWeek = () => {
const [currentWeek, setCurrentWeek] = useState<string[]>([]);
const [currentWeek, setCurrentWeek] = useState<string[]>(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 {

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