@monkey-mini-app/ui 0.1.1 → 0.1.4
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.
- package/README.md +12 -6
- package/dist/globals.css +1 -1
- package/dist/index.js +12 -2
- package/dist/manifest.json +6 -6
- package/dist/runtime.js +9 -0
- package/dist/sdk.js +158 -0
- package/dist/src/blocks/activity-feed.tsx +7 -0
- package/dist/src/blocks/app-shell.tsx +1 -0
- package/dist/src/blocks/attachment-gallery.tsx +7 -0
- package/dist/src/blocks/bar-chart.tsx +11 -0
- package/dist/src/blocks/comment-thread.tsx +7 -0
- package/dist/src/blocks/dashboard-shell.tsx +109 -0
- package/dist/src/blocks/description-list.tsx +7 -0
- package/dist/src/blocks/detail-panel.tsx +1 -0
- package/dist/src/blocks/donut-chart.tsx +7 -0
- package/dist/src/blocks/env-badge.tsx +7 -0
- package/dist/src/blocks/env-table.tsx +316 -0
- package/dist/src/blocks/file-tree.tsx +7 -0
- package/dist/src/blocks/filter-bar.tsx +1 -0
- package/dist/src/blocks/form-sheet.tsx +160 -0
- package/dist/src/blocks/gauge.tsx +7 -0
- package/dist/src/blocks/list-detail.tsx +121 -0
- package/dist/src/blocks/notification-center.tsx +7 -0
- package/dist/src/blocks/page-header.tsx +1 -0
- package/dist/src/blocks/progress-ring.tsx +7 -0
- package/dist/src/blocks/radar-chart.tsx +7 -0
- package/dist/src/blocks/request-inspector.tsx +7 -0
- package/dist/src/blocks/reveal.tsx +56 -0
- package/dist/src/blocks/run-timeline.tsx +7 -0
- package/dist/src/blocks/scrollspy.tsx +7 -0
- package/dist/src/blocks/settings-split.tsx +154 -0
- package/dist/src/blocks/severity-chip.tsx +7 -0
- package/dist/src/blocks/sparkline.tsx +7 -0
- package/dist/src/blocks/stat-card.tsx +1 -0
- package/dist/src/blocks/status-badge.tsx +1 -0
- package/dist/src/blocks/table-page.tsx +83 -0
- package/dist/src/blocks/terminal.tsx +21 -3
- package/dist/src/blocks/test-step-list.tsx +7 -0
- package/dist/src/blocks/trend-card.tsx +7 -0
- package/dist/src/blocks/wizard-shell.tsx +132 -0
- package/dist/src/cdn-modules.d.ts +5 -0
- package/dist/src/components/accordion.tsx +7 -0
- package/dist/src/components/alert-dialog.tsx +7 -0
- package/dist/src/components/alert.tsx +7 -0
- package/dist/src/components/aspect-ratio.tsx +4 -0
- package/dist/src/components/attachment.tsx +4 -0
- package/dist/src/components/avatar.tsx +4 -0
- package/dist/src/components/badge.tsx +7 -0
- package/dist/src/components/breadcrumb.tsx +4 -0
- package/dist/src/components/bubble.tsx +4 -0
- package/dist/src/components/button-group.tsx +6 -0
- package/dist/src/components/button.tsx +7 -0
- package/dist/src/components/calendar.tsx +4 -0
- package/dist/src/components/card.tsx +8 -1
- package/dist/src/components/chart.tsx +7 -0
- package/dist/src/components/checkbox.tsx +5 -0
- package/dist/src/components/collapsible.tsx +4 -0
- package/dist/src/components/combobox.tsx +4 -0
- package/dist/src/components/command.tsx +6 -0
- package/dist/src/components/context-menu.tsx +4 -0
- package/dist/src/components/dialog.tsx +21 -0
- package/dist/src/components/drawer.tsx +4 -0
- package/dist/src/components/dropdown-menu.tsx +7 -0
- package/dist/src/components/empty.tsx +7 -0
- package/dist/src/components/field.tsx +6 -0
- package/dist/src/components/hover-card.tsx +4 -0
- package/dist/src/components/input-group.tsx +4 -0
- package/dist/src/components/input.tsx +7 -0
- package/dist/src/components/item.tsx +4 -0
- package/dist/src/components/kbd.tsx +4 -0
- package/dist/src/components/label.tsx +7 -0
- package/dist/src/components/marker.tsx +4 -0
- package/dist/src/components/menubar.tsx +4 -0
- package/dist/src/components/message-scroller.tsx +4 -0
- package/dist/src/components/message.tsx +4 -0
- package/dist/src/components/native-select.tsx +4 -0
- package/dist/src/components/navigation-menu.tsx +4 -0
- package/dist/src/components/pagination.tsx +4 -0
- package/dist/src/components/popover.tsx +7 -0
- package/dist/src/components/progress.tsx +7 -0
- package/dist/src/components/questionnaire.tsx +4 -0
- package/dist/src/components/radio-group.tsx +4 -0
- package/dist/src/components/resizable.tsx +7 -0
- package/dist/src/components/scroll-area.tsx +4 -0
- package/dist/src/components/select.tsx +7 -0
- package/dist/src/components/separator.tsx +5 -0
- package/dist/src/components/sheet.tsx +22 -0
- package/dist/src/components/sidebar.tsx +4 -0
- package/dist/src/components/skeleton.tsx +7 -0
- package/dist/src/components/slider.tsx +4 -0
- package/dist/src/components/spinner.tsx +6 -0
- package/dist/src/components/switch.tsx +7 -0
- package/dist/src/components/table.tsx +7 -0
- package/dist/src/components/tabs.tsx +7 -0
- package/dist/src/components/textarea.tsx +7 -0
- package/dist/src/components/toast.tsx +7 -0
- package/dist/src/components/toggle-group.tsx +4 -0
- package/dist/src/components/toggle.tsx +4 -0
- package/dist/src/components/tooltip.tsx +7 -0
- package/dist/src/composites/autocomplete.tsx +7 -0
- package/dist/src/composites/cascader.tsx +7 -0
- package/dist/src/composites/color-picker.tsx +7 -0
- package/dist/src/composites/confirm-dialog.tsx +7 -0
- package/dist/src/composites/copyable.tsx +9 -1
- package/dist/src/composites/currency-input.tsx +7 -0
- package/dist/src/composites/date-picker.tsx +7 -0
- package/dist/src/composites/date-range-picker.tsx +4 -0
- package/dist/src/composites/date-time-picker.tsx +7 -0
- package/dist/src/composites/date-time-range-picker.tsx +7 -0
- package/dist/src/composites/duration-input.tsx +7 -0
- package/dist/src/composites/mini-calendar.tsx +7 -0
- package/dist/src/composites/number-field.tsx +7 -0
- package/dist/src/composites/password-field.tsx +7 -0
- package/dist/src/composites/phone-input.tsx +7 -0
- package/dist/src/composites/rating.tsx +7 -0
- package/dist/src/composites/relative-date-picker.tsx +7 -0
- package/dist/src/composites/search-input.tsx +7 -0
- package/dist/src/composites/slider-range.tsx +7 -0
- package/dist/src/composites/tag-input.tsx +7 -0
- package/dist/src/composites/time-picker.tsx +11 -1
- package/dist/src/composites/time-range-picker.tsx +7 -0
- package/dist/src/composites/timezone-select.tsx +7 -0
- package/dist/src/composites/transfer.tsx +7 -0
- package/dist/src/composites/user-picker.tsx +7 -0
- package/dist/src/hooks/use-html-dark.ts +37 -0
- package/dist/src/hooks/use-mobile.ts +3 -0
- package/dist/src/i18n/en.ts +109 -2
- package/dist/src/i18n/zh.ts +99 -2
- package/dist/src/index.ts +147 -134
- package/dist/src/lib/clipboard.ts +10 -0
- package/dist/src/lib/illustrations.tsx +2 -2
- package/dist/src/products/code-block.tsx +35 -18
- package/dist/src/products/code-editor.tsx +156 -24
- package/dist/src/products/commit-graph.tsx +644 -0
- package/dist/src/products/data-grid.tsx +2 -1
- package/dist/src/products/diff-viewer.tsx +104 -8
- package/dist/src/products/event-calendar.tsx +28 -823
- package/dist/src/products/file-dropzone.tsx +7 -0
- package/dist/src/products/full-calendar/animations.ts +38 -0
- package/dist/src/products/full-calendar/calendar-body.tsx +77 -0
- package/dist/src/products/full-calendar/constants.ts +10 -0
- package/dist/src/products/full-calendar/contexts/calendar-context.tsx +255 -0
- package/dist/src/products/full-calendar/contexts/dnd-context.tsx +157 -0
- package/dist/src/products/full-calendar/dialogs/add-edit-event-dialog.tsx +187 -0
- package/dist/src/products/full-calendar/dialogs/delete-event-dialog.tsx +39 -0
- package/dist/src/products/full-calendar/dialogs/event-details-dialog.tsx +100 -0
- package/dist/src/products/full-calendar/dialogs/events-list-dialog.tsx +87 -0
- package/dist/src/products/full-calendar/dnd/draggable-event.tsx +45 -0
- package/dist/src/products/full-calendar/dnd/droppable-area.tsx +45 -0
- package/dist/src/products/full-calendar/dnd/resizable-event.tsx +179 -0
- package/dist/src/products/full-calendar/header/calendar-header.tsx +65 -0
- package/dist/src/products/full-calendar/header/date-navigator.tsx +110 -0
- package/dist/src/products/full-calendar/header/filter.tsx +58 -0
- package/dist/src/products/full-calendar/header/today-button.tsx +45 -0
- package/dist/src/products/full-calendar/header/user-select.tsx +64 -0
- package/dist/src/products/full-calendar/header/view-tabs.tsx +36 -0
- package/dist/src/products/full-calendar/helpers.ts +453 -0
- package/dist/src/products/full-calendar/hooks/use-time-range-create.ts +152 -0
- package/dist/src/products/full-calendar/hooks.ts +70 -0
- package/dist/src/products/full-calendar/interfaces.ts +23 -0
- package/dist/src/products/full-calendar/settings/settings.tsx +80 -0
- package/dist/src/products/full-calendar/shims/avatar-group.tsx +16 -0
- package/dist/src/products/full-calendar/shims/day-picker.tsx +1 -0
- package/dist/src/products/full-calendar/shims/motion.tsx +97 -0
- package/dist/src/products/full-calendar/shims/next-themes.ts +6 -0
- package/dist/src/products/full-calendar/shims/re-resizable.tsx +25 -0
- package/dist/src/products/full-calendar/shims/sonner.ts +4 -0
- package/dist/src/products/full-calendar/types.ts +8 -0
- package/dist/src/products/full-calendar/views/agenda-view/agenda-events.tsx +140 -0
- package/dist/src/products/full-calendar/views/month-view/calendar-month-view.tsx +69 -0
- package/dist/src/products/full-calendar/views/month-view/day-cell.tsx +201 -0
- package/dist/src/products/full-calendar/views/month-view/event-bullet.tsx +40 -0
- package/dist/src/products/full-calendar/views/month-view/month-event-badge.tsx +143 -0
- package/dist/src/products/full-calendar/views/week-and-day-view/calendar-day-view.tsx +295 -0
- package/dist/src/products/full-calendar/views/week-and-day-view/calendar-time-line.tsx +35 -0
- package/dist/src/products/full-calendar/views/week-and-day-view/calendar-week-view.tsx +253 -0
- package/dist/src/products/full-calendar/views/week-and-day-view/day-view-multi-day-events-row.tsx +73 -0
- package/dist/src/products/full-calendar/views/week-and-day-view/event-block.tsx +114 -0
- package/dist/src/products/full-calendar/views/week-and-day-view/render-grouped-events.tsx +49 -0
- package/dist/src/products/full-calendar/views/week-and-day-view/week-view-multi-day-events-row.tsx +143 -0
- package/dist/src/products/full-calendar/views/year-view/calendar-year-view.tsx +161 -0
- package/dist/src/products/gantt.tsx +7 -0
- package/dist/src/products/jira-wiki.tsx +1 -0
- package/dist/src/products/jql-input.tsx +340 -37
- package/dist/src/products/json-viewer.tsx +7 -0
- package/dist/src/products/kanban-issue-panel.tsx +7 -0
- package/dist/src/products/kanban.tsx +1 -0
- package/dist/src/products/log-viewer.tsx +390 -18
- package/dist/src/products/markdown-editor.tsx +21 -6
- package/dist/src/products/markdown.tsx +7 -0
- package/dist/src/products/rich-text-editor.tsx +65 -59
- package/dist/src/products/sortable-list.tsx +7 -0
- package/dist/src/products/stepper.tsx +219 -70
- package/dist/src/products/timeline.tsx +7 -0
- package/dist/src/products/tree-view.tsx +7 -0
- package/dist/src/sdk/app-error-boundary.tsx +240 -0
- package/dist/src/sdk/app-id.ts +7 -0
- package/dist/src/sdk/use-app.ts +126 -0
- package/dist/src/styles/globals.css +1 -0
- package/dist/vendors/lodash.js +20 -0
- package/dist/vendors/motion.js +12 -0
- package/package.json +20 -19
- 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
|
+
}
|