Skip to content

Commit 26d3e00

Browse files
authored
Merge pull request #33 from sangyuo/feat/calendar
feat: calendar
2 parents 832833e + fe8a2bb commit 26d3e00

19 files changed

+1080
-53
lines changed

App.tsx

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,32 @@
11
import React from 'react';
2-
import {SafeAreaView, ScrollView} from 'react-native';
3-
4-
import {Box, ButtonBox, ProgressBar, ProgressCircle} from './src/components';
2+
import {SafeAreaView} from 'react-native';
3+
import {CalendarBox} from './src/atomic/organisms/CalendarBox';
4+
import {formatDate} from './src/utils/date.util';
55

66
function App(): React.JSX.Element {
7-
const [value, setValue] = React.useState(0);
7+
const [value, setValue] = React.useState(formatDate('2024-02-02'));
8+
const [selectedDates, setSelectedDates] = React.useState<{[key: string]: {}}>(
9+
{
10+
'2024-11-01': {
11+
classBox: 'rounded-l-xl bg-primary',
12+
classDot: 'bg-green-400',
13+
},
14+
'2024-11-02': {classText: 'text-black'},
15+
'2024-11-03': {
16+
classText: 'text-black',
17+
},
18+
'2024-11-04': {classBox: 'rounded-r-xl bg-primary'},
19+
},
20+
);
821
return (
9-
<SafeAreaView>
10-
<ScrollView>
11-
<Box className="bg-secondary-light mb-5">
12-
<ButtonBox title="Progress up to 100" onPress={() => setValue(100)} />
13-
</Box>
14-
<Box className="bg-secondary-light h-10 mb-5">
15-
<ButtonBox title="Progress up to 25" onPress={() => setValue(25)} />
16-
</Box>
17-
<Box className="gap-2">
18-
<ProgressBar value={value} varian="secondary" />
19-
</Box>
20-
<Box>
21-
<ProgressCircle
22-
value={value}
23-
varian="primary"
24-
showLabel
25-
label={`yeu em`}
26-
/>
27-
</Box>
28-
</ScrollView>
22+
<SafeAreaView style={{flex: 1, backgroundColor: 'white'}}>
23+
<CalendarBox
24+
initDate={value}
25+
selectedDates={selectedDates}
26+
onChangeDate={({dateString}) => {
27+
setSelectedDates({[dateString]: {}});
28+
}}
29+
/>
2930
</SafeAreaView>
3031
);
3132
}

