Skip to content

Commit

Permalink
feat: support recurring events
Browse files Browse the repository at this point in the history
  • Loading branch information
Howl authored and Howl committed Sep 23, 2024
1 parent 4ed43ce commit c7baf8a
Show file tree
Hide file tree
Showing 20 changed files with 902 additions and 620 deletions.
320 changes: 164 additions & 156 deletions example/app/(drawer)/index.tsx

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
"lodash.merge": "^4.6.2",
"luxon": "^3.4.4"
"luxon": "^3.4.4",
"rrule": "^2.8.1"
}
}
34 changes: 17 additions & 17 deletions src/CalendarContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { LoadingContext } from './context/LoadingContext';
import LocaleProvider from './context/LocaleProvider';
import NowIndicatorProvider from './context/NowIndicatorProvider';
import ThemeProvider from './context/ThemeProvider';
import TimezoneProvider from './context/TimezoneProvider';
import TimezoneProvider from './context/TimeZoneProvider';
import UnavailableHoursProvider from './context/UnavailableHoursProvider';
import VisibleDateProvider from './context/VisibleDateProvider';
import useLatestCallback from './hooks/useLatestCallback';
Expand Down Expand Up @@ -87,7 +87,7 @@ const CalendarContainer: React.ForwardRefRenderFunction<
minTimeIntervalHeight = 60,
allowPinchToZoom = false,
initialTimeIntervalHeight = 60,
timezone: initialTimezone,
timeZone: initialTimeZone,
showWeekNumber = false,
onChange,
onDateChanged,
Expand Down Expand Up @@ -127,14 +127,14 @@ const CalendarContainer: React.ForwardRefRenderFunction<
throw new Error('The maximum number of days is 7');
}

const timezone = useMemo(() => {
const parsedTimezone = parseDateTime(initialTimezone);
if (!parsedTimezone.isValid) {
console.warn('Timezone is invalid, using local timezone');
const timeZone = useMemo(() => {
const parsedTimeZone = parseDateTime(undefined, { zone: initialTimeZone });
if (!parsedTimeZone.isValid) {
console.warn('TimeZone is invalid, using local timeZone');
return 'local';
}
return initialTimezone || 'local';
}, [initialTimezone]);
return initialTimeZone || 'local';
}, [initialTimeZone]);

const hapticService = useRef(new HapticService()).current;
const [hideWeekDays, setHideWeekDays] = useState(initialHideWeekDays ?? []);
Expand Down Expand Up @@ -188,9 +188,9 @@ const CalendarContainer: React.ForwardRefRenderFunction<
firstDay,
isSingleDay,
hideWeekDays,
timezone,
timeZone,
}),
[minDate, maxDate, firstDay, isSingleDay, hideWeekDays, timezone]
[minDate, maxDate, firstDay, isSingleDay, hideWeekDays, timeZone]
);

