@gobrand/react-calendar 0.0.9 → 0.0.10
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 +607 -154
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,24 +1,54 @@
|
|
|
1
1
|
# @gobrand/react-calendar
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@gobrand/react-calendar)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
React hook for building powerful calendar views with the [Temporal API](https://tc39.es/proposal-temporal/docs/). Built on [@gobrand/calendar-core](https://www.npmjs.com/package/@gobrand/calendar-core) with optimized state management using TanStack Store.
|
|
4
7
|
|
|
5
8
|
## Installation
|
|
6
9
|
|
|
7
10
|
```bash
|
|
11
|
+
npm install @gobrand/react-calendar
|
|
12
|
+
# or
|
|
8
13
|
pnpm add @gobrand/react-calendar
|
|
9
14
|
```
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
16
|
+
**Peer dependencies:**
|
|
17
|
+
- `react ^18.0.0 || ^19.0.0`
|
|
18
|
+
|
|
19
|
+
## Why @gobrand/react-calendar?
|
|
20
|
+
|
|
21
|
+
Building React calendar UIs is complex: state management, timezone handling, DST transitions, multi-view navigation, and data mapping. **@gobrand/react-calendar** provides a complete React solution:
|
|
22
|
+
|
|
23
|
+
- **🪝 Simple React Hook** - Single `useCalendar()` hook with reactive state management
|
|
24
|
+
- **📅 Multi-view support** - Month, week, and day views with type-safe view switching
|
|
25
|
+
- **🌍 Timezone-aware** - Native timezone support with Temporal API primitives
|
|
26
|
+
- **🎯 Data-agnostic** - Works with any data type through accessor pattern
|
|
27
|
+
- **⚡️ Type-safe** - Full TypeScript support with conditional types based on configured views
|
|
28
|
+
- **⚙️ TanStack Store** - Optimized state management with TanStack Store
|
|
29
|
+
- **🔧 Zero config** - Sensible defaults, customize only what you need
|
|
30
|
+
- **📦 Minimal** - Built on [@gobrand/calendar-core](https://www.npmjs.com/package/@gobrand/calendar-core) for framework-agnostic logic
|
|
31
|
+
|
|
32
|
+
**Key features:**
|
|
33
|
+
- ✅ Built exclusively on Temporal API (no Date objects, no moment.js, no date-fns)
|
|
34
|
+
- ✅ Automatic DST handling and timezone conversions
|
|
35
|
+
- ✅ Type-safe view methods (only available methods for configured views)
|
|
36
|
+
- ✅ Calendar-aware arithmetic (leap years, month-end dates)
|
|
37
|
+
- ✅ Flexible accessor pattern for any data structure
|
|
38
|
+
- ✅ Polyfill included for browser compatibility
|
|
39
|
+
- ✅ All core utilities re-exported for convenience
|
|
40
|
+
|
|
41
|
+
**Perfect for:**
|
|
42
|
+
- React event calendars and schedulers
|
|
43
|
+
- Booking systems and appointment managers
|
|
44
|
+
- Task management with due dates
|
|
45
|
+
- Analytics dashboards with date ranges
|
|
46
|
+
- Any React app that needs calendar navigation
|
|
17
47
|
|
|
18
48
|
## Quick Start
|
|
19
49
|
|
|
20
50
|
```tsx
|
|
21
|
-
import { useCalendar, createCalendarViews, createCalendarAccessor } from '@gobrand/react-calendar';
|
|
51
|
+
import { useCalendar, createCalendarViews, createCalendarAccessor, getWeekdays } from '@gobrand/react-calendar';
|
|
22
52
|
import { Temporal } from '@js-temporal/polyfill';
|
|
23
53
|
|
|
24
54
|
type Event = {
|
|
@@ -27,17 +57,23 @@ type Event = {
|
|
|
27
57
|
start: Temporal.ZonedDateTime;
|
|
28
58
|
};
|
|
29
59
|
|
|
30
|
-
|
|
60
|
+
const events: Event[] = [
|
|
61
|
+
{
|
|
62
|
+
id: '1',
|
|
63
|
+
title: 'Team Meeting',
|
|
64
|
+
start: Temporal.ZonedDateTime.from('2025-01-20T14:00:00-05:00[America/New_York]')
|
|
65
|
+
}
|
|
66
|
+
];
|
|
67
|
+
|
|
31
68
|
const accessor = createCalendarAccessor<Event>({
|
|
32
69
|
getDate: (event) => event.start.toPlainDate(),
|
|
33
70
|
getStart: (event) => event.start,
|
|
34
|
-
getEnd: (event) => event.start,
|
|
35
71
|
});
|
|
36
72
|
|
|
37
73
|
function MyCalendar() {
|
|
38
74
|
const calendar = useCalendar({
|
|
39
75
|
data: events,
|
|
40
|
-
views: createCalendarViews({
|
|
76
|
+
views: createCalendarViews<Event>()({
|
|
41
77
|
month: { weekStartsOn: 1, accessor },
|
|
42
78
|
}),
|
|
43
79
|
});
|
|
@@ -78,126 +114,336 @@ function MyCalendar() {
|
|
|
78
114
|
}
|
|
79
115
|
```
|
|
80
116
|
|
|
81
|
-
|
|
117
|
+
> **Note:** For vanilla JavaScript/TypeScript usage, check out [@gobrand/calendar-core](https://www.npmjs.com/package/@gobrand/calendar-core) which provides the underlying framework-agnostic functions.
|
|
118
|
+
|
|
119
|
+
## API
|
|
120
|
+
|
|
121
|
+
### React Hooks
|
|
82
122
|
|
|
123
|
+
#### `useCalendar(options)`
|
|
124
|
+
|
|
125
|
+
Create a calendar instance with reactive state management. Returns a Calendar object with type-safe methods based on configured views.
|
|
126
|
+
|
|
127
|
+
**Parameters:**
|
|
128
|
+
- `options` (CalendarOptions): Configuration object
|
|
129
|
+
- `data` (TItem[]): Array of items to display in the calendar
|
|
130
|
+
- `views` (CalendarViewOptions): View configurations (month, week, day)
|
|
131
|
+
- `timeZone` (string, optional): IANA timezone identifier (defaults to system timezone)
|
|
132
|
+
- `state` (Partial<CalendarState>, optional): Initial state override
|
|
133
|
+
- `onStateChange` (function, optional): Callback when state changes
|
|
134
|
+
|
|
135
|
+
**Returns:** `Calendar<TItem, TOptions>` - Calendar instance with conditional methods
|
|
136
|
+
|
|
137
|
+
**Example:**
|
|
83
138
|
```tsx
|
|
84
|
-
import { useCalendar, createCalendarViews } from '@gobrand/react-calendar';
|
|
139
|
+
import { useCalendar, createCalendarViews, createCalendarAccessor } from '@gobrand/react-calendar';
|
|
140
|
+
|
|
141
|
+
type Event = {
|
|
142
|
+
id: string;
|
|
143
|
+
title: string;
|
|
144
|
+
start: Temporal.ZonedDateTime;
|
|
145
|
+
end?: Temporal.ZonedDateTime;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const accessor = createCalendarAccessor<Event>({
|
|
149
|
+
getDate: (event) => event.start.toPlainDate(),
|
|
150
|
+
getStart: (event) => event.start,
|
|
151
|
+
getEnd: (event) => event.end,
|
|
152
|
+
});
|
|
85
153
|
|
|
86
154
|
const calendar = useCalendar({
|
|
87
155
|
data: events,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
156
|
+
timeZone: 'America/New_York',
|
|
157
|
+
views: createCalendarViews<Event>()({
|
|
158
|
+
month: { weekStartsOn: 1, accessor },
|
|
159
|
+
week: { weekStartsOn: 1, startHour: 8, endHour: 18, accessor },
|
|
160
|
+
day: { startHour: 8, endHour: 18, slotDuration: 30, accessor },
|
|
92
161
|
}),
|
|
93
162
|
});
|
|
94
163
|
|
|
95
|
-
//
|
|
96
|
-
calendar.
|
|
164
|
+
// Type-safe methods - only available if view is configured
|
|
165
|
+
calendar.getMonth(); // ✓ Available (month view configured)
|
|
166
|
+
calendar.getWeek(); // ✓ Available (week view configured)
|
|
167
|
+
calendar.getDay(); // ✓ Available (day view configured)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Core Calendar Methods
|
|
171
|
+
|
|
172
|
+
#### View Methods
|
|
173
|
+
|
|
174
|
+
##### `getMonth()`
|
|
97
175
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
176
|
+
Get the current month view. Only available if month view is configured.
|
|
177
|
+
|
|
178
|
+
**Returns:** `CalendarMonth<TItem>` - Month grid with weeks and days
|
|
179
|
+
- `weeks` - Array of weeks, each containing 7 days
|
|
180
|
+
- `month` - Temporal.PlainYearMonth for the current month
|
|
181
|
+
|
|
182
|
+
**Example:**
|
|
183
|
+
```tsx
|
|
184
|
+
const month = calendar.getMonth();
|
|
185
|
+
|
|
186
|
+
month.weeks.forEach(week => {
|
|
187
|
+
week.forEach(day => {
|
|
188
|
+
console.log(day.date, day.items, day.isToday, day.isCurrentMonth);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
102
191
|
```
|
|
103
192
|
|
|
104
|
-
|
|
193
|
+
##### `getWeek()`
|
|
105
194
|
|
|
106
|
-
|
|
195
|
+
Get the current week view. Only available if week view is configured.
|
|
107
196
|
|
|
108
|
-
|
|
197
|
+
**Returns:** `CalendarWeekView<TItem>` - Week with days and optional time slots
|
|
198
|
+
- `days` - Array of 7 days in the week
|
|
199
|
+
- `weekStart` - Temporal.PlainDate for the first day
|
|
200
|
+
- `weekEnd` - Temporal.PlainDate for the last day
|
|
109
201
|
|
|
110
|
-
**
|
|
202
|
+
**Example:**
|
|
111
203
|
```tsx
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
204
|
+
const week = calendar.getWeek();
|
|
205
|
+
|
|
206
|
+
week.days.forEach(day => {
|
|
207
|
+
console.log(day.date, day.items);
|
|
208
|
+
day.timeSlots?.forEach(slot => {
|
|
209
|
+
console.log(slot.hour, slot.minute, slot.items);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
119
212
|
```
|
|
120
213
|
|
|
121
|
-
|
|
214
|
+
##### `getDay()`
|
|
122
215
|
|
|
123
|
-
|
|
124
|
-
- `getState()` - Get current state
|
|
125
|
-
- `setState(updater)` - Update state
|
|
126
|
-
- `goToToday()` - Navigate to today
|
|
127
|
-
- `goToDate(date)` - Navigate to specific date
|
|
128
|
-
- `getTitle(view)` - Get formatted title for view
|
|
129
|
-
- `dateRange` - Current date range (for data fetching)
|
|
130
|
-
- `currentView` - Current view name
|
|
131
|
-
- `setCurrentView(view)` - Switch views
|
|
216
|
+
Get the current day view. Only available if day view is configured.
|
|
132
217
|
|
|
133
|
-
**
|
|
218
|
+
**Returns:** `CalendarDayView<TItem>` - Day with time slots
|
|
219
|
+
- `date` - Temporal.PlainDate for the day
|
|
220
|
+
- `isToday` - Boolean indicating if this is today
|
|
221
|
+
- `timeSlots` - Array of time slots with items
|
|
222
|
+
- `items` - All items for this day
|
|
134
223
|
|
|
135
|
-
**
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
- `goToMonth(year, month)`
|
|
224
|
+
**Example:**
|
|
225
|
+
```tsx
|
|
226
|
+
const day = calendar.getDay();
|
|
139
227
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
228
|
+
console.log(day.date, day.isToday);
|
|
229
|
+
day.timeSlots.forEach(slot => {
|
|
230
|
+
console.log(`${slot.hour}:${slot.minute}`, slot.items);
|
|
231
|
+
});
|
|
232
|
+
```
|
|
143
233
|
|
|
144
|
-
|
|
145
|
-
- `getDay()` - Returns `{ date, timeSlots: { hour, minute, items: T[] }[] }`
|
|
146
|
-
- `nextDay()` / `previousDay()`
|
|
234
|
+
#### Navigation Methods
|
|
147
235
|
|
|
148
|
-
|
|
236
|
+
##### Month Navigation
|
|
149
237
|
|
|
150
|
-
|
|
238
|
+
```tsx
|
|
239
|
+
calendar.nextMonth(); // Go to next month
|
|
240
|
+
calendar.previousMonth(); // Go to previous month
|
|
241
|
+
calendar.goToMonth(2025, 6); // Go to specific month (year, month)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
##### Week Navigation
|
|
151
245
|
|
|
152
246
|
```tsx
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
247
|
+
calendar.nextWeek(); // Go to next week
|
|
248
|
+
calendar.previousWeek(); // Go to previous week
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
##### Day Navigation
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
calendar.nextDay(); // Go to next day
|
|
255
|
+
calendar.previousDay(); // Go to previous day
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
##### Universal Navigation
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
calendar.goToToday(); // Go to today (works for all views)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
#### View Management
|
|
265
|
+
|
|
266
|
+
##### `setCurrentView(view)`
|
|
267
|
+
|
|
268
|
+
Switch between configured views.
|
|
269
|
+
|
|
270
|
+
**Parameters:**
|
|
271
|
+
- `view` (string): One of the configured view names ('month' | 'week' | 'day')
|
|
272
|
+
|
|
273
|
+
**Example:**
|
|
274
|
+
```tsx
|
|
275
|
+
calendar.setCurrentView('week'); // Switch to week view
|
|
276
|
+
calendar.setCurrentView('month'); // Switch to month view
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
##### `getTitle(view?, locales?, options?)`
|
|
280
|
+
|
|
281
|
+
Get a formatted title for the current view or a specific view.
|
|
282
|
+
|
|
283
|
+
**Parameters:**
|
|
284
|
+
- `view` (string, optional): View name (defaults to current view)
|
|
285
|
+
- `locales` (string | string[], optional): Locale(s) for formatting
|
|
286
|
+
- `options` (Intl.DateTimeFormatOptions, optional): Formatting options
|
|
287
|
+
|
|
288
|
+
**Returns:** `string` - Formatted title
|
|
289
|
+
|
|
290
|
+
**Example:**
|
|
291
|
+
```tsx
|
|
292
|
+
calendar.getTitle('month'); // "January 2025"
|
|
293
|
+
calendar.getTitle('month', 'es-ES'); // "enero de 2025"
|
|
294
|
+
calendar.getTitle('week'); // "Jan 20 – 26, 2025"
|
|
295
|
+
calendar.getTitle('day', 'en-US', {
|
|
296
|
+
weekday: 'long',
|
|
297
|
+
year: 'numeric',
|
|
298
|
+
month: 'long',
|
|
299
|
+
day: 'numeric'
|
|
300
|
+
}); // "Monday, January 20, 2025"
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
#### State Management
|
|
304
|
+
|
|
305
|
+
##### `getState()`
|
|
306
|
+
|
|
307
|
+
Get the current calendar state.
|
|
308
|
+
|
|
309
|
+
**Returns:** `CalendarState` - Current state object
|
|
310
|
+
- `currentView` - Active view name
|
|
311
|
+
- `referenceDate` - Current reference date (PlainDate)
|
|
312
|
+
|
|
313
|
+
##### `setState(updater)`
|
|
314
|
+
|
|
315
|
+
Update the calendar state.
|
|
316
|
+
|
|
317
|
+
**Parameters:**
|
|
318
|
+
- `updater` (function | object): State update function or partial state object
|
|
319
|
+
|
|
320
|
+
**Example:**
|
|
321
|
+
```tsx
|
|
322
|
+
// Function updater
|
|
323
|
+
calendar.setState(state => ({
|
|
324
|
+
...state,
|
|
325
|
+
referenceDate: Temporal.PlainDate.from('2025-12-25')
|
|
326
|
+
}));
|
|
327
|
+
|
|
328
|
+
// Object updater
|
|
329
|
+
calendar.setState({ currentView: 'week' });
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Accessor Pattern
|
|
333
|
+
|
|
334
|
+
#### `createCalendarAccessor(accessor)`
|
|
335
|
+
|
|
336
|
+
Create a type-safe accessor for mapping your data to calendar dates. This is a type-identity function for TypeScript inference.
|
|
337
|
+
|
|
338
|
+
**Parameters:**
|
|
339
|
+
- `accessor` (CalendarAccessor<TItem>): Accessor configuration
|
|
340
|
+
- `getDate` (required): Extract PlainDate from item
|
|
341
|
+
- `getStart` (optional): Extract ZonedDateTime start time
|
|
342
|
+
- `getEnd` (optional): Extract ZonedDateTime end time
|
|
343
|
+
|
|
344
|
+
**Returns:** Same accessor object with proper types
|
|
345
|
+
|
|
346
|
+
**Example:**
|
|
347
|
+
```tsx
|
|
348
|
+
// Simple date-based items (tasks, posts)
|
|
349
|
+
type Task = {
|
|
350
|
+
id: string;
|
|
351
|
+
name: string;
|
|
352
|
+
dueDate: Temporal.PlainDate;
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const taskAccessor = createCalendarAccessor<Task>({
|
|
356
|
+
getDate: (task) => task.dueDate,
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Time-based items with start time (events, appointments)
|
|
360
|
+
type Event = {
|
|
361
|
+
id: string;
|
|
362
|
+
title: string;
|
|
363
|
+
start: Temporal.ZonedDateTime;
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const eventAccessor = createCalendarAccessor<Event>({
|
|
367
|
+
getDate: (event) => event.start.toPlainDate(),
|
|
368
|
+
getStart: (event) => event.start,
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Items with start and end times (meetings, bookings)
|
|
372
|
+
type Meeting = {
|
|
373
|
+
id: string;
|
|
374
|
+
title: string;
|
|
375
|
+
start: Temporal.ZonedDateTime;
|
|
376
|
+
end: Temporal.ZonedDateTime;
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const meetingAccessor = createCalendarAccessor<Meeting>({
|
|
380
|
+
getDate: (meeting) => meeting.start.toPlainDate(),
|
|
381
|
+
getStart: (meeting) => meeting.start,
|
|
382
|
+
getEnd: (meeting) => meeting.end,
|
|
171
383
|
});
|
|
172
384
|
```
|
|
173
385
|
|
|
174
|
-
###
|
|
386
|
+
### View Configuration
|
|
387
|
+
|
|
388
|
+
#### `createCalendarViews()`
|
|
175
389
|
|
|
176
|
-
|
|
390
|
+
Create type-safe view configurations. This is a curried function that requires a type parameter.
|
|
177
391
|
|
|
392
|
+
**Usage:**
|
|
178
393
|
```tsx
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
394
|
+
const views = createCalendarViews<TItem>()({
|
|
395
|
+
month?: { ... },
|
|
396
|
+
week?: { ... },
|
|
397
|
+
day?: { ... }
|
|
183
398
|
});
|
|
184
399
|
```
|
|
185
400
|
|
|
186
|
-
|
|
401
|
+
#### Month View Options
|
|
402
|
+
|
|
403
|
+
```tsx
|
|
404
|
+
type MonthViewOptions<TItem> = {
|
|
405
|
+
accessor: CalendarAccessor<TItem>;
|
|
406
|
+
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6; // 0 = Sunday, 1 = Monday, etc.
|
|
407
|
+
};
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
#### Week View Options
|
|
411
|
+
|
|
412
|
+
```tsx
|
|
413
|
+
type WeekViewOptions<TItem> = {
|
|
414
|
+
accessor: CalendarAccessor<TItem>;
|
|
415
|
+
weekStartsOn?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
416
|
+
startHour?: number; // Default: 0
|
|
417
|
+
endHour?: number; // Default: 24
|
|
418
|
+
slotDuration?: number; // Minutes per slot, default: 60
|
|
419
|
+
};
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
#### Day View Options
|
|
187
423
|
|
|
188
|
-
|
|
424
|
+
```tsx
|
|
425
|
+
type DayViewOptions<TItem> = {
|
|
426
|
+
accessor: CalendarAccessor<TItem>;
|
|
427
|
+
startHour?: number; // Default: 0
|
|
428
|
+
endHour?: number; // Default: 24
|
|
429
|
+
slotDuration?: number; // Minutes per slot, default: 60
|
|
430
|
+
};
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### Core Utilities (Re-exported)
|
|
434
|
+
|
|
435
|
+
All utilities from [@gobrand/calendar-core](https://www.npmjs.com/package/@gobrand/calendar-core) are re-exported for convenience. For detailed documentation on these functions, see the [core package documentation](https://www.npmjs.com/package/@gobrand/calendar-core).
|
|
436
|
+
|
|
437
|
+
**Building Functions:**
|
|
438
|
+
- `buildMonth(year, month, options?)` - Build month grid
|
|
439
|
+
- `buildWeek(date, options?)` - Build week view
|
|
440
|
+
- `buildDay(date, options?)` - Build day view with time slots
|
|
189
441
|
|
|
190
442
|
**Formatting:**
|
|
191
|
-
- `getWeekdays(weekStartsOn?)` - Localized weekday names
|
|
443
|
+
- `getWeekdays(weekStartsOn?, locale?, format?)` - Localized weekday names
|
|
192
444
|
- `getMonthName(month, locale?)` - Localized month name
|
|
193
445
|
- `formatTime(time, locale?)` - Format PlainTime
|
|
194
446
|
|
|
195
|
-
**Navigation:**
|
|
196
|
-
- `nextMonth(month)` / `previousMonth(month)`
|
|
197
|
-
- `nextWeek(date)` / `previousWeek(date)`
|
|
198
|
-
- `nextDay(date)` / `previousDay(date)`
|
|
199
|
-
- `goToToday()` - Get current PlainDate
|
|
200
|
-
|
|
201
447
|
**Timezone:**
|
|
202
448
|
- `getMonthRange(timeZone?, weekStartsOn?)` - Week-aligned month range
|
|
203
449
|
- `getWeekRange(timeZone?, weekStartsOn?)` - Current week range
|
|
@@ -206,98 +452,305 @@ All utilities from `@gobrand/calendar-core` are re-exported:
|
|
|
206
452
|
- `convertToTimezone(zdt, timeZone)` - Convert between timezones
|
|
207
453
|
- `createZonedDateTime(date, time, timeZone)` - Create ZonedDateTime
|
|
208
454
|
|
|
209
|
-
|
|
210
|
-
- `getEventPosition(start, end, startHour, endHour, slotDuration)` - Calculate event positioning for time-based grids
|
|
455
|
+
For detailed documentation and examples, see [@gobrand/calendar-core](https://www.npmjs.com/package/@gobrand/calendar-core).
|
|
211
456
|
|
|
212
|
-
##
|
|
457
|
+
## Real World Examples
|
|
213
458
|
|
|
214
|
-
###
|
|
459
|
+
### Example 1: Event Calendar with Multi-View Support
|
|
460
|
+
|
|
461
|
+
A complete event calendar with month, week, and day views, timezone support, and type-safe view switching.
|
|
215
462
|
|
|
216
463
|
```tsx
|
|
217
|
-
{
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}
|
|
223
|
-
|
|
464
|
+
import {
|
|
465
|
+
useCalendar,
|
|
466
|
+
createCalendarViews,
|
|
467
|
+
createCalendarAccessor,
|
|
468
|
+
getWeekdays
|
|
469
|
+
} from '@gobrand/react-calendar';
|
|
470
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
471
|
+
|
|
472
|
+
type Event = {
|
|
473
|
+
id: string;
|
|
474
|
+
title: string;
|
|
475
|
+
description?: string;
|
|
476
|
+
start: Temporal.ZonedDateTime;
|
|
477
|
+
end: Temporal.ZonedDateTime;
|
|
478
|
+
};
|
|
224
479
|
|
|
225
|
-
|
|
480
|
+
const events: Event[] = [
|
|
481
|
+
{
|
|
482
|
+
id: '1',
|
|
483
|
+
title: 'Team Standup',
|
|
484
|
+
start: Temporal.ZonedDateTime.from('2025-01-20T09:00:00-05:00[America/New_York]'),
|
|
485
|
+
end: Temporal.ZonedDateTime.from('2025-01-20T09:30:00-05:00[America/New_York]')
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
id: '2',
|
|
489
|
+
title: 'Client Meeting',
|
|
490
|
+
start: Temporal.ZonedDateTime.from('2025-01-20T14:00:00-05:00[America/New_York]'),
|
|
491
|
+
end: Temporal.ZonedDateTime.from('2025-01-20T15:00:00-05:00[America/New_York]')
|
|
492
|
+
}
|
|
493
|
+
];
|
|
226
494
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
495
|
+
const accessor = createCalendarAccessor<Event>({
|
|
496
|
+
getDate: (event) => event.start.toPlainDate(),
|
|
497
|
+
getStart: (event) => event.start,
|
|
498
|
+
getEnd: (event) => event.end,
|
|
499
|
+
});
|
|
230
500
|
|
|
231
|
-
|
|
501
|
+
function EventCalendar() {
|
|
502
|
+
const calendar = useCalendar({
|
|
503
|
+
data: events,
|
|
504
|
+
timeZone: 'America/New_York',
|
|
505
|
+
views: createCalendarViews<Event>()({
|
|
506
|
+
month: { weekStartsOn: 0, accessor },
|
|
507
|
+
week: { weekStartsOn: 0, startHour: 8, endHour: 18, slotDuration: 30, accessor },
|
|
508
|
+
day: { startHour: 8, endHour: 18, slotDuration: 30, accessor },
|
|
509
|
+
}),
|
|
510
|
+
});
|
|
232
511
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
512
|
+
const currentView = calendar.getState().currentView;
|
|
513
|
+
|
|
514
|
+
return (
|
|
515
|
+
<div className="calendar">
|
|
516
|
+
{/* Header with view switcher */}
|
|
517
|
+
<header>
|
|
518
|
+
<div className="view-buttons">
|
|
519
|
+
<button
|
|
520
|
+
onClick={() => calendar.setCurrentView('month')}
|
|
521
|
+
className={currentView === 'month' ? 'active' : ''}
|
|
522
|
+
>
|
|
523
|
+
Month
|
|
524
|
+
</button>
|
|
525
|
+
<button
|
|
526
|
+
onClick={() => calendar.setCurrentView('week')}
|
|
527
|
+
className={currentView === 'week' ? 'active' : ''}
|
|
528
|
+
>
|
|
529
|
+
Week
|
|
530
|
+
</button>
|
|
531
|
+
<button
|
|
532
|
+
onClick={() => calendar.setCurrentView('day')}
|
|
533
|
+
className={currentView === 'day' ? 'active' : ''}
|
|
534
|
+
>
|
|
535
|
+
Day
|
|
536
|
+
</button>
|
|
537
|
+
</div>
|
|
538
|
+
|
|
539
|
+
<h2>{calendar.getTitle()}</h2>
|
|
540
|
+
|
|
541
|
+
<div className="nav-buttons">
|
|
542
|
+
{currentView === 'month' && (
|
|
543
|
+
<>
|
|
544
|
+
<button onClick={calendar.previousMonth}>←</button>
|
|
545
|
+
<button onClick={calendar.goToToday}>Today</button>
|
|
546
|
+
<button onClick={calendar.nextMonth}>→</button>
|
|
547
|
+
</>
|
|
548
|
+
)}
|
|
549
|
+
{currentView === 'week' && (
|
|
550
|
+
<>
|
|
551
|
+
<button onClick={calendar.previousWeek}>←</button>
|
|
552
|
+
<button onClick={calendar.goToToday}>Today</button>
|
|
553
|
+
<button onClick={calendar.nextWeek}>→</button>
|
|
554
|
+
</>
|
|
555
|
+
)}
|
|
556
|
+
{currentView === 'day' && (
|
|
557
|
+
<>
|
|
558
|
+
<button onClick={calendar.previousDay}>←</button>
|
|
559
|
+
<button onClick={calendar.goToToday}>Today</button>
|
|
560
|
+
<button onClick={calendar.nextDay}>→</button>
|
|
561
|
+
</>
|
|
562
|
+
)}
|
|
563
|
+
</div>
|
|
564
|
+
</header>
|
|
565
|
+
|
|
566
|
+
{/* Month View */}
|
|
567
|
+
{currentView === 'month' && (
|
|
568
|
+
<div className="month-view">
|
|
569
|
+
<div className="weekday-headers">
|
|
570
|
+
{getWeekdays(0).map(day => (
|
|
571
|
+
<div key={day} className="weekday">{day}</div>
|
|
572
|
+
))}
|
|
573
|
+
</div>
|
|
574
|
+
<div className="month-grid">
|
|
575
|
+
{calendar.getMonth().weeks.flat().map(day => (
|
|
576
|
+
<div
|
|
577
|
+
key={day.date.toString()}
|
|
578
|
+
className={`day ${!day.isCurrentMonth ? 'other-month' : ''} ${day.isToday ? 'today' : ''}`}
|
|
579
|
+
>
|
|
580
|
+
<div className="day-number">{day.date.day}</div>
|
|
581
|
+
<div className="events">
|
|
582
|
+
{day.items.map(event => (
|
|
583
|
+
<div key={event.id} className="event">
|
|
584
|
+
{event.title}
|
|
585
|
+
</div>
|
|
586
|
+
))}
|
|
587
|
+
</div>
|
|
588
|
+
</div>
|
|
589
|
+
))}
|
|
590
|
+
</div>
|
|
591
|
+
</div>
|
|
592
|
+
)}
|
|
593
|
+
|
|
594
|
+
{/* Week View */}
|
|
595
|
+
{currentView === 'week' && (
|
|
596
|
+
<div className="week-view">
|
|
597
|
+
<div className="weekday-headers">
|
|
598
|
+
{calendar.getWeek().days.map(day => (
|
|
599
|
+
<div key={day.date.toString()} className="weekday">
|
|
600
|
+
<div>{day.date.toLocaleString('en-US', { weekday: 'short' })}</div>
|
|
601
|
+
<div className={day.isToday ? 'today' : ''}>{day.date.day}</div>
|
|
602
|
+
</div>
|
|
603
|
+
))}
|
|
604
|
+
</div>
|
|
605
|
+
<div className="week-grid">
|
|
606
|
+
{calendar.getWeek().days.map(day => (
|
|
607
|
+
<div key={day.date.toString()} className="day-column">
|
|
608
|
+
{day.timeSlots?.map((slot, i) => (
|
|
609
|
+
<div key={i} className="time-slot">
|
|
610
|
+
{slot.items.map(event => (
|
|
611
|
+
<div key={event.id} className="event">
|
|
612
|
+
{event.title}
|
|
613
|
+
</div>
|
|
614
|
+
))}
|
|
615
|
+
</div>
|
|
616
|
+
))}
|
|
617
|
+
</div>
|
|
618
|
+
))}
|
|
619
|
+
</div>
|
|
620
|
+
</div>
|
|
621
|
+
)}
|
|
622
|
+
|
|
623
|
+
{/* Day View */}
|
|
624
|
+
{currentView === 'day' && (
|
|
625
|
+
<div className="day-view">
|
|
626
|
+
{calendar.getDay().timeSlots.map((slot, i) => (
|
|
627
|
+
<div key={i} className="time-slot">
|
|
628
|
+
<div className="time">{slot.hour}:{String(slot.minute).padStart(2, '0')}</div>
|
|
629
|
+
<div className="slot-events">
|
|
630
|
+
{slot.items.map(event => (
|
|
631
|
+
<div key={event.id} className="event">
|
|
632
|
+
<strong>{event.title}</strong>
|
|
633
|
+
{event.description && <p>{event.description}</p>}
|
|
634
|
+
</div>
|
|
635
|
+
))}
|
|
636
|
+
</div>
|
|
637
|
+
</div>
|
|
638
|
+
))}
|
|
639
|
+
</div>
|
|
640
|
+
)}
|
|
641
|
+
</div>
|
|
642
|
+
);
|
|
237
643
|
}
|
|
238
644
|
```
|
|
239
645
|
|
|
240
|
-
###
|
|
646
|
+
### Example 2: Task Due Date Calendar
|
|
647
|
+
|
|
648
|
+
Simple calendar showing task due dates without time information.
|
|
241
649
|
|
|
242
650
|
```tsx
|
|
243
|
-
{
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
651
|
+
import { useCalendar, createCalendarViews, createCalendarAccessor } from '@gobrand/react-calendar';
|
|
652
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
653
|
+
|
|
654
|
+
type Task = {
|
|
655
|
+
id: string;
|
|
656
|
+
title: string;
|
|
657
|
+
dueDate: Temporal.PlainDate;
|
|
658
|
+
completed: boolean;
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
const tasks: Task[] = [
|
|
662
|
+
{ id: '1', title: 'Review PR #42', dueDate: Temporal.PlainDate.from('2025-01-20'), completed: false },
|
|
663
|
+
{ id: '2', title: 'Write documentation', dueDate: Temporal.PlainDate.from('2025-01-22'), completed: false },
|
|
664
|
+
{ id: '3', title: 'Deploy to production', dueDate: Temporal.PlainDate.from('2025-01-25'), completed: true },
|
|
665
|
+
];
|
|
666
|
+
|
|
667
|
+
function TaskCalendar() {
|
|
668
|
+
const calendar = useCalendar({
|
|
669
|
+
data: tasks,
|
|
670
|
+
views: createCalendarViews<Task>()({
|
|
671
|
+
month: {
|
|
672
|
+
weekStartsOn: 1,
|
|
673
|
+
accessor: createCalendarAccessor({
|
|
674
|
+
getDate: (task) => task.dueDate,
|
|
675
|
+
})
|
|
676
|
+
},
|
|
677
|
+
}),
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
const month = calendar.getMonth();
|
|
681
|
+
|
|
682
|
+
return (
|
|
683
|
+
<div>
|
|
684
|
+
<header>
|
|
685
|
+
<button onClick={calendar.previousMonth}>Previous</button>
|
|
686
|
+
<h2>{calendar.getTitle('month')}</h2>
|
|
687
|
+
<button onClick={calendar.nextMonth}>Next</button>
|
|
688
|
+
</header>
|
|
689
|
+
|
|
690
|
+
<div className="calendar-grid">
|
|
691
|
+
{month.weeks.flat().map(day => (
|
|
692
|
+
<div
|
|
693
|
+
key={day.date.toString()}
|
|
694
|
+
className={!day.isCurrentMonth ? 'dimmed' : ''}
|
|
695
|
+
>
|
|
696
|
+
<div>{day.date.day}</div>
|
|
697
|
+
{day.items.map(task => (
|
|
698
|
+
<div
|
|
699
|
+
key={task.id}
|
|
700
|
+
className={task.completed ? 'completed' : 'pending'}
|
|
701
|
+
>
|
|
702
|
+
{task.title}
|
|
703
|
+
</div>
|
|
704
|
+
))}
|
|
705
|
+
</div>
|
|
706
|
+
))}
|
|
707
|
+
</div>
|
|
708
|
+
</div>
|
|
709
|
+
);
|
|
247
710
|
}
|
|
248
711
|
```
|
|
249
712
|
|
|
250
|
-
|
|
713
|
+
> **For vanilla JavaScript examples:** Check out [@gobrand/calendar-core documentation](https://www.npmjs.com/package/@gobrand/calendar-core) for framework-agnostic usage examples.
|
|
251
714
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
items: T[]; // Items overlapping this time slot
|
|
259
|
-
}[];
|
|
260
|
-
}
|
|
715
|
+
## Browser Support
|
|
716
|
+
|
|
717
|
+
The Temporal API is a Stage 3 TC39 proposal. The polyfill `@js-temporal/polyfill` is included as a dependency, ensuring compatibility across all modern browsers.
|
|
718
|
+
|
|
719
|
+
```typescript
|
|
720
|
+
import { Temporal } from '@js-temporal/polyfill';
|
|
261
721
|
```
|
|
262
722
|
|
|
263
|
-
|
|
723
|
+
**Requirements:**
|
|
724
|
+
- React 18+ or React 19+
|
|
725
|
+
- Modern browsers with ES2015+ support
|
|
264
726
|
|
|
265
|
-
|
|
727
|
+
## Development
|
|
266
728
|
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
data: events,
|
|
271
|
-
views: createCalendarViews({ month: { ... } }),
|
|
272
|
-
});
|
|
729
|
+
```bash
|
|
730
|
+
# Install dependencies
|
|
731
|
+
pnpm install
|
|
273
732
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
calendar.getWeek(); // ❌ Type error - week view not configured
|
|
733
|
+
# Build all packages
|
|
734
|
+
pnpm build
|
|
277
735
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
data: events,
|
|
281
|
-
views: createCalendarViews({
|
|
282
|
-
month: { ... },
|
|
283
|
-
week: { ... },
|
|
284
|
-
day: { ... },
|
|
285
|
-
}),
|
|
286
|
-
});
|
|
736
|
+
# Run tests
|
|
737
|
+
pnpm test --run
|
|
287
738
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
739
|
+
# Type check
|
|
740
|
+
pnpm typecheck
|
|
741
|
+
|
|
742
|
+
# Release new version
|
|
743
|
+
pnpm release <patch|minor|major>
|
|
291
744
|
```
|
|
292
745
|
|
|
293
|
-
##
|
|
746
|
+
## Contributing
|
|
294
747
|
|
|
295
|
-
|
|
296
|
-
- **Month view**: [PostMonthlyView.tsx](../../apps/demo/src/components/PostMonthlyView.tsx)
|
|
297
|
-
- **Week view**: [PostWeeklyView.tsx](../../apps/demo/src/components/PostWeeklyView.tsx)
|
|
298
|
-
- **Day view**: [PostDailyView.tsx](../../apps/demo/src/components/PostDailyView.tsx)
|
|
299
|
-
- **Custom view**: [PostAgendaView.tsx](../../apps/demo/src/components/PostAgendaView.tsx)
|
|
748
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
300
749
|
|
|
301
750
|
## License
|
|
302
751
|
|
|
303
752
|
MIT
|
|
753
|
+
|
|
754
|
+
## Built by Go Brand
|
|
755
|
+
|
|
756
|
+
temporal-calendar is built and maintained by [Go Brand](https://gobrand.app) - a modern social media management platform.
|