@hectorbliss/denik-calendar 0.0.1 → 0.0.3

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 CHANGED
@@ -7,13 +7,13 @@ Built with [@dnd-kit](https://dndkit.com/) for smooth drag interactions.
7
7
  ## Installation
8
8
 
9
9
  ```bash
10
- npm install denik-calendar
10
+ npm install @hectorbliss/denik-calendar
11
11
  ```
12
12
 
13
13
  ## Usage
14
14
 
15
15
  ```tsx
16
- import { Calendar } from "denik-calendar";
16
+ import { Calendar } from "@hectorbliss/denik-calendar";
17
17
 
18
18
  function App() {
19
19
  const [events, setEvents] = useState([
@@ -45,23 +45,31 @@ function App() {
45
45
 
46
46
  ## Headless Hook
47
47
 
48
- Use the overlap detection logic without the UI:
48
+ Use the calendar logic without the UI:
49
49
 
50
50
  ```tsx
51
- import { useEventOverlap } from "denik-calendar";
51
+ import { useCalendarEvents } from "@hectorbliss/denik-calendar";
52
52
 
53
53
  function MyCustomCalendar({ events }) {
54
- const { canMove, hasOverlap, findConflicts } = useEventOverlap(events);
54
+ const {
55
+ canMove,
56
+ hasOverlap,
57
+ findConflicts,
58
+ getEventsForDay,
59
+ getEventsForWeek,
60
+ findAvailableSlots
61
+ } = useCalendarEvents(events);
55
62
 
56
63
  const handleDrop = (eventId, newStart) => {
57
64
  if (canMove(eventId, newStart)) {
58
- // Safe to move
59
65
  updateEvent(eventId, newStart);
60
66
  } else {
61
- // Conflict detected
62
67
  toast.error("Time slot occupied");
63
68
  }
64
69
  };
70
+
71
+ // Get available 60-min slots for today (8am-6pm)
72
+ const slots = findAvailableSlots(new Date(), 60);
65
73
  }
66
74
  ```
67
75
 
@@ -103,9 +111,62 @@ interface CalendarConfig {
103
111
  edit?: ReactNode;
104
112
  close?: ReactNode;
105
113
  };
114
+ renderColumnHeader?: (props: ColumnHeaderProps) => ReactNode;
115
+ }
116
+
117
+ interface ColumnHeaderProps {
118
+ date: Date;
119
+ index: number;
120
+ isToday: boolean;
121
+ locale: string;
106
122
  }
107
123
  ```
108
124
 
125
+ ## Custom Column Headers
126
+
127
+ Transform the calendar from weekdays to any resource type:
128
+
129
+ ```tsx
130
+ // Padel courts booking
131
+ <Calendar
132
+ events={courtEvents}
133
+ config={{
134
+ renderColumnHeader: ({ index }) => (
135
+ <div className="text-center font-semibold">
136
+ Court {index + 1}
137
+ </div>
138
+ )
139
+ }}
140
+ />
141
+
142
+ // Meeting rooms
143
+ <Calendar
144
+ events={roomEvents}
145
+ config={{
146
+ renderColumnHeader: ({ index }) => {
147
+ const rooms = ["Sala A", "Sala B", "Sala C", "Sala D", "Sala E", "Sala F", "Sala G"];
148
+ return <span>{rooms[index]}</span>;
149
+ }
150
+ }}
151
+ />
152
+
153
+ // Employees schedule
154
+ <Calendar
155
+ events={shifts}
156
+ config={{
157
+ renderColumnHeader: ({ index }) => {
158
+ const team = ["Ana", "Carlos", "María", "Pedro", "Laura", "Diego", "Sofia"];
159
+ return (
160
+ <div className="flex flex-col items-center">
161
+ <img src={`/avatars/${index}.jpg`} className="w-8 h-8 rounded-full" />
162
+ <span className="text-sm">{team[index]}</span>
163
+ </div>
164
+ );
165
+ }
166
+ }}
167
+ />
168
+ ```
169
+
109
170
  ## Features
110
171
 
111
172
  - Drag & drop events between time slots
@@ -113,6 +174,7 @@ interface CalendarConfig {
113
174
  - Block time slots
114
175
  - Week navigation
115
176
  - Auto-scroll to current hour
177
+ - Custom column headers (resources, courts, rooms, employees)
116
178
  - Customizable icons
117
179
  - Locale support
118
180
  - TypeScript support
@@ -121,6 +183,12 @@ interface CalendarConfig {
121
183
 
122
184
  - React 18+ or 19+
123
185
 
186
+ ## Author
187
+
188
+ Made by [@blissito](https://github.com/blissito)
189
+
190
+ Learn React & web development at [fixtergeek.com](https://fixtergeek.com)
191
+
124
192
  ## License
125
193
 
126
194
  MIT
package/dist/index.cjs CHANGED
@@ -6,7 +6,88 @@ var utilities = require('@dnd-kit/utilities');
6
6
  var jsxRuntime = require('react/jsx-runtime');
7
7
 
8
8
  // src/Calendar.tsx
9
- function useEventOverlap(events) {
9
+
10
+ // src/utils.ts
11
+ var getMonday = (today = /* @__PURE__ */ new Date()) => {
12
+ const d = new Date(today);
13
+ const day = d.getDay();
14
+ const diff = d.getDate() - day + (day === 0 ? -6 : 1);
15
+ return new Date(d.setDate(diff));
16
+ };
17
+ var completeWeek = (date) => {
18
+ const startDate = new Date(date);
19
+ const day = startDate.getDay();
20
+ const offset = day === 0 ? -6 : 1 - day;
21
+ startDate.setDate(startDate.getDate() + offset);
22
+ return Array.from({ length: 7 }, (_, i) => {
23
+ const d = new Date(startDate);
24
+ d.setDate(d.getDate() + i);
25
+ return d;
26
+ });
27
+ };
28
+ var generateHours = ({
29
+ fromHour,
30
+ toHour
31
+ }) => {
32
+ return Array.from({ length: toHour - fromHour }, (_, index) => {
33
+ const hour = fromHour + index;
34
+ return hour < 10 ? `0${hour}:00` : `${hour}:00`;
35
+ });
36
+ };
37
+ var isToday = (date) => {
38
+ const today = /* @__PURE__ */ new Date();
39
+ return date.getDate() === today.getDate() && date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear();
40
+ };
41
+ var areSameDates = (d1, d2) => {
42
+ if (!d1 || !d2) return false;
43
+ return d1.getDate() === d2.getDate() && d1.getMonth() === d2.getMonth() && d1.getFullYear() === d2.getFullYear();
44
+ };
45
+ var addDaysToDate = (date, days) => {
46
+ const result = new Date(date);
47
+ result.setDate(result.getDate() + days);
48
+ return result;
49
+ };
50
+ var addMinutesToDate = (date, mins) => {
51
+ const result = new Date(date);
52
+ result.setMinutes(result.getMinutes() + mins);
53
+ return result;
54
+ };
55
+ var fromMinsToTimeString = (mins) => {
56
+ const h = Math.floor(mins / 60);
57
+ const m = mins % 60;
58
+ return `${h < 10 ? "0" + h : h}:${m < 10 ? "0" + m : m}`;
59
+ };
60
+ var fromMinsToLocaleTimeString = (mins, locale = "es-MX") => {
61
+ const h = Math.floor(mins / 60);
62
+ const m = mins % 60;
63
+ const today = /* @__PURE__ */ new Date();
64
+ today.setHours(h, m, 0, 0);
65
+ return today.toLocaleTimeString(locale);
66
+ };
67
+ var fromDateToTimeString = (date, locale = "es-MX") => {
68
+ return new Date(date).toLocaleTimeString(locale);
69
+ };
70
+ var getDaysInMonth = (date) => {
71
+ const firstDay = new Date(date.getFullYear(), date.getMonth(), 1);
72
+ const lastDay = new Date(date.getFullYear(), date.getMonth() + 1, 0);
73
+ const numberOfMissing = 6 - lastDay.getDay();
74
+ const leftOffset = firstDay.getDay();
75
+ firstDay.setDate(firstDay.getDate() - leftOffset);
76
+ const days = [];
77
+ days.push(new Date(firstDay));
78
+ while (firstDay < lastDay) {
79
+ firstDay.setDate(firstDay.getDate() + 1);
80
+ days.push(new Date(firstDay));
81
+ }
82
+ for (let i = 0; i < numberOfMissing; i++) {
83
+ firstDay.setDate(firstDay.getDate() + 1);
84
+ days.push(new Date(firstDay));
85
+ }
86
+ return days;
87
+ };
88
+
89
+ // src/useCalendarEvents.ts
90
+ function useCalendarEvents(events) {
10
91
  const hasOverlap = react.useCallback(
11
92
  (start, duration, excludeId) => {
12
93
  const hour = start.getHours() + start.getMinutes() / 60;
@@ -49,21 +130,60 @@ function useEventOverlap(events) {
49
130
  },
50
131
  [events, hasOverlap]
51
132
  );
52
- const getEventsForDay = react.useMemo(() => {
53
- return (date) => {
133
+ const getEventsForDay = react.useCallback(
134
+ (date) => {
54
135
  return events.filter((event) => {
55
136
  const eventDate = new Date(event.start);
56
137
  return eventDate.getDate() === date.getDate() && eventDate.getMonth() === date.getMonth() && eventDate.getFullYear() === date.getFullYear();
57
138
  });
58
- };
59
- }, [events]);
139
+ },
140
+ [events]
141
+ );
142
+ const getEventsForWeek = react.useCallback(
143
+ (date) => {
144
+ const week = completeWeek(date);
145
+ const start = week[0];
146
+ const end = new Date(week[6]);
147
+ end.setHours(23, 59, 59, 999);
148
+ return events.filter((event) => {
149
+ const eventDate = new Date(event.start);
150
+ return eventDate >= start && eventDate <= end;
151
+ });
152
+ },
153
+ [events]
154
+ );
155
+ const findAvailableSlots = react.useCallback(
156
+ (date, duration, startHour = 8, endHour = 18) => {
157
+ const slots = [];
158
+ const dayEvents = getEventsForDay(date);
159
+ for (let hour = startHour; hour <= endHour - duration / 60; hour++) {
160
+ const slotStart = new Date(date);
161
+ slotStart.setHours(hour, 0, 0, 0);
162
+ const hasConflict = dayEvents.some((event) => {
163
+ const eventStart = new Date(event.start);
164
+ const eventHour = eventStart.getHours();
165
+ const eventEnd = eventHour + event.duration / 60;
166
+ const slotEnd = hour + duration / 60;
167
+ return hour >= eventHour && hour < eventEnd || slotEnd > eventHour && slotEnd <= eventEnd || hour <= eventHour && slotEnd >= eventEnd;
168
+ });
169
+ if (!hasConflict) {
170
+ slots.push(slotStart);
171
+ }
172
+ }
173
+ return slots;
174
+ },
175
+ [getEventsForDay]
176
+ );
60
177
  return {
61
178
  hasOverlap,
62
179
  findConflicts,
63
180
  canMove,
64
- getEventsForDay
181
+ getEventsForDay,
182
+ getEventsForWeek,
183
+ findAvailableSlots
65
184
  };
66
185
  }
186
+ var useEventOverlap = useCalendarEvents;
67
187
  function useClickOutside({
68
188
  isActive,
69
189
  onOutsideClick
@@ -93,101 +213,33 @@ function formatDate(date, locale = "es-MX") {
93
213
  minute: "2-digit"
94
214
  });
95
215
  }
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
216
  var cn = (...classes) => classes.filter(Boolean).join(" ");
176
217
  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
218
  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
219
  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
- ] });
220
+ var DayHeader = ({
221
+ date,
222
+ locale,
223
+ index,
224
+ renderColumnHeader
225
+ }) => {
226
+ const isToday2 = isToday(date);
227
+ if (renderColumnHeader) {
228
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid place-items-center", children: renderColumnHeader({ date, index, isToday: isToday2, locale }) });
229
+ }
230
+ return /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "grid place-items-center", children: [
231
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "capitalize", children: date.toLocaleDateString(locale, { weekday: "short" }) }),
232
+ /* @__PURE__ */ jsxRuntime.jsx(
233
+ "span",
234
+ {
235
+ className: cn(
236
+ isToday2 && "bg-blue-500 rounded-full p-1 text-white"
237
+ ),
238
+ children: date.getDate()
239
+ }
240
+ )
241
+ ] });
242
+ };
191
243
  function Calendar({
192
244
  date = /* @__PURE__ */ new Date(),
193
245
  events = [],
@@ -198,10 +250,10 @@ function Calendar({
198
250
  onRemoveBlock,
199
251
  config = {}
200
252
  }) {
201
- const { locale = "es-MX", icons = {} } = config;
253
+ const { locale = "es-MX", icons = {}, renderColumnHeader } = config;
202
254
  const week = completeWeek(date);
203
255
  const [activeId, setActiveId] = react.useState(null);
204
- const { canMove } = useEventOverlap(events);
256
+ const { canMove } = useCalendarEvents(events);
205
257
  const sensors = core.useSensors(
206
258
  core.useSensor(core.PointerSensor, {
207
259
  activationConstraint: { distance: 8 }
@@ -250,7 +302,16 @@ function Calendar({
250
302
  /* @__PURE__ */ jsxRuntime.jsxs("article", { className: "w-full bg-white shadow rounded-xl", children: [
251
303
  /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "grid grid-cols-8 place-items-center py-4", children: [
252
304
  /* @__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()))
305
+ week.map((day, index) => /* @__PURE__ */ jsxRuntime.jsx(
306
+ DayHeader,
307
+ {
308
+ date: day,
309
+ locale,
310
+ index,
311
+ renderColumnHeader
312
+ },
313
+ day.toISOString()
314
+ ))
254
315
  ] }),
255
316
  /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "grid grid-cols-8 max-h-[80vh] overflow-y-auto", children: [
256
317
  /* @__PURE__ */ jsxRuntime.jsx(TimeColumn, {}),
@@ -581,5 +642,6 @@ exports.generateHours = generateHours;
581
642
  exports.getDaysInMonth = getDaysInMonth;
582
643
  exports.getMonday = getMonday;
583
644
  exports.isToday = isToday;
645
+ exports.useCalendarEvents = useCalendarEvents;
584
646
  exports.useClickOutside = useClickOutside;
585
647
  exports.useEventOverlap = useEventOverlap;
package/dist/index.d.cts CHANGED
@@ -1,6 +1,20 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode, RefObject } from 'react';
3
3
 
4
+ /**
5
+ * Props passed to custom column header renderer
6
+ * Use this to build custom headers for resources (courts, rooms, employees, etc.)
7
+ */
8
+ interface ColumnHeaderProps {
9
+ /** The date for this column */
10
+ date: Date;
11
+ /** Column index (0-6) */
12
+ index: number;
13
+ /** Whether this column represents today */
14
+ isToday: boolean;
15
+ /** The configured locale */
16
+ locale: string;
17
+ }
4
18
  /**
5
19
  * Generic calendar event - decoupled from any ORM
6
20
  */
@@ -30,6 +44,23 @@ interface CalendarConfig {
30
44
  edit?: ReactNode;
31
45
  close?: ReactNode;
32
46
  };
47
+ /**
48
+ * Custom renderer for column headers.
49
+ * Use this to display resources (courts, rooms, employees) instead of weekdays.
50
+ *
51
+ * @example
52
+ * // Padel courts
53
+ * renderColumnHeader: ({ index }) => <span>Court {index + 1}</span>
54
+ *
55
+ * @example
56
+ * // With custom styling
57
+ * renderColumnHeader: ({ date, isToday }) => (
58
+ * <div className={isToday ? "font-bold" : ""}>
59
+ * {date.toLocaleDateString("en", { weekday: "short" })}
60
+ * </div>
61
+ * )
62
+ */
63
+ renderColumnHeader?: (props: ColumnHeaderProps) => ReactNode;
33
64
  }
34
65
  /**
35
66
  * Calendar component props
@@ -56,15 +87,17 @@ interface CalendarProps {
56
87
  declare function Calendar({ date, events, onEventClick, onNewEvent, onEventMove, onAddBlock, onRemoveBlock, config, }: CalendarProps): react_jsx_runtime.JSX.Element;
57
88
 
58
89
  /**
59
- * Hook for detecting event overlaps in a calendar
60
- * Can be used standalone (headless) or with the Calendar component
90
+ * Hook for managing calendar events - overlap detection, filtering, and availability
61
91
  */
62
- declare function useEventOverlap(events: CalendarEvent[]): {
92
+ declare function useCalendarEvents(events: CalendarEvent[]): {
63
93
  hasOverlap: (start: Date, duration: number, excludeId?: string) => boolean;
64
94
  findConflicts: (start: Date, duration: number, excludeId?: string) => CalendarEvent[];
65
95
  canMove: (eventId: string, newStart: Date) => boolean;
66
96
  getEventsForDay: (date: Date) => CalendarEvent[];
97
+ getEventsForWeek: (date: Date) => CalendarEvent[];
98
+ findAvailableSlots: (date: Date, duration: number, startHour?: number, endHour?: number) => Date[];
67
99
  };
100
+ declare const useEventOverlap: typeof useCalendarEvents;
68
101
 
69
102
  /**
70
103
  * Get the Monday of the week for a given date
@@ -126,4 +159,4 @@ declare function useClickOutside<T extends HTMLElement>({ isActive, onOutsideCli
126
159
  */
127
160
  declare function formatDate(date: Date, locale?: string): string;
128
161
 
129
- export { Calendar, type CalendarConfig, type CalendarEvent, type CalendarProps, Calendar as SimpleBigWeekView, addDaysToDate, addMinutesToDate, areSameDates, completeWeek, formatDate, fromDateToTimeString, fromMinsToLocaleTimeString, fromMinsToTimeString, generateHours, getDaysInMonth, getMonday, isToday, useClickOutside, useEventOverlap };
162
+ export { Calendar, type CalendarConfig, type CalendarEvent, type CalendarProps, type ColumnHeaderProps, Calendar as SimpleBigWeekView, addDaysToDate, addMinutesToDate, areSameDates, completeWeek, formatDate, fromDateToTimeString, fromMinsToLocaleTimeString, fromMinsToTimeString, generateHours, getDaysInMonth, getMonday, isToday, useCalendarEvents, useClickOutside, useEventOverlap };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,20 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import { ReactNode, RefObject } from 'react';
3
3
 
4
+ /**
5
+ * Props passed to custom column header renderer
6
+ * Use this to build custom headers for resources (courts, rooms, employees, etc.)
7
+ */
8
+ interface ColumnHeaderProps {
9
+ /** The date for this column */
10
+ date: Date;
11
+ /** Column index (0-6) */
12
+ index: number;
13
+ /** Whether this column represents today */
14
+ isToday: boolean;
15
+ /** The configured locale */
16
+ locale: string;
17
+ }
4
18
  /**
5
19
  * Generic calendar event - decoupled from any ORM
6
20
  */
@@ -30,6 +44,23 @@ interface CalendarConfig {
30
44
  edit?: ReactNode;
31
45
  close?: ReactNode;
32
46
  };
47
+ /**
48
+ * Custom renderer for column headers.
49
+ * Use this to display resources (courts, rooms, employees) instead of weekdays.
50
+ *
51
+ * @example
52
+ * // Padel courts
53
+ * renderColumnHeader: ({ index }) => <span>Court {index + 1}</span>
54
+ *
55
+ * @example
56
+ * // With custom styling
57
+ * renderColumnHeader: ({ date, isToday }) => (
58
+ * <div className={isToday ? "font-bold" : ""}>
59
+ * {date.toLocaleDateString("en", { weekday: "short" })}
60
+ * </div>
61
+ * )
62
+ */
63
+ renderColumnHeader?: (props: ColumnHeaderProps) => ReactNode;
33
64
  }
34
65
  /**
35
66
  * Calendar component props
@@ -56,15 +87,17 @@ interface CalendarProps {
56
87
  declare function Calendar({ date, events, onEventClick, onNewEvent, onEventMove, onAddBlock, onRemoveBlock, config, }: CalendarProps): react_jsx_runtime.JSX.Element;
57
88
 
58
89
  /**
59
- * Hook for detecting event overlaps in a calendar
60
- * Can be used standalone (headless) or with the Calendar component
90
+ * Hook for managing calendar events - overlap detection, filtering, and availability
61
91
  */
62
- declare function useEventOverlap(events: CalendarEvent[]): {
92
+ declare function useCalendarEvents(events: CalendarEvent[]): {
63
93
  hasOverlap: (start: Date, duration: number, excludeId?: string) => boolean;
64
94
  findConflicts: (start: Date, duration: number, excludeId?: string) => CalendarEvent[];
65
95
  canMove: (eventId: string, newStart: Date) => boolean;
66
96
  getEventsForDay: (date: Date) => CalendarEvent[];
97
+ getEventsForWeek: (date: Date) => CalendarEvent[];
98
+ findAvailableSlots: (date: Date, duration: number, startHour?: number, endHour?: number) => Date[];
67
99
  };
100
+ declare const useEventOverlap: typeof useCalendarEvents;
68
101
 
69
102
  /**
70
103
  * Get the Monday of the week for a given date
@@ -126,4 +159,4 @@ declare function useClickOutside<T extends HTMLElement>({ isActive, onOutsideCli
126
159
  */
127
160
  declare function formatDate(date: Date, locale?: string): string;
128
161
 
129
- export { Calendar, type CalendarConfig, type CalendarEvent, type CalendarProps, Calendar as SimpleBigWeekView, addDaysToDate, addMinutesToDate, areSameDates, completeWeek, formatDate, fromDateToTimeString, fromMinsToLocaleTimeString, fromMinsToTimeString, generateHours, getDaysInMonth, getMonday, isToday, useClickOutside, useEventOverlap };
162
+ export { Calendar, type CalendarConfig, type CalendarEvent, type CalendarProps, type ColumnHeaderProps, Calendar as SimpleBigWeekView, addDaysToDate, addMinutesToDate, areSameDates, completeWeek, formatDate, fromDateToTimeString, fromMinsToLocaleTimeString, fromMinsToTimeString, generateHours, getDaysInMonth, getMonday, isToday, useCalendarEvents, useClickOutside, useEventOverlap };
package/dist/index.js CHANGED
@@ -1,10 +1,91 @@
1
- import { useCallback, useMemo, useRef, useEffect, useState } from 'react';
1
+ import { useCallback, useRef, useEffect, useState } from 'react';
2
2
  import { useSensors, useSensor, PointerSensor, KeyboardSensor, DndContext, closestCenter, DragOverlay, useDroppable, useDraggable } from '@dnd-kit/core';
3
3
  import { CSS } from '@dnd-kit/utilities';
4
4
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
5
 
6
6
  // src/Calendar.tsx
7
- function useEventOverlap(events) {
7
+
8
+ // src/utils.ts
9
+ var getMonday = (today = /* @__PURE__ */ new Date()) => {
10
+ const d = new Date(today);
11
+ const day = d.getDay();
12
+ const diff = d.getDate() - day + (day === 0 ? -6 : 1);
13
+ return new Date(d.setDate(diff));
14
+ };
15
+ var completeWeek = (date) => {
16
+ const startDate = new Date(date);
17
+ const day = startDate.getDay();
18
+ const offset = day === 0 ? -6 : 1 - day;
19
+ startDate.setDate(startDate.getDate() + offset);
20
+ return Array.from({ length: 7 }, (_, i) => {
21
+ const d = new Date(startDate);
22
+ d.setDate(d.getDate() + i);
23
+ return d;
24
+ });
25
+ };
26
+ var generateHours = ({
27
+ fromHour,
28
+ toHour
29
+ }) => {
30
+ return Array.from({ length: toHour - fromHour }, (_, index) => {
31
+ const hour = fromHour + index;
32
+ return hour < 10 ? `0${hour}:00` : `${hour}:00`;
33
+ });
34
+ };
35
+ var isToday = (date) => {
36
+ const today = /* @__PURE__ */ new Date();
37
+ return date.getDate() === today.getDate() && date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear();
38
+ };
39
+ var areSameDates = (d1, d2) => {
40
+ if (!d1 || !d2) return false;
41
+ return d1.getDate() === d2.getDate() && d1.getMonth() === d2.getMonth() && d1.getFullYear() === d2.getFullYear();
42
+ };
43
+ var addDaysToDate = (date, days) => {
44
+ const result = new Date(date);
45
+ result.setDate(result.getDate() + days);
46
+ return result;
47
+ };
48
+ var addMinutesToDate = (date, mins) => {
49
+ const result = new Date(date);
50
+ result.setMinutes(result.getMinutes() + mins);
51
+ return result;
52
+ };
53
+ var fromMinsToTimeString = (mins) => {
54
+ const h = Math.floor(mins / 60);
55
+ const m = mins % 60;
56
+ return `${h < 10 ? "0" + h : h}:${m < 10 ? "0" + m : m}`;
57
+ };
58
+ var fromMinsToLocaleTimeString = (mins, locale = "es-MX") => {
59
+ const h = Math.floor(mins / 60);
60
+ const m = mins % 60;
61
+ const today = /* @__PURE__ */ new Date();
62
+ today.setHours(h, m, 0, 0);
63
+ return today.toLocaleTimeString(locale);
64
+ };
65
+ var fromDateToTimeString = (date, locale = "es-MX") => {
66
+ return new Date(date).toLocaleTimeString(locale);
67
+ };
68
+ var getDaysInMonth = (date) => {
69
+ const firstDay = new Date(date.getFullYear(), date.getMonth(), 1);
70
+ const lastDay = new Date(date.getFullYear(), date.getMonth() + 1, 0);
71
+ const numberOfMissing = 6 - lastDay.getDay();
72
+ const leftOffset = firstDay.getDay();
73
+ firstDay.setDate(firstDay.getDate() - leftOffset);
74
+ const days = [];
75
+ days.push(new Date(firstDay));
76
+ while (firstDay < lastDay) {
77
+ firstDay.setDate(firstDay.getDate() + 1);
78
+ days.push(new Date(firstDay));
79
+ }
80
+ for (let i = 0; i < numberOfMissing; i++) {
81
+ firstDay.setDate(firstDay.getDate() + 1);
82
+ days.push(new Date(firstDay));
83
+ }
84
+ return days;
85
+ };
86
+
87
+ // src/useCalendarEvents.ts
88
+ function useCalendarEvents(events) {
8
89
  const hasOverlap = useCallback(
9
90
  (start, duration, excludeId) => {
10
91
  const hour = start.getHours() + start.getMinutes() / 60;
@@ -47,21 +128,60 @@ function useEventOverlap(events) {
47
128
  },
48
129
  [events, hasOverlap]
49
130
  );
50
- const getEventsForDay = useMemo(() => {
51
- return (date) => {
131
+ const getEventsForDay = useCallback(
132
+ (date) => {
52
133
  return events.filter((event) => {
53
134
  const eventDate = new Date(event.start);
54
135
  return eventDate.getDate() === date.getDate() && eventDate.getMonth() === date.getMonth() && eventDate.getFullYear() === date.getFullYear();
55
136
  });
56
- };
57
- }, [events]);
137
+ },
138
+ [events]
139
+ );
140
+ const getEventsForWeek = useCallback(
141
+ (date) => {
142
+ const week = completeWeek(date);
143
+ const start = week[0];
144
+ const end = new Date(week[6]);
145
+ end.setHours(23, 59, 59, 999);
146
+ return events.filter((event) => {
147
+ const eventDate = new Date(event.start);
148
+ return eventDate >= start && eventDate <= end;
149
+ });
150
+ },
151
+ [events]
152
+ );
153
+ const findAvailableSlots = useCallback(
154
+ (date, duration, startHour = 8, endHour = 18) => {
155
+ const slots = [];
156
+ const dayEvents = getEventsForDay(date);
157
+ for (let hour = startHour; hour <= endHour - duration / 60; hour++) {
158
+ const slotStart = new Date(date);
159
+ slotStart.setHours(hour, 0, 0, 0);
160
+ const hasConflict = dayEvents.some((event) => {
161
+ const eventStart = new Date(event.start);
162
+ const eventHour = eventStart.getHours();
163
+ const eventEnd = eventHour + event.duration / 60;
164
+ const slotEnd = hour + duration / 60;
165
+ return hour >= eventHour && hour < eventEnd || slotEnd > eventHour && slotEnd <= eventEnd || hour <= eventHour && slotEnd >= eventEnd;
166
+ });
167
+ if (!hasConflict) {
168
+ slots.push(slotStart);
169
+ }
170
+ }
171
+ return slots;
172
+ },
173
+ [getEventsForDay]
174
+ );
58
175
  return {
59
176
  hasOverlap,
60
177
  findConflicts,
61
178
  canMove,
62
- getEventsForDay
179
+ getEventsForDay,
180
+ getEventsForWeek,
181
+ findAvailableSlots
63
182
  };
64
183
  }
184
+ var useEventOverlap = useCalendarEvents;
65
185
  function useClickOutside({
66
186
  isActive,
67
187
  onOutsideClick
@@ -91,101 +211,33 @@ function formatDate(date, locale = "es-MX") {
91
211
  minute: "2-digit"
92
212
  });
93
213
  }
94
-
95
- // src/utils.ts
96
- var getMonday = (today = /* @__PURE__ */ new Date()) => {
97
- const d = new Date(today);
98
- const day = d.getDay();
99
- const diff = d.getDate() - day + (day === 0 ? -6 : 1);
100
- return new Date(d.setDate(diff));
101
- };
102
- var completeWeek = (date) => {
103
- const startDate = new Date(date);
104
- const day = startDate.getDay();
105
- const offset = day === 0 ? -6 : 1 - day;
106
- startDate.setDate(startDate.getDate() + offset);
107
- return Array.from({ length: 7 }, (_, i) => {
108
- const d = new Date(startDate);
109
- d.setDate(d.getDate() + i);
110
- return d;
111
- });
112
- };
113
- var generateHours = ({
114
- fromHour,
115
- toHour
116
- }) => {
117
- return Array.from({ length: toHour - fromHour }, (_, index) => {
118
- const hour = fromHour + index;
119
- return hour < 10 ? `0${hour}:00` : `${hour}:00`;
120
- });
121
- };
122
- var isToday = (date) => {
123
- const today = /* @__PURE__ */ new Date();
124
- return date.getDate() === today.getDate() && date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear();
125
- };
126
- var areSameDates = (d1, d2) => {
127
- if (!d1 || !d2) return false;
128
- return d1.getDate() === d2.getDate() && d1.getMonth() === d2.getMonth() && d1.getFullYear() === d2.getFullYear();
129
- };
130
- var addDaysToDate = (date, days) => {
131
- const result = new Date(date);
132
- result.setDate(result.getDate() + days);
133
- return result;
134
- };
135
- var addMinutesToDate = (date, mins) => {
136
- const result = new Date(date);
137
- result.setMinutes(result.getMinutes() + mins);
138
- return result;
139
- };
140
- var fromMinsToTimeString = (mins) => {
141
- const h = Math.floor(mins / 60);
142
- const m = mins % 60;
143
- return `${h < 10 ? "0" + h : h}:${m < 10 ? "0" + m : m}`;
144
- };
145
- var fromMinsToLocaleTimeString = (mins, locale = "es-MX") => {
146
- const h = Math.floor(mins / 60);
147
- const m = mins % 60;
148
- const today = /* @__PURE__ */ new Date();
149
- today.setHours(h, m, 0, 0);
150
- return today.toLocaleTimeString(locale);
151
- };
152
- var fromDateToTimeString = (date, locale = "es-MX") => {
153
- return new Date(date).toLocaleTimeString(locale);
154
- };
155
- var getDaysInMonth = (date) => {
156
- const firstDay = new Date(date.getFullYear(), date.getMonth(), 1);
157
- const lastDay = new Date(date.getFullYear(), date.getMonth() + 1, 0);
158
- const numberOfMissing = 6 - lastDay.getDay();
159
- const leftOffset = firstDay.getDay();
160
- firstDay.setDate(firstDay.getDate() - leftOffset);
161
- const days = [];
162
- days.push(new Date(firstDay));
163
- while (firstDay < lastDay) {
164
- firstDay.setDate(firstDay.getDate() + 1);
165
- days.push(new Date(firstDay));
166
- }
167
- for (let i = 0; i < numberOfMissing; i++) {
168
- firstDay.setDate(firstDay.getDate() + 1);
169
- days.push(new Date(firstDay));
170
- }
171
- return days;
172
- };
173
214
  var cn = (...classes) => classes.filter(Boolean).join(" ");
174
215
  var DefaultTrashIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "currentColor", children: /* @__PURE__ */ 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" }) });
175
216
  var DefaultEditIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "currentColor", children: /* @__PURE__ */ 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" }) });
176
217
  var DefaultCloseIcon = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", className: "w-4 h-4", fill: "currentColor", children: /* @__PURE__ */ 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" }) });
177
- var DayHeader = ({ date, locale }) => /* @__PURE__ */ jsxs("p", { className: "grid place-items-center", children: [
178
- /* @__PURE__ */ jsx("span", { className: "capitalize", children: date.toLocaleDateString(locale, { weekday: "short" }) }),
179
- /* @__PURE__ */ jsx(
180
- "span",
181
- {
182
- className: cn(
183
- isToday(date) && "bg-blue-500 rounded-full p-1 text-white"
184
- ),
185
- children: date.getDate()
186
- }
187
- )
188
- ] });
218
+ var DayHeader = ({
219
+ date,
220
+ locale,
221
+ index,
222
+ renderColumnHeader
223
+ }) => {
224
+ const isToday2 = isToday(date);
225
+ if (renderColumnHeader) {
226
+ return /* @__PURE__ */ jsx("div", { className: "grid place-items-center", children: renderColumnHeader({ date, index, isToday: isToday2, locale }) });
227
+ }
228
+ return /* @__PURE__ */ jsxs("p", { className: "grid place-items-center", children: [
229
+ /* @__PURE__ */ jsx("span", { className: "capitalize", children: date.toLocaleDateString(locale, { weekday: "short" }) }),
230
+ /* @__PURE__ */ jsx(
231
+ "span",
232
+ {
233
+ className: cn(
234
+ isToday2 && "bg-blue-500 rounded-full p-1 text-white"
235
+ ),
236
+ children: date.getDate()
237
+ }
238
+ )
239
+ ] });
240
+ };
189
241
  function Calendar({
190
242
  date = /* @__PURE__ */ new Date(),
191
243
  events = [],
@@ -196,10 +248,10 @@ function Calendar({
196
248
  onRemoveBlock,
197
249
  config = {}
198
250
  }) {
199
- const { locale = "es-MX", icons = {} } = config;
251
+ const { locale = "es-MX", icons = {}, renderColumnHeader } = config;
200
252
  const week = completeWeek(date);
201
253
  const [activeId, setActiveId] = useState(null);
202
- const { canMove } = useEventOverlap(events);
254
+ const { canMove } = useCalendarEvents(events);
203
255
  const sensors = useSensors(
204
256
  useSensor(PointerSensor, {
205
257
  activationConstraint: { distance: 8 }
@@ -248,7 +300,16 @@ function Calendar({
248
300
  /* @__PURE__ */ jsxs("article", { className: "w-full bg-white shadow rounded-xl", children: [
249
301
  /* @__PURE__ */ jsxs("section", { className: "grid grid-cols-8 place-items-center py-4", children: [
250
302
  /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("span", { className: "text-sm text-gray-500", children: Intl.DateTimeFormat().resolvedOptions().timeZone }) }),
251
- week.map((day) => /* @__PURE__ */ jsx(DayHeader, { date: day, locale }, day.toISOString()))
303
+ week.map((day, index) => /* @__PURE__ */ jsx(
304
+ DayHeader,
305
+ {
306
+ date: day,
307
+ locale,
308
+ index,
309
+ renderColumnHeader
310
+ },
311
+ day.toISOString()
312
+ ))
252
313
  ] }),
253
314
  /* @__PURE__ */ jsxs("section", { className: "grid grid-cols-8 max-h-[80vh] overflow-y-auto", children: [
254
315
  /* @__PURE__ */ jsx(TimeColumn, {}),
@@ -565,4 +626,4 @@ var Options = ({
565
626
  );
566
627
  };
567
628
 
568
- export { Calendar, Calendar as SimpleBigWeekView, addDaysToDate, addMinutesToDate, areSameDates, completeWeek, formatDate, fromDateToTimeString, fromMinsToLocaleTimeString, fromMinsToTimeString, generateHours, getDaysInMonth, getMonday, isToday, useClickOutside, useEventOverlap };
629
+ export { Calendar, Calendar as SimpleBigWeekView, addDaysToDate, addMinutesToDate, areSameDates, completeWeek, formatDate, fromDateToTimeString, fromMinsToLocaleTimeString, fromMinsToTimeString, generateHours, getDaysInMonth, getMonday, isToday, useCalendarEvents, useClickOutside, useEventOverlap };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hectorbliss/denik-calendar",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "private": false,
5
5
  "description": "A React drag-and-drop week calendar component with overlap detection. Built with @dnd-kit.",
6
6
  "type": "module",