Skip to content

Commit

Permalink
feat(calendar): UI date
Browse files Browse the repository at this point in the history
  • Loading branch information
sangyuo committed Nov 15, 2024
1 parent ef8c54a commit efc01dd
Show file tree
Hide file tree
Showing 17 changed files with 898 additions and 155 deletions.
46 changes: 43 additions & 3 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,52 @@
import React from 'react';
import {SafeAreaView, ScrollView} from 'react-native';
import {SafeAreaView} from 'react-native';
import {CalendarBox} from './src/atomic/organisms/CalendarBox';
import {formatDate} from './src/utils/date.util';
import {CalendarSwipeBox, PanResponderBox, TextBox, useClassName} from './src';

function App(): React.JSX.Element {
const [value, setValue] = React.useState(0);
const [value, setValue] = React.useState(formatDate('2024-02-02'));
const [selectedDates, setSelectedDates] = React.useState<{[key: string]: {}}>(
{
'2024-11-01': {
classBox: 'rounded-l-xl bg-primary',
classDot: 'bg-green-400',
},
'2024-11-02': {classText: 'text-black'},
'2024-11-03': {
classText: 'text-black',
},
'2024-11-04': {classBox: 'rounded-r-xl bg-primary'},
},
);
return (
<SafeAreaView style={{flex: 1, backgroundColor: 'white'}}>
<CalendarBox />
<PanResponderBox
className="bg-gray-400 w-full h-96"
onSwipe={type => {
console.log(type);
}}
/>
<CalendarSwipeBox
initDate={value}
selectedDates={selectedDates}
onChangeDate={({dateString}) => {
console.log(dateString);

setSelectedDates({[dateString]: {}});
}}
/>
{/* <CalendarBox
width={320}
initDate={value}
hideExtraDays
classTextSelected="text-red-500"
selectedDates={selectedDates}
onChangeDate={({dateString}) => setSelectedDates({[dateString]: {}})}
renderDateItem={({date, classText}) => {
return <TextBox className={classText}>{date.dateString}</TextBox>;
}}
/> */}
</SafeAreaView>
);
}
Expand Down
112 changes: 112 additions & 0 deletions src/atomic/atoms/PanResponderBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React, {useRef} from 'react';
import {Box} from '..';
import {
GestureResponderEvent,
PanResponder,
PanResponderGestureState,
ViewProps,
} from 'react-native';
import {insertObjectIf} from '../../utils';

const swipeConfig = {
velocityThreshold: 0.3,
directionalOffsetThreshold: 80,
gestureIsClickThreshold: 5,
};

type SwipeDirectionType =
| 'SWIPE_LEFT'
| 'SWIPE_RIGHT'
| 'SWIPE_UP'
| 'SWIPE_DOWN';

export interface PanResponderBoxProps extends ViewProps {
enableResponderMove?: boolean;
className?: string;
onSwipe?: (type: SwipeDirectionType, value: PanResponderGestureState) => void;
onSwipeLeft?: (value: PanResponderGestureState) => void;
onSwipeRight?: (value: PanResponderGestureState) => void;
onSwipeUp?: (value: PanResponderGestureState) => void;
onSwipeDown?: (value: PanResponderGestureState) => void;
}

const PanResponderBox = ({
enableResponderMove = false,
onSwipe,
onSwipeUp,
onSwipeDown,
onSwipeLeft,
onSwipeRight,
...rest
}: PanResponderBoxProps) => {
const handleShouldSetPanResponder = (
evt: GestureResponderEvent,
gestureState: PanResponderGestureState,
) => {
return (
evt.nativeEvent.touches.length === 1 &&
Math.abs(gestureState.dx) < swipeConfig.gestureIsClickThreshold &&
Math.abs(gestureState.dy) < swipeConfig.gestureIsClickThreshold
);
};

const onPanResponderEnd = (
_: GestureResponderEvent,
gestureState: PanResponderGestureState,
) => {
const swipeDirection = getSwipeDirection(gestureState);
if (swipeDirection) {
onSwipe && onSwipe(getSwipeDirection(gestureState)!, gestureState);
switch (getSwipeDirection(gestureState)) {
case 'SWIPE_LEFT':
onSwipeLeft && onSwipeLeft(gestureState);
break;
case 'SWIPE_RIGHT':
onSwipeRight && onSwipeRight(gestureState);
break;
case 'SWIPE_UP':
onSwipeUp && onSwipeUp(gestureState);
break;
case 'SWIPE_DOWN':
onSwipeDown && onSwipeDown(gestureState);
break;
default:
break;
}
}
};

const getSwipeDirection = (
gestureState: PanResponderGestureState,
): SwipeDirectionType | null => {
const {dx, dy, vx} = gestureState;
if (isValidSwipe(vx, dy)) {
return dx > 0 ? 'SWIPE_RIGHT' : 'SWIPE_LEFT';
}
if (isValidSwipe(vx, dx)) {
return dy > 0 ? 'SWIPE_DOWN' : 'SWIPE_UP';
}
return null;
};

function isValidSwipe(velocity: number, directionalOffset: number) {
return (
Math.abs(velocity) > swipeConfig.velocityThreshold &&
Math.abs(directionalOffset) < swipeConfig.directionalOffsetThreshold
);
}

const panResponder = useRef(
PanResponder.create({
onStartShouldSetPanResponder: handleShouldSetPanResponder,
onMoveShouldSetPanResponder: handleShouldSetPanResponder,
onPanResponderRelease: onPanResponderEnd,
...insertObjectIf(enableResponderMove, {
onPanResponderMove: onPanResponderEnd,
}),
}),
).current;
return <Box {...rest} {...panResponder.panHandlers} />;
};

