import React, { useState, useRef, useEffect, useMemo } from 'react'; import { createPortal } from 'react-dom'; import { ChevronLeftIcon, ChevronRightIcon, ClockIcon, } from '@heroicons/react/24/outline'; import { getFirstDayOfWeek, getLocaleFirstDayOfWeek, } from '../../utils/profileService'; import { useTranslation } from 'react-i18next'; import { resolveUserLocale } from '../../utils/localeUtils'; interface DateTimePickerProps { value: string; // ISO string onChange: (value: string) => void; placeholder?: string; disabled?: boolean; className?: string; } const DateTimePicker: React.FC = ({ value, onChange, placeholder = 'Select date and time', disabled = false, className = '', }) => { const { i18n } = useTranslation(); const displayLocale = useMemo( () => resolveUserLocale(i18n?.language), [i18n?.language] ); const [isOpen, setIsOpen] = useState(false); const [position, setPosition] = useState({ top: 0, left: 0, width: 0, openUpward: false, }); const [currentMonth, setCurrentMonth] = useState(new Date()); const [selectedDate, setSelectedDate] = useState(null); const [selectedTime, setSelectedTime] = useState('12:00'); const [firstDayOfWeek, setFirstDayOfWeek] = useState(0); const dropdownRef = useRef(null); const menuRef = useRef(null); const months = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December', ]; // Generate time options in 15-minute increments const generateTimeOptions = () => { const options = []; for (let hour = 0; hour < 24; hour++) { for (let minute = 0; minute < 60; minute += 15) { const h = String(hour).padStart(2, '0'); const m = String(minute).padStart(2, '0'); options.push(`${h}:${m}`); } } return options; }; const timeOptions = generateTimeOptions(); const getAllDays = () => ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; const getDaysOfWeek = () => { const allDays = getAllDays(); return [ ...allDays.slice(firstDayOfWeek), ...allDays.slice(0, firstDayOfWeek), ]; }; const parseDateTime = ( isoString: string ): { date: Date | null; time: string } => { if (!isoString) return { date: null, time: '12:00' }; try { const date = new Date(isoString); if (isNaN(date.getTime())) return { date: null, time: '12:00' }; const hours = String(date.getHours()).padStart(2, '0'); const minutes = String( Math.floor(date.getMinutes() / 15) * 15 ).padStart(2, '0'); return { date, time: `${hours}:${minutes}` }; } catch { return { date: null, time: '12:00' }; } }; const formatDisplayDateTime = (isoString: string) => { if (!isoString) return placeholder; const { date, time } = parseDateTime(isoString); if (!date) return placeholder; const displayDate = new Date(date); if (time) { const [hours, minutes] = time.split(':'); displayDate.setHours(parseInt(hours), parseInt(minutes), 0, 0); } const dateStr = displayDate.toLocaleDateString(displayLocale, { year: 'numeric', month: 'short', day: 'numeric', }); const timeStr = displayDate.toLocaleTimeString(displayLocale, { hour: 'numeric', minute: '2-digit', }); return `${dateStr} at ${timeStr}`; }; useEffect(() => { const { date, time } = parseDateTime(value); if (date) { setSelectedDate(date); setSelectedTime(time); setCurrentMonth(new Date(date.getFullYear(), date.getMonth(), 1)); } }, [value]); 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 = 420; // Calendar + time picker height const padding = 16; const wouldFitBelow = spaceBelow >= menuHeight + padding; const wouldFitAbove = spaceAbove >= menuHeight + padding; let openUpward = false; let top = rect.bottom + 8; if (!wouldFitBelow && wouldFitAbove) { openUpward = true; top = rect.top - menuHeight - 8; } else if (!wouldFitBelow && !wouldFitAbove) { if (spaceAbove > spaceBelow) { openUpward = true; top = Math.max(padding, rect.top - menuHeight - 8); } else { top = Math.min( window.innerHeight - menuHeight - padding, rect.bottom + 8 ); } } const calendarWidth = Math.min(Math.max(rect.width, 280), 320); const left = Math.min( Math.max(padding, rect.left), window.innerWidth - calendarWidth - padding ); setPosition({ top, left, width: calendarWidth, openUpward, }); } 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) => { setSelectedDate(date); }; const handleApply = () => { if (!selectedDate) return; const [hours, minutes] = selectedTime.split(':'); const dateTime = new Date(selectedDate); dateTime.setHours(parseInt(hours), parseInt(minutes), 0, 0); onChange(dateTime.toISOString()); setIsOpen(false); }; const handleClear = (e: React.MouseEvent) => { e.stopPropagation(); onChange(''); setSelectedDate(null); setSelectedTime('12:00'); 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 adjustedStartingDay = (startingDayOfWeek - firstDayOfWeek + 7) % 7; const days = []; for (let i = 0; i < adjustedStartingDay; i++) { days.push(null); } 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) => { return ( selectedDate && date.toDateString() === selectedDate.toDateString() ); }; useEffect(() => { const loadFirstDayOfWeek = async () => { try { const firstDay = await getFirstDayOfWeek(); setFirstDayOfWeek(firstDay); } catch { const fallbackFirstDay = getLocaleFirstDayOfWeek( navigator.language ); setFirstDayOfWeek(fallbackFirstDay); } }; loadFirstDayOfWeek(); }, []); useEffect(() => { if (isOpen) { document.addEventListener('mousedown', handleClickOutside); } else { document.removeEventListener('mousedown', handleClickOutside); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [isOpen]); return (
{value && ( )}
{isOpen && createPortal(
e.stopPropagation()} > {/* Calendar Header */}
{months[currentMonth.getMonth()]}{' '} {currentMonth.getFullYear()}
{/* Calendar Grid */}
{getDaysOfWeek().map((day) => (
{day}
))}
{getDaysInMonth().map((date, index) => (
{date && ( )}
))}
{/* Time Picker */}
{/* Footer */}
{value && ( )}
, document.body )}
); }; export default DateTimePicker;