@umituz/react-native-notifications 1.1.7 → 1.3.0

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 (118) hide show
  1. package/package.json +18 -40
  2. package/src/index.ts +84 -13
  3. package/src/infrastructure/config/reminderPresets.ts +120 -0
  4. package/src/infrastructure/hooks/useQuietHoursActions.ts +52 -0
  5. package/src/infrastructure/hooks/useReminderActions.ts +147 -0
  6. package/src/infrastructure/services/types.ts +137 -44
  7. package/src/infrastructure/storage/RemindersStore.ts +150 -0
  8. package/src/presentation/components/FormButton.tsx +66 -0
  9. package/src/presentation/components/FrequencySelector.tsx +72 -0
  10. package/src/presentation/components/NotificationsSection.tsx +95 -160
  11. package/src/presentation/components/QuietHoursCard.tsx +105 -0
  12. package/src/presentation/components/ReminderForm.tsx +165 -0
  13. package/src/presentation/components/ReminderItem.tsx +124 -0
  14. package/src/presentation/components/TimePresetSelector.tsx +100 -0
  15. package/src/presentation/components/WeekdaySelector.tsx +61 -0
  16. package/src/presentation/screens/NotificationSettingsScreen.tsx +210 -0
  17. package/src/presentation/screens/ReminderListScreen.tsx +138 -0
  18. package/src/types/global.d.ts +11 -8
  19. package/lib/index.d.ts +0 -16
  20. package/lib/index.d.ts.map +0 -1
  21. package/lib/index.js +0 -23
  22. package/lib/index.js.map +0 -1
  23. package/lib/infrastructure/config/notificationsConfig.d.ts +0 -20
  24. package/lib/infrastructure/config/notificationsConfig.d.ts.map +0 -1
  25. package/lib/infrastructure/config/notificationsConfig.js +0 -81
  26. package/lib/infrastructure/config/notificationsConfig.js.map +0 -1
  27. package/lib/infrastructure/hooks/actions/useNotificationActions.d.ts +0 -7
  28. package/lib/infrastructure/hooks/actions/useNotificationActions.d.ts.map +0 -1
  29. package/lib/infrastructure/hooks/actions/useNotificationActions.js +0 -75
  30. package/lib/infrastructure/hooks/actions/useNotificationActions.js.map +0 -1
  31. package/lib/infrastructure/hooks/actions/useNotificationManagementActions.d.ts +0 -8
  32. package/lib/infrastructure/hooks/actions/useNotificationManagementActions.d.ts.map +0 -1
  33. package/lib/infrastructure/hooks/actions/useNotificationManagementActions.js +0 -78
  34. package/lib/infrastructure/hooks/actions/useNotificationManagementActions.js.map +0 -1
  35. package/lib/infrastructure/hooks/state/useNotificationsState.d.ts +0 -12
  36. package/lib/infrastructure/hooks/state/useNotificationsState.d.ts.map +0 -1
  37. package/lib/infrastructure/hooks/state/useNotificationsState.js +0 -30
  38. package/lib/infrastructure/hooks/state/useNotificationsState.js.map +0 -1
  39. package/lib/infrastructure/hooks/types.d.ts +0 -87
  40. package/lib/infrastructure/hooks/types.d.ts.map +0 -1
  41. package/lib/infrastructure/hooks/types.js +0 -8
  42. package/lib/infrastructure/hooks/types.js.map +0 -1
  43. package/lib/infrastructure/hooks/useNotificationSettings.d.ts +0 -10
  44. package/lib/infrastructure/hooks/useNotificationSettings.d.ts.map +0 -1
  45. package/lib/infrastructure/hooks/useNotificationSettings.js +0 -43
  46. package/lib/infrastructure/hooks/useNotificationSettings.js.map +0 -1
  47. package/lib/infrastructure/hooks/useNotifications.d.ts +0 -24
  48. package/lib/infrastructure/hooks/useNotifications.d.ts.map +0 -1
  49. package/lib/infrastructure/hooks/useNotifications.js +0 -72
  50. package/lib/infrastructure/hooks/useNotifications.js.map +0 -1
  51. package/lib/infrastructure/hooks/utils/useNotificationRefresh.d.ts +0 -8
  52. package/lib/infrastructure/hooks/utils/useNotificationRefresh.d.ts.map +0 -1
  53. package/lib/infrastructure/hooks/utils/useNotificationRefresh.js +0 -99
  54. package/lib/infrastructure/hooks/utils/useNotificationRefresh.js.map +0 -1
  55. package/lib/infrastructure/services/NotificationBadgeManager.d.ts +0 -5
  56. package/lib/infrastructure/services/NotificationBadgeManager.d.ts.map +0 -1
  57. package/lib/infrastructure/services/NotificationBadgeManager.js +0 -29
  58. package/lib/infrastructure/services/NotificationBadgeManager.js.map +0 -1
  59. package/lib/infrastructure/services/NotificationManager.d.ts +0 -59
  60. package/lib/infrastructure/services/NotificationManager.d.ts.map +0 -1
  61. package/lib/infrastructure/services/NotificationManager.js +0 -118
  62. package/lib/infrastructure/services/NotificationManager.js.map +0 -1
  63. package/lib/infrastructure/services/NotificationPermissions.d.ts +0 -6
  64. package/lib/infrastructure/services/NotificationPermissions.d.ts.map +0 -1
  65. package/lib/infrastructure/services/NotificationPermissions.js +0 -75
  66. package/lib/infrastructure/services/NotificationPermissions.js.map +0 -1
  67. package/lib/infrastructure/services/NotificationScheduler.d.ts +0 -8
  68. package/lib/infrastructure/services/NotificationScheduler.d.ts.map +0 -1
  69. package/lib/infrastructure/services/NotificationScheduler.js +0 -72
  70. package/lib/infrastructure/services/NotificationScheduler.js.map +0 -1
  71. package/lib/infrastructure/services/NotificationService.d.ts +0 -30
  72. package/lib/infrastructure/services/NotificationService.d.ts.map +0 -1
  73. package/lib/infrastructure/services/NotificationService.js +0 -41
  74. package/lib/infrastructure/services/NotificationService.js.map +0 -1
  75. package/lib/infrastructure/services/channels/ChannelManager.d.ts +0 -18
  76. package/lib/infrastructure/services/channels/ChannelManager.d.ts.map +0 -1
  77. package/lib/infrastructure/services/channels/ChannelManager.js +0 -87
  78. package/lib/infrastructure/services/channels/ChannelManager.js.map +0 -1
  79. package/lib/infrastructure/services/delivery/NotificationDelivery.d.ts +0 -10
  80. package/lib/infrastructure/services/delivery/NotificationDelivery.d.ts.map +0 -1
  81. package/lib/infrastructure/services/delivery/NotificationDelivery.js +0 -71
  82. package/lib/infrastructure/services/delivery/NotificationDelivery.js.map +0 -1
  83. package/lib/infrastructure/services/preferences/PreferencesManager.d.ts +0 -18
  84. package/lib/infrastructure/services/preferences/PreferencesManager.d.ts.map +0 -1
  85. package/lib/infrastructure/services/preferences/PreferencesManager.js +0 -65
  86. package/lib/infrastructure/services/preferences/PreferencesManager.js.map +0 -1
  87. package/lib/infrastructure/services/types.d.ts +0 -89
  88. package/lib/infrastructure/services/types.d.ts.map +0 -1
  89. package/lib/infrastructure/services/types.js +0 -7
  90. package/lib/infrastructure/services/types.js.map +0 -1
  91. package/lib/infrastructure/storage/NotificationsStore.d.ts +0 -23
  92. package/lib/infrastructure/storage/NotificationsStore.d.ts.map +0 -1
  93. package/lib/infrastructure/storage/NotificationsStore.js +0 -26
  94. package/lib/infrastructure/storage/NotificationsStore.js.map +0 -1
  95. package/lib/infrastructure/utils/dev.d.ts +0 -5
  96. package/lib/infrastructure/utils/dev.d.ts.map +0 -1
  97. package/lib/infrastructure/utils/dev.js +0 -24
  98. package/lib/infrastructure/utils/dev.js.map +0 -1
  99. package/lib/presentation/components/NotificationsSection.d.ts +0 -17
  100. package/lib/presentation/components/NotificationsSection.d.ts.map +0 -1
  101. package/lib/presentation/components/NotificationsSection.js +0 -133
  102. package/lib/presentation/components/NotificationsSection.js.map +0 -1
  103. package/lib/presentation/screens/NotificationsScreen.d.ts +0 -20
  104. package/lib/presentation/screens/NotificationsScreen.d.ts.map +0 -1
  105. package/lib/presentation/screens/NotificationsScreen.js +0 -74
  106. package/lib/presentation/screens/NotificationsScreen.js.map +0 -1
  107. package/src/__tests__/NotificationManager.test.ts +0 -215
  108. package/src/__tests__/useNotificationActions.test.ts +0 -189
  109. package/src/__tests__/useNotificationRefresh.test.ts +0 -213
  110. package/src/infrastructure/hooks/actions/useNotificationActions.ts +0 -131
  111. package/src/infrastructure/hooks/actions/useNotificationManagementActions.ts +0 -131
  112. package/src/infrastructure/hooks/state/useNotificationsState.ts +0 -46
  113. package/src/infrastructure/hooks/types.ts +0 -83
  114. package/src/infrastructure/hooks/useNotifications.ts +0 -96
  115. package/src/infrastructure/hooks/utils/useNotificationRefresh.ts +0 -131
  116. package/src/infrastructure/services/channels/ChannelManager.ts +0 -111
  117. package/src/infrastructure/services/delivery/NotificationDelivery.ts +0 -83
  118. package/src/infrastructure/services/preferences/PreferencesManager.ts +0 -77
