@sunsama/event-calendar 0.2.0

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 (236) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +105 -0
  3. package/lib/commonjs/components/all-day-events.js +117 -0
  4. package/lib/commonjs/components/all-day-events.js.map +1 -0
  5. package/lib/commonjs/components/background-hours-content.js +43 -0
  6. package/lib/commonjs/components/background-hours-content.js.map +1 -0
  7. package/lib/commonjs/components/background-hours-layout.js +57 -0
  8. package/lib/commonjs/components/background-hours-layout.js.map +1 -0
  9. package/lib/commonjs/components/drag-bar.js +84 -0
  10. package/lib/commonjs/components/drag-bar.js.map +1 -0
  11. package/lib/commonjs/components/edit-event-container.js +114 -0
  12. package/lib/commonjs/components/edit-event-container.js.map +1 -0
  13. package/lib/commonjs/components/event-container.js +37 -0
  14. package/lib/commonjs/components/event-container.js.map +1 -0
  15. package/lib/commonjs/components/new-event-container.js +73 -0
  16. package/lib/commonjs/components/new-event-container.js.map +1 -0
  17. package/lib/commonjs/components/time-indicator.js +64 -0
  18. package/lib/commonjs/components/time-indicator.js.map +1 -0
  19. package/lib/commonjs/components/timed-event-container.js +91 -0
  20. package/lib/commonjs/components/timed-event-container.js.map +1 -0
  21. package/lib/commonjs/components/timed-events.js +68 -0
  22. package/lib/commonjs/components/timed-events.js.map +1 -0
  23. package/lib/commonjs/components/zoom-provider.js +109 -0
  24. package/lib/commonjs/components/zoom-provider.js.map +1 -0
  25. package/lib/commonjs/enums.js +2 -0
  26. package/lib/commonjs/enums.js.map +1 -0
  27. package/lib/commonjs/hooks/use-cloned-events.js +25 -0
  28. package/lib/commonjs/hooks/use-cloned-events.js.map +1 -0
  29. package/lib/commonjs/hooks/use-events-layout.js +34 -0
  30. package/lib/commonjs/hooks/use-events-layout.js.map +1 -0
  31. package/lib/commonjs/hooks/use-is-editing.js +83 -0
  32. package/lib/commonjs/hooks/use-is-editing.js.map +1 -0
  33. package/lib/commonjs/index.js +129 -0
  34. package/lib/commonjs/index.js.map +1 -0
  35. package/lib/commonjs/types.js +24 -0
  36. package/lib/commonjs/types.js.map +1 -0
  37. package/lib/commonjs/utils/calendar-layout.js +113 -0
  38. package/lib/commonjs/utils/calendar-layout.js.map +1 -0
  39. package/lib/commonjs/utils/compute-positioning.js +33 -0
  40. package/lib/commonjs/utils/compute-positioning.js.map +1 -0
  41. package/lib/commonjs/utils/date-utils.js +152 -0
  42. package/lib/commonjs/utils/date-utils.js.map +1 -0
  43. package/lib/commonjs/utils/double-tap-reset-zoom-gesture.js +19 -0
  44. package/lib/commonjs/utils/double-tap-reset-zoom-gesture.js.map +1 -0
  45. package/lib/commonjs/utils/generate-event-layouts.js +198 -0
  46. package/lib/commonjs/utils/generate-event-layouts.js.map +1 -0
  47. package/lib/commonjs/utils/globals.js +11 -0
  48. package/lib/commonjs/utils/globals.js.map +1 -0
  49. package/lib/commonjs/utils/pan-edit-event-gesture.js +41 -0
  50. package/lib/commonjs/utils/pan-edit-event-gesture.js.map +1 -0
  51. package/lib/module/components/all-day-events.js +110 -0
  52. package/lib/module/components/all-day-events.js.map +1 -0
  53. package/lib/module/components/background-hours-content.js +37 -0
  54. package/lib/module/components/background-hours-content.js.map +1 -0
  55. package/lib/module/components/background-hours-layout.js +51 -0
  56. package/lib/module/components/background-hours-layout.js.map +1 -0
  57. package/lib/module/components/drag-bar.js +78 -0
  58. package/lib/module/components/drag-bar.js.map +1 -0
  59. package/lib/module/components/edit-event-container.js +107 -0
  60. package/lib/module/components/edit-event-container.js.map +1 -0
  61. package/lib/module/components/event-container.js +33 -0
  62. package/lib/module/components/event-container.js.map +1 -0
  63. package/lib/module/components/new-event-container.js +67 -0
  64. package/lib/module/components/new-event-container.js.map +1 -0
  65. package/lib/module/components/time-indicator.js +57 -0
  66. package/lib/module/components/time-indicator.js.map +1 -0
  67. package/lib/module/components/timed-event-container.js +84 -0
  68. package/lib/module/components/timed-event-container.js.map +1 -0
  69. package/lib/module/components/timed-events.js +63 -0
  70. package/lib/module/components/timed-events.js.map +1 -0
  71. package/lib/module/components/zoom-provider.js +102 -0
  72. package/lib/module/components/zoom-provider.js.map +1 -0
  73. package/lib/module/enums.js +2 -0
  74. package/lib/module/enums.js.map +1 -0
  75. package/lib/module/hooks/use-cloned-events.js +21 -0
  76. package/lib/module/hooks/use-cloned-events.js.map +1 -0
  77. package/lib/module/hooks/use-events-layout.js +29 -0
  78. package/lib/module/hooks/use-events-layout.js.map +1 -0
  79. package/lib/module/hooks/use-is-editing.js +75 -0
  80. package/lib/module/hooks/use-is-editing.js.map +1 -0
  81. package/lib/module/index.js +124 -0
  82. package/lib/module/index.js.map +1 -0
  83. package/lib/module/types.js +20 -0
  84. package/lib/module/types.js.map +1 -0
  85. package/lib/module/utils/calendar-layout.js +108 -0
  86. package/lib/module/utils/calendar-layout.js.map +1 -0
  87. package/lib/module/utils/compute-positioning.js +28 -0
  88. package/lib/module/utils/compute-positioning.js.map +1 -0
  89. package/lib/module/utils/date-utils.js +139 -0
  90. package/lib/module/utils/date-utils.js.map +1 -0
  91. package/lib/module/utils/double-tap-reset-zoom-gesture.js +15 -0
  92. package/lib/module/utils/double-tap-reset-zoom-gesture.js.map +1 -0
  93. package/lib/module/utils/generate-event-layouts.js +192 -0
  94. package/lib/module/utils/generate-event-layouts.js.map +1 -0
  95. package/lib/module/utils/globals.js +7 -0
  96. package/lib/module/utils/globals.js.map +1 -0
  97. package/lib/module/utils/pan-edit-event-gesture.js +37 -0
  98. package/lib/module/utils/pan-edit-event-gesture.js.map +1 -0
  99. package/lib/typescript/commonjs/components/all-day-events.d.ts +3 -0
  100. package/lib/typescript/commonjs/components/all-day-events.d.ts.map +1 -0
  101. package/lib/typescript/commonjs/components/background-hours-content.d.ts +7 -0
  102. package/lib/typescript/commonjs/components/background-hours-content.d.ts.map +1 -0
  103. package/lib/typescript/commonjs/components/background-hours-layout.d.ts +7 -0
  104. package/lib/typescript/commonjs/components/background-hours-layout.d.ts.map +1 -0
  105. package/lib/typescript/commonjs/components/drag-bar.d.ts +14 -0
  106. package/lib/typescript/commonjs/components/drag-bar.d.ts.map +1 -0
  107. package/lib/typescript/commonjs/components/edit-event-container.d.ts +7 -0
  108. package/lib/typescript/commonjs/components/edit-event-container.d.ts.map +1 -0
  109. package/lib/typescript/commonjs/components/event-container.d.ts +7 -0
  110. package/lib/typescript/commonjs/components/event-container.d.ts.map +1 -0
  111. package/lib/typescript/commonjs/components/new-event-container.d.ts +3 -0
  112. package/lib/typescript/commonjs/components/new-event-container.d.ts.map +1 -0
  113. package/lib/typescript/commonjs/components/time-indicator.d.ts +3 -0
  114. package/lib/typescript/commonjs/components/time-indicator.d.ts.map +1 -0
  115. package/lib/typescript/commonjs/components/timed-event-container.d.ts +9 -0
  116. package/lib/typescript/commonjs/components/timed-event-container.d.ts.map +1 -0
  117. package/lib/typescript/commonjs/components/timed-events.d.ts +7 -0
  118. package/lib/typescript/commonjs/components/timed-events.d.ts.map +1 -0
  119. package/lib/typescript/commonjs/components/zoom-provider.d.ts +7 -0
  120. package/lib/typescript/commonjs/components/zoom-provider.d.ts.map +1 -0
  121. package/lib/typescript/commonjs/enums.d.ts +2 -0
  122. package/lib/typescript/commonjs/enums.d.ts.map +1 -0
  123. package/lib/typescript/commonjs/hooks/use-cloned-events.d.ts +11 -0
  124. package/lib/typescript/commonjs/hooks/use-cloned-events.d.ts.map +1 -0
  125. package/lib/typescript/commonjs/hooks/use-events-layout.d.ts +13 -0
  126. package/lib/typescript/commonjs/hooks/use-events-layout.d.ts.map +1 -0
  127. package/lib/typescript/commonjs/hooks/use-is-editing.d.ts +17 -0
  128. package/lib/typescript/commonjs/hooks/use-is-editing.d.ts.map +1 -0
  129. package/lib/typescript/commonjs/index.d.ts +27 -0
  130. package/lib/typescript/commonjs/index.d.ts.map +1 -0
  131. package/lib/typescript/commonjs/package.json +1 -0
  132. package/lib/typescript/commonjs/types.d.ts +128 -0
  133. package/lib/typescript/commonjs/types.d.ts.map +1 -0
  134. package/lib/typescript/commonjs/utils/__tests___/compute-positioning.test.d.ts +2 -0
  135. package/lib/typescript/commonjs/utils/__tests___/compute-positioning.test.d.ts.map +1 -0
  136. package/lib/typescript/commonjs/utils/__tests___/date-utils.test.d.ts +2 -0
  137. package/lib/typescript/commonjs/utils/__tests___/date-utils.test.d.ts.map +1 -0
  138. package/lib/typescript/commonjs/utils/__tests___/generate-event-layout.test.d.ts +2 -0
  139. package/lib/typescript/commonjs/utils/__tests___/generate-event-layout.test.d.ts.map +1 -0
  140. package/lib/typescript/commonjs/utils/calendar-layout.d.ts +36 -0
  141. package/lib/typescript/commonjs/utils/calendar-layout.d.ts.map +1 -0
  142. package/lib/typescript/commonjs/utils/compute-positioning.d.ts +10 -0
  143. package/lib/typescript/commonjs/utils/compute-positioning.d.ts.map +1 -0
  144. package/lib/typescript/commonjs/utils/date-utils.d.ts +30 -0
  145. package/lib/typescript/commonjs/utils/date-utils.d.ts.map +1 -0
  146. package/lib/typescript/commonjs/utils/double-tap-reset-zoom-gesture.d.ts +5 -0
  147. package/lib/typescript/commonjs/utils/double-tap-reset-zoom-gesture.d.ts.map +1 -0
  148. package/lib/typescript/commonjs/utils/generate-event-layouts.d.ts +15 -0
  149. package/lib/typescript/commonjs/utils/generate-event-layouts.d.ts.map +1 -0
  150. package/lib/typescript/commonjs/utils/globals.d.ts +5 -0
  151. package/lib/typescript/commonjs/utils/globals.d.ts.map +1 -0
  152. package/lib/typescript/commonjs/utils/pan-edit-event-gesture.d.ts +6 -0
  153. package/lib/typescript/commonjs/utils/pan-edit-event-gesture.d.ts.map +1 -0
  154. package/lib/typescript/module/components/all-day-events.d.ts +3 -0
  155. package/lib/typescript/module/components/all-day-events.d.ts.map +1 -0
  156. package/lib/typescript/module/components/background-hours-content.d.ts +7 -0
  157. package/lib/typescript/module/components/background-hours-content.d.ts.map +1 -0
  158. package/lib/typescript/module/components/background-hours-layout.d.ts +7 -0
  159. package/lib/typescript/module/components/background-hours-layout.d.ts.map +1 -0
  160. package/lib/typescript/module/components/drag-bar.d.ts +14 -0
  161. package/lib/typescript/module/components/drag-bar.d.ts.map +1 -0
  162. package/lib/typescript/module/components/edit-event-container.d.ts +7 -0
  163. package/lib/typescript/module/components/edit-event-container.d.ts.map +1 -0
  164. package/lib/typescript/module/components/event-container.d.ts +7 -0
  165. package/lib/typescript/module/components/event-container.d.ts.map +1 -0
  166. package/lib/typescript/module/components/new-event-container.d.ts +3 -0
  167. package/lib/typescript/module/components/new-event-container.d.ts.map +1 -0
  168. package/lib/typescript/module/components/time-indicator.d.ts +3 -0
  169. package/lib/typescript/module/components/time-indicator.d.ts.map +1 -0
  170. package/lib/typescript/module/components/timed-event-container.d.ts +9 -0
  171. package/lib/typescript/module/components/timed-event-container.d.ts.map +1 -0
  172. package/lib/typescript/module/components/timed-events.d.ts +7 -0
  173. package/lib/typescript/module/components/timed-events.d.ts.map +1 -0
  174. package/lib/typescript/module/components/zoom-provider.d.ts +7 -0
  175. package/lib/typescript/module/components/zoom-provider.d.ts.map +1 -0
  176. package/lib/typescript/module/enums.d.ts +2 -0
  177. package/lib/typescript/module/enums.d.ts.map +1 -0
  178. package/lib/typescript/module/hooks/use-cloned-events.d.ts +11 -0
  179. package/lib/typescript/module/hooks/use-cloned-events.d.ts.map +1 -0
  180. package/lib/typescript/module/hooks/use-events-layout.d.ts +13 -0
  181. package/lib/typescript/module/hooks/use-events-layout.d.ts.map +1 -0
  182. package/lib/typescript/module/hooks/use-is-editing.d.ts +17 -0
  183. package/lib/typescript/module/hooks/use-is-editing.d.ts.map +1 -0
  184. package/lib/typescript/module/index.d.ts +27 -0
  185. package/lib/typescript/module/index.d.ts.map +1 -0
  186. package/lib/typescript/module/package.json +1 -0
  187. package/lib/typescript/module/types.d.ts +128 -0
  188. package/lib/typescript/module/types.d.ts.map +1 -0
  189. package/lib/typescript/module/utils/__tests___/compute-positioning.test.d.ts +2 -0
  190. package/lib/typescript/module/utils/__tests___/compute-positioning.test.d.ts.map +1 -0
  191. package/lib/typescript/module/utils/__tests___/date-utils.test.d.ts +2 -0
  192. package/lib/typescript/module/utils/__tests___/date-utils.test.d.ts.map +1 -0
  193. package/lib/typescript/module/utils/__tests___/generate-event-layout.test.d.ts +2 -0
  194. package/lib/typescript/module/utils/__tests___/generate-event-layout.test.d.ts.map +1 -0
  195. package/lib/typescript/module/utils/calendar-layout.d.ts +36 -0
  196. package/lib/typescript/module/utils/calendar-layout.d.ts.map +1 -0
  197. package/lib/typescript/module/utils/compute-positioning.d.ts +10 -0
  198. package/lib/typescript/module/utils/compute-positioning.d.ts.map +1 -0
  199. package/lib/typescript/module/utils/date-utils.d.ts +30 -0
  200. package/lib/typescript/module/utils/date-utils.d.ts.map +1 -0
  201. package/lib/typescript/module/utils/double-tap-reset-zoom-gesture.d.ts +5 -0
  202. package/lib/typescript/module/utils/double-tap-reset-zoom-gesture.d.ts.map +1 -0
  203. package/lib/typescript/module/utils/generate-event-layouts.d.ts +15 -0
  204. package/lib/typescript/module/utils/generate-event-layouts.d.ts.map +1 -0
  205. package/lib/typescript/module/utils/globals.d.ts +5 -0
  206. package/lib/typescript/module/utils/globals.d.ts.map +1 -0
  207. package/lib/typescript/module/utils/pan-edit-event-gesture.d.ts +6 -0
  208. package/lib/typescript/module/utils/pan-edit-event-gesture.d.ts.map +1 -0
  209. package/package.json +195 -0
  210. package/src/components/all-day-events.tsx +134 -0
  211. package/src/components/background-hours-content.tsx +51 -0
  212. package/src/components/background-hours-layout.tsx +61 -0
  213. package/src/components/drag-bar.tsx +120 -0
  214. package/src/components/edit-event-container.tsx +158 -0
  215. package/src/components/event-container.tsx +44 -0
  216. package/src/components/new-event-container.tsx +90 -0
  217. package/src/components/time-indicator.tsx +72 -0
  218. package/src/components/timed-event-container.tsx +124 -0
  219. package/src/components/timed-events.tsx +72 -0
  220. package/src/components/zoom-provider.tsx +146 -0
  221. package/src/enums.ts +0 -0
  222. package/src/hooks/use-cloned-events.ts +26 -0
  223. package/src/hooks/use-events-layout.ts +55 -0
  224. package/src/hooks/use-is-editing.tsx +109 -0
  225. package/src/index.tsx +165 -0
  226. package/src/types.ts +163 -0
  227. package/src/utils/__tests___/compute-positioning.test.ts +255 -0
  228. package/src/utils/__tests___/date-utils.test.ts +41 -0
  229. package/src/utils/__tests___/generate-event-layout.test.ts +277 -0
  230. package/src/utils/calendar-layout.ts +139 -0
  231. package/src/utils/compute-positioning.ts +44 -0
  232. package/src/utils/date-utils.ts +238 -0
  233. package/src/utils/double-tap-reset-zoom-gesture.ts +23 -0
  234. package/src/utils/generate-event-layouts.ts +314 -0
  235. package/src/utils/globals.ts +8 -0
  236. package/src/utils/pan-edit-event-gesture.ts +64 -0
