mirror of
https://github.com/harivansh-afk/Habit-Tracker.git
synced 2026-04-20 14:05:09 +00:00
fixed tooltip overshadow
This commit is contained in:
parent
b0c218cd65
commit
4f8e262f75
1 changed files with 281 additions and 180 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
import { ChevronLeft, ChevronRight, Check } from 'lucide-react';
|
import { ChevronLeft, ChevronRight, Check } from 'lucide-react';
|
||||||
import { useThemeContext } from '../contexts/ThemeContext';
|
import { useThemeContext } from '../contexts/ThemeContext';
|
||||||
import { Habit } from '../types';
|
import { Habit } from '../types';
|
||||||
|
|
@ -44,196 +45,296 @@ export const Calendar: React.FC<CalendarProps> = ({
|
||||||
await onToggleHabit(habitId, date);
|
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<NodeJS.Timeout>();
|
||||||
|
const tooltipRef = React.useRef<HTMLDivElement>(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 (
|
return (
|
||||||
<div className={`rounded-lg shadow-md p-6 ${theme.calendar.background}`}>
|
<>
|
||||||
<div className="flex justify-between items-center mb-8">
|
<div className={`rounded-lg shadow-md p-6 ${theme.calendar.background}`}>
|
||||||
<h2 className={`text-2xl font-bold ${theme.calendar.header}`}>
|
<div className="flex justify-between items-center mb-8">
|
||||||
{currentMonth.toLocaleString('default', { month: 'long', year: 'numeric' })}
|
<h2 className={`text-2xl font-bold ${theme.calendar.header}`}>
|
||||||
</h2>
|
{currentMonth.toLocaleString('default', { month: 'long', year: 'numeric' })}
|
||||||
<div className="flex space-x-3">
|
</h2>
|
||||||
<button
|
<div className="flex space-x-3">
|
||||||
onClick={() => onChangeMonth('prev')}
|
<button
|
||||||
className={`p-2.5 rounded-lg ${theme.calendar.navigation.button}`}
|
onClick={() => onChangeMonth('prev')}
|
||||||
>
|
className={`p-2.5 rounded-lg ${theme.calendar.navigation.button}`}
|
||||||
<ChevronLeft className={theme.calendar.navigation.icon} />
|
>
|
||||||
</button>
|
<ChevronLeft className={theme.calendar.navigation.icon} />
|
||||||
<button
|
</button>
|
||||||
onClick={() => onChangeMonth('next')}
|
<button
|
||||||
className={`p-2.5 rounded-lg ${theme.calendar.navigation.button}`}
|
onClick={() => onChangeMonth('next')}
|
||||||
>
|
className={`p-2.5 rounded-lg ${theme.calendar.navigation.button}`}
|
||||||
<ChevronRight className={theme.calendar.navigation.icon} />
|
>
|
||||||
</button>
|
<ChevronRight className={theme.calendar.navigation.icon} />
|
||||||
</div>
|
</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-7 gap-4">
|
|
||||||
{daysOfWeek.map(day => (
|
|
||||||
<div key={day} className={`text-center font-semibold mb-2 ${theme.calendar.weekDay}`}>
|
|
||||||
{day}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
</div>
|
||||||
|
|
||||||
{(() => {
|
<div className="grid grid-cols-7 gap-4">
|
||||||
const year = currentMonth.getFullYear();
|
{daysOfWeek.map(day => (
|
||||||
const month = currentMonth.getMonth();
|
<div key={day} className={`text-center font-semibold mb-2 ${theme.calendar.weekDay}`}>
|
||||||
const firstDayOfMonth = getFirstDayOfMonth(year, month);
|
{day}
|
||||||
const daysInMonth = getDaysInMonth(year, month);
|
</div>
|
||||||
const daysInPrevMonth = getDaysInMonth(year, month - 1);
|
))}
|
||||||
|
|
||||||
const days = [];
|
{(() => {
|
||||||
|
const year = currentMonth.getFullYear();
|
||||||
// Previous month days
|
const month = currentMonth.getMonth();
|
||||||
for (let i = 0; i < firstDayOfMonth; i++) {
|
const firstDayOfMonth = getFirstDayOfMonth(year, month);
|
||||||
const day = daysInPrevMonth - firstDayOfMonth + i + 1;
|
const daysInMonth = getDaysInMonth(year, month);
|
||||||
const date = formatDate(new Date(year, month - 1, day));
|
const daysInPrevMonth = getDaysInMonth(year, month - 1);
|
||||||
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 (
|
const days = [];
|
||||||
<div
|
|
||||||
key={date}
|
// Previous month days
|
||||||
className={`
|
for (let i = 0; i < firstDayOfMonth; i++) {
|
||||||
border rounded-lg p-3 min-h-[80px] relative
|
const day = daysInPrevMonth - firstDayOfMonth + i + 1;
|
||||||
${theme.border}
|
const date = formatDate(new Date(year, month - 1, day));
|
||||||
${isCurrentMonth ? theme.calendar.day.default : theme.calendar.day.otherMonth}
|
days.push({
|
||||||
${isToday ? theme.calendar.day.today : ''}
|
date,
|
||||||
`}
|
dayNumber: day,
|
||||||
>
|
isCurrentMonth: false
|
||||||
<span className={`
|
});
|
||||||
font-medium
|
}
|
||||||
${isCurrentMonth ? theme.text : theme.calendar.day.otherMonth}
|
|
||||||
${isToday ? 'relative' : ''}
|
// Current month days
|
||||||
`}>
|
for (let i = 1; i <= daysInMonth; i++) {
|
||||||
{dayNumber}
|
const date = formatDate(new Date(year, month, i));
|
||||||
</span>
|
days.push({
|
||||||
{habits.length > 0 && (
|
date,
|
||||||
<div className="absolute bottom-3 left-1/2 transform -translate-x-1/2">
|
dayNumber: i,
|
||||||
<div className="group relative inline-block">
|
isCurrentMonth: true
|
||||||
<div className="flex items-center gap-1.5">
|
});
|
||||||
{isToday && (
|
}
|
||||||
<div className="w-2 h-2 rounded-full bg-blue-500 dark:bg-blue-400" />
|
|
||||||
)}
|
// Next month days
|
||||||
<div
|
const remainingDays = 42 - days.length;
|
||||||
className={`
|
for (let i = 1; i <= remainingDays; i++) {
|
||||||
h-5 px-2 rounded-full cursor-pointer
|
const date = formatDate(new Date(year, month + 1, i));
|
||||||
transition-colors duration-200 flex items-center justify-center
|
days.push({
|
||||||
${completedHabits.length > 0
|
date,
|
||||||
? 'bg-[#2ecc71] dark:bg-[#2ecc71] shadow-sm shadow-[#2ecc7150]'
|
dayNumber: i,
|
||||||
: `bg-[#e9e9e8] dark:bg-[#393939]`
|
isCurrentMonth: false
|
||||||
}
|
});
|
||||||
`}
|
}
|
||||||
>
|
|
||||||
<span className="text-[8px] font-medium text-black/70 dark:text-white/70">
|
return days.map(({ date, dayNumber, isCurrentMonth }) => {
|
||||||
{completedHabits.length}/{habits.length}
|
const completedHabits = getCompletedHabitsForDate(date);
|
||||||
</span>
|
const incompleteHabits = habits.filter(habit => !habit.completedDates.includes(date));
|
||||||
</div>
|
const isToday = date === todayStr;
|
||||||
</div>
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={date}
|
||||||
|
className={`
|
||||||
|
border rounded-lg p-3 min-h-[80px] relative
|
||||||
|
${theme.border}
|
||||||
|
${isCurrentMonth ? theme.calendar.day.default : theme.calendar.day.otherMonth}
|
||||||
|
${isToday ? theme.calendar.day.today : ''}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<span className={`
|
||||||
|
font-medium
|
||||||
|
${isCurrentMonth ? theme.text : theme.calendar.day.otherMonth}
|
||||||
|
${isToday ? 'relative' : ''}
|
||||||
|
`}>
|
||||||
|
{dayNumber}
|
||||||
|
</span>
|
||||||
|
{habits.length > 0 && (
|
||||||
|
<div className="absolute bottom-3 left-1/2 transform -translate-x-1/2">
|
||||||
<div
|
<div
|
||||||
className={`
|
className="relative"
|
||||||
absolute bottom-full left-1/2 -translate-x-1/2 mb-2
|
onMouseEnter={(e) => showTooltip(e, date, completedHabits, incompleteHabits)}
|
||||||
opacity-0 invisible
|
onMouseLeave={hideTooltip}
|
||||||
group-hover:opacity-100 group-hover:visible
|
|
||||||
transition-all duration-150 ease-in-out
|
|
||||||
z-50 transform
|
|
||||||
translate-y-1 group-hover:translate-y-0
|
|
||||||
`}
|
|
||||||
>
|
>
|
||||||
<div className={`
|
<div className="flex items-center gap-1.5">
|
||||||
pointer-events-auto
|
{isToday && (
|
||||||
rounded-lg p-4
|
<div className="w-2 h-2 rounded-full bg-blue-500 dark:bg-blue-400" />
|
||||||
min-w-[200px] max-w-[300px]
|
|
||||||
${theme.calendar.tooltip.background}
|
|
||||||
${theme.calendar.tooltip.border}
|
|
||||||
${theme.calendar.tooltip.shadow}
|
|
||||||
border
|
|
||||||
backdrop-blur-sm
|
|
||||||
`}>
|
|
||||||
{completedHabits.length > 0 && (
|
|
||||||
<div className="mb-3">
|
|
||||||
<span className="text-[#2ecc71] font-semibold block mb-2">
|
|
||||||
✓ Completed
|
|
||||||
</span>
|
|
||||||
<ul className="space-y-1.5">
|
|
||||||
{completedHabits.map(habit => (
|
|
||||||
<li
|
|
||||||
key={habit.id}
|
|
||||||
className={`${theme.text} text-sm truncate flex items-center justify-between`}
|
|
||||||
>
|
|
||||||
<span>{habit.name}</span>
|
|
||||||
<button
|
|
||||||
onClick={(e) => handleToggleHabit(e, habit.id, date)}
|
|
||||||
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
|
|
||||||
>
|
|
||||||
<Check className="h-4 w-4 text-[#2ecc71]" />
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{incompleteHabits.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<span className="text-[#e74c3c] font-semibold block mb-2">
|
|
||||||
○ Pending
|
|
||||||
</span>
|
|
||||||
<ul className="space-y-1.5">
|
|
||||||
{incompleteHabits.map(habit => (
|
|
||||||
<li
|
|
||||||
key={habit.id}
|
|
||||||
className={`${theme.text} text-sm truncate flex items-center justify-between group`}
|
|
||||||
>
|
|
||||||
<span>{habit.name}</span>
|
|
||||||
<button
|
|
||||||
onClick={(e) => handleToggleHabit(e, habit.id, date)}
|
|
||||||
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded opacity-0 group-hover:opacity-100 transition-opacity"
|
|
||||||
>
|
|
||||||
<Check className="h-4 w-4 text-gray-400 hover:text-[#2ecc71]" />
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
h-6 px-2.5 rounded-full cursor-pointer
|
||||||
|
transition-all duration-200 flex items-center justify-center gap-1
|
||||||
|
${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-gray-100 dark:bg-gray-800 shadow-sm`
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<span className={`
|
||||||
|
text-xs font-medium
|
||||||
|
${completedHabits.length > 0
|
||||||
|
? 'text-green-700 dark:text-green-300'
|
||||||
|
: 'text-gray-600 dark:text-gray-400'
|
||||||
|
}
|
||||||
|
`}>
|
||||||
|
{completedHabits.length}/{habits.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
});
|
||||||
});
|
})()}
|
||||||
})()}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Updated tooltip portal with more subtle animations */}
|
||||||
|
{tooltipData && createPortal(
|
||||||
|
<div
|
||||||
|
ref={tooltipRef}
|
||||||
|
onMouseEnter={cancelHideTooltip}
|
||||||
|
onMouseLeave={hideTooltip}
|
||||||
|
className={`
|
||||||
|
fixed
|
||||||
|
transition-all duration-150 ease-in-out
|
||||||
|
${tooltipData.isVisible
|
||||||
|
? 'opacity-100 translate-y-0'
|
||||||
|
: 'opacity-0 translate-y-1'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
style={{
|
||||||
|
left: tooltipData.x,
|
||||||
|
top: tooltipData.y - 8,
|
||||||
|
transform: 'translate(-50%, -100%)',
|
||||||
|
pointerEvents: tooltipData.isVisible ? 'auto' : 'none',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
rounded-lg p-4
|
||||||
|
w-[200px]
|
||||||
|
${theme.calendar.tooltip.background}
|
||||||
|
${theme.calendar.tooltip.border}
|
||||||
|
shadow-[0_10px_38px_-10px_rgba(22,23,24,0.35),0_10px_20px_-15px_rgba(22,23,24,0.2)]
|
||||||
|
border
|
||||||
|
backdrop-blur-sm
|
||||||
|
relative
|
||||||
|
transition-all duration-150 ease-in-out
|
||||||
|
scale-100 origin-[bottom]
|
||||||
|
${tooltipData.isVisible ? 'scale-100' : 'scale-98'}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{/* Arrow */}
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
absolute -bottom-2 left-1/2 -translate-x-1/2
|
||||||
|
w-4 h-4 rotate-45
|
||||||
|
${theme.calendar.tooltip.background}
|
||||||
|
${theme.calendar.tooltip.border}
|
||||||
|
border-t-0 border-l-0
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
{tooltipData.completedHabits.length > 0 && (
|
||||||
|
<div className="mb-3">
|
||||||
|
<span className="text-[#2ecc71] font-semibold block mb-2">
|
||||||
|
✓ Completed
|
||||||
|
</span>
|
||||||
|
<ul className="space-y-1.5">
|
||||||
|
{tooltipData.completedHabits.map(habit => (
|
||||||
|
<li
|
||||||
|
key={habit.id}
|
||||||
|
className={`${theme.text} text-sm truncate flex items-center justify-between`}
|
||||||
|
>
|
||||||
|
<span>{habit.name}</span>
|
||||||
|
<button
|
||||||
|
onClick={(e) => handleToggleHabit(e, habit.id, tooltipData.date)}
|
||||||
|
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
|
||||||
|
>
|
||||||
|
<Check className="h-4 w-4 text-[#2ecc71]" />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{tooltipData.incompleteHabits.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<span className="text-[#e74c3c] font-semibold block mb-2">
|
||||||
|
○ Pending
|
||||||
|
</span>
|
||||||
|
<ul className="space-y-1.5">
|
||||||
|
{tooltipData.incompleteHabits.map(habit => (
|
||||||
|
<li
|
||||||
|
key={habit.id}
|
||||||
|
className={`${theme.text} text-sm truncate flex items-center justify-between group`}
|
||||||
|
>
|
||||||
|
<span>{habit.name}</span>
|
||||||
|
<button
|
||||||
|
onClick={(e) => handleToggleHabit(e, habit.id, tooltipData.date)}
|
||||||
|
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
>
|
||||||
|
<Check className="h-4 w-4 text-gray-400 hover:text-[#2ecc71]" />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue