mirror of
https://github.com/harivansh-afk/Habit-Tracker.git
synced 2026-04-17 12:04:14 +00:00
added little widget on calendar page of mobile version which shows habits on selected day
This commit is contained in:
parent
9e1602723d
commit
0186c1f730
5 changed files with 271 additions and 107 deletions
|
|
@ -118,6 +118,45 @@ export const Calendar: React.FC<CalendarProps> = ({
|
|||
};
|
||||
}, []);
|
||||
|
||||
// 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 (
|
||||
<>
|
||||
<div className={`
|
||||
|
|
@ -209,12 +248,21 @@ export const Calendar: React.FC<CalendarProps> = ({
|
|||
return (
|
||||
<div
|
||||
key={date}
|
||||
onClick={() => {
|
||||
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]'}
|
||||
`}
|
||||
>
|
||||
<span className={`
|
||||
|
|
@ -284,7 +332,81 @@ export const Calendar: React.FC<CalendarProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tooltip portal - only render on desktop */}
|
||||
{/* Mobile Selected Date Details */}
|
||||
{isMobile && selectedDate && (
|
||||
<div className={`
|
||||
mt-6 p-4 rounded-lg
|
||||
${theme.cardBackground}
|
||||
border ${theme.border}
|
||||
shadow-md
|
||||
`}>
|
||||
<div className="mb-4">
|
||||
<h3 className={`text-lg font-medium ${theme.text}`}>
|
||||
{new Date(selectedDate.date).toLocaleDateString('default', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
})}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{selectedDate.completedHabits.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<span className="text-emerald-500 dark:text-emerald-400 font-medium block mb-2.5 text-sm">
|
||||
✓ Completed
|
||||
</span>
|
||||
<ul className="space-y-2">
|
||||
{selectedDate.completedHabits.map(habit => (
|
||||
<li
|
||||
key={habit.id}
|
||||
className={`${theme.text} text-sm truncate flex items-center justify-between group`}
|
||||
>
|
||||
<span className="truncate mr-2">{habit.name}</span>
|
||||
<button
|
||||
onClick={(e) => handleMobileHabitToggle(e, habit.id, selectedDate.date)}
|
||||
className={`p-1.5 rounded-lg transition-colors ${theme.habitItem}`}
|
||||
>
|
||||
<Check className="h-4 w-4 text-emerald-500 dark:text-emerald-400" />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedDate.incompleteHabits.length > 0 && (
|
||||
<div>
|
||||
<span className="text-rose-500 dark:text-rose-400 font-medium block mb-2.5 text-sm">
|
||||
○ Pending
|
||||
</span>
|
||||
<ul className="space-y-2">
|
||||
{selectedDate.incompleteHabits.map(habit => (
|
||||
<li
|
||||
key={habit.id}
|
||||
className={`${theme.text} text-sm truncate flex items-center justify-between group`}
|
||||
>
|
||||
<span className="truncate mr-2">{habit.name}</span>
|
||||
<button
|
||||
onClick={(e) => handleMobileHabitToggle(e, habit.id, selectedDate.date)}
|
||||
className={`p-1.5 rounded-lg transition-colors ${theme.habitItem}`}
|
||||
>
|
||||
<Check className="h-4 w-4 text-gray-400 dark:text-gray-500 hover:text-emerald-500 dark:hover:text-emerald-400" />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedDate.completedHabits.length === 0 && selectedDate.incompleteHabits.length === 0 && (
|
||||
<p className={`text-sm ${theme.mutedText} text-center py-4`}>
|
||||
No habits tracked for this date
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Desktop Tooltip Portal - unchanged */}
|
||||
{!isMobile && tooltipData && createPortal(
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -11,15 +11,6 @@ export const useHabits = () => {
|
|||
const [error, setError] = useState<string | null>(null);
|
||||
const { user } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
fetchHabits();
|
||||
} else {
|
||||
setHabits([]);
|
||||
setLoading(false);
|
||||
}
|
||||
}, [user?.id]);
|
||||
|
||||
const fetchHabits = async () => {
|
||||
if (!user) {
|
||||
setHabits([]);
|
||||
|
|
@ -31,33 +22,32 @@ export const useHabits = () => {
|
|||
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,11 +56,59 @@ 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<boolean> => {
|
||||
if (!user || !name.trim()) return false;
|
||||
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
const { error } = await supabase
|
||||
.from('habits')
|
||||
.insert([{
|
||||
name: name.trim(),
|
||||
|
|
@ -78,8 +116,7 @@ export const useHabits = () => {
|
|||
created_at: new Date().toISOString(),
|
||||
best_streak: 0
|
||||
}])
|
||||
.select()
|
||||
.single();
|
||||
.select();
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
|
|
@ -96,9 +133,10 @@ export const useHabits = () => {
|
|||
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,11 +174,10 @@ 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();
|
||||
}
|
||||
};
|
||||
|
|
@ -196,10 +234,10 @@ export const useHabits = () => {
|
|||
habits,
|
||||
loading,
|
||||
error,
|
||||
fetchHabits,
|
||||
addHabit,
|
||||
toggleHabit,
|
||||
updateHabit,
|
||||
deleteHabit
|
||||
deleteHabit,
|
||||
fetchHabits
|
||||
};
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue