@herca/rn-kit 0.1.101 → 0.1.102

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.
Files changed (42) hide show
  1. package/lib/module/Icon/Icon.js +3 -1
  2. package/lib/module/Icon/Icon.js.map +1 -1
  3. package/lib/module/Icon/icons/ScanQr.js +22 -0
  4. package/lib/module/Icon/icons/ScanQr.js.map +1 -0
  5. package/lib/module/Input/InputFile.js +178 -68
  6. package/lib/module/Input/InputFile.js.map +1 -1
  7. package/lib/module/Input/README_INPUT_FILE.md +179 -105
  8. package/lib/module/Input/TextArea.js +2 -0
  9. package/lib/module/Input/TextArea.js.map +1 -1
  10. package/lib/module/Input/helpers/uploadFile.js +60 -0
  11. package/lib/module/Input/helpers/uploadFile.js.map +1 -0
  12. package/lib/module/Input/partials/InputFile/CardTriggerSmall.js +1 -0
  13. package/lib/module/Input/partials/InputFile/CardTriggerSmall.js.map +1 -1
  14. package/lib/module/Input/partials/InputFile/ItemPreview.js +15 -4
  15. package/lib/module/Input/partials/InputFile/ItemPreview.js.map +1 -1
  16. package/lib/typescript/src/Icon/Icon.d.ts.map +1 -1
  17. package/lib/typescript/src/Icon/icons/ScanQr.d.ts +9 -0
  18. package/lib/typescript/src/Icon/icons/ScanQr.d.ts.map +1 -0
  19. package/lib/typescript/src/Icon/type.d.ts +1 -1
  20. package/lib/typescript/src/Icon/type.d.ts.map +1 -1
  21. package/lib/typescript/src/Input/InputFile.d.ts +3 -48
  22. package/lib/typescript/src/Input/InputFile.d.ts.map +1 -1
  23. package/lib/typescript/src/Input/TextArea.d.ts +1 -1
  24. package/lib/typescript/src/Input/TextArea.d.ts.map +1 -1
  25. package/lib/typescript/src/Input/helpers/uploadFile.d.ts +3 -0
  26. package/lib/typescript/src/Input/helpers/uploadFile.d.ts.map +1 -0
  27. package/lib/typescript/src/Input/partials/InputFile/CardTriggerSmall.d.ts.map +1 -1
  28. package/lib/typescript/src/Input/partials/InputFile/ItemPreview.d.ts +4 -2
  29. package/lib/typescript/src/Input/partials/InputFile/ItemPreview.d.ts.map +1 -1
  30. package/lib/typescript/src/Input/type.d.ts +91 -0
  31. package/lib/typescript/src/Input/type.d.ts.map +1 -1
  32. package/package.json +1 -1
  33. package/src/Icon/Icon.tsx +2 -0
  34. package/src/Icon/icons/ScanQr.tsx +19 -0
  35. package/src/Icon/type.tsx +1 -0
  36. package/src/Input/InputFile.tsx +235 -111
  37. package/src/Input/README_INPUT_FILE.md +179 -105
  38. package/src/Input/TextArea.tsx +2 -1
  39. package/src/Input/helpers/uploadFile.ts +77 -0
  40. package/src/Input/partials/InputFile/CardTriggerSmall.tsx +1 -0
  41. package/src/Input/partials/InputFile/ItemPreview.tsx +25 -4
  42. package/src/Input/type.tsx +105 -0
@@ -1,9 +1,9 @@
1
- import { useEffect, useState } from 'react';
1
+ import { useEffect, useState, useRef } from 'react';
2
2
  import { View } from 'react-native';
3
3
  import Color from '../Color/Color';
4
4
  import Typography from '../Typography/Typography';
5
5
 
6
- import { pick } from '@react-native-documents/picker';
6
+ import { pick, types } from '@react-native-documents/picker';
7
7
  import { viewDocument } from '@react-native-documents/viewer';
8
8
  import { launchCamera, launchImageLibrary } from 'react-native-image-picker';
9
9
  import { spacing } from '../styles/spacing';
@@ -14,64 +14,24 @@ import ModalDelete from './partials/InputFile/ModalDelete';
14
14
  import ModalPicker from './partials/InputFile/ModalPicker';
15
15
  import CardTriggerSmall from './partials/InputFile/CardTriggerSmall';
16
16
  import ReactNativeBlobUtil from 'react-native-blob-util';
