@plugable-io/react
v0.0.13
Published
React components and hooks for Plugable File Management API
Maintainers
Readme
@plugable-io/react
React components and hooks for the Plugable File Management API.
Installation
npm install @plugable-io/reactComponents & Hooks
| Export | Type | Description |
|--------|------|-------------|
| PlugableProvider | Component | Context provider for configuration |
| Dropzone | Component | Drag & drop file uploader |
| FilePreview | Component | Universal file preview (images + file type icons) |
| FileImage | Component | Image display with caching |
| useFiles | Hook | File fetching with pagination |
| usePlugable | Hook | Access client and context |
Quick Start
Provider Setup
Wrap your app with PlugableProvider. You can provide a getToken function or use a supported auth provider.
With Clerk
import { PlugableProvider } from '@plugable-io/react';
function App() {
return (
<PlugableProvider
bucketId="your-bucket-id"
authProvider="clerk"
clerkJWTTemplate="plugable" // optional
>
<YourApp />
</PlugableProvider>
);
}With Supabase
<PlugableProvider
bucketId="your-bucket-id"
authProvider="supabase"
>
<YourApp />
</PlugableProvider>With Custom getToken
<PlugableProvider
bucketId="your-bucket-id"
getToken={async () => {
return await getAuthToken();
}}
>
<YourApp />
</PlugableProvider>Theme Customization
The library comes with gorgeous default styling inspired by modern design principles. You can easily customize the appearance using theme props:
Dark Mode (Default)
<PlugableProvider
bucketId="your-bucket-id"
authProvider="clerk"
theme="dark" // or omit - dark is default
>
<YourApp />
</PlugableProvider>Light Mode
<PlugableProvider
bucketId="your-bucket-id"
authProvider="clerk"
theme="light"
>
<YourApp />
</PlugableProvider>Auto (System Preference)
<PlugableProvider
bucketId="your-bucket-id"
authProvider="clerk"
theme="auto" // Follows user's system preference
>
<YourApp />
</PlugableProvider>Custom Accent Color
<PlugableProvider
bucketId="your-bucket-id"
authProvider="clerk"
accentColor="#10b981" // Green accent
>
<YourApp />
</PlugableProvider>Custom Base Color
<PlugableProvider
bucketId="your-bucket-id"
authProvider="clerk"
baseColor="#1e293b" // Custom background
theme="dark"
>
<YourApp />
</PlugableProvider>Complete Custom Theme
<PlugableProvider
bucketId="your-bucket-id"
authProvider="clerk"
theme="light"
accentColor="#0ea5e9" // Cyan accent
baseColor="#f8fafc" // Light gray background
>
<YourApp />
</PlugableProvider>CSS Variables
The library uses CSS variables for theming, which are scoped to prevent conflicts with your app's styles. You can override any variable:
.plugable-root {
/* Accent colors */
--plugable-accent-primary: #9333ea;
--plugable-accent-secondary: #2563eb;
--plugable-accent-hover: #7c3aed;
/* Base colors */
--plugable-base-bg: #0f172a;
--plugable-base-surface: rgba(30, 41, 59, 0.5);
--plugable-base-border: rgba(255, 255, 255, 0.1);
/* Text colors */
--plugable-text-primary: #f1f5f9;
--plugable-text-secondary: #cbd5e1;
--plugable-text-muted: #64748b;
/* State colors */
--plugable-success: #34d399;
--plugable-error: #f87171;
--plugable-warning: #fbbf24;
/* Effects */
--plugable-overlay: rgba(0, 0, 0, 0.3);
--plugable-backdrop-blur: blur(24px);
}Dropzone
File uploader with drag-and-drop support. Works with default UI or custom render function.
Default UI
import { Dropzone } from '@plugable-io/react';
function ProfileUploader() {
return (
<Dropzone
metadata={{ category: 'avatar', userId: '123' }}
accept="image/*"
maxFiles={1}
onUploadComplete={(files) => {
console.log('Avatar uploaded:', files[0]);
}}
onUploadError={(error) => {
console.error('Upload failed:', error);
}}
/>
);
}Custom UI with Render Props
import { Dropzone } from '@plugable-io/react';
function DocumentUploader() {
return (
<Dropzone
metadata={{ type: 'document' }}
accept=".pdf,.doc,.docx"
maxFiles={10}
onUploadComplete={(files) => console.log('Uploaded:', files)}
>
{({ isDragActive, isUploading, uploadProgress, openFileDialog, uploadedFiles }) => (
<div
onClick={openFileDialog}
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer
${isDragActive ? 'border-blue-500 bg-blue-50' : 'border-gray-300'}`}
>
{isUploading ? (
<div className="space-y-2">
<p className="font-medium">Uploading...</p>
{Object.entries(uploadProgress).map(([name, progress]) => (
<div key={name} className="text-sm">
<span>{name}</span>
<div className="w-full bg-gray-200 rounded h-2 mt-1">
<div
className="bg-blue-500 h-2 rounded transition-all"
style={{ width: `${progress}%` }}
/>
</div>
</div>
))}
</div>
) : (
<>
<p className="font-medium">
{isDragActive ? 'Drop files here' : 'Drag & drop documents'}
</p>
<p className="text-sm text-gray-500 mt-1">or click to browse</p>
</>
)}
{uploadedFiles.length > 0 && (
<div className="mt-4 text-sm text-green-600">
✓ {uploadedFiles.length} file(s) uploaded
</div>
)}
</div>
)}
</Dropzone>
);
}Props
| Prop | Type | Description |
|------|------|-------------|
| bucketId | string | Override default bucket ID |
| metadata | Record<string, any> | Metadata to attach to uploaded files |
| accept | string | File types to accept (e.g., "image/*", ".pdf,.doc") |
| maxFiles | number | Maximum number of files |
| onUploadComplete | (files: FileObject[]) => void | Called when uploads complete |
| onUploadError | (error: Error) => void | Called on upload error |
| onProgressUpdate | (fileName: string, progress: number) => void | Progress callback |
| children | (props: DropzoneRenderProps) => ReactNode | Custom render function |
| className | string | CSS class name |
| style | CSSProperties | Inline styles |
useFiles Hook
Fetch and paginate files with automatic refresh on uploads.
Basic Usage
import { useFiles, FilePreview } from '@plugable-io/react';
function InvoiceList() {
const { files, isLoading, pagination, refresh } = useFiles({
metadata: { type: 'invoice' },
perPage: 10,
});
if (isLoading) return <div>Loading...</div>;
return (
<div>
<div className="grid grid-cols-4 gap-4">
{files.map((file) => (
<div key={file.id} className="border rounded p-3">
<FilePreview file={file} width={60} height={60} />
<p className="text-sm mt-2 truncate">{file.name}</p>
<div className="flex gap-2 mt-2">
<button onClick={() => file.delete()}>Delete</button>
<a href={file.download_url} download>Download</a>
</div>
</div>
))}
</div>
<div className="flex gap-2 mt-4">
<button
onClick={pagination.loadPreviousPage}
disabled={!pagination.hasPrevious}
>
Previous
</button>
<span>Page {pagination.current}</span>
<button
onClick={pagination.loadNextPage}
disabled={!pagination.hasNext}
>
Next
</button>
</div>
</div>
);
}Filter by Media Type
function ImageGallery() {
const { files, isLoading } = useFiles({
mediaType: 'image',
perPage: 20,
});
return (
<div className="grid grid-cols-3 gap-4">
{files.map((file) => (
<FilePreview key={file.id} file={file} width="100%" height={200} objectFit="cover" />
))}
</div>
);
}Ordering Files
function SortedFileList() {
const [orderBy, setOrderBy] = useState<'created_at' | 'name' | 'byte_size'>('created_at');
const [orderDirection, setOrderDirection] = useState<'asc' | 'desc'>('desc');
const { files, isLoading } = useFiles({
orderBy,
orderDirection,
perPage: 20,
});
return (
<div>
<select value={orderBy} onChange={(e) => setOrderBy(e.target.value as any)}>
<option value="created_at">Date</option>
<option value="name">Name</option>
<option value="byte_size">Size</option>
</select>
<button onClick={() => setOrderDirection(orderDirection === 'asc' ? 'desc' : 'asc')}>
{orderDirection === 'asc' ? '↑' : '↓'}
</button>
{files.map((file) => (
<div key={file.id}>{file.name}</div>
))}
</div>
);
}Manual Loading
function LazyFileList() {
const { files, isLoading, refresh } = useFiles({
metadata: { folder: 'reports' },
autoLoad: false, // Don't load on mount
});
return (
<div>
<button onClick={refresh}>Load Files</button>
{isLoading && <span>Loading...</span>}
{files.map((file) => (
<div key={file.id}>{file.name}</div>
))}
</div>
);
}Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| metadata | Record<string, any> | - | Filter files by metadata |
| mediaType | string | - | Filter by media type (image, video, etc.) |
| perPage | number | 20 | Files per page |
| startPage | number | 1 | Initial page number |
| autoLoad | boolean | true | Fetch on mount |
| orderBy | 'created_at' \| 'name' \| 'byte_size' | 'created_at' | Field to order by |
| orderDirection | 'asc' \| 'desc' | 'desc' | Sort direction |
Return Value
| Property | Type | Description |
|----------|------|-------------|
| files | FileObject[] | Array of files |
| isLoading | boolean | Loading state |
| pagination.current | number | Current page |
| pagination.hasNext | boolean | Has next page |
| pagination.hasPrevious | boolean | Has previous page |
| pagination.loadNextPage | () => void | Go to next page |
| pagination.loadPreviousPage | () => void | Go to previous page |
| setPage | (page: number) => void | Jump to specific page |
| refresh | () => Promise<void> | Refresh current page |
FilePreview
Universal file preview component. Displays images inline and shows file type icons for other files.
Basic Usage
import { FilePreview } from '@plugable-io/react';
function FileCard({ file }) {
return (
<div className="flex items-center gap-3 p-3 border rounded">
<FilePreview file={file} width={48} height={48} />
<div>
<p className="font-medium">{file.name}</p>
<p className="text-sm text-gray-500">{file.content_type}</p>
</div>
</div>
);
}Custom Non-Image Rendering
import { FilePreview } from '@plugable-io/react';
function CustomFilePreview({ file }) {
return (
<FilePreview
file={file}
width={80}
height={80}
objectFit="cover"
renderNonImage={(file) => (
<div className="flex flex-col items-center justify-center text-gray-500">
<span className="text-2xl">📄</span>
<span className="text-xs mt-1">{file.name.split('.').pop()}</span>
</div>
)}
/>
);
}Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| file | FileObject | required | File to preview |
| width | number \| string | 80 | Preview width |
| height | number \| string | 80 | Preview height |
| objectFit | 'contain' \| 'cover' \| 'fill' \| 'none' \| 'scale-down' | 'cover' | Image fit mode |
| className | string | - | CSS class name |
| style | CSSProperties | - | Inline styles |
| showExtension | boolean | true | Show file extension for non-images |
| renderNonImage | (file: FileObject) => ReactNode | - | Custom renderer for non-image files |
FileImage
Display images with automatic URL fetching and caching.
import { FileImage } from '@plugable-io/react';
function Avatar({ file }) {
return (
<FileImage
file={file}
width={100}
height={100}
objectFit="cover"
borderRadius={50}
alt="User avatar"
onLoad={() => console.log('Loaded')}
/>
);
}File Object Methods
Each FileObject provides built-in methods for updating, downloading, and deleting files:
// Update file metadata or name
await file.update(
{ tags: ['important', 'reviewed'] }, // metadata (optional)
'new-filename.txt' // name (optional)
);
// Download the file (initiates browser download)
await file.download();
// Delete the file
await file.delete();
// Access properties
console.log(file.id); // Unique ID
console.log(file.name); // Filename
console.log(file.content_type); // MIME type
console.log(file.download_url); // Signed download URL
console.log(file.metadata); // Custom metadataFull Example
Complete file manager with upload, list, and preview:
import {
PlugableProvider,
Dropzone,
useFiles,
FilePreview
} from '@plugable-io/react';
function FileManager() {
const { files, isLoading, pagination, refresh } = useFiles({
perPage: 12,
});
return (
<div className="max-w-4xl mx-auto p-6">
<h1 className="text-2xl font-bold mb-6">File Manager</h1>
{/* Upload Section */}
<Dropzone
maxFiles={5}
onUploadComplete={(uploaded) => {
console.log('Uploaded:', uploaded);
// Files list auto-refreshes via event system
}}
className="mb-8"
/>
{/* Files Grid */}
{isLoading ? (
<div className="text-center py-8">Loading...</div>
) : (
<div className="grid grid-cols-4 gap-4">
{files.map((file) => (
<div key={file.id} className="border rounded-lg p-3">
<FilePreview
file={file}
width="100%"
height={120}
objectFit="cover"
/>
<p className="text-sm mt-2 truncate" title={file.name}>
{file.name}
</p>
<div className="flex gap-2 mt-2">
<a
href={file.download_url}
download
className="text-blue-500 text-sm"
>
Download
</a>
<button
onClick={() => file.delete()}
className="text-red-500 text-sm"
>
Delete
</button>
</div>
</div>
))}
</div>
)}
{/* Pagination */}
<div className="flex justify-center gap-4 mt-6">
<button
onClick={pagination.loadPreviousPage}
disabled={!pagination.hasPrevious}
className="px-4 py-2 border rounded disabled:opacity-50"
>
Previous
</button>
<span className="py-2">Page {pagination.current}</span>
<button
onClick={pagination.loadNextPage}
disabled={!pagination.hasNext}
className="px-4 py-2 border rounded disabled:opacity-50"
>
Next
</button>
</div>
</div>
);
}
// App wrapper
function App() {
return (
<PlugableProvider
bucketId="your-bucket-id"
authProvider="clerk"
>
<FileManager />
</PlugableProvider>
);
}TypeScript
All types are exported:
import type {
PlugableProviderProps,
AuthProvider,
DropzoneProps,
DropzoneRenderProps,
FilePreviewProps,
FileImageProps,
FileListProps,
FileListRenderProps,
UseFilesOptions,
UseFilesResult,
FileObject,
SearchOptions,
UpdateOptions,
} from '@plugable-io/react';License
MIT
