@umituz/react-native-design-system 2.6.110 → 2.6.112
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 +7 -3
- package/src/atoms/image/AtomicImage.tsx +29 -0
- package/src/atoms/index.ts +3 -0
- package/src/exports/image.ts +7 -0
- package/src/exports/infinite-scroll.ts +7 -0
- package/src/exports/uuid.ts +7 -0
- package/src/image/domain/entities/EditorTypes.ts +23 -0
- package/src/image/domain/entities/ImageConstants.ts +38 -0
- package/src/image/domain/entities/ImageFilterTypes.ts +70 -0
- package/src/image/domain/entities/ImageTemplateTypes.ts +18 -0
- package/src/image/domain/entities/ImageTypes.ts +86 -0
- package/src/image/domain/entities/ValidationResult.ts +15 -0
- package/src/image/domain/entities/editor/EditorConfigTypes.ts +35 -0
- package/src/image/domain/entities/editor/EditorElementTypes.ts +60 -0
- package/src/image/domain/entities/editor/EditorFilterTypes.ts +9 -0
- package/src/image/domain/entities/editor/EditorLayerTypes.ts +34 -0
- package/src/image/domain/entities/editor/EditorStateTypes.ts +35 -0
- package/src/image/domain/entities/editor/EditorToolTypes.ts +33 -0
- package/src/image/domain/utils/ImageUtils.ts +103 -0
- package/src/image/index.ts +123 -0
- package/src/image/infrastructure/services/ImageBatchService.ts +110 -0
- package/src/image/infrastructure/services/ImageConversionService.ts +74 -0
- package/src/image/infrastructure/services/ImageEditorService.ts +136 -0
- package/src/image/infrastructure/services/ImageEnhanceService.ts +123 -0
- package/src/image/infrastructure/services/ImageMetadataService.ts +116 -0
- package/src/image/infrastructure/services/ImageStorageService.ts +37 -0
- package/src/image/infrastructure/services/ImageTemplateService.ts +66 -0
- package/src/image/infrastructure/services/ImageTransformService.ts +89 -0
- package/src/image/infrastructure/services/ImageViewerService.ts +64 -0
- package/src/image/infrastructure/utils/BatchProcessor.ts +95 -0
- package/src/image/infrastructure/utils/FilterProcessor.ts +124 -0
- package/src/image/infrastructure/utils/ImageAnalysisUtils.ts +122 -0
- package/src/image/infrastructure/utils/ImageEditorHistoryUtils.ts +63 -0
- package/src/image/infrastructure/utils/ImageErrorHandler.ts +40 -0
- package/src/image/infrastructure/utils/ImageFilterUtils.ts +21 -0
- package/src/image/infrastructure/utils/ImageQualityPresets.ts +110 -0
- package/src/image/infrastructure/utils/ImageTransformUtils.ts +25 -0
- package/src/image/infrastructure/utils/ImageValidator.ts +59 -0
- package/src/image/infrastructure/utils/LayerManager.ts +77 -0
- package/src/image/infrastructure/utils/MetadataExtractor.ts +83 -0
- package/src/image/infrastructure/utils/filters/BasicFilters.ts +61 -0
- package/src/image/infrastructure/utils/filters/FilterHelpers.ts +21 -0
- package/src/image/infrastructure/utils/filters/SpecialFilters.ts +84 -0
- package/src/image/infrastructure/utils/validation/image-validator.ts +77 -0
- package/src/image/infrastructure/utils/validation/mime-type-validator.ts +101 -0
- package/src/image/infrastructure/utils/validation/mime-types.constants.ts +41 -0
- package/src/image/presentation/components/GalleryHeader.tsx +126 -0
- package/src/image/presentation/components/ImageGallery.tsx +138 -0
- package/src/image/presentation/components/editor/FilterPickerSheet.tsx +75 -0
- package/src/image/presentation/components/editor/StickerPickerSheet.tsx +62 -0
- package/src/image/presentation/components/editor/TextEditorSheet.tsx +98 -0
- package/src/image/presentation/components/editor/TextEditorTabs.tsx +111 -0
- package/src/image/presentation/components/image/AtomicImage.tsx +29 -0
- package/src/image/presentation/hooks/useImage.ts +39 -0
- package/src/image/presentation/hooks/useImageBatch.ts +28 -0
- package/src/image/presentation/hooks/useImageConversion.ts +29 -0
- package/src/image/presentation/hooks/useImageEnhance.ts +32 -0
- package/src/image/presentation/hooks/useImageGallery.ts +90 -0
- package/src/image/presentation/hooks/useImageMetadata.ts +28 -0
- package/src/image/presentation/hooks/useImageOperation.ts +37 -0
- package/src/image/presentation/hooks/useImageTransform.ts +42 -0
- package/src/index.ts +15 -0
- package/src/infinite-scroll/domain/interfaces/infinite-scroll-list-props.ts +67 -0
- package/src/infinite-scroll/domain/types/infinite-scroll-config.ts +108 -0
- package/src/infinite-scroll/domain/types/infinite-scroll-return.ts +40 -0
- package/src/infinite-scroll/domain/types/infinite-scroll-state.ts +58 -0
- package/src/infinite-scroll/domain/utils/pagination-utils.ts +63 -0
- package/src/infinite-scroll/domain/utils/type-guards.ts +53 -0
- package/src/infinite-scroll/index.ts +62 -0
- package/src/infinite-scroll/presentation/components/empty.tsx +44 -0
- package/src/infinite-scroll/presentation/components/error.tsx +66 -0
- package/src/infinite-scroll/presentation/components/infinite-scroll-list.tsx +120 -0
- package/src/infinite-scroll/presentation/components/loading-more.tsx +38 -0
- package/src/infinite-scroll/presentation/components/loading.tsx +40 -0
- package/src/infinite-scroll/presentation/components/types.ts +124 -0
- package/src/infinite-scroll/presentation/hooks/pagination.helper.ts +83 -0
- package/src/infinite-scroll/presentation/hooks/useInfiniteScroll.ts +327 -0
- package/src/uuid/index.ts +15 -0
- package/src/uuid/infrastructure/utils/UUIDUtils.ts +75 -0
- package/src/uuid/package-lock.json +14255 -0
- package/src/uuid/types/UUID.ts +36 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Types
|
|
3
|
+
*
|
|
4
|
+
* Shared types for presentation components
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { TextStyle, ViewStyle, AccessibilityProps } from "react-native";
|
|
8
|
+
|
|
9
|
+
export interface EmptyProps extends AccessibilityProps {
|
|
10
|
+
/**
|
|
11
|
+
* Optional text to display
|
|
12
|
+
* Default: "No items found"
|
|
13
|
+
*/
|
|
14
|
+
text?: string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Optional accessibility label
|
|
18
|
+
* Default: "Empty list"
|
|
19
|
+
*/
|
|
20
|
+
accessibilityLabel?: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Optional custom style for container
|
|
24
|
+
*/
|
|
25
|
+
containerStyle?: ViewStyle;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Optional custom style for text
|
|
29
|
+
*/
|
|
30
|
+
textStyle?: TextStyle;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ErrorProps extends AccessibilityProps {
|
|
34
|
+
/**
|
|
35
|
+
* Error message to display
|
|
36
|
+
*/
|
|
37
|
+
error: string;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Retry callback
|
|
41
|
+
*/
|
|
42
|
+
onRetry: () => void;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Optional text for retry button
|
|
46
|
+
* Default: "Tap to retry"
|
|
47
|
+
*/
|
|
48
|
+
retryText?: string;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Optional accessibility label for error
|
|
52
|
+
* Default: "Error: {error}"
|
|
53
|
+
*/
|
|
54
|
+
accessibilityLabel?: string;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Optional accessibility hint for retry button
|
|
58
|
+
* Default: "Double tap to retry"
|
|
59
|
+
*/
|
|
60
|
+
retryAccessibilityHint?: string;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Optional custom style for container
|
|
64
|
+
*/
|
|
65
|
+
containerStyle?: ViewStyle;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Optional custom style for error text
|
|
69
|
+
*/
|
|
70
|
+
errorTextStyle?: TextStyle;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Optional custom style for retry text
|
|
74
|
+
*/
|
|
75
|
+
retryTextStyle?: TextStyle;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface LoadingProps extends AccessibilityProps {
|
|
79
|
+
/**
|
|
80
|
+
* Optional custom style for container
|
|
81
|
+
*/
|
|
82
|
+
containerStyle?: ViewStyle;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Optional size for indicator
|
|
86
|
+
* Default: "large"
|
|
87
|
+
*/
|
|
88
|
+
size?: "small" | "large" | number;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Optional color for indicator
|
|
92
|
+
*/
|
|
93
|
+
color?: string;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Optional accessibility label
|
|
97
|
+
* Default: "Loading"
|
|
98
|
+
*/
|
|
99
|
+
accessibilityLabel?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface LoadingMoreProps extends AccessibilityProps {
|
|
103
|
+
/**
|
|
104
|
+
* Optional custom style for container
|
|
105
|
+
*/
|
|
106
|
+
containerStyle?: ViewStyle;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Optional size for indicator
|
|
110
|
+
* Default: "small"
|
|
111
|
+
*/
|
|
112
|
+
size?: "small" | "large" | number;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Optional color for indicator
|
|
116
|
+
*/
|
|
117
|
+
color?: string;
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Optional accessibility label
|
|
121
|
+
* Default: "Loading more"
|
|
122
|
+
*/
|
|
123
|
+
accessibilityLabel?: string;
|
|
124
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pagination Helper
|
|
3
|
+
* SOLID: Single Responsibility - Handle pagination operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { InfiniteScrollConfig, PaginatedResult } from "../../domain/types/infinite-scroll-config";
|
|
7
|
+
import type { InfiniteScrollState } from "../../domain/types/infinite-scroll-state";
|
|
8
|
+
|
|
9
|
+
export function isCursorMode<T>(
|
|
10
|
+
config: InfiniteScrollConfig<T>,
|
|
11
|
+
): config is Extract<InfiniteScrollConfig<T>, { paginationMode: "cursor" }> {
|
|
12
|
+
return "paginationMode" in config && config.paginationMode === "cursor";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function loadData<T>(
|
|
16
|
+
config: InfiniteScrollConfig<T>,
|
|
17
|
+
pageOrCursor: number | string | undefined,
|
|
18
|
+
pageSize: number,
|
|
19
|
+
totalItems?: number,
|
|
20
|
+
): Promise<InfiniteScrollState<T>> {
|
|
21
|
+
if (isCursorMode(config)) {
|
|
22
|
+
const result = await config.fetchCursor(pageOrCursor as string | undefined, pageSize);
|
|
23
|
+
return {
|
|
24
|
+
items: result.items,
|
|
25
|
+
pages: [result.items],
|
|
26
|
+
currentPage: 0,
|
|
27
|
+
cursor: result.nextCursor,
|
|
28
|
+
hasMore: result.hasMore,
|
|
29
|
+
isLoading: false,
|
|
30
|
+
isLoadingMore: false,
|
|
31
|
+
isRefreshing: false,
|
|
32
|
+
error: null,
|
|
33
|
+
totalItems,
|
|
34
|
+
};
|
|
35
|
+
} else {
|
|
36
|
+
const data = await config.fetchData(pageOrCursor as number, pageSize);
|
|
37
|
+
const hasMore = data.length >= pageSize;
|
|
38
|
+
return {
|
|
39
|
+
items: data,
|
|
40
|
+
pages: [data],
|
|
41
|
+
currentPage: pageOrCursor as number,
|
|
42
|
+
cursor: null,
|
|
43
|
+
hasMore,
|
|
44
|
+
isLoading: false,
|
|
45
|
+
isLoadingMore: false,
|
|
46
|
+
isRefreshing: false,
|
|
47
|
+
error: null,
|
|
48
|
+
totalItems,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function loadMoreData<T>(
|
|
54
|
+
config: InfiniteScrollConfig<T>,
|
|
55
|
+
state: InfiniteScrollState<T>,
|
|
56
|
+
pageSize: number,
|
|
57
|
+
): Promise<Partial<InfiniteScrollState<T>>> {
|
|
58
|
+
if (isCursorMode(config)) {
|
|
59
|
+
if (!state.cursor) throw new Error("No cursor available");
|
|
60
|
+
const result = await config.fetchCursor(state.cursor, pageSize);
|
|
61
|
+
return {
|
|
62
|
+
items: [...state.items, ...result.items],
|
|
63
|
+
pages: [...state.pages, result.items],
|
|
64
|
+
cursor: result.nextCursor,
|
|
65
|
+
hasMore: result.hasMore,
|
|
66
|
+
isLoadingMore: false,
|
|
67
|
+
error: null,
|
|
68
|
+
};
|
|
69
|
+
} else {
|
|
70
|
+
const nextPage = state.currentPage + 1;
|
|
71
|
+
const data = await config.fetchData(nextPage, pageSize);
|
|
72
|
+
const newPages = [...state.pages, data];
|
|
73
|
+
const hasMore = data.length >= pageSize;
|
|
74
|
+
return {
|
|
75
|
+
items: newPages.flat(),
|
|
76
|
+
pages: newPages,
|
|
77
|
+
currentPage: nextPage,
|
|
78
|
+
hasMore,
|
|
79
|
+
isLoadingMore: false,
|
|
80
|
+
error: null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useInfiniteScroll Hook
|
|
3
|
+
*
|
|
4
|
+
* Supports page-based and cursor-based pagination
|
|
5
|
+
* Features:
|
|
6
|
+
* - AbortController for request cancellation
|
|
7
|
+
* - Automatic retry with exponential backoff
|
|
8
|
+
* - Performance monitoring in __DEV__ mode
|
|
9
|
+
* - Memory leak prevention
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { useState, useCallback, useEffect, useRef } from "react";
|
|
13
|
+
import type { InfiniteScrollConfig } from "../../domain/types/infinite-scroll-config";
|
|
14
|
+
import type { InfiniteScrollState } from "../../domain/types/infinite-scroll-state";
|
|
15
|
+
import type { UseInfiniteScrollReturn } from "../../domain/types/infinite-scroll-return";
|
|
16
|
+
import { loadData, loadMoreData, isCursorMode } from "./pagination.helper";
|
|
17
|
+
|
|
18
|
+
const DEFAULT_CONFIG = {
|
|
19
|
+
pageSize: 20,
|
|
20
|
+
threshold: 5,
|
|
21
|
+
autoLoad: true,
|
|
22
|
+
initialPage: 0,
|
|
23
|
+
maxRetries: 3,
|
|
24
|
+
retryDelay: 1000,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function createInitialState<T>(
|
|
28
|
+
initialPage: number,
|
|
29
|
+
totalItems?: number,
|
|
30
|
+
): InfiniteScrollState<T> {
|
|
31
|
+
return {
|
|
32
|
+
items: [],
|
|
33
|
+
pages: [],
|
|
34
|
+
currentPage: initialPage,
|
|
35
|
+
cursor: null,
|
|
36
|
+
hasMore: true,
|
|
37
|
+
isLoading: true,
|
|
38
|
+
isLoadingMore: false,
|
|
39
|
+
isRefreshing: false,
|
|
40
|
+
error: null,
|
|
41
|
+
totalItems,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Sleep utility for retry delay
|
|
47
|
+
*/
|
|
48
|
+
function sleep(ms: number): Promise<void> {
|
|
49
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Retry logic with exponential backoff
|
|
54
|
+
*/
|
|
55
|
+
async function retryWithBackoff<T>(
|
|
56
|
+
fn: () => Promise<T>,
|
|
57
|
+
maxRetries: number,
|
|
58
|
+
baseDelay: number,
|
|
59
|
+
): Promise<T> {
|
|
60
|
+
let lastError: Error | undefined;
|
|
61
|
+
|
|
62
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
63
|
+
try {
|
|
64
|
+
return await fn();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
67
|
+
|
|
68
|
+
if (attempt < maxRetries) {
|
|
69
|
+
const delay = baseDelay * Math.pow(2, attempt);
|
|
70
|
+
if (__DEV__) {
|
|
71
|
+
console.log(
|
|
72
|
+
`[useInfiniteScroll] Retry attempt ${attempt + 1}/${maxRetries} after ${delay}ms`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
await sleep(delay);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
throw lastError;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function useInfiniteScroll<T>(
|
|
84
|
+
config: InfiniteScrollConfig<T>,
|
|
85
|
+
): UseInfiniteScrollReturn<T> {
|
|
86
|
+
const {
|
|
87
|
+
pageSize = DEFAULT_CONFIG.pageSize,
|
|
88
|
+
autoLoad = DEFAULT_CONFIG.autoLoad,
|
|
89
|
+
totalItems,
|
|
90
|
+
} = config;
|
|
91
|
+
|
|
92
|
+
const initialPage =
|
|
93
|
+
"initialPage" in config ? config.initialPage || 0 : DEFAULT_CONFIG.initialPage;
|
|
94
|
+
|
|
95
|
+
const maxRetries = DEFAULT_CONFIG.maxRetries;
|
|
96
|
+
const retryDelay = DEFAULT_CONFIG.retryDelay;
|
|
97
|
+
|
|
98
|
+
const [state, setState] = useState<InfiniteScrollState<T>>(() =>
|
|
99
|
+
createInitialState<T>(initialPage, totalItems),
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const isLoadingRef = useRef(false);
|
|
103
|
+
const isMountedRef = useRef(true);
|
|
104
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
105
|
+
|
|
106
|
+
// Cleanup on unmount
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
isMountedRef.current = true;
|
|
109
|
+
|
|
110
|
+
return () => {
|
|
111
|
+
isMountedRef.current = false;
|
|
112
|
+
abortControllerRef.current?.abort();
|
|
113
|
+
if (__DEV__) {
|
|
114
|
+
console.log("[useInfiniteScroll] Cleanup: component unmounted");
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
// Cancel pending requests
|
|
120
|
+
const cancelPendingRequests = useCallback(() => {
|
|
121
|
+
abortControllerRef.current?.abort();
|
|
122
|
+
abortControllerRef.current = new AbortController();
|
|
123
|
+
}, []);
|
|
124
|
+
|
|
125
|
+
const loadInitial = useCallback(async () => {
|
|
126
|
+
if (isLoadingRef.current) return;
|
|
127
|
+
isLoadingRef.current = true;
|
|
128
|
+
|
|
129
|
+
cancelPendingRequests();
|
|
130
|
+
|
|
131
|
+
if (isMountedRef.current) {
|
|
132
|
+
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const startTime = __DEV__ ? performance.now() : 0;
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const newState = await retryWithBackoff(
|
|
139
|
+
async () => {
|
|
140
|
+
const result = await loadData(config, initialPage, pageSize, totalItems);
|
|
141
|
+
return result;
|
|
142
|
+
},
|
|
143
|
+
maxRetries,
|
|
144
|
+
retryDelay,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
if (isMountedRef.current) {
|
|
148
|
+
setState(newState);
|
|
149
|
+
|
|
150
|
+
if (__DEV__) {
|
|
151
|
+
const duration = performance.now() - startTime;
|
|
152
|
+
console.log(
|
|
153
|
+
`[useInfiniteScroll] Initial load completed in ${duration.toFixed(2)}ms`,
|
|
154
|
+
`Loaded ${newState.items.length} items`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (isMountedRef.current) {
|
|
160
|
+
const errorMessage =
|
|
161
|
+
error instanceof Error ? error.message : "Failed to load data";
|
|
162
|
+
|
|
163
|
+
setState((prev) => ({
|
|
164
|
+
...prev,
|
|
165
|
+
isLoading: false,
|
|
166
|
+
error: errorMessage,
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
if (__DEV__) {
|
|
170
|
+
console.error("[useInfiniteScroll] Load initial failed:", errorMessage);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} finally {
|
|
174
|
+
isLoadingRef.current = false;
|
|
175
|
+
}
|
|
176
|
+
}, [config, initialPage, pageSize, totalItems, maxRetries, retryDelay, cancelPendingRequests]);
|
|
177
|
+
|
|
178
|
+
const loadMore = useCallback(async () => {
|
|
179
|
+
if (
|
|
180
|
+
isLoadingRef.current ||
|
|
181
|
+
!state.hasMore ||
|
|
182
|
+
state.isLoadingMore ||
|
|
183
|
+
state.isLoading
|
|
184
|
+
) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (isCursorMode(config) && !state.cursor) return;
|
|
189
|
+
|
|
190
|
+
isLoadingRef.current = true;
|
|
191
|
+
|
|
192
|
+
if (isMountedRef.current) {
|
|
193
|
+
setState((prev) => ({ ...prev, isLoadingMore: true, error: null }));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const startTime = __DEV__ ? performance.now() : 0;
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const updates = await retryWithBackoff(
|
|
200
|
+
async () => {
|
|
201
|
+
const result = await loadMoreData(config, state, pageSize);
|
|
202
|
+
return result;
|
|
203
|
+
},
|
|
204
|
+
maxRetries,
|
|
205
|
+
retryDelay,
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
if (isMountedRef.current) {
|
|
209
|
+
setState((prev) => ({ ...prev, ...updates }));
|
|
210
|
+
|
|
211
|
+
if (__DEV__) {
|
|
212
|
+
const duration = performance.now() - startTime;
|
|
213
|
+
const newItemsCount = updates.items?.length || 0;
|
|
214
|
+
console.log(
|
|
215
|
+
`[useInfiniteScroll] Load more completed in ${duration.toFixed(2)}ms`,
|
|
216
|
+
`Loaded ${newItemsCount} items, total: ${updates.items?.length || 0}`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} catch (error) {
|
|
221
|
+
if (isMountedRef.current) {
|
|
222
|
+
const errorMessage =
|
|
223
|
+
error instanceof Error ? error.message : "Failed to load more items";
|
|
224
|
+
|
|
225
|
+
setState((prev) => ({
|
|
226
|
+
...prev,
|
|
227
|
+
isLoadingMore: false,
|
|
228
|
+
error: errorMessage,
|
|
229
|
+
}));
|
|
230
|
+
|
|
231
|
+
if (__DEV__) {
|
|
232
|
+
console.error("[useInfiniteScroll] Load more failed:", errorMessage);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} finally {
|
|
236
|
+
isLoadingRef.current = false;
|
|
237
|
+
}
|
|
238
|
+
}, [config, state, pageSize, maxRetries, retryDelay]);
|
|
239
|
+
|
|
240
|
+
const refresh = useCallback(async () => {
|
|
241
|
+
if (isLoadingRef.current) return;
|
|
242
|
+
isLoadingRef.current = true;
|
|
243
|
+
|
|
244
|
+
cancelPendingRequests();
|
|
245
|
+
|
|
246
|
+
if (isMountedRef.current) {
|
|
247
|
+
setState((prev) => ({ ...prev, isRefreshing: true, error: null }));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const startTime = __DEV__ ? performance.now() : 0;
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const newState = await retryWithBackoff(
|
|
254
|
+
async () => {
|
|
255
|
+
const result = await loadData(config, initialPage, pageSize, totalItems);
|
|
256
|
+
return result;
|
|
257
|
+
},
|
|
258
|
+
maxRetries,
|
|
259
|
+
retryDelay,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
if (isMountedRef.current) {
|
|
263
|
+
setState(newState);
|
|
264
|
+
|
|
265
|
+
if (__DEV__) {
|
|
266
|
+
const duration = performance.now() - startTime;
|
|
267
|
+
console.log(
|
|
268
|
+
`[useInfiniteScroll] Refresh completed in ${duration.toFixed(2)}ms`,
|
|
269
|
+
`Loaded ${newState.items.length} items`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
} catch (error) {
|
|
274
|
+
if (isMountedRef.current) {
|
|
275
|
+
const errorMessage =
|
|
276
|
+
error instanceof Error ? error.message : "Failed to refresh data";
|
|
277
|
+
|
|
278
|
+
setState((prev) => ({
|
|
279
|
+
...prev,
|
|
280
|
+
isRefreshing: false,
|
|
281
|
+
error: errorMessage,
|
|
282
|
+
}));
|
|
283
|
+
|
|
284
|
+
if (__DEV__) {
|
|
285
|
+
console.error("[useInfiniteScroll] Refresh failed:", errorMessage);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
} finally {
|
|
289
|
+
isLoadingRef.current = false;
|
|
290
|
+
}
|
|
291
|
+
}, [config, initialPage, pageSize, totalItems, maxRetries, retryDelay, cancelPendingRequests]);
|
|
292
|
+
|
|
293
|
+
const reset = useCallback(() => {
|
|
294
|
+
isLoadingRef.current = false;
|
|
295
|
+
cancelPendingRequests();
|
|
296
|
+
setState(createInitialState<T>(initialPage, totalItems));
|
|
297
|
+
|
|
298
|
+
if (__DEV__) {
|
|
299
|
+
console.log("[useInfiniteScroll] State reset");
|
|
300
|
+
}
|
|
301
|
+
}, [initialPage, totalItems, cancelPendingRequests]);
|
|
302
|
+
|
|
303
|
+
useEffect(() => {
|
|
304
|
+
if (autoLoad) {
|
|
305
|
+
loadInitial();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return () => {
|
|
309
|
+
// Cleanup on config change
|
|
310
|
+
if (__DEV__) {
|
|
311
|
+
console.log("[useInfiniteScroll] Config changed, cleaning up");
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
}, [autoLoad, loadInitial]);
|
|
315
|
+
|
|
316
|
+
const canLoadMore =
|
|
317
|
+
state.hasMore && !state.isLoadingMore && !state.isLoading;
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
items: state.items,
|
|
321
|
+
state,
|
|
322
|
+
loadMore,
|
|
323
|
+
refresh,
|
|
324
|
+
reset,
|
|
325
|
+
canLoadMore,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Native UUID
|
|
3
|
+
*
|
|
4
|
+
* Cross-platform UUID generation for React Native apps
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
generateUUID,
|
|
9
|
+
generateUUID as uuidv4,
|
|
10
|
+
generateUUID as generateCreationId,
|
|
11
|
+
isValidUUID,
|
|
12
|
+
getUUIDVersion,
|
|
13
|
+
} from "./infrastructure/utils/UUIDUtils";
|
|
14
|
+
export type { UUID } from "./types/UUID";
|
|
15
|
+
export { UUIDVersion, UUID_CONSTANTS } from "./types/UUID";
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UUID Generation Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides cross-platform UUID generation using expo-crypto.
|
|
5
|
+
* Compatible with React Native (iOS, Android) and Web.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as Crypto from 'expo-crypto';
|
|
9
|
+
import type { UUID } from '../../types/UUID';
|
|
10
|
+
import { UUID_CONSTANTS } from '../../types/UUID';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generate a v4 UUID
|
|
14
|
+
* Uses expo-crypto's randomUUID() for secure UUID generation
|
|
15
|
+
*
|
|
16
|
+
* @returns A v4 UUID string
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import { generateUUID } from '@umituz/react-native-uuid';
|
|
21
|
+
*
|
|
22
|
+
* const id = generateUUID();
|
|
23
|
+
* // Returns: "550e8400-e29b-41d4-a716-446655440000"
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export const generateUUID = (): UUID => {
|
|
27
|
+
return Crypto.randomUUID() as UUID;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Validate UUID format
|
|
32
|
+
* Checks if a string is a valid v4 UUID
|
|
33
|
+
*
|
|
34
|
+
* @param value - The value to validate
|
|
35
|
+
* @returns True if the value is a valid v4 UUID
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* import { isValidUUID } from '@umituz/react-native-uuid';
|
|
40
|
+
*
|
|
41
|
+
* isValidUUID('550e8400-e29b-41d4-a716-446655440000'); // true
|
|
42
|
+
* isValidUUID('invalid-uuid'); // false
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export const isValidUUID = (value: string): value is UUID => {
|
|
46
|
+
return UUID_CONSTANTS.PATTERN.test(value);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get version from UUID string
|
|
51
|
+
* Returns the UUID version number (1-5) or null for NIL/invalid
|
|
52
|
+
*
|
|
53
|
+
* @param value - The UUID string
|
|
54
|
+
* @returns UUID version number or null
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* import { getUUIDVersion } from '@umituz/react-native-uuid';
|
|
59
|
+
*
|
|
60
|
+
* getUUIDVersion('550e8400-e29b-41d4-a716-446655440000'); // 4
|
|
61
|
+
* getUUIDVersion('00000000-0000-0000-0000-000000000000'); // 0 (NIL)
|
|
62
|
+
* getUUIDVersion('invalid'); // null
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export const getUUIDVersion = (value: string): number | null => {
|
|
66
|
+
if (value === UUID_CONSTANTS.NIL) {
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const versionChar = value.charAt(14);
|
|
71
|
+
const version = parseInt(versionChar, 10);
|
|
72
|
+
|
|
73
|
+
return (version >= 1 && version <= 5) ? version : null;
|
|
74
|
+
};
|
|
75
|
+
|