mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 08:48:11 +00:00
396 lines
15 KiB
TypeScript
396 lines
15 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useRef } from 'react';
|
|
import { ChevronLeft, ChevronRight, Calendar as CalendarIcon, Globe, X } from 'lucide-react';
|
|
import {
|
|
gregorianToEthiopian,
|
|
ethiopianToGregorian,
|
|
formatEthiopianDate,
|
|
getDaysInEthiopianMonth,
|
|
ETHIOPIAN_MONTHS,
|
|
type EthiopianDate,
|
|
} from '@/lib/ethiopian-calendar';
|
|
import { format } from 'date-fns';
|
|
|
|
interface ModernDatePickerProps {
|
|
value?: Date;
|
|
onChange: (date: Date) => void;
|
|
minDate?: Date;
|
|
maxDate?: Date;
|
|
placeholder?: string;
|
|
}
|
|
|
|
export default function ModernDatePicker({
|
|
value,
|
|
onChange,
|
|
minDate,
|
|
maxDate,
|
|
placeholder = 'Select date',
|
|
}: ModernDatePickerProps) {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [calendarType, setCalendarType] = useState<'gregorian' | 'ethiopian'>('gregorian');
|
|
const [viewMonth, setViewMonth] = useState(value?.getMonth() || new Date().getMonth());
|
|
const [viewYear, setViewYear] = useState(value?.getFullYear() || new Date().getFullYear());
|
|
|
|
// Initialize Ethiopian calendar with current date
|
|
const initialEthDate = gregorianToEthiopian(value || new Date());
|
|
const [ethViewMonth, setEthViewMonth] = useState(initialEthDate.month);
|
|
const [ethViewYear, setEthViewYear] = useState(initialEthDate.year);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (value) {
|
|
setViewMonth(value.getMonth());
|
|
setViewYear(value.getFullYear());
|
|
const ethDate = gregorianToEthiopian(value);
|
|
setEthViewMonth(ethDate.month);
|
|
setEthViewYear(ethDate.year);
|
|
}
|
|
}, [value]);
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
setIsOpen(false);
|
|
}
|
|
};
|
|
|
|
if (isOpen) {
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
}
|
|
|
|
return () => {
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
};
|
|
}, [isOpen]);
|
|
|
|
const toggleCalendarType = () => {
|
|
const newType = calendarType === 'gregorian' ? 'ethiopian' : 'gregorian';
|
|
|
|
if (newType === 'ethiopian') {
|
|
// When switching to Ethiopian, show the Ethiopian equivalent of current Gregorian view
|
|
// Use today's date if no value is selected, otherwise use the selected value
|
|
const referenceDate = value || new Date();
|
|
const ethDate = gregorianToEthiopian(referenceDate);
|
|
setEthViewMonth(ethDate.month);
|
|
setEthViewYear(ethDate.year);
|
|
} else {
|
|
// When switching to Gregorian, show the Gregorian equivalent of current Ethiopian view
|
|
const referenceDate = value || new Date();
|
|
setViewMonth(referenceDate.getMonth());
|
|
setViewYear(referenceDate.getFullYear());
|
|
}
|
|
|
|
setCalendarType(newType);
|
|
};
|
|
|
|
const handleDateSelect = (date: Date) => {
|
|
onChange(date);
|
|
setIsOpen(false);
|
|
};
|
|
|
|
const handleEthiopianDateSelect = (ethDate: EthiopianDate) => {
|
|
const gregDate = ethiopianToGregorian(ethDate);
|
|
handleDateSelect(gregDate);
|
|
};
|
|
|
|
const renderGregorianCalendar = () => {
|
|
const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
|
|
const firstDayOfMonth = new Date(viewYear, viewMonth, 1).getDay();
|
|
const days: (number | null)[] = [];
|
|
|
|
for (let i = 0; i < firstDayOfMonth; i++) {
|
|
days.push(null);
|
|
}
|
|
|
|
for (let day = 1; day <= daysInMonth; day++) {
|
|
days.push(day);
|
|
}
|
|
|
|
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
|
|
return (
|
|
<div className="p-4">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
if (viewMonth === 0) {
|
|
setViewMonth(11);
|
|
setViewYear(viewYear - 1);
|
|
} else {
|
|
setViewMonth(viewMonth - 1);
|
|
}
|
|
}}
|
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
>
|
|
<ChevronLeft className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
|
</button>
|
|
<div className="font-semibold text-base text-gray-900 dark:text-gray-100">
|
|
{monthNames[viewMonth]} {viewYear}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
if (viewMonth === 11) {
|
|
setViewMonth(0);
|
|
setViewYear(viewYear + 1);
|
|
} else {
|
|
setViewMonth(viewMonth + 1);
|
|
}
|
|
}}
|
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
>
|
|
<ChevronRight className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-7 gap-1 mb-2">
|
|
{['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].map(day => (
|
|
<div key={day} className="text-center text-xs font-semibold text-gray-500 dark:text-gray-400 py-2">
|
|
{day}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-7 gap-1">
|
|
{days.map((day, index) => {
|
|
if (day === null) {
|
|
return <div key={`empty-${index}`} />;
|
|
}
|
|
|
|
const date = new Date(viewYear, viewMonth, day);
|
|
const isSelected = value &&
|
|
date.getDate() === value.getDate() &&
|
|
date.getMonth() === value.getMonth() &&
|
|
date.getFullYear() === value.getFullYear();
|
|
const isToday =
|
|
date.getDate() === new Date().getDate() &&
|
|
date.getMonth() === new Date().getMonth() &&
|
|
date.getFullYear() === new Date().getFullYear();
|
|
const isDisabled =
|
|
(minDate && date < new Date(minDate.getFullYear(), minDate.getMonth(), minDate.getDate())) ||
|
|
(maxDate && date > new Date(maxDate.getFullYear(), maxDate.getMonth(), maxDate.getDate()));
|
|
|
|
return (
|
|
<button
|
|
key={day}
|
|
type="button"
|
|
onClick={() => !isDisabled && handleDateSelect(date)}
|
|
disabled={isDisabled}
|
|
className={`
|
|
aspect-square flex items-center justify-center text-sm rounded-lg transition-all
|
|
${isSelected ? 'bg-primary text-white font-semibold shadow-md scale-105' : ''}
|
|
${isToday && !isSelected ? 'border-2 border-primary text-primary font-semibold' : ''}
|
|
${!isSelected && !isToday && !isDisabled ? 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300' : ''}
|
|
${isDisabled ? 'text-gray-300 dark:text-gray-600 cursor-not-allowed' : ''}
|
|
`}
|
|
>
|
|
{day}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const renderEthiopianCalendar = () => {
|
|
const daysInMonth = getDaysInEthiopianMonth(ethViewYear, ethViewMonth);
|
|
const days: number[] = [];
|
|
|
|
for (let day = 1; day <= daysInMonth; day++) {
|
|
days.push(day);
|
|
}
|
|
|
|
const firstDate = ethiopianToGregorian({ year: ethViewYear, month: ethViewMonth, day: 1 });
|
|
const firstDayOfWeek = firstDate.getDay();
|
|
|
|
const daysWithOffset: (number | null)[] = [];
|
|
for (let i = 0; i < firstDayOfWeek; i++) {
|
|
daysWithOffset.push(null);
|
|
}
|
|
daysWithOffset.push(...days);
|
|
|
|
// Get month name safely
|
|
const monthName = ETHIOPIAN_MONTHS[ethViewMonth - 1] || `Month ${ethViewMonth}`;
|
|
|
|
return (
|
|
<div className="p-4">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
if (ethViewMonth === 1) {
|
|
setEthViewMonth(13);
|
|
setEthViewYear(ethViewYear - 1);
|
|
} else {
|
|
setEthViewMonth(ethViewMonth - 1);
|
|
}
|
|
}}
|
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
>
|
|
<ChevronLeft className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
|
</button>
|
|
<div className="font-semibold text-base text-gray-900 dark:text-gray-100">
|
|
{monthName} {ethViewYear}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
if (ethViewMonth === 13) {
|
|
setEthViewMonth(1);
|
|
setEthViewYear(ethViewYear + 1);
|
|
} else {
|
|
setEthViewMonth(ethViewMonth + 1);
|
|
}
|
|
}}
|
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
>
|
|
<ChevronRight className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-7 gap-1 mb-2">
|
|
{['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].map(day => (
|
|
<div key={day} className="text-center text-xs font-semibold text-gray-500 dark:text-gray-400 py-2">
|
|
{day}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="grid grid-cols-7 gap-1">
|
|
{daysWithOffset.map((day, index) => {
|
|
if (day === null) {
|
|
return <div key={`empty-${index}`} />;
|
|
}
|
|
|
|
const ethDate: EthiopianDate = { year: ethViewYear, month: ethViewMonth, day };
|
|
const gregDate = ethiopianToGregorian(ethDate);
|
|
|
|
const isSelected = value &&
|
|
gregDate.getDate() === value.getDate() &&
|
|
gregDate.getMonth() === value.getMonth() &&
|
|
gregDate.getFullYear() === value.getFullYear();
|
|
|
|
const today = new Date();
|
|
const todayEth = gregorianToEthiopian(today);
|
|
const isToday =
|
|
ethDate.day === todayEth.day &&
|
|
ethDate.month === todayEth.month &&
|
|
ethDate.year === todayEth.year;
|
|
|
|
let isDisabled = false;
|
|
if (minDate) {
|
|
const minYear = minDate.getFullYear();
|
|
const minMonth = minDate.getMonth();
|
|
const minDay = minDate.getDate();
|
|
const gregYear = gregDate.getFullYear();
|
|
const gregMonth = gregDate.getMonth();
|
|
const gregDay = gregDate.getDate();
|
|
|
|
if (gregYear < minYear ||
|
|
(gregYear === minYear && gregMonth < minMonth) ||
|
|
(gregYear === minYear && gregMonth === minMonth && gregDay < minDay)) {
|
|
isDisabled = true;
|
|
}
|
|
}
|
|
if (maxDate && !isDisabled) {
|
|
const maxYear = maxDate.getFullYear();
|
|
const maxMonth = maxDate.getMonth();
|
|
const maxDay = maxDate.getDate();
|
|
const gregYear = gregDate.getFullYear();
|
|
const gregMonth = gregDate.getMonth();
|
|
const gregDay = gregDate.getDate();
|
|
|
|
if (gregYear > maxYear ||
|
|
(gregYear === maxYear && gregMonth > maxMonth) ||
|
|
(gregYear === maxYear && gregMonth === maxMonth && gregDay > maxDay)) {
|
|
isDisabled = true;
|
|
}
|
|
}
|
|
|
|
return (
|
|
<button
|
|
key={day}
|
|
type="button"
|
|
onClick={() => !isDisabled && handleEthiopianDateSelect(ethDate)}
|
|
disabled={isDisabled}
|
|
className={`
|
|
aspect-square flex items-center justify-center text-sm rounded-lg transition-all
|
|
${isSelected ? 'bg-primary text-white font-semibold shadow-md scale-105' : ''}
|
|
${isToday && !isSelected ? 'border-2 border-primary text-primary font-semibold' : ''}
|
|
${!isSelected && !isToday && !isDisabled ? 'hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300' : ''}
|
|
${isDisabled ? 'text-gray-300 dark:text-gray-600 cursor-not-allowed' : ''}
|
|
`}
|
|
>
|
|
{day}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="relative" ref={containerRef}>
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsOpen(!isOpen)}
|
|
className="w-full px-4 py-3.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-left flex items-center justify-between bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors group"
|
|
>
|
|
<span className={value ? 'text-gray-900 dark:text-gray-100' : 'text-gray-500 dark:text-gray-400'}>
|
|
{value ? format(value, 'EEEE, MMMM d, yyyy') : placeholder}
|
|
</span>
|
|
<CalendarIcon className="w-5 h-5 text-gray-400 dark:text-gray-500 group-hover:text-primary transition-colors" />
|
|
</button>
|
|
|
|
{isOpen && (
|
|
<div className="absolute z-50 mt-2 bg-white dark:bg-gray-800 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-700 w-80 animate-in fade-in slide-in-from-top-2 duration-200">
|
|
<div className="flex items-center justify-between p-3 border-b border-gray-200 dark:border-gray-700">
|
|
<div className="flex items-center gap-2">
|
|
<CalendarIcon className="w-4 h-4 text-primary" />
|
|
<span className="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
|
{calendarType === 'gregorian' ? 'Gregorian Calendar' : 'Ethiopian Calendar'}
|
|
</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={toggleCalendarType}
|
|
className="flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium bg-primary/10 hover:bg-primary/20 dark:bg-primary/20 dark:hover:bg-primary/30 text-primary rounded-lg transition-colors"
|
|
>
|
|
<Globe className="w-3.5 h-3.5" />
|
|
{calendarType === 'gregorian' ? 'ET' : 'GC'}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsOpen(false)}
|
|
className="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
>
|
|
<X className="w-4 h-4 text-gray-500 dark:text-gray-400" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{calendarType === 'gregorian' ? renderGregorianCalendar() : renderEthiopianCalendar()}
|
|
|
|
{value && (
|
|
<div className="border-t border-gray-200 dark:border-gray-700 p-3 bg-gray-50 dark:bg-gray-900/50 space-y-1.5">
|
|
<div className="flex items-center justify-between text-xs">
|
|
<span className="text-gray-500 dark:text-gray-400">Gregorian:</span>
|
|
<span className="font-medium text-gray-700 dark:text-gray-300">{format(value, 'MMMM d, yyyy')}</span>
|
|
</div>
|
|
<div className="flex items-center justify-between text-xs">
|
|
<span className="text-gray-500 dark:text-gray-400">Ethiopian:</span>
|
|
<span className="font-medium text-gray-700 dark:text-gray-300">{formatEthiopianDate(gregorianToEthiopian(value))}</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|