@momo-kits/calendar 0.121.0-rc.3 → 0.121.0-rc.4

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/Day.tsx CHANGED
@@ -1,5 +1,4 @@
1
- import React, {Component} from 'react';
2
-
1
+ import React, {useContext} from 'react';
3
2
  import {Text as RNText, TouchableOpacity, View} from 'react-native';
4
3
  import {
5
4
  ApplicationContext,
@@ -12,209 +11,130 @@ import {DayProps} from './types';
12
11
  import {ContainerContext} from './index';
13
12
  import styles from './styles';
14
13
 
15
- class Day extends Component<DayProps> {
16
- static contextType = ApplicationContext;
17
- isFocus?: boolean;
18
- isValid?: boolean;
19
- isMid?: boolean;
20
- isStart?: boolean;
21
- isStartPart?: boolean;
22
- isEnd?: boolean;
23
- isWeekEnd?: boolean;
24
- showLunar?: boolean;
25
- lunarDate?: {lunarDay: number; lunarMonth: number};
26
- isDoubleDateMode?: boolean;
27
- isLunarHoliday?: boolean;
28
- isLunarDayStart?: boolean;
29
- isSolarHoliday?: boolean;
30
- isInScope?: boolean;
31
- diffPrice: any;
14
+ const Day: React.FC<DayProps> = props => {
15
+ const {
16
+ startDate,
17
+ endDate,
18
+ date,
19
+ minDate,
20
+ maxDate,
21
+ isEmpty,
22
+ index,
23
+ isShowLunar,
24
+ isDoubleDateMode,
25
+ lunarDate,
26
+ isSolarHoliday,
27
+ isLunarHoliday,
28
+ tabSelected,
29
+ isDisabled = false,
30
+ onChoose,
31
+ price,
32
+ isBestPrice,
33
+ } = props;
34
+
35
+ const {theme} = useContext(ApplicationContext);
36
+ const size = useContext(ContainerContext);
32
37
 
33
- constructor(props: DayProps) {
34
- super(props);
35
- this.statusCheck();
38
+ const itemWidth = (size.width - Spacing.M) / 7;
39
+ if (isEmpty || !date) {
40
+ return <View style={[styles.dayContainer, {width: itemWidth}]} />;
36
41
  }
37
42
 
38
- chooseDay = () => {
39
- const {onChoose, date} = this.props;
40
- onChoose && onChoose(date);
41
- };
43
+ const isValid =
44
+ !isDisabled &&
45
+ (date.isAfter(minDate, 'day') || date.isSame(minDate, 'day')) &&
46
+ (date.isBefore(maxDate, 'day') || date.isSame(maxDate, 'day'));
42
47
 
43
- statusCheck = (props?: DayProps) => {
44
- const {
45
- startDate,
46
- endDate,
47
- date,
48
- minDate,
49
- maxDate,
50
- empty,
51
- index,
52
- isShowLunar,
53
- isDoubleDateMode,
54
- lunarDate,
55
- isSolarHoliday,
56
- isLunarHoliday,
57
- tabSelected,
58
- isDisabled = false,
59
- } = props || this.props;
60
- this.isValid =
61
- !isDisabled &&
62
- date &&
63
- (date >= minDate || date.isSame(minDate, 'day')) &&
64
- (date <= maxDate || date.isSame(maxDate, 'day'));
65
- this.isMid =
66
- (isDoubleDateMode && date > startDate && date < endDate) ||
67
- (!date && empty >= startDate && empty <= endDate);
68
- this.isStart = date && date.isSame(startDate, 'd');
69
- this.isStartPart = !!(this.isStart && endDate);
70
- this.isEnd = isDoubleDateMode && date && date.isSame(endDate, 'day');
71
- this.isFocus = this.isMid || this.isStart || this.isEnd;
72
- this.isWeekEnd = index === 6 || index === 5;
73
- this.showLunar = isShowLunar;
74
- this.lunarDate = lunarDate;
75
- this.isDoubleDateMode = isDoubleDateMode;
76
- this.isLunarHoliday = isLunarHoliday;
77
- this.isLunarDayStart = this.lunarDate && this.lunarDate.lunarDay === 1;
78
- this.isSolarHoliday = isSolarHoliday;
79
- this.isInScope = isDoubleDateMode
80
- ? tabSelected === 0 ||
81
- (tabSelected === 1 &&
82
- startDate &&
83
- date &&
84
- date.isSameOrAfter(startDate, 'day'))
85
- : true;
48
+ const isMid =
49
+ isDoubleDateMode &&
50
+ date.isAfter(startDate, 'day') &&
51
+ date.isBefore(endDate, 'day');
86
52
 
87
- return this.isFocus || this.diffPrice;
88
- };
53
+ const isStart = date.isSame(startDate, 'day');
54
+ const isStartPart = !!(isStart && endDate);
55
+ const isEnd = isDoubleDateMode && date.isSame(endDate, 'day');
56
+ const isWeekEnd = index === 6 || index === 5;
57
+ const showLunar = isShowLunar;
89
58
 
90
- shouldComponentUpdate(nextProps: DayProps) {
91
- const {
92
- isDoubleDateMode,
93
- isShowLunar,
94
- tabSelected,
95
- isSolarHoliday,
96
- isLunarHoliday,
97
- price,
98
- isBestPrice,
99
- startDate,
100
- endDate,
101
- } = this.props;
102
- const prevStatus = this.isFocus;
103
- const selectionModeChange = isDoubleDateMode !== nextProps.isDoubleDateMode;
104
- const lunarChange = isShowLunar !== nextProps.isShowLunar;
105
- const nextStatus = this.statusCheck(nextProps);
106
- const tabChange = tabSelected !== nextProps.tabSelected;
107
- const solarHoliday = isSolarHoliday !== nextProps.isSolarHoliday;
108
- const lunarHoliday = isLunarHoliday !== nextProps.isLunarHoliday;
109
- const diffPrice =
110
- price !== nextProps.price && isBestPrice !== nextProps.isBestPrice;
111
- const startDateChange =
112
- startDate && !startDate?.isSame?.(nextProps.startDate, 'day');
113
- const endDateChange =
114
- endDate && !endDate?.isSame?.(nextProps.endDate, 'day');
115
- return (
116
- prevStatus !== nextStatus ||
117
- selectionModeChange ||
118
- lunarChange ||
119
- tabChange ||
120
- solarHoliday ||
121
- lunarHoliday ||
122
- diffPrice ||
123
- startDateChange ||
124
- endDateChange
125
- );
126
- }
59
+ const isInScope = isDoubleDateMode
60
+ ? tabSelected === 0 || date.isSameOrAfter(startDate, 'day')
61
+ : true;
127
62
 
128
- render() {
129
- const {theme} = this.context;
130
- const {date, empty, price, isBestPrice} = this.props;
131
- const text = date ? date.date() : '';
132
- let textColor = theme.colors.text.default;
133
- let lunarTextColor = theme.colors.text.hint;
134
- let priceColor = theme.colors.text.default;
63
+ const chooseDay = () => {
64
+ onChoose && onChoose(date);
65
+ };
135
66
 
136
- if (this.isWeekEnd || this.isSolarHoliday || this.isLunarHoliday) {
137
- textColor = theme.colors.error.primary;
138
- priceColor = theme.colors.error.primary;
139
- }
67
+ let textColor = theme.colors.text.default;
68
+ let lunarTextColor = theme.colors.text.hint;
69
+ let priceColor = theme.colors.text.default;
140
70
 
141
- if (isBestPrice) {
142
- priceColor = theme.colors.error.primary;
143
- }
71
+ if (isWeekEnd || isSolarHoliday || isLunarHoliday) {
72
+ textColor = theme.colors.error.primary;
73
+ priceColor = theme.colors.error.primary;
74
+ }
144
75
 
145
- if (this.isStart || this.isEnd) {
146
- textColor = Colors.black_01;
147
- priceColor = Colors.black_01;
148
- lunarTextColor = Colors.black_01;
149
- }
76
+ if (isBestPrice) {
77
+ priceColor = theme.colors.error.primary;
78
+ }
150
79
 
151
- if (!this.isValid || !this.isInScope) {
152
- textColor = theme.colors.text.disable;
153
- priceColor = theme.colors.text.disable;
154
- lunarTextColor = theme.colors.text.disable;
155
- }
80
+ if (isStart || isEnd) {
81
+ textColor = Colors.black_01;
82
+ priceColor = Colors.black_01;
83
+ lunarTextColor = Colors.black_01;
84
+ }
156
85
 
157
- return (
158
- <ContainerContext.Consumer>
159
- {size => {
160
- const itemWidth = (size.width - Spacing.M) / 7;
161
- return (
162
- <View style={[styles.dayContainer, {width: itemWidth}]}>
163
- <View
164
- style={[
165
- this.isMid &&
166
- !empty && {
167
- backgroundColor: theme.colors.background.tonal,
168
- },
169
- this.isStartPart && styles.dayStartContainer,
170
- this.isEnd && styles.dayEndContainer,
171
- ]}>
172
- <TouchableOpacity
173
- style={[
174
- styles.day,
175
- {
176
- width: itemWidth,
177
- paddingVertical: !!price ? Spacing.XS : Spacing.S,
178
- },
179
- (this.isStart || this.isEnd) && {
180
- backgroundColor: theme.colors.primary,
181
- },
182
- !!price && {height: scaleSize(48)},
183
- {height: size.height},
184
- ]}
185
- disabled={!(this.isValid && this.isInScope)}
186
- onPress={this.chooseDay}>
187
- {this.lunarDate && this.showLunar && !!text && (
188
- <RNText
189
- style={{
190
- position: 'absolute',
191
- top: Spacing.XS,
192
- right: Spacing.XS,
193
- color: lunarTextColor,
194
- }}>
195
- {this.lunarDate.lunarDay === 1
196
- ? `${this.lunarDate.lunarDay}/${this.lunarDate.lunarMonth}`
197
- : this.lunarDate.lunarDay}
198
- </RNText>
199
- )}
200
- <Text typography={'header_s_semibold'} color={textColor}>
201
- {text}
202
- </Text>
203
- {!!price && (
204
- <Text
205
- typography={'description_xs_regular'}
206
- color={priceColor}>
207
- {price}
208
- </Text>
209
- )}
210
- </TouchableOpacity>
211
- </View>
212
- </View>
213
- );
214
- }}
215
- </ContainerContext.Consumer>
216
- );
86
+ if (!isValid || !isInScope) {
87
+ textColor = theme.colors.text.disable;
88
+ priceColor = theme.colors.text.disable;
89
+ lunarTextColor = theme.colors.text.disable;
217
90
  }
218
- }
91
+
92
+ return (
93
+ <View style={[styles.dayContainer, {width: itemWidth}]}>
94
+ <View
95
+ style={[
96
+ isMid && {backgroundColor: theme.colors.background.tonal},
97
+ isStartPart && styles.dayStartContainer,
98
+ isEnd && styles.dayEndContainer,
99
+ ]}>
100
+ <TouchableOpacity
101
+ style={[
102
+ styles.day,
103
+ {
104
+ width: itemWidth,
105
+ paddingVertical: !!price ? Spacing.XS : Spacing.S,
106
+ },
107
+ (isStart || isEnd) && {backgroundColor: theme.colors.primary},
108
+ !!price && {height: scaleSize(48)},
109
+ {height: size.height},
110
+ ]}
111
+ disabled={!(isValid && isInScope)}
112
+ onPress={chooseDay}>
113
+ {lunarDate && showLunar && !!date && (
114
+ <RNText
115
+ style={{
116
+ position: 'absolute',
117
+ top: Spacing.XS,
118
+ right: Spacing.XS,
119
+ color: lunarTextColor,
120
+ }}>
121
+ {lunarDate.lunarDay === 1
122
+ ? `${lunarDate.lunarDay}/${lunarDate.lunarMonth}`
123
+ : lunarDate.lunarDay}
124
+ </RNText>
125
+ )}
126
+ <Text typography="header_s_semibold" color={textColor}>
127
+ {date ? date.date() : ''}
128
+ </Text>
129
+ {!!price && (
130
+ <Text typography="description_xs_regular" color={priceColor}>
131
+ {price}
132
+ </Text>
133
+ )}
134
+ </TouchableOpacity>
135
+ </View>
136
+ </View>
137
+ );
138
+ };
219
139
 
220
140
  export default Day;
@@ -132,6 +132,13 @@ function LunarSolarConverter(this: any) {
132
132
  lunarMonth: number;
133
133
  lunarDay: number;
134
134
  }) {
135
+ // year range check: only support lunar calendar from 1900
136
+ const minLunarYear = 1900;
137
+ // using converter data, maximum supported lunar year
138
+ const maxLunarYear = this.lunar_month_days[0] + this.lunar_month_days.length - 1;
139
+ if (lunar.lunarYear < minLunarYear || lunar.lunarYear > maxLunarYear) {
140
+ throw new Error(`LunarToSolar: year ${lunar.lunarYear} out of supported range ${minLunarYear}-${maxLunarYear}`);
141
+ }
135
142
  const days =
136
143
  this.lunar_month_days[lunar.lunarYear - this.lunar_month_days[0]];
137
144
  const leap = this.GetBitInt(days, 4, 13);
@@ -163,6 +170,13 @@ function LunarSolarConverter(this: any) {
163
170
  solarMonth: number;
164
171
  solarDay: number;
165
172
  }) {
173
+ // year range check: only support solar dates from 1900
174
+ const minSolarYear = 1900;
175
+ // using converter data, maximum supported solar year
176
+ const maxSolarYear = this.solar_1_1[0] + this.solar_1_1.length - 1;
177
+ if (solar.solarYear < minSolarYear || solar.solarYear > maxSolarYear) {
178
+ throw new Error(`SolarToLunar: year ${solar.solarYear} out of supported range ${minSolarYear}-${maxSolarYear}`);
179
+ }
166
180
  // @ts-ignore
167
181
  const lunar = new Lunar();
168
182
  let index = solar.solarYear - this.solar_1_1[0];
package/Month.tsx CHANGED
@@ -1,114 +1,117 @@
1
- import React, {PureComponent} from 'react';
2
-
1
+ import React, {useContext, useMemo} from 'react';
3
2
  import {View} from 'react-native';
4
- import moment from 'moment';
5
- import Moment from 'moment';
3
+ import moment, {Moment} from 'moment';
6
4
  import Day from './Day';
7
5
  import {scaleSize, Spacing} from '@momo-kits/foundation';
8
6
  import {MonthProps} from './types';
9
7
  import {ContainerContext} from './index';
10
8
 
11
- export default class Month extends PureComponent<MonthProps> {
12
- rowArray;
13
- temp;
14
- constructor(props: MonthProps) {
15
- super(props);
16
- const {dateList} = props;
17
- this.rowArray = new Array(dateList.length / 7).fill('');
18
- this.temp = this.rowArray.map((item, i) =>
19
- dateList.slice(i * 7, i * 7 + 7),
20
- );
21
- }
9
+ const Month: React.FC<MonthProps> = ({
10
+ dateList,
11
+ holidays,
12
+ priceListDate,
13
+ minDate,
14
+ maxDate,
15
+ startDate,
16
+ endDate,
17
+ isShowLunar,
18
+ isDoubleDateMode,
19
+ disabledDays,
20
+ month,
21
+ ...props
22
+ }) => {
23
+ const rows = useMemo(() => {
24
+ const rowCount = dateList.length / 7;
25
+ const rowArray = new Array(rowCount).fill('');
26
+ return rowArray.map((_, i) => dateList.slice(i * 7, i * 7 + 7));
27
+ }, [dateList]);
22
28
 
23
- findHoliday = (date: Moment.Moment) => {
24
- const {holidays} = this.props;
25
- if (date && holidays && holidays.length > 0) {
26
- const day = date.date();
27
- const month = date.month() + 1;
28
- return holidays.find(item => item.day === day && item.month === month);
29
+ const findHoliday = (date?: Moment) => {
30
+ if (!date || !holidays || holidays.length === 0) {
31
+ return null;
29
32
  }
30
- return null;
33
+ const day = date.date();
34
+ const monthNum = date.month() + 1;
35
+ return (
36
+ holidays.find(item => item.day === day && item.month === monthNum) || null
37
+ );
31
38
  };
32
39
 
33
- checkHoliday = (date: moment.Moment) => {
34
- const holiday = this.findHoliday(date);
40
+ const checkHoliday = (date?: Moment) => {
41
+ const holiday = findHoliday(date);
35
42
  return {
36
43
  isSolarHoliday: !!(holiday && !holiday.lunar),
37
44
  isLunarHoliday: !!(holiday && holiday.lunar),
38
45
  };
39
46
  };
40
47
 
41
- renderDayRow = (dayList: any[], index: number) => (
42
- <View
43
- style={{
44
- flexDirection: 'row',
45
- alignItems: 'center',
46
- marginBottom: Spacing.XS,
47
- height: scaleSize(48),
48
- }}
49
- key={`row${index}`}>
50
- {dayList.map((item, i) => {
51
- const keyDay = moment(item.date).format('YYYY-MM-DD');
52
- const priceInfo = this.props?.priceListDate?.[keyDay];
53
- const {
54
- minDate,
55
- maxDate,
56
- startDate,
57
- endDate,
58
- isShowLunar,
59
- isDoubleDateMode,
60
- disabledDays,
61
- } = this.props;
62
-
63
- const dateItem = new Date(item.date);
64
- const mappedDisabledArray = disabledDays?.map(item => String(item));
65
- const isDisabled = mappedDisabledArray?.includes(String(dateItem));
48
+ const size = useContext(ContainerContext);
66
49
 
67
- return (
68
- <Day
69
- {...this.props}
70
- minDate={minDate}
71
- maxDate={maxDate}
72
- isShowLunar={isShowLunar}
73
- isDoubleDateMode={isDoubleDateMode}
74
- startDate={startDate}
75
- endDate={endDate}
76
- {...this.checkHoliday(item.date)}
77
- date={item.date}
78
- havePriceList={!!this.props.priceListDate}
79
- lunarDate={item.lunarDate}
80
- empty={item.empty}
81
- key={`day${i.toString()}`}
82
- index={i}
83
- price={priceInfo?.priceAsString}
84
- isBestPrice={priceInfo?.isBestPrice}
85
- isDisabled={isDisabled}
86
- />
87
- );
88
- })}
89
- </View>
50
+ // Precompute disabled dates set for fast lookup (format: YYYY-MM-DD)
51
+ const disabledSet = useMemo(
52
+ () => new Set((disabledDays || []).map(d => moment(d).format('YYYY-MM-DD'))),
53
+ [disabledDays]
90
54
  );
91
55
 
92
- render() {
93
- const {month} = this.props;
94
- if (month) {
95
- return (
96
- <ContainerContext.Consumer>
97
- {size => (
98
- <View style={{width: size.width}}>
99
- <View
100
- style={{
101
- paddingHorizontal: Spacing.S,
102
- width: '100%',
103
- alignItems: 'center',
104
- }}>
105
- {this.temp.map((item, i) => this.renderDayRow(item, i))}
106
- </View>
107
- </View>
108
- )}
109
- </ContainerContext.Consumer>
110
- );
111
- }
56
+ if (!month) {
112
57
  return <View />;
113
58
  }
114
- }
59
+
60
+ return (
61
+ <View style={{width: size.width}}>
62
+ <View
63
+ style={{
64
+ paddingHorizontal: Spacing.S,
65
+ width: '100%',
66
+ alignItems: 'center',
67
+ }}>
68
+ {rows.map((dayList, rowIndex) => (
69
+ <View
70
+ key={`row${rowIndex}`}
71
+ style={{
72
+ flexDirection: 'row',
73
+ alignItems: 'center',
74
+ marginBottom: Spacing.XS,
75
+ height: scaleSize(48),
76
+ }}>
77
+ {dayList.map((item, i) => {
78
+ const {isEmpty, date: dateMoment, lunarDate} = item;
79
+ // compute key for price/disabled only if not empty
80
+ const keyDay = !isEmpty && dateMoment
81
+ ? dateMoment.format('YYYY-MM-DD')
82
+ : '';
83
+ const priceInfo = keyDay ? priceListDate?.[keyDay] : undefined;
84
+ const isDisabled = keyDay
85
+ ? disabledSet.has(keyDay)
86
+ : false;
87
+
88
+ return (
89
+ <Day
90
+ {...props}
91
+ minDate={minDate}
92
+ maxDate={maxDate}
93
+ isShowLunar={isShowLunar}
94
+ isDoubleDateMode={isDoubleDateMode}
95
+ startDate={startDate}
96
+ endDate={endDate}
97
+ {...checkHoliday(dateMoment)}
98
+ date={dateMoment}
99
+ havePriceList={!!priceListDate}
100
+ lunarDate={lunarDate}
101
+ isEmpty={isEmpty}
102
+ key={`day${i}`}
103
+ index={i}
104
+ price={priceInfo?.priceAsString}
105
+ isBestPrice={priceInfo?.isBestPrice}
106
+ isDisabled={isDisabled}
107
+ />
108
+ );
109
+ })}
110
+ </View>
111
+ ))}
112
+ </View>
113
+ </View>
114
+ );
115
+ };
116
+
117
+ export default React.memo(Month);