+ Selected Range: {startDate.toLocaleString()} to {endDate.toLocaleString()}
+
+
+ )}
+
+ );
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'An interactive example showing date range selection with two DatePickers.',
+ },
+ },
+ },
+};
\ No newline at end of file
diff --git a/src/components/ui/DatePicker.test.tsx b/src/components/ui/DatePicker.test.tsx
new file mode 100644
index 000000000..0f0cef24a
--- /dev/null
+++ b/src/components/ui/DatePicker.test.tsx
@@ -0,0 +1,138 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import DatePicker from './DatePicker';
+import { format } from 'date-fns';
+
+// Mock the date so tests are predictable
+const mockDate = new Date(2025, 3, 10); // April 10, 2025
+jest.useFakeTimers().setSystemTime(mockDate);
+
+describe('DatePicker', () => {
+ test('renders with default props', () => {
+ const handleChange = jest.fn();
+ const { container } = render();
+
+ // Check that the component renders
+ const datepickerElement = container.querySelector('.memori-datepicker');
+ expect(datepickerElement).not.toBeNull();
+
+ // Input should be rendered with the placeholder
+ const input = container.querySelector('input');
+ expect(input).not.toBeNull();
+ expect(input?.getAttribute('placeholder')).toBe('DD/MM/YYYY');
+ });
+
+ test('renders with selected date', () => {
+ const handleChange = jest.fn();
+ const selectedDate = new Date(2025, 3, 15); // April 15, 2025
+ const { container } = render();
+
+ // Input should have the date value
+ const input = container.querySelector('input');
+ expect(input).not.toBeNull();
+ expect(input?.value).toBe(format(selectedDate, 'PP'));
+ });
+
+ test('applies custom className', () => {
+ const handleChange = jest.fn();
+ const customClass = 'custom-datepicker';
+ const { container } = render();
+
+ // Component should have the custom class
+ const datepickerElement = container.querySelector('.memori-datepicker');
+ expect(datepickerElement).not.toBeNull();
+ expect(datepickerElement?.classList.contains(customClass)).toBe(true);
+ });
+
+ test('renders with icon', () => {
+ const handleChange = jest.fn();
+ const icon = icon;
+ const { container } = render();
+
+ // Icon should be rendered
+ const iconContainer = container.querySelector('.memori-datepicker--icon');
+ expect(iconContainer).not.toBeNull();
+ });
+
+ test('renders with label', () => {
+ const handleChange = jest.fn();
+ const label = 'Test Label';
+ const { container } = render();
+
+ // Label should be rendered
+ const labelElement = container.querySelector('.memori-datepicker--label');
+ expect(labelElement).not.toBeNull();
+ expect(labelElement?.textContent).toBe(label);
+ });
+
+ test('renders as disabled', () => {
+ const handleChange = jest.fn();
+ const { container } = render();
+
+ // Input should be disabled
+ const input = container.querySelector('input');
+ expect(input).not.toBeNull();
+ expect(input?.disabled).toBe(true);
+ });
+
+ test('renders with clearable option when a date is selected', () => {
+ const handleChange = jest.fn();
+ const { container } = render(
+
+ );
+
+ // Clear button should be present
+ const clearButton = container.querySelector('.memori-datepicker--clear-button');
+ expect(clearButton).not.toBeNull();
+ });
+
+ test('renders with time selection options when showTimeSelect is true', () => {
+ const handleChange = jest.fn();
+ const { container } = render(
+
+ );
+
+ // Just check that the component renders successfully
+ const datepickerElement = container.querySelector('.memori-datepicker');
+ expect(datepickerElement).not.toBeNull();
+ });
+
+ test('renders with custom timeCaption', () => {
+ const handleChange = jest.fn();
+ const customCaption = 'Custom Time';
+ const { container } = render(
+
+ );
+
+ // Just check that the component renders with the props
+ const datepickerElement = container.querySelector('.memori-datepicker');
+ expect(datepickerElement).not.toBeNull();
+ });
+
+ test('renders with custom popperPlacement', () => {
+ const handleChange = jest.fn();
+ const { container } = render(
+
+ );
+
+ // Component should render successfully
+ const datepickerElement = container.querySelector('.memori-datepicker');
+ expect(datepickerElement).not.toBeNull();
+ });
+});
\ No newline at end of file
diff --git a/src/components/ui/DatePicker.tsx b/src/components/ui/DatePicker.tsx
new file mode 100644
index 000000000..fe95b0154
--- /dev/null
+++ b/src/components/ui/DatePicker.tsx
@@ -0,0 +1,337 @@
+import React, { Fragment, useState, useEffect } from 'react';
+import cx from 'classnames';
+import { Popover, Transition } from '@headlessui/react';
+import { format, parse, isValid, addDays, addMonths, subMonths, setHours, setMinutes, startOfMonth, endOfMonth, startOfWeek, endOfWeek, isSameMonth, isSameDay, addWeeks, Day } from 'date-fns';
+import { enUS, it } from 'date-fns/locale';
+import ChevronLeft from '../icons/ChevronLeft';
+import ChevronRight from '../icons/ChevronRight';
+import Clear from '../icons/Clear';
+
+export interface Props {
+ name?: string;
+ className?: string;
+ placeholderText?: string;
+ selected?: Date | null;
+ onChange: (date: Date | null) => void;
+ locale?: 'en' | 'it';
+ showTimeSelect?: boolean;
+ calendarStartDay?: number;
+ timeIntervals?: number;
+ timeCaption?: string;
+ dateFormat?: string;
+ icon?: React.ReactNode;
+ popperPlacement?: 'bottom-start' | 'bottom-end';
+ isClearable?: boolean;
+ disabled?: boolean;
+ label?: string;
+}
+
+const DatePicker = ({
+ name,
+ className,
+ placeholderText = 'DD/MM/YYYY',
+ selected,
+ onChange,
+ locale = 'en',
+ showTimeSelect = false,
+ calendarStartDay = 0,
+ timeIntervals = 30,
+ timeCaption = 'Time',
+ dateFormat = 'PP',
+ icon,
+ popperPlacement = 'bottom-start',
+ isClearable = false,
+ disabled = false,
+ label,
+}: Props) => {
+ const [currentMonth, setCurrentMonth] = useState(selected || new Date());
+ const [selectedDate, setSelectedDate] = useState(selected || null);
+ const [selectedTime, setSelectedTime] = useState<{ hours: number; minutes: number } | null>(
+ selected ? { hours: selected.getHours(), minutes: selected.getMinutes() } : null
+ );
+ const [inputValue, setInputValue] = useState('');
+
+ const localeObj = locale === 'it' ? it : enUS;
+
+ useEffect(() => {
+ if (selected) {
+ setSelectedDate(selected);
+ setCurrentMonth(selected);
+ setSelectedTime(showTimeSelect ? { hours: selected.getHours(), minutes: selected.getMinutes() } : null);
+ setInputValue(formatDate(selected));
+ } else {
+ setInputValue('');
+ }
+ }, [selected, showTimeSelect]);
+
+ const formatDate = (date: Date): string => {
+ if (!isValid(date)) return '';
+
+ try {
+ return format(date, dateFormat, { locale: localeObj });
+ } catch (error) {
+ return format(date, 'PP', { locale: localeObj });
+ }
+ };
+
+ const handleDateChange = (date: Date) => {
+ let newDate = date;
+
+ if (selectedTime) {
+ newDate = setHours(newDate, selectedTime.hours);
+ newDate = setMinutes(newDate, selectedTime.minutes);
+ }
+
+ setSelectedDate(newDate);
+ setInputValue(formatDate(newDate));
+ onChange(newDate);
+ };
+
+ const handleTimeChange = (hours: number, minutes: number) => {
+ if (!selectedDate) return;
+
+ const newTime = { hours, minutes };
+ setSelectedTime(newTime);
+
+ const newDate = setMinutes(setHours(selectedDate, hours), minutes);
+ setInputValue(formatDate(newDate));
+ onChange(newDate);
+ };
+
+ const handleInputChange = (e: React.ChangeEvent) => {
+ const value = e.target.value;
+ setInputValue(value);
+
+ // Try to parse the input value
+ if (value) {
+ try {
+ const parsedDate = parse(value, dateFormat, new Date(), { locale: localeObj });
+ if (isValid(parsedDate)) {
+ setSelectedDate(parsedDate);
+ setCurrentMonth(parsedDate);
+ onChange(parsedDate);
+ return;
+ }
+ } catch (error) {
+ // Parsing failed, continue with setting just the input value
+ }
+ }
+
+ // If parsing failed or value is empty
+ if (!value && isClearable) {
+ setSelectedDate(null);
+ onChange(null);
+ }
+ };
+
+ const handleClear = () => {
+ setSelectedDate(null);
+ setInputValue('');
+ onChange(null);
+ };
+
+ const renderHeader = () => {
+ const monthName = format(currentMonth, 'MMMM yyyy', { locale: localeObj });
+
+ return (
+
+
+ {monthName}
+
+
+ );
+ };
+
+ const renderDays = () => {
+ const days = [];
+ let startDate = startOfWeek(startOfMonth(currentMonth), { weekStartsOn: calendarStartDay as Day });
+
+ // If we need to start from another day (e.g. Monday(1) instead of Sunday(0))
+ if (calendarStartDay !== 0) {
+ startDate = startOfWeek(startOfMonth(currentMonth), { weekStartsOn: calendarStartDay as Day });
+ }
+
+ // Create day name headers
+ const dayNames = [];
+ for (let i = 0; i < 7; i++) {
+ const dayOfWeek = addDays(startDate, i);
+ dayNames.push(
+