@sunsama/event-calendar 0.13.2 → 0.13.5

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 (86) hide show
  1. package/lib/commonjs/components/all-day-events.js +1 -1
  2. package/lib/commonjs/components/all-day-events.js.map +1 -1
  3. package/lib/commonjs/components/edit-event-container.js +9 -1
  4. package/lib/commonjs/components/edit-event-container.js.map +1 -1
  5. package/lib/commonjs/components/event-container.js +2 -1
  6. package/lib/commonjs/components/event-container.js.map +1 -1
  7. package/lib/commonjs/components/timed-event-container.js +1 -1
  8. package/lib/commonjs/components/timed-event-container.js.map +1 -1
  9. package/lib/commonjs/hooks/use-auto-scroll.js +11 -1
  10. package/lib/commonjs/hooks/use-auto-scroll.js.map +1 -1
  11. package/lib/commonjs/hooks/use-events.js +7 -6
  12. package/lib/commonjs/hooks/use-events.js.map +1 -1
  13. package/lib/commonjs/index.js +33 -32
  14. package/lib/commonjs/index.js.map +1 -1
  15. package/lib/commonjs/utils/calendar-layout.js +15 -2
  16. package/lib/commonjs/utils/calendar-layout.js.map +1 -1
  17. package/lib/commonjs/utils/compute-positioning.js +1 -1
  18. package/lib/commonjs/utils/compute-positioning.js.map +1 -1
  19. package/lib/commonjs/utils/date-utils.js +59 -12
  20. package/lib/commonjs/utils/date-utils.js.map +1 -1
  21. package/lib/commonjs/utils/generate-event-layouts.js +13 -6
  22. package/lib/commonjs/utils/generate-event-layouts.js.map +1 -1
  23. package/lib/module/components/all-day-events.js +2 -2
  24. package/lib/module/components/all-day-events.js.map +1 -1
  25. package/lib/module/components/edit-event-container.js +9 -1
  26. package/lib/module/components/edit-event-container.js.map +1 -1
  27. package/lib/module/components/event-container.js +3 -2
  28. package/lib/module/components/event-container.js.map +1 -1
  29. package/lib/module/components/timed-event-container.js +1 -1
  30. package/lib/module/components/timed-event-container.js.map +1 -1
  31. package/lib/module/hooks/use-auto-scroll.js +11 -1
  32. package/lib/module/hooks/use-auto-scroll.js.map +1 -1
  33. package/lib/module/hooks/use-events.js +8 -7
  34. package/lib/module/hooks/use-events.js.map +1 -1
  35. package/lib/module/index.js +33 -32
  36. package/lib/module/index.js.map +1 -1
  37. package/lib/module/utils/calendar-layout.js +15 -2
  38. package/lib/module/utils/calendar-layout.js.map +1 -1
  39. package/lib/module/utils/compute-positioning.js +1 -1
  40. package/lib/module/utils/compute-positioning.js.map +1 -1
  41. package/lib/module/utils/date-utils.js +57 -11
  42. package/lib/module/utils/date-utils.js.map +1 -1
  43. package/lib/module/utils/generate-event-layouts.js +14 -7
  44. package/lib/module/utils/generate-event-layouts.js.map +1 -1
  45. package/lib/typescript/commonjs/components/all-day-events.d.ts.map +1 -1
  46. package/lib/typescript/commonjs/components/edit-event-container.d.ts.map +1 -1
  47. package/lib/typescript/commonjs/components/event-container.d.ts +2 -1
  48. package/lib/typescript/commonjs/components/event-container.d.ts.map +1 -1
  49. package/lib/typescript/commonjs/components/timed-event-container.d.ts.map +1 -1
  50. package/lib/typescript/commonjs/hooks/use-auto-scroll.d.ts.map +1 -1
  51. package/lib/typescript/commonjs/hooks/use-events.d.ts.map +1 -1
  52. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  53. package/lib/typescript/commonjs/utils/calendar-layout.d.ts +3 -1
  54. package/lib/typescript/commonjs/utils/calendar-layout.d.ts.map +1 -1
  55. package/lib/typescript/commonjs/utils/compute-positioning.d.ts.map +1 -1
  56. package/lib/typescript/commonjs/utils/date-utils.d.ts +1 -0
  57. package/lib/typescript/commonjs/utils/date-utils.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/utils/generate-event-layouts.d.ts.map +1 -1
  59. package/lib/typescript/module/components/all-day-events.d.ts.map +1 -1
  60. package/lib/typescript/module/components/edit-event-container.d.ts.map +1 -1
  61. package/lib/typescript/module/components/event-container.d.ts +2 -1
  62. package/lib/typescript/module/components/event-container.d.ts.map +1 -1
  63. package/lib/typescript/module/components/timed-event-container.d.ts.map +1 -1
  64. package/lib/typescript/module/hooks/use-auto-scroll.d.ts.map +1 -1
  65. package/lib/typescript/module/hooks/use-events.d.ts.map +1 -1
  66. package/lib/typescript/module/index.d.ts.map +1 -1
  67. package/lib/typescript/module/utils/calendar-layout.d.ts +3 -1
  68. package/lib/typescript/module/utils/calendar-layout.d.ts.map +1 -1
  69. package/lib/typescript/module/utils/compute-positioning.d.ts.map +1 -1
  70. package/lib/typescript/module/utils/date-utils.d.ts +1 -0
  71. package/lib/typescript/module/utils/date-utils.d.ts.map +1 -1
  72. package/lib/typescript/module/utils/generate-event-layouts.d.ts.map +1 -1
  73. package/package.json +1 -1
  74. package/src/components/all-day-events.tsx +8 -4
  75. package/src/components/edit-event-container.tsx +11 -1
  76. package/src/components/event-container.tsx +14 -5
  77. package/src/components/timed-event-container.tsx +3 -0
  78. package/src/hooks/use-auto-scroll.ts +11 -1
  79. package/src/hooks/use-events.tsx +12 -8
  80. package/src/index.tsx +68 -34
  81. package/src/utils/__tests___/date-utils-extended.test.ts +34 -0
  82. package/src/utils/__tests___/generate-event-layout.test.ts +29 -0
  83. package/src/utils/calendar-layout.ts +15 -4
  84. package/src/utils/compute-positioning.ts +4 -3
  85. package/src/utils/date-utils.ts +70 -26
  86. package/src/utils/generate-event-layouts.ts +17 -19
