@uploadista/react-native-core 0.0.3

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.
@@ -0,0 +1,130 @@
1
+ import { type ReactNode, useEffect } from "react";
2
+ import {
3
+ ActivityIndicator,
4
+ Pressable,
5
+ StyleSheet,
6
+ Text,
7
+ View,
8
+ } from "react-native";
9
+ import { useFileUpload } from "../hooks";
10
+ import type { UseFileUploadOptions } from "../types";
11
+ import { UploadProgress } from "./UploadProgress";
12
+
13
+ export interface FileUploadButtonProps {
14
+ /** Options for file upload */
15
+ options?: UseFileUploadOptions;
16
+ /** Button label text */
17
+ label?: string;
18
+ /** Custom button content */
19
+ children?: ReactNode;
20
+ /** Callback when upload completes successfully */
21
+ onSuccess?: (result: unknown) => void;
22
+ /** Callback when upload fails */
23
+ onError?: (error: Error) => void;
24
+ /** Callback when upload is cancelled */
25
+ onCancel?: () => void;
26
+ /** Whether to show progress inline */
27
+ showProgress?: boolean;
28
+ }
29
+
30
+ /**
31
+ * Button component for document/file selection and upload
32
+ * Generic file picker with progress display
33
+ */
34
+ export function FileUploadButton({
35
+ options,
36
+ label = "Choose File",
37
+ children,
38
+ onSuccess,
39
+ onError,
40
+ onCancel,
41
+ showProgress = true,
42
+ }: FileUploadButtonProps) {
43
+ const { state, pickAndUpload } = useFileUpload(options);
44
+
45
+ const handlePress = async () => {
46
+ try {
47
+ await pickAndUpload();
48
+ } catch (error) {
49
+ if (error instanceof Error) {
50
+ if (
51
+ error.message.includes("cancelled") ||
52
+ error.message.includes("aborted")
53
+ ) {
54
+ onCancel?.();
55
+ } else {
56
+ onError?.(error);
57
+ }
58
+ }
59
+ }
60
+ };
61
+
62
+ const isLoading = state.status === "uploading";
63
+ const isDisabled = isLoading || state.status === "aborted";
64
+
65
+ useEffect(() => {
66
+ if (state.status === "success" && state.result) {
67
+ onSuccess?.(state.result);
68
+ }
69
+ }, [state.status, state.result, onSuccess]);
70
+
71
+ useEffect(() => {
72
+ if (state.status === "error" && state.error) {
73
+ onError?.(state.error);
74
+ }
75
+ }, [state.status, state.error, onError]);
76
+
77
+ return (
78
+ <View style={styles.container}>
79
+ <Pressable
80
+ style={[styles.button, isDisabled && styles.buttonDisabled]}
81
+ onPress={handlePress}
82
+ disabled={isDisabled}
83
+ >
84
+ {isLoading && (
85
+ <ActivityIndicator
86
+ size="small"
87
+ color="#FFFFFF"
88
+ style={styles.spinner}
89
+ />
90
+ )}
91
+ <Text style={styles.buttonText}>{children || label}</Text>
92
+ </Pressable>
93
+ {showProgress && state.status !== "idle" && (
94
+ <View style={styles.progressContainer}>
95
+ <UploadProgress state={state} label="File upload" />
96
+ </View>
97
+ )}
98
+ </View>
99
+ );
100
+ }
101
+
102
+ const styles = StyleSheet.create({
103
+ container: {
104
+ gap: 8,
105
+ },
106
+ button: {
107
+ flexDirection: "row",
108
+ alignItems: "center",
109
+ justifyContent: "center",
110
+ paddingVertical: 12,
111
+ paddingHorizontal: 16,
112
+ backgroundColor: "#FF9500",
113
+ borderRadius: 8,
114
+ gap: 8,
115
+ },
116
+ buttonDisabled: {
117
+ opacity: 0.6,
118
+ },
119
+ buttonText: {
120
+ fontSize: 16,
121
+ fontWeight: "600",
122
+ color: "#FFFFFF",
123
+ },
124
+ spinner: {
125
+ marginRight: 4,
126
+ },
127
+ progressContainer: {
128
+ marginTop: 4,
129
+ },
130
+ });
@@ -0,0 +1,199 @@
1
+ import React, { type ReactNode } from "react";
2
+ import {
3
+ ActivityIndicator,
4
+ FlatList,
5
+ Pressable,
6
+ StyleSheet,
7
+ Text,
8
+ View,
9
+ } from "react-native";
10
+ import { useGalleryUpload } from "../hooks";
11
+ import type { UseGalleryUploadOptions } from "../types";
12
+ import { UploadProgress } from "./UploadProgress";
13
+
14
+ export interface GalleryUploadButtonProps {
15
+ /** Options for gallery upload */
16
+ options?: UseGalleryUploadOptions;
17
+ /** Button label text */
18
+ label?: string;
19
+ /** Custom button content */
20
+ children?: ReactNode;
21
+ /** Callback when all uploads complete successfully */
22
+ onSuccess?: (results: unknown[]) => void;
23
+ /** Callback when any upload fails */
24
+ onError?: (error: Error) => void;
25
+ /** Callback when upload is cancelled */
26
+ onCancel?: () => void;
27
+ /** Whether to show individual progress for each file */
28
+ showProgress?: boolean;
29
+ }
30
+
31
+ /**
32
+ * Button component for gallery selection and batch upload
33
+ * Triggers gallery picker on press and handles concurrent uploads
34
+ */
35
+ export function GalleryUploadButton({
36
+ options,
37
+ label = "Select from Gallery",
38
+ children,
39
+ onSuccess,
40
+ onError,
41
+ onCancel,
42
+ showProgress = true,
43
+ }: GalleryUploadButtonProps) {
44
+ const { state, selectAndUpload } = useGalleryUpload(options);
45
+
46
+ const handlePress = async () => {
47
+ try {
48
+ await selectAndUpload();
49
+ } catch (error) {
50
+ if (error instanceof Error) {
51
+ if (
52
+ error.message.includes("cancelled") ||
53
+ error.message.includes("aborted")
54
+ ) {
55
+ onCancel?.();
56
+ } else {
57
+ onError?.(error);
58
+ }
59
+ }
60
+ }
61
+ };
62
+
63
+ const isLoading = state.items.some((item) => item.status === "uploading");
64
+ const hasItems = state.items.length > 0;
65
+ const allComplete =
66
+ hasItems &&
67
+ state.items.every(
68
+ (item) => item.status !== "uploading" && item.status !== "idle",
69
+ );
70
+
71
+ React.useEffect(() => {
72
+ if (allComplete) {
73
+ const results = state.items
74
+ .filter((item) => item.status === "success")
75
+ .map((item) => item.result);
76
+ if (results.length > 0) {
77
+ onSuccess?.(results);
78
+ }
79
+ }
80
+ }, [allComplete, state.items, onSuccess]);
81
+
82
+ React.useEffect(() => {
83
+ const errors = state.items.filter((item) => item.status === "error");
84
+ const firstError = errors[0]?.error;
85
+ if (firstError) {
86
+ onError?.(firstError);
87
+ }
88
+ }, [state.items, onError]);
89
+
90
+ const renderItem = ({ item }: { item: (typeof state.items)[0] }) => (
91
+ <View key={item.id} style={styles.itemContainer}>
92
+ <UploadProgress
93
+ state={{
94
+ status: item.status,
95
+ progress: item.progress,
96
+ bytesUploaded: item.bytesUploaded,
97
+ totalBytes: item.totalBytes,
98
+ error: item.error,
99
+ result: item.result,
100
+ }}
101
+ label={item.file.name}
102
+ />
103
+ </View>
104
+ );
105
+
106
+ return (
107
+ <View style={styles.container}>
108
+ <Pressable
109
+ style={[styles.button, isLoading && styles.buttonDisabled]}
110
+ onPress={handlePress}
111
+ disabled={isLoading}
112
+ >
113
+ {isLoading && (
114
+ <ActivityIndicator
115
+ size="small"
116
+ color="#FFFFFF"
117
+ style={styles.spinner}
118
+ />
119
+ )}
120
+ <Text style={styles.buttonText}>
121
+ {children || label}
122
+ {hasItems && ` (${state.items.length})`}
123
+ </Text>
124
+ </Pressable>
125
+
126
+ {hasItems && (
127
+ <View style={styles.statsContainer}>
128
+ <Text style={styles.statsText}>
129
+ Progress: {state.items.filter((i) => i.status === "success").length}
130
+ /{state.items.length} uploaded
131
+ </Text>
132
+ <Text style={styles.statsText}>Overall: {state.totalProgress}%</Text>
133
+ </View>
134
+ )}
135
+
136
+ {showProgress && hasItems && (
137
+ <FlatList
138
+ scrollEnabled={false}
139
+ data={state.items}
140
+ renderItem={renderItem}
141
+ keyExtractor={(item) => item.id}
142
+ style={styles.listContainer}
143
+ contentContainerStyle={styles.listContent}
144
+ ItemSeparatorComponent={() => <View style={styles.separator} />}
145
+ />
146
+ )}
147
+ </View>
148
+ );
149
+ }
150
+
151
+ const styles = StyleSheet.create({
152
+ container: {
153
+ gap: 8,
154
+ },
155
+ button: {
156
+ flexDirection: "row",
157
+ alignItems: "center",
158
+ justifyContent: "center",
159
+ paddingVertical: 12,
160
+ paddingHorizontal: 16,
161
+ backgroundColor: "#34C759",
162
+ borderRadius: 8,
163
+ gap: 8,
164
+ },
165
+ buttonDisabled: {
166
+ opacity: 0.6,
167
+ },
168
+ buttonText: {
169
+ fontSize: 16,
170
+ fontWeight: "600",
171
+ color: "#FFFFFF",
172
+ },
173
+ spinner: {
174
+ marginRight: 4,
175
+ },
176
+ statsContainer: {
177
+ paddingVertical: 8,
178
+ paddingHorizontal: 12,
179
+ backgroundColor: "#f5f5f5",
180
+ borderRadius: 4,
181
+ gap: 4,
182
+ },
183
+ statsText: {
184
+ fontSize: 12,
185
+ color: "#666666",
186
+ },
187
+ listContainer: {
188
+ maxHeight: 400,
189
+ },
190
+ listContent: {
191
+ gap: 8,
192
+ },
193
+ itemContainer: {
194
+ paddingHorizontal: 0,
195
+ },
196
+ separator: {
197
+ height: 4,
198
+ },
199
+ });
@@ -0,0 +1,214 @@
1
+ import { FlatList, Pressable, StyleSheet, Text, View } from "react-native";
2
+ import type { UploadItem } from "../types";
3
+ import { UploadProgress } from "./UploadProgress";
4
+
5
+ export interface UploadListProps {
6
+ /** List of upload items to display */
7
+ items: UploadItem[];
8
+ /** Callback when remove item is pressed */
9
+ onRemove?: (id: string) => void;
10
+ /** Callback when item is pressed */
11
+ onItemPress?: (item: UploadItem) => void;
12
+ /** Whether to show remove button */
13
+ showRemoveButton?: boolean;
14
+ }
15
+
16
+ /**
17
+ * Component to display a list of upload items with individual progress
18
+ * Shows status indicators and allows removal of items
19
+ */
20
+ export function UploadList({
21
+ items,
22
+ onRemove,
23
+ onItemPress,
24
+ showRemoveButton = true,
25
+ }: UploadListProps) {
26
+ const renderItem = ({ item }: { item: UploadItem }) => (
27
+ <Pressable
28
+ style={[
29
+ styles.itemContainer,
30
+ { borderLeftColor: getStatusColor(item.progress.state) },
31
+ ]}
32
+ onPress={() => onItemPress?.(item)}
33
+ >
34
+ <View style={styles.itemContent}>
35
+ <View style={styles.itemHeader}>
36
+ <Text style={styles.fileName} numberOfLines={1}>
37
+ {item.file.name}
38
+ </Text>
39
+ <Text style={styles.fileSize}>
40
+ {getFileSizeDisplay(item.file.size)}
41
+ </Text>
42
+ </View>
43
+ <View style={styles.progressWrapper}>
44
+ <UploadProgress
45
+ state={{
46
+ status:
47
+ item.progress.state === "pending"
48
+ ? "idle"
49
+ : item.progress.state === "cancelled"
50
+ ? "aborted"
51
+ : item.progress.state,
52
+ progress: item.progress.progress,
53
+ bytesUploaded: item.progress.uploadedBytes,
54
+ totalBytes: item.progress.totalBytes,
55
+ error: item.progress.error || null,
56
+ result: (item.result as any) || null,
57
+ }}
58
+ />
59
+ </View>
60
+ </View>
61
+ {showRemoveButton &&
62
+ item.progress.state !== "uploading" &&
63
+ item.progress.state !== "pending" && (
64
+ <Pressable
65
+ style={styles.removeButton}
66
+ onPress={() => onRemove?.(item.id)}
67
+ hitSlop={{ top: 8, right: 8, bottom: 8, left: 8 }}
68
+ >
69
+ <Text style={styles.removeButtonText}>✕</Text>
70
+ </Pressable>
71
+ )}
72
+ </Pressable>
73
+ );
74
+
75
+ if (items.length === 0) {
76
+ return (
77
+ <View style={styles.emptyContainer}>
78
+ <Text style={styles.emptyText}>No uploads</Text>
79
+ </View>
80
+ );
81
+ }
82
+
83
+ return (
84
+ <View style={styles.container}>
85
+ <View style={styles.headerRow}>
86
+ <Text style={styles.headerText}>Uploads ({items.length})</Text>
87
+ <Text style={styles.headerSubtext}>
88
+ {items.filter((i) => i.progress.state === "success").length} complete
89
+ </Text>
90
+ </View>
91
+ <FlatList
92
+ scrollEnabled={false}
93
+ data={items}
94
+ renderItem={renderItem}
95
+ keyExtractor={(item) => item.id}
96
+ ItemSeparatorComponent={() => <View style={styles.separator} />}
97
+ contentContainerStyle={styles.listContent}
98
+ />
99
+ </View>
100
+ );
101
+ }
102
+
103
+ // Helper functions
104
+ function getStatusColor(state: string): string {
105
+ switch (state) {
106
+ case "success":
107
+ return "#34C759";
108
+ case "error":
109
+ case "cancelled":
110
+ return "#FF3B30";
111
+ case "uploading":
112
+ case "pending":
113
+ return "#007AFF";
114
+ default:
115
+ return "#999999";
116
+ }
117
+ }
118
+
119
+ function getFileSizeDisplay(bytes: number): string {
120
+ if (bytes === 0) return "0 B";
121
+ const k = 1024;
122
+ const sizes = ["B", "KB", "MB", "GB"];
123
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
124
+ return `${Math.round((bytes / k ** i) * 10) / 10} ${sizes[i]}`;
125
+ }
126
+
127
+ const styles = StyleSheet.create({
128
+ container: {
129
+ gap: 8,
130
+ },
131
+ headerRow: {
132
+ flexDirection: "row",
133
+ justifyContent: "space-between",
134
+ alignItems: "center",
135
+ paddingHorizontal: 12,
136
+ paddingVertical: 8,
137
+ backgroundColor: "#f9f9f9",
138
+ borderRadius: 4,
139
+ },
140
+ headerText: {
141
+ fontSize: 16,
142
+ fontWeight: "600",
143
+ color: "#333333",
144
+ },
145
+ headerSubtext: {
146
+ fontSize: 14,
147
+ color: "#666666",
148
+ },
149
+ listContent: {
150
+ gap: 8,
151
+ },
152
+ itemContainer: {
153
+ flexDirection: "row",
154
+ alignItems: "center",
155
+ paddingVertical: 8,
156
+ paddingHorizontal: 12,
157
+ borderLeftWidth: 4,
158
+ backgroundColor: "#f5f5f5",
159
+ borderRadius: 4,
160
+ gap: 8,
161
+ },
162
+ itemContent: {
163
+ flex: 1,
164
+ gap: 6,
165
+ },
166
+ itemHeader: {
167
+ flexDirection: "row",
168
+ justifyContent: "space-between",
169
+ alignItems: "center",
170
+ },
171
+ fileName: {
172
+ fontSize: 14,
173
+ fontWeight: "500",
174
+ color: "#333333",
175
+ flex: 1,
176
+ },
177
+ fileSize: {
178
+ fontSize: 12,
179
+ color: "#999999",
180
+ marginLeft: 8,
181
+ },
182
+ progressWrapper: {
183
+ marginTop: 2,
184
+ },
185
+ removeButton: {
186
+ width: 32,
187
+ height: 32,
188
+ justifyContent: "center",
189
+ alignItems: "center",
190
+ borderRadius: 16,
191
+ backgroundColor: "#FFE5E5",
192
+ },
193
+ removeButtonText: {
194
+ fontSize: 16,
195
+ fontWeight: "600",
196
+ color: "#FF3B30",
197
+ },
198
+ separator: {
199
+ height: 4,
200
+ },
201
+ emptyContainer: {
202
+ paddingVertical: 24,
203
+ paddingHorizontal: 12,
204
+ backgroundColor: "#f5f5f5",
205
+ borderRadius: 4,
206
+ alignItems: "center",
207
+ justifyContent: "center",
208
+ },
209
+ emptyText: {
210
+ fontSize: 14,
211
+ color: "#999999",
212
+ fontStyle: "italic",
213
+ },
214
+ });