@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,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calendar Sync Service
|
|
3
|
+
*
|
|
4
|
+
* Handles synchronization with system calendar.
|
|
5
|
+
*
|
|
6
|
+
* SOLID: Single Responsibility - Only sync operations
|
|
7
|
+
* DRY: Centralized sync logic
|
|
8
|
+
* KISS: Simple sync interface
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as Calendar from 'expo-calendar';
|
|
12
|
+
import { Platform } from 'react-native';
|
|
13
|
+
import { CalendarPermissions } from './CalendarPermissions';
|
|
14
|
+
import type { CalendarEvent, SystemCalendar } from '../../domain/entities/CalendarEvent.entity';
|
|
15
|
+
|
|
16
|
+
export class CalendarSync {
|
|
17
|
+
/**
|
|
18
|
+
* Sync event to system calendar
|
|
19
|
+
*/
|
|
20
|
+
static async syncToSystemCalendar(
|
|
21
|
+
event: CalendarEvent
|
|
22
|
+
): Promise<{
|
|
23
|
+
success: boolean;
|
|
24
|
+
eventId?: string;
|
|
25
|
+
calendarId?: string;
|
|
26
|
+
error?: string;
|
|
27
|
+
}> {
|
|
28
|
+
try {
|
|
29
|
+
if (Platform.OS === 'web') {
|
|
30
|
+
return { success: false, error: 'Calendar sync not supported on web' };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const permission = await CalendarPermissions.requestPermissions();
|
|
34
|
+
if (!permission.granted) {
|
|
35
|
+
return { success: false, error: 'Calendar permission not granted' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const primaryCal = await this.getPrimaryCalendar();
|
|
39
|
+
if (!primaryCal) {
|
|
40
|
+
return { success: false, error: 'No writable calendar found' };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const eventData = this.buildSystemEventData(event);
|
|
44
|
+
const systemEventId = await Calendar.createEventAsync(primaryCal.id, eventData);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
success: true,
|
|
48
|
+
eventId: systemEventId,
|
|
49
|
+
calendarId: primaryCal.id
|
|
50
|
+
};
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return {
|
|
53
|
+
success: false,
|
|
54
|
+
error: error instanceof Error ? error.message : 'Failed to sync to calendar'
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Update system calendar event
|
|
61
|
+
*/
|
|
62
|
+
static async updateSystemCalendarEvent(
|
|
63
|
+
event: CalendarEvent
|
|
64
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
65
|
+
try {
|
|
66
|
+
if (Platform.OS === 'web' || !event.systemCalendar) {
|
|
67
|
+
return { success: false, error: 'No system calendar data' };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const permission = await CalendarPermissions.requestPermissions();
|
|
71
|
+
if (!permission.granted) {
|
|
72
|
+
return { success: false, error: 'Calendar permission not granted' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const eventData = this.buildSystemEventData(event);
|
|
76
|
+
await Calendar.updateEventAsync(event.systemCalendar.eventId, eventData);
|
|
77
|
+
|
|
78
|
+
return { success: true };
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
error: error instanceof Error ? error.message : 'Failed to update event'
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Remove event from system calendar
|
|
89
|
+
*/
|
|
90
|
+
static async removeFromSystemCalendar(
|
|
91
|
+
eventId: string
|
|
92
|
+
): Promise<{ success: boolean; error?: string }> {
|
|
93
|
+
try {
|
|
94
|
+
if (Platform.OS === 'web') {
|
|
95
|
+
return { success: false, error: 'Calendar sync not supported on web' };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const permission = await CalendarPermissions.requestPermissions();
|
|
99
|
+
if (!permission.granted) {
|
|
100
|
+
return { success: false, error: 'Calendar permission not granted' };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await Calendar.deleteEventAsync(eventId);
|
|
104
|
+
return { success: true };
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return {
|
|
107
|
+
success: false,
|
|
108
|
+
error: error instanceof Error ? error.message : 'Failed to remove from calendar'
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get primary writable calendar
|
|
115
|
+
*/
|
|
116
|
+
static async getPrimaryCalendar(): Promise<SystemCalendar | null> {
|
|
117
|
+
try {
|
|
118
|
+
if (Platform.OS === 'web') {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const calendars = await Calendar.getCalendarsAsync();
|
|
123
|
+
const writableCalendars = calendars.filter((cal: Calendar.Calendar) =>
|
|
124
|
+
cal.allowsModifications &&
|
|
125
|
+
(cal.source.type === 'local' || cal.source.type === 'caldav')
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Prefer default calendar, fallback to first writable
|
|
129
|
+
const primaryCal = writableCalendars.find((cal: Calendar.Calendar) => cal.isPrimary) || writableCalendars[0];
|
|
130
|
+
if (!primaryCal) return null;
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
id: primaryCal.id,
|
|
134
|
+
title: primaryCal.title,
|
|
135
|
+
color: primaryCal.color,
|
|
136
|
+
allowsModifications: primaryCal.allowsModifications,
|
|
137
|
+
source: primaryCal.source.name,
|
|
138
|
+
isPrimary: primaryCal.isPrimary || false
|
|
139
|
+
};
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get all system calendars
|
|
147
|
+
*/
|
|
148
|
+
static async getSystemCalendars(): Promise<SystemCalendar[]> {
|
|
149
|
+
try {
|
|
150
|
+
if (Platform.OS === 'web') {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const calendars = await Calendar.getCalendarsAsync();
|
|
155
|
+
return calendars.map((cal: Calendar.Calendar) => ({
|
|
156
|
+
id: cal.id,
|
|
157
|
+
title: cal.title,
|
|
158
|
+
color: cal.color,
|
|
159
|
+
allowsModifications: cal.allowsModifications,
|
|
160
|
+
source: cal.source.name,
|
|
161
|
+
isPrimary: cal.isPrimary || false
|
|
162
|
+
}));
|
|
163
|
+
} catch {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Build system calendar event data
|
|
170
|
+
*/
|
|
171
|
+
private static buildSystemEventData(event: CalendarEvent) {
|
|
172
|
+
const [year, month, day] = event.date.split('-').map(Number);
|
|
173
|
+
let startDate = new Date(year, month - 1, day);
|
|
174
|
+
let endDate = new Date(startDate);
|
|
175
|
+
|
|
176
|
+
// Set time if provided
|
|
177
|
+
if (event.time) {
|
|
178
|
+
const [hours, minutes] = event.time.split(':').map(Number);
|
|
179
|
+
startDate.setHours(hours, minutes, 0, 0);
|
|
180
|
+
endDate.setHours(hours, minutes, 0, 0);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Set duration
|
|
184
|
+
if (event.duration) {
|
|
185
|
+
endDate.setMinutes(endDate.getMinutes() + event.duration);
|
|
186
|
+
} else {
|
|
187
|
+
endDate.setHours(endDate.getHours() + 1); // Default 1 hour
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Create reminders
|
|
191
|
+
const alarms = event.reminders?.map(minutesBefore => ({
|
|
192
|
+
relativeOffset: -minutesBefore
|
|
193
|
+
}));
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
title: event.title,
|
|
197
|
+
startDate,
|
|
198
|
+
endDate,
|
|
199
|
+
notes: event.description,
|
|
200
|
+
location: event.location,
|
|
201
|
+
alarms,
|
|
202
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calendar Store (Zustand)
|
|
3
|
+
*
|
|
4
|
+
* Global state management for calendar functionality.
|
|
5
|
+
* Manages calendar view state, selected date, and events.
|
|
6
|
+
*
|
|
7
|
+
* Design Philosophy:
|
|
8
|
+
* - Zustand for lightweight state
|
|
9
|
+
* - AsyncStorage for persistence
|
|
10
|
+
* - Generic event handling
|
|
11
|
+
* - Timezone-aware via CalendarService
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { create } from 'zustand';
|
|
15
|
+
import { persist, createJSONStorage } from 'zustand/middleware';
|
|
16
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
17
|
+
import type { CalendarEvent, CreateCalendarEventRequest, UpdateCalendarEventRequest } from '../../domain/entities/CalendarEvent.entity';
|
|
18
|
+
import { CalendarService } from '../services/CalendarService';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Calendar view mode
|
|
22
|
+
*/
|
|
23
|
+
export type CalendarViewMode = 'month' | 'week' | 'day' | 'list';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Calendar state interface
|
|
27
|
+
*/
|
|
28
|
+
interface CalendarState {
|
|
29
|
+
// State
|
|
30
|
+
events: CalendarEvent[];
|
|
31
|
+
selectedDate: Date;
|
|
32
|
+
currentMonth: Date;
|
|
33
|
+
viewMode: CalendarViewMode;
|
|
34
|
+
isLoading: boolean;
|
|
35
|
+
error: string | null;
|
|
36
|
+
|
|
37
|
+
// Actions
|
|
38
|
+
actions: {
|
|
39
|
+
// Event CRUD
|
|
40
|
+
loadEvents: () => Promise<void>;
|
|
41
|
+
addEvent: (request: CreateCalendarEventRequest) => Promise<void>;
|
|
42
|
+
updateEvent: (request: UpdateCalendarEventRequest) => Promise<void>;
|
|
43
|
+
deleteEvent: (id: string) => Promise<void>;
|
|
44
|
+
completeEvent: (id: string) => Promise<void>;
|
|
45
|
+
uncompleteEvent: (id: string) => Promise<void>;
|
|
46
|
+
|
|
47
|
+
// Navigation
|
|
48
|
+
setSelectedDate: (date: Date) => void;
|
|
49
|
+
goToToday: () => void;
|
|
50
|
+
navigateMonth: (direction: 'prev' | 'next') => void;
|
|
51
|
+
navigateWeek: (direction: 'prev' | 'next') => void;
|
|
52
|
+
setCurrentMonth: (date: Date) => void;
|
|
53
|
+
|
|
54
|
+
// View mode
|
|
55
|
+
setViewMode: (mode: CalendarViewMode) => void;
|
|
56
|
+
|
|
57
|
+
// Utilities
|
|
58
|
+
getEventsForDate: (date: Date) => CalendarEvent[];
|
|
59
|
+
getEventsForMonth: (year: number, month: number) => CalendarEvent[];
|
|
60
|
+
clearError: () => void;
|
|
61
|
+
clearAllEvents: () => Promise<void>;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Storage key for calendar events
|
|
67
|
+
*/
|
|
68
|
+
const STORAGE_KEY = 'calendar_events';
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Generate unique ID for events
|
|
72
|
+
*/
|
|
73
|
+
const generateId = (): string => {
|
|
74
|
+
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Calendar Store
|
|
79
|
+
*/
|
|
80
|
+
export const useCalendarStore = create<CalendarState>()(
|
|
81
|
+
persist(
|
|
82
|
+
(set, get) => ({
|
|
83
|
+
// Initial State
|
|
84
|
+
events: [],
|
|
85
|
+
selectedDate: new Date(),
|
|
86
|
+
currentMonth: new Date(),
|
|
87
|
+
viewMode: 'month',
|
|
88
|
+
isLoading: false,
|
|
89
|
+
error: null,
|
|
90
|
+
|
|
91
|
+
// Actions
|
|
92
|
+
actions: {
|
|
93
|
+
/**
|
|
94
|
+
* Load events from storage
|
|
95
|
+
*/
|
|
96
|
+
loadEvents: async () => {
|
|
97
|
+
set({ isLoading: true, error: null });
|
|
98
|
+
try {
|
|
99
|
+
const stored = await AsyncStorage.getItem(STORAGE_KEY);
|
|
100
|
+
if (stored) {
|
|
101
|
+
const events = JSON.parse(stored);
|
|
102
|
+
// Restore Date objects
|
|
103
|
+
events.forEach((event: CalendarEvent) => {
|
|
104
|
+
event.createdAt = new Date(event.createdAt);
|
|
105
|
+
event.updatedAt = new Date(event.updatedAt);
|
|
106
|
+
});
|
|
107
|
+
set({ events, isLoading: false });
|
|
108
|
+
} else {
|
|
109
|
+
set({ isLoading: false });
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
set({
|
|
113
|
+
error: error instanceof Error ? error.message : 'Failed to load events',
|
|
114
|
+
isLoading: false
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Add a new event
|
|
121
|
+
*/
|
|
122
|
+
addEvent: async (request: CreateCalendarEventRequest) => {
|
|
123
|
+
set({ isLoading: true, error: null });
|
|
124
|
+
try {
|
|
125
|
+
const newEvent: CalendarEvent = {
|
|
126
|
+
id: generateId(),
|
|
127
|
+
...request,
|
|
128
|
+
isCompleted: false,
|
|
129
|
+
createdAt: new Date(),
|
|
130
|
+
updatedAt: new Date(),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const events = [...get().events, newEvent];
|
|
134
|
+
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(events));
|
|
135
|
+
set({ events, isLoading: false });
|
|
136
|
+
} catch (error) {
|
|
137
|
+
set({
|
|
138
|
+
error: error instanceof Error ? error.message : 'Failed to add event',
|
|
139
|
+
isLoading: false
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Update an existing event
|
|
146
|
+
*/
|
|
147
|
+
updateEvent: async (request: UpdateCalendarEventRequest) => {
|
|
148
|
+
set({ isLoading: true, error: null });
|
|
149
|
+
try {
|
|
150
|
+
const events = get().events.map(event => {
|
|
151
|
+
if (event.id === request.id) {
|
|
152
|
+
return {
|
|
153
|
+
...event,
|
|
154
|
+
...request,
|
|
155
|
+
updatedAt: new Date(),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return event;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(events));
|
|
162
|
+
set({ events, isLoading: false });
|
|
163
|
+
} catch (error) {
|
|
164
|
+
set({
|
|
165
|
+
error: error instanceof Error ? error.message : 'Failed to update event',
|
|
166
|
+
isLoading: false
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Delete an event
|
|
173
|
+
*/
|
|
174
|
+
deleteEvent: async (id: string) => {
|
|
175
|
+
set({ isLoading: true, error: null });
|
|
176
|
+
try {
|
|
177
|
+
const events = get().events.filter(event => event.id !== id);
|
|
178
|
+
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(events));
|
|
179
|
+
set({ events, isLoading: false });
|
|
180
|
+
} catch (error) {
|
|
181
|
+
set({
|
|
182
|
+
error: error instanceof Error ? error.message : 'Failed to delete event',
|
|
183
|
+
isLoading: false
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Mark event as completed
|
|
190
|
+
*/
|
|
191
|
+
completeEvent: async (id: string) => {
|
|
192
|
+
await get().actions.updateEvent({ id, isCompleted: true });
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Mark event as incomplete
|
|
197
|
+
*/
|
|
198
|
+
uncompleteEvent: async (id: string) => {
|
|
199
|
+
await get().actions.updateEvent({ id, isCompleted: false });
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Set selected date
|
|
204
|
+
*/
|
|
205
|
+
setSelectedDate: (date: Date) => {
|
|
206
|
+
set({ selectedDate: date });
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Go to today's date
|
|
211
|
+
*/
|
|
212
|
+
goToToday: () => {
|
|
213
|
+
const today = new Date();
|
|
214
|
+
set({
|
|
215
|
+
selectedDate: today,
|
|
216
|
+
currentMonth: today,
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Navigate to previous/next month
|
|
222
|
+
*/
|
|
223
|
+
navigateMonth: (direction: 'prev' | 'next') => {
|
|
224
|
+
const currentMonth = get().currentMonth;
|
|
225
|
+
const newMonth = direction === 'prev'
|
|
226
|
+
? CalendarService.getPreviousMonth(currentMonth)
|
|
227
|
+
: CalendarService.getNextMonth(currentMonth);
|
|
228
|
+
|
|
229
|
+
set({ currentMonth: newMonth });
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Navigate to previous/next week
|
|
234
|
+
*/
|
|
235
|
+
navigateWeek: (direction: 'prev' | 'next') => {
|
|
236
|
+
const selectedDate = get().selectedDate;
|
|
237
|
+
const newDate = direction === 'prev'
|
|
238
|
+
? CalendarService.getPreviousWeek(selectedDate)
|
|
239
|
+
: CalendarService.getNextWeek(selectedDate);
|
|
240
|
+
|
|
241
|
+
set({ selectedDate: newDate });
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Set current month directly
|
|
246
|
+
*/
|
|
247
|
+
setCurrentMonth: (date: Date) => {
|
|
248
|
+
set({ currentMonth: date });
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Set view mode
|
|
253
|
+
*/
|
|
254
|
+
setViewMode: (mode: CalendarViewMode) => {
|
|
255
|
+
set({ viewMode: mode });
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get events for a specific date
|
|
260
|
+
*/
|
|
261
|
+
getEventsForDate: (date: Date) => {
|
|
262
|
+
const events = get().events;
|
|
263
|
+
return CalendarService.getEventsForDate(date, events);
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Get events for a specific month
|
|
268
|
+
*/
|
|
269
|
+
getEventsForMonth: (year: number, month: number) => {
|
|
270
|
+
const events = get().events;
|
|
271
|
+
const firstDay = new Date(year, month, 1);
|
|
272
|
+
const lastDay = new Date(year, month + 1, 0);
|
|
273
|
+
return CalendarService.getEventsInRange(firstDay, lastDay, events);
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Clear error state
|
|
278
|
+
*/
|
|
279
|
+
clearError: () => {
|
|
280
|
+
set({ error: null });
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Clear all events (for testing/reset)
|
|
285
|
+
*/
|
|
286
|
+
clearAllEvents: async () => {
|
|
287
|
+
set({ isLoading: true, error: null });
|
|
288
|
+
try {
|
|
289
|
+
await AsyncStorage.removeItem(STORAGE_KEY);
|
|
290
|
+
set({ events: [], isLoading: false });
|
|
291
|
+
} catch (error) {
|
|
292
|
+
set({
|
|
293
|
+
error: error instanceof Error ? error.message : 'Failed to clear events',
|
|
294
|
+
isLoading: false
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
}),
|
|
300
|
+
{
|
|
301
|
+
name: 'calendar-storage',
|
|
302
|
+
storage: createJSONStorage(() => AsyncStorage),
|
|
303
|
+
// Only persist events, not UI state
|
|
304
|
+
partialize: (state) => ({ events: state.events }),
|
|
305
|
+
}
|
|
306
|
+
)
|
|
307
|
+
);
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date Utilities
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for date operations and formatting.
|
|
5
|
+
* No side effects, timezone-aware operations.
|
|
6
|
+
*
|
|
7
|
+
* SOLID: Single Responsibility - Only date operations
|
|
8
|
+
* DRY: Reusable date utility functions
|
|
9
|
+
* KISS: Simple, focused functions
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export class DateUtilities {
|
|
13
|
+
/**
|
|
14
|
+
* Format date to string (YYYY-MM-DD)
|
|
15
|
+
*/
|
|
16
|
+
static formatDateToString(date: Date): string {
|
|
17
|
+
return date.toISOString().split('T')[0];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if two dates are the same day
|
|
22
|
+
*/
|
|
23
|
+
static isSameDay(date1: Date, date2: Date): boolean {
|
|
24
|
+
return this.formatDateToString(date1) === this.formatDateToString(date2);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Add days to a date
|
|
29
|
+
*/
|
|
30
|
+
static addDays(date: Date, days: number): Date {
|
|
31
|
+
const result = new Date(date);
|
|
32
|
+
result.setDate(result.getDate() + days);
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if date is today
|
|
38
|
+
*/
|
|
39
|
+
static isToday(date: Date): boolean {
|
|
40
|
+
return this.isSameDay(date, new Date());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get current timezone
|
|
45
|
+
*/
|
|
46
|
+
static getCurrentTimezone(): string {
|
|
47
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get start of month
|
|
52
|
+
*/
|
|
53
|
+
static getStartOfMonth(date: Date): Date {
|
|
54
|
+
return new Date(date.getFullYear(), date.getMonth(), 1);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get end of month
|
|
59
|
+
*/
|
|
60
|
+
static getEndOfMonth(date: Date): Date {
|
|
61
|
+
return new Date(date.getFullYear(), date.getMonth() + 1, 0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get number of days in month
|
|
66
|
+
*/
|
|
67
|
+
static getDaysInMonth(date: Date): number {
|
|
68
|
+
return this.getEndOfMonth(date).getDate();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Get start of week (Sunday)
|
|
73
|
+
*/
|
|
74
|
+
static getStartOfWeek(date: Date): Date {
|
|
75
|
+
const result = new Date(date);
|
|
76
|
+
const day = result.getDay();
|
|
77
|
+
const diff = result.getDate() - day;
|
|
78
|
+
return new Date(result.setDate(diff));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Get end of week (Saturday)
|
|
83
|
+
*/
|
|
84
|
+
static getEndOfWeek(date: Date): Date {
|
|
85
|
+
const result = this.getStartOfWeek(date);
|
|
86
|
+
result.setDate(result.getDate() + 6);
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Parse date from string (YYYY-MM-DD)
|
|
92
|
+
*/
|
|
93
|
+
static parseDate(dateString: string): Date {
|
|
94
|
+
const [year, month, day] = dateString.split('-').map(Number);
|
|
95
|
+
return new Date(year, month - 1, day);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Format time to string (HH:MM)
|
|
100
|
+
*/
|
|
101
|
+
static formatTimeToString(date: Date): string {
|
|
102
|
+
return date.toTimeString().slice(0, 5);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if date is in the past
|
|
107
|
+
*/
|
|
108
|
+
static isPast(date: Date): boolean {
|
|
109
|
+
return date < new Date();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if date is in the future
|
|
114
|
+
*/
|
|
115
|
+
static isFuture(date: Date): boolean {
|
|
116
|
+
return date > new Date();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|