npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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/asset

Peer 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 CLI
    • expo-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-picker
import { 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-picker
import * 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 uri from uri or path
  • 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 StyleSheet instead of Tailwind CSS
  • Components: Uses React Native primitives (View, Text, TouchableOpacity, FlatList, Image)
  • File Upload: Requires picker prop 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:

  1. Get S3 signed URLs via API call to /v1/utils/S3signedUploadURL
  2. Upload files to S3 using the signed URLs
  3. 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:

  1. File Selection: User selects files via picker
  2. File Processing: Package converts picker results to standardized format
  3. MIME Type Detection: Automatically detects MIME type from extension or picker result
  4. S3 Signed URL: Gets signed URL from Tradly API
  5. File Upload: Fetches file from URI, converts to blob, uploads to S3
  6. 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 environment includes 'dev' → https://api.dev.tradly.app
  • Otherwise → https://api.tradly.app
  • You can override by providing apiBaseUrl explicitly

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)}

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/backdrop
  • containerClassName - Main popup container
  • headerClassName - Header container
  • titleClassName - Title text
  • closeButtonClassName - Close button
  • tabListClassName - Tab list (passed to MediaTab)
  • tabButtonClassName - Base tab button
  • tabButtonActiveClassName - Active tab button
  • tabButtonInactiveClassName - Inactive tab button
  • gridClassName - Media grid layout
  • imageItemClassName - Image item styles
  • videoItemClassName - Video item styles
  • paginationContainerClassName - 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/backdrop
  • containerStyle - Main popup container
  • headerStyle - Header container
  • titleStyle - Title text
  • closeButtonStyle - Close button
  • closeButtonTextStyle - Close button text/icon

MediaTab:

  • containerStyle - Tab container
  • tabListStyle - Tab list
  • tabButtonStyle - Base tab button
  • tabButtonActiveStyle - Active tab button
  • tabButtonInactiveStyle - Inactive tab button
  • tabButtonTextStyle - Base tab text
  • tabButtonTextActiveStyle - Active tab text
  • tabButtonTextInactiveStyle - Inactive tab text
  • tabIndicatorStyle - Tab indicator line
  • tabPanelStyle - Tab panel container

ImagesGallery/VideosGallery/FilesGallery:

  • containerStyle - Gallery container
  • gridStyle - Media grid layout
  • imageItemStyle / videoItemStyle / fileItemStyle - Media item styles
  • paginationContainerStyle - Pagination container

FileUpload:

  • containerStyle - Upload container
  • buttonStyle - Upload button
  • iconContainerStyle - Icon container (only shown if icon is provided)
  • titleStyle - Upload title text
  • loadingStyle - Loading state container
  • icon - Single icon (backward compatibility, overridden by icons prop)
  • icons - Object with { image, video, default } icon components (React Native)

Pagination:

  • containerStyle - Pagination container
  • navStyle - Navigation container
  • previousButtonStyle - Previous button
  • nextButtonStyle - Next button
  • pageButtonStyle - Page number button
  • pageButtonActiveStyle - Active page button
  • pageButtonTextStyle - Page button text
  • pageButtonTextActiveStyle - Active page text
  • ellipsisStyle - 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-dom for 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 Modal component for bottom sheet (slides up from bottom)
  • React Native StyleSheet for styling
  • Requires file picker library (configurable)
  • Uses FlatList for efficient media rendering
  • Supports both iOS and Android
  • Handles file:// and content:// 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 images
  • VideosGallery - For videos
  • FilesGallery - 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 mimeType or type from picker response
  • From File Extension: Automatically detects MIME type from file extension
  • Fallback: Defaults to application/octet-stream if 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.jsx files
  • React Native bundlers (Metro) automatically use .native.jsx files

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:

  1. Ensure picker prop is provided and returns files in correct format
  2. Check that file URIs are accessible (file:// or content://)
  3. Verify MIME type is being detected correctly (check console logs)
  4. Ensure proper permissions are granted for file access

Problem: MIME type not detected properly

Solutions:

  1. The package automatically detects MIME type from:
    • Picker result (mimeType or type property)
    • File extension (comprehensive mapping included)
  2. If detection fails, it defaults to application/octet-stream
  3. Check console logs to see detected MIME type

Problem: Bottom sheet not showing properly

Solutions:

  1. Adjust bottomSheetHeight prop (default: 0.9 = 90%)
  2. Check container padding in theme
  3. Ensure overflow: 'hidden' is applied (already included)

Grid Layout Issues

Problem: Items showing 2 per row instead of 3

Solutions:

  1. Check container padding matches between skeleton and gallery
  2. Verify ITEM_SIZE calculation accounts for all padding/margins
  3. Ensure FlatList with numColumns={3} is used (not flexWrap)

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;