@@ -1,184 +1,119 @@
1
- import React, { useState, useEffect, useCallback } from 'react';
1
+ /**
2
+ * NotificationsSection Component
3
+ * Settings section for notifications with toggle
4
+ */
5
+
6
+ import React, { useState, useEffect, useCallback, useMemo } from 'react';
2
7
  import type { StyleProp, ViewStyle } from 'react-native';
3
- import { View, Text, Pressable, StyleSheet, Switch } from 'react-native';
4
- import { Feather } from '@expo/vector-icons';
5
- // @ts-ignore - Optional peer dependency
6
- import { useNavigation } from '@react-navigation/native';
8
+ import { View, TouchableOpacity, StyleSheet, Switch } from 'react-native';
9
+ import { AtomicText, AtomicIcon } from '@umituz/react-native-design-system';
7
10
  import { useAppDesignTokens } from '@umituz/react-native-design-system-theme';
8
11
  import { notificationService } from '../../infrastructure/services/NotificationService';
9
12
 
10
13
  export interface NotificationsSectionConfig {
11
- initialValue?: boolean;
12
- onToggleChange?: (value: boolean) => void;
13
- route?: string;
14
- defaultRoute?: string;
15
- title?: string;
16
- description?: string;
17
- showToggle?: boolean;
14
+ initialValue?: boolean;
15
+ onToggleChange?: (value: boolean) => void;
16
+ route?: string;
17
+ title?: string;
18
+ description?: string;
19
+ showToggle?: boolean;
18
20
  }
