@idealyst/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/README.md +88 -0
- package/package.json +77 -0
- package/src/DatePicker/Calendar.native.tsx +159 -0
- package/src/DatePicker/Calendar.styles.tsx +224 -0
- package/src/DatePicker/Calendar.tsx +154 -0
- package/src/DatePicker/DatePicker.native.tsx +33 -0
- package/src/DatePicker/DatePicker.styles.tsx +69 -0
- package/src/DatePicker/DatePicker.web.tsx +31 -0
- package/src/DatePicker/index.native.ts +3 -0
- package/src/DatePicker/index.ts +3 -0
- package/src/DatePicker/types.ts +78 -0
- package/src/DateRangePicker/DateRangePicker.native.tsx +39 -0
- package/src/DateRangePicker/DateRangePicker.styles.tsx +83 -0
- package/src/DateRangePicker/DateRangePicker.web.tsx +40 -0
- package/src/DateRangePicker/RangeCalendar.native.tsx +267 -0
- package/src/DateRangePicker/RangeCalendar.styles.tsx +170 -0
- package/src/DateRangePicker/RangeCalendar.web.tsx +275 -0
- package/src/DateRangePicker/index.native.ts +3 -0
- package/src/DateRangePicker/index.ts +3 -0
- package/src/DateRangePicker/types.ts +98 -0
- package/src/DateTimePicker/DateTimePicker.native.tsx +82 -0
- package/src/DateTimePicker/DateTimePicker.styles.tsx +77 -0
- package/src/DateTimePicker/DateTimePicker.tsx +79 -0
- package/src/DateTimePicker/TimePicker.native.tsx +204 -0
- package/src/DateTimePicker/TimePicker.styles.tsx +116 -0
- package/src/DateTimePicker/TimePicker.tsx +406 -0
- package/src/DateTimePicker/index.native.ts +3 -0
- package/src/DateTimePicker/index.ts +3 -0
- package/src/DateTimePicker/types.ts +84 -0
- package/src/DateTimeRangePicker/DateTimeRangePicker.native.tsx +213 -0
- package/src/DateTimeRangePicker/DateTimeRangePicker.styles.tsx +95 -0
- package/src/DateTimeRangePicker/DateTimeRangePicker.web.tsx +141 -0
- package/src/DateTimeRangePicker/index.native.ts +2 -0
- package/src/DateTimeRangePicker/index.ts +2 -0
- package/src/DateTimeRangePicker/types.ts +72 -0
- package/src/examples/DatePickerExamples.tsx +274 -0
- package/src/examples/index.ts +1 -0
- package/src/index.native.ts +16 -0
- package/src/index.ts +16 -0
- package/src/primitives/CalendarGrid/CalendarGrid.styles.tsx +62 -0
- package/src/primitives/CalendarGrid/CalendarGrid.tsx +138 -0
- package/src/primitives/CalendarGrid/index.ts +1 -0
- package/src/primitives/CalendarHeader/CalendarHeader.styles.tsx +25 -0
- package/src/primitives/CalendarHeader/CalendarHeader.tsx +69 -0
- package/src/primitives/CalendarHeader/index.ts +1 -0
- package/src/primitives/CalendarOverlay/CalendarOverlay.styles.tsx +81 -0
- package/src/primitives/CalendarOverlay/CalendarOverlay.tsx +130 -0
- package/src/primitives/CalendarOverlay/index.ts +1 -0
- package/src/primitives/Wrapper/Wrapper.web.tsx +33 -0
- package/src/primitives/Wrapper/index.ts +1 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { View, Text } from '@idealyst/components';
|
|
3
|
+
import { DatePickerProps } from './types';
|
|
4
|
+
import { Calendar } from './Calendar.native';
|
|
5
|
+
import { datePickerStyles } from './DatePicker.styles';
|
|
6
|
+
|
|
7
|
+
const DatePicker: React.FC<DatePickerProps> = ({
|
|
8
|
+
value,
|
|
9
|
+
onChange,
|
|
10
|
+
minDate,
|
|
11
|
+
maxDate,
|
|
12
|
+
disabled = false,
|
|
13
|
+
style,
|
|
14
|
+
testID,
|
|
15
|
+
}) => {
|
|
16
|
+
const handleDateSelect = (date: Date) => {
|
|
17
|
+
onChange(date);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Calendar
|
|
22
|
+
value={value}
|
|
23
|
+
onChange={handleDateSelect}
|
|
24
|
+
minDate={minDate}
|
|
25
|
+
maxDate={maxDate}
|
|
26
|
+
disabled={disabled}
|
|
27
|
+
style={style}
|
|
28
|
+
testID={testID}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default DatePicker;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
2
|
+
|
|
3
|
+
export const datePickerStyles = StyleSheet.create((theme) => ({
|
|
4
|
+
container: {
|
|
5
|
+
// Base container styles
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
label: {
|
|
9
|
+
marginBottom: theme.spacing?.sm || 8,
|
|
10
|
+
fontSize: theme.typography?.sizes?.small || 14,
|
|
11
|
+
fontWeight: '500',
|
|
12
|
+
color: theme.colors?.text?.primary || '#1f2937',
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
picker: {
|
|
16
|
+
borderRadius: theme.borderRadius?.md || 8,
|
|
17
|
+
border: `1px solid ${theme.colors?.border?.primary || '#e5e7eb'}`,
|
|
18
|
+
backgroundColor: theme.colors?.surface?.primary || '#ffffff',
|
|
19
|
+
padding: theme.spacing?.md || 16,
|
|
20
|
+
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
|
|
21
|
+
|
|
22
|
+
// Native specific styles
|
|
23
|
+
_native: {
|
|
24
|
+
borderWidth: 1,
|
|
25
|
+
borderColor: theme.colors?.border?.primary || '#e5e7eb',
|
|
26
|
+
shadowColor: '#000',
|
|
27
|
+
shadowOffset: { width: 0, height: 1 },
|
|
28
|
+
shadowOpacity: 0.1,
|
|
29
|
+
shadowRadius: 3,
|
|
30
|
+
elevation: 2,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
selectedDateHeader: {
|
|
35
|
+
marginBottom: theme.spacing?.sm || 12,
|
|
36
|
+
paddingBottom: theme.spacing?.sm || 12,
|
|
37
|
+
borderBottomWidth: 1,
|
|
38
|
+
borderBottomColor: theme.colors?.border?.secondary || '#f3f4f6',
|
|
39
|
+
|
|
40
|
+
// Web specific border
|
|
41
|
+
_web: {
|
|
42
|
+
borderBottom: `1px solid ${theme.colors?.border?.secondary || '#f3f4f6'}`,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
selectedDateLabel: {
|
|
47
|
+
fontSize: theme.typography?.sizes?.small || 12,
|
|
48
|
+
color: theme.colors?.text?.secondary || '#6b7280',
|
|
49
|
+
marginBottom: 4,
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
selectedDateValue: {
|
|
53
|
+
fontSize: theme.typography?.sizes?.medium || 16,
|
|
54
|
+
fontWeight: '600',
|
|
55
|
+
color: theme.colors?.text?.primary || '#1f2937',
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
errorText: {
|
|
59
|
+
marginTop: 4,
|
|
60
|
+
fontSize: theme.typography?.sizes?.small || 12,
|
|
61
|
+
color: theme.colors?.text?.error || '#dc2626',
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
helperText: {
|
|
65
|
+
marginTop: 4,
|
|
66
|
+
fontSize: theme.typography?.sizes?.small || 12,
|
|
67
|
+
color: theme.colors?.text?.secondary || '#6b7280',
|
|
68
|
+
},
|
|
69
|
+
}));
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { DatePickerProps } from './types';
|
|
3
|
+
import { Calendar } from './Calendar';
|
|
4
|
+
|
|
5
|
+
const DatePicker: React.FC<DatePickerProps> = ({
|
|
6
|
+
value,
|
|
7
|
+
onChange,
|
|
8
|
+
minDate,
|
|
9
|
+
maxDate,
|
|
10
|
+
disabled = false,
|
|
11
|
+
style,
|
|
12
|
+
testID,
|
|
13
|
+
}) => {
|
|
14
|
+
const handleDateSelect = (date: Date) => {
|
|
15
|
+
onChange(date);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Calendar
|
|
20
|
+
value={value}
|
|
21
|
+
onChange={handleDateSelect}
|
|
22
|
+
minDate={minDate}
|
|
23
|
+
maxDate={maxDate}
|
|
24
|
+
disabled={disabled}
|
|
25
|
+
style={style}
|
|
26
|
+
testID={testID}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default DatePicker;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface DatePickerProps {
|
|
5
|
+
/** Current selected date */
|
|
6
|
+
value?: Date;
|
|
7
|
+
|
|
8
|
+
/** Called when date changes */
|
|
9
|
+
onChange: (date: Date | null) => void;
|
|
10
|
+
|
|
11
|
+
/** Minimum selectable date */
|
|
12
|
+
minDate?: Date;
|
|
13
|
+
|
|
14
|
+
/** Maximum selectable date */
|
|
15
|
+
maxDate?: Date;
|
|
16
|
+
|
|
17
|
+
/** Disabled state */
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
|
|
20
|
+
/** Placeholder text when no date is selected */
|
|
21
|
+
placeholder?: string;
|
|
22
|
+
|
|
23
|
+
/** Label for the picker */
|
|
24
|
+
label?: string;
|
|
25
|
+
|
|
26
|
+
/** Error message to display */
|
|
27
|
+
error?: string;
|
|
28
|
+
|
|
29
|
+
/** Helper text */
|
|
30
|
+
helperText?: string;
|
|
31
|
+
|
|
32
|
+
/** Date format for display (default: 'MM/dd/yyyy') */
|
|
33
|
+
format?: string;
|
|
34
|
+
|
|
35
|
+
/** Locale for date formatting */
|
|
36
|
+
locale?: string;
|
|
37
|
+
|
|
38
|
+
/** Size variant */
|
|
39
|
+
size?: 'small' | 'medium' | 'large';
|
|
40
|
+
|
|
41
|
+
/** Visual variant */
|
|
42
|
+
variant?: 'outlined' | 'filled';
|
|
43
|
+
|
|
44
|
+
/** Custom styles */
|
|
45
|
+
style?: ViewStyle;
|
|
46
|
+
|
|
47
|
+
/** Test ID for testing */
|
|
48
|
+
testID?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface CalendarProps {
|
|
52
|
+
/** Current selected date */
|
|
53
|
+
value?: Date;
|
|
54
|
+
|
|
55
|
+
/** Called when date is selected */
|
|
56
|
+
onChange: (date: Date) => void;
|
|
57
|
+
|
|
58
|
+
/** Minimum selectable date */
|
|
59
|
+
minDate?: Date;
|
|
60
|
+
|
|
61
|
+
/** Maximum selectable date */
|
|
62
|
+
maxDate?: Date;
|
|
63
|
+
|
|
64
|
+
/** Disabled state */
|
|
65
|
+
disabled?: boolean;
|
|
66
|
+
|
|
67
|
+
/** Current month being viewed */
|
|
68
|
+
currentMonth?: Date;
|
|
69
|
+
|
|
70
|
+
/** Called when month changes */
|
|
71
|
+
onMonthChange?: (month: Date) => void;
|
|
72
|
+
|
|
73
|
+
/** Custom styles */
|
|
74
|
+
style?: ViewStyle;
|
|
75
|
+
|
|
76
|
+
/** Test ID for testing */
|
|
77
|
+
testID?: string;
|
|
78
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text } from '@idealyst/components';
|
|
3
|
+
import { DateRangePickerProps, DateRange } from './types';
|
|
4
|
+
import { RangeCalendar } from './RangeCalendar.native';
|
|
5
|
+
import { dateRangePickerStyles } from './DateRangePicker.styles';
|
|
6
|
+
|
|
7
|
+
const DateRangePicker: React.FC<DateRangePickerProps> = ({
|
|
8
|
+
value,
|
|
9
|
+
onChange,
|
|
10
|
+
minDate,
|
|
11
|
+
maxDate,
|
|
12
|
+
disabled = false,
|
|
13
|
+
allowSameDay = true,
|
|
14
|
+
maxDays,
|
|
15
|
+
style,
|
|
16
|
+
testID,
|
|
17
|
+
}) => {
|
|
18
|
+
|
|
19
|
+
const handleRangeChange = (newRange: DateRange) => {
|
|
20
|
+
onChange(newRange.startDate || newRange.endDate ? newRange : null);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<RangeCalendar
|
|
26
|
+
value={value || {}}
|
|
27
|
+
onChange={handleRangeChange}
|
|
28
|
+
minDate={minDate}
|
|
29
|
+
maxDate={maxDate}
|
|
30
|
+
disabled={disabled}
|
|
31
|
+
allowSameDay={allowSameDay}
|
|
32
|
+
maxDays={maxDays}
|
|
33
|
+
style={style}
|
|
34
|
+
testID={testID}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default DateRangePicker;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
2
|
+
|
|
3
|
+
export const dateRangePickerStyles = StyleSheet.create((theme) => ({
|
|
4
|
+
container: {
|
|
5
|
+
gap: theme.spacing?.md || 16,
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
label: {
|
|
9
|
+
fontSize: theme.typography?.sizes?.small || 14,
|
|
10
|
+
fontWeight: '600',
|
|
11
|
+
color: theme.colors?.text?.primary || '#111827',
|
|
12
|
+
marginBottom: theme.spacing?.xs || 4,
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
picker: {
|
|
16
|
+
gap: theme.spacing?.md || 16,
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
selectedRangeHeader: {
|
|
20
|
+
padding: theme.spacing?.sm || 12,
|
|
21
|
+
backgroundColor: theme.colors?.surface?.secondary || '#f9fafb',
|
|
22
|
+
borderRadius: theme.borderRadius?.md || 8,
|
|
23
|
+
borderWidth: 1,
|
|
24
|
+
borderColor: theme.colors?.border?.primary || '#e5e7eb',
|
|
25
|
+
gap: theme.spacing?.xs || 4,
|
|
26
|
+
|
|
27
|
+
_web: {
|
|
28
|
+
border: `1px solid ${theme.colors?.border?.primary || '#e5e7eb'}`,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
selectedRangeLabel: {
|
|
33
|
+
fontSize: theme.typography?.sizes?.small || 12,
|
|
34
|
+
fontWeight: '500',
|
|
35
|
+
color: theme.colors?.text?.secondary || '#6b7280',
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
selectedRangeValue: {
|
|
39
|
+
fontSize: theme.typography?.sizes?.medium || 16,
|
|
40
|
+
fontWeight: '600',
|
|
41
|
+
color: theme.colors?.text?.primary || '#111827',
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
rangeInputs: {
|
|
45
|
+
flexDirection: 'row',
|
|
46
|
+
alignItems: 'center',
|
|
47
|
+
gap: theme.spacing?.sm || 12,
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
rangeInput: {
|
|
51
|
+
flex: 1,
|
|
52
|
+
padding: theme.spacing?.sm || 12,
|
|
53
|
+
borderWidth: 1,
|
|
54
|
+
borderColor: theme.colors?.border?.primary || '#e5e7eb',
|
|
55
|
+
borderRadius: theme.borderRadius?.md || 8,
|
|
56
|
+
backgroundColor: theme.colors?.surface?.primary || '#ffffff',
|
|
57
|
+
fontSize: theme.typography?.sizes?.medium || 16,
|
|
58
|
+
textAlign: 'center',
|
|
59
|
+
|
|
60
|
+
_web: {
|
|
61
|
+
border: `1px solid ${theme.colors?.border?.primary || '#e5e7eb'}`,
|
|
62
|
+
outline: 'none',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
rangeSeparator: {
|
|
67
|
+
fontSize: theme.typography?.sizes?.medium || 16,
|
|
68
|
+
fontWeight: '500',
|
|
69
|
+
color: theme.colors?.text?.secondary || '#6b7280',
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
errorText: {
|
|
73
|
+
fontSize: theme.typography?.sizes?.small || 12,
|
|
74
|
+
color: theme.colors?.semantic?.error || '#dc2626',
|
|
75
|
+
marginTop: theme.spacing?.xs || 4,
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
helperText: {
|
|
79
|
+
fontSize: theme.typography?.sizes?.small || 12,
|
|
80
|
+
color: theme.colors?.text?.secondary || '#6b7280',
|
|
81
|
+
marginTop: theme.spacing?.xs || 4,
|
|
82
|
+
},
|
|
83
|
+
}));
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text } from '@idealyst/components';
|
|
3
|
+
import { getWebProps } from 'react-native-unistyles/web';
|
|
4
|
+
import { DateRangePickerProps, DateRange } from './types';
|
|
5
|
+
import { RangeCalendar } from './RangeCalendar.web';
|
|
6
|
+
import { dateRangePickerStyles } from './DateRangePicker.styles';
|
|
7
|
+
|
|
8
|
+
const DateRangePicker: React.FC<DateRangePickerProps> = ({
|
|
9
|
+
value,
|
|
10
|
+
onChange,
|
|
11
|
+
minDate,
|
|
12
|
+
maxDate,
|
|
13
|
+
disabled = false,
|
|
14
|
+
allowSameDay = true,
|
|
15
|
+
maxDays,
|
|
16
|
+
style,
|
|
17
|
+
testID,
|
|
18
|
+
}) => {
|
|
19
|
+
|
|
20
|
+
const handleRangeChange = (newRange: DateRange) => {
|
|
21
|
+
onChange(newRange.startDate || newRange.endDate ? newRange : null);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<RangeCalendar
|
|
27
|
+
value={value || {}}
|
|
28
|
+
onChange={handleRangeChange}
|
|
29
|
+
minDate={minDate}
|
|
30
|
+
maxDate={maxDate}
|
|
31
|
+
disabled={disabled}
|
|
32
|
+
allowSameDay={allowSameDay}
|
|
33
|
+
maxDays={maxDays}
|
|
34
|
+
style={style}
|
|
35
|
+
testID={testID}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default DateRangePicker;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import React, { useState, useMemo } from 'react';
|
|
2
|
+
import { View, Text, Button } from '@idealyst/components';
|
|
3
|
+
import { TouchableOpacity } from 'react-native';
|
|
4
|
+
import { RangeCalendarProps, DateRange } from './types';
|
|
5
|
+
import { rangeCalendarStyles } from './RangeCalendar.styles';
|
|
6
|
+
|
|
7
|
+
export const RangeCalendar: React.FC<RangeCalendarProps> = ({
|
|
8
|
+
value = {},
|
|
9
|
+
onChange,
|
|
10
|
+
minDate,
|
|
11
|
+
maxDate,
|
|
12
|
+
disabled = false,
|
|
13
|
+
currentMonth: controlledCurrentMonth,
|
|
14
|
+
onMonthChange,
|
|
15
|
+
allowSameDay = true,
|
|
16
|
+
maxDays,
|
|
17
|
+
style,
|
|
18
|
+
testID,
|
|
19
|
+
}) => {
|
|
20
|
+
const [internalCurrentMonth, setInternalCurrentMonth] = useState(
|
|
21
|
+
controlledCurrentMonth || value?.startDate || new Date()
|
|
22
|
+
);
|
|
23
|
+
const [selectingEnd, setSelectingEnd] = useState(false);
|
|
24
|
+
|
|
25
|
+
const currentMonth = controlledCurrentMonth || internalCurrentMonth;
|
|
26
|
+
|
|
27
|
+
const handleMonthChange = (newMonth: Date) => {
|
|
28
|
+
if (onMonthChange) {
|
|
29
|
+
onMonthChange(newMonth);
|
|
30
|
+
} else {
|
|
31
|
+
setInternalCurrentMonth(newMonth);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const { monthStart, monthEnd, daysInMonth, startingDayOfWeek } = useMemo(() => {
|
|
36
|
+
const monthStart = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), 1);
|
|
37
|
+
const monthEnd = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 0);
|
|
38
|
+
const daysInMonth = monthEnd.getDate();
|
|
39
|
+
const startingDayOfWeek = monthStart.getDay();
|
|
40
|
+
|
|
41
|
+
return { monthStart, monthEnd, daysInMonth, startingDayOfWeek };
|
|
42
|
+
}, [currentMonth]);
|
|
43
|
+
|
|
44
|
+
const isDateDisabled = (date: Date): boolean => {
|
|
45
|
+
if (disabled) return true;
|
|
46
|
+
if (minDate && date < minDate) return true;
|
|
47
|
+
if (maxDate && date > maxDate) return true;
|
|
48
|
+
return false;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const isDateInRange = (date: Date): boolean => {
|
|
52
|
+
const { startDate, endDate } = value || {};
|
|
53
|
+
if (!startDate || !endDate) return false;
|
|
54
|
+
return date > startDate && date < endDate;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const isDateRangeStart = (date: Date): boolean => {
|
|
58
|
+
const { startDate } = value || {};
|
|
59
|
+
if (!startDate) return false;
|
|
60
|
+
return (
|
|
61
|
+
date.getDate() === startDate.getDate() &&
|
|
62
|
+
date.getMonth() === startDate.getMonth() &&
|
|
63
|
+
date.getFullYear() === startDate.getFullYear()
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const isDateRangeEnd = (date: Date): boolean => {
|
|
68
|
+
const { endDate } = value || {};
|
|
69
|
+
if (!endDate) return false;
|
|
70
|
+
return (
|
|
71
|
+
date.getDate() === endDate.getDate() &&
|
|
72
|
+
date.getMonth() === endDate.getMonth() &&
|
|
73
|
+
date.getFullYear() === endDate.getFullYear()
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const isDateSelected = (date: Date): boolean => {
|
|
78
|
+
return isDateRangeStart(date) || isDateRangeEnd(date);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const handleDateClick = (date: Date) => {
|
|
82
|
+
if (isDateDisabled(date)) return;
|
|
83
|
+
|
|
84
|
+
const { startDate, endDate } = value || {};
|
|
85
|
+
|
|
86
|
+
// If no range is selected or we're starting fresh
|
|
87
|
+
if (!startDate || (startDate && endDate)) {
|
|
88
|
+
onChange({ startDate: date, endDate: undefined });
|
|
89
|
+
setSelectingEnd(true);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// If we have a start date but no end date
|
|
94
|
+
if (startDate && !endDate) {
|
|
95
|
+
let newStartDate = startDate;
|
|
96
|
+
let newEndDate = date;
|
|
97
|
+
|
|
98
|
+
// Swap if end date is before start date
|
|
99
|
+
if (date < startDate) {
|
|
100
|
+
newStartDate = date;
|
|
101
|
+
newEndDate = startDate;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check if same day selection is allowed
|
|
105
|
+
if (!allowSameDay && newStartDate.getTime() === newEndDate.getTime()) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check max days constraint
|
|
110
|
+
if (maxDays) {
|
|
111
|
+
const daysDiff = Math.ceil((newEndDate.getTime() - newStartDate.getTime()) / (1000 * 60 * 60 * 24));
|
|
112
|
+
if (daysDiff > maxDays) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
onChange({ startDate: newStartDate, endDate: newEndDate });
|
|
118
|
+
setSelectingEnd(false);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const goToPreviousMonth = () => {
|
|
123
|
+
const newMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1);
|
|
124
|
+
handleMonthChange(newMonth);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const goToNextMonth = () => {
|
|
128
|
+
const newMonth = new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1);
|
|
129
|
+
handleMonthChange(newMonth);
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const handlePresetRange = (days: number) => {
|
|
133
|
+
const startDate = new Date();
|
|
134
|
+
const endDate = new Date();
|
|
135
|
+
endDate.setDate(endDate.getDate() + days - 1);
|
|
136
|
+
|
|
137
|
+
onChange({ startDate, endDate });
|
|
138
|
+
setSelectingEnd(false);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const clearRange = () => {
|
|
142
|
+
onChange({});
|
|
143
|
+
setSelectingEnd(false);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const monthName = currentMonth.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
|
|
147
|
+
|
|
148
|
+
// Create calendar grid
|
|
149
|
+
const calendarDays = [];
|
|
150
|
+
|
|
151
|
+
// Add empty cells for days before month starts
|
|
152
|
+
for (let i = 0; i < startingDayOfWeek; i++) {
|
|
153
|
+
calendarDays.push(null);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Add days of the month
|
|
157
|
+
for (let day = 1; day <= daysInMonth; day++) {
|
|
158
|
+
const date = new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day);
|
|
159
|
+
calendarDays.push(date);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
rangeCalendarStyles.useVariants({});
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<View style={[rangeCalendarStyles.container, style]} testID={testID}>
|
|
166
|
+
{/* Header */}
|
|
167
|
+
<View style={rangeCalendarStyles.header}>
|
|
168
|
+
<Button
|
|
169
|
+
variant="text"
|
|
170
|
+
size="small"
|
|
171
|
+
onPress={goToPreviousMonth}
|
|
172
|
+
disabled={disabled}
|
|
173
|
+
style={rangeCalendarStyles.headerButton}
|
|
174
|
+
>
|
|
175
|
+
←
|
|
176
|
+
</Button>
|
|
177
|
+
<Text weight="semibold">{monthName}</Text>
|
|
178
|
+
<Button
|
|
179
|
+
variant="text"
|
|
180
|
+
size="small"
|
|
181
|
+
onPress={goToNextMonth}
|
|
182
|
+
disabled={disabled}
|
|
183
|
+
style={rangeCalendarStyles.headerButton}
|
|
184
|
+
>
|
|
185
|
+
→
|
|
186
|
+
</Button>
|
|
187
|
+
</View>
|
|
188
|
+
|
|
189
|
+
{/* Weekday headers */}
|
|
190
|
+
<View style={rangeCalendarStyles.weekdayHeader}>
|
|
191
|
+
{['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].map((day) => (
|
|
192
|
+
<View key={day} style={rangeCalendarStyles.weekdayCell}>
|
|
193
|
+
<Text style={rangeCalendarStyles.weekdayText}>
|
|
194
|
+
{day}
|
|
195
|
+
</Text>
|
|
196
|
+
</View>
|
|
197
|
+
))}
|
|
198
|
+
</View>
|
|
199
|
+
|
|
200
|
+
{/* Calendar grid */}
|
|
201
|
+
<View style={rangeCalendarStyles.calendarGrid}>
|
|
202
|
+
{calendarDays.map((date, index) => (
|
|
203
|
+
<View key={index} style={rangeCalendarStyles.dayCell}>
|
|
204
|
+
{date && (
|
|
205
|
+
<TouchableOpacity
|
|
206
|
+
onPress={() => handleDateClick(date)}
|
|
207
|
+
disabled={isDateDisabled(date)}
|
|
208
|
+
style={[
|
|
209
|
+
rangeCalendarStyles.dayButton,
|
|
210
|
+
{
|
|
211
|
+
backgroundColor: isDateSelected(date)
|
|
212
|
+
? '#3b82f6'
|
|
213
|
+
: isDateInRange(date)
|
|
214
|
+
? '#3b82f620'
|
|
215
|
+
: 'transparent',
|
|
216
|
+
opacity: isDateDisabled(date) ? 0.5 : 1,
|
|
217
|
+
}
|
|
218
|
+
]}
|
|
219
|
+
>
|
|
220
|
+
<Text
|
|
221
|
+
style={{
|
|
222
|
+
color: isDateSelected(date) ? 'white' : 'black',
|
|
223
|
+
fontSize: 13,
|
|
224
|
+
fontWeight: isDateSelected(date) ? '600' : '400',
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
{date.getDate()}
|
|
228
|
+
</Text>
|
|
229
|
+
</TouchableOpacity>
|
|
230
|
+
)}
|
|
231
|
+
</View>
|
|
232
|
+
))}
|
|
233
|
+
</View>
|
|
234
|
+
|
|
235
|
+
{/* Range presets */}
|
|
236
|
+
<View style={rangeCalendarStyles.rangePresets}>
|
|
237
|
+
<Button
|
|
238
|
+
variant="text"
|
|
239
|
+
size="small"
|
|
240
|
+
onPress={() => handlePresetRange(7)}
|
|
241
|
+
disabled={disabled}
|
|
242
|
+
style={rangeCalendarStyles.presetButton}
|
|
243
|
+
>
|
|
244
|
+
Next 7 days
|
|
245
|
+
</Button>
|
|
246
|
+
<Button
|
|
247
|
+
variant="text"
|
|
248
|
+
size="small"
|
|
249
|
+
onPress={() => handlePresetRange(30)}
|
|
250
|
+
disabled={disabled}
|
|
251
|
+
style={rangeCalendarStyles.presetButton}
|
|
252
|
+
>
|
|
253
|
+
Next 30 days
|
|
254
|
+
</Button>
|
|
255
|
+
<Button
|
|
256
|
+
variant="outlined"
|
|
257
|
+
size="small"
|
|
258
|
+
onPress={clearRange}
|
|
259
|
+
disabled={disabled}
|
|
260
|
+
style={rangeCalendarStyles.clearButton}
|
|
261
|
+
>
|
|
262
|
+
Clear Range
|
|
263
|
+
</Button>
|
|
264
|
+
</View>
|
|
265
|
+
</View>
|
|
266
|
+
);
|
|
267
|
+
};
|