src/atomic/atoms/PanResponderBox.tsx

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import React, {useRef} from 'react';
2+
import {Box} from '..';
3+
import {
4+
GestureResponderEvent,
5+
PanResponder,
6+
PanResponderGestureState,
7+
ViewProps,
8+
} from 'react-native';
9+
import {insertObjectIf} from '../../utils';
10+
11+
const swipeConfig = {
12+
velocityThreshold: 0.3,
13+
directionalOffsetThreshold: 80,
14+
gestureIsClickThreshold: 5,
15+
};
16+
17+
type SwipeDirectionType =
18+
| 'SWIPE_LEFT'
19+
| 'SWIPE_RIGHT'
20+
| 'SWIPE_UP'
21+
| 'SWIPE_DOWN';
22+
23+
export interface PanResponderBoxProps extends ViewProps {
24+
enableResponderMove?: boolean;
25+
className?: string;
26+
onSwipe?: (type: SwipeDirectionType, value: PanResponderGestureState) => void;
27+
onSwipeLeft?: (value: PanResponderGestureState) => void;
28+
onSwipeRight?: (value: PanResponderGestureState) => void;
29+
onSwipeUp?: (value: PanResponderGestureState) => void;
30+
onSwipeDown?: (value: PanResponderGestureState) => void;
31+
}
32+
33+
const PanResponderBox = ({
34+
enableResponderMove = false,
35+
onSwipe,
36+
onSwipeUp,
37+
onSwipeDown,
38+
onSwipeLeft,
39+
onSwipeRight,
40+
...rest
41+
}: PanResponderBoxProps) => {
42+
const handleShouldSetPanResponder = (
43+
evt: GestureResponderEvent,
44+
gestureState: PanResponderGestureState,
45+
) => {
46+
return (
47+
evt.nativeEvent.touches.length === 1 &&
48+
Math.abs(gestureState.dx) < swipeConfig.gestureIsClickThreshold &&
49+
Math.abs(gestureState.dy) < swipeConfig.gestureIsClickThreshold
50+
);
51+
};
52+
53+
const onPanResponderEnd = (
54+
_: GestureResponderEvent,
55+
gestureState: PanResponderGestureState,
56+
) => {
57+
const swipeDirection = getSwipeDirection(gestureState);
58+
if (swipeDirection) {
59+
onSwipe && onSwipe(getSwipeDirection(gestureState)!, gestureState);
60+
switch (getSwipeDirection(gestureState)) {
61+
case 'SWIPE_LEFT':
62+
onSwipeLeft && onSwipeLeft(gestureState);
63+
break;
64+
case 'SWIPE_RIGHT':
65+
onSwipeRight && onSwipeRight(gestureState);
66+
break;
67+
case 'SWIPE_UP':
68+
onSwipeUp && onSwipeUp(gestureState);
69+
break;
70+
case 'SWIPE_DOWN':
71+
onSwipeDown && onSwipeDown(gestureState);
72+
break;
73+
default:
74+
break;
75+
}
76+
}
77+
};
78+
79+
const getSwipeDirection = (
80+
gestureState: PanResponderGestureState,
81+
): SwipeDirectionType | null => {
82+
const {dx, dy, vx} = gestureState;
83+
if (isValidSwipe(vx, dy)) {
84+
return dx > 0 ? 'SWIPE_RIGHT' : 'SWIPE_LEFT';
85+
}
86+
if (isValidSwipe(vx, dx)) {
87+
return dy > 0 ? 'SWIPE_DOWN' : 'SWIPE_UP';
88+
}
89+
return null;
90+
};
91+
92+
function isValidSwipe(velocity: number, directionalOffset: number) {
93+
return (
94+
Math.abs(velocity) > swipeConfig.velocityThreshold &&
95+
Math.abs(directionalOffset) < swipeConfig.directionalOffsetThreshold
96+
);
97+
}
98+
99+
const panResponder = useRef(
100+
PanResponder.create({
101+
onStartShouldSetPanResponder: handleShouldSetPanResponder,
102+
onMoveShouldSetPanResponder: handleShouldSetPanResponder,
103+
onPanResponderRelease: onPanResponderEnd,
104+
...insertObjectIf(enableResponderMove, {
105+
onPanResponderMove: onPanResponderEnd,
106+
}),
107+
}),
108+
).current;
109+
return <Box {...rest} {...panResponder.panHandlers} />;
110+
};
111+
112+
export default PanResponderBox;