19
21
 
20
22
  export interface NotificationsSectionProps {
21
- config?: NotificationsSectionConfig;
22
- containerStyle?: StyleProp<ViewStyle>;
23
+ config?: NotificationsSectionConfig;
24
+ containerStyle?: StyleProp<ViewStyle>;
25
+ onNavigate?: (route: string) => void;
23
26
  }
24
27
 
25
28
  export const NotificationsSection: React.FC<NotificationsSectionProps> = ({
26
- config,
27
- containerStyle,
29
+ config,
30
+ containerStyle,
31
+ onNavigate,
28
32
  }) => {
29
- const navigation = useNavigation();
30
- const tokens = useAppDesignTokens();
31
- const colors = tokens.colors;
32
-
33
- const [notificationsEnabled, setNotificationsEnabled] = useState(
34
- config?.initialValue ?? true,
35
- );
36
-
37
- useEffect(() => {
38
- if (config?.initialValue !== undefined) {
39
- setNotificationsEnabled(config.initialValue);
40
- }
41
- }, [config?.initialValue]);
42
-
43
- const handleToggle = useCallback(async (value: boolean) => {
44
- if (!value) {
45
- // When turning ON (value is false -> true? Wait. onChange value is the NEW value)
46
- // If value is true (turning on)
47
- }
33
+ const tokens = useAppDesignTokens();
34
+ const styles = useMemo(() => createStyles(tokens), [tokens]);
48
35
 
49
- // Logic from settings:
50
- /*
51
- if (notificationService && !value) { // Wait, logic in settings was: if (!value) ...?
52
- // Actually typically we request permissions when enabling.
53
- // Settings code:
54
- // if (notificationService && !value) { ... } -> This implies when value is FALSE?
55
- // Ah, maybe the switch value logic was inverted or I misread.
56
- // Let's re-read settings code.
57
- }
58
- */
36
+ const [enabled, setEnabled] = useState(config?.initialValue ?? false);
59
37
 
60
- if (value) { // Enabling
61
- const hasPermissions = await notificationService.hasPermissions();
62
- if (!hasPermissions) {
63
- const granted = await notificationService.requestPermissions();
64
- if (!granted) {
65
- // Permission denied, maybe don't enable switch?
66
- // For now just allow toggle and let app handle it or sync with state
67
- }
68
- }
69
- }
38
+ useEffect(() => {
39
+ if (config?.initialValue !== undefined) {
40
+ setEnabled(config.initialValue);
41
+ }
42
+ }, [config?.initialValue]);
70
43
 
71
- setNotificationsEnabled(value);
72
- config?.onToggleChange?.(value);
73
- }, [config]);
44
+ const handleToggle = useCallback(async (value: boolean) => {
45
+ if (value) {
46
+ const hasPermissions = await notificationService.hasPermissions();
47
+ if (!hasPermissions) {
48
+ const granted = await notificationService.requestPermissions();
49
+ if (!granted) return;
50
+ }
51
+ }
52
+ setEnabled(value);
53
+ config?.onToggleChange?.(value);
54
+ }, [config]);
74
55
 
75
- const handlePress = useCallback(async () => {
76
- const hasPermissions = await notificationService.hasPermissions();
77
- if (!hasPermissions) {
78
- await notificationService.requestPermissions();
79
- }
80
- navigation.navigate((config?.route || config?.defaultRoute || 'Notifications') as never);
81
- }, [navigation, config?.route, config?.defaultRoute]);
56
+ const handlePress = useCallback(async () => {
57
+ const hasPermissions = await notificationService.hasPermissions();
58
+ if (!hasPermissions) {
59
+ await notificationService.requestPermissions();
60
+ }
61
+ if (config?.route && onNavigate) {
62
+ onNavigate(config.route);
63
+ }
64
+ }, [config?.route, onNavigate]);
82
65
 
83
- const title = config?.title || 'Notifications';
84
- const description = config?.description || 'Manage notification preferences';
85
- const showToggle = config?.showToggle ?? true;
66
+ const title = config?.title || 'Notifications';
67
+ const description = config?.description || 'Manage notification preferences';
68
+ const showToggle = config?.showToggle ?? true;
86
69
 
87
- return (
88
- <View style={[styles.sectionContainer, { backgroundColor: colors.surface }, containerStyle]}>
89
- <Text style={[styles.sectionTitle, { color: colors.textPrimary }]}>General</Text>
70
+ return (
71
+ <View style={[styles.container, containerStyle]}>
72
+ <AtomicText type="bodyLarge" style={styles.sectionTitle}>General</AtomicText>
90
73
 
91
- <Pressable
92
- style={({ pressed }) => [
93
- styles.itemContainer,
94
- {
95
- backgroundColor: pressed && !showToggle ? `${colors.primary}08` : 'transparent',
96
- },
97
- ]}
98
- onPress={showToggle ? undefined : handlePress}
99
- disabled={showToggle}
100
- >
101
- <View style={styles.content}>
102
- <View
103
- style={[
104
- styles.iconContainer,
105
- { backgroundColor: `${colors.primary}15` },
106
- ]}
107
- >
108
- <Feather name="bell" size={24} color={colors.primary} />
109
- </View>
110
- <View style={styles.textContainer}>
111
- <Text style={[styles.title, { color: colors.textPrimary }]}>{title}</Text>
112
- {!showToggle && (
113
- <Text style={[styles.description, { color: colors.textSecondary }]}>
114
- {description}
115
- </Text>
116
- )}
117
- </View>
118
-
119
- {showToggle ? (
120
- <Switch
121
- value={notificationsEnabled}
122
- onValueChange={handleToggle}
123
- trackColor={{
124
- false: `${colors.textSecondary}30`,
125
- true: colors.primary,
126
- }}
127
- thumbColor={"#FFFFFF"}
128
- ios_backgroundColor={`${colors.textSecondary}30`}
129
- />
130
- ) : (
131
- <Feather name="chevron-right" size={20} color={colors.textSecondary} />
132
- )}
133
- </View>
134
- </Pressable>
74
+ <TouchableOpacity
75
+ style={styles.itemContainer}
76
+ onPress={showToggle ? undefined : handlePress}
77
+ disabled={showToggle}
78
+ activeOpacity={0.7}
79
+ >
80
+ <View style={styles.iconContainer}>
81
+ <AtomicIcon name="bell" size="md" color="primary" />
82
+ </View>
83
+ <View style={styles.textContainer}>
84
+ <AtomicText type="bodyLarge">{title}</AtomicText>
85
+ {!showToggle && <AtomicText type="bodySmall" style={styles.description}>{description}</AtomicText>}
135
86
  </View>
136
- );
87
+
88
+ {showToggle ? (
89
+ <Switch
90
+ value={enabled}
91
+ onValueChange={handleToggle}
92
+ trackColor={{ false: tokens.colors.surfaceSecondary, true: tokens.colors.primary }}
93
+ thumbColor={tokens.colors.surface}
94
+ />
95
+ ) : (
96
+ <AtomicIcon name="chevron-right" size="md" color="textSecondary" />
97
+ )}
98
+ </TouchableOpacity>
99
+ </View>
100
+ );
137
101
  };
