@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.
@@ -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
+ };
@@ -29,3 +29,6 @@ export { List, type ListProps } from './List';
29
29
 
30
30
  // Alerts
31
31
  export * from './alerts';
32
+
33
+ // Calendar
34
+ export * from './calendar';