@@ -2,6 +2,46 @@ import moment, { type Moment } from "moment-timezone";
2
2
  import { isDate, range, size } from "lodash";
3
3
  import { CalendarEvent, PrefabHour } from "../types";
4
4
 
5
+ // Cached Intl.DateTimeFormat instances for fast timezone-aware day/hour extraction.
6
+ // Avoids creating Moment instances in hot paths like isAllDayOrSpansMidnight.
7
+ let _cachedTz = "";
8
+ let _dayFormatter: Intl.DateTimeFormat;
9
+ let _hourFormatter: Intl.DateTimeFormat;
10
+
11
+ function ensureFormatters(timezone: string) {
12
+ if (timezone !== _cachedTz) {
13
+ _cachedTz = timezone;
14
+ _dayFormatter = new Intl.DateTimeFormat("en-CA", { timeZone: timezone });
15
+ _hourFormatter = new Intl.DateTimeFormat("en-GB", {
16
+ hour: "2-digit",
17
+ hour12: false,
18
+ timeZone: timezone,
19
+ });
20
+ }
21
+ }
22
+
23
+ function toDate(date: Date | string): Date {
24
+ return typeof date === "string" ? new Date(date) : date;
25
+ }
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
+
5
45
  export const generatePrefabHours = (
6
46
  timeFormat: string = "HH:mm"
7
47
  ): PrefabHour[] => {
@@ -62,15 +102,19 @@ export const isAllDayOrSpansMidnight = <T extends CalendarEvent>(
62
102
  return true;
63
103
  }
64
104
 
65
- // Does the range start/end span midnight in the given timezone?
66
- const startMoment = moment.tz(start, timezone);
67
- const endMoment = moment.tz(end, timezone);
105
+ ensureFormatters(timezone);
68
106
 
69
- // Handle special case where range ends at midnight exactly, in which case spansMidnight should return false
70
- return !startMoment.isSame(
71
- endMoment.hour() === 0 ? endMoment.subtract(1, "minute") : endMoment,
72
- "day"
73
- );
107
+ const endDateObj = toDate(end);
108
+
109
+ // If end hour is 0 in user's timezone, subtract 1 minute so events
110
+ // ending exactly at midnight aren't considered as spanning midnight
111
+ const endHour = parseInt(_hourFormatter.format(endDateObj), 10);
112
+ const effectiveEnd =
113
+ endHour === 0 ? new Date(endDateObj.getTime() - 60_000) : endDateObj;
114
+
115
+ const startDay = _dayFormatter.format(toDate(start));
116
+ const endDay = _dayFormatter.format(effectiveEnd);
117
+ return startDay !== endDay;
74
118
  };
