@hectorbliss/denik-calendar 0.0.1
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 +126 -0
- package/dist/index.cjs +585 -0
- package/dist/index.d.cts +129 -0
- package/dist/index.d.ts +129 -0
- package/dist/index.js +568 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# denik-calendar
|
|
2
|
+
|
|
3
|
+
A **React** drag-and-drop week calendar component with overlap detection.
|
|
4
|
+
|
|
5
|
+
Built with [@dnd-kit](https://dndkit.com/) for smooth drag interactions.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install denik-calendar
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { Calendar } from "denik-calendar";
|
|
17
|
+
|
|
18
|
+
function App() {
|
|
19
|
+
const [events, setEvents] = useState([
|
|
20
|
+
{ id: "1", start: new Date(), duration: 60, title: "Meeting" },
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Calendar
|
|
25
|
+
events={events}
|
|
26
|
+
onEventMove={(eventId, newStart) => {
|
|
27
|
+
// Handle event move
|
|
28
|
+
setEvents(prev =>
|
|
29
|
+
prev.map(e => (e.id === eventId ? { ...e, start: newStart } : e))
|
|
30
|
+
);
|
|
31
|
+
}}
|
|
32
|
+
onAddBlock={(start) => {
|
|
33
|
+
// Handle block creation
|
|
34
|
+
}}
|
|
35
|
+
onRemoveBlock={(eventId) => {
|
|
36
|
+
// Handle block removal
|
|
37
|
+
}}
|
|
38
|
+
onEventClick={(event) => {
|
|
39
|
+
// Handle event click
|
|
40
|
+
}}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Headless Hook
|
|
47
|
+
|
|
48
|
+
Use the overlap detection logic without the UI:
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import { useEventOverlap } from "denik-calendar";
|
|
52
|
+
|
|
53
|
+
function MyCustomCalendar({ events }) {
|
|
54
|
+
const { canMove, hasOverlap, findConflicts } = useEventOverlap(events);
|
|
55
|
+
|
|
56
|
+
const handleDrop = (eventId, newStart) => {
|
|
57
|
+
if (canMove(eventId, newStart)) {
|
|
58
|
+
// Safe to move
|
|
59
|
+
updateEvent(eventId, newStart);
|
|
60
|
+
} else {
|
|
61
|
+
// Conflict detected
|
|
62
|
+
toast.error("Time slot occupied");
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Props
|
|
69
|
+
|
|
70
|
+
### Calendar
|
|
71
|
+
|
|
72
|
+
| Prop | Type | Description |
|
|
73
|
+
|------|------|-------------|
|
|
74
|
+
| `events` | `CalendarEvent[]` | Array of events to display |
|
|
75
|
+
| `date` | `Date` | Current week to display (default: today) |
|
|
76
|
+
| `onEventMove` | `(eventId, newStart) => void` | Called when event is dragged |
|
|
77
|
+
| `onAddBlock` | `(start) => void` | Called when creating a block |
|
|
78
|
+
| `onRemoveBlock` | `(eventId) => void` | Called when removing a block |
|
|
79
|
+
| `onEventClick` | `(event) => void` | Called when clicking an event |
|
|
80
|
+
| `onNewEvent` | `(start) => void` | Called when clicking empty slot |
|
|
81
|
+
| `config` | `CalendarConfig` | Configuration options |
|
|
82
|
+
|
|
83
|
+
### CalendarEvent
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
interface CalendarEvent {
|
|
87
|
+
id: string;
|
|
88
|
+
start: Date;
|
|
89
|
+
duration: number; // minutes
|
|
90
|
+
title?: string;
|
|
91
|
+
type?: "BLOCK" | "EVENT";
|
|
92
|
+
service?: { name: string };
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### CalendarConfig
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
interface CalendarConfig {
|
|
100
|
+
locale?: string; // default: "es-MX"
|
|
101
|
+
icons?: {
|
|
102
|
+
trash?: ReactNode;
|
|
103
|
+
edit?: ReactNode;
|
|
104
|
+
close?: ReactNode;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Features
|
|
110
|
+
|
|
111
|
+
- Drag & drop events between time slots
|
|
112
|
+
- Automatic overlap detection
|
|
113
|
+
- Block time slots
|
|
114
|
+
- Week navigation
|
|
115
|
+
- Auto-scroll to current hour
|
|
116
|
+
- Customizable icons
|
|
117
|
+
- Locale support
|
|
118
|
+
- TypeScript support
|
|
119
|
+
|
|
120
|
+
## Peer Dependencies
|
|
121
|
+
|
|
122
|
+
- React 18+ or 19+
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var core = require('@dnd-kit/core');
|
|
5
|
+
var utilities = require('@dnd-kit/utilities');
|
|
6
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
7
|
+
|
|
8
|
+
// src/Calendar.tsx
|
|
9
|
+
function useEventOverlap(events) {
|
|
10
|
+
const hasOverlap = react.useCallback(
|
|
11
|
+
(start, duration, excludeId) => {
|
|
12
|
+
const hour = start.getHours() + start.getMinutes() / 60;
|
|
13
|
+
const endHour = hour + duration / 60;
|
|
14
|
+
return events.some((existing) => {
|
|
15
|
+
if (excludeId && existing.id === excludeId) return false;
|
|
16
|
+
const existingStart = new Date(existing.start);
|
|
17
|
+
if (existingStart.getDate() !== start.getDate() || existingStart.getMonth() !== start.getMonth() || existingStart.getFullYear() !== start.getFullYear()) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const existingHour = existingStart.getHours() + existingStart.getMinutes() / 60;
|
|
21
|
+
const existingEnd = existingHour + existing.duration / 60;
|
|
22
|
+
return hour >= existingHour && hour < existingEnd || endHour > existingHour && endHour <= existingEnd || hour <= existingHour && endHour >= existingEnd;
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
[events]
|
|
26
|
+
);
|
|
27
|
+
const findConflicts = react.useCallback(
|
|
28
|
+
(start, duration, excludeId) => {
|
|
29
|
+
const hour = start.getHours() + start.getMinutes() / 60;
|
|
30
|
+
const endHour = hour + duration / 60;
|
|
31
|
+
return events.filter((existing) => {
|
|
32
|
+
if (excludeId && existing.id === excludeId) return false;
|
|
33
|
+
const existingStart = new Date(existing.start);
|
|
34
|
+
if (existingStart.getDate() !== start.getDate() || existingStart.getMonth() !== start.getMonth() || existingStart.getFullYear() !== start.getFullYear()) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const existingHour = existingStart.getHours() + existingStart.getMinutes() / 60;
|
|
38
|
+
const existingEnd = existingHour + existing.duration / 60;
|
|
39
|
+
return hour >= existingHour && hour < existingEnd || endHour > existingHour && endHour <= existingEnd || hour <= existingHour && endHour >= existingEnd;
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
[events]
|
|
43
|
+
);
|
|
44
|
+
const canMove = react.useCallback(
|
|
45
|
+
(eventId, newStart) => {
|
|
46
|
+
const event = events.find((e) => e.id === eventId);
|
|
47
|
+
if (!event) return false;
|
|
48
|
+
return !hasOverlap(newStart, event.duration, eventId);
|
|
49
|
+
},
|
|
50
|
+
[events, hasOverlap]
|
|
51
|
+
);
|
|
52
|
+
const getEventsForDay = react.useMemo(() => {
|
|
53
|
+
return (date) => {
|
|
54
|
+
return events.filter((event) => {
|
|
55
|
+
const eventDate = new Date(event.start);
|
|
56
|
+
return eventDate.getDate() === date.getDate() && eventDate.getMonth() === date.getMonth() && eventDate.getFullYear() === date.getFullYear();
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
}, [events]);
|
|
60
|
+
return {
|
|
61
|
+
hasOverlap,
|
|
62
|
+
findConflicts,
|
|
63
|
+
canMove,
|
|
64
|
+
getEventsForDay
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function useClickOutside({
|
|
68
|
+
isActive,
|
|
69
|
+
onOutsideClick
|
|
70
|
+
}) {
|
|
71
|
+
const ref = react.useRef(null);
|
|
72
|
+
react.useEffect(() => {
|
|
73
|
+
if (!isActive) return;
|
|
74
|
+
const handleClickOutside = (event) => {
|
|
75
|
+
if (ref.current && !ref.current.contains(event.target)) {
|
|
76
|
+
onOutsideClick();
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
80
|
+
return () => {
|
|
81
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
82
|
+
};
|
|
83
|
+
}, [isActive, onOutsideClick]);
|
|
84
|
+
return ref;
|
|
85
|
+
}
|
|
86
|
+
function formatDate(date, locale = "es-MX") {
|
|
87
|
+
return new Date(date).toLocaleDateString(locale, {
|
|
88
|
+
weekday: "long",
|
|
89
|
+
year: "numeric",
|
|
90
|
+
month: "long",
|
|
91
|
+
day: "numeric",
|
|
92
|
+
hour: "2-digit",
|
|
93
|
+
minute: "2-digit"
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/utils.ts
|
|
98
|
+
var getMonday = (today = /* @__PURE__ */ new Date()) => {
|
|
99
|
+
const d = new Date(today);
|
|
100
|
+
const day = d.getDay();
|
|
101
|
+
const diff = d.getDate() - day + (day === 0 ? -6 : 1);
|
|
102
|
+
return new Date(d.setDate(diff));
|
|
103
|
+
};
|
|
104
|
+
var completeWeek = (date) => {
|
|
105
|
+
const startDate = new Date(date);
|
|
106
|
+
const day = startDate.getDay();
|
|
107
|
+
const offset = day === 0 ? -6 : 1 - day;
|
|
108
|
+
startDate.setDate(startDate.getDate() + offset);
|
|
109
|
+
return Array.from({ length: 7 }, (_, i) => {
|
|
110
|
+
const d = new Date(startDate);
|
|
111
|
+
d.setDate(d.getDate() + i);
|
|
112
|
+
return d;
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
var generateHours = ({
|
|
116
|
+
fromHour,
|
|
117
|
+
toHour
|
|
118
|
+
}) => {
|
|
119
|
+
return Array.from({ length: toHour - fromHour }, (_, index) => {
|
|
120
|
+
const hour = fromHour + index;
|
|
121
|
+
return hour < 10 ? `0${hour}:00` : `${hour}:00`;
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
var isToday = (date) => {
|
|
125
|
+
const today = /* @__PURE__ */ new Date();
|
|
126
|
+
return date.getDate() === today.getDate() && date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear();
|
|
127
|
+
};
|
|
128
|
+
var areSameDates = (d1, d2) => {
|
|
129
|
+
if (!d1 || !d2) return false;
|
|
130
|
+
return d1.getDate() === d2.getDate() && d1.getMonth() === d2.getMonth() && d1.getFullYear() === d2.getFullYear();
|
|
131
|
+
};
|
|
132
|
+
var addDaysToDate = (date, days) => {
|
|
133
|
+
const result = new Date(date);
|
|
134
|
+
result.setDate(result.getDate() + days);
|
|
135
|
+
return result;
|
|
136
|
+
};
|
|
137
|
+
var addMinutesToDate = (date, mins) => {
|
|
138
|
+
const result = new Date(date);
|
|
139
|
+
result.setMinutes(result.getMinutes() + mins);
|
|
140
|
+
return result;
|
|
141
|
+
};
|
|
142
|
+
var fromMinsToTimeString = (mins) => {
|
|
143
|
+
const h = Math.floor(mins / 60);
|
|
144
|
+
const m = mins % 60;
|
|
145
|
+
return `${h < 10 ? "0" + h : h}:${m < 10 ? "0" + m : m}`;
|
|
146
|
+
};
|
|
147
|
+
var fromMinsToLocaleTimeString = (mins, locale = "es-MX") => {
|
|
148
|
+
const h = Math.floor(mins / 60);
|
|
149
|
+
const m = mins % 60;
|
|
150
|
+
const today = /* @__PURE__ */ new Date();
|
|
151
|
+
today.setHours(h, m, 0, 0);
|
|
152
|
+
return today.toLocaleTimeString(locale);
|
|
153
|
+
};
|
|
154
|
+
var fromDateToTimeString = (date, locale = "es-MX") => {
|
|
155
|
+
return new Date(date).toLocaleTimeString(locale);
|
|
156
|
+
};
|
|
157
|
+
var getDaysInMonth = (date) => {
|
|
158
|
+
const firstDay = new Date(date.getFullYear(), date.getMonth(), 1);
|
|
159
|
+
const lastDay = new Date(date.getFullYear(), date.getMonth() + 1, 0);
|
|
160
|
+
const numberOfMissing = 6 - lastDay.getDay();
|
|
161
|
+
const leftOffset = firstDay.getDay();
|
|
162
|
+
firstDay.setDate(firstDay.getDate() - leftOffset);
|
|
163
|
+
const days = [];
|
|
164
|
+
days.push(new Date(firstDay));
|
|
165
|
+
while (firstDay < lastDay) {
|
|
166
|
+
firstDay.setDate(firstDay.getDate() + 1);
|
|
167
|
+
days.push(new Date(firstDay));
|
|
168
|
+
}
|
|
169
|
+
for (let i = 0; i < numberOfMissing; i++) {
|
|
170
|
+
firstDay.setDate(firstDay.getDate() + 1);
|
|
171
|
+
days.push(new Date(firstDay));
|
|
172
|
+
}
|
|
173
|
+
return days;
|
|
174
|
+
};
|
|
175
|
+
var cn = (...classes) => classes.filter(Boolean).join(" ");
|
|
176
|
+
var DefaultTrashIcon = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" }) });
|
|
177
|
+
var DefaultEditIcon = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" }) });
|
|
178
|
+
var DefaultCloseIcon = () => /* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "currentColor", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" }) });
|
|
179
|
+
var DayHeader = ({ date, locale }) => /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "grid place-items-center", children: [
|
|
180
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "capitalize", children: date.toLocaleDateString(locale, { weekday: "short" }) }),
|
|
181
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
182
|
+
"span",
|
|
183
|
+
{
|
|
184
|
+
className: cn(
|
|
185
|
+
isToday(date) && "bg-blue-500 rounded-full p-1 text-white"
|
|
186
|
+
),
|
|
187
|
+
children: date.getDate()
|
|
188
|
+
}
|
|
189
|
+
)
|
|
190
|
+
] });
|
|
191
|
+
function Calendar({
|
|
192
|
+
date = /* @__PURE__ */ new Date(),
|
|
193
|
+
events = [],
|
|
194
|
+
onEventClick,
|
|
195
|
+
onNewEvent,
|
|
196
|
+
onEventMove,
|
|
197
|
+
onAddBlock,
|
|
198
|
+
onRemoveBlock,
|
|
199
|
+
config = {}
|
|
200
|
+
}) {
|
|
201
|
+
const { locale = "es-MX", icons = {} } = config;
|
|
202
|
+
const week = completeWeek(date);
|
|
203
|
+
const [activeId, setActiveId] = react.useState(null);
|
|
204
|
+
const { canMove } = useEventOverlap(events);
|
|
205
|
+
const sensors = core.useSensors(
|
|
206
|
+
core.useSensor(core.PointerSensor, {
|
|
207
|
+
activationConstraint: { distance: 8 }
|
|
208
|
+
}),
|
|
209
|
+
core.useSensor(core.KeyboardSensor)
|
|
210
|
+
);
|
|
211
|
+
const handleDragStart = (event) => {
|
|
212
|
+
setActiveId(event.active.id);
|
|
213
|
+
};
|
|
214
|
+
const handleDragEnd = (event) => {
|
|
215
|
+
const { active, over } = event;
|
|
216
|
+
setActiveId(null);
|
|
217
|
+
if (!over) return;
|
|
218
|
+
const eventId = active.id.toString().replace("event-", "");
|
|
219
|
+
const [, dayIndexStr, hourStr] = over.id.toString().split("-");
|
|
220
|
+
const dayIndex = parseInt(dayIndexStr);
|
|
221
|
+
const hour = parseInt(hourStr);
|
|
222
|
+
const targetDay = week[dayIndex];
|
|
223
|
+
const newStart = new Date(targetDay);
|
|
224
|
+
newStart.setHours(hour, 0, 0, 0);
|
|
225
|
+
const movedEvent = events.find((e) => e.id === eventId);
|
|
226
|
+
if (!movedEvent) return;
|
|
227
|
+
const currentStart = new Date(movedEvent.start);
|
|
228
|
+
if (currentStart.getDate() === newStart.getDate() && currentStart.getHours() === newStart.getHours()) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (!canMove(eventId, newStart)) {
|
|
232
|
+
console.warn("Cannot move event: time slot is occupied");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
onEventMove?.(eventId, newStart);
|
|
236
|
+
};
|
|
237
|
+
const handleDragCancel = () => {
|
|
238
|
+
setActiveId(null);
|
|
239
|
+
};
|
|
240
|
+
const activeEvent = activeId ? events.find((e) => `event-${e.id}` === activeId) : null;
|
|
241
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
242
|
+
core.DndContext,
|
|
243
|
+
{
|
|
244
|
+
sensors,
|
|
245
|
+
collisionDetection: core.closestCenter,
|
|
246
|
+
onDragStart: handleDragStart,
|
|
247
|
+
onDragEnd: handleDragEnd,
|
|
248
|
+
onDragCancel: handleDragCancel,
|
|
249
|
+
children: [
|
|
250
|
+
/* @__PURE__ */ jsxRuntime.jsxs("article", { className: "w-full bg-white shadow rounded-xl", children: [
|
|
251
|
+
/* @__PURE__ */ jsxRuntime.jsxs("section", { className: "grid grid-cols-8 place-items-center py-4", children: [
|
|
252
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-gray-500", children: Intl.DateTimeFormat().resolvedOptions().timeZone }) }),
|
|
253
|
+
week.map((day) => /* @__PURE__ */ jsxRuntime.jsx(DayHeader, { date: day, locale }, day.toISOString()))
|
|
254
|
+
] }),
|
|
255
|
+
/* @__PURE__ */ jsxRuntime.jsxs("section", { className: "grid grid-cols-8 max-h-[80vh] overflow-y-auto", children: [
|
|
256
|
+
/* @__PURE__ */ jsxRuntime.jsx(TimeColumn, {}),
|
|
257
|
+
week.map((dayOfWeek, dayIndex) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
258
|
+
Column,
|
|
259
|
+
{
|
|
260
|
+
dayIndex,
|
|
261
|
+
dayOfWeek,
|
|
262
|
+
events: events.filter((event) => {
|
|
263
|
+
const eventDate = new Date(event.start);
|
|
264
|
+
return eventDate.getDate() === dayOfWeek.getDate() && eventDate.getMonth() === dayOfWeek.getMonth();
|
|
265
|
+
}),
|
|
266
|
+
onNewEvent,
|
|
267
|
+
onAddBlock,
|
|
268
|
+
onRemoveBlock,
|
|
269
|
+
onEventClick,
|
|
270
|
+
locale,
|
|
271
|
+
icons
|
|
272
|
+
},
|
|
273
|
+
dayOfWeek.toISOString()
|
|
274
|
+
))
|
|
275
|
+
] })
|
|
276
|
+
] }),
|
|
277
|
+
/* @__PURE__ */ jsxRuntime.jsx(core.DragOverlay, { children: activeEvent ? /* @__PURE__ */ jsxRuntime.jsx(EventOverlay, { event: activeEvent }) : null })
|
|
278
|
+
]
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
var Cell = ({
|
|
283
|
+
date,
|
|
284
|
+
hours,
|
|
285
|
+
children,
|
|
286
|
+
onClick,
|
|
287
|
+
className,
|
|
288
|
+
dayIndex
|
|
289
|
+
}) => {
|
|
290
|
+
const isTodayCell = date ? isToday(date) : false;
|
|
291
|
+
const isThisHour = isTodayCell && hours === (/* @__PURE__ */ new Date()).getHours();
|
|
292
|
+
const { setNodeRef, isOver } = core.useDroppable({
|
|
293
|
+
id: dayIndex !== void 0 ? `cell-${dayIndex}-${hours}` : `time-${hours}`,
|
|
294
|
+
disabled: dayIndex === void 0
|
|
295
|
+
});
|
|
296
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
297
|
+
"div",
|
|
298
|
+
{
|
|
299
|
+
ref: setNodeRef,
|
|
300
|
+
tabIndex: 0,
|
|
301
|
+
onKeyDown: (e) => e.code === "Space" && onClick?.(),
|
|
302
|
+
onClick,
|
|
303
|
+
role: "button",
|
|
304
|
+
className: cn(
|
|
305
|
+
"bg-slate-50 w-full h-16 border-gray-300 border-[.5px] border-dashed text-gray-500 flex justify-center items-start relative cursor-pointer",
|
|
306
|
+
isTodayCell && isThisHour && "border-t-2 border-t-blue-500",
|
|
307
|
+
isOver && dayIndex !== void 0 && "bg-blue-100",
|
|
308
|
+
className
|
|
309
|
+
),
|
|
310
|
+
children: children || hours
|
|
311
|
+
}
|
|
312
|
+
);
|
|
313
|
+
};
|
|
314
|
+
var EmptyButton = ({
|
|
315
|
+
hours,
|
|
316
|
+
date,
|
|
317
|
+
onNewEvent,
|
|
318
|
+
onAddBlock
|
|
319
|
+
}) => {
|
|
320
|
+
const d = new Date(date);
|
|
321
|
+
d.setHours(hours, 0, 0, 0);
|
|
322
|
+
const [show, setShow] = react.useState(false);
|
|
323
|
+
const buttonRef = react.useRef(null);
|
|
324
|
+
const outsideRef = useClickOutside({
|
|
325
|
+
isActive: show,
|
|
326
|
+
onOutsideClick: () => setShow(false)
|
|
327
|
+
});
|
|
328
|
+
const handleReserva = (event) => {
|
|
329
|
+
event.stopPropagation();
|
|
330
|
+
onNewEvent?.(d);
|
|
331
|
+
setShow(false);
|
|
332
|
+
};
|
|
333
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
334
|
+
"div",
|
|
335
|
+
{
|
|
336
|
+
role: "button",
|
|
337
|
+
tabIndex: 0,
|
|
338
|
+
ref: buttonRef,
|
|
339
|
+
className: "w-full h-full text-xs hover:bg-blue-50 relative",
|
|
340
|
+
onClick: () => setShow(true),
|
|
341
|
+
children: show && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
342
|
+
"div",
|
|
343
|
+
{
|
|
344
|
+
ref: outsideRef,
|
|
345
|
+
className: "absolute border bg-white rounded-lg grid p-1 bottom-[-100%] left-0 z-20 shadow-lg",
|
|
346
|
+
children: [
|
|
347
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
348
|
+
"button",
|
|
349
|
+
{
|
|
350
|
+
onClick: handleReserva,
|
|
351
|
+
className: "hover:bg-blue-50 px-4 py-2 rounded-lg text-left",
|
|
352
|
+
children: "Reserve"
|
|
353
|
+
}
|
|
354
|
+
),
|
|
355
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
356
|
+
"button",
|
|
357
|
+
{
|
|
358
|
+
onClick: (e) => {
|
|
359
|
+
e.stopPropagation();
|
|
360
|
+
onAddBlock?.(d);
|
|
361
|
+
setShow(false);
|
|
362
|
+
},
|
|
363
|
+
className: "hover:bg-blue-50 px-4 py-2 rounded-lg text-left",
|
|
364
|
+
children: "Block"
|
|
365
|
+
}
|
|
366
|
+
)
|
|
367
|
+
]
|
|
368
|
+
}
|
|
369
|
+
)
|
|
370
|
+
}
|
|
371
|
+
);
|
|
372
|
+
};
|
|
373
|
+
var Column = ({
|
|
374
|
+
onEventClick,
|
|
375
|
+
events = [],
|
|
376
|
+
dayOfWeek,
|
|
377
|
+
onNewEvent,
|
|
378
|
+
onAddBlock,
|
|
379
|
+
onRemoveBlock,
|
|
380
|
+
dayIndex,
|
|
381
|
+
locale,
|
|
382
|
+
icons
|
|
383
|
+
}) => {
|
|
384
|
+
const columnRef = react.useRef(null);
|
|
385
|
+
react.useEffect(() => {
|
|
386
|
+
if (!columnRef.current) return;
|
|
387
|
+
const today = /* @__PURE__ */ new Date();
|
|
388
|
+
const isColumnToday = dayOfWeek.getDate() === today.getDate() && dayOfWeek.getMonth() === today.getMonth() && dayOfWeek.getFullYear() === today.getFullYear();
|
|
389
|
+
if (!isColumnToday) return;
|
|
390
|
+
const currentHour = today.getHours();
|
|
391
|
+
const currentCell = columnRef.current.children[currentHour];
|
|
392
|
+
if (currentCell) {
|
|
393
|
+
setTimeout(() => {
|
|
394
|
+
currentCell.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
395
|
+
}, 100);
|
|
396
|
+
}
|
|
397
|
+
}, [dayOfWeek]);
|
|
398
|
+
const findEvent = (hours) => {
|
|
399
|
+
const eventStartsHere = events.find(
|
|
400
|
+
(event) => new Date(event.start).getHours() === hours
|
|
401
|
+
);
|
|
402
|
+
if (eventStartsHere) {
|
|
403
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
404
|
+
DraggableEvent,
|
|
405
|
+
{
|
|
406
|
+
onClick: () => onEventClick?.(eventStartsHere),
|
|
407
|
+
event: eventStartsHere,
|
|
408
|
+
onRemoveBlock,
|
|
409
|
+
locale,
|
|
410
|
+
icons
|
|
411
|
+
}
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
const eventSpansHere = events.find((event) => {
|
|
415
|
+
const eventStart = new Date(event.start);
|
|
416
|
+
const startHour = eventStart.getHours();
|
|
417
|
+
const endHour = startHour + event.duration / 60;
|
|
418
|
+
return hours > startHour && hours < endHour;
|
|
419
|
+
});
|
|
420
|
+
if (eventSpansHere) return null;
|
|
421
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
422
|
+
EmptyButton,
|
|
423
|
+
{
|
|
424
|
+
hours,
|
|
425
|
+
date: dayOfWeek,
|
|
426
|
+
onNewEvent,
|
|
427
|
+
onAddBlock
|
|
428
|
+
}
|
|
429
|
+
);
|
|
430
|
+
};
|
|
431
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { ref: columnRef, className: "grid", children: Array.from({ length: 24 }, (_, hours) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
432
|
+
Cell,
|
|
433
|
+
{
|
|
434
|
+
hours,
|
|
435
|
+
date: dayOfWeek,
|
|
436
|
+
className: "relative",
|
|
437
|
+
dayIndex,
|
|
438
|
+
children: findEvent(hours)
|
|
439
|
+
},
|
|
440
|
+
hours
|
|
441
|
+
)) });
|
|
442
|
+
};
|
|
443
|
+
var TimeColumn = () => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid", children: Array.from({ length: 24 }, (_, i) => /* @__PURE__ */ jsxRuntime.jsx(Cell, { children: `${i < 10 ? "0" : ""}${i}:00` }, i)) });
|
|
444
|
+
var DraggableEvent = ({
|
|
445
|
+
event,
|
|
446
|
+
onClick,
|
|
447
|
+
onRemoveBlock,
|
|
448
|
+
locale,
|
|
449
|
+
icons
|
|
450
|
+
}) => {
|
|
451
|
+
const [showOptions, setShowOptions] = react.useState(false);
|
|
452
|
+
const { attributes, listeners, setNodeRef, transform, isDragging } = core.useDraggable({
|
|
453
|
+
id: `event-${event.id}`,
|
|
454
|
+
disabled: event.type === "BLOCK"
|
|
455
|
+
});
|
|
456
|
+
const style = {
|
|
457
|
+
height: event.duration / 60 * 64,
|
|
458
|
+
transform: transform ? utilities.CSS.Translate.toString(transform) : void 0
|
|
459
|
+
};
|
|
460
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
461
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
462
|
+
"button",
|
|
463
|
+
{
|
|
464
|
+
ref: setNodeRef,
|
|
465
|
+
style,
|
|
466
|
+
onClick: event.type === "BLOCK" ? () => setShowOptions(true) : () => onClick?.(event),
|
|
467
|
+
...listeners,
|
|
468
|
+
...attributes,
|
|
469
|
+
className: cn(
|
|
470
|
+
"border grid gap-y-1 overflow-hidden place-content-start",
|
|
471
|
+
"text-xs text-left pl-1 absolute top-0 left-0 bg-blue-500 text-white rounded-md z-10 w-[90%]",
|
|
472
|
+
event.type === "BLOCK" && "bg-gray-300 h-full w-full text-center cursor-not-allowed relative p-0",
|
|
473
|
+
event.type !== "BLOCK" && "cursor-grab",
|
|
474
|
+
isDragging && event.type !== "BLOCK" && "cursor-grabbing opacity-50"
|
|
475
|
+
),
|
|
476
|
+
children: [
|
|
477
|
+
event.type === "BLOCK" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute top-0 bottom-0 w-1 bg-gray-500 rounded-l-full pointer-events-none" }),
|
|
478
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: event.title }),
|
|
479
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-300", children: event.service?.name })
|
|
480
|
+
]
|
|
481
|
+
}
|
|
482
|
+
),
|
|
483
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
484
|
+
Options,
|
|
485
|
+
{
|
|
486
|
+
event,
|
|
487
|
+
onClose: () => setShowOptions(false),
|
|
488
|
+
isOpen: showOptions,
|
|
489
|
+
onRemoveBlock,
|
|
490
|
+
locale,
|
|
491
|
+
icons
|
|
492
|
+
}
|
|
493
|
+
)
|
|
494
|
+
] });
|
|
495
|
+
};
|
|
496
|
+
var EventOverlay = ({ event }) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
497
|
+
"div",
|
|
498
|
+
{
|
|
499
|
+
className: cn(
|
|
500
|
+
"border grid gap-y-1 overflow-hidden place-content-start",
|
|
501
|
+
"text-xs text-left pl-1 bg-blue-500 text-white rounded-md w-[200px] opacity-90 shadow-lg",
|
|
502
|
+
event.type === "BLOCK" && "bg-gray-300"
|
|
503
|
+
),
|
|
504
|
+
style: { height: event.duration / 60 * 64 },
|
|
505
|
+
children: [
|
|
506
|
+
event.type === "BLOCK" && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute top-0 bottom-0 w-1 bg-gray-500 rounded-l-full pointer-events-none" }),
|
|
507
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: event.title }),
|
|
508
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-gray-300", children: event.service?.name })
|
|
509
|
+
]
|
|
510
|
+
}
|
|
511
|
+
);
|
|
512
|
+
var Options = ({
|
|
513
|
+
event,
|
|
514
|
+
onClose,
|
|
515
|
+
isOpen,
|
|
516
|
+
onRemoveBlock,
|
|
517
|
+
locale,
|
|
518
|
+
icons
|
|
519
|
+
}) => {
|
|
520
|
+
const mainRef = useClickOutside({
|
|
521
|
+
isActive: isOpen,
|
|
522
|
+
onOutsideClick: onClose
|
|
523
|
+
});
|
|
524
|
+
const eventDate = formatDate(event.start, locale);
|
|
525
|
+
const TrashIcon = icons?.trash ?? /* @__PURE__ */ jsxRuntime.jsx(DefaultTrashIcon, {});
|
|
526
|
+
const EditIcon = icons?.edit ?? /* @__PURE__ */ jsxRuntime.jsx(DefaultEditIcon, {});
|
|
527
|
+
const CloseIcon = icons?.close ?? /* @__PURE__ */ jsxRuntime.jsx(DefaultCloseIcon, {});
|
|
528
|
+
if (!isOpen) return null;
|
|
529
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
530
|
+
"div",
|
|
531
|
+
{
|
|
532
|
+
ref: mainRef,
|
|
533
|
+
style: { top: "-100%", left: "-350%" },
|
|
534
|
+
className: "text-left z-20 bg-white absolute border rounded-lg grid p-3 w-[264px] shadow-lg",
|
|
535
|
+
children: [
|
|
536
|
+
/* @__PURE__ */ jsxRuntime.jsxs("header", { children: [
|
|
537
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "font-medium", children: "Blocked Time" }),
|
|
538
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-gray-500", children: eventDate })
|
|
539
|
+
] }),
|
|
540
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "absolute flex left-0 right-0 justify-end gap-3 px-2 py-2 overflow-hidden", children: [
|
|
541
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-l-2 border-b-2 rounded-full absolute pointer-events-none left-[65%] right-0 h-10 -top-2" }),
|
|
542
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
543
|
+
"button",
|
|
544
|
+
{
|
|
545
|
+
onClick: () => {
|
|
546
|
+
onRemoveBlock?.(event.id);
|
|
547
|
+
onClose();
|
|
548
|
+
},
|
|
549
|
+
type: "button",
|
|
550
|
+
className: "hover:bg-blue-50 rounded-lg p-1",
|
|
551
|
+
children: TrashIcon
|
|
552
|
+
}
|
|
553
|
+
),
|
|
554
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", className: "hover:bg-blue-50 rounded-lg p-1", children: EditIcon }),
|
|
555
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
556
|
+
"button",
|
|
557
|
+
{
|
|
558
|
+
onClick: onClose,
|
|
559
|
+
type: "button",
|
|
560
|
+
className: "hover:bg-blue-50 rounded-lg p-1",
|
|
561
|
+
children: CloseIcon
|
|
562
|
+
}
|
|
563
|
+
)
|
|
564
|
+
] })
|
|
565
|
+
]
|
|
566
|
+
}
|
|
567
|
+
);
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
exports.Calendar = Calendar;
|
|
571
|
+
exports.SimpleBigWeekView = Calendar;
|
|
572
|
+
exports.addDaysToDate = addDaysToDate;
|
|
573
|
+
exports.addMinutesToDate = addMinutesToDate;
|
|
574
|
+
exports.areSameDates = areSameDates;
|
|
575
|
+
exports.completeWeek = completeWeek;
|
|
576
|
+
exports.formatDate = formatDate;
|
|
577
|
+
exports.fromDateToTimeString = fromDateToTimeString;
|
|
578
|
+
exports.fromMinsToLocaleTimeString = fromMinsToLocaleTimeString;
|
|
579
|
+
exports.fromMinsToTimeString = fromMinsToTimeString;
|
|
580
|
+
exports.generateHours = generateHours;
|
|
581
|
+
exports.getDaysInMonth = getDaysInMonth;
|
|
582
|
+
exports.getMonday = getMonday;
|
|
583
|
+
exports.isToday = isToday;
|
|
584
|
+
exports.useClickOutside = useClickOutside;
|
|
585
|
+
exports.useEventOverlap = useEventOverlap;
|