@umituz/react-native-settings 4.23.83 → 4.23.85
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 +1 -1
- package/src/domains/about/presentation/hooks/useAboutInfo.ts +0 -2
- package/src/domains/appearance/presentation/components/ColorPicker.tsx +0 -1
- package/src/domains/appearance/presentation/components/ThemeModeSection.tsx +0 -1
- package/src/domains/appearance/presentation/components/ThemeOption.tsx +0 -1
- package/src/domains/feedback/presentation/components/FeedbackForm.tsx +0 -1
- package/src/domains/feedback/presentation/components/FeedbackModal.tsx +16 -19
- package/src/domains/feedback/presentation/components/SupportSection.tsx +0 -1
- package/src/domains/localization/infrastructure/components/useLanguageSwitcher.ts +0 -1
- package/src/domains/localization/infrastructure/hooks/useLanguageSelection.ts +0 -4
- package/src/domains/localization/infrastructure/hooks/useTranslation.ts +0 -1
- package/src/domains/localization/infrastructure/storage/LanguageInitializer.ts +0 -1
- package/src/domains/localization/infrastructure/storage/LanguageSwitcher.ts +0 -4
- package/src/domains/localization/infrastructure/storage/LocalizationStore.ts +0 -5
- package/src/domains/localization/presentation/components/LanguageItem.tsx +0 -1
- package/src/domains/localization/presentation/providers/LocalizationManager.tsx +0 -1
- package/src/domains/localization/presentation/screens/LanguageSelectionScreen.tsx +0 -3
- package/src/domains/notifications/infrastructure/services/NotificationService.ts +0 -1
- package/src/domains/notifications/infrastructure/utils/dev.ts +0 -3
- package/src/domains/notifications/quietHours/infrastructure/hooks/useQuietHoursActions.ts +0 -1
- package/src/domains/notifications/reminders/infrastructure/hooks/useReminderActions.ts +0 -6
- package/src/domains/notifications/reminders/presentation/components/ReminderForm.tsx +0 -5
- package/src/domains/rating/application/services/RatingService.ts +13 -63
- package/src/domains/rating/infrastructure/storage/RatingStorage.ts +14 -36
- package/src/domains/rating/presentation/hooks/useAppRating.tsx +0 -1
- package/src/domains/video-tutorials/presentation/components/VideoTutorialCard.tsx +17 -14
- package/src/domains/video-tutorials/presentation/screens/VideoTutorialsScreen.tsx +46 -32
- package/src/infrastructure/utils/dateUtils.ts +61 -0
- package/src/infrastructure/utils/memoComparisonUtils.ts +68 -0
- package/src/infrastructure/utils/sanitizers.ts +49 -0
- package/src/infrastructure/utils/translationHelpers.ts +81 -0
- package/src/infrastructure/utils/validators.ts +59 -0
- package/src/presentation/components/SettingsItemCard.tsx +129 -172
- package/src/presentation/components/settings/SettingsItemCardContent.tsx +70 -0
- package/src/presentation/components/settings/SettingsItemCardRightElement.tsx +42 -0
- package/src/presentation/components/settings/SettingsItemCardSection.tsx +29 -0
- package/src/presentation/hooks/useSettingsScreenConfig.ts +19 -54
- package/src/presentation/navigation/hooks/useSettingsScreens.ts +75 -89
- package/src/presentation/screens/components/GamificationSettingsItem.tsx +20 -17
- package/src/presentation/screens/components/SettingsContent.tsx +29 -0
- package/src/presentation/screens/components/SubscriptionSettingsItem.tsx +13 -4
- package/src/presentation/screens/components/VideoTutorialSettingsItem.tsx +47 -0
- package/src/presentation/screens/components/WalletSettingsItem.tsx +13 -4
- package/src/presentation/screens/components/sections/CustomSettingsList.tsx +12 -5
- package/src/presentation/screens/components/sections/FeatureSettingsSection.tsx +12 -11
- package/src/presentation/screens/components/sections/IdentitySettingsSection.tsx +4 -1
- package/src/presentation/screens/components/sections/ProfileSectionLoader.tsx +16 -10
- package/src/presentation/screens/components/sections/SupportSettingsSection.tsx +9 -4
- package/src/presentation/screens/hooks/useFeatureDetection.ts +2 -0
- package/src/presentation/screens/types/SettingsConfig.ts +8 -1
- package/src/presentation/screens/types/UserFeatureConfig.ts +20 -0
- package/src/presentation/screens/types/index.ts +1 -0
- package/src/presentation/screens/utils/normalizeConfig.ts +7 -1
- package/src/presentation/utils/accountConfigUtils.ts +67 -0
- package/src/presentation/utils/screenFactory.ts +87 -0
- package/src/presentation/utils/userProfileUtils.ts +51 -0
|
@@ -44,18 +44,19 @@ export const VideoTutorialCard: React.FC<VideoTutorialCardProps> = ({
|
|
|
44
44
|
style={[styles.thumbnail, horizontal && styles.horizontalThumbnail]}
|
|
45
45
|
resizeMode="cover"
|
|
46
46
|
/>
|
|
47
|
-
<View style={
|
|
48
|
-
<AtomicText style={styles.durationText}>{formatDuration(tutorial.duration)}</AtomicText>
|
|
47
|
+
<View style={styles.durationBadge}>
|
|
48
|
+
<AtomicText type="labelSmall" style={styles.durationText}>{formatDuration(tutorial.duration)}</AtomicText>
|
|
49
49
|
</View>
|
|
50
50
|
{tutorial.featured && (
|
|
51
51
|
<View style={[styles.featuredBadge, { backgroundColor: tokens.colors.primary }]}>
|
|
52
|
-
<AtomicText style={[styles.featuredText, { color: tokens.colors.onPrimary }]}>Featured</AtomicText>
|
|
52
|
+
<AtomicText type="labelSmall" style={[styles.featuredText, { color: tokens.colors.onPrimary }]}>Featured</AtomicText>
|
|
53
53
|
</View>
|
|
54
54
|
)}
|
|
55
55
|
</View>
|
|
56
56
|
|
|
57
57
|
<View style={styles.content}>
|
|
58
58
|
<AtomicText
|
|
59
|
+
type={horizontal ? "titleMedium" : "titleLarge"}
|
|
59
60
|
style={[styles.title, horizontal && styles.horizontalTitle]}
|
|
60
61
|
numberOfLines={2}
|
|
61
62
|
>
|
|
@@ -63,6 +64,8 @@ export const VideoTutorialCard: React.FC<VideoTutorialCardProps> = ({
|
|
|
63
64
|
</AtomicText>
|
|
64
65
|
|
|
65
66
|
<AtomicText
|
|
67
|
+
type={horizontal ? "bodySmall" : "bodyMedium"}
|
|
68
|
+
color="textSecondary"
|
|
66
69
|
style={[styles.description, horizontal && styles.horizontalDescription]}
|
|
67
70
|
numberOfLines={horizontal ? 2 : 3}
|
|
68
71
|
>
|
|
@@ -70,10 +73,10 @@ export const VideoTutorialCard: React.FC<VideoTutorialCardProps> = ({
|
|
|
70
73
|
</AtomicText>
|
|
71
74
|
|
|
72
75
|
<View style={styles.metadata}>
|
|
73
|
-
<AtomicText style={styles.category}>
|
|
76
|
+
<AtomicText type="labelSmall" color="textTertiary" style={styles.category}>
|
|
74
77
|
{tutorial.category.replace("-", " ")}
|
|
75
78
|
</AtomicText>
|
|
76
|
-
<AtomicText style={styles.difficulty}>
|
|
79
|
+
<AtomicText type="labelSmall" color="textSecondary" style={styles.difficulty}>
|
|
77
80
|
{tutorial.difficulty}
|
|
78
81
|
</AtomicText>
|
|
79
82
|
</View>
|
|
@@ -88,16 +91,16 @@ const getStyles = (tokens: ReturnType<typeof useAppDesignTokens>) => StyleSheet.
|
|
|
88
91
|
imageContainer: { position: "relative" },
|
|
89
92
|
thumbnail: { width: "100%", height: 180 },
|
|
90
93
|
horizontalThumbnail: { height: 140 },
|
|
91
|
-
durationBadge: { position: "absolute", bottom: 8, right: 8, paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4 },
|
|
92
|
-
durationText: { color: tokens.colors.textInverse
|
|
94
|
+
durationBadge: { position: "absolute", bottom: 8, right: 8, paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4, backgroundColor: "rgba(0,0,0,0.7)" },
|
|
95
|
+
durationText: { color: tokens.colors.textInverse },
|
|
93
96
|
featuredBadge: { position: "absolute", top: 8, left: 8, paddingHorizontal: 8, paddingVertical: 4, borderRadius: 4 },
|
|
94
|
-
featuredText: {
|
|
97
|
+
featuredText: { fontWeight: "600" },
|
|
95
98
|
content: { padding: 12 },
|
|
96
|
-
title: {
|
|
97
|
-
horizontalTitle: {
|
|
98
|
-
description: {
|
|
99
|
-
horizontalDescription: {
|
|
99
|
+
title: { fontWeight: "600", marginBottom: 6, color: tokens.colors.textPrimary },
|
|
100
|
+
horizontalTitle: {},
|
|
101
|
+
description: { marginBottom: 8 },
|
|
102
|
+
horizontalDescription: { marginBottom: 6 },
|
|
100
103
|
metadata: { flexDirection: "row", justifyContent: "space-between", alignItems: "center" },
|
|
101
|
-
category: {
|
|
102
|
-
difficulty: {
|
|
104
|
+
category: { textTransform: "capitalize", fontWeight: "500" },
|
|
105
|
+
difficulty: { textTransform: "capitalize", fontWeight: "500" },
|
|
103
106
|
});
|
|
@@ -48,6 +48,7 @@ export const VideoTutorialsScreen: React.FC<VideoTutorialsScreenProps> = React.m
|
|
|
48
48
|
onTutorialPress,
|
|
49
49
|
}) => {
|
|
50
50
|
const tokens = useAppDesignTokens();
|
|
51
|
+
const navigation = useAppNavigation();
|
|
51
52
|
const styles = getStyles(tokens);
|
|
52
53
|
|
|
53
54
|
const handleTutorialPress = React.useCallback(
|
|
@@ -69,10 +70,6 @@ export const VideoTutorialsScreen: React.FC<VideoTutorialsScreenProps> = React.m
|
|
|
69
70
|
[handleTutorialPress]
|
|
70
71
|
);
|
|
71
72
|
|
|
72
|
-
const navigation = useAppNavigation();
|
|
73
|
-
|
|
74
|
-
if (isLoading) return <AtomicSpinner size="lg" fullContainer />;
|
|
75
|
-
|
|
76
73
|
const hasFeatured = featuredTutorials && featuredTutorials.length > 0;
|
|
77
74
|
const hasTutorials = tutorials && tutorials.length > 0;
|
|
78
75
|
|
|
@@ -83,21 +80,29 @@ export const VideoTutorialsScreen: React.FC<VideoTutorialsScreenProps> = React.m
|
|
|
83
80
|
/>
|
|
84
81
|
);
|
|
85
82
|
|
|
83
|
+
if (isLoading) {
|
|
84
|
+
return (
|
|
85
|
+
<ScreenLayout header={header}>
|
|
86
|
+
<AtomicSpinner size="lg" fullContainer color="primary" />
|
|
87
|
+
</ScreenLayout>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
86
91
|
if (!hasTutorials && !hasFeatured) {
|
|
87
92
|
return (
|
|
88
|
-
<ScreenLayout header={header}
|
|
93
|
+
<ScreenLayout header={header}>
|
|
89
94
|
<View style={styles.emptyContainer}>
|
|
90
|
-
<AtomicText color="
|
|
95
|
+
<AtomicText color="textSecondary" type="bodyLarge" style={{ textAlign: 'center' }}>{emptyMessage}</AtomicText>
|
|
91
96
|
</View>
|
|
92
97
|
</ScreenLayout>
|
|
93
98
|
);
|
|
94
99
|
}
|
|
95
100
|
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
const ListHeader = () => (
|
|
102
|
+
<>
|
|
98
103
|
{hasFeatured && (
|
|
99
104
|
<View style={styles.section}>
|
|
100
|
-
<AtomicText color="
|
|
105
|
+
<AtomicText color="textSecondary" type="titleLarge" style={styles.sectionTitle}>{featuredTitle}</AtomicText>
|
|
101
106
|
<FlatList
|
|
102
107
|
data={featuredTutorials}
|
|
103
108
|
renderItem={renderFeaturedItem}
|
|
@@ -108,38 +113,47 @@ export const VideoTutorialsScreen: React.FC<VideoTutorialsScreenProps> = React.m
|
|
|
108
113
|
/>
|
|
109
114
|
</View>
|
|
110
115
|
)}
|
|
111
|
-
|
|
112
116
|
{hasTutorials && (
|
|
113
|
-
<
|
|
114
|
-
<AtomicText color="secondary" style={styles.sectionTitle}>{allTutorialsTitle}</AtomicText>
|
|
115
|
-
<FlatList
|
|
116
|
-
data={tutorials}
|
|
117
|
-
renderItem={renderTutorialItem}
|
|
118
|
-
keyExtractor={(item) => item.id}
|
|
119
|
-
showsVerticalScrollIndicator={false}
|
|
120
|
-
contentContainerStyle={styles.verticalList}
|
|
121
|
-
/>
|
|
122
|
-
</View>
|
|
117
|
+
<AtomicText color="textSecondary" type="titleLarge" style={styles.sectionTitle}>{allTutorialsTitle}</AtomicText>
|
|
123
118
|
)}
|
|
119
|
+
</>
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<ScreenLayout header={header} scrollable={false}>
|
|
124
|
+
<FlatList
|
|
125
|
+
data={tutorials}
|
|
126
|
+
renderItem={renderTutorialItem}
|
|
127
|
+
keyExtractor={(item) => item.id}
|
|
128
|
+
showsVerticalScrollIndicator={false}
|
|
129
|
+
contentContainerStyle={styles.verticalList}
|
|
130
|
+
ListHeaderComponent={ListHeader}
|
|
131
|
+
/>
|
|
124
132
|
</ScreenLayout>
|
|
125
133
|
);
|
|
126
134
|
}
|
|
127
135
|
);
|
|
128
136
|
|
|
129
137
|
const getStyles = (tokens: DesignTokens) => StyleSheet.create({
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
color: tokens.colors.textPrimary,
|
|
133
|
-
fontWeight: "600",
|
|
134
|
-
marginBottom: 24,
|
|
138
|
+
section: {
|
|
139
|
+
marginBottom: tokens.spacing.lg,
|
|
135
140
|
},
|
|
136
|
-
section: { marginBottom: 24 },
|
|
137
141
|
sectionTitle: {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
142
|
+
fontWeight: "600",
|
|
143
|
+
marginBottom: tokens.spacing.sm,
|
|
144
|
+
paddingHorizontal: tokens.spacing.md,
|
|
145
|
+
},
|
|
146
|
+
horizontalList: {
|
|
147
|
+
paddingHorizontal: tokens.spacing.md,
|
|
148
|
+
},
|
|
149
|
+
verticalList: {
|
|
150
|
+
paddingBottom: tokens.spacing.xl,
|
|
151
|
+
paddingHorizontal: tokens.spacing.md,
|
|
152
|
+
},
|
|
153
|
+
emptyContainer: {
|
|
154
|
+
flex: 1,
|
|
155
|
+
justifyContent: "center",
|
|
156
|
+
alignItems: "center",
|
|
157
|
+
padding: tokens.spacing.lg,
|
|
141
158
|
},
|
|
142
|
-
horizontalList: { paddingRight: 16 },
|
|
143
|
-
verticalList: { paddingBottom: 16 },
|
|
144
|
-
emptyContainer: { flex: 1, justifyContent: "center", alignItems: "center", padding: 20 },
|
|
145
159
|
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date Utilities
|
|
3
|
+
*
|
|
4
|
+
* Helper functions for date manipulation and calculations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Calculate days between two dates
|
|
9
|
+
* @param dateString ISO date string
|
|
10
|
+
* @param now Current date (defaults to new Date())
|
|
11
|
+
* @returns Number of days between dates
|
|
12
|
+
*/
|
|
13
|
+
export function daysBetween(dateString: string, now: Date = new Date()): number {
|
|
14
|
+
const date = new Date(dateString);
|
|
15
|
+
const diffMs = now.getTime() - date.getTime();
|
|
16
|
+
return Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Format date to ISO string
|
|
21
|
+
* @param date Date to format
|
|
22
|
+
* @returns ISO date string
|
|
23
|
+
*/
|
|
24
|
+
export function toISOString(date: Date = new Date()): string {
|
|
25
|
+
return date.toISOString();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if a date is within the last N days
|
|
30
|
+
* @param dateString ISO date string to check
|
|
31
|
+
* @param days Number of days to check within
|
|
32
|
+
* @returns true if date is within the last N days
|
|
33
|
+
*/
|
|
34
|
+
export function isWithinLastDays(dateString: string, days: number): boolean {
|
|
35
|
+
const daysDiff = daysBetween(dateString);
|
|
36
|
+
return daysDiff >= 0 && daysDiff <= days;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if a date has passed (is older than N days ago)
|
|
41
|
+
* @param dateString ISO date string to check
|
|
42
|
+
* @param days Number of days threshold
|
|
43
|
+
* @returns true if date is older than N days
|
|
44
|
+
*/
|
|
45
|
+
export function isOlderThanDays(dateString: string, days: number): boolean {
|
|
46
|
+
const daysDiff = daysBetween(dateString);
|
|
47
|
+
return daysDiff > days;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Add days to a date
|
|
52
|
+
* @param dateString ISO date string
|
|
53
|
+
* @param days Number of days to add
|
|
54
|
+
* @returns New date with days added
|
|
55
|
+
*/
|
|
56
|
+
export function addDays(dateString: string, days: number): Date {
|
|
57
|
+
const date = new Date(dateString);
|
|
58
|
+
const result = new Date(date);
|
|
59
|
+
result.setDate(result.getDate() + days);
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memo Comparison Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides reusable comparison functions for React.memo to reduce code duplication.
|
|
5
|
+
* These functions follow best practices for shallow comparison in React components.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ComponentType } from "react";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Standard comparison for components with config and translation props
|
|
12
|
+
* @param prevProps Previous props
|
|
13
|
+
* @param nextProps Next props
|
|
14
|
+
* @returns true if props are equal (should not re-render)
|
|
15
|
+
*/
|
|
16
|
+
export function compareConfigAndTranslate(
|
|
17
|
+
prevProps: Record<string, unknown>,
|
|
18
|
+
nextProps: Record<string, unknown>
|
|
19
|
+
): boolean {
|
|
20
|
+
return (
|
|
21
|
+
prevProps.config === nextProps.config &&
|
|
22
|
+
prevProps.t === nextProps.t
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Standard comparison for components with normalized config and features
|
|
28
|
+
* @param prevProps Previous props
|
|
29
|
+
* @param nextProps Next props
|
|
30
|
+
* @returns true if props are equal (should not re-render)
|
|
31
|
+
*/
|
|
32
|
+
export function compareConfigAndFeatures(
|
|
33
|
+
prevProps: Record<string, unknown>,
|
|
34
|
+
nextProps: Record<string, unknown>
|
|
35
|
+
): boolean {
|
|
36
|
+
return (
|
|
37
|
+
prevProps.normalizedConfig === nextProps.normalizedConfig &&
|
|
38
|
+
prevProps.features === nextProps.features
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Standard comparison for components with single prop
|
|
44
|
+
* @param propKey Property key to compare
|
|
45
|
+
* @returns Comparison function
|
|
46
|
+
*/
|
|
47
|
+
export function createSinglePropComparator<K extends string>(
|
|
48
|
+
propKey: K
|
|
49
|
+
): (prevProps: Record<string, unknown>, nextProps: Record<string, unknown>) => boolean {
|
|
50
|
+
return (prevProps, nextProps) => prevProps[propKey] === nextProps[propKey];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Standard comparison for components with gamification config
|
|
55
|
+
* @param prevProps Previous props
|
|
56
|
+
* @param nextProps Next props
|
|
57
|
+
* @returns true if props are equal (should not re-render)
|
|
58
|
+
*/
|
|
59
|
+
export function compareGamificationProps(
|
|
60
|
+
prevProps: Record<string, unknown>,
|
|
61
|
+
nextProps: Record<string, unknown>
|
|
62
|
+
): boolean {
|
|
63
|
+
return (
|
|
64
|
+
prevProps.config === nextProps.config &&
|
|
65
|
+
prevProps.gamificationConfig === nextProps.gamificationConfig &&
|
|
66
|
+
prevProps.t === nextProps.t
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitization Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides sanitization functions for user input and props.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sanitize string by trimming and limiting length
|
|
9
|
+
*/
|
|
10
|
+
export function sanitizeString(str: string | undefined, maxLength: number): string {
|
|
11
|
+
if (!str) return "";
|
|
12
|
+
return str.trim().slice(0, maxLength);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Sanitize title prop
|
|
17
|
+
*/
|
|
18
|
+
export function sanitizeTitle(title: string): string {
|
|
19
|
+
return sanitizeString(title, 200) || "";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Sanitize description prop
|
|
24
|
+
*/
|
|
25
|
+
export function sanitizeDescription(description: string | undefined): string | undefined {
|
|
26
|
+
return sanitizeString(description || "", 500);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Truncate text with ellipsis
|
|
31
|
+
*/
|
|
32
|
+
export function truncateText(text: string, maxLength: number): string {
|
|
33
|
+
if (text.length <= maxLength) return text;
|
|
34
|
+
return text.slice(0, maxLength - 3) + "...";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Escape HTML entities (for security)
|
|
39
|
+
*/
|
|
40
|
+
export function escapeHtml(text: string): string {
|
|
41
|
+
const htmlEntities: Record<string, string> = {
|
|
42
|
+
"&": "&",
|
|
43
|
+
"<": "<",
|
|
44
|
+
">": ">",
|
|
45
|
+
'"': """,
|
|
46
|
+
"'": "'",
|
|
47
|
+
};
|
|
48
|
+
return text.replace(/[&<>"']/g, (char) => htmlEntities[char] || char);
|
|
49
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translation Helper Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides safe translation utilities that handle cases where
|
|
5
|
+
* the translation function might not be available yet.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type TranslationFunction = (key: string, params?: Record<string, string | number>) => string;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Safely get a translation with fallback
|
|
12
|
+
* @param t Translation function
|
|
13
|
+
* @param key Translation key
|
|
14
|
+
* @param fallback Fallback text if translation fails
|
|
15
|
+
* @returns Translated text or fallback
|
|
16
|
+
*/
|
|
17
|
+
export function getTranslationWithFallback(
|
|
18
|
+
t: TranslationFunction | unknown,
|
|
19
|
+
key: string,
|
|
20
|
+
fallback: string
|
|
21
|
+
): string {
|
|
22
|
+
if (typeof t === "function") {
|
|
23
|
+
try {
|
|
24
|
+
return t(key);
|
|
25
|
+
} catch {
|
|
26
|
+
return fallback;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return fallback;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get translation with parameters support
|
|
34
|
+
* @param t Translation function
|
|
35
|
+
* @param key Translation key
|
|
36
|
+
* @param params Parameters for translation
|
|
37
|
+
* @param fallback Fallback text if translation fails
|
|
38
|
+
* @returns Translated text or fallback
|
|
39
|
+
*/
|
|
40
|
+
export function getTranslationWithParams(
|
|
41
|
+
t: TranslationFunction | unknown,
|
|
42
|
+
key: string,
|
|
43
|
+
params: Record<string, string | number>,
|
|
44
|
+
fallback: string
|
|
45
|
+
): string {
|
|
46
|
+
if (typeof t === "function") {
|
|
47
|
+
try {
|
|
48
|
+
return t(key, params);
|
|
49
|
+
} catch {
|
|
50
|
+
return fallback;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return fallback;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if translation function is available
|
|
58
|
+
* @param t Translation function to check
|
|
59
|
+
* @returns true if translation function is available
|
|
60
|
+
*/
|
|
61
|
+
export function isTranslationFunctionAvailable(t: unknown): t is TranslationFunction {
|
|
62
|
+
return typeof t === "function";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create a safe translation wrapper that always returns a string
|
|
67
|
+
* @param t Translation function
|
|
68
|
+
* @returns Safe translation function
|
|
69
|
+
*/
|
|
70
|
+
export function createSafeTranslator(t: TranslationFunction | unknown): TranslationFunction {
|
|
71
|
+
return (key: string, params?: Record<string, string | number>) => {
|
|
72
|
+
if (isTranslationFunctionAvailable(t)) {
|
|
73
|
+
try {
|
|
74
|
+
return t(key, params);
|
|
75
|
+
} catch {
|
|
76
|
+
return key;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return key;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides validation functions for component props.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface ValidationWarning {
|
|
8
|
+
message: string;
|
|
9
|
+
value: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Validate settings item title
|
|
14
|
+
*/
|
|
15
|
+
export function validateTitle(title: unknown): ValidationWarning | null {
|
|
16
|
+
if (!title || typeof title !== "string") {
|
|
17
|
+
return {
|
|
18
|
+
message: "[SettingsItemCard] Invalid title prop",
|
|
19
|
+
value: title,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validate settings item description
|
|
27
|
+
*/
|
|
28
|
+
export function validateDescription(description: unknown): ValidationWarning | null {
|
|
29
|
+
if (description && typeof description !== "string") {
|
|
30
|
+
return {
|
|
31
|
+
message: "[SettingsItemCard] Invalid description prop",
|
|
32
|
+
value: description,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validate switch props
|
|
40
|
+
*/
|
|
41
|
+
export function validateSwitchProps(
|
|
42
|
+
showSwitch: boolean,
|
|
43
|
+
onSwitchChange?: ((value: boolean) => void) | null
|
|
44
|
+
): ValidationWarning | null {
|
|
45
|
+
if (showSwitch && !onSwitchChange) {
|
|
46
|
+
return {
|
|
47
|
+
message: "[SettingsItemCard] Switch shown but no onSwitchChange provided",
|
|
48
|
+
value: { showSwitch, hasOnSwitchChange: !!onSwitchChange },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Return validation result (no logging)
|
|
56
|
+
*/
|
|
57
|
+
export function getValidationResult(warning: ValidationWarning | null): ValidationWarning | null {
|
|
58
|
+
return warning;
|
|
59
|
+
}
|