@sunsama/event-calendar 0.13.4 → 0.13.6

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.
Files changed (47) hide show
  1. package/lib/commonjs/components/edit-event-container.js +14 -10
  2. package/lib/commonjs/components/edit-event-container.js.map +1 -1
  3. package/lib/commonjs/components/timed-event-container.js +13 -8
  4. package/lib/commonjs/components/timed-event-container.js.map +1 -1
  5. package/lib/commonjs/components/zoom-provider.js +4 -1
  6. package/lib/commonjs/components/zoom-provider.js.map +1 -1
  7. package/lib/commonjs/utils/date-utils.js +19 -1
  8. package/lib/commonjs/utils/date-utils.js.map +1 -1
  9. package/lib/commonjs/utils/generate-event-layouts.js +4 -0
  10. package/lib/commonjs/utils/generate-event-layouts.js.map +1 -1
  11. package/lib/commonjs/utils/pan-edit-event-gesture.js +36 -33
  12. package/lib/commonjs/utils/pan-edit-event-gesture.js.map +1 -1
  13. package/lib/module/components/edit-event-container.js +14 -10
  14. package/lib/module/components/edit-event-container.js.map +1 -1
  15. package/lib/module/components/timed-event-container.js +13 -8
  16. package/lib/module/components/timed-event-container.js.map +1 -1
  17. package/lib/module/components/zoom-provider.js +4 -1
  18. package/lib/module/components/zoom-provider.js.map +1 -1
  19. package/lib/module/utils/date-utils.js +17 -0
  20. package/lib/module/utils/date-utils.js.map +1 -1
  21. package/lib/module/utils/generate-event-layouts.js +5 -1
  22. package/lib/module/utils/generate-event-layouts.js.map +1 -1
  23. package/lib/module/utils/pan-edit-event-gesture.js +36 -33
  24. package/lib/module/utils/pan-edit-event-gesture.js.map +1 -1
  25. package/lib/typescript/commonjs/components/edit-event-container.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/components/timed-event-container.d.ts.map +1 -1
  27. package/lib/typescript/commonjs/components/zoom-provider.d.ts.map +1 -1
  28. package/lib/typescript/commonjs/utils/date-utils.d.ts +1 -0
  29. package/lib/typescript/commonjs/utils/date-utils.d.ts.map +1 -1
  30. package/lib/typescript/commonjs/utils/generate-event-layouts.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/utils/pan-edit-event-gesture.d.ts.map +1 -1
  32. package/lib/typescript/module/components/edit-event-container.d.ts.map +1 -1
  33. package/lib/typescript/module/components/timed-event-container.d.ts.map +1 -1
  34. package/lib/typescript/module/components/zoom-provider.d.ts.map +1 -1
  35. package/lib/typescript/module/utils/date-utils.d.ts +1 -0
  36. package/lib/typescript/module/utils/date-utils.d.ts.map +1 -1
  37. package/lib/typescript/module/utils/generate-event-layouts.d.ts.map +1 -1
  38. package/lib/typescript/module/utils/pan-edit-event-gesture.d.ts.map +1 -1
  39. package/package.json +1 -1
  40. package/src/components/edit-event-container.tsx +19 -11
  41. package/src/components/timed-event-container.tsx +14 -8
  42. package/src/components/zoom-provider.tsx +4 -1
  43. package/src/utils/__tests___/date-utils-extended.test.ts +34 -0
  44. package/src/utils/__tests___/generate-event-layout.test.ts +29 -0
  45. package/src/utils/date-utils.ts +18 -0
  46. package/src/utils/generate-event-layouts.ts +5 -0
  47. package/src/utils/pan-edit-event-gesture.ts +6 -4
@@ -18,7 +18,7 @@ import Animated, {
18
18
  useSharedValue,
19
19
  } from "react-native-reanimated";
20
20
  import useAutoScroll from "../hooks/use-auto-scroll";
21
- import { type CalendarEvent, EventExtend } from "../types";
21
+ import { EventExtend } from "../types";
22
22
  import gesturePan from "../utils/pan-edit-event-gesture";
23
23
  import DragBar from "../components/drag-bar";
24
24
  import { StyleSheet } from "react-native";
@@ -77,9 +77,9 @@ const EditEventContainer = memo(
77
77
  });