75
119
 
76
120
  // Returns the count of unique dates in the provided timezone
@@ -79,17 +123,20 @@ export const getDurationInDays = <T extends CalendarEvent>(
79
123
  timezone: string
80
124
  ) => {
81
125
  // the event duration in days calculation depends on if the event is all day
82
- return calendarEvent.isAllDay
83
- ? moment
126
+ if (calendarEvent.isAllDay) {
127
+ return (
128
+ moment
84
129
  .tz(calendarEvent.end, timezone)
85
130
  .diff(moment.tz(calendarEvent.start, timezone), "days") + 1
86
- : size(
87
- daysInRange({
88
- startDate: calendarEvent.start,
89
- endDate: calendarEvent.end,
90
- timezone,
91
- })
92
- );
131
+ );
132
+ }
133
+ return size(
134
+ daysInRange({
135
+ startDate: calendarEvent.start,
136
+ endDate: calendarEvent.end,
137
+ timezone,
138
+ })
139
+ );
93
140
  };
94
141
 
95
142
  // Returns an array of days (e.g. ['2022-01-02']) in a given date range.
@@ -102,23 +149,20 @@ export const daysInRange = ({
102
149
  endDate: Date | string;
103
150
  timezone: string;
104
151
  }) => {
105
- const countOfDaysInRange = moment
106
- .tz(endDate, timezone)
107
- .diff(moment.tz(startDate, timezone), "days");
108
- const startDay = moment.tz(startDate, timezone).format("YYYY-MM-DD");
152
+ const startMoment = moment.tz(startDate, timezone);
153
+ const endMoment = moment.tz(endDate, timezone);
154
+ const countOfDaysInRange = endMoment.diff(startMoment, "days");
109
155
  const days = [];
156
+ const maxIterations = Math.min(30, Math.abs(countOfDaysInRange));
110
157
  // Make sure we loop at a max of 30 times here as we had events that were scheduled for all day long for
111
158
  // 1000 years in the future and this was causing the app to crash
112
159
  for (
113
160
  let countOfDaysAfterStart = 0;
114
- countOfDaysAfterStart <= Math.min(30, Math.abs(countOfDaysInRange));
161
+ countOfDaysAfterStart <= maxIterations;
115
162
  countOfDaysAfterStart++
116
163
  ) {
117
164
  days.push(
118
- moment
119
- .tz(startDay, timezone)
120
- .add(countOfDaysAfterStart, "day")
121
- .format("YYYY-MM-DD")
165
+ startMoment.clone().add(countOfDaysAfterStart, "day").format("YYYY-MM-DD")
122
166
  );
123
167
  }
124
168
  return days;
@@ -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) =>
@@ -76,16 +81,13 @@ export const generateEventLayouts = <T extends CalendarEvent>({
76
81
  ...(calendarViewInterval === "month" ? partDayEventsSorted : []),
77
82
  ];
78
83
 
79
- const visibleDayCount = moment
80
- .tz(endCalendarDate, timezone)
81
- .diff(moment.tz(startCalendarDate, timezone), "days");
84
+ const startCalendarMoment = moment.tz(startCalendarDate, timezone);
85
+ const endCalendarMoment = moment.tz(endCalendarDate, timezone);
86
+ const visibleDayCount = endCalendarMoment.diff(startCalendarMoment, "days");
82
87
  const visibleDays: string[] = [];
83
88
  for (let dayCount = 0; dayCount <= visibleDayCount; dayCount++) {
84
89
  visibleDays.push(
85
- moment
86
- .tz(startCalendarDate, timezone)
87
- .add(dayCount, "day")
88
- .format("YYYY-MM-DD")
90
+ startCalendarMoment.clone().add(dayCount, "day").format("YYYY-MM-DD")
89
91
  );
90
92
  }
91
93
 
@@ -103,15 +105,14 @@ export const generateEventLayouts = <T extends CalendarEvent>({
103
105
  } = {};
104
106
 
105
107
  calendarViewDateRanges.forEach((date) => {
108
+ const basisMoment = moment.tz(date.basisDate, timezone);
106
109
  const calendarLayoutOptions = {
107
110
  visibleX: date.dayIndexes,
108
111
  enableWeekBreaks: calendarViewInterval === "month",
109
- startOfWeekXOffset: moment
110
- .tz(date.basisDate, timezone)
111
- .diff(
112
- startOfUserWeek(startDayOfWeekOffset, date.basisDate!, timezone),
113
- "days"
114
- ),
112
+ startOfWeekXOffset: basisMoment.diff(
113
+ startOfUserWeek(startDayOfWeekOffset, date.basisDate!, timezone),
114
+ "days"
115
+ ),
115
116
  };
116
117
  const calendarViewDayLayout = _reduce(
117
118
  calendarViewDayEvents,
@@ -119,7 +120,7 @@ export const generateEventLayouts = <T extends CalendarEvent>({
119
120
  const eventStartIndex = moment
120
121
  .tz(event.start, timezone)
121
122
  .startOf("day")
122
- .diff(moment.tz(date.basisDate, timezone), "days");
123
+ .diff(basisMoment, "days");
123
124
  const eventDurationDays = getDurationInDays(event, timezone);
124
125
 
125
126
  calendarLayout.findFitAndInsert(
@@ -139,16 +140,13 @@ export const generateEventLayouts = <T extends CalendarEvent>({
139
140
  calendarDates.forEach((calendarDate) => {
140
141
  const currentDayDate = moment.tz(calendarDate, timezone).startOf("day");
141
142
  const startDate = currentDayDate.toDate();
142
- const endDate = moment.tz(startDate, timezone).endOf("day").toDate();
143
+ const endDate = currentDayDate.clone().endOf("day").toDate();
143
144
  const showHours = !["month"].includes(calendarViewInterval);
144
145
  const dayId = calendarDate;
145
146
  const allDayEventsLayout: AllDayEventLayoutType<T>[] = [];
146
147
  let partDayEventsLayout: PartDayEventLayoutType<T>[] = [];
147
148
 
148
- const x = moment(startDate).diff(
149
- moment.tz(date.basisDate, timezone),
150
- "days"
151
- );
149
+ const x = currentDayDate.diff(basisMoment, "days");
152
150
  for (let y = 0; y < calendarViewDayRowHeight; ++y) {
153
151
  const {
154
152
  event,