npm package: @samithahansaka/dropup
Description: Lightweight, headless React file upload library with progress tracking, chunked uploads, and cloud storage support
Author: Samitha Hansaka
Latest version: 1.0.0

# dropup

[![npm version](https://img.shields.io/npm/v/@samithahansaka/dropup.svg)](https://www.npmjs.com/package/@samithahansaka/dropup)
[![npm downloads](https://img.shields.io/npm/dm/@samithahansaka/dropup.svg)](https://www.npmjs.com/package/@samithahansaka/dropup)
[![bundle size](https://img.shields.io/bundlephobia/minzip/@samithahansaka/dropup)](https://bundlephobia.com/package/@samithahansaka/dropup)
[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

A lightweight, headless React file upload library with progress tracking, chunked uploads, and cloud storage support.

**[Documentation](https://samithahansaka.github.io/dropup/)** | **[Live Demo](https://codesandbox.io/p/sandbox/2q74hf)**

## Features

- **Headless** - Full control over UI with hooks-based API
- **Drag & Drop** - Built-in drag-and-drop support with `getDropProps()`
- **Progress Tracking** - Real-time upload progress for each file
- **Chunked Uploads** - Split large files into chunks for reliable uploads
- **Resumable Uploads** - tus protocol support for resumable uploads
- **Cloud Storage** - Pre-built helpers for S3, GCS, and Azure Blob
- **Image Processing** - Built-in compression and preview generation
- **Cross-Platform** - Works with React DOM and React Native
- **SSR Safe** - Compatible with Next.js and other SSR frameworks
- **TypeScript** - Full TypeScript support with comprehensive types
- **Tiny Bundle** - Core is ~10KB gzipped, tree-shakeable

## Installation

```bash
npm install @samithahansaka/dropup
# or
yarn add @samithahansaka/dropup
# or
pnpm add @samithahansaka/dropup
```

## Quick Start

```tsx
import { useDropup } from '@samithahansaka/dropup'

function FileUploader() {
  const { files, actions, state, getDropProps, getInputProps } = useDropup({
    accept: 'image/*',
    maxSize: 10 * 1024 * 1024, // 10MB
    multiple: true,
    upload: {
      url: '/api/upload',
    },
  })

  return (
    <div>
      <div
        {...getDropProps()}
        style={{
          border: state.isDragActive ? '2px dashed blue' : '2px dashed gray',
          padding: 40,
          textAlign: 'center',
        }}
      >
        <input {...getInputProps()} />
        {state.isDragActive ? (
          <p>Drop files here...</p>
        ) : (
          <p>Drag & drop files here, or click to select</p>
        )}
      </div>

      {files.map((file) => (
        <div key={file.id}>
          <span>{file.name}</span>
          <span>{file.progress}%</span>
          <span>{file.status}</span>
          <button onClick={() => actions.remove(file.id)}>Remove</button>
        </div>
      ))}

      <button onClick={() => actions.upload()} disabled={state.isUploading}>
        Upload All
      </button>
    </div>
  )
}
```

## API Reference

### `useDropup(options)`

The main hook for file upload functionality.

#### Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `accept` | `string \| string[]` | - | Accepted file types (e.g., `'image/*'`, `['.pdf', '.doc']`) |
| `maxSize` | `number` | - | Maximum file size in bytes |
| `minSize` | `number` | - | Minimum file size in bytes |
| `maxFiles` | `number` | - | Maximum number of files |
| `multiple` | `boolean` | `false` | Allow multiple file selection |
| `disabled` | `boolean` | `false` | Disable the dropzone |
| `autoUpload` | `boolean` | `false` | Automatically upload files when added |
| `upload` | `UploadOptions` | - | Upload configuration |
| `onFilesAdded` | `(files: DropupFile[]) => void` | - | Callback when files are added |
| `onUploadComplete` | `(file: DropupFile) => void` | - | Callback when a file upload completes |
| `onUploadError` | `(file: DropupFile, error: Error) => void` | - | Callback when a file upload fails |
| `onValidationError` | `(errors: ValidationError[]) => void` | - | Callback for validation errors |

#### Upload Options

```typescript
interface UploadOptions {
  url: string                    // Upload endpoint
  method?: 'POST' | 'PUT'        // HTTP method (default: POST)
  headers?: Record<string, string> | (() => Promise<Record<string, string>>)
  fieldName?: string             // Form field name (default: 'file')
  formData?: Record<string, string | Blob>  // Additional form data
  withCredentials?: boolean      // Include cookies
  timeout?: number               // Request timeout in ms
}
```

#### Return Value

```typescript
interface UseDropupReturn {
  files: DropupFile[]           // Array of files
  actions: DropupActions        // Action methods
  state: ComputedState          // Computed state
  getDropProps: () => DropZoneProps    // Props for drop zone element
  getInputProps: () => InputProps      // Props for hidden input
  openFileDialog: () => void    // Programmatically open file dialog
}
```

#### Actions

| Action | Description |
|--------|-------------|
| `upload()` | Upload all pending files |
| `uploadFile(id)` | Upload a specific file |
| `cancel()` | Cancel all uploads |
| `cancelFile(id)` | Cancel a specific file upload |
| `remove(id)` | Remove a file from the list |
| `reset()` | Reset all state |
| `retry(id)` | Retry a failed upload |
| `retryAll()` | Retry all failed uploads |

#### State

| Property | Type | Description |
|----------|------|-------------|
| `isUploading` | `boolean` | Whether any file is uploading |
| `isDragActive` | `boolean` | Whether files are being dragged over |
| `isDragAccept` | `boolean` | Whether dragged files are accepted |
| `isDragReject` | `boolean` | Whether dragged files are rejected |
| `progress` | `number` | Overall upload progress (0-100) |
| `status` | `'idle' \| 'uploading' \| 'complete' \| 'error'` | Overall status |
| `error` | `DropupError \| null` | Last error |
| `counts` | `StatusCounts` | File counts by status |

## Advanced Usage

### Chunked Uploads

For large files, use chunked uploads:

```tsx
import { useDropup, createChunkedUploader } from '@samithahansaka/dropup'

const chunkedUploader = createChunkedUploader({
  url: '/api/upload',
  chunkSize: 5 * 1024 * 1024, // 5MB chunks
  parallelChunks: 3,
})

const { files, actions } = useDropup({
  upload: chunkedUploader,
})
```

### tus Protocol (Resumable Uploads)

For resumable uploads using the [tus protocol](https://tus.io/):

```bash
npm install tus-js-client
```

```tsx
import { useDropup } from '@samithahansaka/dropup'
import { useTusUploader } from '@samithahansaka/dropup/tus'

const tusUploader = useTusUploader({
  endpoint: 'https://tusd.example.com/files/',
  chunkSize: 5 * 1024 * 1024,
})

const { files, actions } = useDropup({
  upload: tusUploader,
})
```

### Cloud Storage (S3, GCS, Azure)

#### AWS S3

```tsx
import { useDropup } from '@samithahansaka/dropup'
import { createS3Uploader } from '@samithahansaka/dropup/cloud/s3'

const s3Uploader = createS3Uploader({
  getPresignedUrl: async (file) => {
    const response = await fetch('/api/s3-presign', {
      method: 'POST',
      body: JSON.stringify({ filename: file.name, contentType: file.type }),
    })
    return response.json()
  },
})

const { files, actions } = useDropup({
  upload: s3Uploader,
})
```

#### Google Cloud Storage

```tsx
import { createGCSUploader } from '@samithahansaka/dropup/cloud/gcs'

const gcsUploader = createGCSUploader({
  getPresignedUrl: async (file) => {
    const response = await fetch('/api/gcs-presign', {
      method: 'POST',
      body: JSON.stringify({ filename: file.name }),
    })
    return response.json()
  },
})
```

#### Azure Blob Storage

```tsx
import { createAzureUploader } from '@samithahansaka/dropup/cloud/azure'

const azureUploader = createAzureUploader({
  getPresignedUrl: async (file) => {
    const response = await fetch('/api/azure-presign', {
      method: 'POST',
      body: JSON.stringify({ filename: file.name }),
    })
    return response.json()
  },
})
```

### Image Processing

```tsx
import { useDropup } from '@samithahansaka/dropup'
import { compressImage, generatePreview, fixImageOrientation } from '@samithahansaka/dropup/image'

const { files } = useDropup({
  accept: 'image/*',
  onFilesAdded: async (newFiles) => {
    for (const file of newFiles) {
      // Generate preview
      const preview = await generatePreview(file.file, { maxWidth: 200 })

      // Compress before upload
      const compressed = await compressImage(file.file, {
        maxWidth: 1920,
        maxHeight: 1080,
        quality: 0.8,
      })
    }
  },
})
```

### Custom Validation

```tsx
import { useDropup, commonRules } from '@samithahansaka/dropup'

const { files } = useDropup({
  accept: 'image/*',
  maxSize: 5 * 1024 * 1024,
  customRules: [
    commonRules.safeFileName,
    commonRules.extensionMatchesMime,
    {
      name: 'minDimensions',
      validate: async (file) => {
        // Custom async validation
        const img = await createImageBitmap(file)
        if (img.width < 100 || img.height < 100) {
          return 'Image must be at least 100x100 pixels'
        }
        return true
      },
    },
  ],
})
```

### React Native

```tsx
import { useDropup } from '@samithahansaka/dropup/native'
import * as ImagePicker from 'expo-image-picker'

function NativeUploader() {
  const { files, actions } = useDropup({
    upload: { url: 'https://api.example.com/upload' },
  })

  const pickImage = async () => {
    const result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: ImagePicker.MediaTypeOptions.Images,
    })

    if (!result.canceled) {
      actions.addFiles(result.assets.map(asset => ({
        uri: asset.uri,
        name: asset.fileName || 'image.jpg',
        type: asset.mimeType || 'image/jpeg',
        size: asset.fileSize || 0,
      })))
    }
  }

  return (
    <View>
      <Button title="Pick Image" onPress={pickImage} />
      {files.map(file => (
        <Text key={file.id}>{file.name} - {file.progress}%</Text>
      ))}
    </View>
  )
}
```

## File Object

```typescript
interface DropupFile {
  id: string                    // Unique identifier
  name: string                  // File name
  size: number                  // File size in bytes
  type: string                  // MIME type
  status: FileStatus            // 'idle' | 'pending' | 'uploading' | 'paused' | 'complete' | 'error'
  progress: number              // Upload progress (0-100)
  file: File                    // Original File object
  preview?: string              // Preview URL (for images)
  uploadedUrl?: string          // URL after upload
  error?: DropupError           // Error if failed
  meta?: Record<string, unknown> // Custom metadata
}
```

## Bundle Size

| Entry Point | Size (gzipped) |
|-------------|----------------|
| `dropup` | ~10KB |
| `dropup/tus` | ~1.5KB |
| `dropup/image` | ~5KB |
| `dropup/cloud/s3` | ~700B |
| `dropup/cloud/gcs` | ~650B |
| `dropup/cloud/azure` | ~700B |

## Browser Support

- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
- React Native (iOS & Android)

## TypeScript

dropup is written in TypeScript and provides comprehensive type definitions:

```typescript
import type {
  DropupFile,
  UseDropupOptions,
  UseDropupReturn,
  UploadOptions,
  ValidationError,
  DropupError,
} from '@samithahansaka/dropup'
```

## Contributing

Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details.

## License

[MIT](LICENSE) © Samitha Hansaka

## Related

- [tus-js-client](https://github.com/tus/tus-js-client) - tus resumable upload protocol
- [react-dropzone](https://github.com/react-dropzone/react-dropzone) - Simple drag-and-drop