@monkey-mini-app/ui 0.1.1 → 0.1.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 (203) hide show
  1. package/README.md +12 -6
  2. package/dist/globals.css +1 -1
  3. package/dist/index.js +12 -2
  4. package/dist/manifest.json +6 -6
  5. package/dist/runtime.js +9 -0
  6. package/dist/sdk.js +158 -0
  7. package/dist/src/blocks/activity-feed.tsx +7 -0
  8. package/dist/src/blocks/app-shell.tsx +1 -0
  9. package/dist/src/blocks/attachment-gallery.tsx +7 -0
  10. package/dist/src/blocks/bar-chart.tsx +11 -0
  11. package/dist/src/blocks/comment-thread.tsx +7 -0
  12. package/dist/src/blocks/dashboard-shell.tsx +109 -0
  13. package/dist/src/blocks/description-list.tsx +7 -0
  14. package/dist/src/blocks/detail-panel.tsx +1 -0
  15. package/dist/src/blocks/donut-chart.tsx +7 -0
  16. package/dist/src/blocks/env-badge.tsx +7 -0
  17. package/dist/src/blocks/env-table.tsx +316 -0
  18. package/dist/src/blocks/file-tree.tsx +7 -0
  19. package/dist/src/blocks/filter-bar.tsx +1 -0
  20. package/dist/src/blocks/form-sheet.tsx +160 -0
  21. package/dist/src/blocks/gauge.tsx +7 -0
  22. package/dist/src/blocks/list-detail.tsx +121 -0
  23. package/dist/src/blocks/notification-center.tsx +7 -0
  24. package/dist/src/blocks/page-header.tsx +1 -0
  25. package/dist/src/blocks/progress-ring.tsx +7 -0
  26. package/dist/src/blocks/radar-chart.tsx +7 -0
  27. package/dist/src/blocks/request-inspector.tsx +7 -0
  28. package/dist/src/blocks/reveal.tsx +56 -0
  29. package/dist/src/blocks/run-timeline.tsx +7 -0
  30. package/dist/src/blocks/scrollspy.tsx +7 -0
  31. package/dist/src/blocks/settings-split.tsx +154 -0
  32. package/dist/src/blocks/severity-chip.tsx +7 -0
  33. package/dist/src/blocks/sparkline.tsx +7 -0
  34. package/dist/src/blocks/stat-card.tsx +1 -0
  35. package/dist/src/blocks/status-badge.tsx +1 -0
  36. package/dist/src/blocks/table-page.tsx +83 -0
  37. package/dist/src/blocks/terminal.tsx +21 -3
  38. package/dist/src/blocks/test-step-list.tsx +7 -0
  39. package/dist/src/blocks/trend-card.tsx +7 -0
  40. package/dist/src/blocks/wizard-shell.tsx +132 -0
  41. package/dist/src/cdn-modules.d.ts +5 -0
  42. package/dist/src/components/accordion.tsx +7 -0
  43. package/dist/src/components/alert-dialog.tsx +7 -0
  44. package/dist/src/components/alert.tsx +7 -0
  45. package/dist/src/components/aspect-ratio.tsx +4 -0
  46. package/dist/src/components/attachment.tsx +4 -0
  47. package/dist/src/components/avatar.tsx +4 -0
  48. package/dist/src/components/badge.tsx +7 -0
  49. package/dist/src/components/breadcrumb.tsx +4 -0
  50. package/dist/src/components/bubble.tsx +4 -0
  51. package/dist/src/components/button-group.tsx +6 -0
  52. package/dist/src/components/button.tsx +7 -0
  53. package/dist/src/components/calendar.tsx +4 -0
  54. package/dist/src/components/card.tsx +8 -1
  55. package/dist/src/components/chart.tsx +7 -0
  56. package/dist/src/components/checkbox.tsx +5 -0
  57. package/dist/src/components/collapsible.tsx +4 -0
  58. package/dist/src/components/combobox.tsx +4 -0
  59. package/dist/src/components/command.tsx +6 -0
  60. package/dist/src/components/context-menu.tsx +4 -0
  61. package/dist/src/components/dialog.tsx +21 -0
  62. package/dist/src/components/drawer.tsx +4 -0
  63. package/dist/src/components/dropdown-menu.tsx +7 -0
  64. package/dist/src/components/empty.tsx +7 -0
  65. package/dist/src/components/field.tsx +6 -0
  66. package/dist/src/components/hover-card.tsx +4 -0
  67. package/dist/src/components/input-group.tsx +4 -0
  68. package/dist/src/components/input.tsx +7 -0
  69. package/dist/src/components/item.tsx +4 -0
  70. package/dist/src/components/kbd.tsx +4 -0
  71. package/dist/src/components/label.tsx +7 -0
  72. package/dist/src/components/marker.tsx +4 -0
  73. package/dist/src/components/menubar.tsx +4 -0
  74. package/dist/src/components/message-scroller.tsx +4 -0
  75. package/dist/src/components/message.tsx +4 -0
  76. package/dist/src/components/native-select.tsx +4 -0
  77. package/dist/src/components/navigation-menu.tsx +4 -0
  78. package/dist/src/components/pagination.tsx +4 -0
  79. package/dist/src/components/popover.tsx +7 -0
  80. package/dist/src/components/progress.tsx +7 -0
  81. package/dist/src/components/questionnaire.tsx +4 -0
  82. package/dist/src/components/radio-group.tsx +4 -0
  83. package/dist/src/components/resizable.tsx +7 -0
  84. package/dist/src/components/scroll-area.tsx +4 -0
  85. package/dist/src/components/select.tsx +7 -0
  86. package/dist/src/components/separator.tsx +5 -0
  87. package/dist/src/components/sheet.tsx +22 -0
  88. package/dist/src/components/sidebar.tsx +4 -0
  89. package/dist/src/components/skeleton.tsx +7 -0
  90. package/dist/src/components/slider.tsx +4 -0
  91. package/dist/src/components/spinner.tsx +6 -0
  92. package/dist/src/components/switch.tsx +7 -0
  93. package/dist/src/components/table.tsx +7 -0
  94. package/dist/src/components/tabs.tsx +7 -0
  95. package/dist/src/components/textarea.tsx +7 -0
  96. package/dist/src/components/toast.tsx +7 -0
  97. package/dist/src/components/toggle-group.tsx +4 -0
  98. package/dist/src/components/toggle.tsx +4 -0
  99. package/dist/src/components/tooltip.tsx +7 -0
  100. package/dist/src/composites/autocomplete.tsx +7 -0
  101. package/dist/src/composites/cascader.tsx +7 -0
  102. package/dist/src/composites/color-picker.tsx +7 -0
  103. package/dist/src/composites/confirm-dialog.tsx +7 -0
  104. package/dist/src/composites/copyable.tsx +9 -1
  105. package/dist/src/composites/currency-input.tsx +7 -0
  106. package/dist/src/composites/date-picker.tsx +7 -0
  107. package/dist/src/composites/date-range-picker.tsx +4 -0
  108. package/dist/src/composites/date-time-picker.tsx +7 -0
  109. package/dist/src/composites/date-time-range-picker.tsx +7 -0
  110. package/dist/src/composites/duration-input.tsx +7 -0
  111. package/dist/src/composites/mini-calendar.tsx +7 -0
  112. package/dist/src/composites/number-field.tsx +7 -0
  113. package/dist/src/composites/password-field.tsx +7 -0
  114. package/dist/src/composites/phone-input.tsx +7 -0
  115. package/dist/src/composites/rating.tsx +7 -0
  116. package/dist/src/composites/relative-date-picker.tsx +7 -0
  117. package/dist/src/composites/search-input.tsx +7 -0
  118. package/dist/src/composites/slider-range.tsx +7 -0
  119. package/dist/src/composites/tag-input.tsx +7 -0
  120. package/dist/src/composites/time-picker.tsx +11 -1
  121. package/dist/src/composites/time-range-picker.tsx +7 -0
  122. package/dist/src/composites/timezone-select.tsx +7 -0
  123. package/dist/src/composites/transfer.tsx +7 -0
  124. package/dist/src/composites/user-picker.tsx +7 -0
  125. package/dist/src/hooks/use-html-dark.ts +37 -0
  126. package/dist/src/hooks/use-mobile.ts +3 -0
  127. package/dist/src/i18n/en.ts +109 -2
  128. package/dist/src/i18n/zh.ts +99 -2
  129. package/dist/src/index.ts +147 -134
  130. package/dist/src/lib/clipboard.ts +10 -0
  131. package/dist/src/lib/illustrations.tsx +2 -2
  132. package/dist/src/products/code-block.tsx +35 -18
  133. package/dist/src/products/code-editor.tsx +156 -24
  134. package/dist/src/products/commit-graph.tsx +644 -0
  135. package/dist/src/products/data-grid.tsx +2 -1
  136. package/dist/src/products/diff-viewer.tsx +104 -8
  137. package/dist/src/products/event-calendar.tsx +28 -823
  138. package/dist/src/products/file-dropzone.tsx +7 -0
  139. package/dist/src/products/full-calendar/animations.ts +38 -0
  140. package/dist/src/products/full-calendar/calendar-body.tsx +77 -0
  141. package/dist/src/products/full-calendar/constants.ts +10 -0
  142. package/dist/src/products/full-calendar/contexts/calendar-context.tsx +255 -0
  143. package/dist/src/products/full-calendar/contexts/dnd-context.tsx +157 -0
  144. package/dist/src/products/full-calendar/dialogs/add-edit-event-dialog.tsx +187 -0
  145. package/dist/src/products/full-calendar/dialogs/delete-event-dialog.tsx +39 -0
  146. package/dist/src/products/full-calendar/dialogs/event-details-dialog.tsx +100 -0
  147. package/dist/src/products/full-calendar/dialogs/events-list-dialog.tsx +87 -0
  148. package/dist/src/products/full-calendar/dnd/draggable-event.tsx +45 -0
  149. package/dist/src/products/full-calendar/dnd/droppable-area.tsx +45 -0
  150. package/dist/src/products/full-calendar/dnd/resizable-event.tsx +179 -0
  151. package/dist/src/products/full-calendar/header/calendar-header.tsx +65 -0
  152. package/dist/src/products/full-calendar/header/date-navigator.tsx +110 -0
  153. package/dist/src/products/full-calendar/header/filter.tsx +58 -0
  154. package/dist/src/products/full-calendar/header/today-button.tsx +45 -0
  155. package/dist/src/products/full-calendar/header/user-select.tsx +64 -0
  156. package/dist/src/products/full-calendar/header/view-tabs.tsx +36 -0
  157. package/dist/src/products/full-calendar/helpers.ts +453 -0
  158. package/dist/src/products/full-calendar/hooks/use-time-range-create.ts +152 -0
  159. package/dist/src/products/full-calendar/hooks.ts +70 -0
  160. package/dist/src/products/full-calendar/interfaces.ts +23 -0
  161. package/dist/src/products/full-calendar/settings/settings.tsx +80 -0
  162. package/dist/src/products/full-calendar/shims/avatar-group.tsx +16 -0
  163. package/dist/src/products/full-calendar/shims/day-picker.tsx +1 -0
  164. package/dist/src/products/full-calendar/shims/motion.tsx +97 -0
  165. package/dist/src/products/full-calendar/shims/next-themes.ts +6 -0
  166. package/dist/src/products/full-calendar/shims/re-resizable.tsx +25 -0
  167. package/dist/src/products/full-calendar/shims/sonner.ts +4 -0
  168. package/dist/src/products/full-calendar/types.ts +8 -0
  169. package/dist/src/products/full-calendar/views/agenda-view/agenda-events.tsx +140 -0
  170. package/dist/src/products/full-calendar/views/month-view/calendar-month-view.tsx +69 -0
  171. package/dist/src/products/full-calendar/views/month-view/day-cell.tsx +201 -0
  172. package/dist/src/products/full-calendar/views/month-view/event-bullet.tsx +40 -0
  173. package/dist/src/products/full-calendar/views/month-view/month-event-badge.tsx +143 -0
  174. package/dist/src/products/full-calendar/views/week-and-day-view/calendar-day-view.tsx +295 -0
  175. package/dist/src/products/full-calendar/views/week-and-day-view/calendar-time-line.tsx +35 -0
  176. package/dist/src/products/full-calendar/views/week-and-day-view/calendar-week-view.tsx +253 -0
  177. package/dist/src/products/full-calendar/views/week-and-day-view/day-view-multi-day-events-row.tsx +73 -0
  178. package/dist/src/products/full-calendar/views/week-and-day-view/event-block.tsx +114 -0
  179. package/dist/src/products/full-calendar/views/week-and-day-view/render-grouped-events.tsx +49 -0
  180. package/dist/src/products/full-calendar/views/week-and-day-view/week-view-multi-day-events-row.tsx +143 -0
  181. package/dist/src/products/full-calendar/views/year-view/calendar-year-view.tsx +161 -0
  182. package/dist/src/products/gantt.tsx +7 -0
  183. package/dist/src/products/jira-wiki.tsx +1 -0
  184. package/dist/src/products/jql-input.tsx +340 -37
  185. package/dist/src/products/json-viewer.tsx +7 -0
  186. package/dist/src/products/kanban-issue-panel.tsx +7 -0
  187. package/dist/src/products/kanban.tsx +1 -0
  188. package/dist/src/products/log-viewer.tsx +390 -18
  189. package/dist/src/products/markdown-editor.tsx +21 -6
  190. package/dist/src/products/markdown.tsx +7 -0
  191. package/dist/src/products/rich-text-editor.tsx +65 -59
  192. package/dist/src/products/sortable-list.tsx +7 -0
  193. package/dist/src/products/stepper.tsx +219 -70
  194. package/dist/src/products/timeline.tsx +7 -0
  195. package/dist/src/products/tree-view.tsx +7 -0
  196. package/dist/src/sdk/app-error-boundary.tsx +240 -0
  197. package/dist/src/sdk/app-id.ts +7 -0
  198. package/dist/src/sdk/use-app.ts +126 -0
  199. package/dist/src/styles/globals.css +1 -0
  200. package/dist/vendors/lodash.js +20 -0
  201. package/dist/vendors/motion.js +12 -0
  202. package/package.json +20 -19
  203. package/dist/src/products/jql-language.ts +0 -70