src/atomic/atoms/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './Box';
22
export * from './Icons';
33
export * from './ImageBox';
44
export * from './Placeholder';
5+
export {default as PanResponderBox} from './PanResponderBox';
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import React, {memo, ReactNode, useMemo} from 'react';
2+
import {Box, ButtonBox} from '..';
3+
import {
4+
DayItemType,
5+
MonthOfYearType,
6+
SelectedDateItemType,
7+
SelectedDateType,
8+
} from '../../model';
9+
import {classNames} from '../../utils';
10+
11+
interface ListDateItemBoxProps {
12+
item: MonthOfYearType;
13+
widthDay: number;
14+
classToday?: string;
15+
classTextToday?: string;
16+
classSelected?: string;
17+
classTextSelected?: string;
18+
classDay?: string;
19+
classTextDay?: string;
20+
classExtraDay?: string;
21+
classTextExtraDay?: string;
22+
selectedDates?: SelectedDateType;
23+
enableSpecialStyleExtraDays?: boolean;
24+
disablePressExtraDays?: boolean;
25+
hideExtraDays?: boolean;
26+
onChangeDate?: (date: {
27+
year: number;
28+
month: number;
29+
day: number;
30+
dateString: string;
31+
}) => void;
32+
renderDate?: (params: {
33+
date: DayItemType;
34+
dot?: boolean;
35+
classDot?: string;
36+
classBox: string;
37+
classText: string;
38+
}) => ReactNode;
39+
}
40+
41+
const ListDateItemBox = memo((props: ListDateItemBoxProps) => {
42+
const {widthDay, item, selectedDates, hideExtraDays, onChangeDate, ...rest} =
43+
props;
44+
const {days, year, month} = item;
45+
return (
46+
<Box className="row flex-wrap row-gap-1 w-full">
47+
{days.map(item => (
48+
<DateItem
49+
month={month}
50+
year={year}
51+
key={item.dateString}
52+
item={item}
53+
height={widthDay}
54+
width={widthDay}
55+
selected={selectedDates?.[item.dateString]}
56+
onPressDay={onChangeDate}
57+
{...rest}
58+
/>
59+
))}
60+
</Box>
61+
);
62+
});
63+
64+
export default ListDateItemBox;
65+
66+
interface DateItemProps {
67+
width: number;
68+
height: number;
69+
year: number;
70+
month: number;
71+
item: DayItemType;
72+
classToday?: string;
73+
classTextToday?: string;
74+
classSelected?: string;
75+
classTextSelected?: string;
76+
classDay?: string;
77+
classTextDay?: string;
78+
classExtraDay?: string;
79+
classTextExtraDay?: string;
80+
selected?: SelectedDateItemType;
81+
enableSpecialStyleExtraDays?: boolean;
82+
disablePressExtraDays?: boolean;
83+
hideExtraDays?: boolean;
84+
onPressDay?: (date: {
85+
year: number;
86+
month: number;
87+
day: number;
88+
dateString: string;
89+
}) => void;
90+
renderDate?: (params: {
91+
date: DayItemType;
92+
dot?: boolean;
93+
classDot?: string;
94+
classBox: string;
95+
classText: string;
96+
}) => ReactNode;
97+
}
98+
99+
const DateItem = React.memo((props: DateItemProps) => {
100+
const {
101+
width,
102+
height,
103+
month,
104+
year,
105+
item,
106+
classDay,
107+
classTextDay,
108+
classSelected,
109+
classTextSelected,
110+
classToday,
111+
classTextToday,
112+
classExtraDay,
113+
classTextExtraDay,
114+
selected,
115+
enableSpecialStyleExtraDays,
116+
disablePressExtraDays,
117+
hideExtraDays,
118+
renderDate,
119+
onPressDay,
120+
} = props;
121+
122+
const classBox = useMemo(() => {
123+
if (item.isExtraDay && !enableSpecialStyleExtraDays) {
124+
return classNames('items-center justify-center', classExtraDay);
125+
}
126+
return classNames(
127+
'items-center justify-center',
128+
classDay,
129+
item.isToday ? classToday : '',
130+
selected
131+
? selected.classBox || classSelected || 'border border-primary'
132+
: '',
133+
);
134+
}, [item, selected, classDay, classToday, enableSpecialStyleExtraDays]);
135+
136+
const classText = useMemo(() => {
137+
if (item.isExtraDay && !enableSpecialStyleExtraDays) {
138+
return classNames(
139+
'text-md text-center w-full text-gray-400',
140+
classTextExtraDay,
141+
);
142+
}
143+
return classNames(
144+
'text-md text-center w-full',
145+
classTextDay,
146+
item.isToday && selected
147+
? 'text-black'
148+
: item.isToday
149+
? 'text-primary'
150+
: '',
151+
item.isToday ? classTextToday : '',
152+
selected ? selected.classText || classTextSelected : '',
153+
);
154+
}, [
155+
selected,
156+
item,
157+
classTextSelected,
158+
classTextDay,
159+
classTextToday,
160+
enableSpecialStyleExtraDays,
161+
]);
162+
163+
if (renderDate) {
164+
return renderDate({
165+
date: item,
166+
classBox,
167+
classText,
168+
dot: selected?.dot,
169+
classDot: selected?.classDot,
170+
});
171+
}
172+
173+
return (
174+
<ButtonBox
175+
key={item.dateString}
176+
style={{width, height}}
177+
disabled={disablePressExtraDays && item.isExtraDay}
178+
className={classBox}
179+
title={hideExtraDays && item.isExtraDay ? '' : item.day.toString()}
180+
classText={classText}
181+
onPress={() => onPressDay && onPressDay({year, month, ...item})}>
182+
{selected && selected?.dot && (
183+
<Box
184+
className={classNames(
185+
'w-1 h-1 bg-primary rounded-full absolute top-2',
186+
selected.classDot,
187+
)}
188+
/>
189+
)}
190+
</ButtonBox>
191+
);
192+
});

src/atomic/molecules/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export * from './RadioButton';
66
export * from './Checkbox';
77
export * from './PlaceholderCard';
88
export * from './SwitchBox';
9+
export {default as CalendarItemBox} from './CalendarItemBox';

0 commit comments

Comments
 (0)