@umituz/react-native-notifications 1.1.6 → 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 +96 -159
  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 -132
  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,182 +1,119 @@
1
- import React, { useState, useEffect, useCallback } from 'react';
2
- import { View, Text, Pressable, StyleSheet, Switch, ViewStyle } from 'react-native';
3
- import { Feather } from '@expo/vector-icons';
4
- import { useNavigation } from '@react-navigation/native';
1
+ /**
2
+ * NotificationsSection Component
3
+ * Settings section for notifications with toggle
4
+ */
5
+
6
+ import React, { useState, useEffect, useCallback, useMemo } from 'react';
7
+ import type { StyleProp, ViewStyle } from 'react-native';
8
+ import { View, TouchableOpacity, StyleSheet, Switch } from 'react-native';
9
+ import { AtomicText, AtomicIcon } from '@umituz/react-native-design-system';
5
10
  import { useAppDesignTokens } from '@umituz/react-native-design-system-theme';
6
11
  import { notificationService } from '../../infrastructure/services/NotificationService';
7
12
 
8
13
  export interface NotificationsSectionConfig {
9
- initialValue?: boolean;
10
- onToggleChange?: (value: boolean) => void;
11
- route?: string;
12
- defaultRoute?: string;
13
- title?: string;
14
- description?: string;
15
- showToggle?: boolean;
14
+ initialValue?: boolean;
15
+ onToggleChange?: (value: boolean) => void;
16
+ route?: string;
17
+ title?: string;
18
+ description?: string;
19
+ showToggle?: boolean;
16
20
  }
17
21
 
18
22
  export interface NotificationsSectionProps {
19
- config?: NotificationsSectionConfig;
20
- containerStyle?: ViewStyle;
23
+ config?: NotificationsSectionConfig;
24
+ containerStyle?: StyleProp<ViewStyle>;
25
+ onNavigate?: (route: string) => void;
21
26
  }
22
27
 
23
28
  export const NotificationsSection: React.FC<NotificationsSectionProps> = ({
24
- config,
25
- containerStyle,
29
+ config,
30
+ containerStyle,
31
+ onNavigate,
26
32
  }) => {
27
- const navigation = useNavigation();
28
- const tokens = useAppDesignTokens();
29
- const colors = tokens.colors;
30
-
31
- const [notificationsEnabled, setNotificationsEnabled] = useState(
32
- config?.initialValue ?? true,
33
- );
34
-
35
- useEffect(() => {
36
- if (config?.initialValue !== undefined) {
37
- setNotificationsEnabled(config.initialValue);
38
- }
39
- }, [config?.initialValue]);
40
-
41
- const handleToggle = useCallback(async (value: boolean) => {
42
- if (!value) {
43
- // When turning ON (value is false -> true? Wait. onChange value is the NEW value)
44
- // If value is true (turning on)
45
- }
33
+ const tokens = useAppDesignTokens();
34
+ const styles = useMemo(() => createStyles(tokens), [tokens]);
46
35
 
47
- // Logic from settings:
48
- /*
49
- if (notificationService && !value) { // Wait, logic in settings was: if (!value) ...?
50
- // Actually typically we request permissions when enabling.
51
- // Settings code:
52
- // if (notificationService && !value) { ... } -> This implies when value is FALSE?
53
- // Ah, maybe the switch value logic was inverted or I misread.
54
- // Let's re-read settings code.
55
- }
56
- */
36
+ const [enabled, setEnabled] = useState(config?.initialValue ?? false);
57
37
 
58
- if (value) { // Enabling
59
- const hasPermissions = await notificationService.hasPermissions();
60
- if (!hasPermissions) {
61
- const granted = await notificationService.requestPermissions();
62
- if (!granted) {
63
- // Permission denied, maybe don't enable switch?
64
- // For now just allow toggle and let app handle it or sync with state
65
- }
66
- }
67
- }
38
+ useEffect(() => {
39
+ if (config?.initialValue !== undefined) {
40
+ setEnabled(config.initialValue);
41
+ }
42
+ }, [config?.initialValue]);
68
43
 
69
- setNotificationsEnabled(value);
70
- config?.onToggleChange?.(value);
71
- }, [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]);
72
55
 
73
- const handlePress = useCallback(async () => {
74
- const hasPermissions = await notificationService.hasPermissions();
75
- if (!hasPermissions) {
76
- await notificationService.requestPermissions();
77
- }
78
- navigation.navigate((config?.route || config?.defaultRoute || 'Notifications') as never);
79
- }, [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]);
80
65
 
81
- const title = config?.title || 'Notifications';
82
- const description = config?.description || 'Manage notification preferences';
83
- 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;
84
69
 
85
- return (
86
- <View style={[styles.sectionContainer, { backgroundColor: colors.surface }, containerStyle]}>
87
- <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>
88
73
 
89
- <Pressable
90
- style={({ pressed }) => [
91
- styles.itemContainer,
92
- {
93
- backgroundColor: pressed && !showToggle ? `${colors.primary}08` : 'transparent',
94
- },
95
- ]}
96
- onPress={showToggle ? undefined : handlePress}
97
- disabled={showToggle}
98
- >
99
- <View style={styles.content}>
100
- <View
101
- style={[
102
- styles.iconContainer,
103
- { backgroundColor: `${colors.primary}15` },
104
- ]}
105
- >
106
- <Feather name="bell" size={24} color={colors.primary} />
107
- </View>
108
- <View style={styles.textContainer}>
109
- <Text style={[styles.title, { color: colors.textPrimary }]}>{title}</Text>
110
- {!showToggle && (
111
- <Text style={[styles.description, { color: colors.textSecondary }]}>
112
- {description}
113
- </Text>
114
- )}
115
- </View>
116
-
117
- {showToggle ? (
118
- <Switch
119
- value={notificationsEnabled}
120
- onValueChange={handleToggle}
121
- trackColor={{
122
- false: `${colors.textSecondary}30`,
123
- true: colors.primary,
124
- }}
125
- thumbColor={"#FFFFFF"}
126
- ios_backgroundColor={`${colors.textSecondary}30`}
127
- />
128
- ) : (
129
- <Feather name="chevron-right" size={20} color={colors.textSecondary} />
130
- )}
131
- </View>
132
- </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>}
133
86
  </View>
134
- );
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
+ );
135
101
  };
136
102
 
137
- const styles = StyleSheet.create({
138
- sectionContainer: {
139
- marginBottom: 16,
140
- borderRadius: 12,
141
- overflow: 'hidden',
142
- },
143
- sectionTitle: {
144
- fontSize: 18,
145
- fontWeight: '600',
146
- paddingHorizontal: 16,
147
- paddingTop: 16,
148
- paddingBottom: 8,
149
- },
150
- itemContainer: {
151
- flexDirection: 'row',
152
- alignItems: 'center',
153
- paddingHorizontal: 16,
154
- paddingVertical: 16,
155
- minHeight: 72,
156
- },
157
- content: {
158
- flex: 1,
159
- flexDirection: 'row',
160
- alignItems: 'center',
161
- },
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 },
162
108
  iconContainer: {
163
- width: 48,
164
- height: 48,
165
- borderRadius: 12,
166
- justifyContent: 'center',
167
- alignItems: 'center',
168
- marginRight: 16,
169
- },
170
- textContainer: {
171
- flex: 1,
172
- marginRight: 8,
173
- },
174
- title: {
175
- fontSize: 16,
176
- fontWeight: '500',
177
- marginBottom: 4,
178
- },
179
- description: {
180
- 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,
181
116
  },
182
- });
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
+ });