@marco.colia/react-fast-scheduler 2.1.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A lightweight React scheduling library scaffold with CI/CD publishing.
4
4
 
5
- ![img.png](https://bg-so-1.zippyimage.com/2026/03/20/9f3bd8a7a76d94f6e110aa0a0bef11d3.png)
5
+ ![img.gif](https://res.cloudinary.com/dt74zb1rv/image/upload/v1774457086/Video_del_2026-03-25_17-35-51_kuflee.gif)
6
6
 
7
7
  ## Install
8
8
 
@@ -13,7 +13,7 @@ npm i @marco.colia/react-fast-scheduler
13
13
  ## Usage
14
14
 
15
15
  ```tsx
16
- import { useState } from "react";
16
+ import { useEffect, useState } from "react";
17
17
  import { Scheduler } from "@marco.colia/react-fast-scheduler";
18
18
  import "@marco.colia/react-fast-scheduler/styles.css";
19
19
  import type { BaseSchedulerResource } from "@marco.colia/react-fast-scheduler";
@@ -33,30 +33,70 @@ type Appointment = {
33
33
  end: string; // ISO string
34
34
  };
35
35
 
36
- const initialStaff: Staff[] = [
37
- { id: 1, label: "Room A", firstName: "Anna", lastName: "Rossi" },
38
- { id: 2, label: "Room B", firstName: "Luca", lastName: "Bianchi" }
39
- ];
40
-
41
- const initialAppointments: Appointment[] = [
42
- {
43
- id: 100,
44
- staffId: 1,
45
- description: "First consultation",
46
- customerName: "Mario Rossi",
47
- title: "Consultation",
48
- start: "2026-03-20T09:00:00.000Z",
49
- end: "2026-03-20T09:30:00.000Z"
50
- }
51
- ];
52
-
53
36
  export function SchedulerExample() {
37
+ const [staff, setStaff] = useState<Staff[]>([]);
54
38
  const [selectedDate, setSelectedDate] = useState(new Date("2026-03-20T00:00:00.000Z"));
55
- const [appointments, setAppointments] = useState<Appointment[]>(initialAppointments);
39
+ const [appointments, setAppointments] = useState<Appointment[]>([]);
40
+
41
+ const loadStaff = async () => {
42
+ const response = await fetch("/api/staff");
43
+ const data = (await response.json()) as Staff[];
44
+ setStaff(data);
45
+ };
46
+
47
+ const loadAppointments = async (date: Date) => {
48
+ const isoDate = date.toISOString().slice(0, 10);
49
+ const response = await fetch(`/api/appointments?date=${isoDate}`);
50
+ const data = (await response.json()) as Appointment[];
51
+ setAppointments(data);
52
+ };
53
+
54
+ const persistAppointmentChange = async (appointmentId: number, next: Appointment) => {
55
+ await fetch(`/api/appointments/${appointmentId}`, {
56
+ method: "PATCH",
57
+ headers: { "Content-Type": "application/json" },
58
+ body: JSON.stringify(next),
59
+ });
60
+ };
61
+
62
+ const handleAppointmentChange = async ({ appointment, previous, next }) => {
63
+ const nextAppointment = {
64
+ ...appointment,
65
+ staffId: next.resourceId,
66
+ start: next.start.toISOString(),
67
+ end: next.end.toISOString(),
68
+ };
69
+
70
+ try {
71
+ await persistAppointmentChange(appointment.id, nextAppointment);
72
+ await loadAppointments(selectedDate);
73
+ } catch {
74
+ setAppointments((current) =>
75
+ current.map((item) =>
76
+ item.id === appointment.id
77
+ ? {
78
+ ...item,
79
+ staffId: previous.resourceId,
80
+ start: previous.start.toISOString(),
81
+ end: previous.end.toISOString(),
82
+ }
83
+ : item
84
+ )
85
+ );
86
+ }
87
+ };
88
+
89
+ useEffect(() => {
90
+ void loadStaff();
91
+ }, []);
92
+
93
+ useEffect(() => {
94
+ void loadAppointments(selectedDate);
95
+ }, [selectedDate]);
56
96
 
57
97
  return (
58
98
  <Scheduler<Appointment, Staff, number>
59
- resources={initialStaff}
99
+ resources={staff}
60
100
  appointments={appointments}
61
101
  selectedDate={selectedDate}
62
102
  onSelectedDateChange={setSelectedDate}
@@ -67,20 +107,12 @@ export function SchedulerExample() {
67
107
  getResourceId: (a) => a.staffId,
68
108
  getStart: (a) => a.start,
69
109
  getEnd: (a) => a.end,
70
- getTitle: (a) => `${a.customerName} - ${a.title}`
71
- }}
72
- onAppointmentChange={async ({ appointment, next }) => {
73
- setAppointments((prev) =>
74
- prev.map((a) =>
75
- a.id === appointment.id
76
- ? { ...a, staffId: next.resourceId, start: next.start.toISOString(), end: next.end.toISOString() }
77
- : a
78
- )
79
- );
110
+ getTitle: (a) => `${a.customerName} - ${a.title}`,
80
111
  }}
112
+ onAppointmentChange={handleAppointmentChange}
81
113
  resourceAppointmentClassMap={{
82
114
  "1": "bg-amber-100 dark:bg-amber-950/40",
83
- "2": "bg-blue-100 dark:bg-blue-950/40"
115
+ "2": "bg-blue-100 dark:bg-blue-950/40",
84
116
  }}
85
117
  dayStart="09:00"
86
118
  dayEnd="18:00"
@@ -90,11 +122,13 @@ export function SchedulerExample() {
90
122
  ```
91
123
 
92
124
  `renderAppointment` is optional. If you omit it, the scheduler uses the built-in appointment card with light/dark mode support.
125
+ During move drag, the scheduler keeps the original card in place and renders a floating ghost preview by default.
93
126
  `renderToolbar` is optional. If you omit it, the scheduler uses a built-in shadcn-style toolbar with prev/next buttons and a calendar popover date picker.
94
127
  `renderDatePicker` is optional. If you omit it, the default toolbar uses the built-in calendar popover date picker.
95
- `onAppointmentChange` is a simple callback: use it to update your state and run your own side effects.
128
+ `onAppointmentChange` is the main integration point for state updates and API persistence.
96
129
 
97
130
  Main props you need to pass:
131
+
98
132
  - `resources`: your columns (staff, rooms, etc), each with at least `{ id, label }`.
99
133
  - `appointments`: raw items from your backend.
100
134
  - `adapter`: functions that map your raw appointment shape to scheduler fields.
@@ -111,6 +145,15 @@ Main props you need to pass:
111
145
  - `renderResourceHeader`: optional render hook for resource column headers.
112
146
  - `renderAppointment`: optional render hook to fully customize appointment cards.
113
147
 
148
+ If you provide `renderAppointment`, the `appointment` argument also includes:
149
+
150
+ - `visualState`: `"normal" | "ghost" | "dragging"` so custom renderers can react to drag previews.
151
+ - `renderKey`: an internal stable render key for presentation-layer rendering.
152
+
153
+ The `renderAppointment` args also include:
154
+
155
+ - `isDropInvalid`: `true` while the current drag position overlaps another appointment, so custom renderers can show invalid drop feedback.
156
+
114
157
  ### Resource classes by id (recommended)
115
158
 
116
159
  ```tsx
@@ -118,7 +161,7 @@ Main props you need to pass:
118
161
  // ...other props
119
162
  resourceAppointmentClassMap={{
120
163
  "room-a": "bg-yellow-100 dark:bg-yellow-950/40",
121
- "room-b": "bg-rose-100 dark:bg-rose-950/40"
164
+ "room-b": "bg-rose-100 dark:bg-rose-950/40",
122
165
  }}
123
166
  />
124
167
  ```
@@ -131,7 +174,7 @@ Main props you need to pass:
131
174
  getResourceAppointmentColorToken={(resource) => resource.category}
132
175
  appointmentColorTokenClassMap={{
133
176
  vip: "bg-yellow-100 dark:bg-yellow-950/40",
134
- urgent: "bg-rose-100 dark:bg-rose-950/40"
177
+ urgent: "bg-rose-100 dark:bg-rose-950/40",
135
178
  }}
136
179
  />
137
180
  ```
@@ -141,21 +184,48 @@ Main props you need to pass:
141
184
  ```tsx
142
185
  <Scheduler
143
186
  // ...other props
144
- renderAppointment={({ appointment, onPointerDown, onResizePointerDown, appointmentAppearance, appointmentBackgroundColor }) => (
187
+ renderAppointment={({
188
+ appointment,
189
+ isDropInvalid,
190
+ onPointerDown,
191
+ onResizePointerDown,
192
+ appointmentAppearance,
193
+ appointmentBackgroundColor,
194
+ }) => (
145
195
  <div
146
196
  onPointerDown={onPointerDown}
147
- className={`relative h-full cursor-grab overflow-hidden rounded-md border border-slate-300 p-2 pb-5 ${
148
- appointmentAppearance?.className ?? appointmentBackgroundColor ?? "bg-slate-100"
197
+ className={`relative h-full overflow-hidden rounded-md border p-2 pb-5 ${
198
+ appointment.visualState === "ghost"
199
+ ? "cursor-grab border-dashed opacity-55"
200
+ : appointment.visualState === "dragging"
201
+ ? "cursor-grabbing opacity-55"
202
+ : "cursor-grab"
203
+ } ${
204
+ isDropInvalid
205
+ ? "border-red-500 bg-red-100/80 text-red-950 ring-1 ring-red-500/60"
206
+ : `border-slate-300 ${
207
+ appointmentAppearance?.className ?? appointmentBackgroundColor ?? "bg-slate-100"
208
+ }`
149
209
  }`}
150
210
  >
211
+ {isDropInvalid ? (
212
+ <div className="absolute right-1 top-1 flex h-4 w-4 items-center justify-center rounded-full bg-red-600 text-[10px] font-bold text-white">
213
+ X
214
+ </div>
215
+ ) : null}
151
216
  <div className="text-xs font-semibold">{appointment.title}</div>
152
217
  {appointment.raw.description ? (
153
- <div className="mt-1 text-[10px] font-medium text-slate-500">{appointment.raw.description}</div>
218
+ <div className="mt-1 text-[10px] font-medium text-slate-500">
219
+ {appointment.raw.description}
220
+ </div>
154
221
  ) : null}
155
222
  <div
156
223
  role="button"
157
224
  aria-label="Resize appointment"
158
225
  onPointerDown={(e) => {
226
+ if (appointment.visualState === "ghost") {
227
+ return;
228
+ }
159
229
  e.stopPropagation();
160
230
  onResizePointerDown(e);
161
231
  }}
@@ -178,7 +248,11 @@ Main props you need to pass:
178
248
  <div className="text-xs text-muted-foreground">Team schedule</div>
179
249
  </div>
180
250
  <div className="flex items-center gap-2">
181
- <button className="rounded-md border px-3 py-2 text-sm" type="button" onClick={goToPreviousDay}>
251
+ <button
252
+ className="rounded-md border px-3 py-2 text-sm"
253
+ type="button"
254
+ onClick={goToPreviousDay}
255
+ >
182
256
  Previous
183
257
  </button>
184
258
  {defaultDatePicker}
@@ -237,12 +311,6 @@ import "@marco.colia/react-fast-scheduler/styles.css";
237
311
  ReactDOM.createRoot(document.getElementById("root")!).render(<SchedulerExample />);
238
312
  ```
239
313
 
240
- If you work inside this library repo (local demo), import the Tailwind v4 source entry:
241
-
242
- ```tsx
243
- import "../src/global.css";
244
- ```
245
-
246
314
  You can override tokens in your app:
247
315
 
248
316
  ```css
@@ -257,23 +325,4 @@ You can override tokens in your app:
257
325
 
258
326
  ## Development
259
327
 
260
- ```bash
261
- npm install
262
- npm run dev:demo
263
- npm run lint
264
- npm run test
265
- npm run build
266
- ```
267
-
268
- `dev/main.tsx` contains a minimal runnable scheduler demo used by `npm run dev:demo`.
269
- Tailwind v4 source styles are in `src/global.css`, and the demo imports them from `dev/main.tsx`.
270
-
271
- ## Releasing
272
-
273
- This repo uses [Changesets](https://github.com/changesets/changesets) and GitHub Actions.
274
-
275
- 1. Create a changeset: `npx changeset`
276
- 2. Merge to `main`
277
- 3. The `Release` workflow opens/updates a release PR
278
- 4. Merging that PR publishes to npm via npm Trusted Publishing and GitHub OIDC
279
- 4. Merging that PR publishes to npm via npm Trusted Publishing and GitHub OIDC
328
+ Repo-specific development notes, local demo usage, quality checks, and release workflow are in [DEVELOPMENT.md](./DEVELOPMENT.md).