@umituz/react-native-settings 4.21.11 → 4.21.12
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 +11 -2
- package/src/domains/gamification/components/GamificationScreenWrapper.tsx +42 -87
- package/src/domains/gamification/components/index.ts +1 -0
- package/src/domains/gamification/index.ts +3 -11
- package/src/index.ts +3 -0
- package/src/notifications/domains/quietHours/infrastructure/hooks/useQuietHoursActions.ts +52 -0
- package/src/notifications/domains/quietHours/presentation/components/QuietHoursCard.tsx +112 -0
- package/src/notifications/domains/reminders/infrastructure/config/reminderPresets.ts +120 -0
- package/src/notifications/domains/reminders/infrastructure/hooks/useReminderActions.ts +106 -0
- package/src/notifications/domains/reminders/infrastructure/storage/RemindersStore.ts +148 -0
- package/src/notifications/domains/reminders/presentation/components/FormButton.tsx +66 -0
- package/src/notifications/domains/reminders/presentation/components/FrequencySelector.tsx +72 -0
- package/src/notifications/domains/reminders/presentation/components/ReminderForm.tsx +169 -0
- package/src/notifications/domains/reminders/presentation/components/ReminderItem.tsx +130 -0
- package/src/notifications/domains/reminders/presentation/components/TimePresetSelector.tsx +100 -0
- package/src/notifications/domains/reminders/presentation/components/WeekdaySelector.tsx +61 -0
- package/src/notifications/domains/reminders/presentation/screens/ReminderListScreen.tsx +131 -0
- package/src/notifications/index.ts +139 -0
- package/src/notifications/infrastructure/config/notificationsConfig.ts +98 -0
- package/src/notifications/infrastructure/hooks/useNotificationSettings.ts +37 -0
- package/src/notifications/infrastructure/services/NotificationBadgeManager.ts +28 -0
- package/src/notifications/infrastructure/services/NotificationManager.ts +138 -0
- package/src/notifications/infrastructure/services/NotificationPermissions.ts +80 -0
- package/src/notifications/infrastructure/services/NotificationScheduler.ts +77 -0
- package/src/notifications/infrastructure/services/NotificationService.ts +50 -0
- package/src/notifications/infrastructure/services/types.ts +176 -0
- package/src/notifications/infrastructure/storage/NotificationsStore.ts +45 -0
- package/src/notifications/infrastructure/utils/dev.ts +25 -0
- package/src/notifications/infrastructure/utils/idGenerator.ts +14 -0
- package/src/notifications/infrastructure/utils/triggerBuilder.ts +45 -0
- package/src/notifications/presentation/components/NotificationsSection.tsx +84 -0
- package/src/notifications/presentation/components/RemindersNavRow.styles.ts +38 -0
- package/src/notifications/presentation/components/RemindersNavRow.tsx +51 -0
- package/src/notifications/presentation/components/SettingRow.tsx +86 -0
- package/src/notifications/presentation/hooks/useNotificationSettingsUI.ts +52 -0
- package/src/notifications/presentation/hooks/useTimePicker.ts +71 -0
- package/src/notifications/presentation/screens/NotificationSettingsScreen.styles.ts +30 -0
- package/src/notifications/presentation/screens/NotificationSettingsScreen.tsx +131 -0
- package/src/notifications/presentation/screens/NotificationsScreen.tsx +107 -0
- package/src/presentation/navigation/SettingsStackNavigator.tsx +21 -11
- package/src/presentation/screens/components/sections/FeatureSettingsSection.tsx +1 -1
- package/src/domains/gamification/README.md +0 -343
- package/src/domains/gamification/components/GamificationSettingsItem.tsx +0 -33
- package/src/domains/gamification/examples/gamification.config.example.ts +0 -70
- package/src/domains/gamification/examples/localization.example.json +0 -71
- package/src/domains/gamification/types/settings.ts +0 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-settings",
|
|
3
|
-
"version": "4.21.
|
|
3
|
+
"version": "4.21.12",
|
|
4
4
|
"description": "Complete settings hub for React Native apps - consolidated package with settings, about, legal, appearance, feedback, FAQs, rating, and gamification",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -38,7 +38,6 @@
|
|
|
38
38
|
"@umituz/react-native-auth": "latest",
|
|
39
39
|
"@umituz/react-native-design-system": "latest",
|
|
40
40
|
"@umituz/react-native-localization": "latest",
|
|
41
|
-
"@umituz/react-native-notifications": "latest",
|
|
42
41
|
"@umituz/react-native-storage": "latest",
|
|
43
42
|
"@umituz/react-native-tanstack": "latest",
|
|
44
43
|
"firebase": "^12.7.0"
|
|
@@ -48,6 +47,11 @@
|
|
|
48
47
|
"@react-navigation/native": ">=6.0.0",
|
|
49
48
|
"@react-navigation/stack": ">=6.0.0",
|
|
50
49
|
"@tanstack/react-query": ">=5.0.0",
|
|
50
|
+
"expo-notifications": ">=0.28.0",
|
|
51
|
+
"expo-device": ">=6.0.0",
|
|
52
|
+
"expo-haptics": ">=15.0.0",
|
|
53
|
+
"@react-native-community/datetimepicker": ">=8.0.0",
|
|
54
|
+
"@umituz/react-native-haptics": "latest",
|
|
51
55
|
"react": ">=19.0.0",
|
|
52
56
|
"react-native": ">=0.81.0",
|
|
53
57
|
"react-native-safe-area-context": ">=4.0.0"
|
|
@@ -58,6 +62,11 @@
|
|
|
58
62
|
"@react-navigation/stack": "^7.6.13",
|
|
59
63
|
"@tanstack/react-query": "^5.0.0",
|
|
60
64
|
"@types/react": "~19.1.10",
|
|
65
|
+
"expo-notifications": "~0.27.6",
|
|
66
|
+
"expo-device": "~7.0.0",
|
|
67
|
+
"expo-haptics": "~14.0.0",
|
|
68
|
+
"@react-native-community/datetimepicker": "^8.2.0",
|
|
69
|
+
"@umituz/react-native-haptics": "latest",
|
|
61
70
|
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
|
62
71
|
"@typescript-eslint/parser": "^7.18.0",
|
|
63
72
|
"eslint": "^8.57.0",
|
|
@@ -1,91 +1,46 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useGamification } from '../hooks/useGamification';
|
|
3
|
+
import { GamificationScreen } from './GamificationScreen';
|
|
4
|
+
import type { GamificationConfig } from '../types';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import { useGamification } from "../hooks/useGamification";
|
|
9
|
-
import { useLocalization } from "@umituz/react-native-localization";
|
|
10
|
-
import type { GamificationSettingsConfig } from "../types/settings";
|
|
11
|
-
|
|
12
|
-
export interface GamificationScreenWrapperProps {
|
|
13
|
-
config: GamificationSettingsConfig;
|
|
6
|
+
interface GamificationScreenWrapperProps {
|
|
7
|
+
config: GamificationConfig;
|
|
14
8
|
}
|
|
15
9
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
value: gamification.achievements.filter((a) => a.isUnlocked).length,
|
|
53
|
-
label: t("gamification.stats.achievementsUnlocked"),
|
|
54
|
-
},
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
// Level props
|
|
58
|
-
const levelProps = {
|
|
59
|
-
level: gamification.level.currentLevel,
|
|
60
|
-
points: gamification.level.currentPoints,
|
|
61
|
-
levelTitle: t("gamification.level.title", {
|
|
62
|
-
level: gamification.level.currentLevel,
|
|
63
|
-
}),
|
|
64
|
-
pointsToNext: gamification.level.pointsToNext,
|
|
65
|
-
progress: gamification.level.progress,
|
|
66
|
-
showPoints: true,
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
// Streak props
|
|
70
|
-
const streakProps = gamification.streak.current > 0
|
|
71
|
-
? {
|
|
72
|
-
current: gamification.streak.current,
|
|
73
|
-
longest: gamification.streak.longest,
|
|
74
|
-
currentLabel: t("gamification.streak.current"),
|
|
75
|
-
bestLabel: t("gamification.streak.best"),
|
|
76
|
-
daysLabel: t("gamification.streak.days"),
|
|
77
|
-
}
|
|
78
|
-
: undefined;
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
...config.screenProps,
|
|
82
|
-
levelProps,
|
|
83
|
-
stats,
|
|
84
|
-
achievements,
|
|
85
|
-
streakProps,
|
|
86
|
-
emptyAchievementsText: t("gamification.achievements.empty"),
|
|
87
|
-
};
|
|
88
|
-
}, [gamification, config.screenProps, t]);
|
|
89
|
-
|
|
90
|
-
return <BaseGamificationScreen {...screenProps} />;
|
|
10
|
+
export const GamificationScreenWrapper: React.FC<GamificationScreenWrapperProps> = ({ config }) => {
|
|
11
|
+
const {
|
|
12
|
+
points,
|
|
13
|
+
totalTasksCompleted,
|
|
14
|
+
level,
|
|
15
|
+
streak,
|
|
16
|
+
achievements,
|
|
17
|
+
} = useGamification(config);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<GamificationScreen
|
|
21
|
+
title={config.translations.title}
|
|
22
|
+
statsTitle={config.translations.statsTitle}
|
|
23
|
+
achievementsTitle={config.translations.achievementsTitle}
|
|
24
|
+
streakTitle={config.translations.streakTitle}
|
|
25
|
+
levelProps={{
|
|
26
|
+
level: level.currentLevel,
|
|
27
|
+
currentPoints: level.currentPoints,
|
|
28
|
+
pointsToNext: level.pointsToNext,
|
|
29
|
+
progress: level.progress,
|
|
30
|
+
levelTitle: config.translations.levelTitle,
|
|
31
|
+
}}
|
|
32
|
+
streakProps={{
|
|
33
|
+
streak: streak.current,
|
|
34
|
+
longestStreak: streak.longest,
|
|
35
|
+
title: config.translations.streakTitle,
|
|
36
|
+
}}
|
|
37
|
+
stats={{
|
|
38
|
+
points,
|
|
39
|
+
tasksCompleted: totalTasksCompleted,
|
|
40
|
+
streak: streak.current,
|
|
41
|
+
}}
|
|
42
|
+
achievements={achievements}
|
|
43
|
+
emptyAchievementsText={config.translations.emptyAchievements}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
91
46
|
};
|
|
@@ -11,3 +11,4 @@ export { StreakDisplay, type StreakDisplayProps } from "./StreakDisplay";
|
|
|
11
11
|
export { StatsCard, type StatsCardProps } from "./StatsCard";
|
|
12
12
|
export { AchievementItem, type AchievementItemProps } from "./AchievementItem";
|
|
13
13
|
export { GamificationScreen, type GamificationScreenProps } from "./GamificationScreen/index";
|
|
14
|
+
export { GamificationScreenWrapper } from "./GamificationScreenWrapper";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Gamification Domain
|
|
3
|
+
* Part of @umituz/react-native-settings
|
|
3
4
|
*
|
|
4
5
|
* Generic gamification system for React Native apps
|
|
5
6
|
* All text via props - NO hardcoded strings
|
|
@@ -19,12 +20,6 @@ export type {
|
|
|
19
20
|
GamificationStore,
|
|
20
21
|
} from "./types";
|
|
21
22
|
|
|
22
|
-
// Settings Integration Types
|
|
23
|
-
export type {
|
|
24
|
-
GamificationSettingsConfig,
|
|
25
|
-
GamificationMenuConfig,
|
|
26
|
-
} from "./types/settings";
|
|
27
|
-
|
|
28
23
|
// Store
|
|
29
24
|
export { useGamificationStore } from "./store/gamificationStore";
|
|
30
25
|
|
|
@@ -58,8 +53,5 @@ export {
|
|
|
58
53
|
type AchievementItemProps,
|
|
59
54
|
GamificationScreen,
|
|
60
55
|
type GamificationScreenProps,
|
|
56
|
+
GamificationScreenWrapper,
|
|
61
57
|
} from "./components";
|
|
62
|
-
|
|
63
|
-
// Settings Integration Components
|
|
64
|
-
export { GamificationScreenWrapper } from "./components/GamificationScreenWrapper";
|
|
65
|
-
export { GamificationSettingsItem } from "./components/GamificationSettingsItem";
|
package/src/index.ts
CHANGED
|
@@ -128,6 +128,9 @@ export * from "./domains/gamification";
|
|
|
128
128
|
// PRESENTATION LAYER - Config Creator Utilities
|
|
129
129
|
// =============================================================================
|
|
130
130
|
|
|
131
|
+
// Notifications Domain
|
|
132
|
+
export * from "./notifications";
|
|
133
|
+
|
|
131
134
|
export {
|
|
132
135
|
createAppearanceConfig,
|
|
133
136
|
createLanguageConfig,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useQuietHoursActions Hook
|
|
3
|
+
* Manages quiet hours configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback } from 'react';
|
|
7
|
+
import { usePreferencesStore, useQuietHours } from '../../../reminders/infrastructure/storage/RemindersStore';
|
|
8
|
+
import type { QuietHoursConfig } from '../../../../infrastructure/services/types';
|
|
9
|
+
|
|
10
|
+
export const useQuietHoursActions = () => {
|
|
11
|
+
const quietHours = useQuietHours();
|
|
12
|
+
const { updateQuietHours } = usePreferencesStore();
|
|
13
|
+
|
|
14
|
+
const setQuietHoursEnabled = useCallback(async (enabled: boolean): Promise<void> => {
|
|
15
|
+
await updateQuietHours({ ...quietHours, enabled });
|
|
16
|
+
}, [quietHours, updateQuietHours]);
|
|
17
|
+
|
|
18
|
+
const setStartTime = useCallback(async (hour: number, minute: number): Promise<void> => {
|
|
19
|
+
await updateQuietHours({ ...quietHours, startHour: hour, startMinute: minute });
|
|
20
|
+
}, [quietHours, updateQuietHours]);
|
|
21
|
+
|
|
22
|
+
const setEndTime = useCallback(async (hour: number, minute: number): Promise<void> => {
|
|
23
|
+
await updateQuietHours({ ...quietHours, endHour: hour, endMinute: minute });
|
|
24
|
+
}, [quietHours, updateQuietHours]);
|
|
25
|
+
|
|
26
|
+
const setQuietHours = useCallback(async (config: QuietHoursConfig): Promise<void> => {
|
|
27
|
+
await updateQuietHours(config);
|
|
28
|
+
}, [updateQuietHours]);
|
|
29
|
+
|
|
30
|
+
const isInQuietHours = useCallback((): boolean => {
|
|
31
|
+
if (!quietHours.enabled) return false;
|
|
32
|
+
|
|
33
|
+
const now = new Date();
|
|
34
|
+
const currentMinutes = now.getHours() * 60 + now.getMinutes();
|
|
35
|
+
const startMinutes = quietHours.startHour * 60 + quietHours.startMinute;
|
|
36
|
+
const endMinutes = quietHours.endHour * 60 + quietHours.endMinute;
|
|
37
|
+
|
|
38
|
+
if (startMinutes <= endMinutes) {
|
|
39
|
+
return currentMinutes >= startMinutes && currentMinutes < endMinutes;
|
|
40
|
+
}
|
|
41
|
+
return currentMinutes >= startMinutes || currentMinutes < endMinutes;
|
|
42
|
+
}, [quietHours]);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
quietHours,
|
|
46
|
+
setQuietHoursEnabled,
|
|
47
|
+
setStartTime,
|
|
48
|
+
setEndTime,
|
|
49
|
+
setQuietHours,
|
|
50
|
+
isInQuietHours,
|
|
51
|
+
};
|
|
52
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
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, AtomicCard } from '@umituz/react-native-design-system';
|
|
9
|
+
import { Switch } from 'react-native';
|
|
10
|
+
import { useAppDesignTokens } from '@umituz/react-native-design-system';
|
|
11
|
+
import { SettingRow } from '../../../../presentation/components/SettingRow';
|
|
12
|
+
import type { QuietHoursConfig, QuietHoursTranslations } from '../../../../infrastructure/services/types';
|
|
13
|
+
|
|
14
|
+
export interface QuietHoursCardProps {
|
|
15
|
+
config: QuietHoursConfig;
|
|
16
|
+
translations: QuietHoursTranslations;
|
|
17
|
+
onToggle: (enabled: boolean) => void;
|
|
18
|
+
onStartTimePress: () => void;
|
|
19
|
+
onEndTimePress: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const formatTime = (hour: number, minute: number): string => {
|
|
23
|
+
return `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const QuietHoursCard: React.FC<QuietHoursCardProps> = ({
|
|
27
|
+
config,
|
|
28
|
+
translations,
|
|
29
|
+
onToggle,
|
|
30
|
+
onStartTimePress,
|
|
31
|
+
onEndTimePress,
|
|
32
|
+
}) => {
|
|
33
|
+
const tokens = useAppDesignTokens();
|
|
34
|
+
const styles = useMemo(() => createStyles(tokens), [tokens]);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<AtomicCard style={styles.card}>
|
|
38
|
+
<View style={styles.header}>
|
|
39
|
+
<View style={styles.iconContainer}>
|
|
40
|
+
<AtomicIcon name="moon" size="md" color="primary" />
|
|
41
|
+
</View>
|
|
42
|
+
<View style={styles.headerText}>
|
|
43
|
+
<AtomicText type="bodyLarge">{translations.title}</AtomicText>
|
|
44
|
+
<AtomicText type="bodySmall" style={styles.description}>{translations.description}</AtomicText>
|
|
45
|
+
</View>
|
|
46
|
+
<Switch
|
|
47
|
+
value={config.enabled}
|
|
48
|
+
onValueChange={onToggle}
|
|
49
|
+
trackColor={{ false: tokens.colors.surfaceSecondary, true: tokens.colors.primary }}
|
|
50
|
+
thumbColor={tokens.colors.surface}
|
|
51
|
+
/>
|
|
52
|
+
</View>
|
|
53
|
+
|
|
54
|
+
{config.enabled && (
|
|
55
|
+
<View style={styles.timeContainer}>
|
|
56
|
+
<TouchableOpacity style={styles.timeButton} onPress={onStartTimePress} activeOpacity={0.7}>
|
|
57
|
+
<AtomicText type="bodySmall" style={styles.timeLabel}>{translations.startTimeLabel}</AtomicText>
|
|
58
|
+
<AtomicText type="bodyLarge" style={styles.timeValue}>
|
|
59
|
+
{formatTime(config.startHour, config.startMinute)}
|
|
60
|
+
</AtomicText>
|
|
61
|
+
</TouchableOpacity>
|
|
62
|
+
|
|
63
|
+
<View style={styles.timeSeparator}>
|
|
64
|
+
<AtomicIcon name="arrow-forward" size="sm" color="secondary" />
|
|
65
|
+
</View>
|
|
66
|
+
|
|
67
|
+
<TouchableOpacity style={styles.timeButton} onPress={onEndTimePress} activeOpacity={0.7}>
|
|
68
|
+
<AtomicText type="bodySmall" style={styles.timeLabel}>{translations.endTimeLabel}</AtomicText>
|
|
69
|
+
<AtomicText type="bodyLarge" style={styles.timeValue}>
|
|
70
|
+
{formatTime(config.endHour, config.endMinute)}
|
|
71
|
+
</AtomicText>
|
|
72
|
+
</TouchableOpacity>
|
|
73
|
+
</View>
|
|
74
|
+
)}
|
|
75
|
+
</AtomicCard>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const createStyles = (tokens: ReturnType<typeof useAppDesignTokens>) =>
|
|
80
|
+
StyleSheet.create({
|
|
81
|
+
card: { padding: 16, backgroundColor: tokens.colors.surface },
|
|
82
|
+
header: { flexDirection: 'row', alignItems: 'center' },
|
|
83
|
+
iconContainer: {
|
|
84
|
+
width: 48,
|
|
85
|
+
height: 48,
|
|
86
|
+
borderRadius: 24,
|
|
87
|
+
backgroundColor: tokens.colors.surfaceSecondary,
|
|
88
|
+
justifyContent: 'center',
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
marginRight: 12,
|
|
91
|
+
},
|
|
92
|
+
headerText: { flex: 1, marginRight: 12 },
|
|
93
|
+
description: { color: tokens.colors.textSecondary, marginTop: 2 },
|
|
94
|
+
timeContainer: {
|
|
95
|
+
flexDirection: 'row',
|
|
96
|
+
alignItems: 'center',
|
|
97
|
+
marginTop: 16,
|
|
98
|
+
paddingTop: 16,
|
|
99
|
+
borderTopWidth: 1,
|
|
100
|
+
borderTopColor: tokens.colors.surfaceSecondary,
|
|
101
|
+
},
|
|
102
|
+
timeButton: {
|
|
103
|
+
flex: 1,
|
|
104
|
+
backgroundColor: tokens.colors.surfaceSecondary,
|
|
105
|
+
borderRadius: 8,
|
|
106
|
+
padding: 12,
|
|
107
|
+
alignItems: 'center',
|
|
108
|
+
},
|
|
109
|
+
timeLabel: { color: tokens.colors.textSecondary, marginBottom: 4 },
|
|
110
|
+
timeValue: { color: tokens.colors.textPrimary, fontWeight: '600' },
|
|
111
|
+
timeSeparator: { paddingHorizontal: 12 },
|
|
112
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reminder Presets Configuration
|
|
3
|
+
* Default time presets and frequency options for reminders
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { TimePreset, ReminderFrequency } from '../../../../infrastructure/services/types';
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// DEFAULT TIME PRESETS
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_TIME_PRESETS: TimePreset[] = [
|
|
13
|
+
{
|
|
14
|
+
id: 'morning',
|
|
15
|
+
hour: 8,
|
|
16
|
+
minute: 0,
|
|
17
|
+
labelKey: 'notifications.presets.morning',
|
|
18
|
+
iconName: 'sunny',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'noon',
|
|
22
|
+
hour: 12,
|
|
23
|
+
minute: 0,
|
|
24
|
+
labelKey: 'notifications.presets.noon',
|
|
25
|
+
iconName: 'sunny',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'afternoon',
|
|
29
|
+
hour: 15,
|
|
30
|
+
minute: 0,
|
|
31
|
+
labelKey: 'notifications.presets.afternoon',
|
|
32
|
+
iconName: 'partly-sunny',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'evening',
|
|
36
|
+
hour: 18,
|
|
37
|
+
minute: 0,
|
|
38
|
+
labelKey: 'notifications.presets.evening',
|
|
39
|
+
iconName: 'partly-sunny',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'night',
|
|
43
|
+
hour: 21,
|
|
44
|
+
minute: 0,
|
|
45
|
+
labelKey: 'notifications.presets.night',
|
|
46
|
+
iconName: 'moon',
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// FREQUENCY OPTIONS
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
export interface FrequencyOption {
|
|
55
|
+
id: ReminderFrequency;
|
|
56
|
+
labelKey: string;
|
|
57
|
+
iconName: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const FREQUENCY_OPTIONS: FrequencyOption[] = [
|
|
61
|
+
{
|
|
62
|
+
id: 'once',
|
|
63
|
+
labelKey: 'notifications.frequency.once',
|
|
64
|
+
iconName: 'calendar',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'daily',
|
|
68
|
+
labelKey: 'notifications.frequency.daily',
|
|
69
|
+
iconName: 'repeat',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'weekly',
|
|
73
|
+
labelKey: 'notifications.frequency.weekly',
|
|
74
|
+
iconName: 'calendar',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: 'monthly',
|
|
78
|
+
labelKey: 'notifications.frequency.monthly',
|
|
79
|
+
iconName: 'calendar',
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// WEEKDAY OPTIONS
|
|
85
|
+
// ============================================================================
|
|
86
|
+
|
|
87
|
+
export interface WeekdayOption {
|
|
88
|
+
id: number;
|
|
89
|
+
labelKey: string;
|
|
90
|
+
shortLabelKey: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const WEEKDAY_OPTIONS: WeekdayOption[] = [
|
|
94
|
+
{ id: 1, labelKey: 'notifications.weekdays.sunday', shortLabelKey: 'notifications.weekdays.sun' },
|
|
95
|
+
{ id: 2, labelKey: 'notifications.weekdays.monday', shortLabelKey: 'notifications.weekdays.mon' },
|
|
96
|
+
{ id: 3, labelKey: 'notifications.weekdays.tuesday', shortLabelKey: 'notifications.weekdays.tue' },
|
|
97
|
+
{ id: 4, labelKey: 'notifications.weekdays.wednesday', shortLabelKey: 'notifications.weekdays.wed' },
|
|
98
|
+
{ id: 5, labelKey: 'notifications.weekdays.thursday', shortLabelKey: 'notifications.weekdays.thu' },
|
|
99
|
+
{ id: 6, labelKey: 'notifications.weekdays.friday', shortLabelKey: 'notifications.weekdays.fri' },
|
|
100
|
+
{ id: 7, labelKey: 'notifications.weekdays.saturday', shortLabelKey: 'notifications.weekdays.sat' },
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
// ============================================================================
|
|
104
|
+
// HELPER FUNCTIONS
|
|
105
|
+
// ============================================================================
|
|
106
|
+
|
|
107
|
+
export const getTimePresetById = (id: string): TimePreset | undefined => {
|
|
108
|
+
return DEFAULT_TIME_PRESETS.find(preset => preset.id === id);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const formatTime = (hour: number, minute: number): string => {
|
|
112
|
+
const h = hour.toString().padStart(2, '0');
|
|
113
|
+
const m = minute.toString().padStart(2, '0');
|
|
114
|
+
return `${h}:${m}`;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const parseTime = (timeString: string): { hour: number; minute: number } => {
|
|
118
|
+
const [hour, minute] = timeString.split(':').map(Number);
|
|
119
|
+
return { hour: hour || 0, minute: minute || 0 };
|
|
120
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useReminderActions Hook
|
|
3
|
+
* Handles reminder CRUD operations with notification scheduling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback } from 'react';
|
|
7
|
+
import { useRemindersStore } from '../storage/RemindersStore';
|
|
8
|
+
import { NotificationScheduler } from '../../../../infrastructure/services/NotificationScheduler';
|
|
9
|
+
import { generateReminderId } from '../../../../infrastructure/utils/idGenerator';
|
|
10
|
+
import { buildTrigger } from '../../../../infrastructure/utils/triggerBuilder';
|
|
11
|
+
import type { Reminder, CreateReminderInput, UpdateReminderInput } from '../../../../infrastructure/services/types';
|
|
12
|
+
|
|
13
|
+
const scheduler = new NotificationScheduler();
|
|
14
|
+
|
|
15
|
+
export const useReminderActions = () => {
|
|
16
|
+
const { addReminder, updateReminder, deleteReminder, toggleReminder } = useRemindersStore();
|
|
17
|
+
|
|
18
|
+
const createReminder = useCallback(async (input: CreateReminderInput): Promise<Reminder> => {
|
|
19
|
+
const now = new Date().toISOString();
|
|
20
|
+
const reminder: Reminder = {
|
|
21
|
+
id: generateReminderId(),
|
|
22
|
+
...input,
|
|
23
|
+
enabled: true,
|
|
24
|
+
createdAt: now,
|
|
25
|
+
updatedAt: now,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const trigger = buildTrigger(reminder);
|
|
29
|
+
const notificationId = await scheduler.scheduleNotification({
|
|
30
|
+
title: reminder.title,
|
|
31
|
+
body: reminder.body,
|
|
32
|
+
trigger,
|
|
33
|
+
data: { reminderId: reminder.id },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
reminder.notificationId = notificationId;
|
|
37
|
+
await addReminder(reminder);
|
|
38
|
+
|
|
39
|
+
return reminder;
|
|
40
|
+
}, [addReminder]);
|
|
41
|
+
|
|
42
|
+
const editReminder = useCallback(async (id: string, input: UpdateReminderInput): Promise<void> => {
|
|
43
|
+
const { reminders } = useRemindersStore.getState();
|
|
44
|
+
const existing = reminders.find(r => r.id === id);
|
|
45
|
+
if (!existing) return;
|
|
46
|
+
|
|
47
|
+
if (existing.notificationId) {
|
|
48
|
+
await scheduler.cancelNotification(existing.notificationId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const updated: Reminder = { ...existing, ...input, updatedAt: new Date().toISOString() };
|
|
52
|
+
|
|
53
|
+
if (updated.enabled) {
|
|
54
|
+
const trigger = buildTrigger(updated);
|
|
55
|
+
const notificationId = await scheduler.scheduleNotification({
|
|
56
|
+
title: updated.title,
|
|
57
|
+
body: updated.body,
|
|
58
|
+
trigger,
|
|
59
|
+
data: { reminderId: updated.id },
|
|
60
|
+
});
|
|
61
|
+
updated.notificationId = notificationId;
|
|
62
|
+
} else {
|
|
63
|
+
updated.notificationId = undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await updateReminder(id, updated);
|
|
67
|
+
}, [updateReminder]);
|
|
68
|
+
|
|
69
|
+
const removeReminder = useCallback(async (id: string): Promise<void> => {
|
|
70
|
+
const { reminders } = useRemindersStore.getState();
|
|
71
|
+
const reminder = reminders.find(r => r.id === id);
|
|
72
|
+
|
|
73
|
+
if (reminder?.notificationId) {
|
|
74
|
+
await scheduler.cancelNotification(reminder.notificationId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
await deleteReminder(id);
|
|
78
|
+
}, [deleteReminder]);
|
|
79
|
+
|
|
80
|
+
const toggleReminderEnabled = useCallback(async (id: string): Promise<void> => {
|
|
81
|
+
const { reminders } = useRemindersStore.getState();
|
|
82
|
+
const reminder = reminders.find(r => r.id === id);
|
|
83
|
+
if (!reminder) return;
|
|
84
|
+
|
|
85
|
+
if (reminder.enabled && reminder.notificationId) {
|
|
86
|
+
await scheduler.cancelNotification(reminder.notificationId);
|
|
87
|
+
await updateReminder(id, { enabled: false, notificationId: undefined });
|
|
88
|
+
} else if (!reminder.enabled) {
|
|
89
|
+
const trigger = buildTrigger(reminder);
|
|
90
|
+
const notificationId = await scheduler.scheduleNotification({
|
|
91
|
+
title: reminder.title,
|
|
92
|
+
body: reminder.body,
|
|
93
|
+
trigger,
|
|
94
|
+
data: { reminderId: reminder.id },
|
|
95
|
+
});
|
|
96
|
+
await updateReminder(id, { enabled: true, notificationId });
|
|
97
|
+
}
|
|
98
|
+
}, [updateReminder]);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
createReminder,
|
|
102
|
+
editReminder,
|
|
103
|
+
removeReminder,
|
|
104
|
+
toggleReminderEnabled,
|
|
105
|
+
};
|
|
106
|
+
};
|