diff --git a/src/pages/CalendarPage/ui/CalendarPage.module.css b/src/pages/CalendarPage/ui/CalendarPage.module.css
index c6435d2..1fb3f26 100644
--- a/src/pages/CalendarPage/ui/CalendarPage.module.css
+++ b/src/pages/CalendarPage/ui/CalendarPage.module.css
@@ -2,4 +2,5 @@
display: flex;
justify-content: center;
align-items: center;
+ gap: 50px
}
diff --git a/src/pages/CalendarPage/ui/CalendarPage.ui.tsx b/src/pages/CalendarPage/ui/CalendarPage.ui.tsx
index efc1c36..0aaca35 100644
--- a/src/pages/CalendarPage/ui/CalendarPage.ui.tsx
+++ b/src/pages/CalendarPage/ui/CalendarPage.ui.tsx
@@ -1,4 +1,5 @@
import { CalendarWidget } from 'widgets/CalendarWidget';
+import { DatePicker } from 'widgets/DatePicker';
import classes from './CalendarPage.module.css';
@@ -10,6 +11,12 @@ const CalendarPage = () => (
+
+
);
diff --git a/src/widgets/DatePicker/index.ts b/src/widgets/DatePicker/index.ts
new file mode 100644
index 0000000..8d28001
--- /dev/null
+++ b/src/widgets/DatePicker/index.ts
@@ -0,0 +1 @@
+export { default as DatePicker } from './ui/DatePicker.ui';
diff --git a/src/widgets/DatePicker/ui/DatePicker.spec.tsx b/src/widgets/DatePicker/ui/DatePicker.spec.tsx
new file mode 100644
index 0000000..56c189f
--- /dev/null
+++ b/src/widgets/DatePicker/ui/DatePicker.spec.tsx
@@ -0,0 +1,360 @@
+import { render, screen } from '@testing-library/react';
+
+import { getDateCellByDataDate, pickCellByDataDate } from 'shared/utils/spec-helpers/date';
+
+import DatePicker from './DatePicker.ui';
+
+import type { DatePickerProps } from './DatePicker.ui';
+
+describe('Test DatePicker', () => {
+ const setup = (props?: DatePickerProps) => render(
+ ,
+ );
+
+ test('Basic render DatePicker', () => {
+ setup();
+
+ const datePickerElem = screen.queryByTestId('calendar-widget');
+ expect(datePickerElem).toBeVisible();
+ });
+
+ describe('Test base props', () => {
+ test('Default picked first date', () => {
+ setup({
+ firstDate: new Date(2024, 9, 11),
+ value: new Date(2024, 9, 15),
+ });
+
+ const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
+ expect(firstPickedCellElem).toBeVisible();
+ expect(firstPickedCellElem).toHaveAttribute('data-date', '10/11/2024');
+
+ const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
+ expect(lastPickedCellElem).not.toBeInTheDocument();
+ });
+
+ test('Default picked last date', () => {
+ setup({
+ lastDate: new Date(2024, 9, 18),
+ value: new Date(2024, 9, 15),
+ });
+
+ const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
+ expect(lastPickedCellElem).toBeVisible();
+ expect(lastPickedCellElem).toHaveAttribute('data-date', '10/18/2024');
+
+ const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
+ expect(firstPickedCellElem).not.toBeInTheDocument();
+ });
+
+ describe('onSelect handler', () => {
+ test('No default picked', () => {
+ const onSelect = jest.fn();
+
+ const { container } = setup({
+ onSelect,
+ value: new Date(2024, 9, 15),
+ });
+
+ pickCellByDataDate(container, '10/18/2024');
+
+ expect(onSelect).toHaveBeenCalled();
+ expect(onSelect).toHaveBeenCalledWith({
+ dataDate: '10/18/2024',
+ date: new Date('10/18/2024'),
+ type: 'firstDate',
+ });
+ });
+
+ test('Picked first date', () => {
+ const onSelect = jest.fn();
+
+ const { container } = setup({
+ onSelect,
+ value: new Date(2024, 9, 15),
+ firstDate: new Date(2024, 9, 15),
+ });
+
+ pickCellByDataDate(container, '10/18/2024');
+
+ expect(onSelect).toHaveBeenCalled();
+ expect(onSelect).toHaveBeenCalledWith({
+ dataDate: '10/18/2024',
+ date: new Date('10/18/2024'),
+ type: 'lastDate',
+ });
+ });
+
+ test('Picked last date', () => {
+ const onSelect = jest.fn();
+
+ const { container } = setup({
+ onSelect,
+ value: new Date(2024, 9, 15),
+ lastDate: new Date(2024, 9, 15),
+ });
+
+ pickCellByDataDate(container, '10/11/2024');
+
+ expect(onSelect).toHaveBeenCalled();
+ expect(onSelect).toHaveBeenCalledWith({
+ dataDate: '10/11/2024',
+ date: new Date('10/11/2024'),
+ type: 'firstDate',
+ });
+ });
+ });
+
+ describe('onUnselect handler', () => {
+ test('Unpick first date', () => {
+ const onUnselect = jest.fn();
+
+ const { container } = setup({
+ onUnselect,
+ value: new Date(2024, 9, 15),
+ firstDate: new Date(2024, 9, 15),
+ lastDate: new Date(2024, 9, 18),
+ });
+
+ pickCellByDataDate(container, '10/15/2024');
+
+ expect(onUnselect).toHaveBeenCalled();
+ expect(onUnselect).toHaveBeenCalledWith({
+ dataDate: '10/15/2024',
+ date: new Date('10/15/2024'),
+ type: 'firstDate',
+ });
+ });
+
+ test('Unpick last date', () => {
+ const onUnselect = jest.fn();
+
+ const { container } = setup({
+ onUnselect,
+ value: new Date(2024, 9, 15),
+ firstDate: new Date(2024, 9, 15),
+ lastDate: new Date(2024, 9, 18),
+ });
+
+ pickCellByDataDate(container, '10/18/2024');
+
+ expect(onUnselect).toHaveBeenCalled();
+ expect(onUnselect).toHaveBeenCalledWith({
+ dataDate: '10/18/2024',
+ date: new Date('10/18/2024'),
+ type: 'lastDate',
+ });
+ });
+ });
+ });
+
+ describe('Test pick date', () => {
+ test('No picked date, select first', () => {
+ const { container } = setup({
+ value: new Date(2024, 9, 15),
+ });
+
+ const cellElem = getDateCellByDataDate(container, '10/11/2024');
+ pickCellByDataDate(container, '10/11/2024');
+
+ const firstSelectedCellElem = screen.queryByTestId('first-selected-cell');
+ expect(firstSelectedCellElem).toBe(cellElem);
+ });
+
+ test('Picked first date, pick before first date', () => {
+ const { container } = setup({
+ value: new Date(2024, 9, 15),
+ firstDate: new Date(2024, 9, 15),
+ });
+
+ pickCellByDataDate(container, '10/11/2024');
+
+ const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
+ expect(firstPickedCellElem).toHaveAttribute('data-date', '10/11/2024');
+
+ const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
+ expect(lastPickedCellElem).toHaveAttribute('data-date', '10/15/2024');
+ });
+
+ test('Picked first date, pick after first date', () => {
+ const { container } = setup({
+ value: new Date(2024, 9, 15),
+ firstDate: new Date(2024, 9, 15),
+ });
+
+ pickCellByDataDate(container, '10/18/2024');
+
+ const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
+ expect(firstPickedCellElem).toHaveAttribute('data-date', '10/15/2024');
+
+ const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
+ expect(lastPickedCellElem).toHaveAttribute('data-date', '10/18/2024');
+ });
+
+ test('Picked first date, pick this date', () => {
+ const { container } = setup({
+ value: new Date(2024, 9, 15),
+ firstDate: new Date(2024, 9, 15),
+ });
+
+ pickCellByDataDate(container, '10/15/2024');
+
+ const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
+ expect(firstPickedCellElem).not.toBeInTheDocument();
+ });
+
+ test('Picked last date, pick before last date', () => {
+ const { container } = setup({
+ value: new Date(2024, 9, 15),
+ lastDate: new Date(2024, 9, 15),
+ });
+
+ pickCellByDataDate(container, '10/11/2024');
+
+ const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
+ expect(firstPickedCellElem).toHaveAttribute('data-date', '10/11/2024');
+
+ const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
+ expect(lastPickedCellElem).toHaveAttribute('data-date', '10/15/2024');
+ });
+
+ test('Picked last date, pick after last date', () => {
+ const { container } = setup({
+ value: new Date(2024, 9, 15),
+ lastDate: new Date(2024, 9, 15),
+ });
+
+ pickCellByDataDate(container, '10/18/2024');
+
+ const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
+ expect(firstPickedCellElem).toHaveAttribute('data-date', '10/15/2024');
+
+ const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
+ expect(lastPickedCellElem).toHaveAttribute('data-date', '10/18/2024');
+ });
+
+ test('Picked last date, pick this date', () => {
+ const { container } = setup({
+ value: new Date(2024, 9, 15),
+ lastDate: new Date(2024, 9, 15),
+ });
+
+ pickCellByDataDate(container, '10/15/2024');
+
+ const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
+ expect(lastPickedCellElem).not.toBeInTheDocument();
+ });
+
+ test('Picked first & last date, pick date before first', () => {
+ const { container } = setup({
+ value: new Date(2024, 9, 15),
+ firstDate: new Date(2024, 9, 15),
+ lastDate: new Date(2024, 9, 18),
+ });
+
+ pickCellByDataDate(container, '10/11/2024');
+
+ const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
+ expect(firstPickedCellElem).toHaveAttribute('data-date', '10/11/2024');
+
+ const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
+ expect(lastPickedCellElem).toHaveAttribute('data-date', '10/18/2024');
+ });
+
+ test('Picked first & last date, pick date after last', () => {
+ const { container } = setup({
+ value: new Date(2024, 9, 15),
+ firstDate: new Date(2024, 9, 15),
+ lastDate: new Date(2024, 9, 18),
+ });
+
+ pickCellByDataDate(container, '10/21/2024');
+
+ const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
+ expect(firstPickedCellElem).toHaveAttribute('data-date', '10/15/2024');
+
+ const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
+ expect(lastPickedCellElem).toHaveAttribute('data-date', '10/21/2024');
+ });
+
+ test('Picked first & last date, pick fist date', () => {
+ const { container } = setup({
+ value: new Date(2024, 9, 15),
+ firstDate: new Date(2024, 9, 15),
+ lastDate: new Date(2024, 9, 18),
+ });
+
+ pickCellByDataDate(container, '10/15/2024');
+
+ const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
+ expect(firstPickedCellElem).not.toBeInTheDocument();
+
+ const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
+ expect(lastPickedCellElem).toHaveAttribute('data-date', '10/18/2024');
+ });
+
+ test('Picked first & last date, pick last date', () => {
+ const { container } = setup({
+ value: new Date(2024, 9, 15),
+ firstDate: new Date(2024, 9, 15),
+ lastDate: new Date(2024, 9, 18),
+ });
+
+ pickCellByDataDate(container, '10/18/2024');
+
+ const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
+ expect(firstPickedCellElem).toHaveAttribute('data-date', '10/15/2024');
+
+ const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
+ expect(lastPickedCellElem).not.toBeInTheDocument();
+ });
+
+ test('Picked first & last date, pick the date closer to first date', () => {
+ const { container } = setup({
+ value: new Date(2024, 9, 15),
+ firstDate: new Date(2024, 9, 15),
+ lastDate: new Date(2024, 9, 18),
+ });
+
+ pickCellByDataDate(container, '10/16/2024');
+
+ const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
+ expect(firstPickedCellElem).toHaveAttribute('data-date', '10/16/2024');
+
+ const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
+ expect(lastPickedCellElem).toHaveAttribute('data-date', '10/18/2024');
+ });
+
+ test('Picked first & last date, pick the date closer to last date', () => {
+ const { container } = setup({
+ value: new Date(2024, 9, 15),
+ firstDate: new Date(2024, 9, 15),
+ lastDate: new Date(2024, 9, 18),
+ });
+
+ pickCellByDataDate(container, '10/17/2024');
+
+ const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
+ expect(firstPickedCellElem).toHaveAttribute('data-date', '10/15/2024');
+
+ const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
+ expect(lastPickedCellElem).toHaveAttribute('data-date', '10/17/2024');
+ });
+
+ test('Picked first & last date, pick the equidistant date', () => {
+ const { container } = setup({
+ value: new Date(2024, 9, 15),
+ firstDate: new Date(2024, 9, 15),
+ lastDate: new Date(2024, 9, 19),
+ });
+
+ pickCellByDataDate(container, '10/17/2024');
+
+ const firstPickedCellElem = screen.queryByTestId('first-selected-cell');
+ expect(firstPickedCellElem).toHaveAttribute('data-date', '10/17/2024');
+
+ const lastPickedCellElem = screen.queryByTestId('last-selected-cell');
+ expect(lastPickedCellElem).toHaveAttribute('data-date', '10/19/2024');
+ });
+ });
+});
diff --git a/src/widgets/DatePicker/ui/DatePicker.style.module.css b/src/widgets/DatePicker/ui/DatePicker.style.module.css
new file mode 100644
index 0000000..03fcd49
--- /dev/null
+++ b/src/widgets/DatePicker/ui/DatePicker.style.module.css
@@ -0,0 +1,16 @@
+.firstDate {
+ background-color: blue;
+ color: white;
+ border-top-left-radius: 10px
+}
+
+.inSelection {
+ background: gold;
+ color: darkgrey;
+}
+
+.lastDate {
+ background-color: blue;
+ color: white;
+ border-bottom-right-radius: 10px
+}
diff --git a/src/widgets/DatePicker/ui/DatePicker.ui.tsx b/src/widgets/DatePicker/ui/DatePicker.ui.tsx
new file mode 100644
index 0000000..2436918
--- /dev/null
+++ b/src/widgets/DatePicker/ui/DatePicker.ui.tsx
@@ -0,0 +1,262 @@
+import classnames from 'classnames';
+import { useState } from 'react';
+
+import type { DataDateString } from 'shared/types/date';
+import { daysCountBetween, getDataDateString } from 'shared/utils/date';
+import { exhaustiveCheck } from 'shared/utils/type';
+
+import { CalendarWidget } from '../../CalendarWidget';
+
+import classes from './DatePicker.style.module.css';
+
+import type { CalendarWidgetProps } from '../../CalendarWidget';
+import type { FC } from 'react';
+
+export type DatePickerProps = CalendarWidgetProps & {
+ firstDate?: Date;
+ lastDate?: Date;
+ onSelect?: (params: { dataDate: DataDateString; date: Date; type: 'firstDate' | 'lastDate' }) => void;
+ onUnselect?: (params: { dataDate: DataDateString; date: Date; type: 'firstDate' | 'lastDate' }) => void;
+};
+
+const DatePicker: FC = (props) => {
+ const {
+ firstDate,
+ lastDate,
+ onSelect,
+ onUnselect,
+ ...restWidgetProps
+ } = props;
+
+ const [firstSelectedDate, setFirstSelectedDate] = useState(
+ firstDate
+ ? getDataDateString(firstDate)
+ : null,
+ );
+ const [lastSelectedDate, setLastSelectedDate] = useState(
+ lastDate
+ ? getDataDateString(lastDate)
+ : null,
+ );
+
+ const isSelectedFirstDate = firstSelectedDate !== null;
+ const isSelectedLastDate = lastSelectedDate !== null;
+ const isSelectedBothDate = isSelectedFirstDate && isSelectedLastDate;
+
+ const isCellInSelection = (dataDate: DataDateString) => {
+ if (!isSelectedFirstDate || !isSelectedLastDate) {
+ return false;
+ }
+
+ const dateTime = new Date(dataDate).getTime();
+ const firstDateTime = new Date(firstSelectedDate).getTime();
+ const lastDateTime = new Date(lastSelectedDate).getTime();
+
+ return dateTime >= firstDateTime && dateTime <= lastDateTime;
+ };
+
+ const toggleCellDate = (params: { dataDate: DataDateString | null; type: 'firstDate' | 'lastDate' }) => {
+ const { dataDate, type } = params;
+
+ if (dataDate === null) {
+ let typedDataDate: DataDateString;
+
+ switch (type) {
+ case 'firstDate':
+ if (!isSelectedFirstDate) {
+ return;
+ }
+ typedDataDate = firstSelectedDate;
+ setFirstSelectedDate(null);
+ break;
+
+ case 'lastDate':
+ if (!isSelectedLastDate) {
+ return;
+ }
+ typedDataDate = lastSelectedDate;
+ setLastSelectedDate(null);
+
+ break;
+ default:
+ exhaustiveCheck(type);
+ break;
+ }
+
+ onUnselect?.({
+ type,
+ // @ts-ignore
+ dataDate: typedDataDate,
+ // @ts-ignore
+ date: new Date(typedDataDate),
+ });
+
+ return;
+ }
+
+ switch (type) {
+ case 'firstDate':
+ setFirstSelectedDate(dataDate);
+ break;
+
+ case 'lastDate':
+ setLastSelectedDate(dataDate);
+ break;
+
+ default:
+ exhaustiveCheck(type);
+ break;
+ }
+ onSelect?.({
+ type,
+ dataDate,
+ date: new Date(dataDate),
+ });
+ };
+
+ const toggleCellSelection = (dataDate: DataDateString) => {
+ if (!isSelectedFirstDate && !isSelectedLastDate) {
+ toggleCellDate({ dataDate, type: 'firstDate' });
+
+ return;
+ }
+
+ if (isSelectedFirstDate && !isSelectedLastDate) {
+ if (dataDate < firstSelectedDate) {
+ toggleCellDate({ dataDate: firstSelectedDate, type: 'lastDate' });
+ toggleCellDate({ dataDate, type: 'firstDate' });
+
+ return;
+ }
+
+ if (dataDate > firstSelectedDate) {
+ toggleCellDate({ dataDate, type: 'lastDate' });
+
+ return;
+ }
+
+ toggleCellDate({ dataDate: null, type: 'firstDate' });
+ }
+
+ if (!isSelectedFirstDate && isSelectedLastDate) {
+ if (dataDate < lastSelectedDate) {
+ toggleCellDate({ dataDate, type: 'firstDate' });
+
+ return;
+ }
+
+ if (dataDate > lastSelectedDate) {
+ toggleCellDate({ dataDate: lastSelectedDate, type: 'firstDate' });
+ toggleCellDate({ dataDate, type: 'lastDate' });
+
+ return;
+ }
+
+ toggleCellDate({ dataDate: null, type: 'lastDate' });
+ }
+
+ if (isSelectedBothDate) {
+ if (dataDate < firstSelectedDate) {
+ toggleCellDate({ dataDate, type: 'firstDate' });
+
+ return;
+ }
+
+ if (dataDate > lastSelectedDate) {
+ toggleCellDate({ dataDate, type: 'lastDate' });
+
+ return;
+ }
+
+ if (dataDate === firstSelectedDate) {
+ toggleCellDate({ dataDate: null, type: 'firstDate' });
+
+ return;
+ }
+
+ if (dataDate === lastSelectedDate) {
+ toggleCellDate({ dataDate: null, type: 'lastDate' });
+
+ return;
+ }
+
+ const daysToFirstDate = daysCountBetween(new Date(firstSelectedDate), new Date(dataDate));
+ const daysToLastDate = daysCountBetween(new Date(lastSelectedDate), new Date(dataDate));
+
+ if (daysToFirstDate < daysToLastDate) {
+ toggleCellDate({ dataDate, type: 'firstDate' });
+
+ return;
+ }
+
+ if (daysToFirstDate > daysToLastDate) {
+ toggleCellDate({ dataDate, type: 'lastDate' });
+
+ return;
+ }
+
+ toggleCellDate({ dataDate, type: 'firstDate' });
+ }
+ };
+
+ const isFirstDate = (dataDate: DataDateString) => dataDate === firstSelectedDate;
+
+ const isLastDate = (dataDate: DataDateString) => dataDate === lastSelectedDate;
+
+ const getDataTestId = (dataDate: DataDateString) => {
+ if (isFirstDate(dataDate)) {
+ return {
+ 'data-testid': 'first-selected-cell',
+ };
+ }
+
+ if (isLastDate(dataDate)) {
+ return {
+ 'data-testid': 'last-selected-cell',
+ };
+ }
+
+ if (isCellInSelection(dataDate)) {
+ return {
+ 'data-testid': 'cell-in-range',
+ };
+ }
+
+ return {};
+ };
+
+ return (
+
+
+
+ ({
+ onClick: () => {
+ toggleCellSelection(dataDate);
+ },
+ className: classnames({
+ [classes.firstDate as string]: isFirstDate(dataDate),
+ [classes.inSelection as string]: isCellInSelection(dataDate)
+ && !isFirstDate(dataDate)
+ && !isLastDate(dataDate),
+ [classes.lastDate as string]: isLastDate(dataDate),
+ }),
+ ...getDataTestId(dataDate),
+ })}
+ {...restWidgetProps}
+ />
+
+
+ );
+};
+
+export default DatePicker;