@tradly/asset
v1.0.30
Published
A reusable media gallery component for uploading and selecting images, videos, and files with Tradly authentication
Readme
@tradly/asset
A cross-platform React component package for uploading and selecting media (images, videos, files) with Tradly authentication support. Works seamlessly on both Web (React) and Mobile (React Native).
Features
- 📸 Upload and browse images
- 🎥 Upload and browse videos
- 📁 Upload and browse files
- 🔐 Tradly authentication support
- 🎨 Customizable UI
- 📱 Responsive design (Web) + Native mobile support (React Native)
- 🔄 Pagination support
- ⚡ Lightweight and performant
- 🌐 Cross-platform: Works on Web (React) and Mobile (React Native)
Installation
npm install @tradly/asset
# or
yarn add @tradly/assetPeer Dependencies
Web (React)
react(>=16.8.0)react-dom(>=16.8.0)axios(^0.24.0) - for API calls
React Native
react(>=16.8.0)react-native(>=0.60.0)axios(^0.24.0) - for API calls- File Picker Library (choose one):
react-native-image-picker- Recommended for React Native CLIexpo-image-picker- For Expo projects- Any other picker library that returns
{ uri, type, fileName }format
Note:
- All pagination logic is built-in, no external pagination library needed!
- No Tradly SDK dependency - uses direct API calls
- React Native: File picker is configurable - you choose which library to use
Basic Usage
Web (React)
import React, { useState } from "react";
import { MediaPopup, MediaApiService } from "@tradly/asset";
function MyComponent() {
const [isOpen, setIsOpen] = useState(false);
const [selectedMedia, setSelectedMedia] = useState(null);
// Initialize API service with your auth key
// API base URL is automatically detected from ENVIRONMENT:
// - Dev: https://api.dev.tradly.app
// - Prod: https://api.tradly.app
const apiService = new MediaApiService({
authKey: "your-tradly-auth-key", // Required: X-Auth-Key header
bearerToken: "your-bearer-token", // Required: Bearer token for Authorization header
// environment is auto-detected from process.env.ENVIRONMENT
// apiBaseUrl is auto-set based on environment (can be overridden)
});
const handleSelect = (mediaUrl) => {
setSelectedMedia(mediaUrl);
setIsOpen(false);
};
return (
<>
<button onClick={() => setIsOpen(true)}>
Open Media Gallery
</button>
<MediaPopup
isOpen={isOpen}
onClose={() => setIsOpen(false)}
onSelect={handleSelect}
options={["image", "video"]} // Options: 'image', 'video', 'file'
apiService={apiService}
/>
</>
);
}React Native
import React, { useState } from "react";
import { View, Button } from "react-native";
import { MediaPopup, MediaApiService } from "@tradly/asset";
import {
CameraIcon,
VideoIcon,
UploadIcon,
} from "@tradly/asset/native/Icons.native";
import { launchImageLibrary } from "react-native-image-picker";
function MyComponent() {
const [isOpen, setIsOpen] = useState(false);
const [selectedMedia, setSelectedMedia] = useState(null);
const apiService = new MediaApiService({
authKey: "your-tradly-auth-key",
bearerToken: "your-bearer-token",
environment: "dev", // or "production"
});
const handleSelect = (mediaUrl) => {
setSelectedMedia(mediaUrl);
setIsOpen(false);
};
// Configure file picker - REQUIRED for React Native
const picker = (options) => {
return new Promise((resolve) => {
launchImageLibrary(options, (response) => {
if (response.didCancel) {
resolve([]);
return;
}
resolve(response.assets || []);
});
});
};
return (
<View>
<Button
title="Open Media Gallery"
onPress={() => setIsOpen(true)}
/>
<MediaPopup
isOpen={isOpen}
onClose={() => setIsOpen(false)}
onSelect={handleSelect}
options={["image", "video", "file"]}
apiService={apiService}
picker={picker} // REQUIRED: Your file picker function
icons={{
image: <CameraIcon />,
video: <VideoIcon />,
default: <UploadIcon />,
}}
/>
</View>
);
}React Native Setup
File Picker Configuration
React Native requires you to provide a file picker function. The package
supports any picker library that returns files in the format
{ uri, type, fileName }.
Option 1: Using react-native-image-picker
npm install react-native-image-picker
# or
yarn add react-native-image-pickerimport { launchImageLibrary } from "react-native-image-picker";
<MediaPopup
picker={(options) => {
return new Promise((resolve) => {
launchImageLibrary(options, (response) => {
if (response.didCancel) {
resolve([]);
return;
}
resolve(response.assets || []);
});
});
}}
apiService={apiService}
// ... other props
/>;Option 2: Using expo-image-picker
npx expo install expo-image-pickerimport * as ImagePicker from "expo-image-picker";
<MediaPopup
picker={async (options) => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes:
options.mediaType === "photo"
? ImagePicker.MediaTypeOptions.Images
: options.mediaType === "video"
? ImagePicker.MediaTypeOptions.Videos
: ImagePicker.MediaTypeOptions.All,
allowsMultipleSelection: options.allowsMultiple,
quality: options.quality,
});
if (result.canceled) {
return [];
}
return result.assets || [];
}}
apiService={apiService}
// ... other props
/>;Option 3: Custom Picker Options
You can customize picker options based on the accept type:
<MediaPopup
picker={yourPickerFunction}
pickerOptions={(accept) => {
const isImage = accept?.includes("image");
const isVideo = accept?.includes("video");
const isFile = accept?.includes("file");
return {
mediaType: isImage ? "photo" : isVideo ? "video" : "mixed",
quality: 0.9,
allowsMultiple: true,
// Add any other options your picker library supports
};
}}
apiService={apiService}
// ... other props
/>File Picker Response Format
The picker function should return an array of file objects. The package supports multiple formats:
react-native-image-picker format:
{
assets: [
{
uri: "file:///path/to/file.jpg",
fileName: "image.jpg",
mimeType: "image/jpeg",
fileSize: 12345,
},
];
}expo-image-picker format:
{
assets: [
{
uri: "file:///path/to/file.jpg",
fileName: "image.jpg",
mimeType: "image/jpeg",
},
];
}Custom format:
[
{
uri: "file:///path/to/file.jpg",
name: "image.jpg", // or fileName
type: "image/jpeg", // or mimeType
},
];The package will automatically:
- Extract
urifromuriorpath - Extract file name from
fileName,name, or parse from URI - Detect MIME type from
mimeType,type, or file extension - Handle missing properties with smart fallbacks
React Native UI Differences
- Bottom Sheet: Media gallery opens as a bottom sheet (slides up from bottom) - defaults to 90% of screen height, customizable
- Native Styling: Uses React Native
StyleSheetinstead of Tailwind CSS - Components: Uses React Native primitives (
View,Text,TouchableOpacity,FlatList,Image) - File Upload: Requires
pickerprop to be provided - Theme System: Full theme support for easy dark/light mode customization
- Icons: Support for custom icons per media type (image, video, file)
- File Handling: Automatically handles
file://URIs and converts them to blobs for S3 upload - MIME Type Detection: Comprehensive MIME type detection from file extensions and picker results
React Native Theme & Customization
The package includes a comprehensive theme system that makes it easy to customize colors, spacing, and styling for dark/light mode.
Basic Theme Usage
import { MediaPopup, MediaApiService } from "@tradly/asset";
import { createTheme } from "@tradly/asset/native/theme";
// Use dark mode
const darkTheme = createTheme({ mode: "dark" });
// Use light mode (default)
const lightTheme = createTheme({ mode: "light" });
<MediaPopup
apiService={apiService}
picker={picker}
theme={darkTheme} // Pass your theme
bottomSheetHeight={0.9} // 90% of screen (default), customize as needed
/>;Theme Examples
Example 1: Complete Custom Theme
import { createTheme } from "@tradly/asset/native/theme";
const customTheme = createTheme({
mode: "dark",
colors: {
// Primary colors
primary: "#6366F1",
primaryLight: "#818CF8",
primaryDark: "#4F46E5",
// Backgrounds
background: "#0F172A",
backgroundSecondary: "#1E293B",
backgroundTertiary: "#334155",
// Text
text: "#F1F5F9",
textSecondary: "#CBD5E1",
textTertiary: "#94A3B8",
// Tabs
tabActive: "#6366F1",
tabInactive: "#94A3B8",
tabIndicator: "#6366F1",
tabBackground: "#0F172A",
// Upload
uploadBorder: "#6366F1",
uploadBackground: "#1E293B",
uploadIconBackground: "#6366F1",
uploadText: "#F1F5F9",
// Pagination
paginationBackground: "#1E293B",
paginationButtonActive: "#6366F1",
paginationTextActive: "#FFFFFF",
// And all other colors...
},
spacing: {
xs: 4,
sm: 8,
md: 12,
lg: 16,
xl: 20,
xxl: 24,
},
radius: {
sm: 6,
md: 8,
lg: 12,
xl: 16,
xxl: 20,
},
bottomSheetHeight: 0.92,
});
<MediaPopup theme={customTheme} ... />Example 2: Minimal Override (Only Colors)
// Only override specific colors, rest uses defaults
const minimalTheme = createTheme({
colors: {
primary: "#FF6B6B",
tabActive: "#FF6B6B",
tabIndicator: "#FF6B6B",
},
});Complete Theme Reference
All Available Theme Properties:
| Category | Property | Type | Description | Default (Light) |
| ----------------------- | ------------------------ | -------- | --------------------- | ----------------- |
| Colors - Primary | primary | string | Main brand color | #3B3269 |
| | primaryLight | string | Lighter variant | #5A4A8A |
| | primaryDark | string | Darker variant | #2A1F4A |
| Colors - Background | background | string | Main background | #FFFFFF |
| | backgroundSecondary | string | Secondary background | #F5F5F5 |
| | backgroundTertiary | string | Tertiary background | #E5E5E5 |
| Colors - Text | text | string | Primary text | #000000 |
| | textSecondary | string | Secondary text | #4F4F4F |
| | textTertiary | string | Tertiary text | #6B7280 |
| | textInverse | string | Inverse text | #FFFFFF |
| Colors - Border | border | string | Default border | #E5E5E5 |
| | borderLight | string | Light border | #F0F0F0 |
| | borderDark | string | Dark border | #D1D5DB |
| Colors - Overlay | overlay | string | Modal overlay | rgba(0,0,0,0.5) |
| Colors - Tabs | tabActive | string | Active tab text | #3B3269 |
| | tabInactive | string | Inactive tab text | #4F4F4F |
| | tabIndicator | string | Tab indicator line | #3B3269 |
| | tabBackground | string | Tab container | #FFFFFF |
| Colors - Items | itemBackground | string | Media item background | #FFFFFF |
| | itemShadow | string | Item shadow color | #000000 |
| Colors - Pagination | paginationBackground | string | Container background | #F5F5F5 |
| | paginationButton | string | Button background | #FFFFFF |
| | paginationButtonActive | string | Active button | #3B3269 |
| | paginationText | string | Text color | #6B7280 |
| | paginationTextActive | string | Active text | #FFFFFF |
| | paginationBorder | string | Border color | #D1D5DB |
| Colors - Upload | uploadBorder | string | Button border | #3B3269 |
| | uploadBackground | string | Button background | #FFFFFF |
| | uploadIconBackground | string | Icon container | #3B3269 |
| | uploadText | string | Button text | #000000 |
| Colors - Loading | loadingBackground | string | Skeleton background | #E5E5E5 |
| | loadingText | string | Loading text | #666666 |
| Spacing | xs | number | Extra small (4px) | 4 |
| | sm | number | Small (8px) | 8 |
| | md | number | Medium (12px) | 12 |
| | lg | number | Large (16px) | 16 |
| | xl | number | Extra large (20px) | 20 |
| | xxl | number | 2X extra large (24px) | 24 |
| Radius | sm | number | Small radius (6px) | 6 |
| | md | number | Medium radius (8px) | 8 |
| | lg | number | Large radius (12px) | 12 |
| | xl | number | Extra large (16px) | 16 |
| | xxl | number | 2X extra large (20px) | 20 |
| Typography | title.fontSize | number | Title font size | 24 |
| | title.fontWeight | string | Title weight | "bold" |
| | subtitle.fontSize | number | Subtitle size | 18 |
| | subtitle.fontWeight | string | Subtitle weight | "600" |
| | body.fontSize | number | Body font size | 16 |
| | body.fontWeight | string | Body weight | "500" |
| | caption.fontSize | number | Caption size | 14 |
| | caption.fontWeight | string | Caption weight | "400" |
| Other | bottomSheetHeight | number | Sheet height (0-1) | 0.9 |
| | mode | string | "light" or "dark" | "light" |
Complete Theme Customization
The theme system supports comprehensive customization of colors, spacing, typography, and more:
Colors:
const customTheme = createTheme({
mode: "light", // or "dark"
colors: {
// Primary colors
primary: "#3B3269", // Main brand color
primaryLight: "#5A4A8A", // Lighter variant
primaryDark: "#2A1F4A", // Darker variant
// Background colors
background: "#FFFFFF", // Main background
backgroundSecondary: "#F5F5F5", // Secondary background
backgroundTertiary: "#E5E5E5", // Tertiary background
// Text colors
text: "#000000", // Primary text
textSecondary: "#4F4F4F", // Secondary text
textTertiary: "#6B7280", // Tertiary text
textInverse: "#FFFFFF", // Inverse text (for dark backgrounds)
// Border colors
border: "#E5E5E5", // Default border
borderLight: "#F0F0F0", // Light border
borderDark: "#D1D5DB", // Dark border
// Overlay
overlay: "rgba(0, 0, 0, 0.5)", // Modal overlay
// Tab colors
tabActive: "#3B3269", // Active tab text
tabInactive: "#4F4F4F", // Inactive tab text
tabIndicator: "#3B3269", // Tab indicator line
tabBackground: "#FFFFFF", // Tab container background
// Media item colors
itemBackground: "#FFFFFF", // Image/video item background
itemShadow: "#000000", // Item shadow color
// Pagination colors
paginationBackground: "#F5F5F5", // Pagination container
paginationButton: "#FFFFFF", // Page button background
paginationButtonActive: "#3B3269", // Active page button
paginationText: "#6B7280", // Page number text
paginationTextActive: "#FFFFFF", // Active page text
paginationBorder: "#D1D5DB", // Pagination border
// Upload button colors
uploadBorder: "#3B3269", // Upload button border
uploadBackground: "#FFFFFF", // Upload button background
uploadIconBackground: "#3B3269", // Icon container background
uploadText: "#000000", // Upload button text
// Loading state colors
loadingBackground: "#E5E5E5", // Loading skeleton background
loadingText: "#666666", // Loading text color
},
// Spacing (in pixels)
spacing: {
xs: 4, // Extra small
sm: 8, // Small
md: 12, // Medium
lg: 16, // Large
xl: 20, // Extra large
xxl: 24, // 2X extra large
},
// Border radius (in pixels)
radius: {
sm: 6, // Small radius
md: 8, // Medium radius
lg: 12, // Large radius
xl: 16, // Extra large radius
xxl: 20, // 2X extra large radius
},
// Typography
typography: {
title: {
fontSize: 24,
fontWeight: "bold",
},
subtitle: {
fontSize: 18,
fontWeight: "600",
},
body: {
fontSize: 16,
fontWeight: "500",
},
caption: {
fontSize: 14,
fontWeight: "400",
},
},
// Bottom sheet height (0.85 = 85%, 0.9 = 90%, etc.)
bottomSheetHeight: 0.9,
});Partial Theme Override:
You can override only specific parts of the theme - other values will use defaults:
// Only change primary color and background
const brandTheme = createTheme({
colors: {
primary: "#6366F1",
tabActive: "#6366F1",
tabIndicator: "#6366F1",
background: "#F8F9FA",
},
});
// Only change spacing
const compactTheme = createTheme({
spacing: {
md: 8, // Reduce medium spacing
lg: 12, // Reduce large spacing
},
});
// Only change bottom sheet height
const tallTheme = createTheme({
bottomSheetHeight: 0.95, // 95% of screen
});Dark Mode Example
import { createTheme } from "@tradly/asset/native/theme";
const darkTheme = createTheme({
mode: "dark",
colors: {
primary: "#6366F1",
background: "#0F172A",
text: "#F1F5F9",
tabActive: "#6366F1",
tabInactive: "#94A3B8",
// ... customize other colors
},
});
<MediaPopup theme={darkTheme} ... />Brand Color Customization
const brandTheme = createTheme({
colors: {
// Your brand colors
primary: "#FF6B6B",
tabActive: "#FF6B6B",
tabIndicator: "#FF6B6B",
uploadBorder: "#FF6B6B",
uploadIconBackground: "#FF6B6B",
paginationButtonActive: "#FF6B6B",
},
});Customizing Bottom Sheet Height
<MediaPopup
bottomSheetHeight={0.85} // 85% of screen height
// or via theme:
theme={createTheme({ bottomSheetHeight: 0.92 })}
/>Individual Component Styling
You can override individual component styles in addition to theme:
<MediaPopup
theme={darkTheme}
// Override specific styles (these override theme values)
containerStyle={{ padding: 24 }}
headerStyle={{ paddingBottom: 16 }}
titleStyle={{ fontSize: 28, color: "#FFFFFF" }}
closeButtonStyle={{ padding: 12 }}
closeButtonTextStyle={{ color: "#FFFFFF" }}
// Tab styles
tabListStyle={{ paddingHorizontal: 8 }}
tabButtonStyle={{ paddingVertical: 16 }}
tabButtonActiveStyle={{ backgroundColor: "#F0F0F0" }}
tabButtonTextActiveStyle={{ fontWeight: "700" }}
tabButtonTextInactiveStyle={{ color: "#999999" }}
tabIndicatorStyle={{ height: 3 }}
// Gallery styles
gridStyle={{ padding: 8 }}
imageItemStyle={{ borderRadius: 12 }}
videoItemStyle={{ borderRadius: 12 }}
fileItemStyle={{ borderRadius: 12 }}
paginationContainerStyle={{ padding: 16 }}
// ... and many more style props
/>Advanced Usage
Upload Functionality
The package includes complete upload functionality using direct API calls. All upload logic is handled internally:
- Get S3 signed URLs via API call to
/v1/utils/S3signedUploadURL - Upload files to S3 using the signed URLs
- Save media metadata to your API
React Native File Upload:
The package automatically handles React Native file formats:
- Converts
file://URIs to blobs for S3 upload - Handles
content://URIs (Android) - Supports multiple file picker libraries
- Automatic MIME type detection from file extensions
- Proper file name cleaning (removes spaces)
File Format Support:
The package accepts files in these formats:
- react-native-image-picker:
{ uri, fileName, mimeType, ... } - expo-image-picker:
{ uri, fileName, mimeType, ... } - Custom pickers:
{ uri, name, type, ... }or{ uri, fileName, mimeType, ... }
The package will automatically:
- Extract file name from
fileName,name, or URI - Detect MIME type from
mimeType,type, or file extension - Handle file URIs (
file://,content://) for upload - Clean file names (removes spaces for S3 compatibility)
- Convert file URIs to blobs for S3 PUT requests
Upload Process:
- File Selection: User selects files via picker
- File Processing: Package converts picker results to standardized format
- MIME Type Detection: Automatically detects MIME type from extension or picker result
- S3 Signed URL: Gets signed URL from Tradly API
- File Upload: Fetches file from URI, converts to blob, uploads to S3
- Metadata Save: Saves media metadata to Tradly API
Configuration:
import { MediaApiService } from "@tradly/asset";
const apiService = new MediaApiService({
authKey: "your-auth-key", // Required: X-Auth-Key header
bearerToken: "your-bearer-token", // Required: Bearer token for Authorization header
environment: "dev", // Optional: 'dev' or 'production' (auto-detected from process.env.ENVIRONMENT)
apiBaseUrl: "https://api.tradly.app", // Optional: Override auto-detected base URL
});API Base URL Auto-Detection:
- If
environmentincludes 'dev' →https://api.dev.tradly.app - Otherwise →
https://api.tradly.app - You can override by providing
apiBaseUrlexplicitly
Using Individual Components
You can use individual gallery components if you need more control:
Web:
import {
ImagesGallery,
VideosGallery,
FilesGallery,
MediaApiService,
} from "@tradly/asset";
function CustomGallery() {
const apiService = new MediaApiService({
authKey: "your-auth-key",
bearerToken: "your-bearer-token",
});
return (
<ImagesGallery
update_data={(url) => console.log("Selected:", url)}
closePopup={() => console.log("Closed")}
apiService={apiService}
/>
);
}React Native:
import {
ImagesGallery,
VideosGallery,
FilesGallery,
MediaApiService,
} from "@tradly/asset";
import {
CameraIcon,
VideoIcon,
UploadIcon,
} from "@tradly/asset/native/Icons.native";
import { launchImageLibrary } from "react-native-image-picker";
function CustomGallery() {
const apiService = new MediaApiService({
authKey: "your-auth-key",
bearerToken: "your-bearer-token",
});
const picker = (options) => {
return new Promise((resolve) => {
launchImageLibrary(options, (response) => {
if (response.didCancel) {
resolve([]);
return;
}
resolve(response.assets || []);
});
});
};
return (
<FilesGallery
update_data={(url) => console.log("Selected:", url)}
closePopup={() => console.log("Closed")}
apiService={apiService}
picker={picker}
icons={{ default: <UploadIcon /> }}
/>
);
}Error Handling
<MediaPopup
isOpen={isOpen}
onClose={() => setIsOpen(false)}
onSelect={handleSelect}
apiService={apiService}
onError={(error) => {
console.error("Media gallery error:", error);
// Handle error (show toast, etc.)
}}
/>API Service Configuration
MediaApiService Options
const apiService = new MediaApiService({
authKey: "string", // Required: Authentication key for X-Auth-Key header
bearerToken: "string", // Required: Bearer token for Authorization header
environment: "string", // Optional: 'dev' or 'production' (auto-detected from process.env.ENVIRONMENT)
apiBaseUrl: "string", // Optional: Override auto-detected base URL (defaults: https://api.dev.tradly.app for dev, https://api.tradly.app for prod)
onError: (error) => {}, // Optional: Global error handler
});Methods
// Update auth key
apiService.setAuthKey("new-auth-key");
// Update API base URL (used for all API calls)
apiService.setApiBaseUrl("https://api.tradly.app");
// Set Bearer token
apiService.setBearerToken("new-bearer-token");Props
MediaPopup Props
| Prop | Type | Default | Description | Platform |
| ------------------- | ----------------- | ----------------- | --------------------------------------------------- | ------------ |
| isOpen | boolean | false | Controls popup visibility | Web & Native |
| onClose | function | - | Callback when popup closes | Web & Native |
| onSelect | function | - | Callback when media is selected | Web & Native |
| currentData | any | - | Currently selected media data | Web & Native |
| options | array | ['image'] | Media types to show: 'image', 'video', 'file' | Web & Native |
| apiService | MediaApiService | - | Required: API service instance | Web & Native |
| onError | function | - | Error handler callback | Web & Native |
| title | string | 'Media Gallery' | Popup title | Web & Native |
| picker | function | - | Required for React Native: File picker function | Native only |
| pickerOptions | function | - | Optional: Function to generate picker options | Native only |
| icons | object | - | Icon components: { image, video, default } | Native only |
| theme | object | - | Theme object for colors/spacing (see Theme docs) | Native only |
| bottomSheetHeight | number | 0.9 | Bottom sheet height (0.85 = 85%, 0.9 = 90%, etc.) | Native only |
React Native Required Props:
picker: A function that accepts options and returns a Promise resolving to an array of file objects- Each file object should have:
{ uri: string, type?: string, fileName?: string } - Example:
picker={(options) => launchImageLibrary(options, callback)}
- Each file object should have:
Icons Configuration (React Native):
You can provide different icons for each media type:
import {
CameraIcon,
VideoIcon,
UploadIcon,
} from "@tradly/asset/native/Icons.native";
<MediaPopup
icons={{
image: <CameraIcon />, // Icon for image uploads
video: <VideoIcon />, // Icon for video uploads
default: <UploadIcon />, // Icon for file uploads (PDFs, docs, etc.)
}}
// ... other props
/>;If icons prop is not provided, the upload button will show only text (no
icon).
Available Icons:
The package includes built-in icons that you can use:
import {
CameraIcon, // For image uploads
VideoIcon, // For video uploads
UploadIcon, // For file uploads (default)
CloseIcon, // For close button (used internally)
} from "@tradly/asset/native/Icons.native";
<MediaPopup
icons={{
image: <CameraIcon />,
video: <VideoIcon />,
default: <UploadIcon />,
}}
/>;You can also use custom icon components from libraries like
react-native-vector-icons or lucide-react-native:
import { Camera, Video, Upload } from "lucide-react-native";
<MediaPopup
icons={{
image: (
<Camera
size={20}
color="#3B3269"
/>
),
video: (
<Video
size={20}
color="#3B3269"
/>
),
default: (
<Upload
size={20}
color="#3B3269"
/>
),
}}
/>;Styling
Web (React)
This package uses Tailwind CSS classes and supports full customization at every
component level. All components accept className props that allow you to
override default styles.
Quick Customization Example
<MediaPopup
isOpen={isOpen}
onClose={() => setIsOpen(false)}
onSelect={handleSelect}
apiService={apiService}
// Customize popup
overlayClassName="bg-black/60 backdrop-blur-sm"
containerClassName="max-w-4xl bg-gray-900 rounded-xl"
titleClassName="text-2xl font-bold text-white"
// Customize tabs
tabButtonActiveClassName="text-blue-400 border-b-2 border-blue-400"
// Customize gallery
gridClassName="grid grid-cols-4 gap-4"
imageItemClassName="rounded-lg border-2 hover:border-blue-500"
/>Available Styling Props
MediaPopup:
overlayClassName- Overlay/backdropcontainerClassName- Main popup containerheaderClassName- Header containertitleClassName- Title textcloseButtonClassName- Close buttontabListClassName- Tab list (passed to MediaTab)tabButtonClassName- Base tab buttontabButtonActiveClassName- Active tab buttontabButtonInactiveClassName- Inactive tab buttongridClassName- Media grid layoutimageItemClassName- Image item stylesvideoItemClassName- Video item stylespaginationContainerClassName- Pagination container
Individual Components: You can also style individual components when using
them separately. See STYLING_GUIDE.md for complete documentation.
React Native
React Native components use StyleSheet for styling. You can customize styles
using style props:
<MediaPopup
apiService={apiService}
picker={picker}
// Customize styles
containerStyle={{ backgroundColor: "#F5F5F5" }}
headerStyle={{ padding: 20 }}
titleStyle={{ fontSize: 28, color: "#000" }}
// ... other props
/>Available Style Props (React Native):
MediaPopup:
overlayStyle- Overlay/backdropcontainerStyle- Main popup containerheaderStyle- Header containertitleStyle- Title textcloseButtonStyle- Close buttoncloseButtonTextStyle- Close button text/icon
MediaTab:
containerStyle- Tab containertabListStyle- Tab listtabButtonStyle- Base tab buttontabButtonActiveStyle- Active tab buttontabButtonInactiveStyle- Inactive tab buttontabButtonTextStyle- Base tab texttabButtonTextActiveStyle- Active tab texttabButtonTextInactiveStyle- Inactive tab texttabIndicatorStyle- Tab indicator linetabPanelStyle- Tab panel container
ImagesGallery/VideosGallery/FilesGallery:
containerStyle- Gallery containergridStyle- Media grid layoutimageItemStyle/videoItemStyle/fileItemStyle- Media item stylespaginationContainerStyle- Pagination container
FileUpload:
containerStyle- Upload containerbuttonStyle- Upload buttoniconContainerStyle- Icon container (only shown if icon is provided)titleStyle- Upload title textloadingStyle- Loading state containericon- Single icon (backward compatibility, overridden byiconsprop)icons- Object with{ image, video, default }icon components (React Native)
Pagination:
containerStyle- Pagination containernavStyle- Navigation containerpreviousButtonStyle- Previous buttonnextButtonStyle- Next buttonpageButtonStyle- Page number buttonpageButtonActiveStyle- Active page buttonpageButtonTextStyle- Page button textpageButtonTextActiveStyle- Active page textellipsisStyle- Ellipsis container
Full Styling Guide
For detailed styling documentation with examples, see STYLING_GUIDE.md.
Examples
Image Only Gallery
<MediaPopup
isOpen={isOpen}
onClose={() => setIsOpen(false)}
onSelect={handleSelect}
options={["image"]}
apiService={apiService}
/>Video Only Gallery
<MediaPopup
isOpen={isOpen}
onClose={() => setIsOpen(false)}
onSelect={handleSelect}
options={["video"]}
apiService={apiService}
/>All Media Types
Web:
<MediaPopup
isOpen={isOpen}
onClose={() => setIsOpen(false)}
onSelect={handleSelect}
options={["image", "video", "file"]}
apiService={apiService}
/>React Native:
import {
CameraIcon,
VideoIcon,
UploadIcon,
} from "@tradly/asset/native/Icons.native";
<MediaPopup
isOpen={isOpen}
onClose={() => setIsOpen(false)}
onSelect={handleSelect}
options={["image", "video", "file"]}
apiService={apiService}
picker={picker} // Required
icons={{
image: <CameraIcon />,
video: <VideoIcon />,
default: <UploadIcon />, // For file uploads
}}
/>;File Type Gallery
The file option displays non-image, non-video files (PDFs, documents,
archives, etc.):
- Supported File Types: PDF, Word, Excel, PowerPoint, ZIP, RAR, audio files, text files, and more
- File Icons: Automatically displays appropriate icons based on MIME type
- File Information: Shows file name and extension
- Upload Support: Full upload functionality for all file types
Platform-Specific Notes
Web (React)
- Uses
react-domfor portal rendering - Tailwind CSS for styling
- HTML file input for file selection
- Canvas API for image compression
- Direct File/Blob objects for upload
React Native
- Uses
Modalcomponent for bottom sheet (slides up from bottom) - React Native
StyleSheetfor styling - Requires file picker library (configurable)
- Uses
FlatListfor efficient media rendering - Supports both iOS and Android
- Handles
file://andcontent://URIs - Converts file URIs to blobs for S3 upload
- Comprehensive MIME type detection
- File name cleaning (removes spaces)
- 3-column grid layout (optimized for mobile)
- Customizable icons per media type
File Types Support
The package supports three media types:
image: Images (JPEG, PNG, GIF, WebP, SVG, HEIC, etc.)video: Videos (MP4, MOV, AVI, WebM, etc.)file: Other files (PDF, Word, Excel, PowerPoint, ZIP, audio files, text files, etc.)
Each type has its own gallery component:
ImagesGallery- For imagesVideosGallery- For videosFilesGallery- For non-image, non-video files
The package automatically uses the correct gallery based on the options prop.
MIME Type Detection
The package includes comprehensive MIME type detection:
- From Picker Result: Uses
mimeTypeortypefrom picker response - From File Extension: Automatically detects MIME type from file extension
- Fallback: Defaults to
application/octet-streamif detection fails
Supported file types include:
- Images: jpg, jpeg, png, gif, webp, svg, bmp, ico, heic, heif, tiff, avif
- Videos: mp4, mov, avi, wmv, flv, mkv, webm, mpeg, mpg, m4v, 3gp
- Audio: mp3, wav, ogg, m4a, aac, flac
- Documents: pdf, doc, docx, xls, xlsx, ppt, pptx
- Archives: zip, rar, 7z, tar, gz
- Text: txt, html, css, js, json, xml, php
How It Works
The package automatically detects the platform and uses the appropriate components:
- Web bundlers (Webpack, Vite, etc.) automatically use
.web.jsxfiles - React Native bundlers (Metro) automatically use
.native.jsxfiles
You don't need to import different files - the package handles platform detection automatically:
// Same import works for both web and React Native
import { MediaPopup, MediaApiService } from "@tradly/asset";The package uses platform-specific implementations under the hood:
- Web: Uses React DOM, Tailwind CSS, HTML file inputs
- React Native: Uses React Native primitives, StyleSheet, configurable file pickers
Troubleshooting
React Native File Upload Issues
Problem: Files not uploading from React Native
Solutions:
- Ensure
pickerprop is provided and returns files in correct format - Check that file URIs are accessible (
file://orcontent://) - Verify MIME type is being detected correctly (check console logs)
- Ensure proper permissions are granted for file access
Problem: MIME type not detected properly
Solutions:
- The package automatically detects MIME type from:
- Picker result (
mimeTypeortypeproperty) - File extension (comprehensive mapping included)
- Picker result (
- If detection fails, it defaults to
application/octet-stream - Check console logs to see detected MIME type
Problem: Bottom sheet not showing properly
Solutions:
- Adjust
bottomSheetHeightprop (default: 0.9 = 90%) - Check container padding in theme
- Ensure
overflow: 'hidden'is applied (already included)
Grid Layout Issues
Problem: Items showing 2 per row instead of 3
Solutions:
- Check container padding matches between skeleton and gallery
- Verify
ITEM_SIZEcalculation accounts for all padding/margins - Ensure
FlatListwithnumColumns={3}is used (notflexWrap)
License
MIT
Support
For issues and questions, please open an issue on the repository.
Quick Reference
Complete React Native Example
import React, { useState } from "react";
import { View, Button, Alert } from "react-native";
import { MediaPopup, MediaApiService } from "@tradly/asset";
import {
CameraIcon,
VideoIcon,
UploadIcon,
} from "@tradly/asset/native/Icons.native";
import { launchImageLibrary } from "react-native-image-picker";
import { Platform } from "react-native";
function App() {
const [isOpen, setIsOpen] = useState(false);
const [selectedMedia, setSelectedMedia] = useState(null);
const apiService = new MediaApiService({
authKey: "your-tradly-auth-key",
bearerToken: "your-bearer-token",
environment: "dev", // Auto-detected from process.env.ENVIRONMENT
});
const handleSelect = (mediaUrl) => {
setSelectedMedia(mediaUrl);
setIsOpen(false);
Alert.alert("Selected", mediaUrl);
};
const handleError = (error) => {
console.error("Error:", error);
Alert.alert("Error", error.message || "Something went wrong");
};
// File picker configuration
const picker = (options) => {
return new Promise((resolve) => {
launchImageLibrary(options, (response) => {
if (response.didCancel) {
resolve([]);
return;
}
if (response.errorMessage) {
Alert.alert("Error", response.errorMessage);
resolve([]);
return;
}
resolve(response.assets || []);
});
});
};
// Custom picker options
const pickerOptions = (accept) => {
const isImage = accept?.includes("image");
const isVideo = accept?.includes("video");
const isFile = accept?.includes("file");
return {
mediaType: isImage ? "photo" : isVideo ? "video" : "mixed",
quality: 0.8,
allowsMultiple: true,
selectionLimit: 10,
};
};
return (
<View style={{ flex: 1, justifyContent: "center", padding: 20 }}>
<Button
title="Open Media Gallery"
onPress={() => setIsOpen(true)}
/>
<MediaPopup
isOpen={isOpen}
onClose={() => setIsOpen(false)}
onSelect={handleSelect}
onError={handleError}
options={["image", "video", "file"]}
apiService={apiService}
picker={picker}
pickerOptions={pickerOptions}
icons={{
image: <CameraIcon />,
video: <VideoIcon />,
default: <UploadIcon />,
}}
bottomSheetHeight={0.9}
title="Select Media"
/>
</View>
);
}
export default App;Complete Web Example
import React, { useState } from "react";
import { MediaPopup, MediaApiService } from "@tradly/asset";
function App() {
const [isOpen, setIsOpen] = useState(false);
const [selectedMedia, setSelectedMedia] = useState(null);
const apiService = new MediaApiService({
authKey: "your-tradly-auth-key",
bearerToken: "your-bearer-token",
// environment is auto-detected from process.env.ENVIRONMENT
});
const handleSelect = (mediaUrl) => {
setSelectedMedia(mediaUrl);
setIsOpen(false);
console.log("Selected:", mediaUrl);
};
return (
<div>
<button onClick={() => setIsOpen(true)}>
Open Media Gallery
</button>
<MediaPopup
isOpen={isOpen}
onClose={() => setIsOpen(false)}
onSelect={handleSelect}
options={["image", "video", "file"]}
apiService={apiService}
/>
</div>
);
}
export default App;