diff --git a/src/components/Calendar.tsx b/src/components/Calendar.tsx index f4d114d..0287855 100644 --- a/src/components/Calendar.tsx +++ b/src/components/Calendar.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { createPortal } from 'react-dom'; import { ChevronLeft, ChevronRight, Check } from 'lucide-react'; import { useThemeContext } from '../contexts/ThemeContext'; import { Habit } from '../types'; @@ -44,196 +45,296 @@ export const Calendar: React.FC = ({ await onToggleHabit(habitId, date); }; + // Add state for tooltip positioning + const [tooltipData, setTooltipData] = React.useState<{ + x: number; + y: number; + completedHabits: Habit[]; + incompleteHabits: Habit[]; + date: string; + isVisible: boolean; + } | null>(null); + + // Add ref for timeout + const hideTimeoutRef = React.useRef(); + const tooltipRef = React.useRef(null); + + // Modified tooltip handlers + const showTooltip = (e: React.MouseEvent, date: string, completed: Habit[], incomplete: Habit[]) => { + if (hideTimeoutRef.current) { + clearTimeout(hideTimeoutRef.current); + } + + const rect = e.currentTarget.getBoundingClientRect(); + setTooltipData({ + x: rect.left + rect.width / 2, + y: rect.top, + completedHabits: completed, + incompleteHabits: incomplete, + date, + isVisible: true + }); + }; + + const hideTooltip = () => { + if (hideTimeoutRef.current) { + clearTimeout(hideTimeoutRef.current); + } + + hideTimeoutRef.current = setTimeout(() => { + setTooltipData(prev => prev ? { ...prev, isVisible: false } : null); + }, 150); // 150ms delay before hiding + }; + + const cancelHideTooltip = () => { + if (hideTimeoutRef.current) { + clearTimeout(hideTimeoutRef.current); + } + }; + + // Cleanup timeout on unmount + React.useEffect(() => { + return () => { + if (hideTimeoutRef.current) { + clearTimeout(hideTimeoutRef.current); + } + }; + }, []); + return ( -
-
-

- {currentMonth.toLocaleString('default', { month: 'long', year: 'numeric' })} -

-
- - -
-
- -
- {daysOfWeek.map(day => ( -
- {day} + <> +
+
+

+ {currentMonth.toLocaleString('default', { month: 'long', year: 'numeric' })} +

+
+ +
- ))} - - {(() => { - const year = currentMonth.getFullYear(); - const month = currentMonth.getMonth(); - const firstDayOfMonth = getFirstDayOfMonth(year, month); - const daysInMonth = getDaysInMonth(year, month); - const daysInPrevMonth = getDaysInMonth(year, month - 1); +
+ +
+ {daysOfWeek.map(day => ( +
+ {day} +
+ ))} - const days = []; - - // Previous month days - for (let i = 0; i < firstDayOfMonth; i++) { - const day = daysInPrevMonth - firstDayOfMonth + i + 1; - const date = formatDate(new Date(year, month - 1, day)); - days.push({ - date, - dayNumber: day, - isCurrentMonth: false - }); - } - - // Current month days - for (let i = 1; i <= daysInMonth; i++) { - const date = formatDate(new Date(year, month, i)); - days.push({ - date, - dayNumber: i, - isCurrentMonth: true - }); - } - - // Next month days - const remainingDays = 42 - days.length; - for (let i = 1; i <= remainingDays; i++) { - const date = formatDate(new Date(year, month + 1, i)); - days.push({ - date, - dayNumber: i, - isCurrentMonth: false - }); - } - - return days.map(({ date, dayNumber, isCurrentMonth }) => { - const completedHabits = getCompletedHabitsForDate(date); - const incompleteHabits = habits.filter(habit => !habit.completedDates.includes(date)); - const isToday = date === todayStr; + {(() => { + const year = currentMonth.getFullYear(); + const month = currentMonth.getMonth(); + const firstDayOfMonth = getFirstDayOfMonth(year, month); + const daysInMonth = getDaysInMonth(year, month); + const daysInPrevMonth = getDaysInMonth(year, month - 1); - return ( -
- - {dayNumber} - - {habits.length > 0 && ( -
-
-
- {isToday && ( -
- )} -
0 - ? 'bg-[#2ecc71] dark:bg-[#2ecc71] shadow-sm shadow-[#2ecc7150]' - : `bg-[#e9e9e8] dark:bg-[#393939]` - } - `} - > - - {completedHabits.length}/{habits.length} - -
-
+ const days = []; + + // Previous month days + for (let i = 0; i < firstDayOfMonth; i++) { + const day = daysInPrevMonth - firstDayOfMonth + i + 1; + const date = formatDate(new Date(year, month - 1, day)); + days.push({ + date, + dayNumber: day, + isCurrentMonth: false + }); + } + + // Current month days + for (let i = 1; i <= daysInMonth; i++) { + const date = formatDate(new Date(year, month, i)); + days.push({ + date, + dayNumber: i, + isCurrentMonth: true + }); + } + + // Next month days + const remainingDays = 42 - days.length; + for (let i = 1; i <= remainingDays; i++) { + const date = formatDate(new Date(year, month + 1, i)); + days.push({ + date, + dayNumber: i, + isCurrentMonth: false + }); + } + + return days.map(({ date, dayNumber, isCurrentMonth }) => { + const completedHabits = getCompletedHabitsForDate(date); + const incompleteHabits = habits.filter(habit => !habit.completedDates.includes(date)); + const isToday = date === todayStr; + + return ( +
+ + {dayNumber} + + {habits.length > 0 && ( +
showTooltip(e, date, completedHabits, incompleteHabits)} + onMouseLeave={hideTooltip} > -
- {completedHabits.length > 0 && ( -
- - ✓ Completed - -
    - {completedHabits.map(habit => ( -
  • - {habit.name} - -
  • - ))} -
-
- )} - {incompleteHabits.length > 0 && ( -
- - ○ Pending - -
    - {incompleteHabits.map(habit => ( -
  • - {habit.name} - -
  • - ))} -
-
+
+ {isToday && ( +
)} +
0 + ? '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` + } + `} + > + 0 + ? 'text-green-700 dark:text-green-300' + : 'text-gray-600 dark:text-gray-400' + } + `}> + {completedHabits.length}/{habits.length} + +
-
- )} -
- ); - }); - })()} + )} +
+ ); + }); + })()} +
-
+ + {/* Updated tooltip portal with more subtle animations */} + {tooltipData && createPortal( +
+
+ {/* Arrow */} +
+ +
+ {tooltipData.completedHabits.length > 0 && ( +
+ + ✓ Completed + +
    + {tooltipData.completedHabits.map(habit => ( +
  • + {habit.name} + +
  • + ))} +
+
+ )} + {tooltipData.incompleteHabits.length > 0 && ( +
+ + ○ Pending + +
    + {tooltipData.incompleteHabits.map(habit => ( +
  • + {habit.name} + +
  • + ))} +
+
+ )} +
+
+
, + document.body + )} + ); }; \ No newline at end of file