@daryasidorovich/modsen-datepicker 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.babelrc.json +7 -0
- package/.github/workflows/chromatic.yml +25 -0
- package/.github/workflows/ci.yml +56 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +1 -0
- package/.prettierignore +16 -0
- package/.prettierrc +6 -0
- package/.storybook/main.js +93 -0
- package/.storybook/preview-head.html +6 -0
- package/.storybook/preview.js +26 -0
- package/.storybook/preview.tsx +18 -0
- package/README.md +302 -0
- package/commitlint.config.js +23 -0
- package/dist/components/BaseCalendar/index.d.ts +2 -0
- package/dist/components/CalendarGrid/CalendarGridWithTasks/Tooltip/index.d.ts +8 -0
- package/dist/components/CalendarGrid/CalendarGridWithTasks/index.d.ts +2 -0
- package/dist/components/CalendarGrid/CalendarGridWithoutTasks/CalendarGridWithoutTasks.test.d.ts +1 -0
- package/dist/components/CalendarGrid/CalendarGridWithoutTasks/index.d.ts +4 -0
- package/dist/components/CalendarGrid/index.d.ts +2 -0
- package/dist/components/CalendarGrid/types.d.ts +21 -0
- package/dist/components/Clear/index.d.ts +5 -0
- package/dist/components/DatePicker/index.d.ts +5 -0
- package/dist/components/Day/index.d.ts +16 -0
- package/dist/components/Input/index.d.ts +10 -0
- package/dist/components/Modal/CrudModal/index.d.ts +2 -0
- package/dist/components/Modal/index.d.ts +12 -0
- package/dist/components/Modal/types.d.ts +12 -0
- package/dist/components/Month/index.d.ts +7 -0
- package/dist/components/RangePicker/index.d.ts +6 -0
- package/dist/components/TasksManager/AddModal/index.d.ts +6 -0
- package/dist/components/TasksManager/DeleteModal/index.d.ts +6 -0
- package/dist/components/TasksManager/EditModal/index.d.ts +7 -0
- package/dist/components/TasksManager/index.d.ts +9 -0
- package/dist/components/Week/index.d.ts +17 -0
- package/dist/components/WeekDay/index.d.ts +7 -0
- package/dist/constants/dateMask.d.ts +21 -0
- package/dist/constants/firstDayOfWeek.d.ts +5 -0
- package/dist/constants/holidays.d.ts +1 -0
- package/dist/constants/modes.d.ts +5 -0
- package/dist/constants/monthsNames.d.ts +1 -0
- package/dist/constants/weekdaysNames.d.ts +1 -0
- package/dist/hooks/useCalendar.d.ts +7 -0
- package/dist/hooks/useLockBodyScroll.d.ts +2 -0
- package/dist/hooks/useModal.d.ts +13 -0
- package/dist/hooks/useTooltip.d.ts +9 -0
- package/dist/hooks/useValidate.d.ts +11 -0
- package/dist/index.cjs +17250 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +17233 -0
- package/dist/index.js.map +1 -0
- package/dist/services/calendarService.d.ts +19 -0
- package/dist/services/decorators/withHolidays.d.ts +2 -0
- package/dist/services/decorators/withHolidays.test.d.ts +1 -0
- package/dist/services/decorators/withMinMax.d.ts +2 -0
- package/dist/services/decorators/withMinMax.test.d.ts +1 -0
- package/dist/services/holidayService.d.ts +5 -0
- package/dist/setupTests.d.ts +1 -0
- package/dist/store/localStorage.d.ts +3 -0
- package/dist/store/tasksStore.d.ts +8 -0
- package/dist/store/tasksStore.test.d.ts +1 -0
- package/dist/store/types.d.ts +18 -0
- package/dist/utils/calendar.d.ts +7 -0
- package/dist/utils/calendar.test.d.ts +1 -0
- package/dist/utils/controlDate.d.ts +1 -0
- package/dist/utils/dateChecks.d.ts +10 -0
- package/dist/utils/dateChecks.test.d.ts +1 -0
- package/dist/utils/formatDate.d.ts +3 -0
- package/dist/utils/formatDate.test.d.ts +1 -0
- package/dist/utils/schema.d.ts +6 -0
- package/dist/utils/schema.test.d.ts +1 -0
- package/dist/utils/weekdayOrder.d.ts +1 -0
- package/dist/utils/weekdayOrder.test.d.ts +1 -0
- package/docs/base-calendar.png +0 -0
- package/docs/dark-theme.png +0 -0
- package/docs/datepicker.png +0 -0
- package/docs/rangepicker-with-error.png +0 -0
- package/docs/rangepicker.png +0 -0
- package/eslint.config.ts +80 -0
- package/jest.config.json +21 -0
- package/package.json +106 -0
- package/rollup.config.js +65 -0
- package/src/assets/actions/Delete.svg +4 -0
- package/src/assets/actions/Edit.svg +3 -0
- package/src/assets/arrows/Next.svg +4 -0
- package/src/assets/arrows/Prev.svg +4 -0
- package/src/assets/input/Calendar.svg +3 -0
- package/src/assets/input/Clear.svg +3 -0
- package/src/components/BaseCalendar/BaseCalendar.stories.tsx +77 -0
- package/src/components/BaseCalendar/index.tsx +63 -0
- package/src/components/BaseCalendar/styles.module.scss +15 -0
- package/src/components/CalendarGrid/CalendarGrid.stories.tsx +136 -0
- package/src/components/CalendarGrid/CalendarGridWithTasks/Tooltip/index.tsx +27 -0
- package/src/components/CalendarGrid/CalendarGridWithTasks/Tooltip/styles.module.scss +9 -0
- package/src/components/CalendarGrid/CalendarGridWithTasks/index.tsx +51 -0
- package/src/components/CalendarGrid/CalendarGridWithoutTasks/CalendarGridWithoutTasks.test.tsx +223 -0
- package/src/components/CalendarGrid/CalendarGridWithoutTasks/index.tsx +36 -0
- package/src/components/CalendarGrid/CalendarGridWithoutTasks/styles.module.scss +11 -0
- package/src/components/CalendarGrid/index.tsx +19 -0
- package/src/components/CalendarGrid/types.ts +26 -0
- package/src/components/Clear/Clear.stories.tsx +16 -0
- package/src/components/Clear/index.tsx +12 -0
- package/src/components/Clear/styles.module.scss +34 -0
- package/src/components/DatePicker/DatePicker.stories.tsx +127 -0
- package/src/components/DatePicker/index.tsx +72 -0
- package/src/components/DatePicker/styles.module.scss +10 -0
- package/src/components/Day/Day.stories.tsx +75 -0
- package/src/components/Day/index.tsx +95 -0
- package/src/components/Day/styles.module.scss +97 -0
- package/src/components/Input/Input.stories.tsx +37 -0
- package/src/components/Input/index.tsx +64 -0
- package/src/components/Input/styles.module.scss +79 -0
- package/src/components/Modal/CrudModal/index.tsx +43 -0
- package/src/components/Modal/CrudModal/styles.module.scss +44 -0
- package/src/components/Modal/index.tsx +66 -0
- package/src/components/Modal/styles.module.scss +40 -0
- package/src/components/Modal/types.ts +13 -0
- package/src/components/Month/Month.stories.tsx +19 -0
- package/src/components/Month/index.tsx +31 -0
- package/src/components/Month/styles.module.scss +30 -0
- package/src/components/RangePicker/RangePicker.stories.tsx +134 -0
- package/src/components/RangePicker/index.tsx +144 -0
- package/src/components/RangePicker/styles.module.scss +19 -0
- package/src/components/TasksManager/AddModal/index.tsx +62 -0
- package/src/components/TasksManager/DeleteModal/index.tsx +22 -0
- package/src/components/TasksManager/EditModal/index.tsx +96 -0
- package/src/components/TasksManager/index.tsx +112 -0
- package/src/components/TasksManager/styles.module.scss +62 -0
- package/src/components/Week/Week.stories.tsx +49 -0
- package/src/components/Week/index.tsx +68 -0
- package/src/components/Week/styles.module.scss +7 -0
- package/src/components/WeekDay/WeekDay.stories.tsx +38 -0
- package/src/components/WeekDay/index.tsx +35 -0
- package/src/components/WeekDay/styles.module.scss +19 -0
- package/src/constants/dateMask.ts +23 -0
- package/src/constants/firstDayOfWeek.ts +7 -0
- package/src/constants/holidays.ts +32 -0
- package/src/constants/modes.ts +6 -0
- package/src/constants/monthsNames.ts +14 -0
- package/src/constants/weekdaysNames.ts +1 -0
- package/src/hooks/useCalendar.ts +62 -0
- package/src/hooks/useLockBodyScroll.ts +19 -0
- package/src/hooks/useModal.ts +45 -0
- package/src/hooks/useTooltip.ts +41 -0
- package/src/hooks/useValidate.ts +80 -0
- package/src/index.ts +15 -0
- package/src/services/calendarService.ts +62 -0
- package/src/services/decorators/withHolidays.test.ts +64 -0
- package/src/services/decorators/withHolidays.ts +12 -0
- package/src/services/decorators/withMinMax.test.ts +77 -0
- package/src/services/decorators/withMinMax.ts +19 -0
- package/src/services/holidayService.ts +56 -0
- package/src/setupTests.ts +1 -0
- package/src/store/localStorage.ts +33 -0
- package/src/store/tasksStore.test.tsx +208 -0
- package/src/store/tasksStore.tsx +146 -0
- package/src/store/types.ts +22 -0
- package/src/styles/_border.scss +9 -0
- package/src/styles/_colors.scss +53 -0
- package/src/styles/_gap.scss +4 -0
- package/src/styles/_globals.scss +61 -0
- package/src/styles/_grid.scss +9 -0
- package/src/styles/_mixins.scss +6 -0
- package/src/styles/_reset.scss +98 -0
- package/src/styles/_theme-mixins.scss +51 -0
- package/src/styles/_typography.scss +10 -0
- package/src/styles/_variables.scss +8 -0
- package/src/styles/main.scss +3 -0
- package/src/types/types.d.ts +15 -0
- package/src/utils/calendar.test.ts +154 -0
- package/src/utils/calendar.ts +82 -0
- package/src/utils/controlDate.ts +3 -0
- package/src/utils/dateChecks.test.ts +216 -0
- package/src/utils/dateChecks.ts +75 -0
- package/src/utils/formatDate.test.ts +57 -0
- package/src/utils/formatDate.ts +27 -0
- package/src/utils/schema.test.ts +40 -0
- package/src/utils/schema.ts +8 -0
- package/src/utils/weekdayOrder.test.ts +11 -0
- package/src/utils/weekdayOrder.ts +7 -0
- package/tsconfig.json +31 -0
- package/vitest.config.ts +38 -0
- package/vitest.shims.d.ts +1 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
interface UseModalOptions {
|
|
4
|
+
initial?: boolean;
|
|
5
|
+
onOpen?: () => void;
|
|
6
|
+
onClose?: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const useModal = (options: UseModalOptions = {}) => {
|
|
10
|
+
const { initial = false, onOpen, onClose } = options;
|
|
11
|
+
const [isOpen, setIsOpen] = useState(initial);
|
|
12
|
+
|
|
13
|
+
const open = () => {
|
|
14
|
+
onOpen?.();
|
|
15
|
+
setIsOpen(true);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const close = () => {
|
|
19
|
+
onClose?.();
|
|
20
|
+
setIsOpen(false);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const toggle = () => {
|
|
24
|
+
setIsOpen((prev) => {
|
|
25
|
+
const newState = !prev;
|
|
26
|
+
if (newState) {
|
|
27
|
+
onOpen?.();
|
|
28
|
+
} else {
|
|
29
|
+
onClose?.();
|
|
30
|
+
}
|
|
31
|
+
return newState;
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const setOpen = (value: boolean) => {
|
|
36
|
+
if (value) {
|
|
37
|
+
onOpen?.();
|
|
38
|
+
} else {
|
|
39
|
+
onClose?.();
|
|
40
|
+
}
|
|
41
|
+
setIsOpen(value);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return { isOpen, open, close, toggle, setOpen };
|
|
45
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export const useTooltip = () => {
|
|
4
|
+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
5
|
+
|
|
6
|
+
const [tooltip, setTooltip] = useState({
|
|
7
|
+
isVisible: false,
|
|
8
|
+
x: 0,
|
|
9
|
+
y: 0,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const handleDayMouseEnter = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
13
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
14
|
+
const coords = {
|
|
15
|
+
x: rect.right - 5,
|
|
16
|
+
y: rect.top - 10,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
if (timeoutRef.current) {
|
|
20
|
+
clearTimeout(timeoutRef.current);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
timeoutRef.current = setTimeout(() => {
|
|
24
|
+
setTooltip({
|
|
25
|
+
isVisible: true,
|
|
26
|
+
x: coords.x,
|
|
27
|
+
y: coords.y,
|
|
28
|
+
});
|
|
29
|
+
}, 500);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const handleDayMouseLeave = () => {
|
|
33
|
+
if (timeoutRef.current) {
|
|
34
|
+
clearTimeout(timeoutRef.current);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setTooltip((prev) => ({ ...prev, isVisible: false }));
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return { tooltip, handleDayMouseEnter, handleDayMouseLeave };
|
|
41
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useIMask } from 'react-imask';
|
|
3
|
+
|
|
4
|
+
import { DATE_MASK } from '@/constants/dateMask';
|
|
5
|
+
import { getRangeError } from '@/utils/dateChecks';
|
|
6
|
+
import { formatDateForInput, parseDate } from '@/utils/formatDate';
|
|
7
|
+
|
|
8
|
+
export function useValidate(selectedDate: Date | undefined) {
|
|
9
|
+
const [currentDate, setCurrentDate] = useState(selectedDate);
|
|
10
|
+
const [error, setError] = useState('');
|
|
11
|
+
const { ref, value, setValue } = useIMask(DATE_MASK);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
setCurrentDate(selectedDate);
|
|
15
|
+
if (selectedDate) {
|
|
16
|
+
setValue(formatDateForInput(selectedDate));
|
|
17
|
+
} else {
|
|
18
|
+
setValue('');
|
|
19
|
+
}
|
|
20
|
+
}, [selectedDate, setValue]);
|
|
21
|
+
|
|
22
|
+
const handleDateValidation = (
|
|
23
|
+
value: string,
|
|
24
|
+
startDate?: Date
|
|
25
|
+
): Date | undefined => {
|
|
26
|
+
if (!value) {
|
|
27
|
+
setError('');
|
|
28
|
+
setCurrentDate(undefined);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const parsed = parseDate(value);
|
|
32
|
+
|
|
33
|
+
if (!parsed) {
|
|
34
|
+
setError('Invalid date');
|
|
35
|
+
setCurrentDate(undefined);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (startDate) {
|
|
40
|
+
const rangeError = getRangeError(parsed, startDate);
|
|
41
|
+
if (rangeError) {
|
|
42
|
+
setError(rangeError);
|
|
43
|
+
setCurrentDate(undefined);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
setError('');
|
|
49
|
+
setCurrentDate(parsed);
|
|
50
|
+
return parsed;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleClearInput = () => {
|
|
54
|
+
setCurrentDate(undefined);
|
|
55
|
+
setValue('');
|
|
56
|
+
setError('');
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const handleDateSelect = (date?: Date) => {
|
|
60
|
+
setCurrentDate(date);
|
|
61
|
+
if (!date) {
|
|
62
|
+
setValue('');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
setValue(formatDateForInput(date));
|
|
66
|
+
setError('');
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
currentDate,
|
|
71
|
+
setCurrentDate,
|
|
72
|
+
handleDateValidation,
|
|
73
|
+
handleClearInput,
|
|
74
|
+
handleDateSelect,
|
|
75
|
+
ref,
|
|
76
|
+
value,
|
|
77
|
+
setValue,
|
|
78
|
+
error,
|
|
79
|
+
};
|
|
80
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { FIRST_DAY_OF_WEEK } from './constants/firstDayOfWeek';
|
|
2
|
+
import { MODES } from './constants/modes';
|
|
3
|
+
|
|
4
|
+
export { BaseCalendar } from './components/BaseCalendar';
|
|
5
|
+
export type {
|
|
6
|
+
BaseCalendarProps,
|
|
7
|
+
CalendarProps,
|
|
8
|
+
} from './components/Calendar/types';
|
|
9
|
+
export type { DatePickerProps } from './components/DatePicker';
|
|
10
|
+
export { DatePicker } from './components/DatePicker';
|
|
11
|
+
export type { RangePickerProps } from './components/RangePicker';
|
|
12
|
+
export { RangePicker } from './components/RangePicker';
|
|
13
|
+
export type { FIRST_DAY_OF_WEEK_TYPES } from './constants/firstDayOfWeek';
|
|
14
|
+
export type { MODES_TYPES } from './constants/modes';
|
|
15
|
+
export { FIRST_DAY_OF_WEEK, MODES };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { FIRST_DAY_OF_WEEK_TYPES } from '@/constants/firstDayOfWeek';
|
|
2
|
+
import { MODES, MODES_TYPES } from '@/constants/modes';
|
|
3
|
+
import { getMonthDays, getWeekDays, getWeeksFromDays } from '@/utils/calendar';
|
|
4
|
+
import { isCurrentMonth, isWeekend } from '@/utils/dateChecks';
|
|
5
|
+
|
|
6
|
+
export class CalendarService {
|
|
7
|
+
private hideWeekends: boolean = false;
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
public monthDate: Date = new Date(),
|
|
11
|
+
public firstDayOfWeek?: FIRST_DAY_OF_WEEK_TYPES,
|
|
12
|
+
public selectedDate?: Date,
|
|
13
|
+
public mode: MODES_TYPES = MODES.MONTH,
|
|
14
|
+
public rangeStart?: Date,
|
|
15
|
+
public rangeEnd?: Date
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
setHideWeekends(value: boolean) {
|
|
19
|
+
this.hideWeekends = value;
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getDays(): Date[] {
|
|
24
|
+
let days: Date[];
|
|
25
|
+
|
|
26
|
+
if (this.mode === MODES.MONTH) {
|
|
27
|
+
days = getMonthDays(this.monthDate, this.firstDayOfWeek);
|
|
28
|
+
} else {
|
|
29
|
+
days = getWeekDays(this.monthDate, this.firstDayOfWeek);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return this.applyFilters(days);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private applyFilters(days: Date[]): Date[] {
|
|
36
|
+
let filteredDays = days;
|
|
37
|
+
|
|
38
|
+
if (this.hideWeekends) {
|
|
39
|
+
filteredDays = filteredDays.filter((day) => !isWeekend(day));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return filteredDays;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getWeeks(): Date[][] {
|
|
46
|
+
const days = this.getDays();
|
|
47
|
+
const weekLength = this.hideWeekends ? 5 : 7;
|
|
48
|
+
return getWeeksFromDays(days, weekLength);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getIsCurrentMonth = (date: Date) => {
|
|
52
|
+
return isCurrentMonth(date, this.monthDate);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
getIsDisabled = (_date: Date) => {
|
|
56
|
+
return false;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
getIsHoliday = (_date: Date) => {
|
|
60
|
+
return false;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { isSameDay } from '@/utils/dateChecks';
|
|
2
|
+
|
|
3
|
+
import { CalendarService } from '../calendarService';
|
|
4
|
+
import { withHolidays } from './withHolidays';
|
|
5
|
+
|
|
6
|
+
jest.mock('@/utils/dateChecks', () => ({
|
|
7
|
+
isSameDay: jest.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
describe('withHolidays:', () => {
|
|
11
|
+
let calendar: CalendarService = new CalendarService();
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
calendar = new CalendarService();
|
|
15
|
+
jest.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('returns the same calendar instance unchanged when the holidays array is empty', () => {
|
|
19
|
+
const originalGetIsHoliday = calendar.getIsHoliday;
|
|
20
|
+
|
|
21
|
+
const result = withHolidays(calendar, []);
|
|
22
|
+
|
|
23
|
+
expect(result).toBe(calendar);
|
|
24
|
+
expect(result.getIsHoliday).toBe(originalGetIsHoliday);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('returns the same calendar instance (mutates rather than clones)', () => {
|
|
28
|
+
const holidays = [new Date(2026, 0, 1)];
|
|
29
|
+
const result = withHolidays(calendar, holidays);
|
|
30
|
+
|
|
31
|
+
expect(result).toBe(calendar);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('overrides getIsHoliday with a new function when holidays is not empty', () => {
|
|
35
|
+
const holidays = [new Date(2026, 0, 1)];
|
|
36
|
+
const originalGetIsHoliday = calendar.getIsHoliday;
|
|
37
|
+
|
|
38
|
+
withHolidays(calendar, holidays);
|
|
39
|
+
|
|
40
|
+
expect(calendar.getIsHoliday).not.toBe(originalGetIsHoliday);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('returns true when the date matches a holiday', () => {
|
|
44
|
+
(isSameDay as jest.Mock).mockReturnValue(true);
|
|
45
|
+
|
|
46
|
+
const holidays = [new Date(2026, 0, 1)];
|
|
47
|
+
const date = new Date(2026, 0, 1);
|
|
48
|
+
|
|
49
|
+
withHolidays(calendar, holidays);
|
|
50
|
+
|
|
51
|
+
expect(calendar.getIsHoliday(date)).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('returns false when the date does not match a holiday', () => {
|
|
55
|
+
(isSameDay as jest.Mock).mockReturnValue(false);
|
|
56
|
+
|
|
57
|
+
const holidays = [new Date(2026, 0, 1)];
|
|
58
|
+
const date = new Date(2026, 0, 2);
|
|
59
|
+
|
|
60
|
+
withHolidays(calendar, holidays);
|
|
61
|
+
|
|
62
|
+
expect(calendar.getIsHoliday(date)).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { isSameDay } from '@/utils/dateChecks';
|
|
2
|
+
|
|
3
|
+
import { CalendarService } from '../calendarService';
|
|
4
|
+
|
|
5
|
+
export const withHolidays = (calendar: CalendarService, holidays: Date[]) => {
|
|
6
|
+
if (!holidays.length) return calendar;
|
|
7
|
+
|
|
8
|
+
calendar.getIsHoliday = (date: Date) => {
|
|
9
|
+
return holidays.some((holiday) => isSameDay(holiday, date));
|
|
10
|
+
};
|
|
11
|
+
return calendar;
|
|
12
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { isDisabled } from '@/utils/dateChecks';
|
|
2
|
+
|
|
3
|
+
import { CalendarService } from '../calendarService';
|
|
4
|
+
import { withMinMax } from './withMinMax';
|
|
5
|
+
|
|
6
|
+
jest.mock('@/utils/dateChecks', () => ({
|
|
7
|
+
isDisabled: jest.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
describe('withMinMax:', () => {
|
|
11
|
+
let calendar: CalendarService;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
calendar = new CalendarService();
|
|
15
|
+
jest.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('returns the same calendar instance', () => {
|
|
19
|
+
const result = withMinMax(
|
|
20
|
+
calendar,
|
|
21
|
+
new Date(2026, 0, 1),
|
|
22
|
+
new Date(2026, 11, 31)
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
expect(result).toBe(calendar);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('returns true when the date is outside the min/max range', () => {
|
|
29
|
+
calendar.getIsDisabled = jest.fn().mockReturnValue(false);
|
|
30
|
+
(isDisabled as jest.Mock).mockReturnValue(true);
|
|
31
|
+
|
|
32
|
+
withMinMax(calendar, new Date(2026, 0, 1), new Date(2026, 11, 31));
|
|
33
|
+
const result = calendar.getIsDisabled(new Date(2027, 0, 1));
|
|
34
|
+
|
|
35
|
+
expect(result).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns false when the date passes both checks', () => {
|
|
39
|
+
calendar.getIsDisabled = jest.fn().mockReturnValue(false);
|
|
40
|
+
(isDisabled as jest.Mock).mockReturnValue(false);
|
|
41
|
+
|
|
42
|
+
withMinMax(calendar, new Date(2026, 0, 1), new Date(2026, 11, 31));
|
|
43
|
+
const result = calendar.getIsDisabled(new Date(2026, 5, 15));
|
|
44
|
+
|
|
45
|
+
expect(result).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('works with only minDate provided', () => {
|
|
49
|
+
calendar.getIsDisabled = jest.fn().mockReturnValue(false);
|
|
50
|
+
(isDisabled as jest.Mock).mockReturnValue(false);
|
|
51
|
+
|
|
52
|
+
const minDate = new Date(2026, 0, 1);
|
|
53
|
+
withMinMax(calendar, minDate);
|
|
54
|
+
calendar.getIsDisabled(new Date(2026, 5, 15));
|
|
55
|
+
|
|
56
|
+
expect(isDisabled).toHaveBeenCalledWith(
|
|
57
|
+
new Date(2026, 5, 15),
|
|
58
|
+
minDate,
|
|
59
|
+
undefined
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('works with only maxDate provided', () => {
|
|
64
|
+
calendar.getIsDisabled = jest.fn().mockReturnValue(false);
|
|
65
|
+
(isDisabled as jest.Mock).mockReturnValue(false);
|
|
66
|
+
|
|
67
|
+
const maxDate = new Date(2026, 11, 31);
|
|
68
|
+
withMinMax(calendar, undefined, maxDate);
|
|
69
|
+
calendar.getIsDisabled(new Date(2026, 5, 15));
|
|
70
|
+
|
|
71
|
+
expect(isDisabled).toHaveBeenCalledWith(
|
|
72
|
+
new Date(2026, 5, 15),
|
|
73
|
+
undefined,
|
|
74
|
+
maxDate
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { isDisabled } from '@/utils/dateChecks';
|
|
2
|
+
|
|
3
|
+
import { CalendarService } from '../calendarService';
|
|
4
|
+
|
|
5
|
+
export function withMinMax(
|
|
6
|
+
calendar: CalendarService,
|
|
7
|
+
minDate?: Date,
|
|
8
|
+
maxDate?: Date
|
|
9
|
+
) {
|
|
10
|
+
const original = calendar.getIsDisabled;
|
|
11
|
+
|
|
12
|
+
calendar.getIsDisabled = (date: Date): boolean => {
|
|
13
|
+
if (original(date)) return true;
|
|
14
|
+
|
|
15
|
+
return isDisabled(date, minDate, maxDate);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
return calendar;
|
|
19
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const HOLIDAY_API_URL = process.env.STORYBOOK_HOLIDAY_API_URL;
|
|
2
|
+
const API_KEY = process.env.STORYBOOK_HOLIDAY_API_KEY;
|
|
3
|
+
|
|
4
|
+
export interface Holiday {
|
|
5
|
+
name: string;
|
|
6
|
+
date: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const getHolidays = async (
|
|
10
|
+
year: number = new Date().getFullYear(),
|
|
11
|
+
signal?: AbortSignal
|
|
12
|
+
): Promise<Date[]> => {
|
|
13
|
+
try {
|
|
14
|
+
let holidayDates;
|
|
15
|
+
const locale = new Intl.Locale(navigator.language);
|
|
16
|
+
const countryCode = locale.region;
|
|
17
|
+
|
|
18
|
+
if (!countryCode) {
|
|
19
|
+
throw new Error('Cannot get your country code');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!API_KEY) {
|
|
23
|
+
throw new Error('API key is not defined');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const url = `${HOLIDAY_API_URL}?key=${API_KEY}&country=${countryCode}&year=${year}`;
|
|
27
|
+
const response = await fetch(url, { signal });
|
|
28
|
+
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new Error(`Error while loading holidays`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const data = await response.json();
|
|
34
|
+
|
|
35
|
+
if (data.holidays) {
|
|
36
|
+
holidayDates = data.holidays.map((holiday: Holiday) => {
|
|
37
|
+
const date = new Date(holiday.date);
|
|
38
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return holidayDates;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return [];
|
|
45
|
+
} catch (err) {
|
|
46
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
47
|
+
throw new Error('Fetch aborted');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (err instanceof Error) {
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
throw new Error('Failed to fetch holidays');
|
|
55
|
+
}
|
|
56
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Task } from './types';
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY = 'tasks';
|
|
4
|
+
|
|
5
|
+
export const loadTasksFromStorage = (): Task[] => {
|
|
6
|
+
try {
|
|
7
|
+
const stored = localStorage.getItem(STORAGE_KEY);
|
|
8
|
+
if (stored) {
|
|
9
|
+
const parsed = JSON.parse(stored);
|
|
10
|
+
return parsed.map((task: Task) => ({
|
|
11
|
+
...task,
|
|
12
|
+
date: new Date(task.date),
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
} catch (error) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
error instanceof Error
|
|
18
|
+
? error.message
|
|
19
|
+
: 'Failed to load tasks from storage'
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return [];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const saveTasksToStorage = (tasks: Task[]): void => {
|
|
26
|
+
try {
|
|
27
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks));
|
|
28
|
+
} catch (error) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
error instanceof Error ? error.message : 'Failed to save tasks to storage'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
};
|