@umituz/react-native-design-system 2.3.13 → 2.3.15
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 +32 -13
- package/src/index.ts +116 -0
- package/src/layouts/ScreenLayout/ScreenLayout.example.tsx +2 -2
- package/src/layouts/ScreenLayout/ScreenLayout.tsx +1 -1
- package/src/molecules/animation/core/AnimationCore.ts +29 -0
- package/src/molecules/animation/domain/entities/Animation.ts +81 -0
- package/src/molecules/animation/domain/entities/Fireworks.ts +44 -0
- package/src/molecules/animation/domain/entities/Theme.ts +76 -0
- package/src/molecules/animation/index.ts +146 -0
- package/src/molecules/animation/infrastructure/services/AnimationConfigService.ts +35 -0
- package/src/molecules/animation/infrastructure/services/SpringAnimationConfigService.ts +67 -0
- package/src/molecules/animation/infrastructure/services/TimingAnimationConfigService.ts +57 -0
- package/src/molecules/animation/infrastructure/services/__tests__/SpringAnimationConfigService.test.ts +114 -0
- package/src/molecules/animation/infrastructure/services/__tests__/TimingAnimationConfigService.test.ts +105 -0
- package/src/molecules/animation/presentation/components/Fireworks.tsx +126 -0
- package/src/molecules/animation/presentation/components/__tests__/Fireworks.test.tsx +189 -0
- package/src/molecules/animation/presentation/hooks/__tests__/useAnimation.integration.test.ts +216 -0
- package/src/molecules/animation/presentation/hooks/__tests__/useFireworks.test.ts +242 -0
- package/src/molecules/animation/presentation/hooks/__tests__/useGesture.test.ts +111 -0
- package/src/molecules/animation/presentation/hooks/__tests__/useSpringAnimation.test.ts +131 -0
- package/src/molecules/animation/presentation/hooks/__tests__/useTimingAnimation.test.ts +175 -0
- package/src/molecules/animation/presentation/hooks/__tests__/useTransformAnimation.test.ts +137 -0
- package/src/molecules/animation/presentation/hooks/useAnimation.ts +77 -0
- package/src/molecules/animation/presentation/hooks/useFireworks.ts +141 -0
- package/src/molecules/animation/presentation/hooks/useGesture.ts +61 -0
- package/src/molecules/animation/presentation/hooks/useGestureCreators.ts +163 -0
- package/src/molecules/animation/presentation/hooks/useGestureState.ts +53 -0
- package/src/molecules/animation/presentation/hooks/useIconAnimations.ts +119 -0
- package/src/molecules/animation/presentation/hooks/useModalAnimations.ts +124 -0
- package/src/molecules/animation/presentation/hooks/useReanimatedReady.ts +60 -0
- package/src/molecules/animation/presentation/hooks/useSpringAnimation.ts +69 -0
- package/src/molecules/animation/presentation/hooks/useTimingAnimation.ts +111 -0
- package/src/molecules/animation/presentation/hooks/useTransformAnimation.ts +57 -0
- package/src/molecules/animation/presentation/providers/AnimationThemeProvider.tsx +62 -0
- package/src/molecules/animation/presentation/providers/__tests__/AnimationThemeProvider.test.tsx +165 -0
- package/src/molecules/animation/types/global.d.ts +97 -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/celebration/domain/entities/CelebrationConfig.ts +17 -0
- package/src/molecules/celebration/domain/entities/FireworksConfig.ts +32 -0
- package/src/molecules/celebration/index.ts +93 -0
- package/src/molecules/celebration/infrastructure/services/FireworksConfigService.ts +49 -0
- package/src/molecules/celebration/presentation/components/CelebrationFireworksOverlay.tsx +33 -0
- package/src/molecules/celebration/presentation/components/CelebrationModal.tsx +78 -0
- package/src/molecules/celebration/presentation/components/CelebrationModalContent.tsx +90 -0
- package/src/molecules/celebration/presentation/hooks/useCelebrationModalAnimation.ts +49 -0
- package/src/molecules/celebration/presentation/hooks/useCelebrationState.ts +45 -0
- package/src/molecules/celebration/presentation/styles/CelebrationModalStyles.ts +65 -0
- package/src/molecules/countdown/components/Countdown.tsx +128 -0
- package/src/molecules/countdown/components/CountdownHeader.tsx +84 -0
- package/src/molecules/countdown/components/TimeUnit.tsx +73 -0
- package/src/molecules/countdown/hooks/useCountdown.ts +107 -0
- package/src/molecules/countdown/index.ts +25 -0
- package/src/molecules/countdown/types/CountdownTypes.ts +31 -0
- package/src/molecules/countdown/utils/TimeCalculator.ts +46 -0
- package/src/molecules/emoji/domain/entities/Emoji.ts +129 -0
- package/src/molecules/emoji/index.ts +177 -0
- package/src/molecules/emoji/presentation/components/EmojiPicker.tsx +102 -0
- package/src/molecules/emoji/presentation/hooks/useEmojiPicker.ts +171 -0
- package/src/molecules/index.ts +24 -0
- package/src/molecules/long-press-menu/domain/entities/MenuAction.ts +37 -0
- package/src/molecules/long-press-menu/index.ts +16 -0
- package/src/molecules/navigation/StackNavigator.tsx +75 -0
- package/src/molecules/navigation/TabsNavigator.tsx +94 -0
- package/src/molecules/navigation/components/FabButton.tsx +45 -0
- package/src/molecules/navigation/components/TabLabel.tsx +47 -0
- package/src/molecules/navigation/createStackNavigator.ts +20 -0
- package/src/molecules/navigation/createTabNavigator.ts +20 -0
- package/src/molecules/navigation/hooks/useTabBarStyles.ts +54 -0
- package/src/molecules/navigation/index.ts +37 -0
- package/src/molecules/navigation/types.ts +118 -0
- package/src/molecules/navigation/utils/AppNavigation.ts +101 -0
- package/src/molecules/navigation/utils/IconRenderer.ts +50 -0
- package/src/molecules/navigation/utils/LabelProcessor.ts +70 -0
- package/src/molecules/navigation/utils/NavigationCleanup.ts +62 -0
- package/src/molecules/navigation/utils/NavigationTheme.ts +21 -0
- package/src/molecules/navigation/utils/NavigationValidator.ts +61 -0
- package/src/molecules/navigation/utils/ScreenFactory.ts +115 -0
- package/src/molecules/navigation/utils/__tests__/IconRenderer.getIconName.test.ts +109 -0
- package/src/molecules/navigation/utils/__tests__/IconRenderer.renderIcon.test.ts +116 -0
- package/src/molecules/navigation/utils/__tests__/LabelProcessor.processLabel.test.ts +116 -0
- package/src/molecules/navigation/utils/__tests__/LabelProcessor.processTitle.test.ts +59 -0
- package/src/molecules/navigation/utils/__tests__/NavigationCleanup.test.ts +271 -0
- package/src/molecules/navigation/utils/__tests__/NavigationValidator.test.ts +252 -0
- package/src/molecules/swipe-actions/domain/entities/SwipeAction.ts +194 -0
- package/src/molecules/swipe-actions/index.ts +6 -0
- package/src/molecules/swipe-actions/presentation/components/SwipeActionButton.tsx +131 -0
- package/src/theme/hooks/useResponsiveDesignTokens.ts +1 -1
- package/src/utilities/clipboard/ClipboardUtils.ts +71 -0
- package/src/utilities/clipboard/index.ts +5 -0
- package/src/utilities/index.ts +6 -0
- package/src/utilities/sharing/domain/entities/Share.ts +210 -0
- package/src/utilities/sharing/index.ts +205 -0
- package/src/utilities/sharing/infrastructure/services/SharingService.ts +165 -0
- package/src/utilities/sharing/presentation/hooks/useSharing.ts +154 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calendar Service
|
|
3
|
+
*
|
|
4
|
+
* Facade for calendar operations using composition.
|
|
5
|
+
* Delegates to specialized services for specific operations.
|
|
6
|
+
*
|
|
7
|
+
* SOLID: Facade pattern - Single entry point, delegates to specialists
|
|
8
|
+
* DRY: Avoids code duplication by composing smaller services
|
|
9
|
+
* KISS: Simple interface, complex operations delegated
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { CalendarDay, CalendarWeek } from '../../domain/entities/CalendarDay.entity';
|
|
13
|
+
import type { CalendarEvent, SystemCalendar } from '../../domain/entities/CalendarEvent.entity';
|
|
14
|
+
import { CalendarGeneration } from './CalendarGeneration';
|
|
15
|
+
import { CalendarPermissions } from './CalendarPermissions';
|
|
16
|
+
// CalendarEvents is not used in this facade
|
|
17
|
+
// import { CalendarEvents } from './CalendarEvents';
|
|
18
|
+
import { CalendarSync } from './CalendarSync';
|
|
19
|
+
import { DateUtilities } from '../utils/DateUtilities';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Calendar Service Implementation
|
|
23
|
+
*
|
|
24
|
+
* Facade that delegates to specialized services.
|
|
25
|
+
* Follows SOLID principles with composition over inheritance.
|
|
26
|
+
*/
|
|
27
|
+
export class CalendarService {
|
|
28
|
+
/**
|
|
29
|
+
* Generate calendar days for a specific month
|
|
30
|
+
*/
|
|
31
|
+
static getMonthDays(
|
|
32
|
+
year: number,
|
|
33
|
+
month: number,
|
|
34
|
+
events: CalendarEvent[] = []
|
|
35
|
+
): CalendarDay[] {
|
|
36
|
+
return CalendarGeneration.generateMonthDays(year, month, events);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Generate calendar week
|
|
41
|
+
*/
|
|
42
|
+
static getWeek(date: Date, events: CalendarEvent[] = []): CalendarWeek {
|
|
43
|
+
const startDate = DateUtilities.getStartOfWeek(date);
|
|
44
|
+
const endDate = DateUtilities.getEndOfWeek(date);
|
|
45
|
+
const days = CalendarGeneration.generateWeekDays(startDate, events);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
startDate,
|
|
49
|
+
endDate,
|
|
50
|
+
days
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Navigate to previous month
|
|
56
|
+
*/
|
|
57
|
+
static getPreviousMonth(currentDate: Date): Date {
|
|
58
|
+
return CalendarGeneration.getPreviousMonth(currentDate);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Navigate to next month
|
|
63
|
+
*/
|
|
64
|
+
static getNextMonth(currentDate: Date): Date {
|
|
65
|
+
return CalendarGeneration.getNextMonth(currentDate);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Navigate to previous week
|
|
70
|
+
*/
|
|
71
|
+
static getPreviousWeek(currentDate: Date): Date {
|
|
72
|
+
return CalendarGeneration.getPreviousWeek(currentDate);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Navigate to next week
|
|
77
|
+
*/
|
|
78
|
+
static getNextWeek(currentDate: Date): Date {
|
|
79
|
+
return CalendarGeneration.getNextWeek(currentDate);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Request calendar permissions
|
|
84
|
+
*/
|
|
85
|
+
static async requestPermissions() {
|
|
86
|
+
return CalendarPermissions.requestPermissions();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if permissions are granted
|
|
91
|
+
*/
|
|
92
|
+
static async hasPermissions(): Promise<boolean> {
|
|
93
|
+
return CalendarPermissions.hasPermissions();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Sync event to system calendar
|
|
98
|
+
*/
|
|
99
|
+
static async syncToSystemCalendar(event: CalendarEvent) {
|
|
100
|
+
return CalendarSync.syncToSystemCalendar(event);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Update system calendar event
|
|
105
|
+
*/
|
|
106
|
+
static async updateSystemCalendarEvent(event: CalendarEvent) {
|
|
107
|
+
return CalendarSync.updateSystemCalendarEvent(event);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Remove event from system calendar
|
|
112
|
+
*/
|
|
113
|
+
static async removeFromSystemCalendar(eventId: string) {
|
|
114
|
+
return CalendarSync.removeFromSystemCalendar(eventId);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get system calendars
|
|
119
|
+
*/
|
|
120
|
+
static async getSystemCalendars(): Promise<SystemCalendar[]> {
|
|
121
|
+
return CalendarSync.getSystemCalendars();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get events for a specific date
|
|
126
|
+
*/
|
|
127
|
+
static getEventsForDate(date: Date, events: CalendarEvent[]): CalendarEvent[] {
|
|
128
|
+
return CalendarGeneration.getEventsForDate(date, events);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get events in date range
|
|
133
|
+
*/
|
|
134
|
+
static getEventsInRange(
|
|
135
|
+
startDate: Date,
|
|
136
|
+
endDate: Date,
|
|
137
|
+
events: CalendarEvent[]
|
|
138
|
+
): CalendarEvent[] {
|
|
139
|
+
return CalendarGeneration.getEventsInRange(startDate, endDate, events);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get weekday names
|
|
144
|
+
*/
|
|
145
|
+
static getWeekdayNames(): string[] {
|
|
146
|
+
const weekdays: string[] = [];
|
|
147
|
+
for (let i = 0; i < 7; i++) {
|
|
148
|
+
const date = new Date();
|
|
149
|
+
date.setDate(date.getDate() - date.getDay() + i);
|
|
150
|
+
weekdays.push(date.toLocaleDateString('en-US', { weekday: 'short' }));
|
|
151
|
+
}
|
|
152
|
+
return weekdays;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Check if two dates are the same day
|
|
157
|
+
*/
|
|
158
|
+
static isSameDay(date1: Date, date2: Date): boolean {
|
|
159
|
+
return DateUtilities.isSameDay(date1, date2);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -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
|
+
);
|