@umituz/react-native-settings 5.2.34 → 5.2.36
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 +2 -4
- package/src/domains/about/presentation/screens/AboutScreenContent.tsx +87 -63
- package/src/domains/appearance/data/colorPalettes.ts +0 -23
- package/src/domains/appearance/presentation/components/CustomColorsSection.tsx +2 -4
- package/src/domains/appearance/presentation/components/ThemeOption.tsx +2 -2
- package/src/domains/dev/presentation/components/DevSettingsSection.tsx +5 -2
- package/src/domains/faqs/presentation/screens/FAQScreen.tsx +19 -25
- package/src/domains/feedback/presentation/components/FeedbackForm.tsx +160 -81
- package/src/domains/gamification/components/GamificationScreen/GamificationScreenWithConfig.tsx +11 -11
- package/src/domains/localization/infrastructure/components/LanguageSwitcher.tsx +0 -2
- package/src/domains/localization/infrastructure/storage/localizationStoreUtils.ts +1 -1
- package/src/domains/localization/presentation/screens/LanguageSelectionScreen.tsx +85 -48
- package/src/domains/localization/presentation/screens/__tests__/LanguageSelectionScreen.test.tsx +0 -15
- package/src/domains/notifications/presentation/screens/NotificationsScreen.tsx +1 -3
- package/src/domains/notifications/reminders/presentation/components/ReminderForm.constants.ts +0 -4
- package/src/domains/notifications/reminders/presentation/components/ReminderForm.tsx +69 -31
- package/src/domains/rating/presentation/components/StarRating.tsx +7 -13
- package/src/infrastructure/utils/configFactory.ts +0 -26
- package/src/infrastructure/utils/constants/textLimits.ts +0 -2
- package/src/infrastructure/utils/sanitizers.ts +1 -25
- package/src/infrastructure/utils/validation/core.ts +0 -33
- package/src/infrastructure/utils/validation/formValidators.ts +7 -1
- package/src/infrastructure/utils/validation/index.ts +2 -33
- package/src/infrastructure/utils/validators.ts +0 -6
- package/src/presentation/navigation/utils/index.ts +1 -7
- package/src/presentation/navigation/utils/navigationHelpers.ts +2 -87
- package/src/presentation/screens/components/SettingsContent.tsx +4 -19
- package/src/presentation/screens/components/sections/CustomSettingsList.tsx +3 -8
- package/src/presentation/screens/components/sections/FeatureSettingsSection.tsx +0 -4
- package/src/presentation/screens/components/sections/IdentitySettingsSection.tsx +0 -4
- package/src/presentation/screens/components/sections/SupportSettingsSection.tsx +0 -4
- package/src/presentation/utils/screenFactory.ts +0 -25
- package/src/utils/appUtils.ts +0 -18
- package/src/utils/devUtils.ts +0 -10
- package/src/utils/errorUtils.ts +0 -22
- package/src/domains/about/utils/index.ts +0 -156
- package/src/domains/faqs/domain/services/index.ts +0 -1
- package/src/domains/faqs/presentation/screens/index.ts +0 -2
- package/src/domains/gamification/components/GamificationScreen/Header.tsx +0 -30
- package/src/domains/legal/presentation/components/LegalLinks.tsx +0 -137
- package/src/domains/legal/presentation/components/index.ts +0 -5
- package/src/domains/localization/infrastructure/config/languagesData.ts +0 -26
- package/src/domains/localization/infrastructure/hooks/TranslationHook.ts +0 -37
- package/src/domains/localization/infrastructure/storage/AsyncStorageWrapper.ts +0 -34
- package/src/infrastructure/storage/storeConfig.ts +0 -114
- package/src/infrastructure/types/commonComponentTypes.ts +0 -142
- package/src/infrastructure/utils/async/core.ts +0 -110
- package/src/infrastructure/utils/async/debounceAndBatch.ts +0 -69
- package/src/infrastructure/utils/async/index.ts +0 -8
- package/src/infrastructure/utils/async/retryAndTimeout.ts +0 -65
- package/src/infrastructure/utils/dateUtils.ts +0 -61
- package/src/infrastructure/utils/errorHandlers.ts +0 -250
- package/src/infrastructure/utils/index.ts +0 -12
- package/src/infrastructure/utils/memoComparisonUtils.ts +0 -66
- package/src/infrastructure/utils/memoUtils.ts +0 -167
- package/src/infrastructure/utils/styleTokens.ts +0 -145
- package/src/infrastructure/utils/styles/componentStyles.ts +0 -90
- package/src/infrastructure/utils/styles/index.ts +0 -9
- package/src/infrastructure/utils/styles/layoutStyles.ts +0 -56
- package/src/infrastructure/utils/styles/spacingStyles.ts +0 -33
- package/src/infrastructure/utils/styles/styleHelpers.ts +0 -22
- package/src/infrastructure/utils/translationHelpers.ts +0 -81
- package/src/infrastructure/utils/validation/numericValidators.ts +0 -66
- package/src/infrastructure/utils/validation/passwordValidator.ts +0 -53
- package/src/infrastructure/utils/validation/textValidators.ts +0 -118
- package/src/presentation/components/ErrorBoundary/SettingsErrorBoundary.tsx +0 -105
- package/src/presentation/components/ErrorBoundary/index.ts +0 -12
- package/src/presentation/components/ErrorBoundary/withErrorBoundary.tsx +0 -45
- package/src/utils/hooks/index.ts +0 -6
- package/src/utils/index.ts +0 -3
package/src/domains/gamification/components/GamificationScreen/GamificationScreenWithConfig.tsx
CHANGED
|
@@ -38,40 +38,40 @@ export const GamificationScreenWithConfig: React.FC<GamificationConfigProps> = (
|
|
|
38
38
|
type: a.type
|
|
39
39
|
}));
|
|
40
40
|
|
|
41
|
-
const
|
|
41
|
+
const t = config.translations ?? {};
|
|
42
42
|
const screenProps: GamificationScreenProps = {
|
|
43
|
-
title:
|
|
44
|
-
statsTitle:
|
|
45
|
-
achievementsTitle:
|
|
46
|
-
streakTitle:
|
|
43
|
+
title: t.title ?? 'Gamification',
|
|
44
|
+
statsTitle: t.statsTitle ?? 'Stats',
|
|
45
|
+
achievementsTitle: t.achievementsTitle ?? 'Achievements',
|
|
46
|
+
streakTitle: t.streakTitle ?? 'Streak',
|
|
47
47
|
levelProps: {
|
|
48
48
|
level: level.currentLevel,
|
|
49
49
|
points: level.currentPoints,
|
|
50
50
|
pointsToNext: level.pointsToNext,
|
|
51
51
|
progress: level.progress,
|
|
52
|
-
levelTitle:
|
|
52
|
+
levelTitle: t.levelTitle ?? 'Level',
|
|
53
53
|
showPoints: true,
|
|
54
54
|
},
|
|
55
55
|
streakProps: {
|
|
56
56
|
current: streak.current,
|
|
57
57
|
longest: streak.longest,
|
|
58
|
-
bestLabel:
|
|
59
|
-
daysLabel:
|
|
58
|
+
bestLabel: t.bestStreak ?? 'Best',
|
|
59
|
+
daysLabel: t.days ?? 'days',
|
|
60
60
|
},
|
|
61
61
|
stats: [
|
|
62
62
|
{
|
|
63
|
-
label:
|
|
63
|
+
label: t.pointsLabel ?? 'Points',
|
|
64
64
|
value: points,
|
|
65
65
|
icon: "⭐",
|
|
66
66
|
},
|
|
67
67
|
{
|
|
68
|
-
label:
|
|
68
|
+
label: t.totalCompletedLabel ?? 'Completed',
|
|
69
69
|
value: totalTasksCompleted,
|
|
70
70
|
icon: "✅",
|
|
71
71
|
},
|
|
72
72
|
],
|
|
73
73
|
achievements: achievementItems,
|
|
74
|
-
emptyAchievementsText:
|
|
74
|
+
emptyAchievementsText: t.emptyAchievements ?? 'No achievements yet',
|
|
75
75
|
};
|
|
76
76
|
|
|
77
77
|
return <GamificationScreenInner {...screenProps} />;
|
|
@@ -8,7 +8,7 @@ import { LanguageSwitcher } from "./LanguageSwitcher";
|
|
|
8
8
|
import { languageRepository } from "../repository/LanguageRepository";
|
|
9
9
|
import { isDev } from "../../../../utils/devUtils";
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
const LANGUAGE_SWITCH_DEBOUNCE_MS = 300;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Manages localization initialization state
|
|
@@ -12,13 +12,78 @@ import {
|
|
|
12
12
|
NavigationHeader,
|
|
13
13
|
useAppNavigation,
|
|
14
14
|
} from '@umituz/react-native-design-system';
|
|
15
|
-
import { isDev } from '../../../../utils/devUtils';
|
|
16
15
|
import { useLanguageSelection } from '../../infrastructure/hooks/useLanguageSelection';
|
|
17
16
|
import { LanguageItem } from '../components/LanguageItem';
|
|
18
17
|
import type { Language } from '../../infrastructure/storage/types/Language';
|
|
19
18
|
import type { LanguageSelectionScreenProps } from './LanguageSelectionScreen.types';
|
|
20
19
|
import { styles } from './LanguageSelectionScreen.styles';
|
|
21
20
|
|
|
21
|
+
interface LanguageListItemProps {
|
|
22
|
+
item: Language;
|
|
23
|
+
selectedCode: string;
|
|
24
|
+
onSelect: (code: string) => void;
|
|
25
|
+
renderLanguageItem?: LanguageSelectionScreenProps['renderLanguageItem'];
|
|
26
|
+
customStyles?: LanguageSelectionScreenProps['styles'];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const LanguageListItem: React.FC<LanguageListItemProps> = ({
|
|
30
|
+
item,
|
|
31
|
+
selectedCode,
|
|
32
|
+
onSelect,
|
|
33
|
+
renderLanguageItem,
|
|
34
|
+
customStyles,
|
|
35
|
+
}) => {
|
|
36
|
+
const isSelected = selectedCode === item.code;
|
|
37
|
+
|
|
38
|
+
if (renderLanguageItem) {
|
|
39
|
+
return <>{renderLanguageItem(item, isSelected, onSelect)}</>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<LanguageItem
|
|
44
|
+
item={item}
|
|
45
|
+
isSelected={isSelected}
|
|
46
|
+
onSelect={onSelect}
|
|
47
|
+
customStyles={customStyles}
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
interface LanguageSearchComponentProps {
|
|
53
|
+
searchQuery: string;
|
|
54
|
+
setSearchQuery: (query: string) => void;
|
|
55
|
+
searchPlaceholder?: string;
|
|
56
|
+
renderSearchInput?: LanguageSelectionScreenProps['renderSearchInput'];
|
|
57
|
+
customStyles?: LanguageSelectionScreenProps['styles'];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const LanguageSearchComponent: React.FC<LanguageSearchComponentProps> = ({
|
|
61
|
+
searchQuery,
|
|
62
|
+
setSearchQuery,
|
|
63
|
+
searchPlaceholder,
|
|
64
|
+
renderSearchInput,
|
|
65
|
+
customStyles,
|
|
66
|
+
}) => {
|
|
67
|
+
const tokens = useAppDesignTokens();
|
|
68
|
+
|
|
69
|
+
if (renderSearchInput) {
|
|
70
|
+
return renderSearchInput(searchQuery, setSearchQuery, searchPlaceholder || '');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<SearchBar
|
|
75
|
+
value={searchQuery}
|
|
76
|
+
onChangeText={setSearchQuery}
|
|
77
|
+
placeholder={searchPlaceholder}
|
|
78
|
+
containerStyle={[
|
|
79
|
+
{ marginBottom: tokens.spacing.md },
|
|
80
|
+
customStyles?.searchContainer
|
|
81
|
+
]}
|
|
82
|
+
inputStyle={customStyles?.searchInput}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
22
87
|
export const LanguageSelectionScreen: React.FC<LanguageSelectionScreenProps> = ({
|
|
23
88
|
renderLanguageItem,
|
|
24
89
|
renderSearchInput,
|
|
@@ -39,52 +104,20 @@ export const LanguageSelectionScreen: React.FC<LanguageSelectionScreenProps> = (
|
|
|
39
104
|
} = useLanguageSelection();
|
|
40
105
|
|
|
41
106
|
const onSelect = async (code: string) => {
|
|
42
|
-
if (isDev()) {
|
|
43
|
-
}
|
|
44
107
|
await handleLanguageSelect(code, () => {
|
|
45
|
-
if (isDev()) {
|
|
46
|
-
}
|
|
47
108
|
navigation.goBack();
|
|
48
109
|
});
|
|
49
|
-
if (isDev()) {
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const renderItem = ({ item }: { item: Language }) => {
|
|
54
|
-
const isSelected = selectedCode === item.code;
|
|
55
|
-
|
|
56
|
-
if (renderLanguageItem) {
|
|
57
|
-
return <>{renderLanguageItem(item, isSelected, onSelect)}</>;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<LanguageItem
|
|
62
|
-
item={item}
|
|
63
|
-
isSelected={isSelected}
|
|
64
|
-
onSelect={onSelect}
|
|
65
|
-
customStyles={customStyles}
|
|
66
|
-
/>
|
|
67
|
-
);
|
|
68
110
|
};
|
|
69
111
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
placeholder={searchPlaceholder}
|
|
80
|
-
containerStyle={[
|
|
81
|
-
{ marginBottom: tokens.spacing.md },
|
|
82
|
-
customStyles?.searchContainer
|
|
83
|
-
]}
|
|
84
|
-
inputStyle={customStyles?.searchInput}
|
|
85
|
-
/>
|
|
86
|
-
);
|
|
87
|
-
};
|
|
112
|
+
const renderItem = ({ item }: { item: Language }) => (
|
|
113
|
+
<LanguageListItem
|
|
114
|
+
item={item}
|
|
115
|
+
selectedCode={selectedCode}
|
|
116
|
+
onSelect={onSelect}
|
|
117
|
+
renderLanguageItem={renderLanguageItem}
|
|
118
|
+
customStyles={customStyles}
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
88
121
|
|
|
89
122
|
const handleBack = () => {
|
|
90
123
|
if (onBackPress) {
|
|
@@ -101,14 +134,20 @@ export const LanguageSelectionScreen: React.FC<LanguageSelectionScreenProps> = (
|
|
|
101
134
|
edges={['top', 'bottom', 'left', 'right']}
|
|
102
135
|
backgroundColor={tokens.colors.backgroundPrimary}
|
|
103
136
|
header={
|
|
104
|
-
<NavigationHeader
|
|
105
|
-
title={headerTitle || ""}
|
|
106
|
-
onBackPress={handleBack}
|
|
137
|
+
<NavigationHeader
|
|
138
|
+
title={headerTitle || ""}
|
|
139
|
+
onBackPress={handleBack}
|
|
107
140
|
/>
|
|
108
141
|
}
|
|
109
142
|
containerStyle={customStyles?.container}
|
|
110
143
|
>
|
|
111
|
-
|
|
144
|
+
<LanguageSearchComponent
|
|
145
|
+
searchQuery={searchQuery}
|
|
146
|
+
setSearchQuery={setSearchQuery}
|
|
147
|
+
searchPlaceholder={searchPlaceholder}
|
|
148
|
+
renderSearchInput={renderSearchInput}
|
|
149
|
+
customStyles={customStyles}
|
|
150
|
+
/>
|
|
112
151
|
<FlatList
|
|
113
152
|
data={filteredLanguages}
|
|
114
153
|
renderItem={renderItem}
|
|
@@ -134,5 +173,3 @@ export const LanguageSelectionScreen: React.FC<LanguageSelectionScreenProps> = (
|
|
|
134
173
|
);
|
|
135
174
|
};
|
|
136
175
|
|
|
137
|
-
export default LanguageSelectionScreen;
|
|
138
|
-
|
package/src/domains/localization/presentation/screens/__tests__/LanguageSelectionScreen.test.tsx
CHANGED
|
@@ -121,21 +121,6 @@ describe('LanguageSelectionScreen', () => {
|
|
|
121
121
|
);
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
-
it('should use custom container when provided', () => {
|
|
125
|
-
const CustomContainer = ({ children }: { children: React.ReactNode }) => (
|
|
126
|
-
<div testID="custom-container">{children}</div>
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
const { getByTestId } = render(
|
|
130
|
-
<LanguageSelectionScreen
|
|
131
|
-
searchPlaceholder="Search languages..."
|
|
132
|
-
containerComponent={CustomContainer}
|
|
133
|
-
/>
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
expect(getByTestId('custom-container')).toBeTruthy();
|
|
137
|
-
});
|
|
138
|
-
|
|
139
124
|
it('should apply custom styles', () => {
|
|
140
125
|
const customStyles = {
|
|
141
126
|
container: { backgroundColor: 'red' },
|
package/src/domains/notifications/reminders/presentation/components/ReminderForm.constants.ts
CHANGED
|
@@ -6,10 +6,6 @@ export const DEFAULT_WEEKDAY = 2; // Tuesday
|
|
|
6
6
|
export const MAX_TITLE_LENGTH = 100;
|
|
7
7
|
export const MAX_BODY_LENGTH = 500;
|
|
8
8
|
|
|
9
|
-
export const VALID_HOUR_RANGE = { min: 0, max: 23 } as const;
|
|
10
|
-
export const VALID_MINUTE_RANGE = { min: 0, max: 59 } as const;
|
|
11
|
-
export const VALID_WEEKDAY_RANGE = { min: 0, max: 6 } as const;
|
|
12
|
-
|
|
13
9
|
export interface ReminderFormTranslations {
|
|
14
10
|
titleLabel: string;
|
|
15
11
|
titlePlaceholder: string;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Form for creating and editing reminders
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React, {
|
|
6
|
+
import React, { useReducer, useMemo, useCallback } from 'react';
|
|
7
7
|
import { View, TextInput, ScrollView } from 'react-native';
|
|
8
8
|
import { AtomicText } from '@umituz/react-native-design-system';
|
|
9
9
|
import { useAppDesignTokens } from '@umituz/react-native-design-system';
|
|
@@ -24,6 +24,53 @@ import { createReminderFormStyles as createStyles } from './ReminderForm.styles'
|
|
|
24
24
|
import { DEFAULT_TIME_PRESETS, FREQUENCY_OPTIONS } from '../../infrastructure/config/reminderPresets';
|
|
25
25
|
import type { ReminderFrequency, TimePreset } from '../../../infrastructure/services/types';
|
|
26
26
|
|
|
27
|
+
interface ReminderFormState {
|
|
28
|
+
title: string;
|
|
29
|
+
body: string;
|
|
30
|
+
frequency: ReminderFrequency;
|
|
31
|
+
selectedPresetId: string | undefined;
|
|
32
|
+
hour: number;
|
|
33
|
+
minute: number;
|
|
34
|
+
weekday: number;
|
|
35
|
+
isCustomTime: boolean;
|
|
36
|
+
error: string | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type ReminderFormAction =
|
|
40
|
+
| { type: 'SET_TITLE'; payload: string }
|
|
41
|
+
| { type: 'SET_BODY'; payload: string }
|
|
42
|
+
| { type: 'SET_FREQUENCY'; payload: ReminderFrequency }
|
|
43
|
+
| { type: 'SET_WEEKDAY'; payload: number }
|
|
44
|
+
| { type: 'SELECT_PRESET'; payload: TimePreset }
|
|
45
|
+
| { type: 'SELECT_CUSTOM' }
|
|
46
|
+
| { type: 'SET_ERROR'; payload: string | null };
|
|
47
|
+
|
|
48
|
+
function reminderFormReducer(state: ReminderFormState, action: ReminderFormAction): ReminderFormState {
|
|
49
|
+
switch (action.type) {
|
|
50
|
+
case 'SET_TITLE':
|
|
51
|
+
return { ...state, title: action.payload, error: null };
|
|
52
|
+
case 'SET_BODY':
|
|
53
|
+
return { ...state, body: action.payload, error: null };
|
|
54
|
+
case 'SET_FREQUENCY':
|
|
55
|
+
return { ...state, frequency: action.payload };
|
|
56
|
+
case 'SET_WEEKDAY':
|
|
57
|
+
return { ...state, weekday: action.payload };
|
|
58
|
+
case 'SELECT_PRESET':
|
|
59
|
+
return {
|
|
60
|
+
...state,
|
|
61
|
+
selectedPresetId: action.payload.id,
|
|
62
|
+
hour: action.payload.hour,
|
|
63
|
+
minute: action.payload.minute,
|
|
64
|
+
isCustomTime: false,
|
|
65
|
+
error: null,
|
|
66
|
+
};
|
|
67
|
+
case 'SELECT_CUSTOM':
|
|
68
|
+
return { ...state, selectedPresetId: undefined, isCustomTime: true };
|
|
69
|
+
case 'SET_ERROR':
|
|
70
|
+
return { ...state, error: action.payload };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
27
74
|
export const ReminderForm: React.FC<ReminderFormProps> = ({
|
|
28
75
|
initialData,
|
|
29
76
|
translations,
|
|
@@ -34,36 +81,32 @@ export const ReminderForm: React.FC<ReminderFormProps> = ({
|
|
|
34
81
|
const tokens = useAppDesignTokens();
|
|
35
82
|
const styles = useMemo(() => createStyles(tokens), [tokens]);
|
|
36
83
|
|
|
37
|
-
const [
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
84
|
+
const [state, dispatch] = useReducer(reminderFormReducer, {
|
|
85
|
+
title: initialData?.title || '',
|
|
86
|
+
body: initialData?.body || '',
|
|
87
|
+
frequency: initialData?.frequency || 'daily',
|
|
88
|
+
selectedPresetId: initialData?.timePresetId,
|
|
89
|
+
hour: initialData?.hour ?? DEFAULT_HOUR,
|
|
90
|
+
minute: initialData?.minute ?? DEFAULT_MINUTE,
|
|
91
|
+
weekday: initialData?.weekday ?? DEFAULT_WEEKDAY,
|
|
92
|
+
isCustomTime: !initialData?.timePresetId,
|
|
93
|
+
error: null,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const { title, body, frequency, selectedPresetId, hour, minute, weekday, isCustomTime, error } = state;
|
|
47
97
|
|
|
48
98
|
const handlePresetSelect = useCallback((preset: TimePreset) => {
|
|
49
|
-
|
|
50
|
-
setHour(preset.hour);
|
|
51
|
-
setMinute(preset.minute);
|
|
52
|
-
setIsCustomTime(false);
|
|
53
|
-
// Clear error when user changes something
|
|
54
|
-
setError(null);
|
|
99
|
+
dispatch({ type: 'SELECT_PRESET', payload: preset });
|
|
55
100
|
}, []);
|
|
56
101
|
|
|
57
102
|
const handleCustomSelect = useCallback(() => {
|
|
58
|
-
|
|
59
|
-
setIsCustomTime(true);
|
|
103
|
+
dispatch({ type: 'SELECT_CUSTOM' });
|
|
60
104
|
}, []);
|
|
61
105
|
|
|
62
106
|
const handleSave = useCallback(() => {
|
|
63
107
|
const trimmedTitle = title.trim();
|
|
64
108
|
const trimmedBody = body.trim();
|
|
65
109
|
|
|
66
|
-
// Validate using centralized validation
|
|
67
110
|
const validationResult = validateReminderForm({
|
|
68
111
|
title: trimmedTitle,
|
|
69
112
|
body: trimmedBody,
|
|
@@ -76,15 +119,12 @@ export const ReminderForm: React.FC<ReminderFormProps> = ({
|
|
|
76
119
|
});
|
|
77
120
|
|
|
78
121
|
if (!validationResult.isValid) {
|
|
79
|
-
|
|
80
|
-
setError(validationResult.error || "Validation failed");
|
|
122
|
+
dispatch({ type: 'SET_ERROR', payload: validationResult.error || "Validation failed" });
|
|
81
123
|
return;
|
|
82
124
|
}
|
|
83
125
|
|
|
84
|
-
|
|
85
|
-
setError(null);
|
|
126
|
+
dispatch({ type: 'SET_ERROR', payload: null });
|
|
86
127
|
|
|
87
|
-
// Sanitize input (React Native handles XSS, but we trim extra whitespace)
|
|
88
128
|
const sanitizedTitle = trimmedTitle.replace(/\s+/g, ' ').trim();
|
|
89
129
|
const sanitizedBody = trimmedBody.replace(/\s+/g, ' ').trim();
|
|
90
130
|
|
|
@@ -108,8 +148,7 @@ export const ReminderForm: React.FC<ReminderFormProps> = ({
|
|
|
108
148
|
style={styles.input}
|
|
109
149
|
value={title}
|
|
110
150
|
onChangeText={(text) => {
|
|
111
|
-
|
|
112
|
-
setError(null); // Clear error on input
|
|
151
|
+
dispatch({ type: 'SET_TITLE', payload: text });
|
|
113
152
|
}}
|
|
114
153
|
placeholder={translations.titlePlaceholder}
|
|
115
154
|
placeholderTextColor={tokens.colors.textSecondary}
|
|
@@ -122,8 +161,7 @@ export const ReminderForm: React.FC<ReminderFormProps> = ({
|
|
|
122
161
|
style={[styles.input, styles.multilineInput]}
|
|
123
162
|
value={body}
|
|
124
163
|
onChangeText={(text) => {
|
|
125
|
-
|
|
126
|
-
setError(null); // Clear error on input
|
|
164
|
+
dispatch({ type: 'SET_BODY', payload: text });
|
|
127
165
|
}}
|
|
128
166
|
placeholder={translations.bodyPlaceholder}
|
|
129
167
|
placeholderTextColor={tokens.colors.textSecondary}
|
|
@@ -137,7 +175,7 @@ export const ReminderForm: React.FC<ReminderFormProps> = ({
|
|
|
137
175
|
<FrequencySelector
|
|
138
176
|
options={FREQUENCY_OPTIONS}
|
|
139
177
|
selectedFrequency={frequency}
|
|
140
|
-
onSelect={
|
|
178
|
+
onSelect={(freq: ReminderFrequency) => dispatch({ type: 'SET_FREQUENCY', payload: freq })}
|
|
141
179
|
getLabel={translations.getFrequencyLabel}
|
|
142
180
|
/>
|
|
143
181
|
</View>
|
|
@@ -159,7 +197,7 @@ export const ReminderForm: React.FC<ReminderFormProps> = ({
|
|
|
159
197
|
{frequency === 'weekly' && (
|
|
160
198
|
<View style={styles.section}>
|
|
161
199
|
<AtomicText type="bodyMedium" style={styles.label}>{translations.weekdayLabel}</AtomicText>
|
|
162
|
-
<WeekdaySelector selectedWeekday={weekday} onSelect={
|
|
200
|
+
<WeekdaySelector selectedWeekday={weekday} onSelect={(day: number) => dispatch({ type: 'SET_WEEKDAY', payload: day })} getLabel={translations.getWeekdayLabel} />
|
|
163
201
|
</View>
|
|
164
202
|
)}
|
|
165
203
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState
|
|
1
|
+
import React, { useState } from "react";
|
|
2
2
|
import { View, StyleSheet, TouchableOpacity, ViewStyle } from "react-native";
|
|
3
3
|
import { useAppDesignTokens, AtomicIcon } from "@umituz/react-native-design-system";
|
|
4
4
|
|
|
@@ -25,23 +25,17 @@ export const StarRating: React.FC<StarRatingProps> = ({
|
|
|
25
25
|
}) => {
|
|
26
26
|
const tokens = useAppDesignTokens();
|
|
27
27
|
const styles = getStyles(tokens);
|
|
28
|
-
const [
|
|
28
|
+
const [selectedRating, setSelectedRating] = useState<number | null>(null);
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
useEffect(() => {
|
|
32
|
-
setInternalRating(rating);
|
|
33
|
-
}, [rating]);
|
|
30
|
+
const displayRating = selectedRating ?? rating;
|
|
34
31
|
|
|
35
32
|
const filledColor = activeColor || tokens.colors.warning;
|
|
36
33
|
const emptyColor = inactiveColor || tokens.colors.borderLight;
|
|
37
|
-
|
|
38
|
-
// Scale the size
|
|
39
|
-
const responsiveSize = size;
|
|
40
34
|
|
|
41
35
|
const handlePress = (index: number) => {
|
|
42
36
|
if (disabled) return;
|
|
43
37
|
const newRating = index + 1;
|
|
44
|
-
|
|
38
|
+
setSelectedRating(newRating);
|
|
45
39
|
onRatingChange?.(newRating);
|
|
46
40
|
};
|
|
47
41
|
|
|
@@ -56,9 +50,9 @@ export const StarRating: React.FC<StarRatingProps> = ({
|
|
|
56
50
|
style={styles.starContainer}
|
|
57
51
|
>
|
|
58
52
|
<AtomicIcon
|
|
59
|
-
name={index < (onRatingChange ?
|
|
60
|
-
customSize={
|
|
61
|
-
customColor={index < (onRatingChange ?
|
|
53
|
+
name={index < (onRatingChange ? displayRating : rating) ? "star" : "star-outline"}
|
|
54
|
+
customSize={size}
|
|
55
|
+
customColor={index < (onRatingChange ? displayRating : rating) ? filledColor : emptyColor}
|
|
62
56
|
/>
|
|
63
57
|
</TouchableOpacity>
|
|
64
58
|
))}
|
|
@@ -62,29 +62,3 @@ export const createConfigWithExtensions = <T extends BaseConfigType>(
|
|
|
62
62
|
return { ...baseConfig, ...extensions };
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
-
/**
|
|
66
|
-
* Create a disabled configuration
|
|
67
|
-
*/
|
|
68
|
-
export const createDisabledConfig = <T extends BaseConfigType>(
|
|
69
|
-
params: Omit<ConfigCreatorParams, "routeOrOnPress" | "defaultRoute">
|
|
70
|
-
): T => {
|
|
71
|
-
const baseConfig = createBaseConfig<T>(params);
|
|
72
|
-
return { ...baseConfig, enabled: false } as T;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Batch create configurations
|
|
77
|
-
*/
|
|
78
|
-
export const createBatchConfigs = <T extends BaseConfigType>(
|
|
79
|
-
items: Array<{
|
|
80
|
-
icon: string;
|
|
81
|
-
routeOrOnPress?: string | (() => void);
|
|
82
|
-
}>
|
|
83
|
-
): T[] => {
|
|
84
|
-
return items.map((item) =>
|
|
85
|
-
createBaseConfig<T>({
|
|
86
|
-
icon: item.icon,
|
|
87
|
-
routeOrOnPress: item.routeOrOnPress,
|
|
88
|
-
})
|
|
89
|
-
);
|
|
90
|
-
};
|
|
@@ -6,10 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { TEXT_LENGTH_LIMITS } from './constants/textLimits';
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
* Sanitize string by trimming and limiting length
|
|
11
|
-
*/
|
|
12
|
-
export function sanitizeString(str: string | undefined, maxLength: number): string {
|
|
9
|
+
function sanitizeString(str: string | undefined, maxLength: number): string {
|
|
13
10
|
if (!str) return "";
|
|
14
11
|
return str.trim().slice(0, maxLength);
|
|
15
12
|
}
|
|
@@ -28,24 +25,3 @@ export function sanitizeDescription(description: string | undefined): string | u
|
|
|
28
25
|
return sanitizeString(description || "", TEXT_LENGTH_LIMITS.DESCRIPTION);
|
|
29
26
|
}
|
|
30
27
|
|
|
31
|
-
/**
|
|
32
|
-
* Truncate text with ellipsis
|
|
33
|
-
*/
|
|
34
|
-
export function truncateText(text: string, maxLength: number): string {
|
|
35
|
-
if (text.length <= maxLength) return text;
|
|
36
|
-
return text.slice(0, maxLength - 3) + "...";
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Escape HTML entities (for security)
|
|
41
|
-
*/
|
|
42
|
-
export function escapeHtml(text: string): string {
|
|
43
|
-
const htmlEntities: Record<string, string> = {
|
|
44
|
-
"&": "&",
|
|
45
|
-
"<": "<",
|
|
46
|
-
">": ">",
|
|
47
|
-
'"': """,
|
|
48
|
-
"'": "'",
|
|
49
|
-
};
|
|
50
|
-
return text.replace(/[&<>"']/g, (char) => htmlEntities[char] || char);
|
|
51
|
-
}
|
|
@@ -3,40 +3,7 @@
|
|
|
3
3
|
* Base validation interfaces and types
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* Validation result interface
|
|
8
|
-
*/
|
|
9
6
|
export interface ValidationResult {
|
|
10
7
|
isValid: boolean;
|
|
11
8
|
error?: string;
|
|
12
9
|
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Text validation options
|
|
16
|
-
*/
|
|
17
|
-
export interface TextValidationOptions {
|
|
18
|
-
minLength?: number;
|
|
19
|
-
maxLength?: number;
|
|
20
|
-
required?: boolean;
|
|
21
|
-
pattern?: RegExp;
|
|
22
|
-
customValidator?: (value: string) => string | null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Email validation options
|
|
27
|
-
*/
|
|
28
|
-
export interface EmailValidationOptions {
|
|
29
|
-
required?: boolean;
|
|
30
|
-
allowEmpty?: boolean;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Password validation options
|
|
35
|
-
*/
|
|
36
|
-
export interface PasswordValidationOptions {
|
|
37
|
-
minLength?: number;
|
|
38
|
-
requireUppercase?: boolean;
|
|
39
|
-
requireLowercase?: boolean;
|
|
40
|
-
requireNumber?: boolean;
|
|
41
|
-
requireSpecialChar?: boolean;
|
|
42
|
-
}
|
|
@@ -4,9 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { ValidationResult } from "./core";
|
|
7
|
-
import { validateRating } from "./numericValidators";
|
|
8
7
|
import { TEXT_LENGTH_LIMITS } from "../constants/textLimits";
|
|
9
8
|
|
|
9
|
+
const validateRating = (rating: number): ValidationResult => {
|
|
10
|
+
if (rating < 1 || rating > 5) {
|
|
11
|
+
return { isValid: false, error: "Rating must be between 1 and 5" };
|
|
12
|
+
}
|
|
13
|
+
return { isValid: true };
|
|
14
|
+
};
|
|
15
|
+
|
|
10
16
|
/**
|
|
11
17
|
* Feedback form validation
|
|
12
18
|
*/
|
|
@@ -1,37 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Validation Utilities
|
|
3
|
-
* Barrel export for all validation modules
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
|
-
export
|
|
7
|
-
export
|
|
8
|
-
export * from "./passwordValidator";
|
|
9
|
-
export * from "./numericValidators";
|
|
10
|
-
export * from "./formValidators";
|
|
11
|
-
|
|
12
|
-
import type { ValidationResult } from "./core";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Form field validator factory
|
|
16
|
-
* Creates a validator function for a specific field
|
|
17
|
-
*/
|
|
18
|
-
export const createFieldValidator = <T>(
|
|
19
|
-
validator: (value: T) => ValidationResult
|
|
20
|
-
) => {
|
|
21
|
-
return (value: T): ValidationResult => validator(value);
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Multi-field validator
|
|
26
|
-
* Validates multiple fields and returns the first error
|
|
27
|
-
*/
|
|
28
|
-
export const validateMultipleFields = (
|
|
29
|
-
fields: Record<string, ValidationResult>
|
|
30
|
-
): ValidationResult => {
|
|
31
|
-
for (const result of Object.values(fields)) {
|
|
32
|
-
if (!result.isValid) {
|
|
33
|
-
return result;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return { isValid: true };
|
|
37
|
-
};
|
|
5
|
+
export type { ValidationResult } from "./core";
|
|
6
|
+
export { validateFeedbackForm, validateReminderForm } from "./formValidators";
|