@umituz/react-native-design-system 2.3.13 → 2.3.14
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/package.json +14 -12
- package/src/index.ts +25 -0
- package/src/molecules/calendar/domain/entities/CalendarDay.entity.ts +115 -0
- package/src/molecules/calendar/domain/entities/CalendarEvent.entity.ts +202 -0
- package/src/molecules/calendar/domain/repositories/ICalendarRepository.ts +120 -0
- package/src/molecules/calendar/index.ts +98 -0
- package/src/molecules/calendar/infrastructure/services/CalendarEvents.ts +196 -0
- package/src/molecules/calendar/infrastructure/services/CalendarGeneration.ts +172 -0
- package/src/molecules/calendar/infrastructure/services/CalendarPermissions.ts +92 -0
- package/src/molecules/calendar/infrastructure/services/CalendarService.ts +161 -0
- package/src/molecules/calendar/infrastructure/services/CalendarSync.ts +205 -0
- package/src/molecules/calendar/infrastructure/storage/CalendarStore.ts +307 -0
- package/src/molecules/calendar/infrastructure/utils/DateUtilities.ts +128 -0
- package/src/molecules/calendar/presentation/components/AtomicCalendar.tsx +279 -0
- package/src/molecules/calendar/presentation/hooks/useCalendar.ts +356 -0
- package/src/molecules/index.ts +3 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AtomicCalendar Component
|
|
3
|
+
*
|
|
4
|
+
* Generic, reusable calendar component with month view.
|
|
5
|
+
* Works with any type of events (workouts, habits, tasks, etc.)
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Monthly grid view (42 days = 6 weeks)
|
|
9
|
+
* - Timezone-aware via calendar service
|
|
10
|
+
* - Event indicators (colored dots)
|
|
11
|
+
* - Customizable styling
|
|
12
|
+
* - Accessible
|
|
13
|
+
* - Theme-aware
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* ```tsx
|
|
17
|
+
* import { AtomicCalendar, useCalendar } from '@umituz/react-native-calendar';
|
|
18
|
+
*
|
|
19
|
+
* const MyScreen = () => {
|
|
20
|
+
* const { days, selectedDate, actions } = useCalendar();
|
|
21
|
+
*
|
|
22
|
+
* return (
|
|
23
|
+
* <AtomicCalendar
|
|
24
|
+
* days={days}
|
|
25
|
+
* selectedDate={selectedDate}
|
|
26
|
+
* onDateSelect={actions.setSelectedDate}
|
|
27
|
+
* />
|
|
28
|
+
* );
|
|
29
|
+
* };
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import React from 'react';
|
|
34
|
+
import { View, TouchableOpacity, StyleSheet, StyleProp, ViewStyle } from 'react-native';
|
|
35
|
+
import { useAppDesignTokens, AtomicText } from '../../../../index';
|
|
36
|
+
import type { CalendarDay } from '../../domain/entities/CalendarDay.entity';
|
|
37
|
+
import { CalendarService } from '../../infrastructure/services/CalendarService';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* AtomicCalendar Props
|
|
41
|
+
*/
|
|
42
|
+
export interface AtomicCalendarProps {
|
|
43
|
+
/**
|
|
44
|
+
* Calendar days to display (42 days for 6-week grid)
|
|
45
|
+
*/
|
|
46
|
+
days: CalendarDay[];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Currently selected date
|
|
50
|
+
*/
|
|
51
|
+
selectedDate: Date;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Callback when a date is selected
|
|
55
|
+
*/
|
|
56
|
+
onDateSelect: (date: Date) => void;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Whether to show weekday headers
|
|
60
|
+
* @default true
|
|
61
|
+
*/
|
|
62
|
+
showWeekdayHeaders?: boolean;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Maximum number of event indicators to show per day
|
|
66
|
+
* @default 3
|
|
67
|
+
*/
|
|
68
|
+
maxEventIndicators?: number;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Custom container style
|
|
72
|
+
*/
|
|
73
|
+
style?: StyleProp<ViewStyle>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Custom day cell style
|
|
77
|
+
*/
|
|
78
|
+
dayStyle?: StyleProp<ViewStyle>;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Whether to show event count when exceeds max indicators
|
|
82
|
+
* @default true
|
|
83
|
+
*/
|
|
84
|
+
showEventCount?: boolean;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Test ID for testing
|
|
88
|
+
*/
|
|
89
|
+
testID?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* AtomicCalendar Component
|
|
94
|
+
*/
|
|
95
|
+
export const AtomicCalendar: React.FC<AtomicCalendarProps> = ({
|
|
96
|
+
days,
|
|
97
|
+
selectedDate,
|
|
98
|
+
onDateSelect,
|
|
99
|
+
showWeekdayHeaders = true,
|
|
100
|
+
maxEventIndicators = 3,
|
|
101
|
+
style,
|
|
102
|
+
dayStyle,
|
|
103
|
+
showEventCount = true,
|
|
104
|
+
testID,
|
|
105
|
+
}) => {
|
|
106
|
+
const tokens = useAppDesignTokens();
|
|
107
|
+
|
|
108
|
+
// Get weekday names (localized)
|
|
109
|
+
const weekdayNames = CalendarService.getWeekdayNames();
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<View style={[styles.container, { backgroundColor: tokens.colors.surface }, style]} testID={testID}>
|
|
113
|
+
{/* Weekday Headers */}
|
|
114
|
+
{showWeekdayHeaders && (
|
|
115
|
+
<View style={styles.weekdayHeader}>
|
|
116
|
+
{weekdayNames.map((day, index) => (
|
|
117
|
+
<View key={index} style={styles.weekdayCell}>
|
|
118
|
+
<AtomicText
|
|
119
|
+
type="bodySmall"
|
|
120
|
+
color="secondary"
|
|
121
|
+
style={styles.weekdayText}
|
|
122
|
+
>
|
|
123
|
+
{day}
|
|
124
|
+
</AtomicText>
|
|
125
|
+
</View>
|
|
126
|
+
))}
|
|
127
|
+
</View>
|
|
128
|
+
)}
|
|
129
|
+
|
|
130
|
+
{/* Calendar Grid */}
|
|
131
|
+
<View style={styles.grid}>
|
|
132
|
+
{days.map((day, index) => {
|
|
133
|
+
const isSelected = CalendarService.isSameDay(day.date, selectedDate);
|
|
134
|
+
const eventCount = day.events.length;
|
|
135
|
+
const visibleEvents = day.events.slice(0, maxEventIndicators);
|
|
136
|
+
const hiddenEventCount = Math.max(0, eventCount - maxEventIndicators);
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<TouchableOpacity
|
|
140
|
+
key={index}
|
|
141
|
+
style={[
|
|
142
|
+
styles.dayCell,
|
|
143
|
+
{
|
|
144
|
+
backgroundColor: isSelected
|
|
145
|
+
? tokens.colors.primary
|
|
146
|
+
: 'transparent',
|
|
147
|
+
borderColor: isSelected
|
|
148
|
+
? tokens.colors.primary
|
|
149
|
+
: day.isToday
|
|
150
|
+
? tokens.colors.primary
|
|
151
|
+
: tokens.colors.border,
|
|
152
|
+
borderWidth: isSelected ? 2 : day.isToday ? 2 : 1,
|
|
153
|
+
opacity: day.isDisabled ? 0.4 : 1,
|
|
154
|
+
},
|
|
155
|
+
dayStyle,
|
|
156
|
+
]}
|
|
157
|
+
onPress={() => !day.isDisabled && onDateSelect(day.date)}
|
|
158
|
+
disabled={day.isDisabled}
|
|
159
|
+
testID={testID ? `${testID}-day-${index}` : undefined}
|
|
160
|
+
accessibilityLabel={`${day.date.toLocaleDateString()}, ${eventCount} events`}
|
|
161
|
+
accessibilityRole="button"
|
|
162
|
+
accessibilityState={{ disabled: day.isDisabled, selected: isSelected }}
|
|
163
|
+
>
|
|
164
|
+
{/* Day Number */}
|
|
165
|
+
<AtomicText
|
|
166
|
+
type="bodyMedium"
|
|
167
|
+
color={
|
|
168
|
+
isSelected
|
|
169
|
+
? 'inverse'
|
|
170
|
+
: day.isCurrentMonth
|
|
171
|
+
? 'primary'
|
|
172
|
+
: 'secondary'
|
|
173
|
+
}
|
|
174
|
+
style={[
|
|
175
|
+
styles.dayText,
|
|
176
|
+
day.isToday && !isSelected && { fontWeight: 'bold' },
|
|
177
|
+
]}
|
|
178
|
+
>
|
|
179
|
+
{day.date.getDate()}
|
|
180
|
+
</AtomicText>
|
|
181
|
+
|
|
182
|
+
{/* Event Indicators */}
|
|
183
|
+
<View style={styles.eventIndicators}>
|
|
184
|
+
{/* Today indicator (if today and has no events) */}
|
|
185
|
+
{day.isToday && eventCount === 0 && (
|
|
186
|
+
<View
|
|
187
|
+
style={[
|
|
188
|
+
styles.eventDot,
|
|
189
|
+
{ backgroundColor: tokens.colors.success },
|
|
190
|
+
]}
|
|
191
|
+
/>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
{/* Event dots */}
|
|
195
|
+
{visibleEvents.map((event, eventIndex) => (
|
|
196
|
+
<View
|
|
197
|
+
key={eventIndex}
|
|
198
|
+
style={[
|
|
199
|
+
styles.eventDot,
|
|
200
|
+
{
|
|
201
|
+
backgroundColor: event.color
|
|
202
|
+
? event.color
|
|
203
|
+
: event.isCompleted
|
|
204
|
+
? tokens.colors.success
|
|
205
|
+
: tokens.colors.primary,
|
|
206
|
+
},
|
|
207
|
+
]}
|
|
208
|
+
/>
|
|
209
|
+
))}
|
|
210
|
+
|
|
211
|
+
{/* More events count */}
|
|
212
|
+
{showEventCount && hiddenEventCount > 0 && (
|
|
213
|
+
<AtomicText
|
|
214
|
+
type="bodySmall"
|
|
215
|
+
color="secondary"
|
|
216
|
+
style={styles.moreEventsText}
|
|
217
|
+
>
|
|
218
|
+
+{hiddenEventCount}
|
|
219
|
+
</AtomicText>
|
|
220
|
+
)}
|
|
221
|
+
</View>
|
|
222
|
+
</TouchableOpacity>
|
|
223
|
+
);
|
|
224
|
+
})}
|
|
225
|
+
</View>
|
|
226
|
+
</View>
|
|
227
|
+
);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const styles = StyleSheet.create({
|
|
231
|
+
container: {
|
|
232
|
+
borderRadius: 12,
|
|
233
|
+
padding: 16,
|
|
234
|
+
},
|
|
235
|
+
weekdayHeader: {
|
|
236
|
+
flexDirection: 'row',
|
|
237
|
+
marginBottom: 12,
|
|
238
|
+
},
|
|
239
|
+
weekdayCell: {
|
|
240
|
+
flex: 1,
|
|
241
|
+
alignItems: 'center',
|
|
242
|
+
},
|
|
243
|
+
weekdayText: {
|
|
244
|
+
textAlign: 'center',
|
|
245
|
+
},
|
|
246
|
+
grid: {
|
|
247
|
+
flexDirection: 'row',
|
|
248
|
+
flexWrap: 'wrap',
|
|
249
|
+
},
|
|
250
|
+
dayCell: {
|
|
251
|
+
width: `${100 / 7}%`,
|
|
252
|
+
aspectRatio: 1,
|
|
253
|
+
justifyContent: 'center',
|
|
254
|
+
alignItems: 'center',
|
|
255
|
+
borderRadius: 8,
|
|
256
|
+
marginBottom: 4,
|
|
257
|
+
padding: 4,
|
|
258
|
+
},
|
|
259
|
+
dayText: {
|
|
260
|
+
textAlign: 'center',
|
|
261
|
+
},
|
|
262
|
+
eventIndicators: {
|
|
263
|
+
flexDirection: 'row',
|
|
264
|
+
alignItems: 'center',
|
|
265
|
+
justifyContent: 'center',
|
|
266
|
+
marginTop: 4,
|
|
267
|
+
gap: 2,
|
|
268
|
+
flexWrap: 'wrap',
|
|
269
|
+
},
|
|
270
|
+
eventDot: {
|
|
271
|
+
width: 4,
|
|
272
|
+
height: 4,
|
|
273
|
+
borderRadius: 2,
|
|
274
|
+
},
|
|
275
|
+
moreEventsText: {
|
|
276
|
+
fontSize: 8,
|
|
277
|
+
marginLeft: 2,
|
|
278
|
+
},
|
|
279
|
+
});
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useCalendar Hook
|
|
3
|
+
*
|
|
4
|
+
* Main hook for calendar functionality.
|
|
5
|
+
* Provides calendar state, events, and actions.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* ```tsx
|
|
9
|
+
* const {
|
|
10
|
+
* days,
|
|
11
|
+
* events,
|
|
12
|
+
* selectedDate,
|
|
13
|
+
* viewMode,
|
|
14
|
+
* actions
|
|
15
|
+
* } = useCalendar();
|
|
16
|
+
*
|
|
17
|
+
* // Navigate calendar
|
|
18
|
+
* actions.navigateMonth('next');
|
|
19
|
+
*
|
|
20
|
+
* // Add event
|
|
21
|
+
* actions.addEvent({
|
|
22
|
+
* title: 'Team Meeting',
|
|
23
|
+
* date: '2024-10-30',
|
|
24
|
+
* time: '14:00',
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { useMemo, useEffect, useState, useCallback } from 'react';
|
|
30
|
+
import { useCalendarStore, type CalendarViewMode } from '../../infrastructure/storage/CalendarStore';
|
|
31
|
+
import { CalendarService } from '../../infrastructure/services/CalendarService';
|
|
32
|
+
import type { CalendarDay } from '../../domain/entities/CalendarDay.entity';
|
|
33
|
+
import type {
|
|
34
|
+
CalendarEvent,
|
|
35
|
+
SystemCalendar,
|
|
36
|
+
CalendarPermissionResult,
|
|
37
|
+
} from '../../domain/entities/CalendarEvent.entity';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Calendar hook return type
|
|
41
|
+
*/
|
|
42
|
+
export interface UseCalendarReturn {
|
|
43
|
+
// Calendar data
|
|
44
|
+
days: CalendarDay[];
|
|
45
|
+
events: CalendarEvent[];
|
|
46
|
+
selectedDate: Date;
|
|
47
|
+
currentMonth: Date;
|
|
48
|
+
viewMode: CalendarViewMode;
|
|
49
|
+
|
|
50
|
+
// Computed data
|
|
51
|
+
selectedDateEvents: CalendarEvent[];
|
|
52
|
+
currentMonthEvents: CalendarEvent[];
|
|
53
|
+
|
|
54
|
+
// State
|
|
55
|
+
isLoading: boolean;
|
|
56
|
+
error: string | null;
|
|
57
|
+
|
|
58
|
+
// Actions
|
|
59
|
+
actions: {
|
|
60
|
+
loadEvents: () => Promise<void>;
|
|
61
|
+
addEvent: (request: any) => Promise<void>;
|
|
62
|
+
updateEvent: (request: any) => Promise<void>;
|
|
63
|
+
deleteEvent: (id: string) => Promise<void>;
|
|
64
|
+
completeEvent: (id: string) => Promise<void>;
|
|
65
|
+
uncompleteEvent: (id: string) => Promise<void>;
|
|
66
|
+
setSelectedDate: (date: Date) => void;
|
|
67
|
+
goToToday: () => void;
|
|
68
|
+
navigateMonth: (direction: 'prev' | 'next') => void;
|
|
69
|
+
navigateWeek: (direction: 'prev' | 'next') => void;
|
|
70
|
+
setCurrentMonth: (date: Date) => void;
|
|
71
|
+
setViewMode: (mode: CalendarViewMode) => void;
|
|
72
|
+
getEventsForDate: (date: Date) => CalendarEvent[];
|
|
73
|
+
getEventsForMonth: (year: number, month: number) => CalendarEvent[];
|
|
74
|
+
clearError: () => void;
|
|
75
|
+
clearAllEvents: () => Promise<void>;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Main calendar hook
|
|
81
|
+
*/
|
|
82
|
+
export const useCalendar = (): UseCalendarReturn => {
|
|
83
|
+
const {
|
|
84
|
+
events,
|
|
85
|
+
selectedDate,
|
|
86
|
+
currentMonth,
|
|
87
|
+
viewMode,
|
|
88
|
+
isLoading,
|
|
89
|
+
error,
|
|
90
|
+
actions,
|
|
91
|
+
} = useCalendarStore((state) => state);
|
|
92
|
+
|
|
93
|
+
// Load events on mount
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
actions.loadEvents();
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
// Generate calendar days for current month
|
|
99
|
+
const days = useMemo(() => {
|
|
100
|
+
const year = currentMonth.getFullYear();
|
|
101
|
+
const month = currentMonth.getMonth();
|
|
102
|
+
return CalendarService.getMonthDays(year, month, events);
|
|
103
|
+
}, [currentMonth, events]);
|
|
104
|
+
|
|
105
|
+
// Get events for selected date
|
|
106
|
+
const selectedDateEvents = useMemo(() => {
|
|
107
|
+
return actions.getEventsForDate(selectedDate);
|
|
108
|
+
}, [selectedDate, events]);
|
|
109
|
+
|
|
110
|
+
// Get events for current month
|
|
111
|
+
const currentMonthEvents = useMemo(() => {
|
|
112
|
+
const year = currentMonth.getFullYear();
|
|
113
|
+
const month = currentMonth.getMonth();
|
|
114
|
+
return actions.getEventsForMonth(year, month);
|
|
115
|
+
}, [currentMonth, events]);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
days,
|
|
119
|
+
events,
|
|
120
|
+
selectedDate,
|
|
121
|
+
currentMonth,
|
|
122
|
+
viewMode,
|
|
123
|
+
selectedDateEvents,
|
|
124
|
+
currentMonthEvents,
|
|
125
|
+
isLoading,
|
|
126
|
+
error,
|
|
127
|
+
actions,
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Hook for calendar navigation
|
|
133
|
+
* Lightweight hook for just navigation actions
|
|
134
|
+
*/
|
|
135
|
+
export const useCalendarNavigation = () => {
|
|
136
|
+
const {
|
|
137
|
+
selectedDate,
|
|
138
|
+
currentMonth,
|
|
139
|
+
actions: { setSelectedDate, navigateMonth, goToToday, setCurrentMonth },
|
|
140
|
+
} = useCalendarStore((state) => state);
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
selectedDate,
|
|
144
|
+
currentMonth,
|
|
145
|
+
setSelectedDate,
|
|
146
|
+
navigateMonth,
|
|
147
|
+
goToToday,
|
|
148
|
+
setCurrentMonth,
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Hook for calendar events only
|
|
154
|
+
* Lightweight hook for just event operations
|
|
155
|
+
*/
|
|
156
|
+
export const useCalendarEvents = () => {
|
|
157
|
+
const {
|
|
158
|
+
events,
|
|
159
|
+
isLoading,
|
|
160
|
+
error,
|
|
161
|
+
actions: {
|
|
162
|
+
loadEvents,
|
|
163
|
+
addEvent,
|
|
164
|
+
updateEvent,
|
|
165
|
+
deleteEvent,
|
|
166
|
+
completeEvent,
|
|
167
|
+
uncompleteEvent,
|
|
168
|
+
clearError,
|
|
169
|
+
},
|
|
170
|
+
} = useCalendarStore((state) => state);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
events,
|
|
174
|
+
isLoading,
|
|
175
|
+
error,
|
|
176
|
+
loadEvents,
|
|
177
|
+
addEvent,
|
|
178
|
+
updateEvent,
|
|
179
|
+
deleteEvent,
|
|
180
|
+
completeEvent,
|
|
181
|
+
uncompleteEvent,
|
|
182
|
+
clearError,
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Hook for system calendar integration (expo-calendar)
|
|
188
|
+
*
|
|
189
|
+
* USAGE:
|
|
190
|
+
* ```tsx
|
|
191
|
+
* const {
|
|
192
|
+
* systemCalendars,
|
|
193
|
+
* permission,
|
|
194
|
+
* requestPermission,
|
|
195
|
+
* syncEventToCalendar,
|
|
196
|
+
* updateSyncedEvent,
|
|
197
|
+
* deleteSyncedEvent,
|
|
198
|
+
* } = useSystemCalendar();
|
|
199
|
+
*
|
|
200
|
+
* // Request permission
|
|
201
|
+
* const granted = await requestPermission();
|
|
202
|
+
*
|
|
203
|
+
* // Sync event to device calendar
|
|
204
|
+
* await syncEventToCalendar(event);
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
export const useSystemCalendar = () => {
|
|
208
|
+
const [systemCalendars, setSystemCalendars] = useState<SystemCalendar[]>([]);
|
|
209
|
+
const [permission, setPermission] = useState<CalendarPermissionResult | null>(null);
|
|
210
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
211
|
+
|
|
212
|
+
const { actions } = useCalendarStore((state) => state);
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Request calendar permissions
|
|
216
|
+
*/
|
|
217
|
+
const requestPermission = useCallback(async (): Promise<boolean> => {
|
|
218
|
+
setIsLoading(true);
|
|
219
|
+
try {
|
|
220
|
+
const result = await CalendarService.requestPermissions();
|
|
221
|
+
setPermission(result);
|
|
222
|
+
return result.granted;
|
|
223
|
+
} catch {
|
|
224
|
+
return false;
|
|
225
|
+
} finally {
|
|
226
|
+
setIsLoading(false);
|
|
227
|
+
}
|
|
228
|
+
}, []);
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Load system calendars
|
|
232
|
+
*/
|
|
233
|
+
const loadSystemCalendars = useCallback(async () => {
|
|
234
|
+
setIsLoading(true);
|
|
235
|
+
try {
|
|
236
|
+
const calendars = await CalendarService.getSystemCalendars();
|
|
237
|
+
setSystemCalendars(calendars);
|
|
238
|
+
} catch {
|
|
239
|
+
setSystemCalendars([]);
|
|
240
|
+
} finally {
|
|
241
|
+
setIsLoading(false);
|
|
242
|
+
}
|
|
243
|
+
}, []);
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Sync event to system calendar
|
|
247
|
+
*/
|
|
248
|
+
const syncEventToCalendar = useCallback(
|
|
249
|
+
async (event: CalendarEvent): Promise<boolean> => {
|
|
250
|
+
setIsLoading(true);
|
|
251
|
+
try {
|
|
252
|
+
const result = await CalendarService.syncToSystemCalendar(event);
|
|
253
|
+
|
|
254
|
+
if (result.success && result.eventId && result.calendarId) {
|
|
255
|
+
// Update event with system calendar info
|
|
256
|
+
await actions.updateEvent({
|
|
257
|
+
id: event.id,
|
|
258
|
+
systemCalendar: {
|
|
259
|
+
eventId: result.eventId,
|
|
260
|
+
calendarId: result.calendarId,
|
|
261
|
+
lastSyncedAt: new Date(),
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return false;
|
|
268
|
+
} catch {
|
|
269
|
+
return false;
|
|
270
|
+
} finally {
|
|
271
|
+
setIsLoading(false);
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
[actions]
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Update synced event in system calendar
|
|
279
|
+
*/
|
|
280
|
+
const updateSyncedEvent = useCallback(async (event: CalendarEvent): Promise<boolean> => {
|
|
281
|
+
if (!event.systemCalendar) return false;
|
|
282
|
+
|
|
283
|
+
setIsLoading(true);
|
|
284
|
+
try {
|
|
285
|
+
const result = await CalendarService.updateSystemCalendarEvent(event);
|
|
286
|
+
|
|
287
|
+
if (result.success) {
|
|
288
|
+
// Update last synced timestamp
|
|
289
|
+
await actions.updateEvent({
|
|
290
|
+
id: event.id,
|
|
291
|
+
systemCalendar: {
|
|
292
|
+
...event.systemCalendar,
|
|
293
|
+
lastSyncedAt: new Date(),
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return false;
|
|
300
|
+
} catch {
|
|
301
|
+
return false;
|
|
302
|
+
} finally {
|
|
303
|
+
setIsLoading(false);
|
|
304
|
+
}
|
|
305
|
+
}, [actions]);
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Delete synced event from system calendar
|
|
309
|
+
*/
|
|
310
|
+
const deleteSyncedEvent = useCallback(
|
|
311
|
+
async (event: CalendarEvent): Promise<boolean> => {
|
|
312
|
+
if (!event.systemCalendar) return false;
|
|
313
|
+
|
|
314
|
+
setIsLoading(true);
|
|
315
|
+
try {
|
|
316
|
+
const result = await CalendarService.removeFromSystemCalendar(
|
|
317
|
+
event.systemCalendar.eventId
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
if (result.success) {
|
|
321
|
+
// Remove system calendar info from event
|
|
322
|
+
await actions.updateEvent({
|
|
323
|
+
id: event.id,
|
|
324
|
+
systemCalendar: undefined,
|
|
325
|
+
});
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return false;
|
|
330
|
+
} catch {
|
|
331
|
+
return false;
|
|
332
|
+
} finally {
|
|
333
|
+
setIsLoading(false);
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
[actions]
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
// Load calendars when permission is granted
|
|
340
|
+
useEffect(() => {
|
|
341
|
+
if (permission?.granted) {
|
|
342
|
+
loadSystemCalendars();
|
|
343
|
+
}
|
|
344
|
+
}, [permission, loadSystemCalendars]);
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
systemCalendars,
|
|
348
|
+
permission,
|
|
349
|
+
isLoading,
|
|
350
|
+
requestPermission,
|
|
351
|
+
loadSystemCalendars,
|
|
352
|
+
syncEventToCalendar,
|
|
353
|
+
updateSyncedEvent,
|
|
354
|
+
deleteSyncedEvent,
|
|
355
|
+
};
|
|
356
|
+
};
|