@umituz/react-native-design-system 2.6.110 → 2.6.111
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 -2
- package/src/exports/infinite-scroll.ts +7 -0
- package/src/exports/uuid.ts +7 -0
- package/src/index.ts +10 -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,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InfiniteScrollList Component
|
|
3
|
+
*
|
|
4
|
+
* Presentation component for infinite scroll list
|
|
5
|
+
* Optimized with React.memo for performance
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from "react";
|
|
9
|
+
import { FlatList } from "react-native";
|
|
10
|
+
import { useInfiniteScroll } from "../hooks/useInfiniteScroll";
|
|
11
|
+
import { calculateEndReachedThreshold } from "../../domain/utils/pagination-utils";
|
|
12
|
+
import type { InfiniteScrollListProps } from "../../domain/interfaces/infinite-scroll-list-props";
|
|
13
|
+
import { Loading } from "./loading";
|
|
14
|
+
import { LoadingMore } from "./loading-more";
|
|
15
|
+
import { Empty } from "./empty";
|
|
16
|
+
import { Error } from "./error";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Render error component
|
|
20
|
+
*/
|
|
21
|
+
function renderErrorComponent(
|
|
22
|
+
error: string,
|
|
23
|
+
retry: () => void,
|
|
24
|
+
errorComponent?: (error: string, retry: () => void) => React.ReactElement,
|
|
25
|
+
): React.ReactElement {
|
|
26
|
+
if (errorComponent) {
|
|
27
|
+
return errorComponent(error, retry);
|
|
28
|
+
}
|
|
29
|
+
return <Error error={error} onRetry={retry} />;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* InfiniteScrollList Component
|
|
34
|
+
*
|
|
35
|
+
* FlatList wrapper with automatic infinite scroll and pagination
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* <InfiniteScrollList
|
|
40
|
+
* config={{
|
|
41
|
+
* pageSize: 20,
|
|
42
|
+
* threshold: 5,
|
|
43
|
+
* fetchData: async (page, pageSize) => {
|
|
44
|
+
* const response = await api.getItems({ page, limit: pageSize });
|
|
45
|
+
* return response.data;
|
|
46
|
+
* },
|
|
47
|
+
* }}
|
|
48
|
+
* renderItem={(item) => <ItemCard item={item} />}
|
|
49
|
+
* />
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
function InfiniteScrollListComponent<T>({
|
|
53
|
+
config,
|
|
54
|
+
renderItem,
|
|
55
|
+
loadingComponent,
|
|
56
|
+
loadingMoreComponent,
|
|
57
|
+
emptyComponent,
|
|
58
|
+
errorComponent,
|
|
59
|
+
ListHeaderComponent,
|
|
60
|
+
ListFooterComponent,
|
|
61
|
+
flatListProps,
|
|
62
|
+
}: InfiniteScrollListProps<T>): React.ReactElement {
|
|
63
|
+
const { items, state, loadMore, refresh, canLoadMore } = useInfiniteScroll(config);
|
|
64
|
+
|
|
65
|
+
const handleEndReached = React.useCallback(() => {
|
|
66
|
+
if (canLoadMore && config.autoLoad !== false) {
|
|
67
|
+
loadMore();
|
|
68
|
+
}
|
|
69
|
+
}, [canLoadMore, loadMore, config.autoLoad]);
|
|
70
|
+
|
|
71
|
+
const getItemKey = React.useCallback(
|
|
72
|
+
(item: T, index: number): string => {
|
|
73
|
+
if (config.getItemKey) {
|
|
74
|
+
return config.getItemKey(item, index);
|
|
75
|
+
}
|
|
76
|
+
return `item-${index}`;
|
|
77
|
+
},
|
|
78
|
+
[config],
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Loading state
|
|
82
|
+
if (state.isLoading) {
|
|
83
|
+
return loadingComponent || <Loading />;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Error state
|
|
87
|
+
if (state.error) {
|
|
88
|
+
return renderErrorComponent(state.error, refresh, errorComponent);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Empty state
|
|
92
|
+
if (items.length === 0 && !state.isLoading) {
|
|
93
|
+
return emptyComponent || <Empty />;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Render list
|
|
97
|
+
return (
|
|
98
|
+
<FlatList
|
|
99
|
+
data={items}
|
|
100
|
+
renderItem={({ item, index }) => renderItem(item, index)}
|
|
101
|
+
keyExtractor={(item, index) => getItemKey(item, index)}
|
|
102
|
+
onEndReached={handleEndReached}
|
|
103
|
+
onEndReachedThreshold={calculateEndReachedThreshold(config.threshold)}
|
|
104
|
+
onRefresh={refresh}
|
|
105
|
+
refreshing={state.isRefreshing}
|
|
106
|
+
ListHeaderComponent={ListHeaderComponent}
|
|
107
|
+
ListFooterComponent={
|
|
108
|
+
<>
|
|
109
|
+
{ListFooterComponent}
|
|
110
|
+
{state.isLoadingMore && (loadingMoreComponent || <LoadingMore />)}
|
|
111
|
+
</>
|
|
112
|
+
}
|
|
113
|
+
{...flatListProps}
|
|
114
|
+
/>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const InfiniteScrollList = React.memo(InfiniteScrollListComponent) as <T>(
|
|
119
|
+
props: InfiniteScrollListProps<T>,
|
|
120
|
+
) => React.ReactElement;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loading More Component
|
|
3
|
+
*
|
|
4
|
+
* Presentation component for loading more state
|
|
5
|
+
* Fully customizable via props
|
|
6
|
+
* Accessibility support included
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React from "react";
|
|
10
|
+
import { View, ActivityIndicator, StyleSheet } from "react-native";
|
|
11
|
+
import type { LoadingMoreProps } from "./types";
|
|
12
|
+
|
|
13
|
+
export const LoadingMore = React.memo<LoadingMoreProps>(({
|
|
14
|
+
containerStyle,
|
|
15
|
+
size = "small",
|
|
16
|
+
color,
|
|
17
|
+
accessibilityLabel = "Loading more",
|
|
18
|
+
...accessibilityProps
|
|
19
|
+
}) => (
|
|
20
|
+
<View
|
|
21
|
+
style={[styles.container, containerStyle]}
|
|
22
|
+
accessibilityLabel={accessibilityLabel}
|
|
23
|
+
accessibilityRole="progressbar"
|
|
24
|
+
accessibilityState={{ busy: true }}
|
|
25
|
+
{...accessibilityProps}
|
|
26
|
+
>
|
|
27
|
+
<ActivityIndicator size={size} color={color} />
|
|
28
|
+
</View>
|
|
29
|
+
));
|
|
30
|
+
|
|
31
|
+
LoadingMore.displayName = "LoadingMore";
|
|
32
|
+
|
|
33
|
+
const styles = StyleSheet.create({
|
|
34
|
+
container: {
|
|
35
|
+
padding: 16,
|
|
36
|
+
alignItems: "center",
|
|
37
|
+
},
|
|
38
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loading Component
|
|
3
|
+
*
|
|
4
|
+
* Presentation component for loading state
|
|
5
|
+
* Fully customizable via props
|
|
6
|
+
* Accessibility support included
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React from "react";
|
|
10
|
+
import { View, ActivityIndicator, StyleSheet } from "react-native";
|
|
11
|
+
import type { LoadingProps } from "./types";
|
|
12
|
+
|
|
13
|
+
export const Loading = React.memo<LoadingProps>(({
|
|
14
|
+
containerStyle,
|
|
15
|
+
size = "large",
|
|
16
|
+
color,
|
|
17
|
+
accessibilityLabel = "Loading",
|
|
18
|
+
...accessibilityProps
|
|
19
|
+
}) => (
|
|
20
|
+
<View
|
|
21
|
+
style={[styles.container, containerStyle]}
|
|
22
|
+
accessibilityLabel={accessibilityLabel}
|
|
23
|
+
accessibilityRole="progressbar"
|
|
24
|
+
accessibilityState={{ busy: true }}
|
|
25
|
+
{...accessibilityProps}
|
|
26
|
+
>
|
|
27
|
+
<ActivityIndicator size={size} color={color} />
|
|
28
|
+
</View>
|
|
29
|
+
));
|
|
30
|
+
|
|
31
|
+
Loading.displayName = "Loading";
|
|
32
|
+
|
|
33
|
+
const styles = StyleSheet.create({
|
|
34
|
+
container: {
|
|
35
|
+
flex: 1,
|
|
36
|
+
justifyContent: "center",
|
|
37
|
+
alignItems: "center",
|
|
38
|
+
padding: 20,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -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
|
+
}
|