@umituz/react-native-design-system 2.6.107 → 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/exception/domain/entities/ExceptionEntity.ts +115 -0
- package/src/exception/domain/repositories/IExceptionRepository.ts +37 -0
- package/src/exception/index.ts +65 -0
- package/src/exception/infrastructure/services/ExceptionHandler.ts +93 -0
- package/src/exception/infrastructure/services/ExceptionLogger.ts +142 -0
- package/src/exception/infrastructure/services/ExceptionReporter.ts +134 -0
- package/src/exception/infrastructure/services/ExceptionService.ts +154 -0
- package/src/exception/infrastructure/storage/ExceptionStore.ts +44 -0
- package/src/exception/presentation/components/ErrorBoundary.tsx +129 -0
- package/src/exception/presentation/components/ExceptionEmptyState.tsx +123 -0
- package/src/exception/presentation/components/ExceptionErrorState.tsx +118 -0
- package/src/exports/exception.ts +7 -0
- package/src/exports/infinite-scroll.ts +7 -0
- package/src/exports/uuid.ts +7 -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,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exception Service - Infrastructure Layer
|
|
3
|
+
*
|
|
4
|
+
* Facade for exception handling using composition.
|
|
5
|
+
* Delegates to specialized services for specific operations.
|
|
6
|
+
*
|
|
7
|
+
* SOLID: Facade pattern - Single entry point, delegates to specialists
|
|
8
|
+
* DRY: Avoids code duplication by composing smaller services
|
|
9
|
+
* KISS: Simple interface, complex operations delegated
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
ExceptionEntity,
|
|
14
|
+
ExceptionContext,
|
|
15
|
+
ExceptionSeverity,
|
|
16
|
+
ExceptionCategory,
|
|
17
|
+
} from '../../domain/entities/ExceptionEntity';
|
|
18
|
+
import { ExceptionHandler } from './ExceptionHandler';
|
|
19
|
+
import { ExceptionReporter } from './ExceptionReporter';
|
|
20
|
+
import { ExceptionLogger } from './ExceptionLogger';
|
|
21
|
+
import { useExceptionStore } from '../storage/ExceptionStore';
|
|
22
|
+
|
|
23
|
+
export class ExceptionService {
|
|
24
|
+
private logger: ExceptionLogger;
|
|
25
|
+
private reporter: ExceptionReporter;
|
|
26
|
+
|
|
27
|
+
constructor(reporterConfig?: ExceptionReporter['config']) {
|
|
28
|
+
this.logger = new ExceptionLogger();
|
|
29
|
+
this.reporter = new ExceptionReporter(
|
|
30
|
+
reporterConfig || {
|
|
31
|
+
enabled: false,
|
|
32
|
+
environment: 'development'
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Handle an exception
|
|
39
|
+
*/
|
|
40
|
+
async handleException(
|
|
41
|
+
error: Error,
|
|
42
|
+
severity: ExceptionSeverity = 'error',
|
|
43
|
+
category: ExceptionCategory = 'unknown',
|
|
44
|
+
context: ExceptionContext = {},
|
|
45
|
+
): Promise<void> {
|
|
46
|
+
const exception = ExceptionHandler.createException(error, severity, category, context);
|
|
47
|
+
|
|
48
|
+
// Add to store
|
|
49
|
+
useExceptionStore.getState().addException(exception);
|
|
50
|
+
|
|
51
|
+
// Log locally
|
|
52
|
+
await this.logger.logException(exception);
|
|
53
|
+
|
|
54
|
+
// Report to external service if needed
|
|
55
|
+
if (ExceptionHandler.shouldReportException(exception)) {
|
|
56
|
+
await this.reporter.reportException(exception);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Mark as handled
|
|
60
|
+
useExceptionStore.getState().markAsHandled(exception.id);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Handle network errors
|
|
65
|
+
*/
|
|
66
|
+
async handleNetworkError(error: Error, context: ExceptionContext = {}): Promise<void> {
|
|
67
|
+
await this.handleException(error, 'error', 'network', context);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Handle validation errors
|
|
72
|
+
*/
|
|
73
|
+
async handleValidationError(error: Error, context: ExceptionContext = {}): Promise<void> {
|
|
74
|
+
await this.handleException(error, 'warning', 'validation', context);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Handle authentication errors
|
|
79
|
+
*/
|
|
80
|
+
async handleAuthError(error: Error, context: ExceptionContext = {}): Promise<void> {
|
|
81
|
+
await this.handleException(error, 'error', 'authentication', context);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Handle system errors
|
|
86
|
+
*/
|
|
87
|
+
async handleSystemError(error: Error, context: ExceptionContext = {}): Promise<void> {
|
|
88
|
+
await this.handleException(error, 'fatal', 'system', context);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get stored exceptions
|
|
93
|
+
*/
|
|
94
|
+
async getStoredExceptions(): Promise<ExceptionEntity[]> {
|
|
95
|
+
return this.logger.getStoredExceptions();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get exception statistics
|
|
100
|
+
*/
|
|
101
|
+
async getExceptionStats() {
|
|
102
|
+
return this.logger.getExceptionStats();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Clear stored exceptions
|
|
107
|
+
*/
|
|
108
|
+
async clearStoredExceptions(): Promise<void> {
|
|
109
|
+
await this.logger.clearStoredExceptions();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Update reporter configuration
|
|
114
|
+
*/
|
|
115
|
+
updateReporterConfig(config: Partial<ExceptionReporter['config']>): void {
|
|
116
|
+
this.reporter.updateConfig(config);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Set max stored exceptions
|
|
121
|
+
*/
|
|
122
|
+
setMaxStoredExceptions(limit: number): void {
|
|
123
|
+
this.logger.setMaxStoredExceptions(limit);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Handle storage/permission errors
|
|
128
|
+
*/
|
|
129
|
+
async handleStorageError(error: Error, context: ExceptionContext = {}): Promise<void> {
|
|
130
|
+
await this.handleException(error, 'error', 'storage', context);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Handle fatal errors
|
|
135
|
+
*/
|
|
136
|
+
async handleFatalError(error: Error, context: ExceptionContext = {}): Promise<void> {
|
|
137
|
+
await this.handleException(error, 'fatal', 'system', context);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Clear all exceptions
|
|
142
|
+
*/
|
|
143
|
+
clearExceptions(): void {
|
|
144
|
+
useExceptionStore.getState().clearExceptions();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Export default instance
|
|
149
|
+
export const exceptionService = new ExceptionService();
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exception Store
|
|
3
|
+
* Zustand store for exception state management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createStore } from '@umituz/react-native-storage';
|
|
7
|
+
import type { ExceptionEntity } from '../../domain/entities/ExceptionEntity';
|
|
8
|
+
|
|
9
|
+
interface ExceptionState {
|
|
10
|
+
exceptions: ExceptionEntity[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ExceptionActions {
|
|
14
|
+
addException: (exception: ExceptionEntity) => void;
|
|
15
|
+
clearExceptions: () => void;
|
|
16
|
+
markAsHandled: (id: string) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const useExceptionStore = createStore<ExceptionState, ExceptionActions>({
|
|
20
|
+
name: 'exception-store',
|
|
21
|
+
initialState: {
|
|
22
|
+
exceptions: [],
|
|
23
|
+
},
|
|
24
|
+
persist: false,
|
|
25
|
+
actions: (set: (state: Partial<ExceptionState>) => void, get: () => ExceptionState) => ({
|
|
26
|
+
addException: (exception: ExceptionEntity) =>
|
|
27
|
+
set({ exceptions: [...get().exceptions, exception] }),
|
|
28
|
+
clearExceptions: () => set({ exceptions: [] }),
|
|
29
|
+
markAsHandled: (id: string) =>
|
|
30
|
+
set({
|
|
31
|
+
exceptions: get().exceptions.map((e: ExceptionEntity) =>
|
|
32
|
+
e.id === id ? { ...e, handled: true } : e
|
|
33
|
+
),
|
|
34
|
+
}),
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Hook to get exceptions from store
|
|
40
|
+
*/
|
|
41
|
+
export const useExceptions = () => {
|
|
42
|
+
const exceptions = useExceptionStore((state: ExceptionState) => state.exceptions);
|
|
43
|
+
return exceptions;
|
|
44
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Boundary Component
|
|
3
|
+
* Catches React errors and provides fallback UI
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { Component, ReactNode } from 'react';
|
|
7
|
+
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
|
|
8
|
+
import { exceptionService } from '../../infrastructure/services/ExceptionService';
|
|
9
|
+
import { useAppDesignTokens } from '../../../theme';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
fallback?: ReactNode;
|
|
14
|
+
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface State {
|
|
18
|
+
hasError: boolean;
|
|
19
|
+
error: Error | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class ErrorBoundary extends Component<Props, State> {
|
|
23
|
+
constructor(props: Props) {
|
|
24
|
+
super(props);
|
|
25
|
+
this.state = {
|
|
26
|
+
hasError: false,
|
|
27
|
+
error: null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static getDerivedStateFromError(error: Error): State {
|
|
32
|
+
return {
|
|
33
|
+
hasError: true,
|
|
34
|
+
error,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
|
|
39
|
+
// Log error to exception service
|
|
40
|
+
exceptionService.handleFatalError(error, {
|
|
41
|
+
componentStack: errorInfo.componentStack ?? undefined,
|
|
42
|
+
screen: 'ErrorBoundary',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Call external onError if provided
|
|
46
|
+
if (this.props.onError) {
|
|
47
|
+
this.props.onError(error, errorInfo);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
handleReset = (): void => {
|
|
52
|
+
this.setState({
|
|
53
|
+
hasError: false,
|
|
54
|
+
error: null,
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
render(): ReactNode {
|
|
59
|
+
if (this.state.hasError) {
|
|
60
|
+
if (this.props.fallback) {
|
|
61
|
+
return this.props.fallback;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<ErrorDisplay error={this.state.error} onReset={this.handleReset} />
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return this.props.children;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface ErrorDisplayProps {
|
|
74
|
+
error: Error | null;
|
|
75
|
+
onReset: () => void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const ErrorDisplay: React.FC<ErrorDisplayProps> = ({ error, onReset }) => {
|
|
79
|
+
const tokens = useAppDesignTokens();
|
|
80
|
+
const styles = getStyles(tokens);
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<View style={styles.container}>
|
|
84
|
+
<Text style={styles.title}>Something went wrong</Text>
|
|
85
|
+
<Text style={styles.message}>
|
|
86
|
+
{error?.message || 'An unexpected error occurred'}
|
|
87
|
+
</Text>
|
|
88
|
+
<TouchableOpacity style={styles.button} onPress={onReset}>
|
|
89
|
+
<Text style={styles.buttonText}>Try Again</Text>
|
|
90
|
+
</TouchableOpacity>
|
|
91
|
+
</View>
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const getStyles = (tokens: ReturnType<typeof useAppDesignTokens>) =>
|
|
96
|
+
StyleSheet.create({
|
|
97
|
+
button: {
|
|
98
|
+
backgroundColor: tokens.colors.primary,
|
|
99
|
+
borderRadius: tokens.borders.radius.sm,
|
|
100
|
+
paddingHorizontal: tokens.spacing.lg,
|
|
101
|
+
paddingVertical: tokens.spacing.sm,
|
|
102
|
+
},
|
|
103
|
+
buttonText: {
|
|
104
|
+
color: tokens.colors.textInverse,
|
|
105
|
+
fontSize: tokens.typography.bodyLarge.fontSize,
|
|
106
|
+
fontWeight: tokens.typography.labelLarge.fontWeight,
|
|
107
|
+
},
|
|
108
|
+
container: {
|
|
109
|
+
alignItems: 'center',
|
|
110
|
+
backgroundColor: tokens.colors.backgroundPrimary,
|
|
111
|
+
flex: 1,
|
|
112
|
+
justifyContent: 'center',
|
|
113
|
+
padding: tokens.spacing.lg,
|
|
114
|
+
},
|
|
115
|
+
message: {
|
|
116
|
+
color: tokens.colors.textSecondary,
|
|
117
|
+
fontSize: tokens.typography.bodyLarge.fontSize,
|
|
118
|
+
marginBottom: tokens.spacing.lg,
|
|
119
|
+
textAlign: 'center',
|
|
120
|
+
},
|
|
121
|
+
title: {
|
|
122
|
+
color: tokens.colors.textPrimary,
|
|
123
|
+
fontSize: tokens.typography.headlineSmall.fontSize,
|
|
124
|
+
fontWeight: tokens.typography.headlineSmall.fontWeight,
|
|
125
|
+
marginBottom: tokens.spacing.sm,
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Empty State Component
|
|
3
|
+
* Displays when no data is available
|
|
4
|
+
*
|
|
5
|
+
* Presentation Layer - UI Component
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from "react";
|
|
9
|
+
import { View, StyleSheet, TouchableOpacity } from "react-native";
|
|
10
|
+
import { AtomicIcon, AtomicText } from "../../../atoms";
|
|
11
|
+
import { useAppDesignTokens } from "../../../theme";
|
|
12
|
+
|
|
13
|
+
export interface ExceptionEmptyStateProps {
|
|
14
|
+
icon?: string;
|
|
15
|
+
title: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
actionLabel?: string;
|
|
18
|
+
onAction?: () => void;
|
|
19
|
+
illustration?: React.ReactNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const ExceptionEmptyState: React.FC<ExceptionEmptyStateProps> = ({
|
|
23
|
+
icon = "inbox",
|
|
24
|
+
title,
|
|
25
|
+
description,
|
|
26
|
+
actionLabel,
|
|
27
|
+
onAction,
|
|
28
|
+
illustration,
|
|
29
|
+
}) => {
|
|
30
|
+
const tokens = useAppDesignTokens();
|
|
31
|
+
|
|
32
|
+
const styles = React.useMemo(
|
|
33
|
+
() =>
|
|
34
|
+
StyleSheet.create({
|
|
35
|
+
actionButton: {
|
|
36
|
+
borderRadius: tokens.borders.radius.md,
|
|
37
|
+
marginTop: tokens.spacing.sm,
|
|
38
|
+
paddingHorizontal: tokens.spacing.lg,
|
|
39
|
+
paddingVertical: tokens.spacing.md,
|
|
40
|
+
},
|
|
41
|
+
actionButtonText: {
|
|
42
|
+
// AtomicText handles typography
|
|
43
|
+
},
|
|
44
|
+
container: {
|
|
45
|
+
alignItems: "center",
|
|
46
|
+
flex: 1,
|
|
47
|
+
justifyContent: "center",
|
|
48
|
+
padding: tokens.spacing.xl,
|
|
49
|
+
},
|
|
50
|
+
description: {
|
|
51
|
+
marginBottom: tokens.spacing.lg,
|
|
52
|
+
maxWidth: 280,
|
|
53
|
+
},
|
|
54
|
+
iconContainer: {
|
|
55
|
+
alignItems: "center",
|
|
56
|
+
borderRadius: 60,
|
|
57
|
+
height: 120,
|
|
58
|
+
justifyContent: "center",
|
|
59
|
+
marginBottom: tokens.spacing.lg,
|
|
60
|
+
width: 120,
|
|
61
|
+
},
|
|
62
|
+
title: {
|
|
63
|
+
marginBottom: tokens.spacing.sm,
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
[tokens],
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<View style={styles.container}>
|
|
71
|
+
{illustration ? (
|
|
72
|
+
illustration
|
|
73
|
+
) : (
|
|
74
|
+
<View
|
|
75
|
+
style={[
|
|
76
|
+
styles.iconContainer,
|
|
77
|
+
{ backgroundColor: tokens.colors.surface },
|
|
78
|
+
]}
|
|
79
|
+
>
|
|
80
|
+
<AtomicIcon name={icon} size="xxl" color="secondary" />
|
|
81
|
+
</View>
|
|
82
|
+
)}
|
|
83
|
+
|
|
84
|
+
<AtomicText
|
|
85
|
+
type="headlineSmall"
|
|
86
|
+
color="primary"
|
|
87
|
+
style={[styles.title, { textAlign: "center" }]}
|
|
88
|
+
>
|
|
89
|
+
{title}
|
|
90
|
+
</AtomicText>
|
|
91
|
+
|
|
92
|
+
{description && (
|
|
93
|
+
<AtomicText
|
|
94
|
+
type="bodyMedium"
|
|
95
|
+
color="secondary"
|
|
96
|
+
style={[styles.description, { textAlign: "center" }]}
|
|
97
|
+
>
|
|
98
|
+
{description}
|
|
99
|
+
</AtomicText>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
{actionLabel && onAction && (
|
|
103
|
+
<TouchableOpacity
|
|
104
|
+
style={[
|
|
105
|
+
styles.actionButton,
|
|
106
|
+
{ backgroundColor: tokens.colors.primary },
|
|
107
|
+
]}
|
|
108
|
+
onPress={onAction}
|
|
109
|
+
activeOpacity={0.8}
|
|
110
|
+
>
|
|
111
|
+
<AtomicText
|
|
112
|
+
type="labelLarge"
|
|
113
|
+
color="onPrimary"
|
|
114
|
+
style={styles.actionButtonText}
|
|
115
|
+
>
|
|
116
|
+
{actionLabel}
|
|
117
|
+
</AtomicText>
|
|
118
|
+
</TouchableOpacity>
|
|
119
|
+
)}
|
|
120
|
+
</View>
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error State Component
|
|
3
|
+
* Generic error display with retry action
|
|
4
|
+
*
|
|
5
|
+
* Presentation Layer - UI Component
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from "react";
|
|
9
|
+
import { View, StyleSheet, TouchableOpacity } from "react-native";
|
|
10
|
+
import {
|
|
11
|
+
AtomicIcon,
|
|
12
|
+
AtomicText,
|
|
13
|
+
} from "../../../atoms";
|
|
14
|
+
import {
|
|
15
|
+
useAppDesignTokens,
|
|
16
|
+
} from "../../../theme";
|
|
17
|
+
|
|
18
|
+
export interface ExceptionErrorStateProps {
|
|
19
|
+
/** Icon name from Ionicons */
|
|
20
|
+
icon?: string;
|
|
21
|
+
/** Error title */
|
|
22
|
+
title: string;
|
|
23
|
+
/** Error description */
|
|
24
|
+
description?: string;
|
|
25
|
+
/** Retry button label */
|
|
26
|
+
actionLabel?: string;
|
|
27
|
+
/** Retry action callback */
|
|
28
|
+
onAction?: () => void;
|
|
29
|
+
/** Custom illustration instead of icon */
|
|
30
|
+
illustration?: React.ReactNode;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const ExceptionErrorState: React.FC<ExceptionErrorStateProps> = ({
|
|
34
|
+
icon = "alert-circle-outline",
|
|
35
|
+
title,
|
|
36
|
+
description,
|
|
37
|
+
actionLabel,
|
|
38
|
+
onAction,
|
|
39
|
+
illustration,
|
|
40
|
+
}) => {
|
|
41
|
+
const tokens = useAppDesignTokens();
|
|
42
|
+
|
|
43
|
+
const styles = React.useMemo(
|
|
44
|
+
() =>
|
|
45
|
+
StyleSheet.create({
|
|
46
|
+
actionButton: {
|
|
47
|
+
alignItems: "center",
|
|
48
|
+
borderRadius: tokens.borders.radius.full,
|
|
49
|
+
flexDirection: "row",
|
|
50
|
+
gap: tokens.spacing.sm,
|
|
51
|
+
marginTop: tokens.spacing.sm,
|
|
52
|
+
paddingHorizontal: tokens.spacing.lg,
|
|
53
|
+
paddingVertical: tokens.spacing.md,
|
|
54
|
+
},
|
|
55
|
+
container: {
|
|
56
|
+
alignItems: "center",
|
|
57
|
+
flex: 1,
|
|
58
|
+
justifyContent: "center",
|
|
59
|
+
padding: tokens.spacing.xl,
|
|
60
|
+
},
|
|
61
|
+
description: {
|
|
62
|
+
marginBottom: tokens.spacing.lg,
|
|
63
|
+
maxWidth: 280,
|
|
64
|
+
textAlign: "center",
|
|
65
|
+
},
|
|
66
|
+
iconContainer: {
|
|
67
|
+
alignItems: "center",
|
|
68
|
+
borderRadius: 60,
|
|
69
|
+
height: 120,
|
|
70
|
+
justifyContent: "center",
|
|
71
|
+
marginBottom: tokens.spacing.lg,
|
|
72
|
+
width: 120,
|
|
73
|
+
},
|
|
74
|
+
title: {
|
|
75
|
+
marginBottom: tokens.spacing.sm,
|
|
76
|
+
textAlign: "center",
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
79
|
+
[tokens],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<View style={styles.container}>
|
|
84
|
+
{illustration ? (
|
|
85
|
+
illustration
|
|
86
|
+
) : (
|
|
87
|
+
<View
|
|
88
|
+
style={[styles.iconContainer, { backgroundColor: tokens.colors.surface }]}
|
|
89
|
+
>
|
|
90
|
+
<AtomicIcon name={icon as never} size="xxl" color="secondary" />
|
|
91
|
+
</View>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
<AtomicText type="headlineSmall" color="primary" style={styles.title}>
|
|
95
|
+
{title}
|
|
96
|
+
</AtomicText>
|
|
97
|
+
|
|
98
|
+
{description && (
|
|
99
|
+
<AtomicText type="bodyMedium" color="secondary" style={styles.description}>
|
|
100
|
+
{description}
|
|
101
|
+
</AtomicText>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
{actionLabel && onAction && (
|
|
105
|
+
<TouchableOpacity
|
|
106
|
+
style={[styles.actionButton, { backgroundColor: tokens.colors.primary }]}
|
|
107
|
+
onPress={onAction}
|
|
108
|
+
activeOpacity={0.8}
|
|
109
|
+
>
|
|
110
|
+
<AtomicIcon name="refresh-outline" size="sm" color="onPrimary" />
|
|
111
|
+
<AtomicText type="labelLarge" color="onPrimary">
|
|
112
|
+
{actionLabel}
|
|
113
|
+
</AtomicText>
|
|
114
|
+
</TouchableOpacity>
|
|
115
|
+
)}
|
|
116
|
+
</View>
|
|
117
|
+
);
|
|
118
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -57,6 +57,21 @@ export * from './exports/organisms';
|
|
|
57
57
|
// =============================================================================
|
|
58
58
|
export * from './exports/safe-area';
|
|
59
59
|
|
|
60
|
+
// =============================================================================
|
|
61
|
+
// EXCEPTION EXPORTS
|
|
62
|
+
// =============================================================================
|
|
63
|
+
export * from './exports/exception';
|
|
64
|
+
|
|
65
|
+
// =============================================================================
|
|
66
|
+
// INFINITE SCROLL EXPORTS
|
|
67
|
+
// =============================================================================
|
|
68
|
+
export * from './exports/infinite-scroll';
|
|
69
|
+
|
|
70
|
+
// =============================================================================
|
|
71
|
+
// UUID EXPORTS
|
|
72
|
+
// =============================================================================
|
|
73
|
+
export * from './exports/uuid';
|
|
74
|
+
|
|
60
75
|
// =============================================================================
|
|
61
76
|
// VARIANT UTILITIES
|
|
62
77
|
// =============================================================================
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infinite Scroll List Props Interface
|
|
3
|
+
*
|
|
4
|
+
* Domain interface for component props
|
|
5
|
+
* Follows SOLID, DRY, KISS principles
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type React from "react";
|
|
9
|
+
import type { InfiniteScrollConfig } from "../types/infinite-scroll-config";
|
|
10
|
+
|
|
11
|
+
export interface InfiniteScrollListProps<T> {
|
|
12
|
+
/**
|
|
13
|
+
* Configuration for infinite scroll
|
|
14
|
+
*/
|
|
15
|
+
config: InfiniteScrollConfig<T>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Render function for each item
|
|
19
|
+
*/
|
|
20
|
+
renderItem: (item: T, index: number) => React.ReactElement;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Optional: Custom loading component
|
|
24
|
+
*/
|
|
25
|
+
loadingComponent?: React.ReactElement;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Optional: Custom loading more component
|
|
29
|
+
*/
|
|
30
|
+
loadingMoreComponent?: React.ReactElement;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Optional: Custom empty component
|
|
34
|
+
*/
|
|
35
|
+
emptyComponent?: React.ReactElement;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Optional: Custom error component
|
|
39
|
+
*/
|
|
40
|
+
errorComponent?: (error: string, retry: () => void) => React.ReactElement;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Optional: List header component
|
|
44
|
+
*/
|
|
45
|
+
ListHeaderComponent?: React.ReactElement;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Optional: List footer component
|
|
49
|
+
*/
|
|
50
|
+
ListFooterComponent?: React.ReactElement;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Optional: Additional FlatList props
|
|
54
|
+
*/
|
|
55
|
+
flatListProps?: Omit<
|
|
56
|
+
React.ComponentProps<typeof import("react-native").FlatList<T>>,
|
|
57
|
+
| "data"
|
|
58
|
+
| "renderItem"
|
|
59
|
+
| "keyExtractor"
|
|
60
|
+
| "onEndReached"
|
|
61
|
+
| "onEndReachedThreshold"
|
|
62
|
+
| "onRefresh"
|
|
63
|
+
| "refreshing"
|
|
64
|
+
| "ListHeaderComponent"
|
|
65
|
+
| "ListFooterComponent"
|
|
66
|
+
>;
|
|
67
|
+
}
|