Calendar

<Calendar 
    className=" w-60 " 
    initialDate={null} 
    placeholder={Select a date} 
    toDate={null} 
    fromDate={null} 
    theme={['red', 'green']} 
    onChange={e=>{console.log("Selected date:",e)}} 
/>

import React, { useState, useEffect, useRef } from 'react';
 


/**
 * @typedef {Object} CalendarProps
 * @property {string} [className] - Additional CSS class names to apply to the calendar container.
 * @property {Date} [initialDate] - The initial date to display in the calendar. Defaults to the current date.
 * @property {Date} [toDate] - The latest selectable date in the calendar.
 * @property {Date} [fromDate] - The earliest selectable date in the calendar.
 * @property {string} [position='absolute'] - The position of the calendar dropdown.
 * @property {string} [placeholder="Select date"] - The placeholder text for the input field.
 * @property {Function} [onChange] - A callback function that is triggered when a date is selected.
 * @property {Array<string>} [theme=['blue', 'green']] - An array of two color names for primary and secondary colors.
 *
 * A customizable and responsive calendar component for selecting dates.
 *
 * @param {CalendarProps} props - The properties for the calendar component.
 * @returns {JSX.Element} The rendered calendar component.
 * 
 * example * 
 * <Calendar 
 *     initialDate={new Date('2024-03-15')} 
 *     toDate={new Date('2025-03-15')} 
 *     fromDate={new Date('2023-03-15')} 
 *     placeholder="Select a date" 
 *     onChange={handleDateChange} 
 *     theme={['red', 'gray']} 
 * />
 * 
 */

