import React, { useState, useRef, useEffect } from 'react'; import { createPortal } from 'react-dom'; import { ChevronLeftIcon, ChevronRightIcon, CalendarDaysIcon, } from '@heroicons/react/24/outline'; interface DatePickerProps { value: string; onChange: (value: string) => void; placeholder?: string; disabled?: boolean; className?: string; } const DatePicker: React.FC = ({ value, onChange, placeholder = 'Select date', disabled = false, className = '', }) => { const [isOpen, setIsOpen] = useState(false); const [position, setPosition] = useState({ top: 0, left: 0, width: 0, openUpward: false, }); const [currentMonth, setCurrentMonth] = useState(new Date()); const dropdownRef = useRef(null); const menuRef = useRef(null); const months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ]; const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; const formatDate = (date: Date) => { 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 parseDate = (dateString: string) => { return dateString ? new Date(dateString + 'T00:00:00') : null; }; const formatDisplayDate = (dateString: string) => { if (!dateString) return placeholder; const date = parseDate(dateString); if (!date || isNaN(date.getTime())) return placeholder; return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', }); }; const handleToggle = () => { if (disabled) return; if (!isOpen && dropdownRef.current) { const rect = dropdownRef.current.getBoundingClientRect(); const spaceBelow = window.innerHeight - rect.bottom; const spaceAbove = rect.top; const menuHeight = 320; // Calendar height const padding = 16; // Extra padding from viewport edges // Determine if we should open upward const wouldFitBelow = spaceBelow >= menuHeight + padding; const wouldFitAbove = spaceAbove >= menuHeight + padding; let openUpward = false; let top = rect.bottom + 8; if (!wouldFitBelow && wouldFitAbove) { // Open upward if it fits above but not below openUpward = true; top = rect.top - menuHeight - 8; } else if (!wouldFitBelow && !wouldFitAbove) { // If it doesn't fit in either direction, choose the side with more space if (spaceAbove > spaceBelow) { openUpward = true; top = Math.max(padding, rect.top - menuHeight - 8); } else { top = Math.min( window.innerHeight - menuHeight - padding, rect.bottom + 8 ); } } // Ensure left position doesn't go off screen const left = Math.min( Math.max(padding, rect.left), window.innerWidth - Math.max(rect.width, 280) - padding ); setPosition({ top, left, width: Math.max(rect.width, 280), // Minimum width for calendar openUpward, }); // Set current month based on selected date or today if (value) { const selectedDate = parseDate(value); if (selectedDate && !isNaN(selectedDate.getTime())) { setCurrentMonth( new Date( selectedDate.getFullYear(), selectedDate.getMonth(), 1 ) ); } } else { setCurrentMonth( new Date(new Date().getFullYear(), new Date().getMonth(), 1) ); } } setIsOpen(!isOpen); }; const handleClickOutside = (event: MouseEvent) => { if ( dropdownRef.current && !dropdownRef.current.contains(event.target as Node) && menuRef.current && !menuRef.current.contains(event.target as Node) ) { setIsOpen(false); } }; const handleDateSelect = (date: Date) => { try { onChange(formatDate(date)); setIsOpen(false); } catch (error) { console.error('Error in date selection:', error); setIsOpen(false); } }; const handleClear = (e: React.MouseEvent) => { e.stopPropagation(); onChange(''); setIsOpen(false); }; const navigateMonth = (direction: 'prev' | 'next') => { setCurrentMonth((prev) => { const newMonth = new Date(prev); if (direction === 'prev') { newMonth.setMonth(newMonth.getMonth() - 1); } else { newMonth.setMonth(newMonth.getMonth() + 1); } return newMonth; }); }; const getDaysInMonth = () => { const year = currentMonth.getFullYear(); const month = currentMonth.getMonth(); const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const daysInMonth = lastDay.getDate(); const startingDayOfWeek = firstDay.getDay(); const days = []; // Add empty cells for days before the first day of the month for (let i = 0; i < startingDayOfWeek; i++) { days.push(null); } // Add days of the month for (let day = 1; day <= daysInMonth; day++) { days.push(new Date(year, month, day)); } return days; }; const isToday = (date: Date) => { const today = new Date(); return date.toDateString() === today.toDateString(); }; const isSelected = (date: Date) => { if (!value) return false; const selectedDate = parseDate(value); return ( selectedDate && date.toDateString() === selectedDate.toDateString() ); }; useEffect(() => { if (isOpen) { document.addEventListener('mousedown', handleClickOutside); } else { document.removeEventListener('mousedown', handleClickOutside); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [isOpen]); return (
)}
{isOpen && createPortal(
e.stopPropagation()} > {/* Calendar Header */}
{months[currentMonth.getMonth()]}{' '} {currentMonth.getFullYear()}
{/* Calendar Grid */}
{/* Day Headers */}
{days.map((day) => (
{day}
))}
{/* Calendar Days */}
{getDaysInMonth().map((date, index) => (
{date && ( )}
))}
{/* Footer */}
{value && ( )}
, document.body )} ); }; export default DatePicker;