export default PanResponderBox;
1 change: 1 addition & 0 deletions src/atomic/atoms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './Box';
export * from './Icons';
export * from './ImageBox';
export * from './Placeholder';
export {default as PanResponderBox} from './PanResponderBox';
186 changes: 186 additions & 0 deletions src/atomic/molecules/CalendarItemBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import React, {memo, ReactNode, useMemo} from 'react';
import {Box, ButtonBox} from '..';
import {
DayItemType,
MonthOfYearType,
SelectedDateItemType,
SelectedDateType,
} from '../../model';
import {classNames} from '../../utils';

interface ListDateItemBoxProps {
item: MonthOfYearType;
widthDay: number;
classToday?: string;
classTextToday?: string;
classSelected?: string;
classTextSelected?: string;
classDay?: string;
classTextDay?: string;
classExtraDay?: string;
classTextExtraDay?: string;
selectedDates?: SelectedDateType;
enableSpecialStyleExtraDays?: boolean;
disablePressExtraDays?: boolean;
hideExtraDays?: boolean;
onChangeDate?: (date: {
year: number;
month: number;
day: number;
dateString: string;
}) => void;
renderDate?: (params: {
date: DayItemType;
dot?: boolean;
classDot?: string;
classBox: string;
classText: string;
}) => ReactNode;
}

const ListDateItemBox = memo((props: ListDateItemBoxProps) => {
const {widthDay, item, selectedDates, hideExtraDays, onChangeDate, ...rest} =
props;
const {days, year, month} = item;
return (
<Box className="row flex-wrap row-gap-1 w-full">
{days.map(item => (
<DateItem
month={month}
year={year}
key={item.dateString}
item={item}
height={widthDay}
width={widthDay}
selected={selectedDates?.[item.dateString]}
onPressDay={onChangeDate}
{...rest}
/>
))}
</Box>
);
});

export default ListDateItemBox;

interface DateItemProps {
width: number;
height: number;
year: number;
month: number;
item: DayItemType;
classToday?: string;
classTextToday?: string;
classSelected?: string;
classTextSelected?: string;
classDay?: string;
classTextDay?: string;
classExtraDay?: string;
classTextExtraDay?: string;
selected?: SelectedDateItemType;
enableSpecialStyleExtraDays?: boolean;
disablePressExtraDays?: boolean;
hideExtraDays?: boolean;
onPressDay?: (date: {
year: number;
month: number;
day: number;
dateString: string;
}) => void;
renderDate?: (params: {
date: DayItemType;
dot?: boolean;
classDot?: string;
classBox: string;
classText: string;
}) => ReactNode;
}

const DateItem = React.memo((props: DateItemProps) => {
const {
width,
height,
month,
year,
item,
classDay,
classTextDay,
classSelected,
classTextSelected,
classToday,
classTextToday,
classExtraDay,
classTextExtraDay,
selected,
enableSpecialStyleExtraDays,
disablePressExtraDays,
hideExtraDays,
renderDate,
onPressDay,
} = props;
const classBox = useMemo(() => {
if (item.isExtraDay && !enableSpecialStyleExtraDays) {
return classNames('items-center justify-center', classExtraDay);
}
return classNames(
'items-center justify-center',
classDay,
item.isToday ? classToday : '',
selected
? selected.classBox || classSelected || 'border border-primary'
: '',
);
}, [item, selected, classDay, classToday, enableSpecialStyleExtraDays]);

const classText = useMemo(() => {
if (item.isExtraDay && !enableSpecialStyleExtraDays) {
return classNames(
'text-md text-center w-full text-gray-400',
classTextExtraDay,
);
}
return classNames(
'text-md text-center w-full',
classTextDay,
item.isToday && selected ? 'text-black' : '',
item.isToday ? classTextToday : '',
selected ? selected.classText || classTextSelected : '',
);
}, [
selected,
item,
classTextSelected,
classTextDay,
classTextToday,
enableSpecialStyleExtraDays,
]);

if (renderDate) {
return renderDate({
date: item,
classBox,
classText,
dot: selected?.dot,
classDot: selected?.classDot,
});
}

return (
<ButtonBox
style={{width, height}}
disabled={disablePressExtraDays && item.isExtraDay}
className={classBox}
title={hideExtraDays && item.isExtraDay ? '' : item.day.toString()}
classText={classText}
onPress={() => onPressDay && onPressDay({year, month, ...item})}>
{selected && selected?.dot && (
<Box
className={classNames(
'w-1 h-1 bg-primary rounded-full absolute top-2',
selected.classDot,
)}
/>
)}
</ButtonBox>
);
});
1 change: 1 addition & 0 deletions src/atomic/molecules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './RadioButton';
export * from './Checkbox';
export * from './PlaceholderCard';
export * from './SwitchBox';
export {default as CalendarItemBox} from './CalendarItemBox';
Loading

0 comments on commit efc01dd

Please sign in to comment.