@@ -0,0 +1,277 @@
1
+ import generateEventLayouts from "../generate-event-layouts";
2
+ import { CalendarEvent, EventExtend } from "src/types";
3
+
4
+ describe("generateEventLayouts", () => {
5
+ it("should separate all-day events and timed events", () => {
6
+ const events: CalendarEvent[] = [
7
+ {
8
+ id: "1",
9
+ calendarId: "primary-calendar",
10
+ title: "All-Day Event",
11
+ start: "2023-10-10T00:00:00Z",
12
+ end: "2023-10-11T02:00:00Z",
13
+ isAllDay: true,
14
+ },
15
+ {
16
+ id: "2",
17
+ calendarId: "primary-calendar",
18
+ title: "Timed Event",
19
+ start: "2023-10-10T08:30:00Z",
20
+ end: "2023-10-10T09:30:00Z",
21
+ },
22
+ ];
23
+
24
+ const layouts = generateEventLayouts({
25
+ startCalendarDate: "2023-10-10",
26
+ endCalendarDate: "2023-10-10",
27
+ events,
28
+ timezone: "UTC",
29
+ userCalendarId: "primary-calendar",
30
+ });
31
+
32
+ const dayLayout = layouts["2023-10-10"];
33
+
34
+ expect(dayLayout).toBeDefined();
35
+ expect(dayLayout.allDayEventsLayout.length).toBe(1);
36
+ expect(dayLayout.partDayEventsLayout.length).toBe(1);
37
+ expect(dayLayout.allDayEventsLayout[0].event.id).toBe("1");
38
+ expect(dayLayout.partDayEventsLayout[0].event.id).toBe("2");
39
+ });
40
+
41
+ it("should handle events with overlapping times", () => {
42
+ const events: CalendarEvent[] = [
43
+ {
44
+ id: "1",
45
+ calendarId: "primary-calendar",
46
+ title: "Event 1",
47
+ start: "2023-10-10T08:00:00Z",
48
+ end: "2023-10-10T09:00:00Z",
49
+ },
50
+ {
51
+ id: "2",
52
+ calendarId: "primary-calendar",
53
+ title: "Event 2",
54
+ start: "2023-10-10T08:30:00Z",
55
+ end: "2023-10-10T09:30:00Z",
56
+ },
57
+ ];
58
+
59
+ const layouts = generateEventLayouts({
60
+ startCalendarDate: "2023-10-10",
61
+ endCalendarDate: "2023-10-10",
62
+ events,
63
+ timezone: "UTC",
64
+ userCalendarId: "primary-calendar",
65
+ });
66
+
67
+ const dayLayout = layouts["2023-10-10"];
68
+ expect(dayLayout).toBeDefined();
69
+ const { partDayEventsLayout } = dayLayout;
70
+ expect(partDayEventsLayout.length).toBe(2);
71
+ // Overlapping events should have collisions.total = 2
72
+ expect(partDayEventsLayout[0].collisions?.total).toBe(2);
73
+ expect(partDayEventsLayout[1].collisions?.total).toBe(2);
74
+ });
75
+
76
+ it("should handle events with no overlap", () => {
77
+ const events: CalendarEvent[] = [
78
+ {
79
+ id: "1",
80
+ calendarId: "primary-calendar",
81
+ title: "Event 1",
82
+ start: "2023-10-10T08:00:00Z",
83
+ end: "2023-10-10T09:00:00Z",
84
+ },
85
+ {
86
+ id: "2",
87
+ calendarId: "primary-calendar",
88
+ title: "Event 2",
89
+ start: "2023-10-10T09:30:00Z",
90
+ end: "2023-10-10T10:30:00Z",
91
+ },
92
+ ];
93
+
94
+ const layouts = generateEventLayouts({
95
+ startCalendarDate: "2023-10-10",
96
+ endCalendarDate: "2023-10-10",
97
+ events,
98
+ timezone: "UTC",
99
+ userCalendarId: "primary-calendar",
100
+ });
101
+
102
+ const dayLayout = layouts["2023-10-10"];
103
+ expect(dayLayout).toBeDefined();
104
+ const { partDayEventsLayout } = dayLayout;
105
+ expect(partDayEventsLayout.length).toBe(2);
106
+ expect(partDayEventsLayout[0].collisions).toBe(undefined);
107
+ expect(partDayEventsLayout[1].collisions).toBe(undefined);
108
+ });
109
+
110
+ it("should handle an empty event list", () => {
111
+ const events: CalendarEvent[] = [];
112
+
113
+ const layouts = generateEventLayouts({
114
+ startCalendarDate: "2023-10-10",
115
+ endCalendarDate: "2023-10-10",
116
+ events,
117
+ timezone: "UTC",
118
+ userCalendarId: "primary-calendar",
119
+ });
120
+
121
+ const dayLayout = layouts["2023-10-10"];
122
+ // Depending on implementation, if no events exist, the day entry might be missing.
123
+ if (!dayLayout) {
124
+ expect(Object.keys(layouts).length).toBe(0);
125
+ return;
126
+ }
127
+ expect(dayLayout.allDayEventsLayout.length).toBe(0);
128
+ expect(dayLayout.partDayEventsLayout.length).toBe(0);
129
+ });
130
+
131
+ it("should handle a single event", () => {
132
+ const events: CalendarEvent[] = [
133
+ {
134
+ id: "1",
135
+ calendarId: "primary-calendar",
136
+ title: "Event 1",
137
+ start: "2023-10-10T08:00:00Z",
138
+ end: "2023-10-10T09:00:00Z",
139
+ },
140
+ ];
141
+
142
+ const layouts = generateEventLayouts({
143
+ startCalendarDate: "2023-10-10",
144
+ endCalendarDate: "2023-10-10",
145
+ events,
146
+ timezone: "UTC",
147
+ userCalendarId: "primary-calendar",
148
+ });
149
+
150
+ const dayLayout = layouts["2023-10-10"];
151
+ expect(dayLayout.partDayEventsLayout.length).toBe(1);
152
+ expect(dayLayout.partDayEventsLayout[0].collisions).toBe(undefined);
153
+ });
154
+
155
+ it("should handle events spanning midnight", () => {
156
+ const events: CalendarEvent[] = [
157
+ {
158
+ id: "1",
159
+ calendarId: "primary-calendar",
160
+ title: "Event 1",
161
+ start: "2023-10-10T05:00:00Z",
162
+ end: "2023-10-12T05:00:00Z",
163
+ },
164
+ {
165
+ id: "2",
166
+ calendarId: "primary-calendar",
167
+ title: "Event 2",
168
+ start: "2023-10-11T05:00:00Z",
169
+ end: "2023-10-11T09:00:00Z",
170
+ },
171
+ ];
172
+
173
+ const layouts = generateEventLayouts({
174
+ startCalendarDate: "2023-10-10",
175
+ endCalendarDate: "2023-10-12",
176
+ events,
177
+ timezone: "UTC",
178
+ userCalendarId: "primary-calendar",
179
+ });
180
+
181
+ const layoutDay1 = layouts["2023-10-10"];
182
+ const layoutDay2 = layouts["2023-10-11"];
183
+ const layoutDay3 = layouts["2023-10-12"];
184
+
185
+ // Day 2 should see both events overlapping from 00:00–01:00
186
+ expect(layoutDay2?.allDayEventsLayout.length).toBe(1);
187
+ expect(layoutDay2?.partDayEventsLayout.length).toBe(1);
188
+
189
+ const evt1 = layoutDay1.allDayEventsLayout[0];
190
+ const evt2 = layoutDay2.allDayEventsLayout[0];
191
+ const evt3 = layoutDay3.allDayEventsLayout[0];
192
+
193
+ expect(evt1.extend).toBe(EventExtend.Future);
194
+ expect(evt2.extend).toBe(EventExtend.Both);
195
+ expect(evt3.extend).toBe(EventExtend.Past);
196
+ });
197
+
198
+ it("should handle events in different time zones", () => {
199
+ const events: CalendarEvent[] = [
200
+ {
201
+ id: "1",
202
+ calendarId: "primary-calendar",
203
+ title: "Event 1",
204
+ start: "2023-10-10T08:00:00+00:00",
205
+ end: "2023-10-10T09:00:00+00:00",
206
+ },
207
+ {
208
+ id: "2",
209
+ calendarId: "secondary-calendar",
210
+ title: "Event 2",
211
+ start: "2023-10-10T08:00:00-04:00",
212
+ end: "2023-10-10T09:00:00-04:00",
213
+ },
214
+ ];
215
+
216
+ const layouts = generateEventLayouts({
217
+ startCalendarDate: "2023-10-10",
218
+ endCalendarDate: "2023-10-10",
219
+ events,
220
+ timezone: "UTC",
221
+ userCalendarId: "primary-calendar",
222
+ });
223
+
224
+ const dayLayout = layouts["2023-10-10"];
225
+ expect(dayLayout).toBeDefined();
226
+
227
+ const { partDayEventsLayout } = dayLayout;
228
+ expect(partDayEventsLayout.length).toBe(2);
229
+
230
+ // These events do not overlap in UTC after conversion, so expect collisions.total = 1 for each.
231
+ expect(partDayEventsLayout[0].collisions).toBe(undefined);
232
+ expect(partDayEventsLayout[1].collisions).toBe(undefined);
233
+ });
234
+
235
+ it("should prioritize primary calendar events in collision sorting", () => {
236
+ // Here we mix a primary calendar event with one from a non-primary calendar.
237
+ const events: CalendarEvent[] = [
238
+ {
239
+ id: "1",
240
+ calendarId: "primary-calendar",
241
+ title: "Primary Event",
242
+ start: "2023-10-10T08:00:00Z",
243
+ end: "2023-10-10T09:00:00Z",
244
+ },
245
+ {
246
+ id: "2",
247
+ calendarId: "secondary-calendar",
248
+ title: "Secondary Event",
249
+ start: "2023-10-10T08:15:00Z",
250
+ end: "2023-10-10T09:15:00Z",
251
+ },
252
+ ];
253
+
254
+ const layouts = generateEventLayouts({
255
+ startCalendarDate: "2023-10-10",
256
+ endCalendarDate: "2023-10-10",
257
+ events,
258
+ timezone: "UTC",
259
+ userCalendarId: "primary-calendar",
260
+ });
261
+
262
+ const dayLayout = layouts["2023-10-10"];
263
+ expect(dayLayout).toBeDefined();
264
+ const { partDayEventsLayout } = dayLayout;
265
+ expect(partDayEventsLayout.length).toBe(2);
266
+
267
+ // Expect both events to have collision data with total = 2.
268
+ const primaryEvent = partDayEventsLayout.find((e) => e.event.id === "1")!;
269
+ const secondaryEvent = partDayEventsLayout.find((e) => e.event.id === "2")!;
270
+ expect(primaryEvent.collisions?.total).toBe(2);
271
+ expect(secondaryEvent.collisions?.total).toBe(2);
272
+ // The primary event (from "primary-calendar") should be ordered before the secondary event.
273
+ expect(primaryEvent.collisions?.order).toBeLessThan(
274
+ secondaryEvent.collisions?.order || 0
275
+ );
276
+ });
277
+ });
@@ -0,0 +1,139 @@
1
+ import { max } from "lodash";
2
+ import { EventExtend } from "src/types";
3
+
4
+ export class CalendarLayout {
5
+ // visibleX is an array of numbers representing the x indexes currently 'in view'
6
+ // enableWeekBreaks is a bool indicating if events are split at week boundary (monthview)
7
+ // startOfWeekXOffset is the offset to the start of the week from index 0
8
+ array2d: any[];
9
+ visibleX: Set<number>;
10
+ enableWeekBreaks: boolean;
11
+ startOfWeekXOffset: number;
12
+
13
+ constructor({
14
+ visibleX,
15
+ enableWeekBreaks,
16
+ startOfWeekXOffset,
17
+ }: {
18
+ visibleX: number[];
19
+ enableWeekBreaks: boolean;
20
+ startOfWeekXOffset: number;
21
+ }) {
22
+ this.array2d = [];
23
+ this.visibleX = new Set(visibleX);
24
+ this.enableWeekBreaks = enableWeekBreaks;
25
+ this.startOfWeekXOffset = startOfWeekXOffset;
26
+ }
27
+
28
+ getAt(x: number, y: number) {
29
+ return this.array2d[x] && this.array2d[x][y];
30
+ }
31
+
32
+ setAt(x: number, y: number, value: { value: any; meta: { x: any } }) {
33
+ let column = this.array2d[x];
34
+ if (!column) {
35
+ column = [];
36
+ this.array2d[x] = column;
37
+ }
38
+ column[y] = value;
39
+ }
40
+ // assign value to the line of cells from (x, y) to (x + w, y)
41
+ setRange(x: number, y: number, duration: number, value: any) {
42
+ for (let increment = 0; increment < duration; ++increment) {
43
+ this.setAt(x + increment, y, { value, meta: { x } });
44
+ }
45
+ }
46
+ // does the event starting time at x with duration w fit at position y?
47
+ fit(x: number, y: number, duration: number) {
48
+ for (let increment = 0; increment < duration; ++increment) {
49
+ if (this.getAt(x + increment, y)) {
50
+ return false;
51
+ }
52
+ }
53
+ return true;
54
+ }
55
+ // find the row where the event starting at time x with duration w fits
56
+ findFit(x: number, duration: number) {
57
+ let rowIndex = 0;
58
+ while (!this.fit(x, rowIndex, duration)) {
59
+ ++rowIndex;
60
+ }
61
+ return rowIndex;
62
+ }
63
+ // inserts an event record into the matrix
64
+ findFitAndInsert(
65
+ eventStartIndex: number,
66
+ eventDurationDays: number,
67
+ event: any
68
+ ) {
69
+ const rowIndex = this.findFit(eventStartIndex, eventDurationDays);
70
+ this.setRange(eventStartIndex, rowIndex, eventDurationDays, event);
71
+ }
72
+ // find the height of the 2d array
73
+ height() {
74
+ return this.array2d.length > 0
75
+ ? max(this.array2d.map((col) => (col && col.length) || 0))
76
+ : 0;
77
+ }
78
+ // get the event and associated view data for the given cell
79
+ getViewAt(x: number, y: number) {
80
+ const record = this.getAt(x, y);
81
+ if (!record) {
82
+ return {};
83
+ }
84
+ const {
85
+ value: event,
86
+ meta: { x: rootX },
87
+ } = record;
88
+ const previousRecord = this.visibleX.has(x - 1)
89
+ ? this.getAt(x - 1, y)
90
+ : null;
91
+ if (event) {
92
+ const isPrimaryRendered =
93
+ x === rootX ||
94
+ !previousRecord ||
95
+ previousRecord.value !== event ||
96
+ (this.enableWeekBreaks &&
97
+ Math.floor((x + this.startOfWeekXOffset) / 7) !==
98
+ Math.floor((x + this.startOfWeekXOffset - 1) / 7));
99
+ // count the contiguous visible days for this event
100
+ let visibleWidthDays = 1;
101
+ while (
102
+ this.visibleX.has(x + visibleWidthDays) &&
103
+ (!this.enableWeekBreaks ||
104
+ Math.floor((x + this.startOfWeekXOffset + visibleWidthDays) / 7) ===
105
+ Math.floor(
106
+ (x + this.startOfWeekXOffset + visibleWidthDays - 1) / 7
107
+ )) &&
108
+ this.getAt(x + visibleWidthDays, y) &&
109
+ this.getAt(x + visibleWidthDays, y).value === event
110
+ ) {
111
+ visibleWidthDays++;
112
+ }
113
+
114
+ const wrapStart = x !== rootX;
115
+ const wrapEnd =
116
+ this.getAt(x + visibleWidthDays, y) &&
117
+ this.getAt(x + visibleWidthDays, y).value === event;
118
+
119
+ let extend = EventExtend.None;
120
+
121
+ if (wrapStart && wrapEnd) {
122
+ extend = EventExtend.Both;
123
+ } else if (wrapStart) {
124
+ extend = EventExtend.Past;
125
+ } else if (wrapEnd) {
126
+ extend = EventExtend.Future;
127
+ }
128
+
129
+ return {
130
+ event,
131
+ visibleWidthDays,
132
+ isPrimaryRendered,
133
+ extend,
134
+ };
135
+ }
136
+
137
+ return {};
138
+ }
139
+ }
@@ -0,0 +1,44 @@
1
+ import moment, { Moment } from "moment-timezone";
2
+ import { CollisionObject, EventPosition } from "src/types";
3
+
4
+ type ComputePositioning = {
5
+ // We only need the collisions part of this type
6
+ collisionObject: CollisionObject;
7
+ startOfDayMoment: Moment;
8
+ timezone: string;
9
+ };
10
+
11
+ const computePositioning = ({
12
+ collisionObject,
13
+ startOfDayMoment,
14
+ timezone,
15
+ }: ComputePositioning): EventPosition => {
16
+ const startDateMoment = moment.tz(collisionObject.event.start, timezone);
17
+ const durationMinutes = moment
18
+ .tz(collisionObject.event.end, timezone)
19
+ .diff(startDateMoment, "minutes");
20
+
21
+ let width = 100;
22
+ let margin = 0;
23
+
24
+ const top = startDateMoment.diff(startOfDayMoment, "minutes");
25
+ const height = Math.max(30, durationMinutes);
26
+ const collisions = collisionObject.collisions;
27
+
28
+ if (collisions) {
29
+ margin = (100 / collisions.total) * collisions.order;
30
+ width =
31
+ collisions.order + 1 < collisions.total
32
+ ? Math.max(100 - 12 * collisions.total, 20)
33
+ : 100 / collisions.total;
34
+ }
35
+
36
+ return {
37
+ top,
38
+ height,
39
+ width: `${width}%`,
40
+ marginLeft: `${margin}%`,
41
+ };
42
+ };
43
+
44
+ export default computePositioning;
@@ -0,0 +1,238 @@
1
+ import moment, { type Moment } from "moment-timezone";
2
+ import { isDate, range, size } from "lodash";
3
+ import { CalendarEvent, PrefabHour } from "src/types";
4
+
5
+ export const generatePrefabHours = (
6
+ timeFormat: string = "HH:mm"
7
+ ): PrefabHour[] => {
8
+ const startOfDayMoment = moment().startOf("day");
9
+
10
+ return [...Array(24).keys()].reduce(
11
+ (
12
+ accum: {
13
+ increment: number;
14
+ hourFormatted: string;
15
+ hourMoment: Moment;
16
+ }[],
17
+ increment
18
+ ) => {
19
+ const hourMoment = startOfDayMoment.clone().hour(increment);
20
+
21
+ accum.push({
22
+ increment,
23
+ hourFormatted: hourMoment.format(timeFormat),
24
+ hourMoment,
25
+ });
26
+
27
+ return accum;
28
+ },
29
+ []
30
+ );
31
+ };
32
+
33
+ // Returns a new moment instance at the start of the week in the user's
34
+ // timezone, with the user's start of week preference applied.
35
+ export const startOfUserWeek = (
36
+ startDayOfWeekOffset: number,
37
+ dateOrMoment: Date | Moment | string,
38
+ timezone: string
39
+ ) => {
40
+ // If the day is Sunday, and the user's start of week preference is Sunday, return the day
41
+ // otherwise, the start of the 'isoWeek' will be for the previous week
42
+ if (
43
+ startDayOfWeekOffset === 0 &&
44
+ moment.tz(dateOrMoment, timezone).isoWeekday() === 7
45
+ ) {
46
+ return moment.tz(dateOrMoment, timezone).startOf("day");
47
+ }
48
+
49
+ return moment
50
+ .tz(dateOrMoment, timezone)
51
+ .startOf("isoWeek")
52
+ .isoWeekday(startDayOfWeekOffset);
53
+ };
54
+
55
+ export const isAllDayOrSpansMidnight = (
56
+ calendarEvent: CalendarEvent,
57
+ timezone: string
58
+ ) => {
59
+ const { start, end, isAllDay } = calendarEvent;
60
+
61
+ if (isAllDay) {
62
+ return true;
63
+ }
64
+
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);
68
+
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
+ );
74
+ };
75
+
76
+ // Returns the count of unique dates in the provided timezone
77
+ export const getDurationInDays = (
78
+ calendarEvent: CalendarEvent,
79
+ timezone: string
80
+ ) => {
81
+ // the event duration in days calculation depends on if the event is all day
82
+ return calendarEvent.isAllDay
83
+ ? moment
84
+ .tz(calendarEvent.end, timezone)
85
+ .diff(moment.tz(calendarEvent.start, timezone), "days") + 1
86
+ : size(
87
+ daysInRange({
88
+ startDate: calendarEvent.start,
89
+ endDate: calendarEvent.end,
90
+ timezone,
91
+ })
92
+ );
93
+ };
94
+
95
+ // Returns an array of days (e.g. ['2022-01-02']) in a given date range.
96
+ export const daysInRange = ({
97
+ startDate,
98
+ endDate,
99
+ timezone,
100
+ }: {
101
+ startDate: Date | string;
102
+ endDate: Date | string;
103
+ timezone: string;
104
+ }) => {
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");
109
+ const days = [];
110
+ // Make sure we loop at a max of 30 times here as we had events that were scheduled for all day long for
111
+ // 1000 years in the future and this was causing the app to crash
112
+ for (
113
+ let countOfDaysAfterStart = 0;
114
+ countOfDaysAfterStart <= Math.min(30, Math.abs(countOfDaysInRange));
115
+ countOfDaysAfterStart++
116
+ ) {
117
+ days.push(
118
+ moment
119
+ .tz(startDay, timezone)
120
+ .add(countOfDaysAfterStart, "day")
121
+ .format("YYYY-MM-DD")
122
+ );
123
+ }
124
+ return days;
125
+ };
126
+
127
+ export const getDuration = (
128
+ calendarEvent: CalendarEvent,
129
+ trueDuration?: boolean
130
+ ) => {
131
+ const minDiff =
132
+ (new Date(calendarEvent.end).valueOf() -
133
+ new Date(calendarEvent.start).valueOf()) /
134
+ (1000 * 60);
135
+
136
+ // If all-day, we want to throw in an extra 24 hours since we represent them a bit oddly.
137
+ return calendarEvent.isAllDay && !trueDuration
138
+ ? minDiff + 24 * 60 * 60
139
+ : minDiff;
140
+ };
141
+
142
+ export const computeCalendarDateRange = (
143
+ date: string | Date | Moment,
144
+ tz: string,
145
+ viewType: "month" | "workweek" | "3day" | "1day" | "week",
146
+ startDayOfWeekOffset: number
147
+ ) => {
148
+ const momentDate = moment.tz(date, tz).startOf("day").toDate();
149
+
150
+ let basis: Moment;
151
+ let dayIndexes: number[];
152
+
153
+ if (viewType === "month") {
154
+ const startOfMonth = moment.tz(momentDate, tz).startOf("month");
155
+ const numberOfDaysInViewBeforeStartOfMonth =
156
+ startOfMonth.isoWeekday() - startDayOfWeekOffset;
157
+ basis = startOfMonth.subtract(numberOfDaysInViewBeforeStartOfMonth, "days");
158
+ const startOfRange = 0;
159
+ const numberOfDaysInViewAfterEndOfMonth =
160
+ (numberOfDaysInViewBeforeStartOfMonth +
161
+ moment.tz(date, tz).daysInMonth()) %
162
+ 7
163
+ ? 7 -
164
+ ((numberOfDaysInViewBeforeStartOfMonth +
165
+ moment.tz(date, tz).daysInMonth()) %
166
+ 7)
167
+ : 0;
168
+ const endOfRange =
169
+ numberOfDaysInViewBeforeStartOfMonth +
170
+ moment.tz(date, tz).daysInMonth() +
171
+ numberOfDaysInViewAfterEndOfMonth;
172
+ dayIndexes = range(startOfRange, endOfRange);
173
+ } else if (viewType === "workweek") {
174
+ basis = startOfUserWeek(startDayOfWeekOffset, momentDate, tz);
175
+ dayIndexes = range(0, 7).filter(
176
+ (dayIndex) =>
177
+ [0, 6].indexOf(basis.clone().add(dayIndex, "day").day()) === -1
178
+ );
179
+ } else if (viewType === "3day") {
180
+ // On PYD, we need to see more than just today, as potentially we'll see yesterday and tomorrow
181
+ basis = moment.tz(momentDate, tz);
182
+ dayIndexes = [0, 1, 2];
183
+ } else if (viewType === "1day") {
184
+ basis = moment.tz(momentDate, tz);
185
+ dayIndexes = [0];
186
+ } else {
187
+ basis = startOfUserWeek(startDayOfWeekOffset, momentDate, tz);
188
+ dayIndexes = range(0, 7);
189
+ }
190
+
191
+ const days = dayIndexes.map((dayIndex) =>
192
+ moment.tz(basis, tz).add(dayIndex, "day").toDate()
193
+ );
194
+ const calendarDates = dayIndexes.map((dayIndex) =>
195
+ moment.tz(basis, tz).add(dayIndex, "day").format("YYYY-MM-DD")
196
+ );
197
+
198
+ return {
199
+ basisDate: moment(basis, tz).toDate(),
200
+ dayIndexes,
201
+ days,
202
+ startDate: days[0],
203
+ endDate: moment
204
+ .tz(days[days.length - 1], tz)
205
+ .add(1, "day")
206
+ .toDate(),
207
+ calendarDates,
208
+ startCalendarDate: calendarDates[0],
209
+ endCalendarDate: calendarDates[calendarDates.length - 1],
210
+ };
211
+ };
212
+
213
+ // tests if the date ranges intersect
214
+ export const dateRangeIntersect = (
215
+ { startDate: start0, endDate: end0 }: { startDate: Date; endDate: Date },
216
+ { startDate: start1, endDate: end1 }: { startDate: Date; endDate: Date }
217
+ ) => {
218
+ if (!isDate(start0) || !isDate(end0) || !isDate(start1) || !isDate(end1)) {
219
+ throw `invalid parameter ${start0} ${end0}; must pass dates`;
220
+ }
221
+
222
+ const s0 = start0.getTime();
223
+ const e0 = end0.getTime();
224
+ const s1 = start1.getTime();
225
+ const e1 = end1.getTime();
226
+
227
+ if (s0 > e0 || s1 > e1) {
228
+ return false;
229
+ }
230
+
231
+ return isBetween(s0, s1, e1) || isBetween(s1, s0, e0);
232
+ };
233
+
234
+ const isBetween = (
235
+ value: number,
236
+ startInclusive: number,
237
+ endExclusive: number
238
+ ) => value >= startInclusive && value < endExclusive;