const hours = useMemo(
Expand Down Expand Up @@ -222,7 +222,7 @@ const CalendarContainer: React.ForwardRefRenderFunction<
// Current visible date
const visibleDateUnix = useLazyRef(() => {
const zonedInitialDate = parseDateTime(initialDate, {
zone: timezone,
zone: timeZone,
}).toISODate();
let date;
if (scrollByDay) {
Expand Down Expand Up @@ -286,7 +286,7 @@ const CalendarContainer: React.ForwardRefRenderFunction<
const startOffset = useDerivedValue(() => start * minuteHeight.value);

const goToDate = useLatestCallback((props?: GoToDateOptions) => {
const date = parseDateTime(props?.date, { zone: timezone });
const date = parseDateTime(props?.date, { zone: timeZone });
const isoDate = date.toISODate();
let targetDateUnix = parseDateTime(isoDate).toMillis();
if (!scrollByDay) {
Expand Down Expand Up @@ -445,7 +445,7 @@ const CalendarContainer: React.ForwardRefRenderFunction<
);

const setVisibleDate = useLatestCallback((initDate: DateType) => {
const dateObj = parseDateTime(initDate, { zone: timezone });
const dateObj = parseDateTime(initDate, { zone: timeZone });
const isoDate = dateObj.toISODate();
const targetDateUnix = parseDateTime(isoDate).toMillis();
const visibleDates = calendarData.visibleDatesArray;
Expand Down Expand Up @@ -478,7 +478,7 @@ const CalendarContainer: React.ForwardRefRenderFunction<
if (!date) {
return null;
}
const dateObj = forceUpdateZone(date, timezone);
const dateObj = forceUpdateZone(date, timeZone);
return dateTimeToISOString(dateObj);
}
);
Expand Down Expand Up @@ -678,7 +678,7 @@ const CalendarContainer: React.ForwardRefRenderFunction<
return (
<CalendarProvider value={value}>
<LocaleProvider initialLocales={initialLocales} locale={locale}>
<TimezoneProvider timezone={timezone}>
<TimezoneProvider timeZone={timeZone}>
<NowIndicatorProvider>
<ThemeProvider theme={theme}>
<ActionsProvider {...actionsProps}>
Expand All @@ -687,14 +687,14 @@ const CalendarContainer: React.ForwardRefRenderFunction<
<HighlightDatesProvider highlightDates={highlightDates}>
<UnavailableHoursProvider
unavailableHours={unavailableHours}
timezone={timezone}
timeZone={timeZone}
pagesPerSide={pagesPerSide}
>
<EventsProvider
ref={eventsRef}
events={events}
firstDay={firstDay}
timezone={timezone}
timeZone={timeZone}
useAllDayEvent={useAllDayEvent}
pagesPerSide={pagesPerSide}
hideWeekDays={hideWeekDays}
Expand Down
106 changes: 38 additions & 68 deletions src/components/EventItem.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,21 @@
import isEqual from 'lodash.isequal';
import React, { FC, useCallback, useEffect, useMemo, useRef } from 'react';
import React, { FC, useMemo } from 'react';
import { StyleSheet, TouchableOpacity, View } from 'react-native';
import Animated, {
runOnUI,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
import { MILLISECONDS_IN_DAY } from '../constants';
import { useBody } from '../context/BodyContext';
import { useTheme } from '../context/ThemeProvider';
import {
EventItem as EventItemType,
PackedEvent,
SizeAnimation,
} from '../types';
import { OnEventResponse, PackedEvent, SizeAnimation } from '../types';

interface EventItemProps {
event: PackedEvent;
startUnix: number;
renderEvent?: (event: PackedEvent, size: SizeAnimation) => React.ReactNode;
onPressEvent?: (event: EventItemType) => void;
onPressEvent?: (event: OnEventResponse) => void;
onLongPressEvent?: (event: PackedEvent) => void;
isDragging?: boolean;
visibleDates: Record<string, { diffDays: number; unix: number }>;
Expand Down Expand Up @@ -55,7 +49,7 @@ const EventItem: FC<EventItemProps> = ({
startUnix: eventStartUnix,
} = _internal;

const getInitialData = useCallback(() => {
const data = useMemo(() => {
const maxDuration = end - start;
let newStart = startMinutes - start;
let totalDuration = Math.min(duration, maxDuration);
Expand All @@ -68,9 +62,17 @@ const EventItem: FC<EventItemProps> = ({
(eventStartUnix - startUnix) / MILLISECONDS_IN_DAY
);

for (let i = startUnix; i < eventStartUnix; i += MILLISECONDS_IN_DAY) {
if (!visibleDates[i]) {
diffDays--;
if (eventStartUnix < startUnix) {
for (let i = eventStartUnix; i < startUnix; i += MILLISECONDS_IN_DAY) {
if (!visibleDates[i]) {
diffDays++;
}
}
} else {
for (let i = startUnix; i < eventStartUnix; i += MILLISECONDS_IN_DAY) {
if (!visibleDates[i]) {
diffDays--;
}
}
}

Expand All @@ -80,84 +82,52 @@ const EventItem: FC<EventItemProps> = ({
diffDays,
};
}, [
duration,
end,
eventStartUnix,
start,
startMinutes,
duration,
eventStartUnix,
startUnix,
visibleDates,
]);

const data = useMemo(() => getInitialData(), [getInitialData]);

const initialStartDuration = useRef(getInitialData());
const durationAnim = useSharedValue(
initialStartDuration.current.totalDuration
);
const startMinutesAnim = useSharedValue(
initialStartDuration.current.startMinutes
);

const diffDaysAnim = useSharedValue(initialStartDuration.current.diffDays);

useEffect(() => {
runOnUI(() => {
durationAnim.value = withTiming(data.totalDuration, { duration: 150 });
startMinutesAnim.value = withTiming(data.startMinutes, { duration: 150 });
diffDaysAnim.value = withTiming(data.diffDays, { duration: 150 });
})();
}, [
data.diffDays,
data.startMinutes,
data.totalDuration,
diffDaysAnim,
durationAnim,
startMinutesAnim,
]);

const eventHeight = useDerivedValue(
() => durationAnim.value * minuteHeight.value
() => data.totalDuration * minuteHeight.value,
[data.totalDuration]
);

const eventWidth = useDerivedValue(() => {
let width = columnWidthAnim.value * (columnSpan / total);
if (total > 1) {
width -= (rightEdgeSpacing + 1) / total;
const isLast = total - columnSpan === index;
if (isLast) {
width -= rightEdgeSpacing - 1;
} else {
width -= (overlapEventsSpacing * (total - 1)) / total;
}
} else {
width -= rightEdgeSpacing + 1;
}
return width;
});
const totalColumns = total - columnSpan;
const totalOverlap = totalColumns * overlapEventsSpacing;
const totalWidth = columnWidthAnim.value - rightEdgeSpacing - totalOverlap;
let width = (totalWidth / total) * columnSpan;

return withTiming(width, { duration: 150 });
}, [columnSpan, rightEdgeSpacing, overlapEventsSpacing, total]);

const eventPosX = useDerivedValue(() => {
// TODO: check logic here
const extraX =
(columnWidthAnim.value / total - overlapEventsSpacing) * index;
let left = diffDaysAnim.value * columnWidthAnim.value + extraX;
if (total > 1 && index > 0 && index < total) {
left += overlapEventsSpacing * index;
}
return left;
});
let left = data.diffDays * columnWidthAnim.value;
left += (eventWidth.value + overlapEventsSpacing) * index;
return withTiming(left, { duration: 150 });
}, [data.diffDays, overlapEventsSpacing, rightEdgeSpacing, index, total]);

const top = useDerivedValue(() => {
return data.startMinutes * minuteHeight.value;
}, [data.startMinutes]);

const animView = useAnimatedStyle(() => {
return {
height: eventHeight.value,
width: eventWidth.value,
left: eventPosX.value + 1,
top: startMinutesAnim.value * minuteHeight.value,
top: top.value,
};
});

const _onPressEvent = () => {
onPressEvent!(event);
if (onPressEvent) {
onPressEvent(eventInput);
}
};

const _onLongPressEvent = () => {
Expand Down
41 changes: 27 additions & 14 deletions src/components/Events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import {
useDragEventActions,
} from '../context/DragEventProvider';
import { useRegularEvents } from '../context/EventsProvider';
import { useTimezone } from '../context/TimezoneProvider';
import { DragEventProps, PackedEvent } from '../types';
import { forceUpdateZone } from '../utils/dateUtils';
import { useTimezone } from '../context/TimeZoneProvider';
import { PackedEvent } from '../types';
import { forceUpdateZone, parseDateTime } from '../utils/dateUtils';
import EventItem from './EventItem';

const Events: FC<{
Expand All @@ -19,7 +19,7 @@ const Events: FC<{
const { renderEvent, numberOfDays } = useBody();
const { onPressEvent, onLongPressEvent } = useActions();
const { draggingId, selectedEventId } = useDragEvent();
const { timezone } = useTimezone();
const { timeZone } = useTimezone();
const { triggerDragEvent } = useDragEventActions();
const { data: events } = useRegularEvents(
startUnix,
Expand All @@ -29,24 +29,35 @@ const Events: FC<{

const _triggerDragEvent = useCallback(
(event: PackedEvent) => {
if (!event.start.dateTime || !event.end.dateTime) {
return;
}

const eventStart = forceUpdateZone(
event._internal.startUnix,
timezone
timeZone
).startOf('day');
const originalStart = forceUpdateZone(
parseDateTime(event.start.dateTime, { zone: event.start.timeZone }),
timeZone
).startOf('day');
const originalStart = forceUpdateZone(event.start, timezone).startOf(
'day'
);
const startIndex = eventStart.diff(originalStart, 'days').days;
triggerDragEvent!(
{
start: event.start,
end: event.end,
start: {
dateTime: event.start.dateTime,
timeZone: event.start.timeZone,
},
end: {
dateTime: event.end.dateTime,
timeZone: event.end.timeZone,
},
startIndex,
},
event as DragEventProps
event
);
},
[triggerDragEvent, timezone]
[triggerDragEvent, timeZone]
);

const _onLongPressEvent = useCallback(
Expand All @@ -65,15 +76,17 @@ const Events: FC<{
const _renderEvent = (event: PackedEvent) => {
return (
<EventItem
key={event._internal.id}
key={event.localId}
event={event}
startUnix={startUnix}
renderEvent={renderEvent}
onPressEvent={onPressEvent}
onLongPressEvent={
triggerDragEvent ? _triggerDragEvent : _onLongPressEvent
}
isDragging={draggingId === event.id || selectedEventId === event.id}
isDragging={
draggingId === event.localId || selectedEventId === event.localId
}
visibleDates={visibleDates}
/>
);
Expand Down
Loading

0 comments on commit c7baf8a

Please sign in to comment.