17
-
18
- export interface ModalOption {
19
- title?: string;
20
- description?: string;
21
- }
22
- export interface ModalPickFileText {
23
- title?: string;
24
- description?: string;
25
- camera?: ModalOption;
26
- gallery?: ModalOption;
27
- document?: ModalOption;
28
- }
29
-
30
- export interface ChangeLabelProps {
31
- label?: string;
32
- placeholder?: string;
33
- }
34
-
35
- type BaseInputFileProps = {
36
- title?: string;
37
- accept?: string[];
38
- multiple?: boolean;
39
- onChange?: (files: any) => void;
40
- value?: any[];
41
- modalPickFileText?: ModalPickFileText;
42
- btnChooseFileText?: string;
43
- modalDeleteText?: ModalOption & {
44
- confirmBtn?: {
45
- confirm?: string;
46
- cancel?: string;
47
- };
48
- };
49
- hasError?: boolean;
50
- };
51
-
52
- type DefaultVariantProps = BaseInputFileProps & {
53
- variant?: 'default';
54
- description?: string;
55
- useChangeLabel?: boolean;
56
- changeLableProps?: ChangeLabelProps;
57
- hint?: string;
58
- };
59
-
60
- type SmallVariantProps = BaseInputFileProps & {
61
- variant: 'small';
62
- description?: never;
63
- useChangeLabel?: never;
64
- changeLableProps?: never;
65
- hint?: never;
66
- };
67
-
68
- // Union type
69
- type InputFileProps = DefaultVariantProps | SmallVariantProps;
17
+ import type {
18
+ FileItem,
19
+ InputFileProps,
20
+ UploadConfig,
21
+ UploadedFile,
22
+ } from './type';
23
+ import { uploadFile } from './helpers/uploadFile';
70
24
 
