mirror of
https://github.com/harivansh-afk/Habit-Tracker.git
synced 2026-04-21 16:01:11 +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
|
|
@ -39,7 +39,7 @@ export const Calendar: React.FC<CalendarProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const { theme } = useThemeContext();
|
const { theme } = useThemeContext();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const daysOfWeek = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
const daysOfWeek = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||||
|
|
||||||
const getFirstDayOfMonth = (year: number, month: number) => {
|
const getFirstDayOfMonth = (year: number, month: number) => {
|
||||||
|
|
@ -81,7 +81,7 @@ export const Calendar: React.FC<CalendarProps> = ({
|
||||||
if (hideTimeoutRef.current) {
|
if (hideTimeoutRef.current) {
|
||||||
clearTimeout(hideTimeoutRef.current);
|
clearTimeout(hideTimeoutRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rect = e.currentTarget.getBoundingClientRect();
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
setTooltipData({
|
setTooltipData({
|
||||||
x: rect.left + rect.width / 2,
|
x: rect.left + rect.width / 2,
|
||||||
|
|
@ -118,10 +118,49 @@ 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={`
|
<div className={`
|
||||||
rounded-lg shadow-md p-6 md:p-6
|
rounded-lg shadow-md p-6 md:p-6
|
||||||
${theme.calendar.background}
|
${theme.calendar.background}
|
||||||
${isMobile ? 'p-2 mx-[-1rem]' : ''}
|
${isMobile ? 'p-2 mx-[-1rem]' : ''}
|
||||||
`}>
|
`}>
|
||||||
|
|
@ -152,21 +191,21 @@ export const Calendar: React.FC<CalendarProps> = ({
|
||||||
<div className="grid grid-cols-7 gap-1 md:gap-4">
|
<div className="grid grid-cols-7 gap-1 md:gap-4">
|
||||||
{daysOfWeek.map(day => (
|
{daysOfWeek.map(day => (
|
||||||
<div key={day} className={`
|
<div key={day} className={`
|
||||||
text-center font-semibold mb-1 md:mb-2
|
text-center font-semibold mb-1 md:mb-2
|
||||||
${theme.calendar.weekDay}
|
${theme.calendar.weekDay}
|
||||||
${isMobile ? 'text-xs' : ''}
|
${isMobile ? 'text-xs' : ''}
|
||||||
`}>
|
`}>
|
||||||
{isMobile ? day.charAt(0) : day}
|
{isMobile ? day.charAt(0) : day}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{(() => {
|
{(() => {
|
||||||
const year = currentMonth.getFullYear();
|
const year = currentMonth.getFullYear();
|
||||||
const month = currentMonth.getMonth();
|
const month = currentMonth.getMonth();
|
||||||
const firstDayOfMonth = getFirstDayOfMonth(year, month);
|
const firstDayOfMonth = getFirstDayOfMonth(year, month);
|
||||||
const daysInMonth = getDaysInMonth(year, month);
|
const daysInMonth = getDaysInMonth(year, month);
|
||||||
const daysInPrevMonth = getDaysInMonth(year, month - 1);
|
const daysInPrevMonth = getDaysInMonth(year, month - 1);
|
||||||
|
|
||||||
const days = [];
|
const days = [];
|
||||||
|
|
||||||
// Previous month days
|
// Previous month days
|
||||||
|
|
@ -205,20 +244,29 @@ export const Calendar: React.FC<CalendarProps> = ({
|
||||||
const completedHabits = getCompletedHabitsForDate(date);
|
const completedHabits = getCompletedHabitsForDate(date);
|
||||||
const incompleteHabits = habits.filter(habit => !habit.completedDates.includes(date));
|
const incompleteHabits = habits.filter(habit => !habit.completedDates.includes(date));
|
||||||
const isToday = date === todayStr;
|
const isToday = date === todayStr;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={date}
|
key={date}
|
||||||
|
onClick={() => {
|
||||||
|
if (isMobile) {
|
||||||
|
setSelectedDate({
|
||||||
|
date,
|
||||||
|
completedHabits,
|
||||||
|
incompleteHabits
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
className={`
|
className={`
|
||||||
border rounded-lg relative
|
border rounded-lg relative
|
||||||
${theme.border}
|
${theme.border}
|
||||||
${isCurrentMonth ? theme.calendar.day.default : theme.calendar.day.otherMonth}
|
${isCurrentMonth ? theme.calendar.day.default : theme.calendar.day.otherMonth}
|
||||||
${isToday ? theme.calendar.day.today : ''}
|
${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={`
|
<span className={`
|
||||||
font-medium
|
font-medium
|
||||||
${isCurrentMonth ? theme.text : theme.calendar.day.otherMonth}
|
${isCurrentMonth ? theme.text : theme.calendar.day.otherMonth}
|
||||||
${isToday ? 'relative' : ''}
|
${isToday ? 'relative' : ''}
|
||||||
${isMobile ? 'text-sm' : ''}
|
${isMobile ? 'text-sm' : ''}
|
||||||
|
|
@ -230,7 +278,7 @@ export const Calendar: React.FC<CalendarProps> = ({
|
||||||
absolute bottom-1 left-1/2 transform -translate-x-1/2
|
absolute bottom-1 left-1/2 transform -translate-x-1/2
|
||||||
${isMobile ? 'w-full px-1' : ''}
|
${isMobile ? 'w-full px-1' : ''}
|
||||||
`}>
|
`}>
|
||||||
<div
|
<div
|
||||||
className="relative"
|
className="relative"
|
||||||
{...(!isMobile ? {
|
{...(!isMobile ? {
|
||||||
onMouseEnter: (e) => showTooltip(e, date, completedHabits, incompleteHabits),
|
onMouseEnter: (e) => showTooltip(e, date, completedHabits, incompleteHabits),
|
||||||
|
|
@ -241,8 +289,8 @@ export const Calendar: React.FC<CalendarProps> = ({
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<div className={`
|
<div className={`
|
||||||
text-xs font-medium px-1.5 py-0.5 rounded
|
text-xs font-medium px-1.5 py-0.5 rounded
|
||||||
${completedHabits.length > 0
|
${completedHabits.length > 0
|
||||||
? 'text-green-700 dark:text-green-300'
|
? 'text-green-700 dark:text-green-300'
|
||||||
: `${theme.text} opacity-75`
|
: `${theme.text} opacity-75`
|
||||||
}
|
}
|
||||||
`}>
|
`}>
|
||||||
|
|
@ -257,15 +305,15 @@ export const Calendar: React.FC<CalendarProps> = ({
|
||||||
<div className={`
|
<div className={`
|
||||||
h-6 px-2.5 rounded-full cursor-pointer
|
h-6 px-2.5 rounded-full cursor-pointer
|
||||||
transition-all duration-200 flex items-center justify-center gap-1
|
transition-all duration-200 flex items-center justify-center gap-1
|
||||||
${completedHabits.length > 0
|
${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-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`
|
: `bg-gray-100 dark:bg-gray-800 shadow-sm`
|
||||||
}
|
}
|
||||||
`}>
|
`}>
|
||||||
<span className={`
|
<span className={`
|
||||||
text-xs font-medium
|
text-xs font-medium
|
||||||
${completedHabits.length > 0
|
${completedHabits.length > 0
|
||||||
? 'text-green-700 dark:text-green-300'
|
? 'text-green-700 dark:text-green-300'
|
||||||
: 'text-gray-600 dark:text-gray-400'
|
: 'text-gray-600 dark:text-gray-400'
|
||||||
}
|
}
|
||||||
`}>
|
`}>
|
||||||
|
|
@ -284,7 +332,81 @@ export const Calendar: React.FC<CalendarProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</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(
|
{!isMobile && tooltipData && createPortal(
|
||||||
<div
|
<div
|
||||||
ref={tooltipRef}
|
ref={tooltipRef}
|
||||||
|
|
@ -293,8 +415,8 @@ export const Calendar: React.FC<CalendarProps> = ({
|
||||||
className={`
|
className={`
|
||||||
fixed
|
fixed
|
||||||
transition-all duration-150 ease-in-out
|
transition-all duration-150 ease-in-out
|
||||||
${tooltipData.isVisible
|
${tooltipData.isVisible
|
||||||
? 'opacity-100 translate-y-0'
|
? 'opacity-100 translate-y-0'
|
||||||
: 'opacity-0 translate-y-1'
|
: 'opacity-0 translate-y-1'
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
|
|
@ -306,7 +428,7 @@ export const Calendar: React.FC<CalendarProps> = ({
|
||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
rounded-2xl p-5
|
rounded-2xl p-5
|
||||||
w-[240px]
|
w-[240px]
|
||||||
|
|
@ -321,7 +443,7 @@ export const Calendar: React.FC<CalendarProps> = ({
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{/* Updated Arrow */}
|
{/* Updated Arrow */}
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
absolute -bottom-[6px] left-1/2 -translate-x-1/2
|
absolute -bottom-[6px] left-1/2 -translate-x-1/2
|
||||||
w-3 h-3 rotate-45
|
w-3 h-3 rotate-45
|
||||||
|
|
@ -330,7 +452,7 @@ export const Calendar: React.FC<CalendarProps> = ({
|
||||||
border-t-0 border-l-0
|
border-t-0 border-l-0
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{tooltipData.completedHabits.length > 0 && (
|
{tooltipData.completedHabits.length > 0 && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
|
|
@ -339,8 +461,8 @@ export const Calendar: React.FC<CalendarProps> = ({
|
||||||
</span>
|
</span>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{tooltipData.completedHabits.map(habit => (
|
{tooltipData.completedHabits.map(habit => (
|
||||||
<li
|
<li
|
||||||
key={habit.id}
|
key={habit.id}
|
||||||
className={`${theme.text} text-sm truncate flex items-center justify-between group`}
|
className={`${theme.text} text-sm truncate flex items-center justify-between group`}
|
||||||
>
|
>
|
||||||
<span className="truncate mr-2">{habit.name}</span>
|
<span className="truncate mr-2">{habit.name}</span>
|
||||||
|
|
@ -362,8 +484,8 @@ export const Calendar: React.FC<CalendarProps> = ({
|
||||||
</span>
|
</span>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{tooltipData.incompleteHabits.map(habit => (
|
{tooltipData.incompleteHabits.map(habit => (
|
||||||
<li
|
<li
|
||||||
key={habit.id}
|
key={habit.id}
|
||||||
className={`${theme.text} text-sm truncate flex items-center justify-between group`}
|
className={`${theme.text} text-sm truncate flex items-center justify-between group`}
|
||||||
>
|
>
|
||||||
<span className="truncate mr-2">{habit.name}</span>
|
<span className="truncate mr-2">{habit.name}</span>
|
||||||
|
|
@ -385,4 +507,4 @@ export const Calendar: React.FC<CalendarProps> = ({
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ export function HabitList({
|
||||||
<tbody>
|
<tbody>
|
||||||
{sortedHabits.map((habit) => {
|
{sortedHabits.map((habit) => {
|
||||||
const { currentStreak } = calculateStreak(habit.completedDates);
|
const { currentStreak } = calculateStreak(habit.completedDates);
|
||||||
|
|
||||||
return (
|
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">
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export function SignUp({ onSwitchToLogin }: { onSwitchToLogin: () => void }) {
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
// Validate password before submission
|
// Validate password before submission
|
||||||
const passwordValidationError = validatePassword(password);
|
const passwordValidationError = validatePassword(password);
|
||||||
if (passwordValidationError) {
|
if (passwordValidationError) {
|
||||||
|
|
@ -48,8 +48,12 @@ export function SignUp({ onSwitchToLogin }: { onSwitchToLogin: () => void }) {
|
||||||
setError('');
|
setError('');
|
||||||
setPasswordError(null);
|
setPasswordError(null);
|
||||||
await signUp(email, password);
|
await signUp(email, password);
|
||||||
} catch (err) {
|
} catch (err: unknown) {
|
||||||
setError('Failed to create an account');
|
// 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 }) {
|
||||||
<div className="min-h-screen w-full flex">
|
<div className="min-h-screen w-full flex">
|
||||||
<div className="hidden md:flex md:w-1/2 bg-black p-12 flex-col justify-center relative overflow-hidden">
|
<div className="hidden md:flex md:w-1/2 bg-black p-12 flex-col justify-center relative overflow-hidden">
|
||||||
<div className="absolute inset-0 flex items-center justify-center opacity-20">
|
<div className="absolute inset-0 flex items-center justify-center opacity-20">
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute w-96 h-96"
|
className="absolute w-96 h-96"
|
||||||
animate={{
|
animate={{
|
||||||
rotate: 360,
|
rotate: 360,
|
||||||
scale: [1, 1.2, 1],
|
scale: [1, 1.2, 1],
|
||||||
}}
|
}}
|
||||||
transition={{
|
transition={{
|
||||||
duration: 15,
|
duration: 15,
|
||||||
repeat: Infinity,
|
repeat: Infinity,
|
||||||
ease: "linear"
|
ease: "linear"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Circle className="w-full h-full text-white" />
|
<Circle className="w-full h-full text-white" />
|
||||||
|
|
@ -103,30 +107,30 @@ export function SignUp({ onSwitchToLogin }: { onSwitchToLogin: () => void }) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="absolute inset-0 md:hidden overflow-hidden">
|
<div className="absolute inset-0 md:hidden overflow-hidden">
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute top-[-30%] right-[-20%] w-[80%] h-[80%] opacity-[0.07]"
|
className="absolute top-[-30%] right-[-20%] w-[80%] h-[80%] opacity-[0.07]"
|
||||||
animate={{
|
animate={{
|
||||||
rotate: 360,
|
rotate: 360,
|
||||||
scale: [1, 1.1, 1],
|
scale: [1, 1.1, 1],
|
||||||
}}
|
}}
|
||||||
transition={{
|
transition={{
|
||||||
duration: 12,
|
duration: 12,
|
||||||
repeat: Infinity,
|
repeat: Infinity,
|
||||||
ease: "linear"
|
ease: "linear"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Circle className="w-full h-full" />
|
<Circle className="w-full h-full" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<motion.div
|
<motion.div
|
||||||
className="absolute bottom-[-40%] left-[-30%] w-[90%] h-[90%] opacity-[0.05]"
|
className="absolute bottom-[-40%] left-[-30%] w-[90%] h-[90%] opacity-[0.05]"
|
||||||
animate={{
|
animate={{
|
||||||
rotate: -360,
|
rotate: -360,
|
||||||
scale: [1, 1.2, 1],
|
scale: [1, 1.2, 1],
|
||||||
}}
|
}}
|
||||||
transition={{
|
transition={{
|
||||||
duration: 15,
|
duration: 15,
|
||||||
repeat: Infinity,
|
repeat: Infinity,
|
||||||
ease: "linear"
|
ease: "linear"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Circle className="w-full h-full" />
|
<Circle className="w-full h-full" />
|
||||||
|
|
@ -134,7 +138,7 @@ export function SignUp({ onSwitchToLogin }: { onSwitchToLogin: () => void }) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full max-w-md relative mt-24 md:mt-0">
|
<div className="w-full max-w-md relative mt-24 md:mt-0">
|
||||||
<div className={`w-full p-6 md:p-8 rounded-2xl backdrop-blur-sm ${theme.cardBackground} relative z-10
|
<div className={`w-full p-6 md:p-8 rounded-2xl backdrop-blur-sm ${theme.cardBackground} relative z-10
|
||||||
border border-gray-100 dark:border-gray-800 shadow-xl`}>
|
border border-gray-100 dark:border-gray-800 shadow-xl`}>
|
||||||
<h2 className={`text-2xl font-bold mb-6 ${theme.text} md:block hidden`}>Sign Up</h2>
|
<h2 className={`text-2xl font-bold mb-6 ${theme.text} md:block hidden`}>Sign Up</h2>
|
||||||
{error && (
|
{error && (
|
||||||
|
|
@ -149,7 +153,7 @@ export function SignUp({ onSwitchToLogin }: { onSwitchToLogin: () => void }) {
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
placeholder="Email"
|
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`}
|
focus:ring-2 focus:ring-black dark:focus:ring-white border-gray-200 dark:border-gray-700`}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
@ -160,8 +164,8 @@ export function SignUp({ onSwitchToLogin }: { onSwitchToLogin: () => void }) {
|
||||||
value={password}
|
value={password}
|
||||||
onChange={handlePasswordChange}
|
onChange={handlePasswordChange}
|
||||||
placeholder="Password"
|
placeholder="Password"
|
||||||
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
|
focus:ring-2 focus:ring-black dark:focus:ring-white
|
||||||
${passwordError ? 'border-red-500' : 'border-gray-200 dark:border-gray-700'}`}
|
${passwordError ? 'border-red-500' : 'border-gray-200 dark:border-gray-700'}`}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
@ -182,7 +186,7 @@ export function SignUp({ onSwitchToLogin }: { onSwitchToLogin: () => void }) {
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className={`w-full py-3 rounded-xl bg-black text-white hover:bg-gray-800
|
className={`w-full py-3 rounded-xl bg-black text-white hover:bg-gray-800
|
||||||
transition-all duration-200 shadow-lg hover:shadow-xl`}
|
transition-all duration-200 shadow-lg hover:shadow-xl`}
|
||||||
>
|
>
|
||||||
Create Account
|
Create Account
|
||||||
|
|
@ -202,4 +206,4 @@ export function SignUp({ onSwitchToLogin }: { onSwitchToLogin: () => void }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,53 +11,43 @@ export const useHabits = () => {
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (user) {
|
|
||||||
fetchHabits();
|
|
||||||
} else {
|
|
||||||
setHabits([]);
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [user?.id]);
|
|
||||||
|
|
||||||
const fetchHabits = async () => {
|
const fetchHabits = async () => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
setHabits([]);
|
setHabits([]);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const { data, error } = await supabase
|
// Fetch habits
|
||||||
|
const { data: habitsData, error: habitsError } = await supabase
|
||||||
.from('habits')
|
.from('habits')
|
||||||
.select(`
|
.select('*')
|
||||||
id,
|
|
||||||
name,
|
|
||||||
created_at,
|
|
||||||
best_streak,
|
|
||||||
habit_completions (
|
|
||||||
completion_date
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
.eq('user_id', user.id)
|
.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 => ({
|
// Fetch all completions for user's habits
|
||||||
id: habit.id,
|
const { data: completionsData, error: completionsError } = await supabase
|
||||||
name: habit.name,
|
.from('habit_completions')
|
||||||
created_at: habit.created_at,
|
.select('*')
|
||||||
best_streak: habit.best_streak,
|
.eq('user_id', user.id);
|
||||||
completedDates: habit.habit_completions?.map(
|
|
||||||
(completion: { completion_date: string }) => completion.completion_date
|
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) {
|
} catch (err) {
|
||||||
console.error('Error fetching habits:', err);
|
console.error('Error fetching habits:', err);
|
||||||
setError(err instanceof Error ? err.message : 'Failed to fetch habits');
|
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<boolean> => {
|
const addHabit = async (name: string): Promise<boolean> => {
|
||||||
if (!user || !name.trim()) return false;
|
if (!user || !name.trim()) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const { error } = await supabase
|
||||||
.from('habits')
|
.from('habits')
|
||||||
.insert([{
|
.insert([{
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
best_streak: 0
|
best_streak: 0
|
||||||
}])
|
}])
|
||||||
.select()
|
.select();
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
|
|
@ -94,11 +131,12 @@ export const useHabits = () => {
|
||||||
|
|
||||||
const toggleHabit = async (id: number, date: string): Promise<void> => {
|
const toggleHabit = async (id: number, date: string): Promise<void> => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const isCompleted = habits
|
const habit = habits.find(h => h.id === id);
|
||||||
.find(h => h.id === id)
|
if (!habit) throw new Error('Habit not found');
|
||||||
?.completedDates.includes(date);
|
|
||||||
|
const isCompleted = habit.completedDates.includes(date);
|
||||||
|
|
||||||
if (isCompleted) {
|
if (isCompleted) {
|
||||||
// Remove completion
|
// Remove completion
|
||||||
|
|
@ -106,7 +144,8 @@ export const useHabits = () => {
|
||||||
.from('habit_completions')
|
.from('habit_completions')
|
||||||
.delete()
|
.delete()
|
||||||
.eq('habit_id', id)
|
.eq('habit_id', id)
|
||||||
.eq('completion_date', date);
|
.eq('completion_date', date)
|
||||||
|
.eq('user_id', user.id);
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -135,18 +174,17 @@ export const useHabits = () => {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fetch updated data to ensure consistency
|
|
||||||
await fetchHabits();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error toggling habit:', err);
|
console.error('Error toggling habit:', err);
|
||||||
setError(err instanceof Error ? err.message : 'Failed to toggle habit');
|
setError(err instanceof Error ? err.message : 'Failed to toggle habit');
|
||||||
|
// Refresh habits to ensure consistency
|
||||||
await fetchHabits();
|
await fetchHabits();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateHabit = async (id: number, name: string): Promise<void> => {
|
const updateHabit = async (id: number, name: string): Promise<void> => {
|
||||||
if (!user || !name.trim()) return;
|
if (!user || !name.trim()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
.from('habits')
|
.from('habits')
|
||||||
|
|
@ -172,7 +210,7 @@ export const useHabits = () => {
|
||||||
|
|
||||||
const deleteHabit = async (id: number): Promise<void> => {
|
const deleteHabit = async (id: number): Promise<void> => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
.from('habits')
|
.from('habits')
|
||||||
|
|
@ -196,10 +234,10 @@ export const useHabits = () => {
|
||||||
habits,
|
habits,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
fetchHabits,
|
|
||||||
addHabit,
|
addHabit,
|
||||||
toggleHabit,
|
toggleHabit,
|
||||||
updateHabit,
|
updateHabit,
|
||||||
deleteHabit
|
deleteHabit,
|
||||||
|
fetchHabits
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -5,21 +5,21 @@ export const formatDate = (date: Date): string => {
|
||||||
const day = String(date.getDate()).padStart(2, '0');
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
return `${year}-${month}-${day}`;
|
return `${year}-${month}-${day}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get day index (0-6) where Monday is 0 and Sunday is 6
|
// Get day index (0-6) where Monday is 0 and Sunday is 6
|
||||||
export const getDayIndex = (date: Date): number => {
|
export const getDayIndex = (date: Date): number => {
|
||||||
const day = date.getDay();
|
const day = date.getDay();
|
||||||
return day === 0 ? 6 : day - 1;
|
return day === 0 ? 6 : day - 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get dates for current week starting from Monday
|
// Get dates for current week starting from Monday
|
||||||
export const getWeekDates = (baseDate: Date = new Date()): string[] => {
|
export const getWeekDates = (baseDate: Date = new Date()): string[] => {
|
||||||
const current = new Date(baseDate);
|
const current = new Date(baseDate);
|
||||||
const dayIndex = getDayIndex(current);
|
const dayIndex = getDayIndex(current);
|
||||||
|
|
||||||
// Adjust to Monday
|
// Adjust to Monday
|
||||||
current.setDate(current.getDate() - dayIndex);
|
current.setDate(current.getDate() - dayIndex);
|
||||||
|
|
||||||
// Generate dates starting from Monday
|
// Generate dates starting from Monday
|
||||||
const dates = [];
|
const dates = [];
|
||||||
for (let i = 0; i < 7; i++) {
|
for (let i = 0; i < 7; i++) {
|
||||||
|
|
@ -27,16 +27,16 @@ export const formatDate = (date: Date): string => {
|
||||||
date.setDate(current.getDate() + i);
|
date.setDate(current.getDate() + i);
|
||||||
dates.push(formatDate(date));
|
dates.push(formatDate(date));
|
||||||
}
|
}
|
||||||
|
|
||||||
return dates;
|
return dates;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get the first day of the month adjusted for Monday start
|
// Get the first day of the month adjusted for Monday start
|
||||||
export const getFirstDayOfMonth = (year: number, month: number): number => {
|
export const getFirstDayOfMonth = (year: number, month: number): number => {
|
||||||
const date = new Date(year, month, 1);
|
const date = new Date(year, month, 1);
|
||||||
return getDayIndex(date);
|
return getDayIndex(date);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if two dates are the same day
|
// Check if two dates are the same day
|
||||||
export const isSameDay = (date1: Date, date2: Date): boolean => {
|
export const isSameDay = (date1: Date, date2: Date): boolean => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -44,4 +44,4 @@ export const formatDate = (date: Date): string => {
|
||||||
date1.getMonth() === date2.getMonth() &&
|
date1.getMonth() === date2.getMonth() &&
|
||||||
date1.getFullYear() === date2.getFullYear()
|
date1.getFullYear() === date2.getFullYear()
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue