@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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-settings",
|
|
3
|
-
"version": "5.2.
|
|
3
|
+
"version": "5.2.36",
|
|
4
4
|
"description": "Complete settings hub for React Native apps - consolidated package with settings, localization, about, legal, appearance, feedback, FAQs, rating, and gamification",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -37,10 +37,8 @@
|
|
|
37
37
|
"type": "git",
|
|
38
38
|
"url": "https://github.com/umituz/react-native-settings"
|
|
39
39
|
},
|
|
40
|
-
"dependencies": {
|
|
41
|
-
"firebase": "^12.7.0"
|
|
42
|
-
},
|
|
43
40
|
"peerDependencies": {
|
|
41
|
+
"firebase": ">=10.0.0",
|
|
44
42
|
"@expo/vector-icons": ">=14.0.0",
|
|
45
43
|
"@react-navigation/native": ">=6.0.0",
|
|
46
44
|
"@react-navigation/stack": ">=6.0.0",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Pure presentational component for About screen
|
|
4
4
|
* No business logic, only rendering
|
|
5
5
|
*/
|
|
6
|
-
import React, { useMemo
|
|
6
|
+
import React, { useMemo } from 'react';
|
|
7
7
|
import {
|
|
8
8
|
View,
|
|
9
9
|
StyleSheet,
|
|
@@ -15,14 +15,80 @@ import type { DesignTokens } from '@umituz/react-native-design-system';
|
|
|
15
15
|
import type { AboutScreenProps } from './AboutScreen';
|
|
16
16
|
|
|
17
17
|
export interface AboutScreenContentProps extends Omit<AboutScreenProps, 'config'> {
|
|
18
|
-
/** App info data */
|
|
19
18
|
appInfo: AppInfo;
|
|
20
|
-
/** Configuration for the about screen */
|
|
21
19
|
config: AboutConfig;
|
|
22
|
-
/** Design tokens */
|
|
23
20
|
_tokens: DesignTokens;
|
|
24
21
|
}
|
|
25
22
|
|
|
23
|
+
interface AboutHeaderSectionProps {
|
|
24
|
+
appInfo: AppInfo;
|
|
25
|
+
config: AboutConfig;
|
|
26
|
+
showHeader: boolean;
|
|
27
|
+
headerComponent?: React.ReactNode;
|
|
28
|
+
headerStyle?: AboutScreenContentProps['headerStyle'];
|
|
29
|
+
titleStyle?: AboutScreenContentProps['titleStyle'];
|
|
30
|
+
versionStyle?: AboutScreenContentProps['versionStyle'];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const AboutHeaderSection: React.FC<AboutHeaderSectionProps> = ({
|
|
34
|
+
appInfo,
|
|
35
|
+
config,
|
|
36
|
+
showHeader,
|
|
37
|
+
headerComponent,
|
|
38
|
+
headerStyle,
|
|
39
|
+
titleStyle,
|
|
40
|
+
versionStyle,
|
|
41
|
+
}) => {
|
|
42
|
+
if (headerComponent) {
|
|
43
|
+
return <>{headerComponent}</>;
|
|
44
|
+
}
|
|
45
|
+
if (!showHeader || !appInfo) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return (
|
|
49
|
+
<AboutHeader
|
|
50
|
+
appInfo={appInfo}
|
|
51
|
+
containerStyle={headerStyle}
|
|
52
|
+
titleStyle={titleStyle}
|
|
53
|
+
versionStyle={versionStyle}
|
|
54
|
+
versionPrefix={config.texts?.versionPrefix}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
interface AboutFooterSectionProps {
|
|
60
|
+
footerComponent?: React.ReactNode;
|
|
61
|
+
borderColor: string;
|
|
62
|
+
footerStyle: ReturnType<typeof getStyles>['footer'];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const AboutFooterSection: React.FC<AboutFooterSectionProps> = ({
|
|
66
|
+
footerComponent,
|
|
67
|
+
borderColor,
|
|
68
|
+
footerStyle,
|
|
69
|
+
}) => {
|
|
70
|
+
if (!footerComponent) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return (
|
|
74
|
+
<View style={[footerStyle, { borderTopColor: borderColor }]}>
|
|
75
|
+
{footerComponent}
|
|
76
|
+
</View>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
interface AboutContentSectionProps {
|
|
81
|
+
appInfo: AppInfo;
|
|
82
|
+
config: AboutConfig;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const AboutContentSection: React.FC<AboutContentSectionProps> = ({ appInfo, config }) => {
|
|
86
|
+
if (!appInfo) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return <AboutContent appInfo={appInfo} config={config} />;
|
|
90
|
+
};
|
|
91
|
+
|
|
26
92
|
export const AboutScreenContent: React.FC<AboutScreenContentProps> = ({
|
|
27
93
|
appInfo,
|
|
28
94
|
config,
|
|
@@ -39,71 +105,29 @@ export const AboutScreenContent: React.FC<AboutScreenContentProps> = ({
|
|
|
39
105
|
const styles = getStyles(_tokens);
|
|
40
106
|
const colors = _tokens.colors;
|
|
41
107
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
108
|
+
const containerStyles = useMemo(() => [
|
|
109
|
+
styles.container,
|
|
110
|
+
{ backgroundColor: colors.backgroundPrimary },
|
|
111
|
+
containerStyle
|
|
112
|
+
], [containerStyle, colors.backgroundPrimary, styles]);
|
|
47
113
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return (
|
|
53
|
-
<AboutHeader
|
|
114
|
+
return (
|
|
115
|
+
<View style={containerStyles} testID={testID}>
|
|
116
|
+
<AboutHeaderSection
|
|
54
117
|
appInfo={appInfo}
|
|
55
|
-
|
|
118
|
+
config={config}
|
|
119
|
+
showHeader={showHeader}
|
|
120
|
+
headerComponent={headerComponent}
|
|
121
|
+
headerStyle={headerStyle}
|
|
56
122
|
titleStyle={titleStyle}
|
|
57
123
|
versionStyle={versionStyle}
|
|
58
|
-
versionPrefix={config.texts?.versionPrefix}
|
|
59
124
|
/>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (!footerComponent) {
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return (
|
|
70
|
-
<View style={[styles.footer, { borderTopColor: colors.border }]}>
|
|
71
|
-
{footerComponent}
|
|
72
|
-
</View>
|
|
73
|
-
);
|
|
74
|
-
}, [footerComponent, colors.border, styles]);
|
|
75
|
-
|
|
76
|
-
// Memoize content rendering
|
|
77
|
-
const renderContent = useCallback(() => {
|
|
78
|
-
if (!appInfo) {
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return (
|
|
83
|
-
<AboutContent
|
|
84
|
-
appInfo={appInfo}
|
|
85
|
-
config={config}
|
|
125
|
+
<AboutContentSection appInfo={appInfo} config={config} />
|
|
126
|
+
<AboutFooterSection
|
|
127
|
+
footerComponent={footerComponent}
|
|
128
|
+
borderColor={colors.border}
|
|
129
|
+
footerStyle={styles.footer}
|
|
86
130
|
/>
|
|
87
|
-
);
|
|
88
|
-
}, [appInfo, config]);
|
|
89
|
-
|
|
90
|
-
// Memoize container style to prevent unnecessary re-renders
|
|
91
|
-
const containerStyles = useMemo(() => {
|
|
92
|
-
return [
|
|
93
|
-
styles.container,
|
|
94
|
-
{ backgroundColor: colors.backgroundPrimary },
|
|
95
|
-
containerStyle
|
|
96
|
-
];
|
|
97
|
-
}, [containerStyle, colors.backgroundPrimary, styles]);
|
|
98
|
-
|
|
99
|
-
return (
|
|
100
|
-
<View
|
|
101
|
-
style={containerStyles}
|
|
102
|
-
testID={testID}
|
|
103
|
-
>
|
|
104
|
-
{renderHeader()}
|
|
105
|
-
{renderContent()}
|
|
106
|
-
{renderFooter()}
|
|
107
131
|
</View>
|
|
108
132
|
);
|
|
109
133
|
};
|
|
@@ -64,26 +64,3 @@ export const DEFAULT_ACCENT_COLORS: ColorPalette = {
|
|
|
64
64
|
],
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
/**
|
|
68
|
-
* Generate custom color palette
|
|
69
|
-
* @param baseColor - Base color to generate variations from
|
|
70
|
-
* @param count - Number of variations to generate
|
|
71
|
-
* @returns Color palette with variations
|
|
72
|
-
*/
|
|
73
|
-
export const generateColorPalette = (
|
|
74
|
-
baseColor: string,
|
|
75
|
-
count: number = 8
|
|
76
|
-
): ColorPalette => {
|
|
77
|
-
const colors: string[] = [];
|
|
78
|
-
|
|
79
|
-
for (let i = 0; i < count; i++) {
|
|
80
|
-
// Simple color variation logic - in real implementation,
|
|
81
|
-
// this would use HSL/HSV color space manipulation
|
|
82
|
-
colors.push(baseColor);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
name: "custom",
|
|
87
|
-
colors,
|
|
88
|
-
};
|
|
89
|
-
};
|
|
@@ -81,8 +81,6 @@ export const CustomColorsSection: React.FC<CustomColorsSectionProps> = ({
|
|
|
81
81
|
// Memoize styles to prevent unnecessary re-creation
|
|
82
82
|
const styles = useMemo(() => getStyles(tokens), [tokens]);
|
|
83
83
|
|
|
84
|
-
// Memoize color fields to prevent unnecessary re-renders
|
|
85
|
-
const colorFieldsMemo = useMemo(() => colorFields, [colorFields]);
|
|
86
84
|
|
|
87
85
|
// Stable callback for color change to prevent infinite re-renders
|
|
88
86
|
const handleColorChange = useCallback((key: keyof CustomThemeColors, color: string) => {
|
|
@@ -104,7 +102,7 @@ export const CustomColorsSection: React.FC<CustomColorsSectionProps> = ({
|
|
|
104
102
|
|
|
105
103
|
// Memoize color pickers to prevent unnecessary re-renders
|
|
106
104
|
const colorPickers = useMemo(() => {
|
|
107
|
-
return
|
|
105
|
+
return colorFields.map((field) => (
|
|
108
106
|
<ColorPicker
|
|
109
107
|
key={field.key}
|
|
110
108
|
label={field.label}
|
|
@@ -113,7 +111,7 @@ export const CustomColorsSection: React.FC<CustomColorsSectionProps> = ({
|
|
|
113
111
|
colors={field.colorPalette}
|
|
114
112
|
/>
|
|
115
113
|
));
|
|
116
|
-
}, [
|
|
114
|
+
}, [colorFields, localCustomColors, handleColorChange]);
|
|
117
115
|
|
|
118
116
|
// Memoize reset button to prevent unnecessary re-renders
|
|
119
117
|
const resetButton = useMemo(() => {
|
|
@@ -113,9 +113,9 @@ export const ThemeOption: React.FC<ThemeOptionProps> = React.memo(({
|
|
|
113
113
|
{featuresTitle}
|
|
114
114
|
</AtomicText>
|
|
115
115
|
) : null}
|
|
116
|
-
{features.map((feature
|
|
116
|
+
{features.map((feature) => (
|
|
117
117
|
<AtomicText
|
|
118
|
-
key={
|
|
118
|
+
key={feature}
|
|
119
119
|
type="bodySmall"
|
|
120
120
|
color="secondary"
|
|
121
121
|
style={styles.feature}
|
|
@@ -39,11 +39,14 @@ export interface DevSettingsProps {
|
|
|
39
39
|
customDevComponents?: React.ReactNode[];
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
const EMPTY_TEXTS: Partial<typeof DEFAULT_TEXTS> = {};
|
|
43
|
+
const EMPTY_DEV_COMPONENTS: React.ReactNode[] = [];
|
|
44
|
+
|
|
42
45
|
export const DevSettingsSection: React.FC<DevSettingsProps> = ({
|
|
43
46
|
enabled = true,
|
|
44
47
|
onAfterClear,
|
|
45
|
-
texts =
|
|
46
|
-
customDevComponents =
|
|
48
|
+
texts = EMPTY_TEXTS,
|
|
49
|
+
customDevComponents = EMPTY_DEV_COMPONENTS,
|
|
47
50
|
}) => {
|
|
48
51
|
const tokens = useAppDesignTokens();
|
|
49
52
|
|
|
@@ -122,37 +122,31 @@ export const FAQScreen: React.FC<FAQScreenProps> = ({
|
|
|
122
122
|
<View style={{ height: tokens.spacing.xl * 2 }} />
|
|
123
123
|
), [tokens.spacing.xl]);
|
|
124
124
|
|
|
125
|
-
const renderContent = () => {
|
|
126
|
-
return (
|
|
127
|
-
<View style={{ flex: 1 }}>
|
|
128
|
-
<FlatList
|
|
129
|
-
data={filteredCategories}
|
|
130
|
-
renderItem={renderCategory}
|
|
131
|
-
keyExtractor={keyExtractor}
|
|
132
|
-
ListHeaderComponent={renderListHeader}
|
|
133
|
-
ListEmptyComponent={renderListEmpty}
|
|
134
|
-
ListFooterComponent={renderListFooter}
|
|
135
|
-
style={[styles.content, customStyles?.content]}
|
|
136
|
-
contentContainerStyle={{ paddingVertical: tokens.spacing.md }}
|
|
137
|
-
showsVerticalScrollIndicator={false}
|
|
138
|
-
initialNumToRender={5}
|
|
139
|
-
maxToRenderPerBatch={5}
|
|
140
|
-
windowSize={10}
|
|
141
|
-
removeClippedSubviews={true}
|
|
142
|
-
/>
|
|
143
|
-
</View>
|
|
144
|
-
);
|
|
145
|
-
};
|
|
146
|
-
|
|
147
125
|
return (
|
|
148
|
-
<ScreenLayout
|
|
149
|
-
edges={['bottom']}
|
|
126
|
+
<ScreenLayout
|
|
127
|
+
edges={['bottom']}
|
|
150
128
|
scrollable={false}
|
|
151
129
|
header={header}
|
|
152
130
|
>
|
|
153
131
|
<View style={[styles.container, customStyles?.container]}>
|
|
154
132
|
<View style={{ alignSelf: 'center', width: '100%', maxWidth: contentMaxWidth, flex: 1 }}>
|
|
155
|
-
{
|
|
133
|
+
<View style={{ flex: 1 }}>
|
|
134
|
+
<FlatList
|
|
135
|
+
data={filteredCategories}
|
|
136
|
+
renderItem={renderCategory}
|
|
137
|
+
keyExtractor={keyExtractor}
|
|
138
|
+
ListHeaderComponent={renderListHeader}
|
|
139
|
+
ListEmptyComponent={renderListEmpty}
|
|
140
|
+
ListFooterComponent={renderListFooter}
|
|
141
|
+
style={[styles.content, customStyles?.content]}
|
|
142
|
+
contentContainerStyle={{ paddingVertical: tokens.spacing.md }}
|
|
143
|
+
showsVerticalScrollIndicator={false}
|
|
144
|
+
initialNumToRender={5}
|
|
145
|
+
maxToRenderPerBatch={5}
|
|
146
|
+
windowSize={10}
|
|
147
|
+
removeClippedSubviews={true}
|
|
148
|
+
/>
|
|
149
|
+
</View>
|
|
156
150
|
</View>
|
|
157
151
|
</View>
|
|
158
152
|
</ScreenLayout>
|
|
@@ -3,13 +3,140 @@
|
|
|
3
3
|
* Form for submitting user feedback with type, rating, and description
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React, {
|
|
6
|
+
import React, { useReducer } from "react";
|
|
7
7
|
import { View, TouchableOpacity, ScrollView, TextInput } from "react-native";
|
|
8
8
|
import { useAppDesignTokens, AtomicText, AtomicButton, AtomicIcon } from "@umituz/react-native-design-system";
|
|
9
9
|
import type { FeedbackType, FeedbackRating } from "../../domain/entities/FeedbackEntity";
|
|
10
10
|
import { validateFeedbackForm } from "../../../../infrastructure/utils/validation";
|
|
11
11
|
import type { FeedbackFormProps } from "./FeedbackFormProps";
|
|
12
12
|
import { getFeedbackFormStyles as getStyles } from "./FeedbackForm.styles";
|
|
13
|
+
import type { DesignTokens } from "@umituz/react-native-design-system";
|
|
14
|
+
|
|
15
|
+
interface FeedbackFormState {
|
|
16
|
+
selectedType: FeedbackType;
|
|
17
|
+
rating: FeedbackRating;
|
|
18
|
+
description: string;
|
|
19
|
+
title: string;
|
|
20
|
+
error: string | null;
|
|
21
|
+
isSubmittingLocal: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type FeedbackFormAction =
|
|
25
|
+
| { type: 'SET_TYPE'; payload: FeedbackType }
|
|
26
|
+
| { type: 'SET_RATING'; payload: FeedbackRating }
|
|
27
|
+
| { type: 'SET_DESCRIPTION'; payload: string }
|
|
28
|
+
| { type: 'SET_TITLE'; payload: string }
|
|
29
|
+
| { type: 'SET_ERROR'; payload: string | null }
|
|
30
|
+
| { type: 'SET_SUBMITTING'; payload: boolean }
|
|
31
|
+
| { type: 'RESET_FORM'; payload: FeedbackRating };
|
|
32
|
+
|
|
33
|
+
function feedbackFormReducer(state: FeedbackFormState, action: FeedbackFormAction): FeedbackFormState {
|
|
34
|
+
switch (action.type) {
|
|
35
|
+
case 'SET_TYPE':
|
|
36
|
+
return { ...state, selectedType: action.payload };
|
|
37
|
+
case 'SET_RATING':
|
|
38
|
+
return { ...state, rating: action.payload };
|
|
39
|
+
case 'SET_DESCRIPTION':
|
|
40
|
+
return { ...state, description: action.payload, error: null };
|
|
41
|
+
case 'SET_TITLE':
|
|
42
|
+
return { ...state, title: action.payload };
|
|
43
|
+
case 'SET_ERROR':
|
|
44
|
+
return { ...state, error: action.payload };
|
|
45
|
+
case 'SET_SUBMITTING':
|
|
46
|
+
return { ...state, isSubmittingLocal: action.payload };
|
|
47
|
+
case 'RESET_FORM':
|
|
48
|
+
return { ...state, description: "", title: "", rating: action.payload };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const STAR_VALUES = [1, 2, 3, 4, 5] as const;
|
|
53
|
+
|
|
54
|
+
interface FeedbackRatingSectionProps {
|
|
55
|
+
rating: FeedbackRating;
|
|
56
|
+
onRatingChange: (star: FeedbackRating) => void;
|
|
57
|
+
ratingLabel: string;
|
|
58
|
+
styles: ReturnType<typeof getStyles>;
|
|
59
|
+
tokens: DesignTokens;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const FeedbackRatingSection: React.FC<FeedbackRatingSectionProps> = ({
|
|
63
|
+
rating,
|
|
64
|
+
onRatingChange,
|
|
65
|
+
ratingLabel,
|
|
66
|
+
styles,
|
|
67
|
+
tokens,
|
|
68
|
+
}) => (
|
|
69
|
+
<View style={styles.ratingContainer}>
|
|
70
|
+
<AtomicText type="bodyMedium" style={{ marginBottom: 8, color: tokens.colors.textSecondary }}>
|
|
71
|
+
{ratingLabel}
|
|
72
|
+
</AtomicText>
|
|
73
|
+
<View style={styles.stars}>
|
|
74
|
+
{STAR_VALUES.map((star) => (
|
|
75
|
+
<TouchableOpacity
|
|
76
|
+
key={star}
|
|
77
|
+
onPress={() => onRatingChange(star as FeedbackRating)}
|
|
78
|
+
style={styles.starButton}
|
|
79
|
+
>
|
|
80
|
+
<AtomicIcon
|
|
81
|
+
name={star <= rating ? "star" : "star-outline"}
|
|
82
|
+
customSize={32}
|
|
83
|
+
customColor={star <= rating ? tokens.colors.warning : tokens.colors.border}
|
|
84
|
+
/>
|
|
85
|
+
</TouchableOpacity>
|
|
86
|
+
))}
|
|
87
|
+
</View>
|
|
88
|
+
</View>
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
interface FeedbackTypeSelectorProps {
|
|
92
|
+
feedbackTypes: Array<{ type: FeedbackType; label: string }>;
|
|
93
|
+
selectedType: FeedbackType;
|
|
94
|
+
onTypeSelect: (type: FeedbackType) => void;
|
|
95
|
+
styles: ReturnType<typeof getStyles>;
|
|
96
|
+
tokens: DesignTokens;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const FeedbackTypeSelector: React.FC<FeedbackTypeSelectorProps> = ({
|
|
100
|
+
feedbackTypes,
|
|
101
|
+
selectedType,
|
|
102
|
+
onTypeSelect,
|
|
103
|
+
styles,
|
|
104
|
+
tokens,
|
|
105
|
+
}) => (
|
|
106
|
+
<ScrollView
|
|
107
|
+
horizontal
|
|
108
|
+
showsHorizontalScrollIndicator={false}
|
|
109
|
+
contentContainerStyle={styles.typeScroll}
|
|
110
|
+
style={styles.typeContainer}
|
|
111
|
+
>
|
|
112
|
+
{feedbackTypes.map((item) => {
|
|
113
|
+
const isSelected = selectedType === item.type;
|
|
114
|
+
return (
|
|
115
|
+
<TouchableOpacity
|
|
116
|
+
key={item.type}
|
|
117
|
+
style={[
|
|
118
|
+
styles.typeButton,
|
|
119
|
+
{
|
|
120
|
+
backgroundColor: isSelected ? tokens.colors.primary : tokens.colors.surface,
|
|
121
|
+
borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
|
|
122
|
+
},
|
|
123
|
+
]}
|
|
124
|
+
onPress={() => onTypeSelect(item.type)}
|
|
125
|
+
>
|
|
126
|
+
<AtomicText
|
|
127
|
+
type="bodySmall"
|
|
128
|
+
style={{
|
|
129
|
+
color: isSelected ? tokens.colors.onPrimary : tokens.colors.textSecondary,
|
|
130
|
+
fontWeight: isSelected ? "600" : "400",
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
{item.label}
|
|
134
|
+
</AtomicText>
|
|
135
|
+
</TouchableOpacity>
|
|
136
|
+
);
|
|
137
|
+
})}
|
|
138
|
+
</ScrollView>
|
|
139
|
+
);
|
|
13
140
|
|
|
14
141
|
export const FeedbackForm: React.FC<FeedbackFormProps> = ({
|
|
15
142
|
onSubmit,
|
|
@@ -19,15 +146,19 @@ export const FeedbackForm: React.FC<FeedbackFormProps> = ({
|
|
|
19
146
|
}) => {
|
|
20
147
|
const tokens = useAppDesignTokens();
|
|
21
148
|
const styles = getStyles(tokens);
|
|
22
|
-
|
|
23
|
-
const [
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
149
|
+
|
|
150
|
+
const [state, dispatch] = useReducer(feedbackFormReducer, {
|
|
151
|
+
selectedType: initialType || texts.feedbackTypes[0].type,
|
|
152
|
+
rating: 5,
|
|
153
|
+
description: "",
|
|
154
|
+
title: "",
|
|
155
|
+
error: null,
|
|
156
|
+
isSubmittingLocal: false,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const { selectedType, rating, description, title, error, isSubmittingLocal } = state;
|
|
28
160
|
|
|
29
161
|
const handleSubmit = async () => {
|
|
30
|
-
// Validate using centralized validation
|
|
31
162
|
const validationResult = validateFeedbackForm({
|
|
32
163
|
type: selectedType,
|
|
33
164
|
rating,
|
|
@@ -35,12 +166,12 @@ export const FeedbackForm: React.FC<FeedbackFormProps> = ({
|
|
|
35
166
|
});
|
|
36
167
|
|
|
37
168
|
if (!validationResult.isValid) {
|
|
38
|
-
|
|
169
|
+
dispatch({ type: 'SET_ERROR', payload: validationResult.error || "Validation failed" });
|
|
39
170
|
return;
|
|
40
171
|
}
|
|
41
172
|
|
|
42
|
-
|
|
43
|
-
|
|
173
|
+
dispatch({ type: 'SET_SUBMITTING', payload: true });
|
|
174
|
+
dispatch({ type: 'SET_ERROR', payload: null });
|
|
44
175
|
|
|
45
176
|
try {
|
|
46
177
|
await onSubmit({
|
|
@@ -50,90 +181,38 @@ export const FeedbackForm: React.FC<FeedbackFormProps> = ({
|
|
|
50
181
|
title: title || texts.defaultTitle(selectedType),
|
|
51
182
|
});
|
|
52
183
|
|
|
53
|
-
|
|
54
|
-
setDescription("");
|
|
55
|
-
setTitle("");
|
|
56
|
-
setRating(5);
|
|
184
|
+
dispatch({ type: 'RESET_FORM', payload: 5 });
|
|
57
185
|
} catch (err) {
|
|
58
186
|
const errorMessage = err instanceof Error ? err.message : "Failed to submit feedback";
|
|
59
|
-
|
|
187
|
+
dispatch({ type: 'SET_ERROR', payload: errorMessage });
|
|
60
188
|
} finally {
|
|
61
|
-
|
|
189
|
+
dispatch({ type: 'SET_SUBMITTING', payload: false });
|
|
62
190
|
}
|
|
63
191
|
};
|
|
64
192
|
|
|
65
|
-
const renderRating = () => (
|
|
66
|
-
<View style={styles.ratingContainer}>
|
|
67
|
-
<AtomicText type="bodyMedium" style={{ marginBottom: 8, color: tokens.colors.textSecondary }}>
|
|
68
|
-
{texts.ratingLabel}
|
|
69
|
-
</AtomicText>
|
|
70
|
-
<View style={styles.stars}>
|
|
71
|
-
{[1, 2, 3, 4, 5].map((star) => (
|
|
72
|
-
<TouchableOpacity
|
|
73
|
-
key={star}
|
|
74
|
-
onPress={() => setRating(star as FeedbackRating)}
|
|
75
|
-
style={styles.starButton}
|
|
76
|
-
>
|
|
77
|
-
<AtomicIcon
|
|
78
|
-
name={star <= rating ? "star" : "star-outline"}
|
|
79
|
-
customSize={32}
|
|
80
|
-
customColor={star <= rating ? tokens.colors.warning : tokens.colors.border}
|
|
81
|
-
/>
|
|
82
|
-
</TouchableOpacity>
|
|
83
|
-
))}
|
|
84
|
-
</View>
|
|
85
|
-
</View>
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
const renderTypeSelector = () => (
|
|
89
|
-
<ScrollView
|
|
90
|
-
horizontal
|
|
91
|
-
showsHorizontalScrollIndicator={false}
|
|
92
|
-
contentContainerStyle={styles.typeScroll}
|
|
93
|
-
style={styles.typeContainer}
|
|
94
|
-
>
|
|
95
|
-
{texts.feedbackTypes.map((item) => {
|
|
96
|
-
const isSelected = selectedType === item.type;
|
|
97
|
-
|
|
98
|
-
return (
|
|
99
|
-
<TouchableOpacity
|
|
100
|
-
key={item.type}
|
|
101
|
-
style={[
|
|
102
|
-
styles.typeButton,
|
|
103
|
-
{
|
|
104
|
-
backgroundColor: isSelected ? tokens.colors.primary : tokens.colors.surface,
|
|
105
|
-
borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
|
|
106
|
-
},
|
|
107
|
-
]}
|
|
108
|
-
onPress={() => setSelectedType(item.type)}
|
|
109
|
-
>
|
|
110
|
-
<AtomicText
|
|
111
|
-
type="bodySmall"
|
|
112
|
-
style={{
|
|
113
|
-
color: isSelected ? tokens.colors.onPrimary : tokens.colors.textSecondary,
|
|
114
|
-
fontWeight: isSelected ? "600" : "400",
|
|
115
|
-
}}
|
|
116
|
-
>
|
|
117
|
-
{item.label}
|
|
118
|
-
</AtomicText>
|
|
119
|
-
</TouchableOpacity>
|
|
120
|
-
);
|
|
121
|
-
})}
|
|
122
|
-
</ScrollView>
|
|
123
|
-
);
|
|
124
|
-
|
|
125
193
|
return (
|
|
126
194
|
<View style={styles.container}>
|
|
127
|
-
|
|
195
|
+
<FeedbackTypeSelector
|
|
196
|
+
feedbackTypes={texts.feedbackTypes}
|
|
197
|
+
selectedType={selectedType}
|
|
198
|
+
onTypeSelect={(type: FeedbackType) => dispatch({ type: 'SET_TYPE', payload: type })}
|
|
199
|
+
styles={styles}
|
|
200
|
+
tokens={tokens}
|
|
201
|
+
/>
|
|
128
202
|
|
|
129
|
-
|
|
203
|
+
<FeedbackRatingSection
|
|
204
|
+
rating={rating}
|
|
205
|
+
onRatingChange={(star: FeedbackRating) => dispatch({ type: 'SET_RATING', payload: star })}
|
|
206
|
+
ratingLabel={texts.ratingLabel}
|
|
207
|
+
styles={styles}
|
|
208
|
+
tokens={tokens}
|
|
209
|
+
/>
|
|
130
210
|
|
|
131
211
|
<View style={styles.inputContainer}>
|
|
132
212
|
<TextInput
|
|
133
213
|
value={description}
|
|
134
214
|
onChangeText={(text) => {
|
|
135
|
-
|
|
136
|
-
setError(null); // Clear error on input
|
|
215
|
+
dispatch({ type: 'SET_DESCRIPTION', payload: text });
|
|
137
216
|
}}
|
|
138
217
|
placeholder={texts.descriptionPlaceholder}
|
|
139
218
|
placeholderTextColor={tokens.colors.textTertiary}
|