cleanhaus-calendar 1.0.0 → 1.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 +118 -152
- package/package.json +2 -2
- package/src/Calendar.tsx +18 -2
- package/src/DayView/hooks/useScrollSynchronization.ts +2 -2
- package/src/DayView/index.tsx +34 -31
- package/src/MonthView/OverflowIndicator.tsx +1 -1
- package/src/MonthView/index.tsx +3 -2
- package/src/MonthView/utils.ts +4 -3
- package/src/WeekView/OverflowIndicator.tsx +2 -2
- package/src/WeekView/index.tsx +18 -18
- package/src/hooks/useSwipeGesture.ts +3 -3
- package/src/shared/ErrorBoundary.tsx +0 -3
- package/src/shared/sparks.png +0 -0
- package/src/types/dayjs.d.ts +25 -0
- package/src/types/react-native.d.ts +22 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/validation.ts +203 -0
- package/tsup.config.ts +1 -1
package/README.md
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
# cleanhaus-calendar
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Cross-platform calendar component for React Native and Next.js with Month, Week, and Day views. Features horizontal time positioning, multi-day event spanning, and type-based event rendering.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
- ✅ **React Native** (iOS/Android)
|
|
8
|
-
- ✅ **Next.js** (Web with react-native-web)
|
|
9
|
-
|
|
10
|
-
## 📦 Installation
|
|
5
|
+
## Installation
|
|
11
6
|
|
|
12
7
|
```bash
|
|
13
8
|
npm install cleanhaus-calendar
|
|
@@ -19,23 +14,24 @@ npm install cleanhaus-calendar
|
|
|
19
14
|
npm install react react-native react-native-web react-native-reanimated dayjs calendarize
|
|
20
15
|
```
|
|
21
16
|
|
|
22
|
-
**
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
17
|
+
**Compatibility:**
|
|
18
|
+
- React >=18.0.0
|
|
19
|
+
- React Native >=0.70.0
|
|
20
|
+
- react-native-web >=0.19.0 (optional, for web)
|
|
21
|
+
- react-native-reanimated >=3.0.0
|
|
22
|
+
- dayjs ^1.11.0
|
|
23
|
+
- calendarize ^1.1.0
|
|
24
|
+
- Node >=18.0.0
|
|
30
25
|
|
|
31
|
-
##
|
|
26
|
+
## Quick Start
|
|
32
27
|
|
|
33
|
-
### React Native
|
|
28
|
+
### React Native
|
|
34
29
|
|
|
35
|
-
No additional configuration needed
|
|
30
|
+
No additional configuration needed. Install and use:
|
|
36
31
|
|
|
37
32
|
```tsx
|
|
38
33
|
import { Calendar, CalendarEvent } from "cleanhaus-calendar";
|
|
34
|
+
import { useState } from "react";
|
|
39
35
|
|
|
40
36
|
const events: CalendarEvent[] = [
|
|
41
37
|
{
|
|
@@ -48,24 +44,29 @@ const events: CalendarEvent[] = [
|
|
|
48
44
|
},
|
|
49
45
|
];
|
|
50
46
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
47
|
+
function App() {
|
|
48
|
+
const [date, setDate] = useState(new Date());
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Calendar
|
|
52
|
+
events={events}
|
|
53
|
+
view="month"
|
|
54
|
+
date={date}
|
|
55
|
+
onDateChange={setDate}
|
|
56
|
+
onEventPress={(event) => {
|
|
57
|
+
// Handle event press
|
|
58
|
+
}}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
58
62
|
```
|
|
59
63
|
|
|
60
|
-
### Next.js
|
|
64
|
+
### Next.js
|
|
61
65
|
|
|
62
|
-
**1.
|
|
63
|
-
```bash
|
|
64
|
-
npm install cleanhaus-calendar react react-native react-native-web react-native-reanimated dayjs calendarize
|
|
65
|
-
```
|
|
66
|
+
**1. Configure Next.js:**
|
|
66
67
|
|
|
67
|
-
**2. Update `next.config.ts`:**
|
|
68
68
|
```typescript
|
|
69
|
+
// next.config.ts
|
|
69
70
|
import type { NextConfig } from "next";
|
|
70
71
|
|
|
71
72
|
const withCalendar = require("cleanhaus-calendar/next-plugin");
|
|
@@ -77,7 +78,10 @@ const nextConfig: NextConfig = {
|
|
|
77
78
|
export default withCalendar(nextConfig);
|
|
78
79
|
```
|
|
79
80
|
|
|
80
|
-
**
|
|
81
|
+
**2. Development (Next.js 16+):**
|
|
82
|
+
|
|
83
|
+
Next.js 16+ uses Turbopack by default for development, but this package requires webpack configuration. Use the `--webpack` flag during development:
|
|
84
|
+
|
|
81
85
|
```json
|
|
82
86
|
{
|
|
83
87
|
"scripts": {
|
|
@@ -86,105 +90,69 @@ export default withCalendar(nextConfig);
|
|
|
86
90
|
}
|
|
87
91
|
```
|
|
88
92
|
|
|
89
|
-
**
|
|
90
|
-
```tsx
|
|
91
|
-
"use client"; // Required for App Router
|
|
93
|
+
**Note:** Production builds automatically use webpack, so no additional configuration is needed for deployment.
|
|
92
94
|
|
|
93
|
-
|
|
94
|
-
import { useState } from "react";
|
|
95
|
-
|
|
96
|
-
export default function MyPage() {
|
|
97
|
-
const [view, setView] = useState<ViewMode>("month");
|
|
98
|
-
const [date, setDate] = useState(new Date());
|
|
99
|
-
|
|
100
|
-
return (
|
|
101
|
-
<Calendar
|
|
102
|
-
events={events}
|
|
103
|
-
view={view}
|
|
104
|
-
date={date}
|
|
105
|
-
onDateChange={setDate}
|
|
106
|
-
onEventPress={(event) => console.log(event)}
|
|
107
|
-
onViewChange={setView}
|
|
108
|
-
/>
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
**Note:** Next.js 16+ uses Turbopack by default. The plugin requires webpack, so use `--webpack` flag.
|
|
114
|
-
|
|
115
|
-
## 📖 Usage
|
|
116
|
-
|
|
117
|
-
### Basic Example
|
|
95
|
+
**3. Use the component:**
|
|
118
96
|
|
|
119
97
|
```tsx
|
|
98
|
+
"use client";
|
|
99
|
+
|
|
120
100
|
import { Calendar, CalendarEvent, ViewMode } from "cleanhaus-calendar";
|
|
121
101
|
import { useState } from "react";
|
|
122
102
|
|
|
123
|
-
|
|
124
|
-
{
|
|
125
|
-
id: "1",
|
|
126
|
-
eventId: "property-1",
|
|
127
|
-
title: "Booking",
|
|
128
|
-
start: new Date(2025, 0, 15, 10, 0),
|
|
129
|
-
end: new Date(2025, 0, 20, 14, 0),
|
|
130
|
-
meta: { type: "property" },
|
|
131
|
-
},
|
|
132
|
-
];
|
|
133
|
-
|
|
134
|
-
function MyCalendar() {
|
|
103
|
+
export default function CalendarPage() {
|
|
135
104
|
const [view, setView] = useState<ViewMode>("month");
|
|
136
105
|
const [date, setDate] = useState(new Date());
|
|
137
106
|
|
|
107
|
+
const events: CalendarEvent[] = [
|
|
108
|
+
{
|
|
109
|
+
id: "1",
|
|
110
|
+
eventId: "property-1",
|
|
111
|
+
title: "Booking",
|
|
112
|
+
start: new Date(2025, 0, 15, 10, 0),
|
|
113
|
+
end: new Date(2025, 0, 20, 14, 0),
|
|
114
|
+
meta: { type: "property" },
|
|
115
|
+
},
|
|
116
|
+
];
|
|
117
|
+
|
|
138
118
|
return (
|
|
139
119
|
<Calendar
|
|
140
120
|
events={events}
|
|
141
121
|
view={view}
|
|
142
122
|
date={date}
|
|
143
123
|
onDateChange={setDate}
|
|
144
|
-
onEventPress={(event) => console.log(event)}
|
|
145
124
|
onViewChange={setView}
|
|
125
|
+
onEventPress={(event) => {
|
|
126
|
+
// Handle event press
|
|
127
|
+
}}
|
|
146
128
|
/>
|
|
147
129
|
);
|
|
148
130
|
}
|
|
149
131
|
```
|
|
150
132
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
The package includes a default sparkle icon (✨) for cleaning events. Override it:
|
|
154
|
-
|
|
155
|
-
```tsx
|
|
156
|
-
import sparksIcon from "./assets/sparks.png";
|
|
157
|
-
|
|
158
|
-
<Calendar
|
|
159
|
-
events={events}
|
|
160
|
-
cleaningIcon={sparksIcon} // Optional: custom icon
|
|
161
|
-
// ... other props
|
|
162
|
-
/>
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
## 🔄 Data Format
|
|
133
|
+
## Data Format
|
|
166
134
|
|
|
167
135
|
Events must follow this structure:
|
|
168
136
|
|
|
169
137
|
```typescript
|
|
170
138
|
interface CalendarEvent {
|
|
171
|
-
id: string; //
|
|
172
|
-
eventId: string; //
|
|
173
|
-
title: string; //
|
|
174
|
-
start: Date; //
|
|
175
|
-
end: Date; //
|
|
139
|
+
id: string; // Unique identifier
|
|
140
|
+
eventId: string; // Group identifier (e.g., "property-1")
|
|
141
|
+
title: string; // Event title
|
|
142
|
+
start: Date; // Start date/time (must be Date object)
|
|
143
|
+
end: Date; // End date/time (must be Date object)
|
|
176
144
|
meta?: {
|
|
177
145
|
type?: "property" | "cleaning" | "service" | "otherService" | "unassigned";
|
|
178
|
-
jobTypeId?: number; // For cleaning: 1 = cleaning, 2-4 = service types
|
|
179
146
|
[key: string]: any;
|
|
180
147
|
};
|
|
181
148
|
}
|
|
182
149
|
```
|
|
183
150
|
|
|
184
|
-
|
|
151
|
+
**Important:** Always convert date strings to `Date` objects. The component does not accept string dates.
|
|
152
|
+
|
|
153
|
+
### Transforming API Data
|
|
185
154
|
|
|
186
155
|
```typescript
|
|
187
|
-
// Transform API response to CalendarEvent format
|
|
188
156
|
function transformApiEvents(apiData: ApiEvent[]): CalendarEvent[] {
|
|
189
157
|
return apiData.map((item) => ({
|
|
190
158
|
id: item.id.toString(),
|
|
@@ -199,77 +167,75 @@ function transformApiEvents(apiData: ApiEvent[]): CalendarEvent[] {
|
|
|
199
167
|
}
|
|
200
168
|
```
|
|
201
169
|
|
|
202
|
-
|
|
170
|
+
## API Reference
|
|
171
|
+
|
|
172
|
+
### Calendar Props
|
|
203
173
|
|
|
204
|
-
|
|
174
|
+
| Prop | Type | Required | Default | Description |
|
|
175
|
+
|------|------|----------|---------|-------------|
|
|
176
|
+
| `events` | `CalendarEvent[]` | Yes | - | Array of events to display |
|
|
177
|
+
| `date` | `Date` | Yes | - | Current date/month |
|
|
178
|
+
| `onDateChange` | `(date: Date) => void` | Yes | - | Called when date changes |
|
|
179
|
+
| `onEventPress` | `(event: CalendarEvent) => void` | Yes | - | Called when event is pressed |
|
|
180
|
+
| `view` | `"month" \| "week" \| "day"` | No | `"month"` | View mode |
|
|
181
|
+
| `onViewChange` | `(view: ViewMode) => void` | No | - | Called when view mode changes |
|
|
182
|
+
| `onDateTimeChange` | `(dateTime: Date) => void` | No | - | Unified handler for date+time changes (navigates to day view) |
|
|
183
|
+
| `isLoading` | `boolean` | No | `false` | Show loading spinner |
|
|
184
|
+
| `theme` | `Partial<CalendarTheme>` | No | - | Custom theme override |
|
|
185
|
+
| `availableProperties` | `Array<{ id: number }>` | No | - | Properties for consistent color assignment |
|
|
186
|
+
| `propertiesToShow` | `Array<{ id: number; name?: string }>` | No | - | Properties to show in DayView lanes |
|
|
187
|
+
| `propertyColors` | `string[]` | No | - | Custom property colors array |
|
|
188
|
+
| `propertyColorsDark` | `string[]` | No | - | Custom dark property colors array |
|
|
189
|
+
| `cleaningIcon` | `any` | No | - | Custom icon for cleaning events |
|
|
190
|
+
| `showFAB` | `boolean` | No | `false` | Show floating action button |
|
|
191
|
+
| `onFABPress` | `() => void` | No | - | FAB press handler |
|
|
192
|
+
| `fabStyle` | `ViewStyle` | No | - | Custom FAB styles |
|
|
193
|
+
| `renderFAB` | `() => React.ReactElement \| null` | No | - | Custom FAB component renderer |
|
|
194
|
+
| `autoScrollToNow` | `boolean` | No | `false` | Auto-scroll to current time in day view |
|
|
205
195
|
|
|
206
|
-
|
|
207
|
-
|------|------|----------|-------------|
|
|
208
|
-
| `events` | `CalendarEvent[]` | Yes | Array of events |
|
|
209
|
-
| `view` | `"month" \| "week" \| "day"` | No | View mode (default: `"month"`) |
|
|
210
|
-
| `date` | `Date` | Yes | Current date/month |
|
|
211
|
-
| `onDateChange` | `(date: Date) => void` | Yes | Date navigation handler |
|
|
212
|
-
| `onEventPress` | `(event: CalendarEvent) => void` | Yes | Event press handler |
|
|
213
|
-
| `onViewChange` | `(view: ViewMode) => void` | No | View mode change handler |
|
|
214
|
-
| `cleaningIcon` | `any` | No | Custom icon for cleaning events |
|
|
215
|
-
| `theme` | `CalendarTheme` | No | Custom theme |
|
|
216
|
-
| `availableProperties` | `Property[]` | No | Properties for color assignment |
|
|
196
|
+
### Exports
|
|
217
197
|
|
|
218
|
-
|
|
198
|
+
- `Calendar` - Main calendar component
|
|
199
|
+
- `CalendarEvent` - Event type
|
|
200
|
+
- `ViewMode` - View mode type (`"day" | "week" | "month"`)
|
|
201
|
+
- `MonthView`, `WeekView`, `DayView` - Individual view components
|
|
202
|
+
- `CalendarFAB` - Floating action button component
|
|
203
|
+
- Utilities: `dateUtils`, `weekDayUtils`, `theme`, `propertyColors`
|
|
204
|
+
- Hooks: `useSwipeGesture`
|
|
219
205
|
|
|
220
|
-
##
|
|
206
|
+
## Features
|
|
221
207
|
|
|
222
208
|
- **Month View**: Calendar grid with event bars and swipe navigation
|
|
223
209
|
- **Week View**: 7-day view with time-based positioning
|
|
224
210
|
- **Day View**: Single-day view with property lanes
|
|
225
211
|
- **Multi-day Events**: Continuous bars across day cells
|
|
226
|
-
- **Type-based Rendering**: Different styles for
|
|
227
|
-
- **
|
|
228
|
-
|
|
229
|
-
|
|
212
|
+
- **Type-based Rendering**: Different styles for different event types
|
|
213
|
+
- **Theme Customization**: Customizable colors and styles
|
|
214
|
+
- **Cross-platform**: Works on React Native (iOS/Android) and Web (Next.js)
|
|
215
|
+
- **SSR-safe**: Server-side rendering compatible
|
|
230
216
|
|
|
231
|
-
|
|
232
|
-
- ✅ Ensure `start` and `end` are `Date` objects (not strings)
|
|
233
|
-
- ✅ Check `eventId` is set correctly
|
|
234
|
-
- ✅ Verify `containerHeight` is sufficient (minimum 400px)
|
|
217
|
+
## Troubleshooting
|
|
235
218
|
|
|
236
|
-
|
|
219
|
+
**Events not appearing:**
|
|
220
|
+
- Ensure `start` and `end` are `Date` objects (not strings)
|
|
221
|
+
- Verify `eventId` is set correctly
|
|
222
|
+
- Check that events fall within the visible date range
|
|
237
223
|
|
|
238
|
-
**
|
|
239
|
-
-
|
|
240
|
-
-
|
|
224
|
+
**Next.js issues:**
|
|
225
|
+
- Use `--webpack` flag for development: `npm run dev -- --webpack`
|
|
226
|
+
- Clear Next.js cache: `rm -rf .next`
|
|
227
|
+
- Ensure `react-native-web` is installed
|
|
228
|
+
- Verify the plugin is correctly applied in `next.config.ts`
|
|
241
229
|
|
|
242
|
-
**
|
|
243
|
-
-
|
|
244
|
-
-
|
|
245
|
-
|
|
246
|
-
**Turbopack error:**
|
|
247
|
-
- ✅ Use `npm run dev -- --webpack`
|
|
248
|
-
- ✅ Or add `turbopack: {}` to `next.config.ts`
|
|
249
|
-
|
|
250
|
-
## 📚 Props Reference
|
|
251
|
-
|
|
252
|
-
### Calendar Props
|
|
253
|
-
|
|
254
|
-
| Prop | Type | Required | Default | Description |
|
|
255
|
-
|------|------|----------|---------|-------------|
|
|
256
|
-
| `events` | `CalendarEvent[]` | Yes | - | Array of events |
|
|
257
|
-
| `view` | `ViewMode` | No | `"month"` | View mode |
|
|
258
|
-
| `date` | `Date` | Yes | - | Current date |
|
|
259
|
-
| `onDateChange` | `(date: Date) => void` | Yes | - | Date change handler |
|
|
260
|
-
| `onEventPress` | `(event: CalendarEvent) => void` | Yes | - | Event press handler |
|
|
261
|
-
| `onViewChange` | `(view: ViewMode) => void` | No | - | View change handler |
|
|
262
|
-
| `isLoading` | `boolean` | No | `false` | Show loading spinner |
|
|
263
|
-
| `theme` | `CalendarTheme` | No | - | Custom theme |
|
|
264
|
-
| `availableProperties` | `Property[]` | No | - | Properties for colors |
|
|
265
|
-
| `cleaningIcon` | `any` | No | - | Custom cleaning icon |
|
|
266
|
-
| `showFAB` | `boolean` | No | `false` | Show floating action button |
|
|
267
|
-
| `autoScrollToNow` | `boolean` | No | `false` | Auto-scroll to current time |
|
|
230
|
+
**Module not found errors:**
|
|
231
|
+
- Restart the development server
|
|
232
|
+
- Clear node_modules and reinstall: `rm -rf node_modules && npm install`
|
|
268
233
|
|
|
269
|
-
##
|
|
234
|
+
## License
|
|
270
235
|
|
|
271
236
|
MIT
|
|
272
237
|
|
|
273
|
-
|
|
238
|
+
## Links
|
|
274
239
|
|
|
275
|
-
|
|
240
|
+
- [Repository](https://github.com/cleanhaus/calendar-component)
|
|
241
|
+
- [Issues](https://github.com/cleanhaus/calendar-component/issues)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cleanhaus-calendar",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Cross-platform calendar component for React Native and Web",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"files": [
|
|
20
20
|
"**/*.ts",
|
|
21
21
|
"**/*.tsx",
|
|
22
|
-
"shared/**/*.png",
|
|
22
|
+
"src/shared/**/*.png",
|
|
23
23
|
"README.md",
|
|
24
24
|
"LICENSE",
|
|
25
25
|
"CHANGELOG.md",
|
package/src/Calendar.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// React Native Calendar Component - Uses only RN components
|
|
2
|
-
import React, { useState, useCallback } from "react";
|
|
2
|
+
import React, { useState, useCallback, useEffect } from "react";
|
|
3
3
|
import {
|
|
4
4
|
StyleSheet,
|
|
5
5
|
View,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
Dimensions,
|
|
8
8
|
ActivityIndicator,
|
|
9
9
|
} from "react-native";
|
|
10
|
+
import type { LayoutChangeEvent } from "react-native";
|
|
10
11
|
import { CalendarProps, CalendarEvent } from "./types";
|
|
11
12
|
import { MonthView } from "./MonthView";
|
|
12
13
|
import { DayView } from "./DayView";
|
|
@@ -18,6 +19,7 @@ import {
|
|
|
18
19
|
DEFAULT_PROPERTY_COLORS,
|
|
19
20
|
DEFAULT_PROPERTY_COLORS_DARK,
|
|
20
21
|
} from "./utils/propertyColors";
|
|
22
|
+
import { validateCalendarProps, logValidationErrors } from "./utils/validation";
|
|
21
23
|
|
|
22
24
|
export const CustomCalendar: React.FC<CalendarProps> = ({
|
|
23
25
|
events = [],
|
|
@@ -40,6 +42,20 @@ export const CustomCalendar: React.FC<CalendarProps> = ({
|
|
|
40
42
|
autoScrollToNow = false,
|
|
41
43
|
cleaningIcon,
|
|
42
44
|
}) => {
|
|
45
|
+
// Validate props in development mode
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (process.env.NODE_ENV !== "production") {
|
|
48
|
+
const errors = validateCalendarProps({
|
|
49
|
+
events,
|
|
50
|
+
date,
|
|
51
|
+
onDateChange,
|
|
52
|
+
onEventPress,
|
|
53
|
+
view,
|
|
54
|
+
});
|
|
55
|
+
logValidationErrors(errors);
|
|
56
|
+
}
|
|
57
|
+
}, [events, date, onDateChange, onEventPress, view]);
|
|
58
|
+
|
|
43
59
|
// Merge user theme with default theme
|
|
44
60
|
const mergedTheme = mergeTheme(theme);
|
|
45
61
|
|
|
@@ -155,7 +171,7 @@ export const CustomCalendar: React.FC<CalendarProps> = ({
|
|
|
155
171
|
<View
|
|
156
172
|
key={`month-${viewKey}`} // Force remount on view change
|
|
157
173
|
style={[styles.container, { backgroundColor: mergedTheme.background }]}
|
|
158
|
-
onLayout={(e) => {
|
|
174
|
+
onLayout={(e: LayoutChangeEvent) => {
|
|
159
175
|
const newHeight = e.nativeEvent.layout.height;
|
|
160
176
|
if (newHeight > 0) {
|
|
161
177
|
setHeight(newHeight);
|
|
@@ -39,7 +39,7 @@ export const useScrollSynchronization = () => {
|
|
|
39
39
|
* Runs on UI thread for zero-latency synchronization
|
|
40
40
|
*/
|
|
41
41
|
const handleHeaderScroll = useAnimatedScrollHandler({
|
|
42
|
-
onScroll: (event) => {
|
|
42
|
+
onScroll: (event: { contentOffset: { x: number; y: number } }) => {
|
|
43
43
|
// Ignore if content is currently being scrolled by user
|
|
44
44
|
if (isContentScrolling.value) {
|
|
45
45
|
return;
|
|
@@ -69,7 +69,7 @@ export const useScrollSynchronization = () => {
|
|
|
69
69
|
* Runs on UI thread for zero-latency synchronization
|
|
70
70
|
*/
|
|
71
71
|
const handleContentScroll = useAnimatedScrollHandler({
|
|
72
|
-
onScroll: (event) => {
|
|
72
|
+
onScroll: (event: { contentOffset: { x: number; y: number } }) => {
|
|
73
73
|
// Ignore if header is currently being scrolled by user
|
|
74
74
|
if (isHeaderScrolling.value) {
|
|
75
75
|
return;
|
package/src/DayView/index.tsx
CHANGED
|
@@ -197,9 +197,7 @@ export const DayView: React.FC<DayViewProps> = ({
|
|
|
197
197
|
<CalendarErrorBoundary
|
|
198
198
|
theme={theme}
|
|
199
199
|
onError={(error, errorInfo) => {
|
|
200
|
-
|
|
201
|
-
console.error("DayView Error:", error, errorInfo);
|
|
202
|
-
}
|
|
200
|
+
// Error handled by ErrorBoundary's onError callback
|
|
203
201
|
}}
|
|
204
202
|
>
|
|
205
203
|
<View style={[styles.container, { backgroundColor: theme.background }]}>
|
|
@@ -238,7 +236,7 @@ export const DayView: React.FC<DayViewProps> = ({
|
|
|
238
236
|
showsVerticalScrollIndicator={true}
|
|
239
237
|
showsHorizontalScrollIndicator={false}
|
|
240
238
|
onScroll={useAnimatedScrollHandler({
|
|
241
|
-
onScroll: (event) => {
|
|
239
|
+
onScroll: (event: { contentOffset: { y: number } }) => {
|
|
242
240
|
scrollY.value = event.contentOffset.y;
|
|
243
241
|
},
|
|
244
242
|
})}
|
|
@@ -310,19 +308,21 @@ const createStyles = (theme: CalendarTheme) =>
|
|
|
310
308
|
container: {
|
|
311
309
|
flex: 1,
|
|
312
310
|
// Web-specific: Ensure container has constrained height for scrolling
|
|
313
|
-
...(Platform.OS ===
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
311
|
+
...(Platform.OS === "web" &&
|
|
312
|
+
typeof window !== "undefined" && {
|
|
313
|
+
height: "100%",
|
|
314
|
+
minHeight: 0, // Important for flex children on web
|
|
315
|
+
}),
|
|
317
316
|
},
|
|
318
317
|
contentContainer: {
|
|
319
318
|
flex: 1,
|
|
320
319
|
backgroundColor: theme.background,
|
|
321
320
|
// Web-specific: Constrain height for scrolling
|
|
322
|
-
...(Platform.OS ===
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
321
|
+
...(Platform.OS === "web" &&
|
|
322
|
+
typeof window !== "undefined" && {
|
|
323
|
+
height: "100%",
|
|
324
|
+
minHeight: 0,
|
|
325
|
+
}),
|
|
326
326
|
},
|
|
327
327
|
stickyHeadersContainer: {
|
|
328
328
|
flexDirection: "row",
|
|
@@ -336,9 +336,10 @@ const createStyles = (theme: CalendarTheme) =>
|
|
|
336
336
|
stickyHeadersScroll: {
|
|
337
337
|
flex: 1,
|
|
338
338
|
// Web-specific: Ensure horizontal scrolling works
|
|
339
|
-
...(Platform.OS ===
|
|
340
|
-
|
|
341
|
-
|
|
339
|
+
...(Platform.OS === "web" &&
|
|
340
|
+
typeof window !== "undefined" && {
|
|
341
|
+
minHeight: 0,
|
|
342
|
+
}),
|
|
342
343
|
},
|
|
343
344
|
stickyHeadersRow: {
|
|
344
345
|
flexDirection: "row",
|
|
@@ -347,15 +348,16 @@ const createStyles = (theme: CalendarTheme) =>
|
|
|
347
348
|
flex: 1,
|
|
348
349
|
paddingTop: CONTENT_PADDING_TOP,
|
|
349
350
|
// Web-specific: Explicit height constraint and overflow for scrolling
|
|
350
|
-
...(Platform.OS ===
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
351
|
+
...(Platform.OS === "web" &&
|
|
352
|
+
typeof window !== "undefined" && {
|
|
353
|
+
height: "100%",
|
|
354
|
+
maxHeight: "100%",
|
|
355
|
+
minHeight: 0,
|
|
356
|
+
// @ts-ignore - web-specific CSS properties
|
|
357
|
+
overflowY: "auto",
|
|
358
|
+
overflowX: "hidden",
|
|
359
|
+
WebkitOverflowScrolling: "touch",
|
|
360
|
+
}),
|
|
359
361
|
},
|
|
360
362
|
content: {
|
|
361
363
|
flexDirection: "row",
|
|
@@ -368,13 +370,14 @@ const createStyles = (theme: CalendarTheme) =>
|
|
|
368
370
|
propertyLanesScroll: {
|
|
369
371
|
flex: 1,
|
|
370
372
|
// Web-specific: Ensure horizontal scrolling works
|
|
371
|
-
...(Platform.OS ===
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
373
|
+
...(Platform.OS === "web" &&
|
|
374
|
+
typeof window !== "undefined" && {
|
|
375
|
+
minHeight: 0,
|
|
376
|
+
// @ts-ignore - web-specific CSS properties
|
|
377
|
+
overflowX: "auto",
|
|
378
|
+
overflowY: "hidden",
|
|
379
|
+
WebkitOverflowScrolling: "touch",
|
|
380
|
+
}),
|
|
378
381
|
},
|
|
379
382
|
propertyLanesRow: {
|
|
380
383
|
flexDirection: "row",
|
package/src/MonthView/index.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import calendarize from "calendarize";
|
|
|
2
2
|
import dayjs from "dayjs";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Text, TouchableOpacity, View, Dimensions } from "react-native";
|
|
5
|
+
import type { LayoutChangeEvent } from "react-native";
|
|
5
6
|
import { CalendarEvent } from "../types";
|
|
6
7
|
import { CalendarTheme } from "../utils/theme";
|
|
7
8
|
import { useSwipeGesture } from "../hooks";
|
|
@@ -146,8 +147,8 @@ export const MonthView: React.FC<MonthViewProps> = ({
|
|
|
146
147
|
// borderColor: theme.border,
|
|
147
148
|
// borderRadius: 4,
|
|
148
149
|
}}
|
|
149
|
-
onLayout={(
|
|
150
|
-
setCalendarWidth(layout.width);
|
|
150
|
+
onLayout={(e: LayoutChangeEvent) => {
|
|
151
|
+
setCalendarWidth(e.nativeEvent.layout.width);
|
|
151
152
|
}}
|
|
152
153
|
{...panResponder.panHandlers}
|
|
153
154
|
>
|
package/src/MonthView/utils.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import dayjs from "dayjs";
|
|
2
|
+
import type { Dayjs } from "dayjs";
|
|
2
3
|
import isBetween from "dayjs/plugin/isBetween";
|
|
3
4
|
import { CalendarEvent } from "../types";
|
|
4
5
|
import { EventPosition, HorizontalPosition } from "./types";
|
|
@@ -16,7 +17,7 @@ dayjs.extend(isBetween);
|
|
|
16
17
|
*/
|
|
17
18
|
export function getHorizontalPositionInDay(
|
|
18
19
|
event: CalendarEvent,
|
|
19
|
-
date:
|
|
20
|
+
date: Dayjs,
|
|
20
21
|
cellWidth: number
|
|
21
22
|
): HorizontalPosition {
|
|
22
23
|
const dayStart = date.startOf("day");
|
|
@@ -51,7 +52,7 @@ export function getHorizontalPositionInDay(
|
|
|
51
52
|
*/
|
|
52
53
|
export function getMultiDayPosition(
|
|
53
54
|
event: CalendarEvent,
|
|
54
|
-
weekStartDate:
|
|
55
|
+
weekStartDate: Dayjs,
|
|
55
56
|
weekIndex: number,
|
|
56
57
|
cellWidth: number
|
|
57
58
|
): EventPosition | null {
|
|
@@ -212,7 +213,7 @@ export function assignGlobalRows(
|
|
|
212
213
|
export function calculateOverflowByDay(
|
|
213
214
|
eventPositions: EventPosition[],
|
|
214
215
|
weeks: number[][],
|
|
215
|
-
target:
|
|
216
|
+
target: Dayjs,
|
|
216
217
|
maxVisibleRows: number
|
|
217
218
|
): Map<string, number> {
|
|
218
219
|
const overflowMap = new Map<string, number>();
|
|
@@ -33,13 +33,13 @@ export const WeekOverflowIndicator: React.FC<Props> = ({
|
|
|
33
33
|
}) => {
|
|
34
34
|
const label = showCount && count > 0 ? `+${count}` : undefined;
|
|
35
35
|
// Use Unicode chevrons instead of icon library for web compatibility
|
|
36
|
-
const chevronSymbol =
|
|
36
|
+
const chevronSymbol =
|
|
37
|
+
chevron === "up" ? "▲" : chevron === "down" ? "▼" : null;
|
|
37
38
|
|
|
38
39
|
return (
|
|
39
40
|
<TouchableOpacity
|
|
40
41
|
style={[styles.container, style]}
|
|
41
42
|
onPress={() => {
|
|
42
|
-
console.log("WeekOverflowIndicator pressed");
|
|
43
43
|
onPress && onPress();
|
|
44
44
|
}}
|
|
45
45
|
hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
|
package/src/WeekView/index.tsx
CHANGED
|
@@ -73,7 +73,7 @@ export const WeekView: React.FC<WeekViewProps> = ({
|
|
|
73
73
|
}) => {
|
|
74
74
|
// SSR-safe: useWindowDimensions may not be available during SSR
|
|
75
75
|
const windowDimensions = useWindowDimensions();
|
|
76
|
-
const width = typeof window !==
|
|
76
|
+
const width = typeof window !== "undefined" ? windowDimensions.width : 0;
|
|
77
77
|
const dayColumnWidth = Math.floor((width - TIME_COLUMN_WIDTH) / 7);
|
|
78
78
|
|
|
79
79
|
const totalHeight = HOURS_IN_DAY * WEEK_VIEW_HOUR_HEIGHT;
|
|
@@ -184,9 +184,7 @@ export const WeekView: React.FC<WeekViewProps> = ({
|
|
|
184
184
|
<CalendarErrorBoundary
|
|
185
185
|
theme={theme}
|
|
186
186
|
onError={(error, errorInfo) => {
|
|
187
|
-
|
|
188
|
-
console.error("WeekView Error:", error, errorInfo);
|
|
189
|
-
}
|
|
187
|
+
// Error handled by ErrorBoundary's onError callback
|
|
190
188
|
}}
|
|
191
189
|
>
|
|
192
190
|
<View style={styles.container}>
|
|
@@ -237,7 +235,7 @@ export const WeekView: React.FC<WeekViewProps> = ({
|
|
|
237
235
|
showsVerticalScrollIndicator
|
|
238
236
|
showsHorizontalScrollIndicator={false}
|
|
239
237
|
onScroll={useAnimatedScrollHandler({
|
|
240
|
-
onScroll: (event) => {
|
|
238
|
+
onScroll: (event: { contentOffset: { y: number } }) => {
|
|
241
239
|
scrollY.value = event.contentOffset.y;
|
|
242
240
|
},
|
|
243
241
|
})}
|
|
@@ -339,10 +337,11 @@ const createStyles = (theme: CalendarTheme, totalHeight: number) =>
|
|
|
339
337
|
backgroundColor: theme.background,
|
|
340
338
|
position: "relative",
|
|
341
339
|
// Web-specific: Ensure container has constrained height for scrolling
|
|
342
|
-
...(Platform.OS ===
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
340
|
+
...(Platform.OS === "web" &&
|
|
341
|
+
typeof window !== "undefined" && {
|
|
342
|
+
height: "100%",
|
|
343
|
+
minHeight: 0, // Important for flex children on web
|
|
344
|
+
}),
|
|
346
345
|
},
|
|
347
346
|
todayHighlight: {
|
|
348
347
|
position: "absolute",
|
|
@@ -367,15 +366,16 @@ const createStyles = (theme: CalendarTheme, totalHeight: number) =>
|
|
|
367
366
|
flex: 1,
|
|
368
367
|
paddingTop: DAY_OVERFLOW_TOP_INSET,
|
|
369
368
|
// Web-specific: Explicit height constraint and overflow for scrolling
|
|
370
|
-
...(Platform.OS ===
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
369
|
+
...(Platform.OS === "web" &&
|
|
370
|
+
typeof window !== "undefined" && {
|
|
371
|
+
height: "100%",
|
|
372
|
+
maxHeight: "100%",
|
|
373
|
+
minHeight: 0,
|
|
374
|
+
// @ts-ignore - web-specific CSS properties
|
|
375
|
+
overflowY: "auto",
|
|
376
|
+
overflowX: "hidden",
|
|
377
|
+
WebkitOverflowScrolling: "touch",
|
|
378
|
+
}),
|
|
379
379
|
},
|
|
380
380
|
content: {
|
|
381
381
|
flexDirection: "row",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useRef, useMemo } from "react";
|
|
2
|
-
import { PanResponder, PanResponderInstance, Platform } from "react-native";
|
|
2
|
+
import { PanResponder, PanResponderInstance, PanResponderGestureState, Platform } from "react-native";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Configuration options for swipe gesture detection
|
|
@@ -76,7 +76,7 @@ export function useSwipeGesture(
|
|
|
76
76
|
* Only respond to horizontal gestures (where horizontal movement > vertical movement).
|
|
77
77
|
* See: https://stackoverflow.com/questions/47568850/touchableopacity-with-parent-panresponder
|
|
78
78
|
*/
|
|
79
|
-
onMoveShouldSetPanResponder: (_, gestureState) => {
|
|
79
|
+
onMoveShouldSetPanResponder: (_: any, gestureState: PanResponderGestureState) => {
|
|
80
80
|
if (!enabled) {
|
|
81
81
|
return false;
|
|
82
82
|
}
|
|
@@ -97,7 +97,7 @@ export function useSwipeGesture(
|
|
|
97
97
|
* Handle the gesture movement.
|
|
98
98
|
* Trigger callbacks when threshold is exceeded.
|
|
99
99
|
*/
|
|
100
|
-
onPanResponderMove: (_, gestureState) => {
|
|
100
|
+
onPanResponderMove: (_: any, gestureState: PanResponderGestureState) => {
|
|
101
101
|
const { dx, dy } = gestureState;
|
|
102
102
|
|
|
103
103
|
// Ignore if:
|
|
@@ -38,9 +38,6 @@ export class CalendarErrorBoundary extends Component<
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
|
41
|
-
if (process.env.NODE_ENV !== "production") {
|
|
42
|
-
console.error("Calendar Error:", error, errorInfo);
|
|
43
|
-
}
|
|
44
41
|
this.props.onError?.(error, errorInfo);
|
|
45
42
|
}
|
|
46
43
|
|
|
Binary file
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type declarations for dayjs
|
|
3
|
+
* dayjs is a peer dependency, so types are provided here for type checking
|
|
4
|
+
*/
|
|
5
|
+
declare module 'dayjs' {
|
|
6
|
+
export interface Dayjs {
|
|
7
|
+
format(template?: string): string;
|
|
8
|
+
startOf(unit: 'day' | 'month' | 'year' | 'hour' | 'minute' | 'second' | 'week'): Dayjs;
|
|
9
|
+
endOf(unit: 'day' | 'month' | 'year' | 'hour' | 'minute' | 'second' | 'week'): Dayjs;
|
|
10
|
+
add(value: number, unit: 'day' | 'month' | 'year' | 'hour' | 'minute' | 'second' | 'week'): Dayjs;
|
|
11
|
+
subtract(value: number, unit: 'day' | 'month' | 'year' | 'hour' | 'minute' | 'second' | 'week'): Dayjs;
|
|
12
|
+
toDate(): Date;
|
|
13
|
+
valueOf(): number;
|
|
14
|
+
[key: string]: any;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface DayjsExtend {
|
|
18
|
+
extend(plugin: any, option?: any): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const dayjs: ((date?: Date | string | number) => Dayjs) & DayjsExtend;
|
|
22
|
+
|
|
23
|
+
export default dayjs;
|
|
24
|
+
}
|
|
25
|
+
|
|
@@ -26,8 +26,30 @@ declare module 'react-native' {
|
|
|
26
26
|
export const PanResponder: any;
|
|
27
27
|
|
|
28
28
|
export type PanResponderInstance = any;
|
|
29
|
+
export type PanResponderGestureState = {
|
|
30
|
+
dx: number;
|
|
31
|
+
dy: number;
|
|
32
|
+
moveX: number;
|
|
33
|
+
moveY: number;
|
|
34
|
+
numberActiveTouches: number;
|
|
35
|
+
stateID: number;
|
|
36
|
+
vx: number;
|
|
37
|
+
vy: number;
|
|
38
|
+
x0: number;
|
|
39
|
+
y0: number;
|
|
40
|
+
};
|
|
29
41
|
export type NativeSyntheticEvent<T> = any;
|
|
30
42
|
export type NativeTouchEvent = any;
|
|
31
43
|
export type StyleProp<T> = any;
|
|
44
|
+
export type LayoutChangeEvent = {
|
|
45
|
+
nativeEvent: {
|
|
46
|
+
layout: {
|
|
47
|
+
x: number;
|
|
48
|
+
y: number;
|
|
49
|
+
width: number;
|
|
50
|
+
height: number;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
};
|
|
32
54
|
}
|
|
33
55
|
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prop validation utilities for Calendar component
|
|
3
|
+
* Provides helpful error messages for common mistakes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { CalendarEvent, ViewMode } from "../types";
|
|
7
|
+
|
|
8
|
+
export interface ValidationError {
|
|
9
|
+
field: string;
|
|
10
|
+
message: string;
|
|
11
|
+
index?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validate a CalendarEvent object
|
|
16
|
+
* @param event - The event to validate
|
|
17
|
+
* @param index - Index of the event in the array (for error messages)
|
|
18
|
+
* @returns Array of validation errors (empty if valid)
|
|
19
|
+
*/
|
|
20
|
+
export function validateEvent(
|
|
21
|
+
event: CalendarEvent,
|
|
22
|
+
index: number
|
|
23
|
+
): ValidationError[] {
|
|
24
|
+
const errors: ValidationError[] = [];
|
|
25
|
+
|
|
26
|
+
// Check required fields
|
|
27
|
+
if (!event.id || typeof event.id !== "string") {
|
|
28
|
+
errors.push({
|
|
29
|
+
field: "id",
|
|
30
|
+
message: `Event at index ${index}: "id" is required and must be a string`,
|
|
31
|
+
index,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!event.eventId || typeof event.eventId !== "string") {
|
|
36
|
+
errors.push({
|
|
37
|
+
field: "eventId",
|
|
38
|
+
message: `Event at index ${index}: "eventId" is required and must be a string`,
|
|
39
|
+
index,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!event.title || typeof event.title !== "string") {
|
|
44
|
+
errors.push({
|
|
45
|
+
field: "title",
|
|
46
|
+
message: `Event at index ${index}: "title" is required and must be a string`,
|
|
47
|
+
index,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check start date
|
|
52
|
+
if (!event.start) {
|
|
53
|
+
errors.push({
|
|
54
|
+
field: "start",
|
|
55
|
+
message: `Event at index ${index}: "start" is required`,
|
|
56
|
+
index,
|
|
57
|
+
});
|
|
58
|
+
} else if (!(event.start instanceof Date)) {
|
|
59
|
+
errors.push({
|
|
60
|
+
field: "start",
|
|
61
|
+
message: `Event at index ${index}: "start" must be a Date object, got ${typeof event.start}. Use: new Date(dateString) to convert.`,
|
|
62
|
+
index,
|
|
63
|
+
});
|
|
64
|
+
} else if (isNaN(event.start.getTime())) {
|
|
65
|
+
errors.push({
|
|
66
|
+
field: "start",
|
|
67
|
+
message: `Event at index ${index}: "start" is an invalid Date`,
|
|
68
|
+
index,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check end date
|
|
73
|
+
if (!event.end) {
|
|
74
|
+
errors.push({
|
|
75
|
+
field: "end",
|
|
76
|
+
message: `Event at index ${index}: "end" is required`,
|
|
77
|
+
index,
|
|
78
|
+
});
|
|
79
|
+
} else if (!(event.end instanceof Date)) {
|
|
80
|
+
errors.push({
|
|
81
|
+
field: "end",
|
|
82
|
+
message: `Event at index ${index}: "end" must be a Date object, got ${typeof event.end}. Use: new Date(dateString) to convert.`,
|
|
83
|
+
index,
|
|
84
|
+
});
|
|
85
|
+
} else if (isNaN(event.end.getTime())) {
|
|
86
|
+
errors.push({
|
|
87
|
+
field: "end",
|
|
88
|
+
message: `Event at index ${index}: "end" is an invalid Date`,
|
|
89
|
+
index,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check date range validity
|
|
94
|
+
if (
|
|
95
|
+
event.start instanceof Date &&
|
|
96
|
+
event.end instanceof Date &&
|
|
97
|
+
!isNaN(event.start.getTime()) &&
|
|
98
|
+
!isNaN(event.end.getTime())
|
|
99
|
+
) {
|
|
100
|
+
if (event.start >= event.end) {
|
|
101
|
+
errors.push({
|
|
102
|
+
field: "dateRange",
|
|
103
|
+
message: `Event at index ${index}: "start" date must be before "end" date`,
|
|
104
|
+
index,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return errors;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Validate Calendar component props
|
|
114
|
+
* @param props - The props to validate
|
|
115
|
+
* @returns Array of validation errors (empty if valid)
|
|
116
|
+
*/
|
|
117
|
+
export function validateCalendarProps(props: {
|
|
118
|
+
events: CalendarEvent[];
|
|
119
|
+
date: Date;
|
|
120
|
+
onDateChange?: (date: Date) => void;
|
|
121
|
+
onEventPress?: (event: CalendarEvent) => void;
|
|
122
|
+
view?: ViewMode;
|
|
123
|
+
}): ValidationError[] {
|
|
124
|
+
const errors: ValidationError[] = [];
|
|
125
|
+
|
|
126
|
+
// Validate date prop
|
|
127
|
+
if (!props.date) {
|
|
128
|
+
errors.push({
|
|
129
|
+
field: "date",
|
|
130
|
+
message: '"date" prop is required',
|
|
131
|
+
});
|
|
132
|
+
} else if (!(props.date instanceof Date)) {
|
|
133
|
+
errors.push({
|
|
134
|
+
field: "date",
|
|
135
|
+
message: `"date" must be a Date object, got ${typeof props.date}. Use: new Date() or new Date(dateString)`,
|
|
136
|
+
});
|
|
137
|
+
} else if (isNaN(props.date.getTime())) {
|
|
138
|
+
errors.push({
|
|
139
|
+
field: "date",
|
|
140
|
+
message: '"date" is an invalid Date',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Validate required callbacks
|
|
145
|
+
if (!props.onDateChange || typeof props.onDateChange !== "function") {
|
|
146
|
+
errors.push({
|
|
147
|
+
field: "onDateChange",
|
|
148
|
+
message: '"onDateChange" prop is required and must be a function',
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!props.onEventPress || typeof props.onEventPress !== "function") {
|
|
153
|
+
errors.push({
|
|
154
|
+
field: "onEventPress",
|
|
155
|
+
message: '"onEventPress" prop is required and must be a function',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Validate view prop
|
|
160
|
+
if (props.view && !["month", "week", "day"].includes(props.view)) {
|
|
161
|
+
errors.push({
|
|
162
|
+
field: "view",
|
|
163
|
+
message: `"view" must be one of: "month", "week", "day". Got: ${props.view}`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Validate events array
|
|
168
|
+
if (!Array.isArray(props.events)) {
|
|
169
|
+
errors.push({
|
|
170
|
+
field: "events",
|
|
171
|
+
message: '"events" must be an array',
|
|
172
|
+
});
|
|
173
|
+
} else {
|
|
174
|
+
// Validate each event
|
|
175
|
+
props.events.forEach((event, index) => {
|
|
176
|
+
const eventErrors = validateEvent(event, index);
|
|
177
|
+
errors.push(...eventErrors);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return errors;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Log validation errors in development mode
|
|
186
|
+
* @param errors - Array of validation errors
|
|
187
|
+
*/
|
|
188
|
+
export function logValidationErrors(errors: ValidationError[]): void {
|
|
189
|
+
if (errors.length === 0) return;
|
|
190
|
+
|
|
191
|
+
if (process.env.NODE_ENV !== "production") {
|
|
192
|
+
console.error(
|
|
193
|
+
"❌ Calendar Component Validation Errors:\n" +
|
|
194
|
+
errors.map((err) => ` • ${err.message}`).join("\n") +
|
|
195
|
+
"\n\n" +
|
|
196
|
+
"Common fixes:\n" +
|
|
197
|
+
" • Convert date strings to Date objects: new Date(dateString)\n" +
|
|
198
|
+
" • Ensure start < end for all events\n" +
|
|
199
|
+
" • Provide required callbacks: onDateChange, onEventPress"
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|