const Calendar = ({ className, initialDate, toDate, fromDate,position, placeholder = "Select date", onChange, theme }) => {
    const [currentDate, setCurrentDate] = useState(initialDate || new Date());
    const [selectedDate, setSelectedDate] = useState(null);
    const [isOpen, setIsOpen] = useState(false);
    const calendarRef = useRef(null);

    const daysInWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
    const monthNames = [
        'January', 'February', 'March', 'April', 'May', 'June',
        'July', 'August', 'September', 'October', 'November', 'December'
    ];

    // Default theme: blue, gray
    const defaultTheme = ['blue', 'green'];
    const [primary, secondary] = theme || defaultTheme;

    const getDaysInMonth = (month, year) => {
        const date = new Date(year, month, 1);
        const days = [];
        while (date.getMonth() === month) {
            days.push(new Date(date));
            date.setDate(date.getDate() + 1);
        }
        return days;
    };

    const goToPreviousMonth = () => {
        const newMonth = currentDate.getMonth() - 1;
        const newYear = currentDate.getFullYear();

        // Check if the new month is within the allowed range
        const newDate = new Date(newYear, newMonth);
        if (isValidMonth(newDate)) {
            setCurrentDate(newDate);
        }
    };

    const goToNextMonth = () => {
        const newMonth = currentDate.getMonth() + 1;
        const newYear = currentDate.getFullYear();

        // Check if the new month is within the allowed range
        const newDate = new Date(newYear, newMonth);
        if (isValidMonth(newDate)) {
            setCurrentDate(newDate);
        }
    };

    const goToToday = () => {
        const today = new Date();
        setCurrentDate(today);
        setSelectedDate(today);
        if (onChange) {
            onChange(today);
        }
    };

    const handleDateClick = (day) => {
        // Check if the date is within the allowed range (if provided)
        const isInAllowedRange =
            (!toDate && !fromDate) ||
            (toDate && !fromDate && day <= toDate) ||
            (!toDate && fromDate && day >= fromDate) ||
            (toDate && fromDate && day >= toDate && day <= fromDate);


        // Only update the selected date if it's within the allowed range
        if (isInAllowedRange) {
            setSelectedDate(day);
            if (onChange) {
                onChange(day);
            }
            setIsOpen(false);
        }
    };

    const handleYearChange = (event) => {
        const newYear = parseInt(event.target.value, 10);
        const newDate = new Date(newYear, currentDate.getMonth());

        // Check if the new year is within the allowed range
        if (isValidMonth(newDate)) {
            setCurrentDate(newDate);
        }
    };

    const handleMonthChange = (event) => {
        const newMonth = parseInt(event.target.value, 10);
        const newDate = new Date(currentDate.getFullYear(), newMonth);

        // Check if the new month is within the allowed range
        if (isValidMonth(newDate)) {
            setCurrentDate(newDate);
        }
    };

    const handleClickOutside = (event) => {
        if (calendarRef.current && !calendarRef.current.contains(event.target)) {
            setIsOpen(false);
        }
    };

    useEffect(() => {
        document.addEventListener('mousedown', handleClickOutside);
        return () => {
            document.removeEventListener('mousedown', handleClickOutside);
        };
    }, []);

    const renderHeader = () => {
        return (
            <div className="flex justify-between items-center p-2 gap-2 ">
                <button onClick={goToPreviousMonth} className="hover:text-blue-500">
                    {"<"}
                </button>
                <div className="flex gap-2">
                    <select
                        value={currentDate.getMonth()}
                        onChange={handleMonthChange}
                        className={` text-gray-700 border rounded-md px-2 py-1 border-${secondary} focus:outline-none focus:ring-1 focus:ring-${primary}-500 focus:ring-opacity-50`}
                    >
                        {monthNames.map((month, index) => (
                            <option key={index} value={index}>
                                {month}
                            </option>
                        ))}
                    </select>
                    <select
                        value={currentDate.getFullYear()}
                        onChange={handleYearChange}
                        className={` text-gray-700 border rounded-md px-2 py-1 border-${secondary} focus:outline-none focus:ring-1 focus:ring-${primary}-500 focus:ring-opacity-50`}
                    >
                        {Array.from({ length: 10 }, (_, i) => currentDate.getFullYear() - 5 + i).map((year) => (
                            <option key={year} value={year}>
                                {year}
                            </option>
                        ))}
                    </select>
                </div>
                <button onClick={goToNextMonth} className="hover:text-blue-500">
                    {">"}
                </button>
            </div>
        );
    };

    const renderDays = () => {
        const days = getDaysInMonth(currentDate.getMonth(), currentDate.getFullYear());
        const firstDayIndex = days[0].getDay();
        const lastDayIndex = days[days.length - 1].getDay();

        // Calculate leading days
        const prevMonthLastDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), 0);
        const prevDays = [];
        for (let i = 0; i < firstDayIndex; i++) {
            prevDays.unshift(new Date(
                prevMonthLastDate.getFullYear(), prevMonthLastDate.getMonth(), prevMonthLastDate.getDate() - i));
        }

        const nextDays = [];
        for (let i = 1; i <= 6 - lastDayIndex; i++) {
            nextDays.push(new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, i));
        }

        return [
            ...prevDays.map((day) => ({ day, isPlaceholder: true })),
            ...days.map((day) => ({ day, isPlaceholder: false })),
            ...nextDays.map((day) => ({ day, isPlaceholder: true }))
        ].map(({ day, isPlaceholder }, index) => {
            const isToday = day.toDateString() === new Date().toDateString();
            const isSelected = day.toDateString() === selectedDate?.toDateString();

            const isInAllowedRange =
                (!toDate && !fromDate) ||
                (toDate && !fromDate && day <= toDate) ||
                (!toDate && fromDate && day >= fromDate) ||
                (toDate && fromDate && day >= toDate && day <= fromDate);

            const isDisabled = isPlaceholder || !isInAllowedRange;


            return (
                <button
                    key={index}
                    onClick={() => !isDisabled && handleDateClick(day)}
                    disabled={isDisabled} // Disable the button if needed
                    className={`
                        text-center rounded-full border-0 w-9 flex items-center justify-center h-9 p-2 relative transition duration-200 ease-in-out cursor-pointer
                        ${isPlaceholder ? ` text-gray-300 border-transparent` : ` `} 
                        ${isSelected ? ` bg-${primary}-500 text-white border-${primary}-500` : ''} 
                        ${isToday ? `  border-2 border-${primary}-500` : ''} 
                        ${isToday && !isSelected ? ` text-${primary} border-${primary}-200` : ''}  
                        ${!isPlaceholder && !isDisabled && `text-gray-700 duration-200 hover:border-2  `}  
                        ${isDisabled ? `cursor-default text-gray-300` : ''} `}
                >
                    {day.getDate()}
                </button>
            );
        });
    };

    // Helper function to check if a month is within the allowed range
    const isValidMonth = (date) => {
        if (toDate && fromDate) {
            const startMonth = toDate.getMonth();
            const startYear = toDate.getFullYear();
            const endMonth = fromDate.getMonth();
            const endYear = fromDate.getFullYear();

            const currentMonth = date.getMonth();
            const currentYear = date.getFullYear();

            // Check if the current month and year are within the allowed range
            return (
                (currentYear > startYear || (currentYear === startYear && currentMonth >= startMonth)) &&
                (currentYear < endYear || (currentYear === endYear && currentMonth <= endMonth))
            );
        }
        return true; // No range restrictions, so any month is valid
    };


    return (
        <div className="relative inline-block text-left w-full mt-1" ref={calendarRef}>
            <input
                type="text"
                className={` placeholder:text-gray-400 rounded-md px-3 bg-gray-900 focus:border-white py-2 w-full text-gray-300 focus:outline-none 900 border border-gray-500 shadow-sm sm:text-sm focus:border disabled:bg-gray-900/90 disabled:cursor-not-allowed ${className} `}
                placeholder={placeholder}
                value={selectedDate ? selectedDate.toLocaleDateString() : ''}
                readOnly
                onClick={() => setIsOpen(!isOpen)}
            />

            {isOpen && (
                <div className={`absolute z-10 mt-2 ${position} border border-${secondary}-300  rounded-md shadow-md bg-white`}>
                    <div className="flex items-center justify-between text-center border-b border-${secondary} p-1">
                        <button onClick={goToToday} className={`text-sm text-${primary}-500 hover:underline px-2`}>
                            Today
                        </button>
                        {renderHeader()}
                    </div>
                    <div className="grid grid-cols-7 gap-1 px-2 pt-2">
                        {daysInWeek.map((day) => (
                            <div key={day} className="text-center py-2 font-medium text-gray-700">
                                {day}
                            </div>
                        ))}
                    </div>
                    <div className="grid grid-cols-7 gap-1 px-2 pb-2">{renderDays()}</div>
                </div>
            )}
        </div>
    );
};

export default Calendar;