Files
edr-platform/apps/edr-passenger-web/portal/src/components/DualCalendarPicker.tsx
2026-06-02 21:58:09 +03:00

363 lines
12 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { ChevronLeft, ChevronRight, Calendar as CalendarIcon, Globe } from 'lucide-react';
import {
gregorianToEthiopian,
ethiopianToGregorian,
formatEthiopianDate,
getDaysInEthiopianMonth,
ETHIOPIAN_MONTHS,
type EthiopianDate,
} from '@/lib/ethiopian-calendar';
import { format } from 'date-fns';
interface DualCalendarPickerProps {
value?: Date;
onChange: (date: Date) => void;
minDate?: Date;
maxDate?: Date;
placeholder?: string;
}
export default function DualCalendarPicker({
value,
onChange,
minDate,
maxDate,
placeholder = 'Select date',
}: DualCalendarPickerProps) {
const [isOpen, setIsOpen] = useState(false);
const [calendarType, setCalendarType] = useState<'gregorian' | 'ethiopian'>('gregorian');
const [_currentDate, setCurrentDate] = useState(value || new Date());
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);
useEffect(() => {
if (value) {
setCurrentDate(value);
setViewMonth(value.getMonth());
setViewYear(value.getFullYear());
const ethDate = gregorianToEthiopian(value);
setEthViewMonth(ethDate.month);
setEthViewYear(ethDate.year);
}
}, [value]);
const toggleCalendarType = () => {
const newType = calendarType === 'gregorian' ? 'ethiopian' : 'gregorian';
if (newType === 'ethiopian') {
// Sync Ethiopian view to current Gregorian view
const currentViewDate = new Date(viewYear, viewMonth, 15);
const ethDate = gregorianToEthiopian(currentViewDate);
setEthViewMonth(ethDate.month);
setEthViewYear(ethDate.year);
} else {
// Sync Gregorian view to current Ethiopian view
const currentEthDate = { year: ethViewYear, month: ethViewMonth, day: 15 };
const gregDate = ethiopianToGregorian(currentEthDate);
setViewMonth(gregDate.getMonth());
setViewYear(gregDate.getFullYear());
}
setCalendarType(newType);
};
const handleDateSelect = (date: Date) => {
onChange(date);
setCurrentDate(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)[] = [];
// Add empty cells for days before month starts
for (let i = 0; i < firstDayOfMonth; i++) {
days.push(null);
}
// Add days of month
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">
{/* Header */}
<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 rounded-lg"
>
<ChevronLeft className="w-5 h-5" />
</button>
<div className="font-semibold text-lg">
{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 rounded-lg"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
{/* Day headers */}
<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-medium text-gray-600 py-2">
{day}
</div>
))}
</div>
{/* Calendar grid */}
<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 < minDate) ||
(maxDate && date > maxDate);
return (
<button
key={day}
type="button"
onClick={() => !isDisabled && handleDateSelect(date)}
disabled={isDisabled}
className={`
p-2 text-sm rounded-lg transition-colors
${isSelected ? 'bg-primary text-white font-semibold' : ''}
${isToday && !isSelected ? 'border-2 border-primary text-primary font-semibold' : ''}
${!isSelected && !isToday ? 'hover:bg-gray-100' : ''}
${isDisabled ? 'text-gray-300 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);
}
// Calculate first day offset (Ethiopian week starts on Sunday)
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">
{/* Header */}
<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 rounded-lg"
>
<ChevronLeft className="w-5 h-5" />
</button>
<div className="font-semibold text-lg">
{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 rounded-lg"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
{/* Day headers */}
<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-medium text-gray-600 py-2">
{day}
</div>
))}
</div>
{/* Calendar grid */}
<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;
const isDisabled =
(minDate && gregDate < minDate) ||
(maxDate && gregDate > maxDate);
return (
<button
key={day}
type="button"
onClick={() => !isDisabled && handleEthiopianDateSelect(ethDate)}
disabled={isDisabled}
className={`
p-2 text-sm rounded-lg transition-colors
${isSelected ? 'bg-primary text-white font-semibold' : ''}
${isToday && !isSelected ? 'border-2 border-primary text-primary font-semibold' : ''}
${!isSelected && !isToday ? 'hover:bg-gray-100' : ''}
${isDisabled ? 'text-gray-300 cursor-not-allowed' : ''}
`}
>
{day}
</button>
);
})}
</div>
</div>
);
};
return (
<div className="relative">
{/* Input trigger */}
<button
type="button"
onClick={() => setIsOpen(!isOpen)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent text-left flex items-center justify-between bg-white"
>
<span className={value ? 'text-gray-900' : 'text-gray-500'}>
{value ? format(value, 'MMMM d, yyyy') : placeholder}
</span>
<CalendarIcon className="w-5 h-5 text-gray-400" />
</button>
{/* Calendar dropdown */}
{isOpen && (
<>
<div
className="fixed inset-0 z-10"
onClick={() => setIsOpen(false)}
/>
<div className="absolute z-20 mt-2 bg-white rounded-xl shadow-2xl border border-gray-200 w-80">
{/* Calendar type toggle */}
<div className="border-b border-gray-200 p-3 flex items-center justify-between">
<div className="text-sm font-medium text-gray-700">
{calendarType === 'gregorian' ? 'Gregorian Calendar' : 'Ethiopian Calendar'}
</div>
<button
type="button"
onClick={toggleCalendarType}
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
>
<Globe className="w-4 h-4" />
Switch to {calendarType === 'gregorian' ? 'Ethiopian' : 'Gregorian'}
</button>
</div>
{/* Calendar content */}
{calendarType === 'gregorian' ? renderGregorianCalendar() : renderEthiopianCalendar()}
{/* Footer with selected date info */}
{value && (
<div className="border-t border-gray-200 p-3 text-xs text-gray-600 space-y-1">
<div>
<span className="font-medium">Gregorian:</span> {format(value, 'MMMM d, yyyy')}
</div>
<div>
<span className="font-medium">Ethiopian:</span> {formatEthiopianDate(gregorianToEthiopian(value))}
</div>
</div>
)}
</div>
</>
)}
</div>
);
}