78
78
 
79
79
  const calculateHeight = useCallback(
80
- (event: CalendarEvent, zoom: number) => {
81
- const start = new Date(event.start);
82
- const end = new Date(event.end);
80
+ (startStr: string, endStr: string, zoom: number) => {
81
+ const start = new Date(startStr);
82
+ const end = new Date(endStr);
83
83
 
84
84
  // We can't use the position.height as that has a minimum time of 30 minutes, which might not be relevant to
85
85
  // our actual duration, meaning we'll have to calculate the height based on the start and end times
@@ -93,23 +93,31 @@ const EditEventContainer = memo(
93
93
  return;
94
94
  }
95
95
 
96
- calculateHeight(editingEvent.event, zoomLevel.value);
96
+ calculateHeight(
97
+ editingEvent.event.start,
98
+ editingEvent.event.end,
99
+ zoomLevel.value
100
+ );
97
101
  currentY.value = editingEvent.position.top * zoomLevel.value;
98
102
  }, [height, editingEvent, currentY, zoomLevel, timezone, calculateHeight]);
99
103
 
104
+ const eventStart = editingEvent?.event.start;
105
+ const eventEnd = editingEvent?.event.end;
106
+ const eventTop = editingEvent?.position.top;
100
107
  useAnimatedReaction(
101
108
  () => zoomLevel.value,
102
109
  (zoom) => {
103
- if (editingEvent) {
104
- runOnJS(calculateHeight)(editingEvent.event, zoom);
105
- currentY.value = editingEvent.position.top * zoom;
110
+ if (eventStart && eventEnd) {
111
+ runOnJS(calculateHeight)(eventStart, eventEnd, zoom);
112
+ currentY.value = (eventTop || 0) * zoom;
106
113
  }
107
114
  },
108
- [height, currentY, zoomLevel, editingEvent]
115
+ [height, currentY, zoomLevel, eventStart, eventEnd, eventTop]
109
116
  );
110
117
 
118
+ const hasEditingEvent = !!editingEvent;
111
119
  const styleAnimatedPosition = useAnimatedStyle(() => {
112
- if (!editingEvent) {
120
+ if (!hasEditingEvent) {
113
121
  return { opacity: 0 };
114
122
  }
115
123
 
@@ -121,7 +129,7 @@ const EditEventContainer = memo(
121
129
  opacity: 1,
122
130
  width: "100%",
123
131
  };
124
- }, [editingEvent]);
132
+ }, [hasEditingEvent, height, currentY]);
125
133
 
126
134
  const startY = useSharedValue(0);
127
135
 
@@ -103,16 +103,18 @@ const TimedEventContainerInner = <T extends CalendarEvent>({
103
103
  ).activateAfterLongPress(500)
104
104
  );
105
105
 
106
+ const eventStart = layout.event.start;
107
+ const eventEnd = layout.event.end;
106
108
  useAnimatedReaction(
107
109
  () => zoomLevel.value,
108
110
  (newZoomLevel) => {
109
- const start = new Date(layout.event.start);
110
- const end = new Date(layout.event.end);
111
+ const start = new Date(eventStart);
112
+ const end = new Date(eventEnd);
111
113
  const diff = (end.valueOf() - start.valueOf()) / 60_000;
112
114
 
113
115
  height.value = Math.max(MIN_EVENT_HEIGHT_PX, newZoomLevel * diff);
114
116
  },
115
- [layout.event]
117
+ [eventStart, eventEnd]
116
118
  );
117
119
 
118
120
  useAnimatedReaction(
@@ -128,25 +130,29 @@ const TimedEventContainerInner = <T extends CalendarEvent>({
128
130
  [height, layout.event, renderEvent]
129
131
  );
130
132
 
133
+ const eventId = layout.event.id;
134
+ const layoutPosition = layout.position;
135
+ const isEditingEventId = isEditing?.event.id;
136
+
131
137
  const stylePosition = useAnimatedStyle(() => {
132
138
  const basePosition: any = {
133
139
  position: "absolute",
134
140
  height: height.value,
135
141
  top: top.value,
136
142
  opacity: 1,
137
- width: layout.position.width,
138
- marginLeft: layout.position.marginLeft,
143
+ width: layoutPosition.width,
144
+ marginLeft: layoutPosition.marginLeft,
139
145
 
140
146
  // This is to prevent the event from being clickable while editing
141
- pointerEvents: isEditing ? "none" : "auto",
147
+ pointerEvents: isEditingEventId ? "none" : "auto",
142
148
  };
143
149
 
144
- if (isEditing?.event.id === layout.event.id) {
150
+ if (isEditingEventId === eventId) {
145
151
  basePosition.opacity = 0.5;
146
152
  }
147
153
 
148
154
  return basePosition;
149
- }, [layout, isEditing]);
155
+ }, [layoutPosition, isEditingEventId, eventId]);
150
156
 
151
157
  return (
152
158
  <GestureDetector gesture={gestures}>
@@ -38,7 +38,10 @@ export default function ZoomProvider({
38
38
  maximumHour,
39
39
  onZoomChange,
40
40
  } = useContext(ConfigProvider);
41
- const previewScale = useSharedValue(zoomLevel.get());
41
+ // Always overwritten in onStart before onUpdate reads it, so the initial
42
+ // value here is never actually used — avoids reading zoomLevel.value
43
+ // during render, which Reanimated strict mode warns about.
44
+ const previewScale = useSharedValue(0);
42
45
  const focalTime = useSharedValue(0);
43
46
  const screenFocalY = useSharedValue(0);
44
47
 
@@ -7,6 +7,7 @@ import {
7
7
  daysInRange,
8
8
  startOfUserWeek,
9
9
  computeCalendarDateRange,
10
+ normalizeEventDates,
10
11
  } from "../date-utils";
11
12
  import { CalendarEvent } from "../../types";
12
13
 
@@ -301,6 +302,39 @@ describe("startOfUserWeek", () => {
301
302
  });
302
303
  });
303
304
 
305
+ describe("normalizeEventDates", () => {
306
+ it("should convert Date start/end to ISO strings", () => {
307
+ const evt = {
308
+ ...event("", ""),
309
+ start: new Date("2023-01-01T08:00:00.000Z"),
310
+ end: new Date("2023-01-01T09:00:00.000Z"),
311
+ } as unknown as CalendarEvent;
312
+
313
+ const normalized = normalizeEventDates(evt);
314
+
315
+ expect(normalized.start).toBe("2023-01-01T08:00:00.000Z");
316
+ expect(normalized.end).toBe("2023-01-01T09:00:00.000Z");
317
+ });
318
+
319
+ it("should leave string start/end untouched and return the same reference", () => {
320
+ const evt = event("2023-01-01T08:00:00Z", "2023-01-01T09:00:00Z");
321
+
322
+ expect(normalizeEventDates(evt)).toBe(evt);
323
+ });
324
+
325
+ it("should normalize only the fields that are Date instances", () => {
326
+ const evt = {
327
+ ...event("2023-01-01T08:00:00.000Z", ""),
328
+ end: new Date("2023-01-01T09:00:00.000Z"),
329
+ } as unknown as CalendarEvent;
330
+
331
+ const normalized = normalizeEventDates(evt);
332
+
333
+ expect(normalized.start).toBe("2023-01-01T08:00:00.000Z");
334
+ expect(normalized.end).toBe("2023-01-01T09:00:00.000Z");
335
+ });
336
+ });
337
+
304
338
  describe("computeCalendarDateRange", () => {
305
339
  it("should return single day for 1day view", () => {
306
340
  const result = computeCalendarDateRange("2023-10-10", "UTC", "1day", 0);
@@ -38,6 +38,35 @@ describe("generateEventLayouts", () => {
38
38
  expect(dayLayout.partDayEventsLayout[0].event.id).toBe("2");
39
39
  });
40
40
 
41
+ it("should normalize Date start/end to strings in the resulting layout", () => {
42
+ // Consumers may pass native Date objects even though CalendarEvent types
43
+ // start/end as string; layouts flow into Reanimated worklet closures that
44
+ // can't clone a Date, so generateEventLayouts must normalize them first.
45
+ const events = [
46
+ {
47
+ id: "1",
48
+ calendarId: "primary-calendar",
49
+ title: "Timed Event",
50
+ start: new Date("2023-10-10T08:30:00Z"),
51
+ end: new Date("2023-10-10T09:30:00Z"),
52
+ },
53
+ ] as unknown as CalendarEvent[];
54
+
55
+ const layouts = generateEventLayouts({
56
+ startCalendarDate: "2023-10-10",
57
+ endCalendarDate: "2023-10-10",
58
+ events,
59
+ timezone: "UTC",
60
+ userCalendarId: "primary-calendar",
61
+ });
62
+
63
+ const { event } = layouts["2023-10-10"]!.partDayEventsLayout[0]!;
64
+ expect(typeof event.start).toBe("string");
65
+ expect(typeof event.end).toBe("string");
66
+ expect(event.start).toBe("2023-10-10T08:30:00.000Z");
67
+ expect(event.end).toBe("2023-10-10T09:30:00.000Z");
68
+ });
69
+
41
70
  it("should handle events with overlapping times", () => {
42
71
  const events: CalendarEvent[] = [
43
72
  {
@@ -24,6 +24,24 @@ function toDate(date: Date | string): Date {
24
24
  return typeof date === "string" ? new Date(date) : date;
25
25
  }
26
26
 
27
+ // Event layouts flow into Reanimated worklet closures (useAnimatedReaction,
28
+ // useAnimatedStyle, gesture handlers), which can't clone a `Date` instance
29
+ // across the JS/UI thread boundary ("[Worklets] Cannot copy value of type
30
+ // 'Date'."). Consumers may pass `start`/`end` as `Date` objects even though
31
+ // `CalendarEvent` types them as `string`, so normalize to ISO strings as
32
+ // soon as events enter the layout pipeline.
33
+ export const normalizeEventDates = <T extends CalendarEvent>(event: T): T => {
34
+ if (!isDate(event.start) && !isDate(event.end)) {
35
+ return event;
36
+ }
37
+
38
+ return {
39
+ ...event,
40
+ start: isDate(event.start) ? event.start.toISOString() : event.start,
41
+ end: isDate(event.end) ? event.end.toISOString() : event.end,
42
+ };
43
+ };
44
+
27
45
  export const generatePrefabHours = (
28
46
  timeFormat: string = "HH:mm"
29
47
  ): PrefabHour[] => {
@@ -10,6 +10,7 @@ import {
10
10
  getDuration,
11
11
  getDurationInDays,
12
12
  isAllDayOrSpansMidnight,
13
+ normalizeEventDates,
13
14
  startOfUserWeek,
14
15
  } from "../utils/date-utils";
15
16
  import computePositioning from "../utils/compute-positioning";
@@ -42,6 +43,10 @@ export const generateEventLayouts = <T extends CalendarEvent>({
42
43
  startDayOfWeekOffset = 0,
43
44
  timezone,
44
45
  }: GenerateEventLayouts<T>) => {
46
+ // Normalize start/end to strings before events flow into any layout object,
47
+ // since layouts get closed over by Reanimated worklets that can't clone Dates.
48
+ events = events.map(normalizeEventDates);
49
+
45
50
  // Calculate spacial layout for CalendarViewDay events
46
51
  // in month view, midnight-spanning part-day events are rendered as all-day multi-day
47
52
  const [allDayEvents, partDayEvents] = _partition([...events], (event) =>
@@ -16,16 +16,17 @@ const gesturePan = <T extends CalendarEvent>(
16
16
  startEditing?: () => void,
17
17
  isDragging?: SharedValue<boolean>,
18
18
  autoScrollOffset?: SharedValue<number>
19
- ) =>
20
- Gesture.Pan()
19
+ ) => {
20
+ const hasIsEditing = !!isEditing;
21
+ return Gesture.Pan()
21
22
  .blocksExternalGesture(refNewEvent)
22
23
  .onStart(() => {
23
24
  if (startEditing) {
24
- if (isEditing) {
25
+ if (hasIsEditing) {
25
26
  return;
26
27
  }
27
28
 
28
- if (!isEditing) {
29
+ if (!hasIsEditing) {
29
30
  runOnJS(startEditing)();
30
31
  }
31
32
  }
@@ -69,5 +70,6 @@ const gesturePan = <T extends CalendarEvent>(
69
70
  .onFinalize(() => {
70
71
  if (isDragging) isDragging.value = false;
71
72
  });
73
+ };
72
74
 
73
75
  export default gesturePan;