@@ -0,0 +1,453 @@
1
+ import {
2
+ addDays,
3
+ addMonths,
4
+ addWeeks,
5
+ addYears,
6
+ differenceInDays,
7
+ differenceInMinutes,
8
+ eachDayOfInterval,
9
+ endOfMonth,
10
+ endOfWeek,
11
+ endOfYear,
12
+ format,
13
+ isSameDay,
14
+ isSameMonth,
15
+ isSameWeek,
16
+ isSameYear,
17
+ isValid,
18
+ parseISO,
19
+ startOfDay,
20
+ startOfMonth,
21
+ startOfWeek,
22
+ startOfYear,
23
+ subDays,
24
+ subMonths,
25
+ subWeeks,
26
+ subYears,
27
+ } from "date-fns";
28
+ import type { Locale } from "date-fns";
29
+ import { useCalendar } from "./contexts/calendar-context";
30
+ import type {
31
+ ICalendarCell,
32
+ IEvent,
33
+ } from "./interfaces";
34
+ import type {
35
+ TCalendarView,
36
+ TEventColor,
37
+ } from "./types";
38
+
39
+ const FORMAT_STRING = "MMM d, yyyy";
40
+ export const HOUR_HEIGHT_PX = 96;
41
+
42
+ export function rangeText(
43
+ view: TCalendarView,
44
+ date: Date,
45
+ opts?: { locale?: Locale; formatError?: string },
46
+ ): string {
47
+ const { locale, formatError = "Error while formatting" } = opts ?? {};
48
+ let start: Date;
49
+ let end: Date;
50
+
51
+ switch (view) {
52
+ case "month":
53
+ start = startOfMonth(date);
54
+ end = endOfMonth(date);
55
+ break;
56
+ case "week":
57
+ start = startOfWeek(date);
58
+ end = endOfWeek(date);
59
+ break;
60
+ case "day":
61
+ return format(date, FORMAT_STRING, { locale });
62
+ case "year":
63
+ start = startOfYear(date);
64
+ end = endOfYear(date);
65
+ break;
66
+ case "agenda":
67
+ start = startOfMonth(date);
68
+ end = endOfMonth(date);
69
+ break;
70
+ default:
71
+ return formatError;
72
+ }
73
+
74
+ return `${format(start, FORMAT_STRING, { locale })} - ${format(end, FORMAT_STRING, { locale })}`;
75
+ }
76
+
77
+ export function navigateDate(
78
+ date: Date,
79
+ view: TCalendarView,
80
+ direction: "previous" | "next",
81
+ ): Date {
82
+ const operations: Record<TCalendarView, (d: Date, n: number) => Date> = {
83
+ month: direction === "next" ? addMonths : subMonths,
84
+ week: direction === "next" ? addWeeks : subWeeks,
85
+ day: direction === "next" ? addDays : subDays,
86
+ year: direction === "next" ? addYears : subYears,
87
+ agenda: direction === "next" ? addMonths : subMonths,
88
+ };
89
+
90
+ return operations[view](date, 1);
91
+ }
92
+
93
+ export function getEventsCount(
94
+ events: IEvent[],
95
+ date: Date,
96
+ view: TCalendarView,
97
+ ): number {
98
+ const compareFns: Record<TCalendarView, (d1: Date, d2: Date) => boolean> = {
99
+ day: isSameDay,
100
+ week: isSameWeek,
101
+ month: isSameMonth,
102
+ year: isSameYear,
103
+ agenda: isSameMonth,
104
+ };
105
+
106
+ const compareFn = compareFns[view];
107
+ return events.filter((event) => compareFn(parseISO(event.startDate), date))
108
+ .length;
109
+ }
110
+
111
+ export function groupEvents(dayEvents: IEvent[]): IEvent[][] {
112
+ const sortedEvents = dayEvents.sort(
113
+ (a, b) => parseISO(a.startDate).getTime() - parseISO(b.startDate).getTime(),
114
+ );
115
+ const groups: IEvent[][] = [];
116
+
117
+ for (const event of sortedEvents) {
118
+ const eventStart = parseISO(event.startDate);
119
+ let placed = false;
120
+
121
+ for (const group of groups) {
122
+ const lastEventInGroup = group[group.length - 1];
123
+ const lastEventEnd = parseISO(lastEventInGroup.endDate);
124
+
125
+ if (eventStart >= lastEventEnd) {
126
+ group.push(event);
127
+ placed = true;
128
+ break;
129
+ }
130
+ }
131
+
132
+ if (!placed) groups.push([event]);
133
+ }
134
+
135
+ return groups;
136
+ }
137
+
138
+ export function getEventBlockStyle(
139
+ event: IEvent,
140
+ day: Date,
141
+ groupIndex: number,
142
+ groupSize: number,
143
+ ) {
144
+ const startDate = parseISO(event.startDate);
145
+ const dayStart = startOfDay(day); // Use startOfDay instead of manual reset
146
+ const eventStart = startDate < dayStart ? dayStart : startDate;
147
+ const startMinutes = differenceInMinutes(eventStart, dayStart);
148
+
149
+ const top = (startMinutes / 1440) * 100; // 1440 minutes in a day
150
+ const width = 100 / groupSize;
151
+ const left = groupIndex * width;
152
+
153
+ return { top: `${top}%`, width: `${width}%`, left: `${left}%` };
154
+ }
155
+
156
+ export function getCalendarCells(selectedDate: Date): ICalendarCell[] {
157
+ const year = selectedDate.getFullYear();
158
+ const month = selectedDate.getMonth();
159
+
160
+ const daysInMonth = endOfMonth(selectedDate).getDate(); // Faster than new Date(year, month + 1, 0)
161
+ const firstDayOfMonth = startOfMonth(selectedDate).getDay();
162
+ const daysInPrevMonth = endOfMonth(new Date(year, month - 1)).getDate();
163
+ const totalDays = firstDayOfMonth + daysInMonth;
164
+
165
+ const prevMonthCells = Array.from({ length: firstDayOfMonth }, (_, i) => ({
166
+ day: daysInPrevMonth - firstDayOfMonth + i + 1,
167
+ currentMonth: false,
168
+ date: new Date(year, month - 1, daysInPrevMonth - firstDayOfMonth + i + 1),
169
+ }));
170
+
171
+ const currentMonthCells = Array.from({ length: daysInMonth }, (_, i) => ({
172
+ day: i + 1,
173
+ currentMonth: true,
174
+ date: new Date(year, month, i + 1),
175
+ }));
176
+
177
+ const nextMonthCells = Array.from(
178
+ { length: (7 - (totalDays % 7)) % 7 },
179
+ (_, i) => ({
180
+ day: i + 1,
181
+ currentMonth: false,
182
+ date: new Date(year, month + 1, i + 1),
183
+ }),
184
+ );
185
+
186
+ return [...prevMonthCells, ...currentMonthCells, ...nextMonthCells];
187
+ }
188
+
189
+ export function calculateMonthEventPositions(
190
+ multiDayEvents: IEvent[],
191
+ singleDayEvents: IEvent[],
192
+ selectedDate: Date,
193
+ ): Record<string, number> {
194
+ const monthStart = startOfMonth(selectedDate);
195
+ const monthEnd = endOfMonth(selectedDate);
196
+
197
+ const eventPositions: Record<string, number> = {};
198
+ const occupiedPositions: Record<string, boolean[]> = {};
199
+
200
+ eachDayOfInterval({ start: monthStart, end: monthEnd }).forEach((day) => {
201
+ occupiedPositions[day.toISOString()] = [false, false, false];
202
+ });
203
+
204
+ const sortedEvents = [
205
+ ...multiDayEvents.sort((a, b) => {
206
+ const aDuration = differenceInDays(
207
+ parseISO(a.endDate),
208
+ parseISO(a.startDate),
209
+ );
210
+ const bDuration = differenceInDays(
211
+ parseISO(b.endDate),
212
+ parseISO(b.startDate),
213
+ );
214
+ return (
215
+ bDuration - aDuration ||
216
+ parseISO(a.startDate).getTime() - parseISO(b.startDate).getTime()
217
+ );
218
+ }),
219
+ ...singleDayEvents.sort(
220
+ (a, b) =>
221
+ parseISO(a.startDate).getTime() - parseISO(b.startDate).getTime(),
222
+ ),
223
+ ];
224
+
225
+ sortedEvents.forEach((event) => {
226
+ const eventStart = parseISO(event.startDate);
227
+ const eventEnd = parseISO(event.endDate);
228
+ const eventDays = eachDayOfInterval({
229
+ start: eventStart < monthStart ? monthStart : eventStart,
230
+ end: eventEnd > monthEnd ? monthEnd : eventEnd,
231
+ });
232
+
233
+ let position = -1;
234
+
235
+ for (let i = 0; i < 3; i++) {
236
+ if (
237
+ eventDays.every((day) => {
238
+ const dayPositions = occupiedPositions[startOfDay(day).toISOString()];
239
+ return dayPositions && !dayPositions[i];
240
+ })
241
+ ) {
242
+ position = i;
243
+ break;
244
+ }
245
+ }
246
+
247
+ if (position !== -1) {
248
+ eventDays.forEach((day) => {
249
+ const dayKey = startOfDay(day).toISOString();
250
+ occupiedPositions[dayKey][position] = true;
251
+ });
252
+ eventPositions[event.id] = position;
253
+ }
254
+ });
255
+
256
+ return eventPositions;
257
+ }
258
+
259
+ export function getMonthCellEvents(
260
+ date: Date,
261
+ events: IEvent[],
262
+ eventPositions: Record<string, number>,
263
+ ) {
264
+ const dayStart = startOfDay(date);
265
+ const eventsForDate = events.filter((event) => {
266
+ const eventStart = parseISO(event.startDate);
267
+ const eventEnd = parseISO(event.endDate);
268
+ return (
269
+ (dayStart >= eventStart && dayStart <= eventEnd) ||
270
+ isSameDay(dayStart, eventStart) ||
271
+ isSameDay(dayStart, eventEnd)
272
+ );
273
+ });
274
+
275
+ return eventsForDate
276
+ .map((event) => ({
277
+ ...event,
278
+ position: eventPositions[event.id] ?? -1,
279
+ isMultiDay: event.startDate !== event.endDate,
280
+ }))
281
+ .sort((a, b) => {
282
+ if (a.isMultiDay && !b.isMultiDay) return -1;
283
+ if (!a.isMultiDay && b.isMultiDay) return 1;
284
+ return a.position - b.position;
285
+ });
286
+ }
287
+
288
+ export function formatTime(
289
+ date: Date | string,
290
+ use24HourFormat: boolean,
291
+ ): string {
292
+ const parsedDate = typeof date === "string" ? parseISO(date) : date;
293
+ if (!isValid(parsedDate)) return "";
294
+ return format(parsedDate, use24HourFormat ? "HH:mm" : "h:mm a");
295
+ }
296
+
297
+ export const getFirstLetters = (str: string): string => {
298
+ if (!str) return "";
299
+ const words = str.split(" ");
300
+ if (words.length === 1) return words[0].charAt(0).toUpperCase();
301
+ return `${words[0].charAt(0).toUpperCase()}${words[1].charAt(0).toUpperCase()}`;
302
+ };
303
+
304
+ export const getEventsForDay = (
305
+ events: IEvent[],
306
+ date: Date,
307
+ isWeek = false,
308
+ ): IEvent[] => {
309
+ const targetDate = startOfDay(date);
310
+ return events
311
+ .filter((event) => {
312
+ const startOfDayForEventStart = startOfDay(parseISO(event.startDate));
313
+ const startOfDayForEventEnd = startOfDay(parseISO(event.endDate));
314
+ if (isWeek) {
315
+ return (
316
+ event.startDate !== event.endDate &&
317
+ startOfDayForEventStart <= targetDate &&
318
+ startOfDayForEventEnd >= targetDate
319
+ );
320
+ }
321
+ return (
322
+ startOfDayForEventStart <= targetDate &&
323
+ startOfDayForEventEnd >= targetDate
324
+ );
325
+ })
326
+ .map((event) => {
327
+ const eventStart = startOfDay(parseISO(event.startDate));
328
+ const eventEnd = startOfDay(parseISO(event.endDate));
329
+ let point: "start" | "end" | "none" | undefined;
330
+
331
+ if (isSameDay(eventStart, eventEnd)) {
332
+ point = "none";
333
+ } else if (isSameDay(eventStart, targetDate)) {
334
+ point = "start";
335
+ } else if (isSameDay(eventEnd, targetDate)) {
336
+ point = "end";
337
+ }
338
+
339
+ return { ...event, point };
340
+ });
341
+ };
342
+
343
+ export const getWeekDates = (date: Date): Date[] => {
344
+ const startDate = startOfWeek(date, { weekStartsOn: 1 });
345
+ return Array.from({ length: 7 }, (_, i) => addDays(startDate, i));
346
+ };
347
+
348
+ export const getEventsForWeek = (events: IEvent[], date: Date): IEvent[] => {
349
+ const weekDates = getWeekDates(date);
350
+ const startOfWeekDate = weekDates[0];
351
+ const endOfWeekDate = weekDates[6];
352
+
353
+ return events.filter((event) => {
354
+ const eventStart = parseISO(event.startDate);
355
+ const eventEnd = parseISO(event.endDate);
356
+ return (
357
+ isValid(eventStart) &&
358
+ isValid(eventEnd) &&
359
+ eventStart <= endOfWeekDate &&
360
+ eventEnd >= startOfWeekDate
361
+ );
362
+ });
363
+ };
364
+
365
+ export const getEventsForMonth = (events: IEvent[], date: Date): IEvent[] => {
366
+ const startOfMonthDate = startOfMonth(date);
367
+ const endOfMonthDate = endOfMonth(date);
368
+
369
+ return events.filter((event) => {
370
+ const eventStart = parseISO(event.startDate);
371
+ const eventEnd = parseISO(event.endDate);
372
+ return (
373
+ isValid(eventStart) &&
374
+ isValid(eventEnd) &&
375
+ eventStart <= endOfMonthDate &&
376
+ eventEnd >= startOfMonthDate
377
+ );
378
+ });
379
+ };
380
+
381
+ export const getEventsForYear = (events: IEvent[], date: Date): IEvent[] => {
382
+ if (!events || !Array.isArray(events) || !isValid(date)) return [];
383
+
384
+ const startOfYearDate = startOfYear(date);
385
+ const endOfYearDate = endOfYear(date);
386
+
387
+ return events.filter((event) => {
388
+ const eventStart = parseISO(event.startDate);
389
+ const eventEnd = parseISO(event.endDate);
390
+ return (
391
+ isValid(eventStart) &&
392
+ isValid(eventEnd) &&
393
+ eventStart <= endOfYearDate &&
394
+ eventEnd >= startOfYearDate
395
+ );
396
+ });
397
+ };
398
+
399
+ export const getColorClass = (color: string): string => {
400
+ const colorClasses: Record<TEventColor, string> = {
401
+ red: "border-red-200 bg-red-50 text-red-700 dark:border-red-800 dark:bg-red-950 dark:text-red-300",
402
+ yellow:
403
+ "border-yellow-200 bg-yellow-50 text-yellow-700 dark:border-yellow-800 dark:bg-yellow-950 dark:text-yellow-300",
404
+ green:
405
+ "border-green-200 bg-green-50 text-green-700 dark:border-green-800 dark:bg-green-950 dark:text-green-300",
406
+ blue: "border-blue-200 bg-blue-50 text-blue-700 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-300",
407
+ orange:
408
+ "border-orange-200 bg-orange-50 text-orange-700 dark:border-orange-800 dark:bg-orange-950 dark:text-orange-300",
409
+ purple:
410
+ "border-purple-200 bg-purple-50 text-purple-700 dark:border-purple-800 dark:bg-purple-950 dark:text-purple-300",
411
+ };
412
+ return colorClasses[color as TEventColor] || "";
413
+ };
414
+
415
+ export const getBgColor = (color: string): string => {
416
+ const colorClasses: Record<TEventColor, string> = {
417
+ red: "bg-red-400 dark:bg-red-600",
418
+ yellow: "bg-yellow-400 dark:bg-yellow-600",
419
+ green: "bg-green-400 dark:bg-green-600",
420
+ blue: "bg-blue-400 dark:bg-blue-600",
421
+ orange: "bg-orange-400 dark:bg-orange-600",
422
+ purple: "bg-purple-400 dark:bg-purple-600",
423
+ };
424
+ return colorClasses[color as TEventColor] || "";
425
+ };
426
+
427
+ export const useGetEventsByMode = (events: IEvent[]) => {
428
+ const { view, selectedDate } = useCalendar();
429
+
430
+ switch (view) {
431
+ case "day":
432
+ return getEventsForDay(events, selectedDate);
433
+ case "week":
434
+ return getEventsForWeek(events, selectedDate);
435
+ case "agenda":
436
+ case "month":
437
+ return getEventsForMonth(events, selectedDate);
438
+ case "year":
439
+ return getEventsForYear(events, selectedDate);
440
+ default:
441
+ return [];
442
+ }
443
+ };
444
+
445
+ export const toCapitalize = (str: string): string => {
446
+ if (!str) return "";
447
+ return str.charAt(0).toUpperCase() + str.slice(1);
448
+ };
449
+
450
+ export const useScrollPosition = () => {
451
+ const { startOfDayHour } = useCalendar();
452
+ return startOfDayHour * HOUR_HEIGHT_PX;
453
+ };
@@ -0,0 +1,152 @@
1
+ import { set as setTime } from "date-fns"
2
+ import { useCallback, useRef, useState, type PointerEvent as ReactPointerEvent } from "react"
3
+
4
+ import { HOUR_HEIGHT_PX } from "../helpers"
5
+
6
+ export type TimeRangeDraft = {
7
+ start: Date
8
+ end: Date
9
+ }
10
+
11
+ type Selection = {
12
+ day: Date
13
+ startMin: number
14
+ endMin: number
15
+ }
16
+
17
+ function clamp(n: number, min: number, max: number) {
18
+ return Math.min(max, Math.max(min, n))
19
+ }
20
+
21
+ function snapMinutes(raw: number, step = 15) {
22
+ return Math.round(raw / step) * step
23
+ }
24
+
25
+ function minutesToDate(day: Date, minutes: number) {
26
+ const h = Math.floor(minutes / 60)
27
+ const m = minutes % 60
28
+ return setTime(day, { hours: h, minutes: m, seconds: 0, milliseconds: 0 })
29
+ }
30
+
31
+ /** Drag on a day column to select a time range (Google Calendar style). */
32
+ export function useTimeRangeCreate() {
33
+ const [selection, setSelection] = useState<Selection | null>(null)
34
+ const [draft, setDraft] = useState<TimeRangeDraft | null>(null)
35
+ const selectionRef = useRef<Selection | null>(null)
36
+ const dragRef = useRef<{
37
+ day: Date
38
+ originMin: number
39
+ moved: boolean
40
+ pointerId: number
41
+ } | null>(null)
42
+ /**
43
+ * Column geometry, captured at pointerdown. While dragging, the column stays
44
+ * connected we re-read its rect (so an ancestor scroll is followed); if the
45
+ * node was replaced by a re-render we keep the last good top. A detached
46
+ * element reports an all-zero DOMRect, and `clientY - 0` counts minutes from
47
+ * the top of the viewport — which drags a few pixels into a multi-hour range.
48
+ */
49
+ const geomRef = useRef<{ el: HTMLElement; top: number } | null>(null)
50
+
51
+ const updateSelection = useCallback((next: Selection | null) => {
52
+ selectionRef.current = next
53
+ setSelection(next)
54
+ }, [])
55
+
56
+ const yToMinutes = useCallback((clientY: number) => {
57
+ const geom = geomRef.current
58
+ if (!geom) return 0
59
+ // getBoundingClientRect tracks scroll of ancestors; do not add scrollTop again.
60
+ if (geom.el.isConnected) geom.top = geom.el.getBoundingClientRect().top
61
+ return clamp(snapMinutes(((clientY - geom.top) / HOUR_HEIGHT_PX) * 60), 0, 24 * 60)
62
+ }, [])
63
+
64
+ const onPointerDown = useCallback(
65
+ (e: ReactPointerEvent<HTMLElement>, day: Date, columnEl: HTMLElement) => {
66
+ // Only primary button; ignore when starting on an existing event.
67
+ if (e.button !== 0) return
68
+ const target = e.target as HTMLElement | null
69
+ if (target?.closest("[data-calendar-event],button,a,input,textarea")) return
70
+
71
+ e.preventDefault()
72
+ columnEl.setPointerCapture?.(e.pointerId)
73
+ geomRef.current = { el: columnEl, top: columnEl.getBoundingClientRect().top }
74
+ const originMin = yToMinutes(e.clientY)
75
+ dragRef.current = { day, originMin, moved: false, pointerId: e.pointerId }
76
+ updateSelection({ day, startMin: originMin, endMin: originMin + 30 })
77
+ },
78
+ [updateSelection, yToMinutes],
79
+ )
80
+
81
+ const onPointerMove = useCallback(
82
+ (e: ReactPointerEvent<HTMLElement>) => {
83
+ const drag = dragRef.current
84
+ if (!drag || !geomRef.current) return
85
+ const mins = yToMinutes(e.clientY)
86
+ if (Math.abs(mins - drag.originMin) >= 15) drag.moved = true
87
+ const startMin = Math.min(drag.originMin, mins)
88
+ const endMin = Math.max(drag.originMin, mins)
89
+ updateSelection({
90
+ day: drag.day,
91
+ startMin,
92
+ endMin: endMin === startMin ? startMin + 30 : endMin,
93
+ })
94
+ },
95
+ [updateSelection, yToMinutes],
96
+ )
97
+
98
+ const finish = useCallback(() => {
99
+ const drag = dragRef.current
100
+ const sel = selectionRef.current
101
+ dragRef.current = null
102
+ geomRef.current = null
103
+ updateSelection(null)
104
+ if (!drag || !sel) return
105
+
106
+ const startMin = Math.min(sel.startMin, sel.endMin)
107
+ let endMin = Math.max(sel.startMin, sel.endMin)
108
+ // Click without drag → default 30 minutes.
109
+ if (!drag.moved || endMin - startMin < 15) endMin = startMin + 30
110
+ if (endMin > 24 * 60) endMin = 24 * 60
111
+
112
+ setDraft({
113
+ start: minutesToDate(sel.day, startMin),
114
+ end: minutesToDate(sel.day, endMin),
115
+ })
116
+ }, [updateSelection])
117
+
118
+ const onPointerUp = useCallback(
119
+ (e: ReactPointerEvent<HTMLElement>) => {
120
+ if (!dragRef.current) return
121
+ try {
122
+ ;(e.currentTarget as HTMLElement).releasePointerCapture?.(e.pointerId)
123
+ } catch {
124
+ /* ignore */
125
+ }
126
+ finish()
127
+ },
128
+ [finish],
129
+ )
130
+
131
+ const clearDraft = useCallback(() => setDraft(null), [])
132
+
133
+ const overlayStyle =
134
+ selection == null
135
+ ? null
136
+ : {
137
+ top: (Math.min(selection.startMin, selection.endMin) / 60) * HOUR_HEIGHT_PX,
138
+ height:
139
+ (Math.abs(selection.endMin - selection.startMin) / 60) * HOUR_HEIGHT_PX ||
140
+ HOUR_HEIGHT_PX / 2,
141
+ }
142
+
143
+ return {
144
+ selection,
145
+ draft,
146
+ clearDraft,
147
+ overlayStyle,
148
+ onPointerDown,
149
+ onPointerMove,
150
+ onPointerUp,
151
+ }
152
+ }
@@ -0,0 +1,70 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ export function useDisclosure({
4
+ defaultIsOpen = false,
5
+ }: {
6
+ defaultIsOpen?: boolean;
7
+ } = {}) {
8
+ const [isOpen, setIsOpen] = useState(defaultIsOpen);
9
+
10
+ const onOpen = () => setIsOpen(true);
11
+ const onClose = () => setIsOpen(false);
12
+ const onToggle = () => setIsOpen((currentValue) => !currentValue);
13
+
14
+ return { onOpen, onClose, isOpen, onToggle };
15
+ }
16
+
17
+ export const useLocalStorage = <T>(
18
+ key: string,
19
+ initialValue: T,
20
+ ): [T, (value: T | ((prev: T) => T)) => void] => {
21
+ const readValue = (): T => {
22
+ if (typeof window === "undefined") {
23
+ return initialValue;
24
+ }
25
+
26
+ try {
27
+ const item = window.localStorage.getItem(key);
28
+ return item ? (JSON.parse(item) as T) : initialValue;
29
+ } catch (error) {
30
+ console.warn(`Error reading localStorage key "${key}":`, error);
31
+ return initialValue;
32
+ }
33
+ };
34
+
35
+ const [storedValue, setStoredValue] = useState<T>(readValue);
36
+
37
+ const setValue = (value: T | ((prev: T) => T)) => {
38
+ try {
39
+ const valueToStore =
40
+ value instanceof Function ? value(storedValue) : value;
41
+ setStoredValue(valueToStore);
42
+ if (typeof window !== "undefined") {
43
+ window.localStorage.setItem(key, JSON.stringify(valueToStore));
44
+ }
45
+ } catch (error) {
46
+ console.warn(`Error setting localStorage key "${key}":`, error);
47
+ }
48
+ };
49
+
50
+ return [storedValue, setValue];
51
+ };
52
+
53
+ export function useMediaQuery(query: string): boolean {
54
+ const [matches, setMatches] = useState(false);
55
+
56
+ useEffect(() => {
57
+ if (typeof window.matchMedia !== "function") return;
58
+ const media = window.matchMedia(query);
59
+ if (media.matches !== matches) {
60
+ setMatches(media.matches);
61
+ }
62
+
63
+ const listener = () => setMatches(media.matches);
64
+ media.addEventListener("change", listener);
65
+
66
+ return () => media.removeEventListener("change", listener);
67
+ }, [matches, query]);
68
+
69
+ return matches;
70
+ }
@@ -0,0 +1,23 @@
1
+ import type { TEventColor } from "./types";
2
+
3
+ export interface IUser {
4
+ id: string;
5
+ name: string;
6
+ picturePath: string | null;
7
+ }
8
+
9
+ export interface IEvent {
10
+ id: number;
11
+ startDate: string;
12
+ endDate: string;
13
+ title: string;
14
+ color: TEventColor;
15
+ description: string;
16
+ user: IUser;
17
+ }
18
+
19
+ export interface ICalendarCell {
20
+ day: number;
21
+ currentMonth: boolean;
22
+ date: Date;
23
+ }