@tradly/asset 1.0.26 → 1.0.28

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/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # @tradly/asset
2
2
 
3
- A reusable React component package for uploading and selecting media (images,
4
- videos, files) with Tradly authentication support.
3
+ A cross-platform React component package for uploading and selecting media
4
+ (images, videos, files) with Tradly authentication support. Works seamlessly on
5
+ both Web (React) and Mobile (React Native).
5
6
 
6
7
  ## Features
7
8
 
@@ -100,6 +101,11 @@ function MyComponent() {
100
101
  import React, { useState } from "react";
101
102
  import { View, Button } from "react-native";
102
103
  import { MediaPopup, MediaApiService } from "@tradly/asset";
104
+ import {
105
+ CameraIcon,
106
+ VideoIcon,
107
+ UploadIcon,
108
+ } from "@tradly/asset/native/Icons.native";
103
109
  import { launchImageLibrary } from "react-native-image-picker";
104
110
 
105
111
  function MyComponent() {
@@ -121,6 +127,10 @@ function MyComponent() {
121
127
  const picker = (options) => {
122
128
  return new Promise((resolve) => {
123
129
  launchImageLibrary(options, (response) => {
130
+ if (response.didCancel) {
131
+ resolve([]);
132
+ return;
133
+ }
124
134
  resolve(response.assets || []);
125
135
  });
126
136
  });
@@ -137,9 +147,14 @@ function MyComponent() {
137
147
  isOpen={isOpen}
138
148
  onClose={() => setIsOpen(false)}
139
149
  onSelect={handleSelect}
140
- options={["image", "video"]}
150
+ options={["image", "video", "file"]}
141
151
  apiService={apiService}
142
152
  picker={picker} // REQUIRED: Your file picker function
153
+ icons={{
154
+ image: <CameraIcon />,
155
+ video: <VideoIcon />,
156
+ default: <UploadIcon />,
157
+ }}
143
158
  />
144
159
  </View>
145
160
  );
@@ -222,17 +237,76 @@ You can customize picker options based on the accept type:
222
237
  ```jsx
223
238
  <MediaPopup
224
239
  picker={yourPickerFunction}
225
- pickerOptions={(accept) => ({
226
- mediaType: accept?.includes("image") ? "photo" : "video",
227
- quality: 0.9,
228
- allowsMultiple: true,
229
- // Add any other options your picker library supports
230
- })}
240
+ pickerOptions={(accept) => {
241
+ const isImage = accept?.includes("image");
242
+ const isVideo = accept?.includes("video");
243
+ const isFile = accept?.includes("file");
244
+
245
+ return {
246
+ mediaType: isImage ? "photo" : isVideo ? "video" : "mixed",
247
+ quality: 0.9,
248
+ allowsMultiple: true,
249
+ // Add any other options your picker library supports
250
+ };
251
+ }}
231
252
  apiService={apiService}
232
253
  // ... other props
233
254
  />
234
255
  ```
235
256
 
257
+ #### File Picker Response Format
258
+
259
+ The picker function should return an array of file objects. The package supports
260
+ multiple formats:
261
+
262
+ **react-native-image-picker format:**
263
+
264
+ ```javascript
265
+ {
266
+ assets: [
267
+ {
268
+ uri: "file:///path/to/file.jpg",
269
+ fileName: "image.jpg",
270
+ mimeType: "image/jpeg",
271
+ fileSize: 12345,
272
+ },
273
+ ];
274
+ }
275
+ ```
276
+
277
+ **expo-image-picker format:**
278
+
279
+ ```javascript
280
+ {
281
+ assets: [
282
+ {
283
+ uri: "file:///path/to/file.jpg",
284
+ fileName: "image.jpg",
285
+ mimeType: "image/jpeg",
286
+ },
287
+ ];
288
+ }
289
+ ```
290
+
291
+ **Custom format:**
292
+
293
+ ```javascript
294
+ [
295
+ {
296
+ uri: "file:///path/to/file.jpg",
297
+ name: "image.jpg", // or fileName
298
+ type: "image/jpeg", // or mimeType
299
+ },
300
+ ];
301
+ ```
302
+
303
+ The package will automatically:
304
+
305
+ - Extract `uri` from `uri` or `path`
306
+ - Extract file name from `fileName`, `name`, or parse from URI
307
+ - Detect MIME type from `mimeType`, `type`, or file extension
308
+ - Handle missing properties with smart fallbacks
309
+
236
310
  ### React Native UI Differences
237
311
 
238
312
  - **Bottom Sheet**: Media gallery opens as a bottom sheet (slides up from
@@ -242,6 +316,11 @@ You can customize picker options based on the accept type:
242
316
  `TouchableOpacity`, `FlatList`, `Image`)
243
317
  - **File Upload**: Requires `picker` prop to be provided
244
318
  - **Theme System**: Full theme support for easy dark/light mode customization
319
+ - **Icons**: Support for custom icons per media type (image, video, file)
320
+ - **File Handling**: Automatically handles `file://` URIs and converts them to
321
+ blobs for S3 upload
322
+ - **MIME Type Detection**: Comprehensive MIME type detection from file
323
+ extensions and picker results
245
324
 
246
325
  ### React Native Theme & Customization
247
326
 
@@ -268,68 +347,313 @@ const lightTheme = createTheme({ mode: "light" });
268
347
  />;
269
348
  ```
270
349
 
271
- #### Custom Theme Colors
350
+ #### Theme Examples
351
+
352
+ **Example 1: Complete Custom Theme**
272
353
 
273
354
  ```jsx
274
355
  import { createTheme } from "@tradly/asset/native/theme";
275
356
 
276
357
  const customTheme = createTheme({
277
- mode: "dark", // or "light"
358
+ mode: "dark",
278
359
  colors: {
279
360
  // Primary colors
280
361
  primary: "#6366F1",
281
- tabActive: "#6366F1",
282
- tabIndicator: "#6366F1",
362
+ primaryLight: "#818CF8",
363
+ primaryDark: "#4F46E5",
283
364
 
284
365
  // Backgrounds
285
366
  background: "#0F172A",
286
367
  backgroundSecondary: "#1E293B",
368
+ backgroundTertiary: "#334155",
287
369
 
288
370
  // Text
289
371
  text: "#F1F5F9",
290
372
  textSecondary: "#CBD5E1",
373
+ textTertiary: "#94A3B8",
291
374
 
292
375
  // Tabs
293
376
  tabActive: "#6366F1",
294
377
  tabInactive: "#94A3B8",
378
+ tabIndicator: "#6366F1",
295
379
  tabBackground: "#0F172A",
296
380
 
297
- // Borders
298
- border: "#334155",
381
+ // Upload
382
+ uploadBorder: "#6366F1",
383
+ uploadBackground: "#1E293B",
384
+ uploadIconBackground: "#6366F1",
385
+ uploadText: "#F1F5F9",
299
386
 
300
387
  // Pagination
388
+ paginationBackground: "#1E293B",
301
389
  paginationButtonActive: "#6366F1",
302
390
  paginationTextActive: "#FFFFFF",
303
391
 
304
- // Upload button
305
- uploadBorder: "#6366F1",
306
- uploadIconBackground: "#6366F1",
307
-
308
- // And many more...
392
+ // And all other colors...
393
+ },
394
+ spacing: {
395
+ xs: 4,
396
+ sm: 8,
397
+ md: 12,
398
+ lg: 16,
399
+ xl: 20,
400
+ xxl: 24,
309
401
  },
310
- bottomSheetHeight: 0.92, // 92% of screen height
402
+ radius: {
403
+ sm: 6,
404
+ md: 8,
405
+ lg: 12,
406
+ xl: 16,
407
+ xxl: 20,
408
+ },
409
+ bottomSheetHeight: 0.92,
311
410
  });
312
411
 
313
412
  <MediaPopup theme={customTheme} ... />
314
413
  ```
315
414
 
316
- #### Available Theme Colors
415
+ **Example 2: Minimal Override (Only Colors)**
317
416
 
318
- All colors are customizable via the theme object:
417
+ ```jsx
418
+ // Only override specific colors, rest uses defaults
419
+ const minimalTheme = createTheme({
420
+ colors: {
421
+ primary: "#FF6B6B",
422
+ tabActive: "#FF6B6B",
423
+ tabIndicator: "#FF6B6B",
424
+ },
425
+ });
426
+ ```
319
427
 
320
- - **Primary**: `primary`, `primaryLight`, `primaryDark`
321
- - **Backgrounds**: `background`, `backgroundSecondary`, `backgroundTertiary`
322
- - **Text**: `text`, `textSecondary`, `textTertiary`, `textInverse`
323
- - **Borders**: `border`, `borderLight`, `borderDark`
324
- - **Tabs**: `tabActive`, `tabInactive`, `tabIndicator`, `tabBackground`
325
- - **Pagination**: `paginationBackground`, `paginationButton`,
326
- `paginationButtonActive`, `paginationText`, `paginationTextActive`,
327
- `paginationBorder`
328
- - **Upload**: `uploadBorder`, `uploadBackground`, `uploadIconBackground`,
329
- `uploadText`
330
- - **Items**: `itemBackground`, `itemShadow`
331
- - **Loading**: `loadingBackground`, `loadingText`
332
- - **Overlay**: `overlay`
428
+ #### Complete Theme Reference
429
+
430
+ **All Available Theme Properties:**
431
+
432
+ | Category | Property | Type | Description | Default (Light) |
433
+ | ----------------------- | ------------------------ | -------- | --------------------- | ----------------- |
434
+ | **Colors - Primary** | `primary` | `string` | Main brand color | `#3B3269` |
435
+ | | `primaryLight` | `string` | Lighter variant | `#5A4A8A` |
436
+ | | `primaryDark` | `string` | Darker variant | `#2A1F4A` |
437
+ | **Colors - Background** | `background` | `string` | Main background | `#FFFFFF` |
438
+ | | `backgroundSecondary` | `string` | Secondary background | `#F5F5F5` |
439
+ | | `backgroundTertiary` | `string` | Tertiary background | `#E5E5E5` |
440
+ | **Colors - Text** | `text` | `string` | Primary text | `#000000` |
441
+ | | `textSecondary` | `string` | Secondary text | `#4F4F4F` |
442
+ | | `textTertiary` | `string` | Tertiary text | `#6B7280` |
443
+ | | `textInverse` | `string` | Inverse text | `#FFFFFF` |
444
+ | **Colors - Border** | `border` | `string` | Default border | `#E5E5E5` |
445
+ | | `borderLight` | `string` | Light border | `#F0F0F0` |
446
+ | | `borderDark` | `string` | Dark border | `#D1D5DB` |
447
+ | **Colors - Overlay** | `overlay` | `string` | Modal overlay | `rgba(0,0,0,0.5)` |
448
+ | **Colors - Tabs** | `tabActive` | `string` | Active tab text | `#3B3269` |
449
+ | | `tabInactive` | `string` | Inactive tab text | `#4F4F4F` |
450
+ | | `tabIndicator` | `string` | Tab indicator line | `#3B3269` |
451
+ | | `tabBackground` | `string` | Tab container | `#FFFFFF` |
452
+ | **Colors - Items** | `itemBackground` | `string` | Media item background | `#FFFFFF` |
453
+ | | `itemShadow` | `string` | Item shadow color | `#000000` |
454
+ | **Colors - Pagination** | `paginationBackground` | `string` | Container background | `#F5F5F5` |
455
+ | | `paginationButton` | `string` | Button background | `#FFFFFF` |
456
+ | | `paginationButtonActive` | `string` | Active button | `#3B3269` |
457
+ | | `paginationText` | `string` | Text color | `#6B7280` |
458
+ | | `paginationTextActive` | `string` | Active text | `#FFFFFF` |
459
+ | | `paginationBorder` | `string` | Border color | `#D1D5DB` |
460
+ | **Colors - Upload** | `uploadBorder` | `string` | Button border | `#3B3269` |
461
+ | | `uploadBackground` | `string` | Button background | `#FFFFFF` |
462
+ | | `uploadIconBackground` | `string` | Icon container | `#3B3269` |
463
+ | | `uploadText` | `string` | Button text | `#000000` |
464
+ | **Colors - Loading** | `loadingBackground` | `string` | Skeleton background | `#E5E5E5` |
465
+ | | `loadingText` | `string` | Loading text | `#666666` |
466
+ | **Spacing** | `xs` | `number` | Extra small (4px) | `4` |
467
+ | | `sm` | `number` | Small (8px) | `8` |
468
+ | | `md` | `number` | Medium (12px) | `12` |
469
+ | | `lg` | `number` | Large (16px) | `16` |
470
+ | | `xl` | `number` | Extra large (20px) | `20` |
471
+ | | `xxl` | `number` | 2X extra large (24px) | `24` |
472
+ | **Radius** | `sm` | `number` | Small radius (6px) | `6` |
473
+ | | `md` | `number` | Medium radius (8px) | `8` |
474
+ | | `lg` | `number` | Large radius (12px) | `12` |
475
+ | | `xl` | `number` | Extra large (16px) | `16` |
476
+ | | `xxl` | `number` | 2X extra large (20px) | `20` |
477
+ | **Typography** | `title.fontSize` | `number` | Title font size | `24` |
478
+ | | `title.fontWeight` | `string` | Title weight | `"bold"` |
479
+ | | `subtitle.fontSize` | `number` | Subtitle size | `18` |
480
+ | | `subtitle.fontWeight` | `string` | Subtitle weight | `"600"` |
481
+ | | `body.fontSize` | `number` | Body font size | `16` |
482
+ | | `body.fontWeight` | `string` | Body weight | `"500"` |
483
+ | | `caption.fontSize` | `number` | Caption size | `14` |
484
+ | | `caption.fontWeight` | `string` | Caption weight | `"400"` |
485
+ | **Other** | `bottomSheetHeight` | `number` | Sheet height (0-1) | `0.9` |
486
+ | | `mode` | `string` | `"light"` or `"dark"` | `"light"` |
487
+
488
+ #### Complete Theme Customization
489
+
490
+ The theme system supports comprehensive customization of colors, spacing,
491
+ typography, and more:
492
+
493
+ **Colors:**
494
+
495
+ ```jsx
496
+ const customTheme = createTheme({
497
+ mode: "light", // or "dark"
498
+ colors: {
499
+ // Primary colors
500
+ primary: "#3B3269", // Main brand color
501
+ primaryLight: "#5A4A8A", // Lighter variant
502
+ primaryDark: "#2A1F4A", // Darker variant
503
+
504
+ // Background colors
505
+ background: "#FFFFFF", // Main background
506
+ backgroundSecondary: "#F5F5F5", // Secondary background
507
+ backgroundTertiary: "#E5E5E5", // Tertiary background
508
+
509
+ // Text colors
510
+ text: "#000000", // Primary text
511
+ textSecondary: "#4F4F4F", // Secondary text
512
+ textTertiary: "#6B7280", // Tertiary text
513
+ textInverse: "#FFFFFF", // Inverse text (for dark backgrounds)
514
+
515
+ // Border colors
516
+ border: "#E5E5E5", // Default border
517
+ borderLight: "#F0F0F0", // Light border
518
+ borderDark: "#D1D5DB", // Dark border
519
+
520
+ // Overlay
521
+ overlay: "rgba(0, 0, 0, 0.5)", // Modal overlay
522
+
523
+ // Tab colors
524
+ tabActive: "#3B3269", // Active tab text
525
+ tabInactive: "#4F4F4F", // Inactive tab text
526
+ tabIndicator: "#3B3269", // Tab indicator line
527
+ tabBackground: "#FFFFFF", // Tab container background
528
+
529
+ // Media item colors
530
+ itemBackground: "#FFFFFF", // Image/video item background
531
+ itemShadow: "#000000", // Item shadow color
532
+
533
+ // Pagination colors
534
+ paginationBackground: "#F5F5F5", // Pagination container
535
+ paginationButton: "#FFFFFF", // Page button background
536
+ paginationButtonActive: "#3B3269", // Active page button
537
+ paginationText: "#6B7280", // Page number text
538
+ paginationTextActive: "#FFFFFF", // Active page text
539
+ paginationBorder: "#D1D5DB", // Pagination border
540
+
541
+ // Upload button colors
542
+ uploadBorder: "#3B3269", // Upload button border
543
+ uploadBackground: "#FFFFFF", // Upload button background
544
+ uploadIconBackground: "#3B3269", // Icon container background
545
+ uploadText: "#000000", // Upload button text
546
+
547
+ // Loading state colors
548
+ loadingBackground: "#E5E5E5", // Loading skeleton background
549
+ loadingText: "#666666", // Loading text color
550
+ },
551
+ // Spacing (in pixels)
552
+ spacing: {
553
+ xs: 4, // Extra small
554
+ sm: 8, // Small
555
+ md: 12, // Medium
556
+ lg: 16, // Large
557
+ xl: 20, // Extra large
558
+ xxl: 24, // 2X extra large
559
+ },
560
+ // Border radius (in pixels)
561
+ radius: {
562
+ sm: 6, // Small radius
563
+ md: 8, // Medium radius
564
+ lg: 12, // Large radius
565
+ xl: 16, // Extra large radius
566
+ xxl: 20, // 2X extra large radius
567
+ },
568
+ // Typography
569
+ typography: {
570
+ title: {
571
+ fontSize: 24,
572
+ fontWeight: "bold",
573
+ },
574
+ subtitle: {
575
+ fontSize: 18,
576
+ fontWeight: "600",
577
+ },
578
+ body: {
579
+ fontSize: 16,
580
+ fontWeight: "500",
581
+ },
582
+ caption: {
583
+ fontSize: 14,
584
+ fontWeight: "400",
585
+ },
586
+ },
587
+ // Bottom sheet height (0.85 = 85%, 0.9 = 90%, etc.)
588
+ bottomSheetHeight: 0.9,
589
+ });
590
+ ```
591
+
592
+ **Partial Theme Override:**
593
+
594
+ You can override only specific parts of the theme - other values will use
595
+ defaults:
596
+
597
+ ```jsx
598
+ // Only change primary color and background
599
+ const brandTheme = createTheme({
600
+ colors: {
601
+ primary: "#6366F1",
602
+ tabActive: "#6366F1",
603
+ tabIndicator: "#6366F1",
604
+ background: "#F8F9FA",
605
+ },
606
+ });
607
+
608
+ // Only change spacing
609
+ const compactTheme = createTheme({
610
+ spacing: {
611
+ md: 8, // Reduce medium spacing
612
+ lg: 12, // Reduce large spacing
613
+ },
614
+ });
615
+
616
+ // Only change bottom sheet height
617
+ const tallTheme = createTheme({
618
+ bottomSheetHeight: 0.95, // 95% of screen
619
+ });
620
+ ```
621
+
622
+ #### Dark Mode Example
623
+
624
+ ```jsx
625
+ import { createTheme } from "@tradly/asset/native/theme";
626
+
627
+ const darkTheme = createTheme({
628
+ mode: "dark",
629
+ colors: {
630
+ primary: "#6366F1",
631
+ background: "#0F172A",
632
+ text: "#F1F5F9",
633
+ tabActive: "#6366F1",
634
+ tabInactive: "#94A3B8",
635
+ // ... customize other colors
636
+ },
637
+ });
638
+
639
+ <MediaPopup theme={darkTheme} ... />
640
+ ```
641
+
642
+ #### Brand Color Customization
643
+
644
+ ```jsx
645
+ const brandTheme = createTheme({
646
+ colors: {
647
+ // Your brand colors
648
+ primary: "#FF6B6B",
649
+ tabActive: "#FF6B6B",
650
+ tabIndicator: "#FF6B6B",
651
+ uploadBorder: "#FF6B6B",
652
+ uploadIconBackground: "#FF6B6B",
653
+ paginationButtonActive: "#FF6B6B",
654
+ },
655
+ });
656
+ ```
333
657
 
334
658
  #### Customizing Bottom Sheet Height
335
659
 
@@ -343,20 +667,30 @@ All colors are customizable via the theme object:
343
667
 
344
668
  #### Individual Component Styling
345
669
 
346
- You can also override individual component styles:
670
+ You can override individual component styles in addition to theme:
347
671
 
348
672
  ```jsx
349
673
  <MediaPopup
350
674
  theme={darkTheme}
351
- // Override specific styles
675
+ // Override specific styles (these override theme values)
352
676
  containerStyle={{ padding: 24 }}
353
677
  headerStyle={{ paddingBottom: 16 }}
354
- titleStyle={{ fontSize: 28 }}
678
+ titleStyle={{ fontSize: 28, color: "#FFFFFF" }}
355
679
  closeButtonStyle={{ padding: 12 }}
680
+ closeButtonTextStyle={{ color: "#FFFFFF" }}
681
+ // Tab styles
356
682
  tabListStyle={{ paddingHorizontal: 8 }}
357
683
  tabButtonStyle={{ paddingVertical: 16 }}
358
684
  tabButtonActiveStyle={{ backgroundColor: "#F0F0F0" }}
359
685
  tabButtonTextActiveStyle={{ fontWeight: "700" }}
686
+ tabButtonTextInactiveStyle={{ color: "#999999" }}
687
+ tabIndicatorStyle={{ height: 3 }}
688
+ // Gallery styles
689
+ gridStyle={{ padding: 8 }}
690
+ imageItemStyle={{ borderRadius: 12 }}
691
+ videoItemStyle={{ borderRadius: 12 }}
692
+ fileItemStyle={{ borderRadius: 12 }}
693
+ paginationContainerStyle={{ padding: 16 }}
360
694
  // ... and many more style props
361
695
  />
362
696
  ```
@@ -372,6 +706,43 @@ upload logic is handled internally:
372
706
  2. **Upload files to S3** using the signed URLs
373
707
  3. **Save media metadata** to your API
374
708
 
709
+ **React Native File Upload:**
710
+
711
+ The package automatically handles React Native file formats:
712
+
713
+ - Converts `file://` URIs to blobs for S3 upload
714
+ - Handles `content://` URIs (Android)
715
+ - Supports multiple file picker libraries
716
+ - Automatic MIME type detection from file extensions
717
+ - Proper file name cleaning (removes spaces)
718
+
719
+ **File Format Support:**
720
+
721
+ The package accepts files in these formats:
722
+
723
+ - **react-native-image-picker**: `{ uri, fileName, mimeType, ... }`
724
+ - **expo-image-picker**: `{ uri, fileName, mimeType, ... }`
725
+ - **Custom pickers**: `{ uri, name, type, ... }` or
726
+ `{ uri, fileName, mimeType, ... }`
727
+
728
+ The package will automatically:
729
+
730
+ - Extract file name from `fileName`, `name`, or URI
731
+ - Detect MIME type from `mimeType`, `type`, or file extension
732
+ - Handle file URIs (`file://`, `content://`) for upload
733
+ - Clean file names (removes spaces for S3 compatibility)
734
+ - Convert file URIs to blobs for S3 PUT requests
735
+
736
+ **Upload Process:**
737
+
738
+ 1. **File Selection**: User selects files via picker
739
+ 2. **File Processing**: Package converts picker results to standardized format
740
+ 3. **MIME Type Detection**: Automatically detects MIME type from extension or
741
+ picker result
742
+ 4. **S3 Signed URL**: Gets signed URL from Tradly API
743
+ 5. **File Upload**: Fetches file from URI, converts to blob, uploads to S3
744
+ 6. **Metadata Save**: Saves media metadata to Tradly API
745
+
375
746
  Configuration:
376
747
 
377
748
  ```jsx
@@ -393,15 +764,22 @@ const apiService = new MediaApiService({
393
764
 
394
765
  ### Using Individual Components
395
766
 
396
- You can also use the components individually:
767
+ You can use individual gallery components if you need more control:
768
+
769
+ **Web:**
397
770
 
398
771
  ```jsx
399
- import { ImagesGallery, MediaApiService } from "@tradly/asset";
772
+ import {
773
+ ImagesGallery,
774
+ VideosGallery,
775
+ FilesGallery,
776
+ MediaApiService,
777
+ } from "@tradly/asset";
400
778
 
401
779
  function CustomGallery() {
402
780
  const apiService = new MediaApiService({
403
781
  authKey: "your-auth-key",
404
- baseUrl: "/api",
782
+ bearerToken: "your-bearer-token",
405
783
  });
406
784
 
407
785
  return (
@@ -414,6 +792,52 @@ function CustomGallery() {
414
792
  }
415
793
  ```
416
794
 
795
+ **React Native:**
796
+
797
+ ```jsx
798
+ import {
799
+ ImagesGallery,
800
+ VideosGallery,
801
+ FilesGallery,
802
+ MediaApiService,
803
+ } from "@tradly/asset";
804
+ import {
805
+ CameraIcon,
806
+ VideoIcon,
807
+ UploadIcon,
808
+ } from "@tradly/asset/native/Icons.native";
809
+ import { launchImageLibrary } from "react-native-image-picker";
810
+
811
+ function CustomGallery() {
812
+ const apiService = new MediaApiService({
813
+ authKey: "your-auth-key",
814
+ bearerToken: "your-bearer-token",
815
+ });
816
+
817
+ const picker = (options) => {
818
+ return new Promise((resolve) => {
819
+ launchImageLibrary(options, (response) => {
820
+ if (response.didCancel) {
821
+ resolve([]);
822
+ return;
823
+ }
824
+ resolve(response.assets || []);
825
+ });
826
+ });
827
+ };
828
+
829
+ return (
830
+ <FilesGallery
831
+ update_data={(url) => console.log("Selected:", url)}
832
+ closePopup={() => console.log("Closed")}
833
+ apiService={apiService}
834
+ picker={picker}
835
+ icons={{ default: <UploadIcon /> }}
836
+ />
837
+ );
838
+ }
839
+ ```
840
+
417
841
  ### Error Handling
418
842
 
419
843
  ```jsx
@@ -472,6 +896,7 @@ apiService.setBearerToken("new-bearer-token");
472
896
  | `title` | `string` | `'Media Gallery'` | Popup title | Web & Native |
473
897
  | `picker` | `function` | - | **Required for React Native**: File picker function | Native only |
474
898
  | `pickerOptions` | `function` | - | Optional: Function to generate picker options | Native only |
899
+ | `icons` | `object` | - | Icon components: `{ image, video, default }` | Native only |
475
900
  | `theme` | `object` | - | Theme object for colors/spacing (see Theme docs) | Native only |
476
901
  | `bottomSheetHeight` | `number` | `0.9` | Bottom sheet height (0.85 = 85%, 0.9 = 90%, etc.) | Native only |
477
902
 
@@ -483,6 +908,81 @@ apiService.setBearerToken("new-bearer-token");
483
908
  `{ uri: string, type?: string, fileName?: string }`
484
909
  - Example: `picker={(options) => launchImageLibrary(options, callback)}`
485
910
 
911
+ **Icons Configuration (React Native):**
912
+
913
+ You can provide different icons for each media type:
914
+
915
+ ```jsx
916
+ import {
917
+ CameraIcon,
918
+ VideoIcon,
919
+ UploadIcon,
920
+ } from "@tradly/asset/native/Icons.native";
921
+
922
+ <MediaPopup
923
+ icons={{
924
+ image: <CameraIcon />, // Icon for image uploads
925
+ video: <VideoIcon />, // Icon for video uploads
926
+ default: <UploadIcon />, // Icon for file uploads (PDFs, docs, etc.)
927
+ }}
928
+ // ... other props
929
+ />;
930
+ ```
931
+
932
+ If `icons` prop is not provided, the upload button will show only text (no
933
+ icon).
934
+
935
+ **Available Icons:**
936
+
937
+ The package includes built-in icons that you can use:
938
+
939
+ ```jsx
940
+ import {
941
+ CameraIcon, // For image uploads
942
+ VideoIcon, // For video uploads
943
+ UploadIcon, // For file uploads (default)
944
+ CloseIcon, // For close button (used internally)
945
+ } from "@tradly/asset/native/Icons.native";
946
+
947
+ <MediaPopup
948
+ icons={{
949
+ image: <CameraIcon />,
950
+ video: <VideoIcon />,
951
+ default: <UploadIcon />,
952
+ }}
953
+ />;
954
+ ```
955
+
956
+ You can also use custom icon components from libraries like
957
+ `react-native-vector-icons` or `lucide-react-native`:
958
+
959
+ ```jsx
960
+ import { Camera, Video, Upload } from "lucide-react-native";
961
+
962
+ <MediaPopup
963
+ icons={{
964
+ image: (
965
+ <Camera
966
+ size={20}
967
+ color="#3B3269"
968
+ />
969
+ ),
970
+ video: (
971
+ <Video
972
+ size={20}
973
+ color="#3B3269"
974
+ />
975
+ ),
976
+ default: (
977
+ <Upload
978
+ size={20}
979
+ color="#3B3269"
980
+ />
981
+ ),
982
+ }}
983
+ />;
984
+ ```
985
+
486
986
  ## Styling
487
987
 
488
988
  ### Web (React)
@@ -573,20 +1073,23 @@ using style props:
573
1073
  - `tabIndicatorStyle` - Tab indicator line
574
1074
  - `tabPanelStyle` - Tab panel container
575
1075
 
576
- **ImagesGallery/VideosGallery:**
1076
+ **ImagesGallery/VideosGallery/FilesGallery:**
577
1077
 
578
1078
  - `containerStyle` - Gallery container
579
1079
  - `gridStyle` - Media grid layout
580
- - `imageItemStyle` / `videoItemStyle` - Media item styles
1080
+ - `imageItemStyle` / `videoItemStyle` / `fileItemStyle` - Media item styles
581
1081
  - `paginationContainerStyle` - Pagination container
582
1082
 
583
1083
  **FileUpload:**
584
1084
 
585
1085
  - `containerStyle` - Upload container
586
1086
  - `buttonStyle` - Upload button
587
- - `iconContainerStyle` - Icon container
1087
+ - `iconContainerStyle` - Icon container (only shown if icon is provided)
588
1088
  - `titleStyle` - Upload title text
589
1089
  - `loadingStyle` - Loading state container
1090
+ - `icon` - Single icon (backward compatibility, overridden by `icons` prop)
1091
+ - `icons` - Object with `{ image, video, default }` icon components (React
1092
+ Native)
590
1093
 
591
1094
  **Pagination:**
592
1095
 
@@ -648,6 +1151,12 @@ For detailed styling documentation with examples, see
648
1151
  **React Native:**
649
1152
 
650
1153
  ```jsx
1154
+ import {
1155
+ CameraIcon,
1156
+ VideoIcon,
1157
+ UploadIcon,
1158
+ } from "@tradly/asset/native/Icons.native";
1159
+
651
1160
  <MediaPopup
652
1161
  isOpen={isOpen}
653
1162
  onClose={() => setIsOpen(false)}
@@ -655,9 +1164,25 @@ For detailed styling documentation with examples, see
655
1164
  options={["image", "video", "file"]}
656
1165
  apiService={apiService}
657
1166
  picker={picker} // Required
658
- />
1167
+ icons={{
1168
+ image: <CameraIcon />,
1169
+ video: <VideoIcon />,
1170
+ default: <UploadIcon />, // For file uploads
1171
+ }}
1172
+ />;
659
1173
  ```
660
1174
 
1175
+ ### File Type Gallery
1176
+
1177
+ The `file` option displays non-image, non-video files (PDFs, documents,
1178
+ archives, etc.):
1179
+
1180
+ - **Supported File Types**: PDF, Word, Excel, PowerPoint, ZIP, RAR, audio files,
1181
+ text files, and more
1182
+ - **File Icons**: Automatically displays appropriate icons based on MIME type
1183
+ - **File Information**: Shows file name and extension
1184
+ - **Upload Support**: Full upload functionality for all file types
1185
+
661
1186
  ## Platform-Specific Notes
662
1187
 
663
1188
  ### Web (React)
@@ -666,30 +1191,118 @@ For detailed styling documentation with examples, see
666
1191
  - Tailwind CSS for styling
667
1192
  - HTML file input for file selection
668
1193
  - Canvas API for image compression
1194
+ - Direct File/Blob objects for upload
669
1195
 
670
1196
  ### React Native
671
1197
 
672
- - Uses `Modal` component for bottom sheet
1198
+ - Uses `Modal` component for bottom sheet (slides up from bottom)
673
1199
  - React Native `StyleSheet` for styling
674
1200
  - Requires file picker library (configurable)
675
1201
  - Uses `FlatList` for efficient media rendering
676
1202
  - Supports both iOS and Android
1203
+ - Handles `file://` and `content://` URIs
1204
+ - Converts file URIs to blobs for S3 upload
1205
+ - Comprehensive MIME type detection
1206
+ - File name cleaning (removes spaces)
1207
+ - 3-column grid layout (optimized for mobile)
1208
+ - Customizable icons per media type
677
1209
 
678
- ## Development
1210
+ ## File Types Support
679
1211
 
680
- To develop or modify this package:
1212
+ The package supports three media types:
681
1213
 
682
- ```bash
683
- cd packages/asset
684
- npm install
685
- npm run dev
1214
+ - **`image`**: Images (JPEG, PNG, GIF, WebP, SVG, HEIC, etc.)
1215
+ - **`video`**: Videos (MP4, MOV, AVI, WebM, etc.)
1216
+ - **`file`**: Other files (PDF, Word, Excel, PowerPoint, ZIP, audio files, text
1217
+ files, etc.)
1218
+
1219
+ Each type has its own gallery component:
1220
+
1221
+ - `ImagesGallery` - For images
1222
+ - `VideosGallery` - For videos
1223
+ - `FilesGallery` - For non-image, non-video files
1224
+
1225
+ The package automatically uses the correct gallery based on the `options` prop.
1226
+
1227
+ ## MIME Type Detection
1228
+
1229
+ The package includes comprehensive MIME type detection:
1230
+
1231
+ - **From Picker Result**: Uses `mimeType` or `type` from picker response
1232
+ - **From File Extension**: Automatically detects MIME type from file extension
1233
+ - **Fallback**: Defaults to `application/octet-stream` if detection fails
1234
+
1235
+ Supported file types include:
1236
+
1237
+ - **Images**: jpg, jpeg, png, gif, webp, svg, bmp, ico, heic, heif, tiff, avif
1238
+ - **Videos**: mp4, mov, avi, wmv, flv, mkv, webm, mpeg, mpg, m4v, 3gp
1239
+ - **Audio**: mp3, wav, ogg, m4a, aac, flac
1240
+ - **Documents**: pdf, doc, docx, xls, xlsx, ppt, pptx
1241
+ - **Archives**: zip, rar, 7z, tar, gz
1242
+ - **Text**: txt, html, css, js, json, xml, php
1243
+
1244
+ ## How It Works
1245
+
1246
+ The package automatically detects the platform and uses the appropriate
1247
+ components:
1248
+
1249
+ - **Web bundlers** (Webpack, Vite, etc.) automatically use `.web.jsx` files
1250
+ - **React Native bundlers** (Metro) automatically use `.native.jsx` files
1251
+
1252
+ You don't need to import different files - the package handles platform
1253
+ detection automatically:
1254
+
1255
+ ```jsx
1256
+ // Same import works for both web and React Native
1257
+ import { MediaPopup, MediaApiService } from "@tradly/asset";
686
1258
  ```
687
1259
 
688
- **Note:** The package automatically detects the platform and uses the
689
- appropriate components:
1260
+ The package uses platform-specific implementations under the hood:
1261
+
1262
+ - Web: Uses React DOM, Tailwind CSS, HTML file inputs
1263
+ - React Native: Uses React Native primitives, StyleSheet, configurable file
1264
+ pickers
1265
+
1266
+ ## Troubleshooting
1267
+
1268
+ ### React Native File Upload Issues
1269
+
1270
+ **Problem**: Files not uploading from React Native
1271
+
1272
+ **Solutions**:
690
1273
 
691
- - Web bundlers will use `.web.jsx` files
692
- - React Native bundlers (Metro) will use `.native.jsx` files
1274
+ 1. Ensure `picker` prop is provided and returns files in correct format
1275
+ 2. Check that file URIs are accessible (`file://` or `content://`)
1276
+ 3. Verify MIME type is being detected correctly (check console logs)
1277
+ 4. Ensure proper permissions are granted for file access
1278
+
1279
+ **Problem**: MIME type not detected properly
1280
+
1281
+ **Solutions**:
1282
+
1283
+ 1. The package automatically detects MIME type from:
1284
+ - Picker result (`mimeType` or `type` property)
1285
+ - File extension (comprehensive mapping included)
1286
+ 2. If detection fails, it defaults to `application/octet-stream`
1287
+ 3. Check console logs to see detected MIME type
1288
+
1289
+ **Problem**: Bottom sheet not showing properly
1290
+
1291
+ **Solutions**:
1292
+
1293
+ 1. Adjust `bottomSheetHeight` prop (default: 0.9 = 90%)
1294
+ 2. Check container padding in theme
1295
+ 3. Ensure `overflow: 'hidden'` is applied (already included)
1296
+
1297
+ ### Grid Layout Issues
1298
+
1299
+ **Problem**: Items showing 2 per row instead of 3
1300
+
1301
+ **Solutions**:
1302
+
1303
+ 1. Check container padding matches between skeleton and gallery
1304
+ 2. Verify `ITEM_SIZE` calculation accounts for all padding/margins
1305
+ 3. Ensure `FlatList` with `numColumns={3}` is used (not `flexWrap`)
693
1306
 
694
1307
  ## License
695
1308
 
@@ -699,3 +1312,145 @@ MIT
699
1312
 
700
1313
  For issues and questions, please open an issue on the repository.
701
1314
 
1315
+ ## Quick Reference
1316
+
1317
+ ### Complete React Native Example
1318
+
1319
+ ```jsx
1320
+ import React, { useState } from "react";
1321
+ import { View, Button, Alert } from "react-native";
1322
+ import { MediaPopup, MediaApiService } from "@tradly/asset";
1323
+ import {
1324
+ CameraIcon,
1325
+ VideoIcon,
1326
+ UploadIcon,
1327
+ } from "@tradly/asset/native/Icons.native";
1328
+ import { launchImageLibrary } from "react-native-image-picker";
1329
+ import { Platform } from "react-native";
1330
+
1331
+ function App() {
1332
+ const [isOpen, setIsOpen] = useState(false);
1333
+ const [selectedMedia, setSelectedMedia] = useState(null);
1334
+
1335
+ const apiService = new MediaApiService({
1336
+ authKey: "your-tradly-auth-key",
1337
+ bearerToken: "your-bearer-token",
1338
+ environment: "dev", // Auto-detected from process.env.ENVIRONMENT
1339
+ });
1340
+
1341
+ const handleSelect = (mediaUrl) => {
1342
+ setSelectedMedia(mediaUrl);
1343
+ setIsOpen(false);
1344
+ Alert.alert("Selected", mediaUrl);
1345
+ };
1346
+
1347
+ const handleError = (error) => {
1348
+ console.error("Error:", error);
1349
+ Alert.alert("Error", error.message || "Something went wrong");
1350
+ };
1351
+
1352
+ // File picker configuration
1353
+ const picker = (options) => {
1354
+ return new Promise((resolve) => {
1355
+ launchImageLibrary(options, (response) => {
1356
+ if (response.didCancel) {
1357
+ resolve([]);
1358
+ return;
1359
+ }
1360
+ if (response.errorMessage) {
1361
+ Alert.alert("Error", response.errorMessage);
1362
+ resolve([]);
1363
+ return;
1364
+ }
1365
+ resolve(response.assets || []);
1366
+ });
1367
+ });
1368
+ };
1369
+
1370
+ // Custom picker options
1371
+ const pickerOptions = (accept) => {
1372
+ const isImage = accept?.includes("image");
1373
+ const isVideo = accept?.includes("video");
1374
+ const isFile = accept?.includes("file");
1375
+
1376
+ return {
1377
+ mediaType: isImage ? "photo" : isVideo ? "video" : "mixed",
1378
+ quality: 0.8,
1379
+ allowsMultiple: true,
1380
+ selectionLimit: 10,
1381
+ };
1382
+ };
1383
+
1384
+ return (
1385
+ <View style={{ flex: 1, justifyContent: "center", padding: 20 }}>
1386
+ <Button
1387
+ title="Open Media Gallery"
1388
+ onPress={() => setIsOpen(true)}
1389
+ />
1390
+
1391
+ <MediaPopup
1392
+ isOpen={isOpen}
1393
+ onClose={() => setIsOpen(false)}
1394
+ onSelect={handleSelect}
1395
+ onError={handleError}
1396
+ options={["image", "video", "file"]}
1397
+ apiService={apiService}
1398
+ picker={picker}
1399
+ pickerOptions={pickerOptions}
1400
+ icons={{
1401
+ image: <CameraIcon />,
1402
+ video: <VideoIcon />,
1403
+ default: <UploadIcon />,
1404
+ }}
1405
+ bottomSheetHeight={0.9}
1406
+ title="Select Media"
1407
+ />
1408
+ </View>
1409
+ );
1410
+ }
1411
+
1412
+ export default App;
1413
+ ```
1414
+
1415
+ ### Complete Web Example
1416
+
1417
+ ```jsx
1418
+ import React, { useState } from "react";
1419
+ import { MediaPopup, MediaApiService } from "@tradly/asset";
1420
+
1421
+ function App() {
1422
+ const [isOpen, setIsOpen] = useState(false);
1423
+ const [selectedMedia, setSelectedMedia] = useState(null);
1424
+
1425
+ const apiService = new MediaApiService({
1426
+ authKey: "your-tradly-auth-key",
1427
+ bearerToken: "your-bearer-token",
1428
+ // environment is auto-detected from process.env.ENVIRONMENT
1429
+ });
1430
+
1431
+ const handleSelect = (mediaUrl) => {
1432
+ setSelectedMedia(mediaUrl);
1433
+ setIsOpen(false);
1434
+ console.log("Selected:", mediaUrl);
1435
+ };
1436
+
1437
+ return (
1438
+ <div>
1439
+ <button onClick={() => setIsOpen(true)}>
1440
+ Open Media Gallery
1441
+ </button>
1442
+
1443
+ <MediaPopup
1444
+ isOpen={isOpen}
1445
+ onClose={() => setIsOpen(false)}
1446
+ onSelect={handleSelect}
1447
+ options={["image", "video", "file"]}
1448
+ apiService={apiService}
1449
+ />
1450
+ </div>
1451
+ );
1452
+ }
1453
+
1454
+ export default App;
1455
+ ```
1456
+
@@ -101,14 +101,21 @@ var FileUpload = function FileUpload(_ref) {
101
101
  // Determine which icon to use based on accept type and icons prop
102
102
  var getIcon = function getIcon() {
103
103
  // If icons object is provided, use it to select based on accept type
104
- if (icons && _typeof(icons) === "object") {
104
+ if (icons && _typeof(icons) === "object" && !Array.isArray(icons)) {
105
105
  var isImage = accept === null || accept === void 0 ? void 0 : accept.includes("image");
106
106
  var isVideo = accept === null || accept === void 0 ? void 0 : accept.includes("video");
107
+ var isFile = (accept === null || accept === void 0 ? void 0 : accept.includes("file")) || !isImage && !isVideo;
108
+
109
+ // Priority: specific icon > default icon > single icon prop
107
110
  if (isImage && icons.image) {
108
111
  return icons.image;
109
112
  } else if (isVideo && icons.video) {
110
113
  return icons.video;
114
+ } else if (isFile && icons.default) {
115
+ // Use default icon for file types
116
+ return icons.default;
111
117
  } else if (icons.default) {
118
+ // Fallback to default if specific icon not found
112
119
  return icons.default;
113
120
  }
114
121
  }
@@ -251,11 +258,6 @@ var FileUpload = function FileUpload(_ref) {
251
258
  if (!mimeType || !mimeType.includes("/")) {
252
259
  mimeType = "application/octet-stream";
253
260
  }
254
- console.log("mimeType", {
255
- uri: uri,
256
- name: fileName,
257
- type: mimeType
258
- });
259
261
  return {
260
262
  uri: uri,
261
263
  name: fileName,
@@ -412,7 +414,7 @@ var FileUpload = function FileUpload(_ref) {
412
414
  borderRadius: theme.radius.md
413
415
  }, buttonStyle],
414
416
  disabled: isLoading,
415
- children: [selectedIcon && /*#__PURE__*/_jsx(View, {
417
+ children: [selectedIcon ? /*#__PURE__*/_jsx(View, {
416
418
  style: [styles.iconContainer, {
417
419
  backgroundColor: theme.colors.uploadIconBackground,
418
420
  borderRadius: theme.radius.xl,
@@ -420,7 +422,7 @@ var FileUpload = function FileUpload(_ref) {
420
422
  padding: theme.spacing.md
421
423
  }, iconContainerStyle],
422
424
  children: selectedIcon
423
- }), /*#__PURE__*/_jsx(Text, {
425
+ }) : null, /*#__PURE__*/_jsx(Text, {
424
426
  style: [styles.title, {
425
427
  color: theme.colors.uploadText,
426
428
  fontSize: theme.typography.body.fontSize,
@@ -106,14 +106,21 @@ var FileUpload = function FileUpload(_ref) {
106
106
  // Determine which icon to use based on accept type and icons prop
107
107
  var getIcon = function getIcon() {
108
108
  // If icons object is provided, use it to select based on accept type
109
- if (icons && _typeof(icons) === "object") {
109
+ if (icons && _typeof(icons) === "object" && !Array.isArray(icons)) {
110
110
  var isImage = accept === null || accept === void 0 ? void 0 : accept.includes("image");
111
111
  var isVideo = accept === null || accept === void 0 ? void 0 : accept.includes("video");
112
+ var isFile = (accept === null || accept === void 0 ? void 0 : accept.includes("file")) || !isImage && !isVideo;
113
+
114
+ // Priority: specific icon > default icon > single icon prop
112
115
  if (isImage && icons.image) {
113
116
  return icons.image;
114
117
  } else if (isVideo && icons.video) {
115
118
  return icons.video;
119
+ } else if (isFile && icons.default) {
120
+ // Use default icon for file types
121
+ return icons.default;
116
122
  } else if (icons.default) {
123
+ // Fallback to default if specific icon not found
117
124
  return icons.default;
118
125
  }
119
126
  }
@@ -256,11 +263,6 @@ var FileUpload = function FileUpload(_ref) {
256
263
  if (!mimeType || !mimeType.includes("/")) {
257
264
  mimeType = "application/octet-stream";
258
265
  }
259
- console.log("mimeType", {
260
- uri: uri,
261
- name: fileName,
262
- type: mimeType
263
- });
264
266
  return {
265
267
  uri: uri,
266
268
  name: fileName,
@@ -417,7 +419,7 @@ var FileUpload = function FileUpload(_ref) {
417
419
  borderRadius: theme.radius.md
418
420
  }, buttonStyle],
419
421
  disabled: isLoading,
420
- children: [selectedIcon && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
422
+ children: [selectedIcon ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
421
423
  style: [styles.iconContainer, {
422
424
  backgroundColor: theme.colors.uploadIconBackground,
423
425
  borderRadius: theme.radius.xl,
@@ -425,7 +427,7 @@ var FileUpload = function FileUpload(_ref) {
425
427
  padding: theme.spacing.md
426
428
  }, iconContainerStyle],
427
429
  children: selectedIcon
428
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
430
+ }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
429
431
  style: [styles.title, {
430
432
  color: theme.colors.uploadText,
431
433
  fontSize: theme.typography.body.fontSize,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tradly/asset",
3
- "version": "1.0.26",
3
+ "version": "1.0.28",
4
4
  "description": "A reusable media gallery component for uploading and selecting images, videos, and files with Tradly authentication",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/esm/index.js",