@umituz/react-native-settings 4.23.86 → 4.23.88
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/feedback/presentation/components/FeedbackForm.tsx +10 -3
- package/src/domains/gamification/store/gamificationStore.ts +6 -7
- package/src/domains/localization/infrastructure/storage/LocalizationStore.ts +50 -181
- package/src/domains/localization/infrastructure/storage/localizationStoreUtils.ts +182 -0
- package/src/domains/notifications/reminders/presentation/components/ReminderForm.tsx +51 -45
- package/src/infrastructure/types/commonComponentTypes.ts +142 -0
- package/src/infrastructure/utils/async/core.ts +109 -0
- package/src/infrastructure/utils/async/debounceAndBatch.ts +69 -0
- package/src/infrastructure/utils/async/index.ts +8 -0
- package/src/infrastructure/utils/async/retryAndTimeout.ts +57 -0
- package/src/infrastructure/utils/configFactory.ts +101 -0
- package/src/infrastructure/utils/errorHandlers.ts +249 -0
- package/src/infrastructure/utils/index.ts +5 -0
- package/src/infrastructure/utils/memoUtils.ts +10 -2
- package/src/infrastructure/utils/styleTokens.ts +132 -0
- package/src/infrastructure/utils/validation/core.ts +42 -0
- package/src/infrastructure/utils/validation/formValidators.ts +82 -0
- package/src/infrastructure/utils/validation/index.ts +37 -0
- package/src/infrastructure/utils/validation/numericValidators.ts +66 -0
- package/src/infrastructure/utils/validation/passwordValidator.ts +53 -0
- package/src/infrastructure/utils/validation/textValidators.ts +118 -0
- package/src/presentation/hooks/useSettingsScreenConfig.ts +32 -79
- package/src/presentation/navigation/SettingsStackNavigator.tsx +6 -1
- package/src/presentation/utils/config-creators/base-configs.ts +54 -42
- package/src/presentation/utils/faqTranslator.ts +31 -0
- package/src/presentation/utils/index.ts +6 -1
- package/src/presentation/utils/settingsConfigFactory.ts +89 -0
- package/src/presentation/utils/useAuthHandlers.ts +98 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Handling Utilities
|
|
3
|
+
* Centralized error handling and error message generation
|
|
4
|
+
* FIXED: Added safety checks for showToast and proper error handling
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Error types for better error classification
|
|
9
|
+
*/
|
|
10
|
+
export enum ErrorType {
|
|
11
|
+
NETWORK = "NETWORK",
|
|
12
|
+
VALIDATION = "VALIDATION",
|
|
13
|
+
AUTHENTICATION = "AUTHENTICATION",
|
|
14
|
+
AUTHORIZATION = "AUTHORIZATION",
|
|
15
|
+
NOT_FOUND = "NOT_FOUND",
|
|
16
|
+
SERVER = "SERVER",
|
|
17
|
+
UNKNOWN = "UNKNOWN",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Custom error class with error type
|
|
22
|
+
*/
|
|
23
|
+
export class AppError extends Error {
|
|
24
|
+
constructor(
|
|
25
|
+
message: string,
|
|
26
|
+
public type: ErrorType = ErrorType.UNKNOWN,
|
|
27
|
+
public code?: string,
|
|
28
|
+
public statusCode?: number
|
|
29
|
+
) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "AppError";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Error handler result
|
|
37
|
+
*/
|
|
38
|
+
export interface ErrorHandlerResult {
|
|
39
|
+
message: string;
|
|
40
|
+
type: ErrorType;
|
|
41
|
+
shouldShowToUser: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Classify error based on error properties
|
|
46
|
+
*/
|
|
47
|
+
export const classifyError = (error: unknown): ErrorType => {
|
|
48
|
+
if (error instanceof AppError) {
|
|
49
|
+
return error.type;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!(error instanceof Error)) {
|
|
53
|
+
return ErrorType.UNKNOWN;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const errorMessage = error.message.toLowerCase();
|
|
57
|
+
|
|
58
|
+
// Network errors
|
|
59
|
+
if (
|
|
60
|
+
errorMessage.includes("network") ||
|
|
61
|
+
errorMessage.includes("fetch") ||
|
|
62
|
+
errorMessage.includes("connection")
|
|
63
|
+
) {
|
|
64
|
+
return ErrorType.NETWORK;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Authentication errors
|
|
68
|
+
if (
|
|
69
|
+
errorMessage.includes("unauthorized") ||
|
|
70
|
+
errorMessage.includes("unauthenticated") ||
|
|
71
|
+
errorMessage.includes("token") ||
|
|
72
|
+
errorMessage.includes("401")
|
|
73
|
+
) {
|
|
74
|
+
return ErrorType.AUTHENTICATION;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Authorization errors
|
|
78
|
+
if (
|
|
79
|
+
errorMessage.includes("forbidden") ||
|
|
80
|
+
errorMessage.includes("permission") ||
|
|
81
|
+
errorMessage.includes("403")
|
|
82
|
+
) {
|
|
83
|
+
return ErrorType.AUTHORIZATION;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Not found errors
|
|
87
|
+
if (
|
|
88
|
+
errorMessage.includes("not found") ||
|
|
89
|
+
errorMessage.includes("404")
|
|
90
|
+
) {
|
|
91
|
+
return ErrorType.NOT_FOUND;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Server errors
|
|
95
|
+
if (
|
|
96
|
+
errorMessage.includes("500") ||
|
|
97
|
+
errorMessage.includes("502") ||
|
|
98
|
+
errorMessage.includes("503")
|
|
99
|
+
) {
|
|
100
|
+
return ErrorType.SERVER;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return ErrorType.UNKNOWN;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get user-friendly error message
|
|
108
|
+
*/
|
|
109
|
+
export const getUserFriendlyErrorMessage = (
|
|
110
|
+
error: unknown,
|
|
111
|
+
context?: {
|
|
112
|
+
operation?: string;
|
|
113
|
+
entity?: string;
|
|
114
|
+
}
|
|
115
|
+
): ErrorHandlerResult => {
|
|
116
|
+
const errorType = classifyError(error);
|
|
117
|
+
|
|
118
|
+
let message = "An unexpected error occurred";
|
|
119
|
+
let shouldShowToUser = true;
|
|
120
|
+
|
|
121
|
+
switch (errorType) {
|
|
122
|
+
case ErrorType.NETWORK:
|
|
123
|
+
message = "Unable to connect. Please check your internet connection and try again.";
|
|
124
|
+
break;
|
|
125
|
+
|
|
126
|
+
case ErrorType.AUTHENTICATION:
|
|
127
|
+
message = "Your session has expired. Please sign in again.";
|
|
128
|
+
break;
|
|
129
|
+
|
|
130
|
+
case ErrorType.AUTHORIZATION:
|
|
131
|
+
message = "You don't have permission to perform this action.";
|
|
132
|
+
break;
|
|
133
|
+
|
|
134
|
+
case ErrorType.NOT_FOUND:
|
|
135
|
+
if (context?.entity) {
|
|
136
|
+
// FIXED: Sanitize entity name to prevent injection
|
|
137
|
+
const sanitizedEntity = String(context.entity).replace(/[<>]/g, "");
|
|
138
|
+
message = `${sanitizedEntity} not found`;
|
|
139
|
+
} else {
|
|
140
|
+
message = "The requested resource was not found.";
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
|
|
144
|
+
case ErrorType.SERVER:
|
|
145
|
+
message = "Server error. Please try again later.";
|
|
146
|
+
shouldShowToUser = false;
|
|
147
|
+
break;
|
|
148
|
+
|
|
149
|
+
case ErrorType.VALIDATION:
|
|
150
|
+
message = error instanceof Error ? error.message : "Invalid input";
|
|
151
|
+
break;
|
|
152
|
+
|
|
153
|
+
default:
|
|
154
|
+
if (error instanceof Error) {
|
|
155
|
+
message = error.message;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { message, type: errorType, shouldShowToUser };
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Log error appropriately based on type
|
|
164
|
+
*/
|
|
165
|
+
export const logError = (
|
|
166
|
+
error: unknown,
|
|
167
|
+
context?: {
|
|
168
|
+
operation?: string;
|
|
169
|
+
userId?: string;
|
|
170
|
+
additionalInfo?: Record<string, unknown>;
|
|
171
|
+
}
|
|
172
|
+
): void => {
|
|
173
|
+
const errorType = classifyError(error);
|
|
174
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
175
|
+
|
|
176
|
+
const logData = {
|
|
177
|
+
type: errorType,
|
|
178
|
+
message: errorMessage,
|
|
179
|
+
operation: context?.operation,
|
|
180
|
+
userId: context?.userId,
|
|
181
|
+
...context?.additionalInfo,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// In production, send to error tracking service
|
|
185
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
186
|
+
console.error("[Error]", logData);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// TODO: Send to error tracking service in production
|
|
190
|
+
// ErrorTracking.captureException(error, logData);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Handle error with logging and user message
|
|
195
|
+
*/
|
|
196
|
+
export const handleError = (
|
|
197
|
+
error: unknown,
|
|
198
|
+
context?: {
|
|
199
|
+
operation?: string;
|
|
200
|
+
entity?: string;
|
|
201
|
+
userId?: string;
|
|
202
|
+
showToast?: (message: string) => void;
|
|
203
|
+
}
|
|
204
|
+
): ErrorHandlerResult => {
|
|
205
|
+
// Log the error
|
|
206
|
+
logError(error, context);
|
|
207
|
+
|
|
208
|
+
// Get user-friendly message
|
|
209
|
+
const result = getUserFriendlyErrorMessage(error, context);
|
|
210
|
+
|
|
211
|
+
// Show toast if provided and safe to do so
|
|
212
|
+
// FIXED: Added safety check and try-catch for showToast
|
|
213
|
+
if (context?.showToast && result.shouldShowToUser) {
|
|
214
|
+
try {
|
|
215
|
+
if (typeof context.showToast === "function") {
|
|
216
|
+
context.showToast(result.message);
|
|
217
|
+
}
|
|
218
|
+
} catch (toastError) {
|
|
219
|
+
// Log toast error but don't crash error handling
|
|
220
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
221
|
+
console.error("[ErrorHandlers] showToast failed:", toastError);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return result;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Wrap an async function with error handling
|
|
231
|
+
*/
|
|
232
|
+
export const withErrorHandling = async <T>(
|
|
233
|
+
operation: () => Promise<T>,
|
|
234
|
+
options: {
|
|
235
|
+
operation?: string;
|
|
236
|
+
entity?: string;
|
|
237
|
+
showToast?: (message: string) => void;
|
|
238
|
+
onError?: (result: ErrorHandlerResult) => void;
|
|
239
|
+
}
|
|
240
|
+
): Promise<{ success: true; data: T } | { success: false; error: ErrorHandlerResult }> => {
|
|
241
|
+
try {
|
|
242
|
+
const data = await operation();
|
|
243
|
+
return { success: true, data };
|
|
244
|
+
} catch (error) {
|
|
245
|
+
const errorResult = handleError(error, options);
|
|
246
|
+
options?.onError?.(errorResult);
|
|
247
|
+
return { success: false, error: errorResult };
|
|
248
|
+
}
|
|
249
|
+
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Memo Utilities
|
|
3
3
|
* Centralized memoization helpers to reduce code duplication
|
|
4
4
|
*/
|
|
5
|
-
import { useMemo, useCallback, useRef, DependencyList } from 'react';
|
|
5
|
+
import { useMemo, useCallback, useRef, useEffect, DependencyList } from 'react';
|
|
6
6
|
import type { DesignTokens } from '@umituz/react-native-design-system';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -114,7 +114,7 @@ export function useMemoWithEquality<T>(
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
|
-
* Custom hook that creates a debounced callback
|
|
117
|
+
* Custom hook that creates a debounced callback with proper cleanup
|
|
118
118
|
* @param callback Function to debounce
|
|
119
119
|
* @param delay Delay in milliseconds
|
|
120
120
|
* @returns Debounced callback
|
|
@@ -125,6 +125,14 @@ export function useDebouncedCallback<T extends (...args: any[]) => any>(
|
|
|
125
125
|
): T {
|
|
126
126
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
127
127
|
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
return () => {
|
|
130
|
+
if (timeoutRef.current) {
|
|
131
|
+
clearTimeout(timeoutRef.current);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}, []);
|
|
135
|
+
|
|
128
136
|
return useCallback((...args: Parameters<T>) => {
|
|
129
137
|
if (timeoutRef.current) {
|
|
130
138
|
clearTimeout(timeoutRef.current);
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Style Tokens
|
|
3
|
+
* Centralized design tokens to replace magic numbers across the codebase
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Spacing tokens - replaces magic numbers for padding, margins, gaps
|
|
8
|
+
*/
|
|
9
|
+
export const SPACING = {
|
|
10
|
+
xs: 4,
|
|
11
|
+
sm: 8,
|
|
12
|
+
md: 12,
|
|
13
|
+
lg: 16,
|
|
14
|
+
xl: 20,
|
|
15
|
+
xxl: 24,
|
|
16
|
+
xxxl: 32,
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Font size tokens - replaces magic numbers for font sizes
|
|
21
|
+
*/
|
|
22
|
+
export const FONT_SIZE = {
|
|
23
|
+
xs: 10,
|
|
24
|
+
sm: 12,
|
|
25
|
+
md: 14,
|
|
26
|
+
base: 16,
|
|
27
|
+
lg: 18,
|
|
28
|
+
xl: 20,
|
|
29
|
+
xxl: 24,
|
|
30
|
+
xxxl: 28,
|
|
31
|
+
display: 32,
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Border radius tokens - replaces magic numbers for border radius
|
|
36
|
+
*/
|
|
37
|
+
export const BORDER_RADIUS = {
|
|
38
|
+
none: 0,
|
|
39
|
+
sm: 4,
|
|
40
|
+
md: 8,
|
|
41
|
+
lg: 12,
|
|
42
|
+
xl: 16,
|
|
43
|
+
full: 9999,
|
|
44
|
+
} as const;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Opacity tokens - replaces magic numbers for opacity values
|
|
48
|
+
*/
|
|
49
|
+
export const OPACITY = {
|
|
50
|
+
disabled: 0.3,
|
|
51
|
+
hover: 0.7,
|
|
52
|
+
focus: 0.8,
|
|
53
|
+
pressed: 0.9,
|
|
54
|
+
overlay: 0.5,
|
|
55
|
+
icon: 0.6,
|
|
56
|
+
} as const;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Icon size tokens
|
|
60
|
+
*/
|
|
61
|
+
export const ICON_SIZE = {
|
|
62
|
+
xs: 16,
|
|
63
|
+
sm: 20,
|
|
64
|
+
md: 24,
|
|
65
|
+
lg: 32,
|
|
66
|
+
xl: 40,
|
|
67
|
+
xxl: 48,
|
|
68
|
+
} as const;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Layout dimension tokens
|
|
72
|
+
*/
|
|
73
|
+
export const DIMENSION = {
|
|
74
|
+
touchTarget: {
|
|
75
|
+
minHeight: 44,
|
|
76
|
+
minWidth: 44,
|
|
77
|
+
},
|
|
78
|
+
card: {
|
|
79
|
+
minHeight: 72,
|
|
80
|
+
},
|
|
81
|
+
input: {
|
|
82
|
+
minHeight: 48,
|
|
83
|
+
},
|
|
84
|
+
button: {
|
|
85
|
+
minHeight: 44,
|
|
86
|
+
height: 48,
|
|
87
|
+
},
|
|
88
|
+
avatar: {
|
|
89
|
+
xs: 24,
|
|
90
|
+
sm: 32,
|
|
91
|
+
md: 40,
|
|
92
|
+
lg: 48,
|
|
93
|
+
xl: 64,
|
|
94
|
+
xxl: 96,
|
|
95
|
+
},
|
|
96
|
+
} as const;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Z-index tokens for layering
|
|
100
|
+
*/
|
|
101
|
+
export const Z_INDEX = {
|
|
102
|
+
base: 0,
|
|
103
|
+
overlay: 10,
|
|
104
|
+
dropdown: 100,
|
|
105
|
+
sticky: 200,
|
|
106
|
+
fixed: 300,
|
|
107
|
+
modalBackdrop: 400,
|
|
108
|
+
modal: 500,
|
|
109
|
+
popover: 600,
|
|
110
|
+
toast: 700,
|
|
111
|
+
} as const;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Animation duration tokens (in ms)
|
|
115
|
+
*/
|
|
116
|
+
export const DURATION = {
|
|
117
|
+
fast: 150,
|
|
118
|
+
normal: 250,
|
|
119
|
+
slow: 350,
|
|
120
|
+
extraSlow: 500,
|
|
121
|
+
} as const;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Type exports for token values
|
|
125
|
+
*/
|
|
126
|
+
export type SpacingToken = typeof SPACING[keyof typeof SPACING];
|
|
127
|
+
export type FontSizeToken = typeof FONT_SIZE[keyof typeof FONT_SIZE];
|
|
128
|
+
export type BorderRadiusToken = typeof BORDER_RADIUS[keyof typeof BORDER_RADIUS];
|
|
129
|
+
export type OpacityToken = typeof OPACITY[keyof typeof OPACITY];
|
|
130
|
+
export type IconSizeToken = typeof ICON_SIZE[keyof typeof ICON_SIZE];
|
|
131
|
+
export type ZIndexToken = typeof Z_INDEX[keyof typeof Z_INDEX];
|
|
132
|
+
export type DurationToken = typeof DURATION[keyof typeof DURATION];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Validation Utilities
|
|
3
|
+
* Base validation interfaces and types
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Validation result interface
|
|
8
|
+
*/
|
|
9
|
+
export interface ValidationResult {
|
|
10
|
+
isValid: boolean;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form Validators
|
|
3
|
+
* Domain-specific form validation functions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ValidationResult } from "./core";
|
|
7
|
+
import { validateRating } from "./numericValidators";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Feedback form validation
|
|
11
|
+
*/
|
|
12
|
+
export const validateFeedbackForm = (data: {
|
|
13
|
+
type: string;
|
|
14
|
+
rating: number;
|
|
15
|
+
description: string;
|
|
16
|
+
}): ValidationResult => {
|
|
17
|
+
// Validate rating
|
|
18
|
+
const ratingResult = validateRating(data.rating);
|
|
19
|
+
if (!ratingResult.isValid) {
|
|
20
|
+
return ratingResult;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Validate description (required)
|
|
24
|
+
if (!data.description.trim()) {
|
|
25
|
+
return { isValid: false, error: "Please provide a description" };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Check description length
|
|
29
|
+
if (data.description.length < 10) {
|
|
30
|
+
return { isValid: false, error: "Description must be at least 10 characters" };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (data.description.length > 1000) {
|
|
34
|
+
return { isValid: false, error: "Description must be less than 1000 characters" };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return { isValid: true };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Reminder form validation
|
|
42
|
+
*/
|
|
43
|
+
export const validateReminderForm = (data: {
|
|
44
|
+
title: string;
|
|
45
|
+
body?: string;
|
|
46
|
+
frequency: string;
|
|
47
|
+
hour?: number;
|
|
48
|
+
minute?: number;
|
|
49
|
+
weekday?: number;
|
|
50
|
+
maxTitleLength?: number;
|
|
51
|
+
maxBodyLength?: number;
|
|
52
|
+
}): ValidationResult => {
|
|
53
|
+
// Validate title
|
|
54
|
+
if (!data.title.trim()) {
|
|
55
|
+
return { isValid: false, error: "Title is required" };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (data.maxTitleLength && data.title.length > data.maxTitleLength) {
|
|
59
|
+
return { isValid: false, error: `Title must be less than ${data.maxTitleLength} characters` };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Validate body length if provided
|
|
63
|
+
if (data.body && data.maxBodyLength && data.body.length > data.maxBodyLength) {
|
|
64
|
+
return { isValid: false, error: `Body must be less than ${data.maxBodyLength} characters` };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Validate time values if provided
|
|
68
|
+
if (data.hour !== undefined && (data.hour < 0 || data.hour > 23)) {
|
|
69
|
+
return { isValid: false, error: "Hour must be between 0 and 23" };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (data.minute !== undefined && (data.minute < 0 || data.minute > 59)) {
|
|
73
|
+
return { isValid: false, error: "Minute must be between 0 and 59" };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Validate weekday if frequency is weekly
|
|
77
|
+
if (data.frequency === "weekly" && (data.weekday === undefined || data.weekday < 0 || data.weekday > 6)) {
|
|
78
|
+
return { isValid: false, error: "Please select a valid day" };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { isValid: true };
|
|
82
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation Utilities
|
|
3
|
+
* Barrel export for all validation modules
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export * from "./core";
|
|
7
|
+
export * from "./textValidators";
|
|
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
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Numeric Validators
|
|
3
|
+
* Validation functions for numeric inputs
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ValidationResult } from "./core";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validates a rating value (1-5)
|
|
10
|
+
*/
|
|
11
|
+
export const validateRating = (rating: number): ValidationResult => {
|
|
12
|
+
if (rating < 1 || rating > 5) {
|
|
13
|
+
return { isValid: false, error: "Rating must be between 1 and 5" };
|
|
14
|
+
}
|
|
15
|
+
return { isValid: true };
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validates a number is within a range
|
|
20
|
+
*/
|
|
21
|
+
export const validateRange = (
|
|
22
|
+
value: number,
|
|
23
|
+
min: number,
|
|
24
|
+
max: number,
|
|
25
|
+
fieldName?: string
|
|
26
|
+
): ValidationResult => {
|
|
27
|
+
if (value < min || value > max) {
|
|
28
|
+
return {
|
|
29
|
+
isValid: false,
|
|
30
|
+
error: `${fieldName || "Value"} must be between ${min} and ${max}`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return { isValid: true };
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Validates a positive number
|
|
38
|
+
*/
|
|
39
|
+
export const validatePositiveNumber = (
|
|
40
|
+
value: number,
|
|
41
|
+
fieldName?: string
|
|
42
|
+
): ValidationResult => {
|
|
43
|
+
if (value <= 0) {
|
|
44
|
+
return {
|
|
45
|
+
isValid: false,
|
|
46
|
+
error: `${fieldName || "Value"} must be greater than 0`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return { isValid: true };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Validates a non-negative number
|
|
54
|
+
*/
|
|
55
|
+
export const validateNonNegativeNumber = (
|
|
56
|
+
value: number,
|
|
57
|
+
fieldName?: string
|
|
58
|
+
): ValidationResult => {
|
|
59
|
+
if (value < 0) {
|
|
60
|
+
return {
|
|
61
|
+
isValid: false,
|
|
62
|
+
error: `${fieldName || "Value"} must be 0 or greater`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return { isValid: true };
|
|
66
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Password Validator
|
|
3
|
+
* Password-specific validation logic
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ValidationResult, PasswordValidationOptions } from "./core";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validates a password
|
|
10
|
+
*/
|
|
11
|
+
export const validatePassword = (
|
|
12
|
+
password: string,
|
|
13
|
+
options: PasswordValidationOptions = {}
|
|
14
|
+
): ValidationResult => {
|
|
15
|
+
const {
|
|
16
|
+
minLength = 8,
|
|
17
|
+
requireUppercase = false,
|
|
18
|
+
requireLowercase = false,
|
|
19
|
+
requireNumber = false,
|
|
20
|
+
requireSpecialChar = false,
|
|
21
|
+
} = options;
|
|
22
|
+
|
|
23
|
+
const errors: string[] = [];
|
|
24
|
+
|
|
25
|
+
if (password.length < minLength) {
|
|
26
|
+
errors.push(`at least ${minLength} characters`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (requireUppercase && !/[A-Z]/.test(password)) {
|
|
30
|
+
errors.push("one uppercase letter");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (requireLowercase && !/[a-z]/.test(password)) {
|
|
34
|
+
errors.push("one lowercase letter");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (requireNumber && !/\d/.test(password)) {
|
|
38
|
+
errors.push("one number");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (requireSpecialChar && !/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) {
|
|
42
|
+
errors.push("one special character");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (errors.length > 0) {
|
|
46
|
+
return {
|
|
47
|
+
isValid: false,
|
|
48
|
+
error: `Password must contain ${errors.join(", ")}`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { isValid: true };
|
|
53
|
+
};
|