138
102
 
139
- const styles = StyleSheet.create({
140
- sectionContainer: {
141
- marginBottom: 16,
142
- borderRadius: 12,
143
- overflow: 'hidden',
144
- },
145
- sectionTitle: {
146
- fontSize: 18,
147
- fontWeight: '600',
148
- paddingHorizontal: 16,
149
- paddingTop: 16,
150
- paddingBottom: 8,
151
- },
152
- itemContainer: {
153
- flexDirection: 'row',
154
- alignItems: 'center',
155
- paddingHorizontal: 16,
156
- paddingVertical: 16,
157
- minHeight: 72,
158
- },
159
- content: {
160
- flex: 1,
161
- flexDirection: 'row',
162
- alignItems: 'center',
163
- },
103
+ const createStyles = (tokens: ReturnType<typeof useAppDesignTokens>) =>
104
+ StyleSheet.create({
105
+ container: { marginBottom: 16, borderRadius: 12, overflow: 'hidden', backgroundColor: tokens.colors.surface },
106
+ sectionTitle: { fontWeight: '600', paddingHorizontal: 16, paddingTop: 16, paddingBottom: 8 },
107
+ itemContainer: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 16, minHeight: 72 },
164
108
  iconContainer: {
165
- width: 48,
166
- height: 48,
167
- borderRadius: 12,
168
- justifyContent: 'center',
169
- alignItems: 'center',
170
- marginRight: 16,
171
- },
172
- textContainer: {
173
- flex: 1,
174
- marginRight: 8,
175
- },
176
- title: {
177
- fontSize: 16,
178
- fontWeight: '500',
179
- marginBottom: 4,
180
- },
181
- description: {
182
- fontSize: 14,
109
+ width: 48,
110
+ height: 48,
111
+ borderRadius: 12,
112
+ justifyContent: 'center',
113
+ alignItems: 'center',
114
+ marginRight: 16,
115
+ backgroundColor: tokens.colors.surfaceSecondary,
183
116
  },
184
- });
117
+ textContainer: { flex: 1, marginRight: 8 },
118
+ description: { color: tokens.colors.textSecondary, marginTop: 4 },
119
+ });
@@ -0,0 +1,105 @@
1
+ /**
2
+ * QuietHoursCard Component
3
+ * Displays and manages quiet hours settings
4
+ */
5
+
6
+ import React, { useMemo } from 'react';
7
+ import { View, TouchableOpacity, StyleSheet } from 'react-native';
8
+ import { AtomicText, AtomicIcon, AtomicSwitch, AtomicCard } from '@umituz/react-native-design-system';
9
+ import { useAppDesignTokens } from '@umituz/react-native-design-system-theme';
10
+ import type { QuietHoursConfig, QuietHoursTranslations } from '../../infrastructure/services/types';
11
+
12
+ export interface QuietHoursCardProps {
13
+ config: QuietHoursConfig;
14
+ translations: QuietHoursTranslations;
15
+ onToggle: (enabled: boolean) => void;
16
+ onStartTimePress: () => void;
17
+ onEndTimePress: () => void;
18
+ }
19
+
20
+ const formatTime = (hour: number, minute: number): string => {
21
+ return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
22
+ };
23
+
24
+ export const QuietHoursCard: React.FC<QuietHoursCardProps> = ({
25
+ config,
26
+ translations,
27
+ onToggle,
28
+ onStartTimePress,
29
+ onEndTimePress,
30
+ }) => {
31
+ const tokens = useAppDesignTokens();
32
+ const styles = useMemo(() => createStyles(tokens), [tokens]);
33
+
34
+ return (
35
+ <AtomicCard style={styles.card}>
36
+ <View style={styles.header}>
37
+ <View style={styles.iconContainer}>
38
+ <AtomicIcon name="moon" size="md" color="primary" />
39
+ </View>
40
+ <View style={styles.headerText}>
41
+ <AtomicText type="bodyLarge">{translations.title}</AtomicText>
42
+ <AtomicText type="bodySmall" style={styles.description}>{translations.description}</AtomicText>
43
+ </View>
44
+ <AtomicSwitch value={config.enabled} onValueChange={onToggle} />
45
+ </View>
46
+
47
+ {config.enabled && (
48
+ <View style={styles.timeContainer}>
49
+ <TouchableOpacity style={styles.timeButton} onPress={onStartTimePress} activeOpacity={0.7}>
50
+ <AtomicText type="bodySmall" style={styles.timeLabel}>{translations.startTimeLabel}</AtomicText>
51
+ <AtomicText type="bodyLarge" style={styles.timeValue}>
52
+ {formatTime(config.startHour, config.startMinute)}
53
+ </AtomicText>
54
+ </TouchableOpacity>
55
+
56
+ <View style={styles.timeSeparator}>
57
+ <AtomicIcon name="arrow-right" size="sm" color="textSecondary" />
58
+ </View>
59
+
60
+ <TouchableOpacity style={styles.timeButton} onPress={onEndTimePress} activeOpacity={0.7}>
61
+ <AtomicText type="bodySmall" style={styles.timeLabel}>{translations.endTimeLabel}</AtomicText>
62
+ <AtomicText type="bodyLarge" style={styles.timeValue}>
63
+ {formatTime(config.endHour, config.endMinute)}
64
+ </AtomicText>
65
+ </TouchableOpacity>
66
+ </View>
67
+ )}
68
+ </AtomicCard>
69
+ );
70
+ };
71
+
72
+ const createStyles = (tokens: ReturnType<typeof useAppDesignTokens>) =>
73
+ StyleSheet.create({
74
+ card: { padding: 16, backgroundColor: tokens.colors.surface },
75
+ header: { flexDirection: 'row', alignItems: 'center' },
76
+ iconContainer: {
77
+ width: 48,
78
+ height: 48,
79
+ borderRadius: 24,
80
+ backgroundColor: tokens.colors.surfaceSecondary,
81
+ justifyContent: 'center',
82
+ alignItems: 'center',
83
+ marginRight: 12,
84
+ },
85
+ headerText: { flex: 1, marginRight: 12 },
86
+ description: { color: tokens.colors.textSecondary, marginTop: 2 },
87
+ timeContainer: {
88
+ flexDirection: 'row',
89
+ alignItems: 'center',
90
+ marginTop: 16,
91
+ paddingTop: 16,
92
+ borderTopWidth: 1,
93
+ borderTopColor: tokens.colors.surfaceSecondary,
94
+ },
95
+ timeButton: {
96
+ flex: 1,
97
+ backgroundColor: tokens.colors.surfaceSecondary,
98
+ borderRadius: 8,
99
+ padding: 12,
100
+ alignItems: 'center',
101
+ },
102
+ timeLabel: { color: tokens.colors.textSecondary, marginBottom: 4 },
103
+ timeValue: { color: tokens.colors.textPrimary, fontWeight: '600' },
104
+ timeSeparator: { paddingHorizontal: 12 },
105
+ });
@@ -0,0 +1,165 @@
1
+ /**
2
+ * ReminderForm Component
3
+ * Form for creating and editing reminders
4
+ */
5
+
6
+ import React, { useState, useMemo, useCallback } from 'react';
7
+ import { View, TextInput, StyleSheet, ScrollView } from 'react-native';
8
+ import { AtomicText } from '@umituz/react-native-design-system';
9
+ import { useAppDesignTokens } from '@umituz/react-native-design-system-theme';
10
+ import { TimePresetSelector } from './TimePresetSelector';
11
+ import { FrequencySelector } from './FrequencySelector';
12
+ import { WeekdaySelector } from './WeekdaySelector';
13
+ import { FormButton } from './FormButton';
14
+ import { DEFAULT_TIME_PRESETS, FREQUENCY_OPTIONS } from '../../infrastructure/config/reminderPresets';
15
+ import type { Reminder, ReminderFrequency, CreateReminderInput, TimePreset } from '../../infrastructure/services/types';
16
+
17
+ export interface ReminderFormTranslations {
18
+ titleLabel: string;
19
+ titlePlaceholder: string;
20
+ bodyLabel: string;
21
+ bodyPlaceholder: string;
22
+ timeLabel: string;
23
+ frequencyLabel: string;
24
+ weekdayLabel: string;
25
+ saveButton: string;
26
+ cancelButton: string;
27
+ customTimeLabel: string;
28
+ getPresetLabel: (key: string) => string;
29
+ getFrequencyLabel: (key: string) => string;
30
+ getWeekdayLabel: (key: string) => string;
31
+ }
32
+
33
+ export interface ReminderFormProps {
34
+ initialData?: Reminder;
35
+ translations: ReminderFormTranslations;
36
+ onSave: (data: CreateReminderInput) => void;
37
+ onCancel: () => void;
38
+ timePresets?: TimePreset[];
39
+ }
40
+
41
+ export const ReminderForm: React.FC<ReminderFormProps> = ({
42
+ initialData,
43
+ translations,
44
+ onSave,
45
+ onCancel,
46
+ timePresets = DEFAULT_TIME_PRESETS,
47
+ }) => {
48
+ const tokens = useAppDesignTokens();
49
+ const styles = useMemo(() => createStyles(tokens), [tokens]);
50
+
51
+ const [title, setTitle] = useState(initialData?.title || '');
52
+ const [body, setBody] = useState(initialData?.body || '');
53
+ const [frequency, setFrequency] = useState<ReminderFrequency>(initialData?.frequency || 'daily');
54
+ const [selectedPresetId, setSelectedPresetId] = useState<string | undefined>(initialData?.timePresetId);
55
+ const [hour, setHour] = useState(initialData?.hour ?? 9);
56
+ const [minute, setMinute] = useState(initialData?.minute ?? 0);
57
+ const [weekday, setWeekday] = useState(initialData?.weekday ?? 2);
58
+ const [isCustomTime, setIsCustomTime] = useState(!initialData?.timePresetId);
59
+
60
+ const handlePresetSelect = useCallback((preset: TimePreset) => {
61
+ setSelectedPresetId(preset.id);
62
+ setHour(preset.hour);
63
+ setMinute(preset.minute);
64
+ setIsCustomTime(false);
65
+ }, []);
66
+
67
+ const handleCustomSelect = useCallback(() => {
68
+ setSelectedPresetId(undefined);
69
+ setIsCustomTime(true);
70
+ }, []);
71
+
72
+ const handleSave = useCallback(() => {
73
+ if (!title.trim()) return;
74
+ onSave({
75
+ title: title.trim(),
76
+ body: body.trim(),
77
+ frequency,
78
+ timePresetId: isCustomTime ? undefined : selectedPresetId,
79
+ hour,
80
+ minute,
81
+ weekday: frequency === 'weekly' ? weekday : undefined,
82
+ dayOfMonth: frequency === 'monthly' ? 1 : undefined,
83
+ });
84
+ }, [title, body, frequency, selectedPresetId, hour, minute, weekday, isCustomTime, onSave]);
85
+
86
+ return (
87
+ <ScrollView style={styles.container} showsVerticalScrollIndicator={false}>
88
+ <View style={styles.section}>
89
+ <AtomicText type="bodyMedium" style={styles.label}>{translations.titleLabel}</AtomicText>
90
+ <TextInput
91
+ style={styles.input}
92
+ value={title}
93
+ onChangeText={setTitle}
94
+ placeholder={translations.titlePlaceholder}
95
+ placeholderTextColor={tokens.colors.textSecondary}
96
+ />
97
+ </View>
98
+
99
+ <View style={styles.section}>
100
+ <AtomicText type="bodyMedium" style={styles.label}>{translations.bodyLabel}</AtomicText>
101
+ <TextInput
102
+ style={[styles.input, styles.multilineInput]}
103
+ value={body}
104
+ onChangeText={setBody}
105
+ placeholder={translations.bodyPlaceholder}
106
+ placeholderTextColor={tokens.colors.textSecondary}
107
+ multiline
108
+ numberOfLines={3}
109
+ />
110
+ </View>
111
+
112
+ <View style={styles.section}>
113
+ <AtomicText type="bodyMedium" style={styles.label}>{translations.frequencyLabel}</AtomicText>
114
+ <FrequencySelector
115
+ options={FREQUENCY_OPTIONS}
116
+ selectedFrequency={frequency}
117
+ onSelect={setFrequency}
118
+ getLabel={translations.getFrequencyLabel}
119
+ />
120
+ </View>
121
+
122
+ <View style={styles.section}>
123
+ <AtomicText type="bodyMedium" style={styles.label}>{translations.timeLabel}</AtomicText>
124
+ <TimePresetSelector
125
+ presets={timePresets}
126
+ selectedPresetId={selectedPresetId}
127
+ customTime={isCustomTime ? { hour, minute } : undefined}
128
+ onSelectPreset={handlePresetSelect}
129
+ onSelectCustom={handleCustomSelect}
130
+ getPresetLabel={translations.getPresetLabel}
131
+ customLabel={translations.customTimeLabel}
132
+ isCustomSelected={isCustomTime}
133
+ />
134
+ </View>
135
+
136
+ {frequency === 'weekly' && (
137
+ <View style={styles.section}>
138
+ <AtomicText type="bodyMedium" style={styles.label}>{translations.weekdayLabel}</AtomicText>
139
+ <WeekdaySelector selectedWeekday={weekday} onSelect={setWeekday} getLabel={translations.getWeekdayLabel} />
140
+ </View>
141
+ )}
142
+
143
+ <View style={styles.buttonRow}>
144
+ <FormButton label={translations.cancelButton} onPress={onCancel} variant="secondary" />
145
+ <FormButton label={translations.saveButton} onPress={handleSave} disabled={!title.trim()} />
146
+ </View>
147
+ </ScrollView>
148
+ );
149
+ };
150
+
151
+ const createStyles = (tokens: ReturnType<typeof useAppDesignTokens>) =>
152
+ StyleSheet.create({
153
+ container: { flex: 1, padding: 16 },
154
+ section: { marginBottom: 20 },
155
+ label: { color: tokens.colors.textPrimary, marginBottom: 8 },
156
+ input: {
157
+ backgroundColor: tokens.colors.surfaceSecondary,
158
+ borderRadius: 8,
159
+ padding: 12,
160
+ fontSize: 16,
161
+ color: tokens.colors.textPrimary,
162
+ },
163
+ multilineInput: { minHeight: 80, textAlignVertical: 'top' },
164
+ buttonRow: { flexDirection: 'row', gap: 12, marginTop: 24 },
165
+ });
@@ -0,0 +1,124 @@
1
+ /**
2
+ * ReminderItem Component
3
+ * Displays a single reminder with toggle and actions
4
+ */
5
+
6
+ import React, { useMemo } from 'react';
7
+ import { View, TouchableOpacity, StyleSheet } from 'react-native';
8
+ import { AtomicText, AtomicIcon, AtomicSwitch } from '@umituz/react-native-design-system';
9
+ import { useAppDesignTokens } from '@umituz/react-native-design-system-theme';
10
+ import type { Reminder, ReminderFrequency } from '../../infrastructure/services/types';
11
+
12
+ export interface ReminderItemTranslations {
13
+ frequencyOnce: string;
14
+ frequencyDaily: string;
15
+ frequencyWeekly: string;
16
+ frequencyMonthly: string;
17
+ }
18
+
19
+ export interface ReminderItemProps {
20
+ reminder: Reminder;
21
+ translations: ReminderItemTranslations;
22
+ onToggle: (id: string) => void;
23
+ onEdit: (reminder: Reminder) => void;
24
+ onDelete: (id: string) => void;
25
+ }
26
+
27
+ const formatTime = (hour: number, minute: number): string => {
28
+ return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
29
+ };
30
+
31
+ const getFrequencyIcon = (frequency: ReminderFrequency): string => {
32
+ const icons: Record<ReminderFrequency, string> = {
33
+ once: 'calendar',
34
+ daily: 'repeat',
35
+ weekly: 'calendar-days',
36
+ monthly: 'calendar-range',
37
+ };
38
+ return icons[frequency] || 'bell';
39
+ };
40
+
41
+ export const ReminderItem: React.FC<ReminderItemProps> = ({
42
+ reminder,
43
+ translations,
44
+ onToggle,
45
+ onEdit,
46
+ onDelete,
47
+ }) => {
48
+ const tokens = useAppDesignTokens();
49
+ const styles = useMemo(() => createStyles(tokens), [tokens]);
50
+
51
+ const getFrequencyLabel = (frequency: ReminderFrequency): string => {
52
+ const labels: Record<ReminderFrequency, string> = {
53
+ once: translations.frequencyOnce,
54
+ daily: translations.frequencyDaily,
55
+ weekly: translations.frequencyWeekly,
56
+ monthly: translations.frequencyMonthly,
57
+ };
58
+ return labels[frequency] || '';
59
+ };
60
+
61
+ return (
62
+ <View style={[styles.container, !reminder.enabled ? styles.disabled : undefined]}>
63
+ <TouchableOpacity style={styles.content} onPress={() => onEdit(reminder)} activeOpacity={0.7}>
64
+ <View style={styles.iconContainer}>
65
+ <AtomicIcon
66
+ name={getFrequencyIcon(reminder.frequency)}
67
+ size="md"
68
+ color={reminder.enabled ? 'primary' : 'textSecondary'}
69
+ />
70
+ </View>
71
+ <View style={styles.textContainer}>
72
+ <AtomicText type="bodyLarge" style={!reminder.enabled ? styles.disabledText : undefined}>
73
+ {reminder.title}
74
+ </AtomicText>
75
+ <View style={styles.metaRow}>
76
+ <AtomicText type="bodySmall" style={styles.time}>
77
+ {formatTime(reminder.hour, reminder.minute)}
78
+ </AtomicText>
79
+ <AtomicText type="bodySmall" style={styles.frequency}>
80
+ {getFrequencyLabel(reminder.frequency)}
81
+ </AtomicText>
82
+ </View>
83
+ </View>
84
+ </TouchableOpacity>
85
+
86
+ <View style={styles.actions}>
87
+ <TouchableOpacity style={styles.deleteButton} onPress={() => onDelete(reminder.id)}>
88
+ <AtomicIcon name="trash-2" size="sm" color="error" />
89
+ </TouchableOpacity>
90
+ <AtomicSwitch value={reminder.enabled} onValueChange={() => onToggle(reminder.id)} />
91
+ </View>
92
+ </View>
93
+ );
94
+ };
95
+
96
+ const createStyles = (tokens: ReturnType<typeof useAppDesignTokens>) =>
97
+ StyleSheet.create({
98
+ container: {
99
+ flexDirection: 'row',
100
+ alignItems: 'center',
101
+ padding: 16,
102
+ backgroundColor: tokens.colors.surface,
103
+ borderRadius: 12,
104
+ marginBottom: 8,
105
+ },
106
+ disabled: { opacity: 0.6 },
107
+ content: { flex: 1, flexDirection: 'row', alignItems: 'center' },
108
+ iconContainer: {
109
+ width: 44,
110
+ height: 44,
111
+ borderRadius: 22,
112
+ backgroundColor: tokens.colors.surfaceSecondary,
113
+ justifyContent: 'center',
114
+ alignItems: 'center',
115
+ marginRight: 12,
116
+ },
117
+ textContainer: { flex: 1 },
118
+ disabledText: { color: tokens.colors.textSecondary },
119
+ metaRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginTop: 4 },
120
+ time: { color: tokens.colors.primary, fontWeight: '600' },
121
+ frequency: { color: tokens.colors.textSecondary },
122
+ actions: { flexDirection: 'row', alignItems: 'center', gap: 8 },
123
+ deleteButton: { padding: 4 },
124
+ });