dudu-file-upload-components
Version:
A highly customizable, config-driven file upload component library built with React, TailwindCSS, and class-variance-authority
831 lines (649 loc) • 23.2 kB
Markdown
# Dudu File Upload Components
A highly customizable, config-driven file upload component library built with React, TailwindCSS, and class-variance-authority. Inspired by shadcn/ui's API patterns with support for multiple variants, themes, and JSON-based configuration.
## ✨ Features
- 🎨 **Multiple Variants**: Button, dropzone, preview, and compact layouts
- 🎯 **Config-Driven**: JSON-based configuration for easy customization
- 🎭 **Theme Support**: Built-in themes with TailwindCSS integration
- ♿ **Accessible**: Full keyboard navigation and screen reader support
- 📱 **Responsive**: Works seamlessly across all device sizes
- 🔧 **TypeScript**: Full type safety and IntelliSense support
- 🎪 **Drag & Drop**: Native drag and drop with keyboard fallback
- 🖼️ **Preview Support**: Image previews and file type icons
- 📏 **Validation**: File size, type, and count validation
- 🎛️ **Flexible API**: Use as components or config-driven renderers
## 📦 Installation
### 1. Install the Package
```bash
npm install dudu-file-upload-components
# or
yarn add dudu-file-upload-components
# or
pnpm add dudu-file-upload-components
```
### 2. Install Required Peer Dependencies
**Important**: You must install the required peer dependencies manually to avoid version conflicts and reduce bundle size.
```bash
npm install react react-dom lucide-react class-variance-authority clsx -ui/react-slot
```
### 3. Import Styles
**Important**: Import the CSS file in your main application file (e.g., `main.tsx`, `App.tsx`, or `_app.tsx`):
```tsx
import "dudu-file-upload-components/dist/style.css";
```
This CSS file contains all the necessary styles including:
- CSS variables for theming
- All required TailwindCSS utility classes
- Component-specific styles
- **Works standalone** - No TailwindCSS required in your project!
### 4. Configure TailwindCSS (Optional)
If you're using TailwindCSS in your project, you can optionally add the component paths to your `tailwind.config.js` for better integration:
```js
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx}",
"./node_modules/dudu-file-upload-components/**/*.{js,ts,jsx,tsx}",
],
// ... rest of your config
};
```
**Note**: This step is optional. The components work perfectly without TailwindCSS in your project.
### Why Install Peer Dependencies Manually?
1. **Version Flexibility**: Use your preferred versions of React and other dependencies
2. **Bundle Size**: Prevents duplicate dependencies in your project
3. **Compatibility**: Ensures the component works with your existing React setup
4. **Control**: You maintain control over which versions are used in your project
## 🚀 Quick Start
### Basic Usage
```tsx
import { FileUpload } from "dudu-file-upload-components";
import "dudu-file-upload-components/dist/style.css";
function App() {
const handleFilesChange = (files) => {
console.log("Files:", files);
};
return <FileUpload variant="dropzone" onFilesChange={handleFilesChange} />;
}
```
### Using Presets
```tsx
import { FileUploadPresetRenderer } from "dudu-file-upload-components";
function ImageUpload() {
return (
<FileUploadPresetRenderer
preset="image-upload"
onFilesChange={(files) => console.log(files)}
/>
);
}
```
### Config-Driven Approach
```tsx
import { FileUploadRenderer } from "dudu-file-upload-components";
const config = {
variant: "dropzone",
multiple: true,
maxFiles: 5,
accept: "image/*",
labels: {
dropzone: "Drop your images here",
},
};
function ConfigDrivenUpload() {
return (
<FileUploadRenderer
config={config}
onFilesChange={(files) => console.log(files)}
/>
);
}
```
## Example Usage Code
### Complete Component Example
```tsx
import React, { useState } from "react";
import { FileUpload, type FileWithPreview } from "dudu-file-upload-components";
function MyFileUploader() {
const [files, setFiles] = useState<FileWithPreview[]>([]);
const [errors, setErrors] = useState<string[]>([]);
const handleFilesChange = (newFiles: FileWithPreview[]) => {
setFiles(newFiles);
console.log("Files updated:", newFiles);
};
const handleError = (errorMessages: string[]) => {
setErrors(errorMessages);
console.error("Upload errors:", errorMessages);
};
return (
<div className="max-w-md mx-auto p-6">
<h2 className="text-xl font-semibold mb-4">Upload Your Files</h2>
<FileUpload
variant="dropzone"
size="md"
multiple={true}
maxFiles={5}
maxSize={10 * 1024 * 1024} // 10MB
accept="image/*,application/pdf"
onFilesChange={handleFilesChange}
onError={handleError}
labels={{
dropzone: "Drop your files here or click to browse",
dragActive: "Release to upload files",
}}
/>
{errors.length > 0 && (
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded">
<h3 className="text-red-800 font-medium">Upload Errors:</h3>
<ul className="text-red-700 text-sm mt-1">
{errors.map((error, index) => (
<li key={index}>• {error}</li>
))}
</ul>
</div>
)}
{files.length > 0 && (
<div className="mt-4">
<h3 className="font-medium mb-2">Uploaded Files:</h3>
<ul className="space-y-1">
{files.map((file) => (
<li key={file.id} className="text-sm text-gray-600">
{file.file.name} ({Math.round(file.file.size / 1024)}KB)
</li>
))}
</ul>
</div>
)}
</div>
);
}
export default MyFileUploader;
```
### Using with Form Libraries
```tsx
import { useForm, Controller } from "react-hook-form";
import { FileUpload } from "dudu-file-upload-components";
function FormWithFileUpload() {
const { control, handleSubmit } = useForm();
const onSubmit = (data) => {
console.log("Form data:", data);
// Handle form submission with files
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="files"
control={control}
render={({ field }) => (
<FileUpload
variant="dropzone"
multiple
onFilesChange={field.onChange}
/>
)}
/>
<button type="submit">Submit</button>
</form>
);
}
```
### Custom Hook Usage
```tsx
import { useFileUpload } from "dudu-file-upload-components";
function CustomFileUploader() {
const [state, actions] = useFileUpload({
multiple: true,
maxSize: 5 * 1024 * 1024, // 5MB
accept: "image/*",
});
return (
<div>
<input {...actions.getInputProps()} />
<button onClick={actions.openFileDialog}>
Choose Files ({state.files.length} selected)
</button>
{state.files.map((file) => (
<div key={file.id}>
<span>{file.file.name}</span>
<button onClick={() => actions.removeFile(file.id)}>Remove</button>
</div>
))}
{state.errors.map((error, index) => (
<div key={index} className="text-red-500">
{error}
</div>
))}
</div>
);
}
```
## API Reference
### FileUpload Component
| Prop | Type | Default | Description |
| --------------- | -------------------------------------------------- | ------------ | ----------------------------- |
| `variant` | `"button" \| "dropzone" \| "preview" \| "compact"` | `"dropzone"` | Upload component variant |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Component size |
| `radius` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Border radius |
| `multiple` | `boolean` | `false` | Allow multiple file selection |
| `maxFiles` | `number` | `1` | Maximum number of files |
| `maxSize` | `number` | `10MB` | Maximum file size in bytes |
| `accept` | `string` | `"*"` | Accepted file types |
| `disabled` | `boolean` | `false` | Disable the component |
| `showPreview` | `boolean` | `true` | Show file previews |
| `allowRemove` | `boolean` | `true` | Allow file removal |
| `onFilesChange` | `(files: FileWithPreview[]) => void` | - | Files change callback |
| `onFilesAdded` | `(files: FileWithPreview[]) => void` | - | Files added callback |
| `onError` | `(errors: string[]) => void` | - | Error callback |
### Available Presets
- `default` - Basic dropzone upload
- `image-upload` - Optimized for images with preview
- `document-upload` - For documents with file list
- `multi-file` - Multiple file upload with previews
- `compact` - Minimal button-style upload
## 📋 JSON Configuration Documentation
The component supports JSON-based configuration for dynamic behavior. This makes it easy to configure file upload components through external configuration files, APIs, or content management systems.
### Complete Configuration Schema
Here's the complete configuration schema with all available options:
```json
{
"variant": "dropzone",
"size": "md",
"radius": "lg",
"multiple": true,
"maxFiles": 10,
"maxSize": 52428800,
"accept": "image/*,application/pdf",
"disabled": false,
"showPreview": true,
"showProgress": false,
"allowRemove": true,
"labels": {
"dropzone": "Drop files here or click to browse",
"button": "Choose Files",
"dragActive": "Drop files here",
"maxFiles": "Maximum {count} files allowed",
"maxSize": "File size must be less than {size}",
"fileType": "File type not supported",
"remove": "Remove file",
"clear": "Clear all files"
},
"icons": {
"upload": "upload",
"file": "file",
"image": "image",
"remove": "x",
"error": "alert-circle"
}
}
```
### Configuration Properties
| Property | Type | Default | Description |
| -------------- | -------------------------------------------------- | ------------ | ----------------------------------------------- |
| `variant` | `"button" \| "dropzone" \| "preview" \| "compact"` | `"dropzone"` | Upload component variant |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Component size |
| `radius` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Border radius |
| `multiple` | `boolean` | `false` | Allow multiple file selection |
| `maxFiles` | `number` | `1` | Maximum number of files (when multiple is true) |
| `maxSize` | `number` | `10485760` | Maximum file size in bytes (default: 10MB) |
| `accept` | `string` | `"*"` | Accepted file types (MIME types or extensions) |
| `disabled` | `boolean` | `false` | Disable the component |
| `showPreview` | `boolean` | `true` | Show file previews |
| `showProgress` | `boolean` | `false` | Show upload progress bars |
| `allowRemove` | `boolean` | `true` | Allow file removal |
### Labels Configuration
All labels support placeholder replacement:
| Label | Placeholders | Description |
| ------------ | ------------ | -------------------------------------------- |
| `dropzone` | - | Text shown in dropzone area |
| `button` | - | Text shown on upload button |
| `dragActive` | - | Text shown when dragging files over dropzone |
| `maxFiles` | `{count}` | Error message for too many files |
| `maxSize` | `{size}` | Error message for file too large |
| `fileType` | - | Error message for invalid file type |
| `remove` | - | Tooltip text for remove button |
| `clear` | - | Text for clear all button |
### 📝 Example Configurations
#### Image Gallery Upload
```json
{
"variant": "dropzone",
"size": "lg",
"multiple": true,
"maxFiles": 20,
"maxSize": 5242880,
"accept": "image/*",
"showPreview": true,
"allowRemove": true,
"labels": {
"dropzone": "Drop your photos here or click to browse",
"dragActive": "Release to add photos",
"maxFiles": "You can upload up to {count} photos",
"maxSize": "Each photo must be smaller than {size}"
}
}
```
#### Document Upload
```json
{
"variant": "button",
"size": "md",
"multiple": true,
"maxFiles": 10,
"maxSize": 26214400,
"accept": ".pdf,.doc,.docx,.txt",
"showPreview": false,
"labels": {
"button": "Select Documents",
"maxFiles": "Maximum {count} documents allowed",
"fileType": "Only PDF, Word, and text files are supported"
}
}
```
#### Single Avatar Upload
```json
{
"variant": "preview",
"size": "sm",
"multiple": false,
"maxFiles": 1,
"maxSize": 2097152,
"accept": "image/jpeg,image/png,image/webp",
"showPreview": true,
"allowRemove": true,
"labels": {
"dropzone": "Upload your avatar",
"maxSize": "Image must be smaller than {size}",
"fileType": "Only JPEG, PNG, and WebP images are supported"
}
}
```
#### Compact File Upload
```json
{
"variant": "compact",
"size": "sm",
"multiple": true,
"maxFiles": 5,
"maxSize": 10485760,
"accept": "*",
"showPreview": false,
"labels": {
"button": "Attach Files",
"maxFiles": "Up to {count} files",
"maxSize": "Max {size} per file"
}
}
```
### Using JSON Configuration
```tsx
import { FileUploadJSONRenderer } from "dudu-file-upload-components";
// Load configuration from API, file, or CMS
const config = await fetch("/api/upload-config").then((res) => res.json());
function DynamicFileUpload() {
return (
<FileUploadJSONRenderer
configJSON={JSON.stringify(config)}
onFilesChange={(files) => console.log(files)}
onConfigError={(error) => console.error("Config error:", error)}
fallbackPreset="default"
/>
);
}
```
## Advanced Usage
### Custom Hook
```tsx
import { useFileUpload } from "dudu-file-upload-components";
function CustomUpload() {
const [state, actions] = useFileUpload({
multiple: true,
maxSize: 5 * 1024 * 1024, // 5MB
accept: "image/*",
});
return (
<div>
<input {...actions.getInputProps()} />
<button onClick={actions.openFileDialog}>Upload Files</button>
{state.files.map((file) => (
<div key={file.id}>
{file.file.name}
<button onClick={() => actions.removeFile(file.id)}>Remove</button>
</div>
))}
</div>
);
}
```
### Live Configuration
```tsx
import {
useFileUploadConfig,
FileUploadRenderer,
} from "dudu-file-upload-components";
function LiveConfigExample() {
const { config, updateConfig, exportConfig } = useFileUploadConfig();
return (
<div>
<button onClick={() => updateConfig({ multiple: !config.multiple })}>
Toggle Multiple
</button>
<FileUploadRenderer config={config} />
<pre>{exportConfig()}</pre>
</div>
);
}
```
## Styling
The components use TailwindCSS classes and CSS custom properties. You can customize the appearance by:
1. **Using the built-in variants and sizes**
2. **Overriding CSS custom properties**
3. **Passing custom className props**
4. **Using the configuration system**
## TypeScript Support
Full TypeScript support with comprehensive type definitions:
```tsx
import type {
FileUploadConfig,
FileWithPreview,
FileUploadPreset,
} from "dudu-file-upload-components";
```
## Publishing to NPM
### Building the Package
Before publishing, build the package for distribution:
```bash
# Build the library for distribution
npm run build:lib
# Build the demo site (for development/testing)
npm run build
# Run the development server
npm run dev
```
The build process:
1. **TypeScript Compilation**: Compiles TypeScript to JavaScript with type definitions
2. **Bundling**: Uses Rollup to create optimized bundles for both CommonJS and ES modules
3. **CSS Processing**: Processes and optimizes CSS/TailwindCSS styles
4. **Tree Shaking**: Removes unused code for smaller bundle sizes
### Publishing to NPM
1. **Prepare for Publishing**
```bash
# Update version in package.json
npm version patch # or minor/major
# Build the package
npm run build:lib
```
2. **Publish to NPM**
```bash
# Login to NPM (first time only)
npm login
# Publish the package
npm publish
# For scoped packages (like dudu-file-upload-components)
npm publish --access public
```
3. **Verify Publication**
```bash
# Check if package is published
npm view dudu-file-upload-components
```
### Using in Your Project
Once published to NPM, you can use the package in any React project:
#### 1. Install the Package
```bash
npm install dudu-file-upload-components
```
#### 2. Install Required Peer Dependencies
```bash
npm install react react-dom lucide-react class-variance-authority clsx -ui/react-slot
```
#### 3. Configure TailwindCSS
Add the package path to your `tailwind.config.js`:
```js
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx}",
"./node_modules/dudu-file-upload-components/**/*.{js,ts,jsx,tsx}",
],
// ... rest of your config
};
```
#### 4. Use in Your Components
```tsx
import { FileUpload } from "dudu-file-upload-components";
function MyApp() {
return (
<FileUpload
variant="dropzone"
multiple
onFilesChange={(files) => console.log(files)}
/>
);
}
```
### Package Distribution
The package includes:
- **ES Modules**: `dist/index.esm.js`
- **CommonJS**: `dist/index.js`
- **TypeScript Definitions**: `dist/index.d.ts`
- **Styles**: `dist/styles.css` (if needed)
### Local Development
For local development and testing:
```bash
# Pack the package locally
npm pack
# Install in another project
cd /path/to/your-project
npm install /path/to/dudu-file-upload-components-0.1.0.tgz
```
## Testing
The component library includes comprehensive testing to ensure reliability and functionality.
### Running Tests
```bash
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverage
```
### Test Structure
The test suite covers:
- **Unit Tests**: Individual component functionality
- **Integration Tests**: Component interactions and workflows
- **Accessibility Tests**: Screen reader and keyboard navigation
- **Visual Regression Tests**: UI consistency across changes
### Writing Tests
When contributing, please include tests for:
```tsx
// Example test structure
import { render, screen, fireEvent } from "@testing-library/react";
import { FileUpload } from "dudu-file-upload-components";
describe("FileUpload", () => {
it("should handle file selection", () => {
const handleFilesChange = jest.fn();
render(<FileUpload onFilesChange={handleFilesChange} />);
const input = screen.getByRole("button");
// Test file upload functionality
});
});
```
## Contributing
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## License
MIT © [dudu](https://github.com/dudu)
## Demo
Visit the demo at `/demo` route when running the development server to see all variants and configuration options in action.
```bash
npm run dev
# Open http://localhost:3000/demo
```
## 🔧 Troubleshooting
### Common Issues
#### Styles Not Applied
1. **Import the CSS file** in your main application file (this is required):
```tsx
import "dudu-file-upload-components/dist/style.css";
```
2. **If using TailwindCSS**, add the package path to your `tailwind.config.js`:
```js
module.exports = {
content: [
"./src/**/*.{js,ts,jsx,tsx}",
"./node_modules/dudu-file-upload-components/**/*.{js,ts,jsx,tsx}",
],
// ... rest of config
};
```
**Note**: The CSS file contains all necessary styles and will work even without TailwindCSS in your project.
#### TypeScript Errors
Ensure you have the correct type definitions installed:
```bash
npm install /react @types/react-dom
```
#### Peer Dependency Warnings
Install all required peer dependencies:
```bash
npm install react react-dom lucide-react class-variance-authority clsx -ui/react-slot
```
#### Component Not Rendering
1. Check that you've imported the CSS file
2. Verify all peer dependencies are installed
3. Ensure your React version is 16.8 or higher
4. Check the browser console for any error messages
### Best Practices
1. **File Validation**: Always validate files on both client and server side
2. **Error Handling**: Implement proper error handling for upload failures
3. **Progress Tracking**: Use the built-in progress features for better UX
4. **Accessibility**: Test with screen readers and keyboard navigation
5. **Performance**: Consider lazy loading for large file previews
### Browser Support
- **Modern Browsers**: Chrome 90+, Firefox 88+, Safari 14+, Edge 90+
- **File API**: Required for drag & drop functionality
- **ES2017**: Minimum JavaScript support level
## ✅ Testing Your Installation
After installing the package, you can test it with this simple component:
```tsx
import React from "react";
import { FileUpload } from "dudu-file-upload-components";
import "dudu-file-upload-components/dist/style.css";
function TestFileUpload() {
const handleFilesChange = (files) => {
console.log("Files selected:", files);
};
return (
<div className="p-8">
<h2 className="text-xl font-semibold mb-4">Test File Upload</h2>
<FileUpload
variant="dropzone"
multiple={true}
maxFiles={3}
onFilesChange={handleFilesChange}
/>
</div>
);
}
export default TestFileUpload;
```
If the component renders correctly and you can select files, your installation is working properly!