71
25
  export default function InputFile({
72
26
  title = 'Upload File',
73
27
  description = 'File harus berformat JPG, PNG dan PDF',
74
- accept = ['application/pdf', 'image/jpeg', 'image/png'],
28
+ accept = [
29
+ 'application/pdf',
30
+ 'image/jpeg',
31
+ 'image/png',
32
+ types.pdf,
33
+ types.images,
34
+ ],
75
35
  btnChooseFileText = 'Choose File',
76
36
  multiple = false,
77
37
  value,
@@ -83,39 +43,81 @@ export default function InputFile({
83
43
  hint,
84
44
  hasError,
85
45
  onChange,
46
+ uploadConfig,
47
+ maxSize = 5, //5MB
48
+ maxSizeErrorMessage,
86
49
  }: InputFileProps) {
87
- const [isOpenBottomSheetTypeFile, setIsOpenBottomSheetTypeFile] =
88
- useState(false);
89
- const [isOpenBottomSheetDeleteFile, setIsOpenBottomSheetDeleteFile] =
90
- useState(false);
91
- const [files, setFiles] = useState<any[]>(value || []);
92
- const [selectedFileIndex, setSelectedFileIndex] = useState<number | null>(
93
- null
94
- );
50
+ const [isOpenBottomSheetTypeFile, setIsOpenBottomSheetTypeFile] = useState(false); //prettier-ignore
51
+ const [isOpenBottomSheetDeleteFile, setIsOpenBottomSheetDeleteFile] = useState(false); //prettier-ignore
52
+ const [selectedFileIndex, setSelectedFileIndex] = useState<number | null>(null); //prettier-ignore
53
+ const [uploadedFiles, setUploadedFiles] = useState<UploadedFile<unknown>[]>([]); //prettier-ignore
54
+ const [internalErrorMessage, setInternalErrorMessage] = useState<string | null>(null); //prettier-ignore
55
+ const [files, setFiles] = useState<FileItem[]>(value || []);
56
+ const filesRef = useRef<FileItem[]>(files);
95
57
 
96
- const handleOnPresChoseFile = () => {
97
- setIsOpenBottomSheetTypeFile(true);
98
- };
58
+ const calculatedMaxSize = maxSize * 1024 * 1024;
99
59
 
100
- const handlePickFile = async () => {
101
- try {
102
- if (selectedFileIndex !== null) {
103
- confirmReplaceFile(selectedFileIndex);
104
- return;
105
- }
60
+ const uploadAndTrack = async (newFiles: FileItem[], config: UploadConfig) => {
61
+ setIsOpenBottomSheetTypeFile(false);
106
62
 
107
- const result = await pick({ type: accept, multiple });
108
- const pickedFiles = multiple ? result : [result[0]];
109
- const newFiles = [...files, ...pickedFiles];
110
- setFiles(newFiles);
111
- onChange?.(newFiles);
63
+ const withLoading = newFiles.map((f) => ({ ...f, uploading: true }));
64
+ setFiles((prev) => [...prev, ...withLoading]);
65
+
66
+ const results = await Promise.all(
67
+ newFiles.map((f) => uploadFile(f, config))
68
+ );
69
+
70
+ const currentFiles = filesRef.current;
71
+ const updatedFiles = currentFiles.map((f) => {
72
+ const match = results.find(
73
+ (r) => r.originalName === ('name' in f ? f.name : f.fileName)
74
+ );
75
+
76
+ if (!match) return f;
77
+
78
+ return {
79
+ ...f,
80
+ uploading: false,
81
+ error: match.uploadedData === null,
82
+ uploadedData: match.uploadedData,
83
+ hint:
84
+ match.uploadedData === null
85
+ ? (config.errorMessage ?? 'Upload gagal')
86
+ : f.hint,
87
+ };
88
+ });
89
+
90
+ setFiles(updatedFiles);
91
+
92
+ const newUploaded = [...uploadedFiles, ...results];
93
+ setUploadedFiles(newUploaded);
94
+ uploadConfig?.onUploadSuccess?.(newUploaded);
95
+ onChange?.(updatedFiles);
96
+ };
97
+
98
+ // validate max size
99
+ const validateMaxSize = (
100
+ assets: { size?: number | null; fileSize?: number | null }[]
101
+ ): boolean => {
102
+ if (!maxSize) return true;
103
+
104
+ const oversized = assets.filter((f) => {
105
+ const size = f.size ?? f.fileSize ?? 0;
106
+ return size > (calculatedMaxSize ?? Infinity);
107
+ });
108
+
109
+ if (oversized.length > 0) {
110
+ setInternalErrorMessage(
111
+ maxSizeErrorMessage ?? 'Ukuran file melebihi batas maksimal'
112
+ );
112
113
  setIsOpenBottomSheetTypeFile(false);
113
- } catch (error: any) {
114
- if (error?.message?.toLowerCase().includes('user canceled')) return;
115
- console.error('Failed to pick document:', error);
114
+ return false;
116
115
  }
116
+
117
+ return true;
117
118
  };
118
119
 
120
+ // preview file
119
121
  const handlePreviewFile = async (file: any) => {
120
122
  try {
121
123
  let fileUri = file.uri;
@@ -145,18 +147,63 @@ export default function InputFile({
145
147
  }
146
148
  };
147
149
 
150
+ // pick file
151
+ const handlePickFile = async () => {
152
+ try {
153
+ if (selectedFileIndex !== null) {
154
+ confirmReplaceFile(selectedFileIndex);
155
+ return;
156
+ }
157
+
158
+ const result = await pick({
159
+ type: accept,
160
+ allowMultiSelection: multiple,
161
+ });
162
+
163
+ const pickedFiles = multiple ? result : [result[0]];
164
+
165
+ if (!validateMaxSize(pickedFiles)) return;
166
+ setInternalErrorMessage(null);
167
+
168
+ if (uploadConfig) {
169
+ await uploadAndTrack(
170
+ pickedFiles as unknown as FileItem[],
171
+ uploadConfig
172
+ );
173
+ } else {
174
+ const newFiles = [...files, ...(pickedFiles as unknown as FileItem[])];
175
+ setFiles(newFiles);
176
+ onChange?.(newFiles);
177
+ }
178
+
179
+ setIsOpenBottomSheetTypeFile(false);
180
+ } catch (error: any) {
181
+ if (error?.message?.toLowerCase().includes('user canceled')) return;
182
+ console.error('Failed to pick document:', error);
183
+ }
184
+ };
185
+
148
186
  const handleOnPressButtonCamera = async () => {
149
187
  try {
150
188
  if (selectedFileIndex !== null) {
151
- confirmReplaceFile(selectedFileIndex, 'camera');
189
+ await confirmReplaceFile(selectedFileIndex, 'camera');
152
190
  return;
153
191
  }
154
192
 
155
193
  const result = await launchCamera({ mediaType: 'photo' });
156
194
  if (result.didCancel || !result.assets) return;
157
- const newFiles = [...files, result.assets[0]];
158
- setFiles(newFiles);
159
- onChange?.(newFiles);
195
+
196
+ if (!validateMaxSize(result.assets)) return;
197
+ setInternalErrorMessage(null);
198
+
199
+ if (uploadConfig) {
200
+ await uploadAndTrack(result.assets as FileItem[], uploadConfig);
201
+ } else {
202
+ const newFiles = [...files, result.assets[0] as FileItem];
203
+ setFiles(newFiles);
204
+ onChange?.(newFiles);
205
+ }
206
+
160
207
  setIsOpenBottomSheetTypeFile(false);
161
208
  } catch (error: any) {
162
209
  console.error('Failed to open camera:', error);
@@ -166,7 +213,7 @@ export default function InputFile({
166
213
  const handleOnPressButtonGallery = async () => {
167
214
  try {
168
215
  if (selectedFileIndex !== null) {
169
- confirmReplaceFile(selectedFileIndex, 'gallery');
216
+ await confirmReplaceFile(selectedFileIndex, 'gallery');
170
217
  return;
171
218
  }
172
219
 
@@ -175,9 +222,18 @@ export default function InputFile({
175
222
  selectionLimit: multiple ? 0 : 1,
176
223
  });
177
224
  if (result.didCancel || !result.assets) return;
178
- const newFiles = [...files, ...result.assets];
179
- setFiles(newFiles);
180
- onChange?.(newFiles);
225
+
226
+ if (!validateMaxSize(result.assets)) return;
227
+ setInternalErrorMessage(null);
228
+
229
+ if (uploadConfig) {
230
+ await uploadAndTrack(result.assets as FileItem[], uploadConfig);
231
+ } else {
232
+ const newFiles = [...files, ...(result.assets as FileItem[])];
233
+ setFiles(newFiles);
234
+ onChange?.(newFiles);
235
+ }
236
+
181
237
  setIsOpenBottomSheetTypeFile(false);
182
238
  } catch (error: any) {
183
239
  console.error('Failed to open gallery:', error);
@@ -192,13 +248,16 @@ export default function InputFile({
192
248
  const deleteFile = () => {
193
249
  if (selectedFileIndex !== null) {
194
250
  const newFiles = files.filter((_, i) => i !== selectedFileIndex);
251
+
195
252
  setFiles(newFiles);
253
+ setUploadedFiles(newFiles as unknown as UploadedFile<unknown>[]);
196
254
  onChange?.(newFiles);
197
255
  setSelectedFileIndex(null);
198
256
  }
199
257
  setIsOpenBottomSheetDeleteFile(false);
200
258
  };
201
259
 
260
+ // replace attachment
202
261
  const handleReplaceFile = (index: number) => {
203
262
  setIsOpenBottomSheetTypeFile(true);
204
263
  setSelectedFileIndex(index);
@@ -208,33 +267,91 @@ export default function InputFile({
208
267
  index: number,
209
268
  type?: 'gallery' | 'camera'
210
269
  ) => {
211
- let replacedFile: any | null = null;
270
+ const labelFile = files?.[index]?.labelFile;
271
+
272
+ // handler untuk pick
273
+ const pickFile = async (): Promise<FileItem | null> => {
274
+ if (type === 'gallery') {
275
+ const result = await launchImageLibrary({
276
+ mediaType: 'photo',
277
+ selectionLimit: multiple ? 0 : 1,
278
+ });
279
+
280
+ if (!result.assets?.length) return null;
281
+ return { ...result.assets[0], labelFile };
282
+ }
212
283
 
213
- if (type === 'gallery') {
214
- const result = await launchImageLibrary({
215
- mediaType: 'photo',
216
- selectionLimit: multiple ? 0 : 1,
284
+ if (type === 'camera') {
285
+ const result = await launchCamera({ mediaType: 'photo' });
286
+
287
+ if (!result.assets?.length) return null;
288
+ return { ...result.assets[0], labelFile: files?.[index]?.labelFile };
289
+ }
290
+
291
+ const result = await pick({
292
+ type: accept,
293
+ allowMultiSelection: multiple,
217
294
  });
218
- if (!result.assets || result.assets.length === 0) return;
219
- replacedFile = { ...result.assets[0], labelFile: files[index].labelFile };
220
- } else if (type === 'camera') {
221
- const result = await launchCamera({ mediaType: 'photo' });
222
- if (!result.assets || result.assets.length === 0) return;
223
- replacedFile = { ...result.assets[0], labelFile: files[index].labelFile };
295
+
296
+ if (!result?.length) return null;
297
+ return { ...result[0], labelFile } as unknown as FileItem;
298
+ };
299
+
300
+ const replacedFile = await pickFile();
301
+ if (!replacedFile) return;
302
+
303
+ // handle validasi max size
304
+ if (!validateMaxSize([replacedFile])) return;
305
+ setInternalErrorMessage(null);
306
+
307
+ // handle upload
308
+ if (uploadConfig) {
309
+ setIsOpenBottomSheetTypeFile(false);
310
+
311
+ const withLoading = files.map((f, i) =>
312
+ i === index ? { ...replacedFile, uploading: true } : f
313
+ );
314
+
315
+ setFiles(withLoading);
316
+
317
+ const results = await Promise.all([
318
+ uploadFile(replacedFile, uploadConfig),
319
+ ]);
320
+
321
+ const uploadResult = results[0];
322
+ const updatedFiles = filesRef.current.map((f, i) =>
323
+ i === index
324
+ ? {
325
+ ...replacedFile,
326
+ uploading: false,
327
+ uploadedData: uploadResult.uploadedData,
328
+ error: uploadResult.uploadedData === null,
329
+ hint:
330
+ uploadResult.uploadedData === null
331
+ ? (uploadConfig.errorMessage ?? 'Upload gagal')
332
+ : replacedFile.hint,
333
+ }
334
+ : f
335
+ );
336
+
337
+ setFiles(updatedFiles);
338
+ onChange?.(updatedFiles);
224
339
  } else {
225
- const result = await pick({ type: accept, multiple });
226
- if (!result || result.length === 0) return;
227
- replacedFile = { ...result[0], labelFile: files[index].labelFile };
340
+ const updatedFiles = files.map((f, i) =>
341
+ i === index ? replacedFile : f
342
+ );
343
+ setFiles(updatedFiles);
344
+ onChange?.(updatedFiles);
228
345
  }
229
346
 
230
- const updatedFiles = files.map((f, i) => (i === index ? replacedFile : f));
231
-
232
- setFiles(updatedFiles);
233
- onChange?.(updatedFiles);
234
347
  setSelectedFileIndex(null);
235
348
  setIsOpenBottomSheetTypeFile(false);
236
349
  };
237
350
 
351
+ useEffect(() => {
352
+ filesRef.current = files;
353
+ }, [files]);
354
+
238
355
  useEffect(() => {
239
356
  setFiles(value ?? []);
240
357
  }, [value]);
@@ -244,11 +361,11 @@ export default function InputFile({
244
361
  {variant === 'default' && (
245
362
  <CardTrigger
246
363
  title={title}
247
- hint={hint}
248
- hasError={hasError}
364
+ hint={hint || (internalErrorMessage as string)}
365
+ hasError={hasError || !!internalErrorMessage}
249
366
  description={description}
250
367
  btnSelect={btnChooseFileText}
251
- onPress={handleOnPresChoseFile}
368
+ onPress={() => setIsOpenBottomSheetTypeFile(true)}
252
369
  />
253
370
  )}
254
371
 
@@ -259,11 +376,17 @@ export default function InputFile({
259
376
  title={title}
260
377
  hasError={hasError}
261
378
  textButton={btnChooseFileText}
262
- onChooseFile={handleOnPresChoseFile}
379
+ onChooseFile={() => setIsOpenBottomSheetTypeFile(true)}
263
380
  onPreview={handlePreviewFile}
264
381
  onReplace={handleReplaceFile}
265
382
  onDelete={confirmDeleteFile}
266
383
  />
384
+ {internalErrorMessage && (
385
+ <Typography variant="t3" color={Color.danger[500]}>
386
+ {internalErrorMessage}
387
+ </Typography>
388
+ )}
389
+
267
390
  {files.length > 0 && (
268
391
  <>
269
392
  {files
@@ -306,6 +429,7 @@ export default function InputFile({
306
429
  <ItemPreview
307
430
  index={index}
308
431
  file={file}
432
+ loading={file.uploading}
309
433
  onPress={() => handlePreviewFile(file)}
310
434
  onReplace={() => handleReplaceFile(index)}
311
435
  onDelete={() => confirmDeleteFile(index)}