@umituz/react-native-design-system 4.23.82 → 4.23.84

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.
Files changed (29) hide show
  1. package/package.json +1 -1
  2. package/src/atoms/input/hooks/useInputState.ts +2 -4
  3. package/src/image/presentation/components/editor/text-editor/TextTransformTab.tsx +40 -152
  4. package/src/image/presentation/components/editor/text-editor/components/TransformButtonRow.tsx +124 -0
  5. package/src/layouts/Grid/Grid.tsx +16 -11
  6. package/src/media/domain/utils/FileValidator.ts +156 -0
  7. package/src/media/infrastructure/services/MediaPickerService.ts +18 -57
  8. package/src/media/infrastructure/utils/PermissionManager.ts +92 -0
  9. package/src/media/infrastructure/utils/file-media-utils.ts +25 -8
  10. package/src/media/presentation/hooks/useMedia.ts +5 -4
  11. package/src/molecules/alerts/AlertBanner.tsx +9 -25
  12. package/src/molecules/alerts/AlertInline.tsx +4 -23
  13. package/src/molecules/alerts/AlertModal.tsx +4 -11
  14. package/src/molecules/alerts/AlertToast.tsx +14 -13
  15. package/src/molecules/alerts/utils/alertUtils.ts +133 -0
  16. package/src/molecules/calendar/infrastructure/storage/CalendarStore.ts +65 -25
  17. package/src/molecules/countdown/hooks/useCountdown.ts +13 -5
  18. package/src/molecules/swipe-actions/domain/entities/SwipeAction.ts +15 -123
  19. package/src/molecules/swipe-actions/domain/utils/swipeActionHelpers.ts +109 -0
  20. package/src/molecules/swipe-actions/domain/utils/swipeActionValidator.ts +54 -0
  21. package/src/molecules/swipe-actions/presentation/components/SwipeActionButton.tsx +24 -6
  22. package/src/offline/presentation/hooks/useOffline.ts +2 -1
  23. package/src/storage/domain/utils/devUtils.ts +7 -6
  24. package/src/storage/infrastructure/adapters/StorageService.ts +0 -9
  25. package/src/storage/infrastructure/repositories/BaseStorageOperations.ts +0 -3
  26. package/src/tanstack/domain/utils/MetricsCalculator.ts +103 -0
  27. package/src/tanstack/infrastructure/monitoring/DevMonitor.ts +35 -29
  28. package/src/timezone/infrastructure/utils/SimpleCache.ts +24 -2
  29. package/src/molecules/alerts/utils/alertToastHelpers.ts +0 -70
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Permission Manager
3
+ *
4
+ * Centralized permission handling for media operations.
5
+ */
6
+
7
+ import * as ImagePicker from "expo-image-picker";
8
+ import { MediaLibraryPermission } from "../../domain/entities/Media";
9
+ import { mapPermissionStatus } from "./mediaPickerMappers";
10
+
11
+ /**
12
+ * Permission type for media operations
13
+ */
14
+ export type PermissionType = 'camera' | 'mediaLibrary';
15
+
16
+ /**
17
+ * Permission manager for media operations
18
+ */
19
+ export class PermissionManager {
20
+ /**
21
+ * Requests camera permission
22
+ */
23
+ static async requestCameraPermission(): Promise<MediaLibraryPermission> {
24
+ try {
25
+ const { status } = await ImagePicker.requestCameraPermissionsAsync();
26
+ return mapPermissionStatus(status);
27
+ } catch {
28
+ return MediaLibraryPermission.DENIED;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Requests media library permission
34
+ */
35
+ static async requestMediaLibraryPermission(): Promise<MediaLibraryPermission> {
36
+ try {
37
+ const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
38
+ return mapPermissionStatus(status);
39
+ } catch {
40
+ return MediaLibraryPermission.DENIED;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Gets current camera permission status
46
+ */
47
+ static async getCameraPermissionStatus(): Promise<MediaLibraryPermission> {
48
+ try {
49
+ const { status } = await ImagePicker.getCameraPermissionsAsync();
50
+ return mapPermissionStatus(status);
51
+ } catch {
52
+ return MediaLibraryPermission.DENIED;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Gets current media library permission status
58
+ */
59
+ static async getMediaLibraryPermissionStatus(): Promise<MediaLibraryPermission> {
60
+ try {
61
+ const { status } = await ImagePicker.getMediaLibraryPermissionsAsync();
62
+ return mapPermissionStatus(status);
63
+ } catch {
64
+ return MediaLibraryPermission.DENIED;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Generic permission request based on type
70
+ */
71
+ static async requestPermission(type: PermissionType): Promise<MediaLibraryPermission> {
72
+ return type === 'camera'
73
+ ? this.requestCameraPermission()
74
+ : this.requestMediaLibraryPermission();
75
+ }
76
+
77
+ /**
78
+ * Generic permission status check based on type
79
+ */
80
+ static async getPermissionStatus(type: PermissionType): Promise<MediaLibraryPermission> {
81
+ return type === 'camera'
82
+ ? this.getCameraPermissionStatus()
83
+ : this.getMediaLibraryPermissionStatus();
84
+ }
85
+
86
+ /**
87
+ * Checks if permission is granted
88
+ */
89
+ static isPermissionGranted(status: MediaLibraryPermission): boolean {
90
+ return status === MediaLibraryPermission.GRANTED || status === MediaLibraryPermission.LIMITED;
91
+ }
92
+ }
@@ -74,16 +74,33 @@ export const downloadMediaToFile = async (url: string, isVideo: boolean): Promis
74
74
  const filename = `media_${timestamp}.${extension}`;
75
75
  const file = new ExpoFile(Paths.cache, filename);
76
76
 
77
- const response = await fetch(url);
78
- if (!response.ok) {
79
- throw new Error(`Failed to download media: ${response.statusText}`);
80
- }
77
+ // Create abort controller for timeout
78
+ const controller = new AbortController();
79
+ const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout
81
80
 
82
- const arrayBuffer = await response.arrayBuffer();
83
- const bytes = new Uint8Array(arrayBuffer);
84
- file.write(bytes);
81
+ try {
82
+ const response = await fetch(url, { signal: controller.signal });
83
+ clearTimeout(timeoutId);
85
84
 
86
- return file.uri;
85
+ if (!response.ok) {
86
+ throw new Error(`Failed to download media: ${response.statusText}`);
87
+ }
88
+
89
+ const arrayBuffer = await response.arrayBuffer();
90
+ const bytes = new Uint8Array(arrayBuffer);
91
+ file.write(bytes);
92
+
93
+ return file.uri;
94
+ } catch (error) {
95
+ // Clear timeout if error occurs
96
+ clearTimeout(timeoutId);
97
+
98
+ if (error instanceof Error && error.name === 'AbortError') {
99
+ throw new Error('Download timeout - request took longer than 30 seconds');
100
+ }
101
+
102
+ throw error;
103
+ }
87
104
  };
88
105
 
89
106
  export interface SaveToGalleryResult {
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { useState, useCallback } from "react";
9
9
  import { MediaPickerService } from "../../infrastructure/services/MediaPickerService";
10
+ import { PermissionManager } from "../../infrastructure/utils/PermissionManager";
10
11
  import type {
11
12
  MediaPickerOptions,
12
13
  MediaPickerResult,
@@ -140,7 +141,7 @@ export const useMedia = () => {
140
141
  const requestCameraPermission =
141
142
  useCallback(async (): Promise<MediaLibraryPermission> => {
142
143
  try {
143
- return await MediaPickerService.requestCameraPermission();
144
+ return await PermissionManager.requestCameraPermission();
144
145
  } catch {
145
146
  return MediaLibraryPermission.DENIED;
146
147
  }
@@ -149,7 +150,7 @@ export const useMedia = () => {
149
150
  const requestMediaLibraryPermission =
150
151
  useCallback(async (): Promise<MediaLibraryPermission> => {
151
152
  try {
152
- return await MediaPickerService.requestMediaLibraryPermission();
153
+ return await PermissionManager.requestMediaLibraryPermission();
153
154
  } catch {
154
155
  return MediaLibraryPermission.DENIED;
155
156
  }
@@ -158,7 +159,7 @@ export const useMedia = () => {
158
159
  const getCameraPermissionStatus =
159
160
  useCallback(async (): Promise<MediaLibraryPermission> => {
160
161
  try {
161
- return await MediaPickerService.getCameraPermissionStatus();
162
+ return await PermissionManager.getCameraPermissionStatus();
162
163
  } catch {
163
164
  return MediaLibraryPermission.DENIED;
164
165
  }
@@ -167,7 +168,7 @@ export const useMedia = () => {
167
168
  const getMediaLibraryPermissionStatus =
168
169
  useCallback(async (): Promise<MediaLibraryPermission> => {
169
170
  try {
170
- return await MediaPickerService.getMediaLibraryPermissionStatus();
171
+ return await PermissionManager.getMediaLibraryPermissionStatus();
171
172
  } catch {
172
173
  return MediaLibraryPermission.DENIED;
173
174
  }
@@ -6,15 +6,14 @@
6
6
  * Auto-dismisses after duration (default 3 seconds).
7
7
  */
8
8
 
9
- import React, { useEffect } from 'react';
9
+ import React, { useEffect, useCallback } from 'react';
10
10
  import { StyleSheet, View, Pressable } from 'react-native';
11
11
  import { useSafeAreaInsets } from '../../safe-area';
12
12
  import { AtomicText, AtomicIcon, useIconName } from '../../atoms';
13
13
  import { useAppDesignTokens } from '../../theme';
14
- import { Alert, AlertType, AlertPosition } from './AlertTypes';
14
+ import { Alert, AlertPosition } from './AlertTypes';
15
15
  import { useAlertStore } from './AlertStore';
16
-
17
- const DEFAULT_DURATION = 3000;
16
+ import { getAlertBackgroundColor, getAlertTextColor, DEFAULT_ALERT_DURATION } from './utils/alertUtils';
18
17
 
19
18
  interface AlertBannerProps {
20
19
  alert: Alert;
@@ -26,37 +25,22 @@ export function AlertBanner({ alert }: AlertBannerProps) {
26
25
  const tokens = useAppDesignTokens();
27
26
  const closeIcon = useIconName('close');
28
27
 
29
- const handleDismiss = () => {
28
+ const handleDismiss = useCallback(() => {
30
29
  dismissAlert(alert.id);
31
30
  alert.onDismiss?.();
32
- };
31
+ }, [alert.id, dismissAlert, alert.onDismiss]);
33
32
 
34
33
  // Auto-dismiss after duration
35
34
  useEffect(() => {
36
- const duration = alert.duration ?? DEFAULT_DURATION;
35
+ const duration = alert.duration ?? DEFAULT_ALERT_DURATION;
37
36
  if (duration <= 0) return;
38
37
 
39
38
  const timer = setTimeout(handleDismiss, duration);
40
39
  return () => clearTimeout(timer);
41
- }, [alert.id, alert.duration]);
42
-
43
- const getBackgroundColor = (type: AlertType): string => {
44
- switch (type) {
45
- case AlertType.SUCCESS:
46
- return tokens.colors.success;
47
- case AlertType.ERROR:
48
- return tokens.colors.error;
49
- case AlertType.WARNING:
50
- return tokens.colors.warning;
51
- case AlertType.INFO:
52
- return tokens.colors.info;
53
- default:
54
- return tokens.colors.backgroundSecondary;
55
- }
56
- };
40
+ }, [alert.duration, handleDismiss]);
57
41
 
58
- const backgroundColor = getBackgroundColor(alert.type);
59
- const textColor = tokens.colors.textInverse;
42
+ const backgroundColor = getAlertBackgroundColor(alert.type, tokens);
43
+ const textColor = getAlertTextColor(tokens);
60
44
  const isTop = alert.position === AlertPosition.TOP;
61
45
 
62
46
  return (
@@ -6,7 +6,8 @@ import React from 'react';
6
6
  import { StyleSheet, View } from 'react-native';
7
7
  import { AtomicText } from '../../atoms';
8
8
  import { useAppDesignTokens } from '../../theme';
9
- import { Alert, AlertType } from './AlertTypes';
9
+ import { Alert } from './AlertTypes';
10
+ import { getAlertBorderColor, getAlertBackgroundColorInline } from './utils/alertUtils';
10
11
 
11
12
  interface AlertInlineProps {
12
13
  alert: Alert;
@@ -15,32 +16,12 @@ interface AlertInlineProps {
15
16
  export const AlertInline: React.FC<AlertInlineProps> = ({ alert }) => {
16
17
  const tokens = useAppDesignTokens();
17
18
 
18
- const getBorderColor = () => {
19
- switch (alert.type) {
20
- case AlertType.SUCCESS: return tokens.colors.success;
21
- case AlertType.ERROR: return tokens.colors.error;
22
- case AlertType.WARNING: return tokens.colors.warning;
23
- case AlertType.INFO: return tokens.colors.info;
24
- default: return tokens.colors.border;
25
- }
26
- };
27
-
28
- const getBackgroundColor = () => {
29
- switch (alert.type) {
30
- case AlertType.SUCCESS: return tokens.colors.success + '15';
31
- case AlertType.ERROR: return tokens.colors.error + '15';
32
- case AlertType.WARNING: return tokens.colors.warning + '15';
33
- case AlertType.INFO: return tokens.colors.info + '15';
34
- default: return tokens.colors.backgroundSecondary;
35
- }
36
- };
37
-
38
19
  return (
39
20
  <View style={[
40
21
  styles.container,
41
22
  {
42
- borderColor: getBorderColor(),
43
- backgroundColor: getBackgroundColor(),
23
+ borderColor: getAlertBorderColor(alert.type, tokens),
24
+ backgroundColor: getAlertBackgroundColorInline(alert.type, tokens),
44
25
  borderRadius: tokens.borders.radius.sm,
45
26
  padding: tokens.spacing.md,
46
27
  marginVertical: tokens.spacing.sm,
@@ -6,8 +6,9 @@ import React from 'react';
6
6
  import { StyleSheet, View, Modal, Pressable } from 'react-native';
7
7
  import { AtomicText, AtomicButton } from '../../atoms';
8
8
  import { useAppDesignTokens } from '../../theme';
9
- import { Alert, AlertType } from './AlertTypes';
9
+ import { Alert } from './AlertTypes';
10
10
  import { useAlertStore } from './AlertStore';
11
+ import { getAlertBackgroundColor } from './utils/alertUtils';
11
12
 
12
13
  interface AlertModalProps {
13
14
  alert: Alert;
@@ -22,15 +23,7 @@ export const AlertModal: React.FC<AlertModalProps> = ({ alert }) => {
22
23
  alert.onDismiss?.();
23
24
  };
24
25
 
25
- const getHeaderColor = () => {
26
- switch (alert.type) {
27
- case AlertType.SUCCESS: return tokens.colors.success;
28
- case AlertType.ERROR: return tokens.colors.error;
29
- case AlertType.WARNING: return tokens.colors.warning;
30
- case AlertType.INFO: return tokens.colors.info;
31
- default: return tokens.colors.primary;
32
- }
33
- };
26
+ const headerColor = getAlertBackgroundColor(alert.type, tokens);
34
27
 
35
28
  return (
36
29
  <Modal
@@ -53,7 +46,7 @@ export const AlertModal: React.FC<AlertModalProps> = ({ alert }) => {
53
46
  borderColor: tokens.colors.border,
54
47
  }
55
48
  ]}>
56
- <View style={[styles.header, { backgroundColor: getHeaderColor() }]}>
49
+ <View style={[styles.header, { backgroundColor: headerColor }]}>
57
50
  <AtomicText type="titleLarge" style={{ color: tokens.colors.textInverse }}>
58
51
  {alert.title}
59
52
  </AtomicText>
@@ -5,18 +5,19 @@
5
5
  * Floats on top of content.
6
6
  */
7
7
 
8
- import React, { useEffect } from 'react';
8
+ import React, { useEffect, useCallback } from 'react';
9
9
  import { StyleSheet, View, Pressable } from 'react-native';
10
10
  import { AtomicText, AtomicIcon, useIconName } from '../../atoms';
11
11
  import { useAppDesignTokens } from '../../theme';
12
12
  import { Alert } from './AlertTypes';
13
13
  import { useAlertStore } from './AlertStore';
14
14
  import {
15
- getAlertBackgroundColor,
16
- getActionButtonStyle,
17
- getActionTextColor,
18
- DEFAULT_TOAST_DURATION,
19
- } from './utils/alertToastHelpers';
15
+ getAlertBackgroundColor,
16
+ getAlertTextColor,
17
+ getActionButtonStyle,
18
+ getActionTextColor,
19
+ DEFAULT_ALERT_DURATION,
20
+ } from './utils/alertUtils';
20
21
 
21
22
  interface AlertToastProps {
22
23
  alert: Alert;
@@ -27,28 +28,28 @@ export function AlertToast({ alert }: AlertToastProps) {
27
28
  const tokens = useAppDesignTokens();
28
29
  const closeIcon = useIconName('close');
29
30
 
30
- const dismiss = () => {
31
+ const dismiss = useCallback(() => {
31
32
  dismissAlert(alert.id);
32
33
  alert.onDismiss?.();
33
- };
34
+ }, [alert.id, dismissAlert, alert.onDismiss]);
34
35
 
35
- const handleDismiss = () => {
36
+ const handleDismiss = useCallback(() => {
36
37
  if (alert.dismissible) {
37
38
  dismiss();
38
39
  }
39
- };
40
+ }, [alert.dismissible, dismiss]);
40
41
 
41
42
  // Auto-dismiss after duration
42
43
  useEffect(() => {
43
- const duration = alert.duration ?? DEFAULT_TOAST_DURATION;
44
+ const duration = alert.duration ?? DEFAULT_ALERT_DURATION;
44
45
  if (duration <= 0) return;
45
46
 
46
47
  const timer = setTimeout(dismiss, duration);
47
48
  return () => clearTimeout(timer);
48
- }, [alert.id, alert.duration]);
49
+ }, [alert.duration, dismiss]);
49
50
 
50
51
  const backgroundColor = getAlertBackgroundColor(alert.type, tokens);
51
- const textColor = tokens.colors.textInverse;
52
+ const textColor = getAlertTextColor(tokens);
52
53
 
53
54
  return (
54
55
  <View
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Alert Utilities
3
+ *
4
+ * Helper functions for alert component styling and behavior.
5
+ */
6
+
7
+ import type { DesignTokens } from '../../../theme';
8
+ import type { StyleProp, ViewStyle } from 'react-native';
9
+ import { AlertType } from '../AlertTypes';
10
+ import type { AlertAction } from '../AlertTypes';
11
+
12
+ /**
13
+ * Gets background color for alert type
14
+ *
15
+ * @param type - Alert type
16
+ * @param tokens - Design tokens containing color definitions
17
+ * @returns Color string for the alert type
18
+ */
19
+ export function getAlertBackgroundColor(type: AlertType, tokens: DesignTokens): string {
20
+ const colors = {
21
+ [AlertType.SUCCESS]: tokens.colors.success,
22
+ [AlertType.ERROR]: tokens.colors.error,
23
+ [AlertType.WARNING]: tokens.colors.warning,
24
+ [AlertType.INFO]: tokens.colors.info,
25
+ };
26
+ return colors[type] || tokens.colors.backgroundSecondary;
27
+ }
28
+
29
+ /**
30
+ * Gets border color for alert type
31
+ *
32
+ * @param type - Alert type
33
+ * @param tokens - Design tokens containing color definitions
34
+ * @returns Border color string for the alert type
35
+ */
36
+ export function getAlertBorderColor(type: AlertType, tokens: DesignTokens): string {
37
+ const colors = {
38
+ [AlertType.SUCCESS]: tokens.colors.success,
39
+ [AlertType.ERROR]: tokens.colors.error,
40
+ [AlertType.WARNING]: tokens.colors.warning,
41
+ [AlertType.INFO]: tokens.colors.info,
42
+ };
43
+ return colors[type] || tokens.colors.border;
44
+ }
45
+
46
+ /**
47
+ * Gets background color with opacity for inline alerts
48
+ *
49
+ * @param type - Alert type
50
+ * @param tokens - Design tokens containing color definitions
51
+ * @param opacity - Opacity value (0-255 hex), default '15' for ~8%
52
+ * @returns Background color string with opacity
53
+ */
54
+ export function getAlertBackgroundColorInline(type: AlertType, tokens: DesignTokens, opacity: string = '15'): string {
55
+ const baseColor = getAlertBackgroundColor(type, tokens);
56
+ return baseColor + opacity;
57
+ }
58
+
59
+ /**
60
+ * Gets text color for alert type
61
+ *
62
+ * @param tokens - Design tokens containing color definitions
63
+ * @returns Text color string (typically inverse for alerts)
64
+ */
65
+ export function getAlertTextColor(tokens: DesignTokens): string {
66
+ return tokens.colors.textInverse;
67
+ }
68
+
69
+ /**
70
+ * Gets default icon for alert type
71
+ *
72
+ * @param type - Alert type
73
+ * @returns Icon name or undefined
74
+ */
75
+ export function getAlertIcon(type: AlertType): string | undefined {
76
+ switch (type) {
77
+ case AlertType.SUCCESS:
78
+ return 'CheckCircle';
79
+ case AlertType.ERROR:
80
+ return 'AlertCircle';
81
+ case AlertType.WARNING:
82
+ return 'AlertTriangle';
83
+ case AlertType.INFO:
84
+ return 'Info';
85
+ default:
86
+ return undefined;
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Gets action button style
92
+ *
93
+ * @param actionStyle - Button style type
94
+ * @param tokens - Design tokens
95
+ * @returns Style object
96
+ */
97
+ export function getActionButtonStyle(
98
+ actionStyle: AlertAction['style'],
99
+ tokens: DesignTokens
100
+ ): StyleProp<ViewStyle> {
101
+ if (actionStyle === 'secondary') {
102
+ return {
103
+ backgroundColor: undefined,
104
+ borderWidth: 1,
105
+ borderColor: tokens.colors.textInverse,
106
+ };
107
+ }
108
+
109
+ const colors = {
110
+ primary: tokens.colors.backgroundPrimary,
111
+ destructive: tokens.colors.error,
112
+ };
113
+ return { backgroundColor: colors[actionStyle as keyof typeof colors] || tokens.colors.backgroundSecondary };
114
+ }
115
+
116
+ /**
117
+ * Gets action text color
118
+ *
119
+ * @param actionStyle - Button style type
120
+ * @param tokens - Design tokens
121
+ * @returns Text color string
122
+ */
123
+ export function getActionTextColor(
124
+ actionStyle: AlertAction['style'],
125
+ tokens: DesignTokens
126
+ ): string {
127
+ return actionStyle === 'primary' ? tokens.colors.textPrimary : tokens.colors.textInverse;
128
+ }
129
+
130
+ /**
131
+ * Default alert duration in milliseconds
132
+ */
133
+ export const DEFAULT_ALERT_DURATION = 3000;
@@ -3,7 +3,7 @@
3
3
  * Convenience hook that combines all calendar stores
4
4
  */
5
5
 
6
- import { useMemo } from 'react';
6
+ import { useMemo, useCallback } from 'react';
7
7
  import { useCalendarEvents } from '../stores/useCalendarEvents';
8
8
  import { useCalendarNavigation } from '../stores/useCalendarNavigation';
9
9
  import { useCalendarView } from '../stores/useCalendarView';
@@ -26,22 +26,22 @@ export const useCalendar = () => {
26
26
  const navigation = useCalendarNavigation();
27
27
  const view = useCalendarView();
28
28
 
29
- // Utility functions
30
- const getEventsForDate = (date: Date) => {
29
+ // Utility functions - memoized to prevent recreating on every render
30
+ const getEventsForDate = useCallback((date: Date) => {
31
31
  return events.events.filter(event => {
32
32
  const eventDate = new Date(event.date);
33
33
  return eventDate.toDateString() === date.toDateString();
34
34
  });
35
- };
35
+ }, [events.events]);
36
36
 
37
- const getEventsForMonth = (year: number, month: number) => {
37
+ const getEventsForMonth = useCallback((year: number, month: number) => {
38
38
  return events.events.filter(event => {
39
39
  const eventDate = new Date(event.date);
40
40
  return eventDate.getFullYear() === year && eventDate.getMonth() === month;
41
41
  });
42
- };
42
+ }, [events.events]);
43
43
 
44
- return {
44
+ return useMemo(() => ({
45
45
  // Events state and actions
46
46
  events: events.events,
47
47
  isLoading: events.isLoading,
@@ -70,7 +70,29 @@ export const useCalendar = () => {
70
70
  // Utility functions
71
71
  getEventsForDate,
72
72
  getEventsForMonth,
73
- };
73
+ }), [
74
+ events.events,
75
+ events.isLoading,
76
+ events.error,
77
+ events.loadEvents,
78
+ events.addEvent,
79
+ events.updateEvent,
80
+ events.deleteEvent,
81
+ events.completeEvent,
82
+ events.uncompleteEvent,
83
+ events.clearError,
84
+ events.clearAllEvents,
85
+ navigation.selectedDate,
86
+ navigation.currentMonth,
87
+ navigation.setSelectedDate,
88
+ navigation.goToToday,
89
+ navigation.navigateMonth,
90
+ navigation.setCurrentMonth,
91
+ view.viewMode,
92
+ view.setViewMode,
93
+ getEventsForDate,
94
+ getEventsForMonth,
95
+ ]);
74
96
  };
75
97
 
76
98
  /**
@@ -85,6 +107,40 @@ export const useCalendarStore = () => {
85
107
  return CalendarService.getMonthDays(year, month, calendar.events);
86
108
  }, [calendar.currentMonth, calendar.events]);
87
109
 
110
+ const actions = useMemo(() => ({
111
+ loadEvents: calendar.loadEvents,
112
+ addEvent: calendar.addEvent,
113
+ updateEvent: calendar.updateEvent,
114
+ deleteEvent: calendar.deleteEvent,
115
+ completeEvent: calendar.completeEvent,
116
+ uncompleteEvent: calendar.uncompleteEvent,
117
+ setSelectedDate: calendar.setSelectedDate,
118
+ goToToday: calendar.goToToday,
119
+ navigateMonth: calendar.navigateMonth,
120
+ setCurrentMonth: calendar.setCurrentMonth,
121
+ setViewMode: calendar.setViewMode,
122
+ getEventsForDate: calendar.getEventsForDate,
123
+ getEventsForMonth: calendar.getEventsForMonth,
124
+ clearError: calendar.clearError,
125
+ clearAllEvents: calendar.clearAllEvents,
126
+ }), [
127
+ calendar.loadEvents,
128
+ calendar.addEvent,
129
+ calendar.updateEvent,
130
+ calendar.deleteEvent,
131
+ calendar.completeEvent,
132
+ calendar.uncompleteEvent,
133
+ calendar.setSelectedDate,
134
+ calendar.goToToday,
135
+ calendar.navigateMonth,
136
+ calendar.setCurrentMonth,
137
+ calendar.setViewMode,
138
+ calendar.getEventsForDate,
139
+ calendar.getEventsForMonth,
140
+ calendar.clearError,
141
+ calendar.clearAllEvents,
142
+ ]);
143
+
88
144
  return {
89
145
  events: calendar.events,
90
146
  selectedDate: calendar.selectedDate,
@@ -92,23 +148,7 @@ export const useCalendarStore = () => {
92
148
  viewMode: calendar.viewMode,
93
149
  isLoading: calendar.isLoading,
94
150
  error: calendar.error,
95
- actions: {
96
- loadEvents: calendar.loadEvents,
97
- addEvent: calendar.addEvent,
98
- updateEvent: calendar.updateEvent,
99
- deleteEvent: calendar.deleteEvent,
100
- completeEvent: calendar.completeEvent,
101
- uncompleteEvent: calendar.uncompleteEvent,
102
- setSelectedDate: calendar.setSelectedDate,
103
- goToToday: calendar.goToToday,
104
- navigateMonth: calendar.navigateMonth,
105
- setCurrentMonth: calendar.setCurrentMonth,
106
- setViewMode: calendar.setViewMode,
107
- getEventsForDate: calendar.getEventsForDate,
108
- getEventsForMonth: calendar.getEventsForMonth,
109
- clearError: calendar.clearError,
110
- clearAllEvents: calendar.clearAllEvents,
111
- },
151
+ actions,
112
152
  days,
113
153
  };
114
154
  };