@djangocfg/ui-tools 2.1.160 → 2.1.162
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +12 -6
- package/src/tools/Uploader/README.md +223 -0
- package/src/tools/Uploader/Uploader.story.tsx +300 -0
- package/src/tools/Uploader/components/UploadAddButton.tsx +92 -0
- package/src/tools/Uploader/components/UploadDropzone.tsx +145 -0
- package/src/tools/Uploader/components/UploadPageDropOverlay.tsx +130 -0
- package/src/tools/Uploader/components/UploadPreviewItem.tsx +182 -0
- package/src/tools/Uploader/components/UploadPreviewList.tsx +204 -0
- package/src/tools/Uploader/components/Uploader.tsx +74 -0
- package/src/tools/Uploader/components/index.ts +6 -0
- package/src/tools/Uploader/context/UploadProvider.tsx +42 -0
- package/src/tools/Uploader/context/index.ts +1 -0
- package/src/tools/Uploader/hooks/index.ts +2 -0
- package/src/tools/Uploader/hooks/useUploadEvents.ts +56 -0
- package/src/tools/Uploader/hooks/useUploadProvider.ts +8 -0
- package/src/tools/Uploader/index.ts +37 -0
- package/src/tools/Uploader/types/index.ts +100 -0
- package/src/tools/Uploader/utils/assetTypes.ts +58 -0
- package/src/tools/Uploader/utils/formatters.ts +16 -0
- package/src/tools/Uploader/utils/index.ts +17 -0
- package/src/tools/Uploader/utils/logger.ts +3 -0
- package/src/tools/Uploader/utils/transformers.ts +114 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-tools",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.162",
|
|
4
4
|
"description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-tools",
|
|
@@ -52,6 +52,11 @@
|
|
|
52
52
|
"import": "./src/tools/Mermaid/index.tsx",
|
|
53
53
|
"require": "./src/tools/Mermaid/index.tsx"
|
|
54
54
|
},
|
|
55
|
+
"./upload": {
|
|
56
|
+
"types": "./src/tools/Uploader/index.ts",
|
|
57
|
+
"import": "./src/tools/Uploader/index.ts",
|
|
58
|
+
"require": "./src/tools/Uploader/index.ts"
|
|
59
|
+
},
|
|
55
60
|
"./styles": "./src/styles/index.css"
|
|
56
61
|
},
|
|
57
62
|
"files": [
|
|
@@ -68,8 +73,8 @@
|
|
|
68
73
|
"check": "tsc --noEmit"
|
|
69
74
|
},
|
|
70
75
|
"peerDependencies": {
|
|
71
|
-
"@djangocfg/i18n": "^2.1.
|
|
72
|
-
"@djangocfg/ui-core": "^2.1.
|
|
76
|
+
"@djangocfg/i18n": "^2.1.162",
|
|
77
|
+
"@djangocfg/ui-core": "^2.1.162",
|
|
73
78
|
"lucide-react": "^0.545.0",
|
|
74
79
|
"react": "^19.0.0",
|
|
75
80
|
"react-dom": "^19.0.0",
|
|
@@ -78,6 +83,7 @@
|
|
|
78
83
|
"consola": "^3.4.2"
|
|
79
84
|
},
|
|
80
85
|
"dependencies": {
|
|
86
|
+
"@rpldy/uploady": "^1.8.5",
|
|
81
87
|
"@rjsf/core": "^6.1.2",
|
|
82
88
|
"@rjsf/utils": "^6.1.2",
|
|
83
89
|
"@rjsf/validator-ajv8": "^6.1.2",
|
|
@@ -101,10 +107,10 @@
|
|
|
101
107
|
"@maplibre/maplibre-gl-geocoder": "^1.7.0"
|
|
102
108
|
},
|
|
103
109
|
"devDependencies": {
|
|
104
|
-
"@djangocfg/i18n": "^2.1.
|
|
110
|
+
"@djangocfg/i18n": "^2.1.162",
|
|
105
111
|
"@djangocfg/playground": "workspace:*",
|
|
106
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
107
|
-
"@djangocfg/ui-core": "^2.1.
|
|
112
|
+
"@djangocfg/typescript-config": "^2.1.162",
|
|
113
|
+
"@djangocfg/ui-core": "^2.1.162",
|
|
108
114
|
"@types/mapbox__mapbox-gl-draw": "^1.4.8",
|
|
109
115
|
"@types/node": "^24.7.2",
|
|
110
116
|
"@types/react": "^19.1.0",
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# Uploader
|
|
2
|
+
|
|
3
|
+
Drag-drop file uploader built on `@rpldy/uploady` with shadcn/ui styling.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @djangocfg/ui-tools
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Basic Usage
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { Uploader } from '@djangocfg/ui-tools/upload';
|
|
15
|
+
|
|
16
|
+
function MyPage() {
|
|
17
|
+
return (
|
|
18
|
+
<Uploader
|
|
19
|
+
destination="/api/upload"
|
|
20
|
+
accept={['image', 'document']}
|
|
21
|
+
maxSizeMB={10}
|
|
22
|
+
onUploadComplete={(asset) => console.log('Uploaded:', asset)}
|
|
23
|
+
/>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Components
|
|
29
|
+
|
|
30
|
+
### Uploader
|
|
31
|
+
|
|
32
|
+
All-in-one component with dropzone and preview list.
|
|
33
|
+
|
|
34
|
+
```tsx
|
|
35
|
+
<Uploader
|
|
36
|
+
destination="/api/upload" // Upload URL
|
|
37
|
+
accept={['image', 'video']} // Asset types: image, audio, video, document
|
|
38
|
+
maxSizeMB={50} // Max file size
|
|
39
|
+
multiple={true} // Allow multiple files
|
|
40
|
+
autoUpload={true} // Auto-upload on drop
|
|
41
|
+
showPreview={true} // Show upload queue
|
|
42
|
+
compact={false} // Compact dropzone mode
|
|
43
|
+
concurrent={3} // Max concurrent uploads
|
|
44
|
+
headers={{ 'X-Token': '...' }}
|
|
45
|
+
params={{ folder: 'uploads' }}
|
|
46
|
+
onUploadComplete={(asset, rawResponse) => {}}
|
|
47
|
+
onUploadError={(error, file, rawResponse) => {}}
|
|
48
|
+
onBatchComplete={(assets) => {}}
|
|
49
|
+
/>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Custom Composition
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import {
|
|
56
|
+
UploadProvider,
|
|
57
|
+
UploadDropzone,
|
|
58
|
+
UploadPreviewList,
|
|
59
|
+
UploadAddButton,
|
|
60
|
+
useUploadEvents,
|
|
61
|
+
} from '@djangocfg/ui-tools/upload';
|
|
62
|
+
|
|
63
|
+
function CustomUploader() {
|
|
64
|
+
useUploadEvents({
|
|
65
|
+
onFileComplete: (asset, rawResponse) => saveToState(asset),
|
|
66
|
+
onError: (error, fileName, rawResponse) => toast.error(error),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<div className="grid grid-cols-2 gap-4">
|
|
71
|
+
<UploadDropzone accept={['image']} />
|
|
72
|
+
<UploadPreviewList />
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
<UploadProvider destination={{ url: '/api/upload' }}>
|
|
78
|
+
<CustomUploader />
|
|
79
|
+
</UploadProvider>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Page-Level Drop
|
|
83
|
+
|
|
84
|
+
Enable drag-drop anywhere on the page:
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
<UploadProvider
|
|
88
|
+
destination={{ url: '/api/upload' }}
|
|
89
|
+
pageDropEnabled
|
|
90
|
+
pageDropProps={{
|
|
91
|
+
accept: ['image'],
|
|
92
|
+
maxSizeMB: 10,
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
<YourApp />
|
|
96
|
+
</UploadProvider>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Custom overlay:
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
<UploadProvider
|
|
103
|
+
destination={{ url: '/api/upload' }}
|
|
104
|
+
pageDropEnabled
|
|
105
|
+
pageDropOverlay={
|
|
106
|
+
<div className="p-12 bg-primary/10 rounded-xl border-dashed border-2">
|
|
107
|
+
<p className="text-xl">Drop files here!</p>
|
|
108
|
+
</div>
|
|
109
|
+
}
|
|
110
|
+
>
|
|
111
|
+
<YourApp />
|
|
112
|
+
</UploadProvider>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Exports
|
|
116
|
+
|
|
117
|
+
### Components
|
|
118
|
+
|
|
119
|
+
| Component | Description |
|
|
120
|
+
|-----------|-------------|
|
|
121
|
+
| `Uploader` | All-in-one (Provider + Dropzone + Preview) |
|
|
122
|
+
| `UploadProvider` | Context provider wrapping @rpldy/uploady |
|
|
123
|
+
| `UploadDropzone` | Drag-drop zone with file input |
|
|
124
|
+
| `UploadPreviewList` | List of upload items with progress |
|
|
125
|
+
| `UploadPreviewItem` | Single item (thumbnail, status, actions) |
|
|
126
|
+
| `UploadAddButton` | Button to add files |
|
|
127
|
+
| `UploadPageDropOverlay` | Full-page drop overlay |
|
|
128
|
+
|
|
129
|
+
### Hooks
|
|
130
|
+
|
|
131
|
+
| Hook | Description |
|
|
132
|
+
|------|-------------|
|
|
133
|
+
| `useUploadEvents` | Subscribe to upload lifecycle events |
|
|
134
|
+
| `useUploadProvider` | Access uploady context |
|
|
135
|
+
| `useAbortAll` | Abort all uploads |
|
|
136
|
+
| `useAbortBatch` | Abort batch by ID |
|
|
137
|
+
| `useAbortItem` | Abort single item |
|
|
138
|
+
|
|
139
|
+
### Utils
|
|
140
|
+
|
|
141
|
+
| Function | Description |
|
|
142
|
+
|----------|-------------|
|
|
143
|
+
| `getAssetTypeFromMime(mime)` | Get asset type from MIME |
|
|
144
|
+
| `buildAcceptString(types)` | Build accept string for input |
|
|
145
|
+
| `formatFileSize(bytes)` | Format bytes to human readable |
|
|
146
|
+
| `formatDuration(seconds)` | Format seconds to mm:ss |
|
|
147
|
+
|
|
148
|
+
### Types
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
type AssetType = 'image' | 'audio' | 'video' | 'document';
|
|
152
|
+
|
|
153
|
+
type UploadStatus = 'pending' | 'uploading' | 'complete' | 'error' | 'aborted';
|
|
154
|
+
|
|
155
|
+
interface UploadedAsset {
|
|
156
|
+
id: string;
|
|
157
|
+
name: string;
|
|
158
|
+
type: AssetType;
|
|
159
|
+
url: string;
|
|
160
|
+
thumbnailUrl?: string;
|
|
161
|
+
size: number;
|
|
162
|
+
mimeType: string;
|
|
163
|
+
duration?: number;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
interface UploadItem {
|
|
167
|
+
id: string;
|
|
168
|
+
file: File;
|
|
169
|
+
status: UploadStatus;
|
|
170
|
+
progress: number;
|
|
171
|
+
previewUrl?: string;
|
|
172
|
+
asset?: UploadedAsset;
|
|
173
|
+
error?: string;
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Server Response
|
|
178
|
+
|
|
179
|
+
Expected response format:
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{
|
|
183
|
+
"id": "abc123",
|
|
184
|
+
"url": "https://cdn.example.com/file.jpg",
|
|
185
|
+
"thumbnail_url": "https://cdn.example.com/file_thumb.jpg",
|
|
186
|
+
"duration": 120
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Also supports: `uuid`, `file`, `file_url`, `thumbnail`, `thumb_url`, `preview_url`.
|
|
191
|
+
|
|
192
|
+
### Custom Response Handling
|
|
193
|
+
|
|
194
|
+
Use `onUploadComplete` callback to access raw response:
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
<Uploader
|
|
198
|
+
destination="/api/upload"
|
|
199
|
+
onUploadComplete={(asset, rawResponse) => {
|
|
200
|
+
// asset - parsed asset with defaults
|
|
201
|
+
// rawResponse - original API response for custom handling
|
|
202
|
+
console.log('Custom field:', rawResponse.my_custom_field);
|
|
203
|
+
}}
|
|
204
|
+
onUploadError={(error, file, rawResponse) => {
|
|
205
|
+
// error - parsed error message
|
|
206
|
+
// rawResponse - original error response for custom handling
|
|
207
|
+
}}
|
|
208
|
+
/>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Features
|
|
212
|
+
|
|
213
|
+
- Drag & drop with visual feedback
|
|
214
|
+
- Multiple file upload
|
|
215
|
+
- Concurrent uploads (configurable)
|
|
216
|
+
- Progress tracking per file
|
|
217
|
+
- Image preview thumbnails
|
|
218
|
+
- File type validation
|
|
219
|
+
- Size validation
|
|
220
|
+
- Abort/cancel uploads
|
|
221
|
+
- Page-level drop zone
|
|
222
|
+
- Tooltips for long filenames
|
|
223
|
+
- Error handling with retry
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { defineStory, useBoolean, useNumber } from '@djangocfg/playground';
|
|
3
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@djangocfg/ui-core/components';
|
|
4
|
+
import { ImageIcon } from 'lucide-react';
|
|
5
|
+
import { Uploader } from './components/Uploader';
|
|
6
|
+
import { logger } from './utils';
|
|
7
|
+
import { UploadProvider } from './context';
|
|
8
|
+
import { UploadDropzone } from './components/UploadDropzone';
|
|
9
|
+
import { UploadPreviewList } from './components/UploadPreviewList';
|
|
10
|
+
import { UploadAddButton } from './components/UploadAddButton';
|
|
11
|
+
import { useUploadEvents } from './hooks/useUploadEvents';
|
|
12
|
+
import type { UploadedAsset, AssetType } from './types';
|
|
13
|
+
|
|
14
|
+
export default defineStory({
|
|
15
|
+
title: 'Tools/Uploader',
|
|
16
|
+
component: Uploader,
|
|
17
|
+
description: 'Drag-drop file uploader with progress tracking and preview.',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Mock upload endpoint that simulates server response
|
|
21
|
+
const MOCK_DESTINATION = 'https://httpbin.org/post';
|
|
22
|
+
|
|
23
|
+
export const Interactive = () => {
|
|
24
|
+
const [compact] = useBoolean('compact', {
|
|
25
|
+
defaultValue: false,
|
|
26
|
+
label: 'Compact Mode',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const [showPreview] = useBoolean('showPreview', {
|
|
30
|
+
defaultValue: true,
|
|
31
|
+
label: 'Show Preview',
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const [multiple] = useBoolean('multiple', {
|
|
35
|
+
defaultValue: true,
|
|
36
|
+
label: 'Multiple Files',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const [maxSizeMB] = useNumber('maxSizeMB', {
|
|
40
|
+
defaultValue: 10,
|
|
41
|
+
min: 1,
|
|
42
|
+
max: 100,
|
|
43
|
+
label: 'Max Size (MB)',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const [concurrent] = useNumber('concurrent', {
|
|
47
|
+
defaultValue: 3,
|
|
48
|
+
min: 1,
|
|
49
|
+
max: 10,
|
|
50
|
+
label: 'Concurrent Uploads',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="max-w-2xl space-y-4">
|
|
57
|
+
<Uploader
|
|
58
|
+
destination={MOCK_DESTINATION}
|
|
59
|
+
compact={compact}
|
|
60
|
+
showPreview={showPreview}
|
|
61
|
+
multiple={multiple}
|
|
62
|
+
maxSizeMB={maxSizeMB}
|
|
63
|
+
concurrent={concurrent}
|
|
64
|
+
accept={['image', 'document']}
|
|
65
|
+
onUploadComplete={(asset) => {
|
|
66
|
+
setUploadedFiles((prev) => [...prev, asset.name]);
|
|
67
|
+
}}
|
|
68
|
+
/>
|
|
69
|
+
|
|
70
|
+
{uploadedFiles.length > 0 && (
|
|
71
|
+
<Card>
|
|
72
|
+
<CardHeader>
|
|
73
|
+
<CardTitle className="text-sm">Uploaded Files</CardTitle>
|
|
74
|
+
</CardHeader>
|
|
75
|
+
<CardContent>
|
|
76
|
+
<ul className="text-sm space-y-1">
|
|
77
|
+
{uploadedFiles.map((name, i) => (
|
|
78
|
+
<li key={i} className="text-muted-foreground">
|
|
79
|
+
{name}
|
|
80
|
+
</li>
|
|
81
|
+
))}
|
|
82
|
+
</ul>
|
|
83
|
+
</CardContent>
|
|
84
|
+
</Card>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const Default = () => (
|
|
91
|
+
<div className="max-w-2xl">
|
|
92
|
+
<Uploader
|
|
93
|
+
destination={MOCK_DESTINATION}
|
|
94
|
+
accept={['image', 'video', 'document']}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
export const ImagesOnly = () => (
|
|
100
|
+
<div className="max-w-2xl">
|
|
101
|
+
<Uploader
|
|
102
|
+
destination={MOCK_DESTINATION}
|
|
103
|
+
accept={['image']}
|
|
104
|
+
maxSizeMB={5}
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
export const Compact = () => (
|
|
110
|
+
<div className="max-w-md">
|
|
111
|
+
<Uploader
|
|
112
|
+
destination={MOCK_DESTINATION}
|
|
113
|
+
compact
|
|
114
|
+
accept={['image', 'document']}
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
export const NoPreview = () => (
|
|
120
|
+
<div className="max-w-2xl">
|
|
121
|
+
<Uploader
|
|
122
|
+
destination={MOCK_DESTINATION}
|
|
123
|
+
showPreview={false}
|
|
124
|
+
accept={['image', 'document']}
|
|
125
|
+
/>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
export const SingleFile = () => (
|
|
130
|
+
<div className="max-w-2xl">
|
|
131
|
+
<Uploader
|
|
132
|
+
destination={MOCK_DESTINATION}
|
|
133
|
+
multiple={false}
|
|
134
|
+
accept={['image']}
|
|
135
|
+
/>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Custom composition example
|
|
140
|
+
function CustomUploaderContent() {
|
|
141
|
+
const [assets, setAssets] = useState<UploadedAsset[]>([]);
|
|
142
|
+
|
|
143
|
+
useUploadEvents({
|
|
144
|
+
onFileComplete: (asset) => {
|
|
145
|
+
setAssets((prev) => [...prev, asset]);
|
|
146
|
+
},
|
|
147
|
+
onError: (error, fileName) => {
|
|
148
|
+
logger.error(`Error uploading ${fileName}: ${error}`);
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div className="grid grid-cols-2 gap-4">
|
|
154
|
+
<div>
|
|
155
|
+
<h3 className="text-sm font-medium mb-2">Drop Zone</h3>
|
|
156
|
+
<UploadDropzone accept={['image']} />
|
|
157
|
+
</div>
|
|
158
|
+
<div>
|
|
159
|
+
<h3 className="text-sm font-medium mb-2">Upload Queue</h3>
|
|
160
|
+
<UploadPreviewList />
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const CustomComposition = () => (
|
|
167
|
+
<div className="max-w-3xl">
|
|
168
|
+
<UploadProvider destination={{ url: MOCK_DESTINATION }}>
|
|
169
|
+
<CustomUploaderContent />
|
|
170
|
+
</UploadProvider>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// With add button
|
|
175
|
+
function WithAddButtonContent() {
|
|
176
|
+
return (
|
|
177
|
+
<div className="space-y-4">
|
|
178
|
+
<div className="flex items-center gap-2">
|
|
179
|
+
<UploadAddButton accept={['image', 'document']} />
|
|
180
|
+
<span className="text-sm text-muted-foreground">
|
|
181
|
+
Click to add files
|
|
182
|
+
</span>
|
|
183
|
+
</div>
|
|
184
|
+
<UploadPreviewList />
|
|
185
|
+
</div>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const WithAddButton = () => (
|
|
190
|
+
<div className="max-w-2xl">
|
|
191
|
+
<UploadProvider destination={{ url: MOCK_DESTINATION }}>
|
|
192
|
+
<WithAddButtonContent />
|
|
193
|
+
</UploadProvider>
|
|
194
|
+
</div>
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
export const DocumentsOnly = () => (
|
|
198
|
+
<div className="max-w-2xl">
|
|
199
|
+
<Uploader
|
|
200
|
+
destination={MOCK_DESTINATION}
|
|
201
|
+
accept={['document']}
|
|
202
|
+
maxSizeMB={20}
|
|
203
|
+
>
|
|
204
|
+
<div className="text-center">
|
|
205
|
+
<p className="text-muted-foreground">Drop PDF, DOC, or XLS files</p>
|
|
206
|
+
<p className="text-xs text-muted-foreground/60 mt-1">
|
|
207
|
+
Max 20MB per file
|
|
208
|
+
</p>
|
|
209
|
+
</div>
|
|
210
|
+
</Uploader>
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
export const CustomContent = () => (
|
|
215
|
+
<div className="max-w-2xl">
|
|
216
|
+
<Uploader
|
|
217
|
+
destination={MOCK_DESTINATION}
|
|
218
|
+
accept={['image']}
|
|
219
|
+
compact
|
|
220
|
+
>
|
|
221
|
+
<div className="flex items-center gap-2">
|
|
222
|
+
<span className="text-2xl">+</span>
|
|
223
|
+
<span className="text-sm">Add images</span>
|
|
224
|
+
</div>
|
|
225
|
+
</Uploader>
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// Page-level drop zone
|
|
230
|
+
function PageDropContent() {
|
|
231
|
+
return (
|
|
232
|
+
<div className="space-y-4">
|
|
233
|
+
<Card>
|
|
234
|
+
<CardHeader>
|
|
235
|
+
<CardTitle className="text-sm">Page Drop Enabled</CardTitle>
|
|
236
|
+
</CardHeader>
|
|
237
|
+
<CardContent>
|
|
238
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
239
|
+
Drag files anywhere on the page to upload. An overlay will appear when dragging.
|
|
240
|
+
</p>
|
|
241
|
+
<UploadPreviewList />
|
|
242
|
+
</CardContent>
|
|
243
|
+
</Card>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export const PageDrop = () => (
|
|
249
|
+
<div className="max-w-2xl">
|
|
250
|
+
<UploadProvider
|
|
251
|
+
destination={{ url: MOCK_DESTINATION }}
|
|
252
|
+
pageDropEnabled
|
|
253
|
+
pageDropProps={{
|
|
254
|
+
accept: ['image', 'document'],
|
|
255
|
+
maxSizeMB: 10,
|
|
256
|
+
}}
|
|
257
|
+
>
|
|
258
|
+
<PageDropContent />
|
|
259
|
+
</UploadProvider>
|
|
260
|
+
</div>
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
// Page drop with custom overlay
|
|
264
|
+
function PageDropCustomOverlayContent() {
|
|
265
|
+
return (
|
|
266
|
+
<div className="space-y-4">
|
|
267
|
+
<Card>
|
|
268
|
+
<CardHeader>
|
|
269
|
+
<CardTitle className="text-sm">Custom Page Drop Overlay</CardTitle>
|
|
270
|
+
</CardHeader>
|
|
271
|
+
<CardContent>
|
|
272
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
273
|
+
This example uses a custom overlay design.
|
|
274
|
+
</p>
|
|
275
|
+
<UploadPreviewList />
|
|
276
|
+
</CardContent>
|
|
277
|
+
</Card>
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export const PageDropCustomOverlay = () => (
|
|
283
|
+
<div className="max-w-2xl">
|
|
284
|
+
<UploadProvider
|
|
285
|
+
destination={{ url: MOCK_DESTINATION }}
|
|
286
|
+
pageDropEnabled
|
|
287
|
+
pageDropProps={{
|
|
288
|
+
accept: ['image'],
|
|
289
|
+
}}
|
|
290
|
+
pageDropOverlay={
|
|
291
|
+
<div className="text-center p-12 bg-primary/10 rounded-2xl border-4 border-dashed border-primary">
|
|
292
|
+
<ImageIcon className="h-16 w-16 text-primary mx-auto mb-4" />
|
|
293
|
+
<p className="text-xl font-bold text-primary">Drop your images here!</p>
|
|
294
|
+
</div>
|
|
295
|
+
}
|
|
296
|
+
>
|
|
297
|
+
<PageDropCustomOverlayContent />
|
|
298
|
+
</UploadProvider>
|
|
299
|
+
</div>
|
|
300
|
+
);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useRef, useMemo } from 'react';
|
|
4
|
+
import { useUploady } from '@rpldy/uploady';
|
|
5
|
+
import { Plus } from 'lucide-react';
|
|
6
|
+
import { Button } from '@djangocfg/ui-core/components';
|
|
7
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
8
|
+
import { buildAcceptString, logger } from '../utils';
|
|
9
|
+
import type { AssetType } from '../types';
|
|
10
|
+
|
|
11
|
+
interface UploadAddButtonProps {
|
|
12
|
+
accept?: AssetType[];
|
|
13
|
+
multiple?: boolean;
|
|
14
|
+
maxSizeMB?: number;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
className?: string;
|
|
17
|
+
variant?: 'default' | 'outline' | 'ghost' | 'secondary';
|
|
18
|
+
size?: 'default' | 'sm' | 'lg' | 'icon';
|
|
19
|
+
children?: React.ReactNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function UploadAddButton({
|
|
23
|
+
accept = ['image', 'audio', 'video', 'document'],
|
|
24
|
+
multiple = true,
|
|
25
|
+
maxSizeMB = 100,
|
|
26
|
+
disabled = false,
|
|
27
|
+
className,
|
|
28
|
+
variant = 'outline',
|
|
29
|
+
size = 'default',
|
|
30
|
+
children,
|
|
31
|
+
}: UploadAddButtonProps) {
|
|
32
|
+
const { upload } = useUploady();
|
|
33
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
34
|
+
|
|
35
|
+
const acceptString = useMemo(() => buildAcceptString(accept), [accept]);
|
|
36
|
+
|
|
37
|
+
const handleFiles = useCallback((files: FileList | File[]) => {
|
|
38
|
+
const fileArray = Array.from(files);
|
|
39
|
+
const maxBytes = maxSizeMB * 1024 * 1024;
|
|
40
|
+
|
|
41
|
+
const validFiles = fileArray.filter(file => {
|
|
42
|
+
if (file.size > maxBytes) {
|
|
43
|
+
logger.warn(`File ${file.name} exceeds max size of ${maxSizeMB}MB`);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (validFiles.length > 0) {
|
|
50
|
+
upload(validFiles);
|
|
51
|
+
}
|
|
52
|
+
}, [upload, maxSizeMB]);
|
|
53
|
+
|
|
54
|
+
const handleClick = useCallback(() => {
|
|
55
|
+
inputRef.current?.click();
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
59
|
+
if (e.target.files?.length) {
|
|
60
|
+
handleFiles(e.target.files);
|
|
61
|
+
}
|
|
62
|
+
e.target.value = '';
|
|
63
|
+
}, [handleFiles]);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<>
|
|
67
|
+
<input
|
|
68
|
+
ref={inputRef}
|
|
69
|
+
type="file"
|
|
70
|
+
accept={acceptString}
|
|
71
|
+
multiple={multiple}
|
|
72
|
+
onChange={handleInputChange}
|
|
73
|
+
className="hidden"
|
|
74
|
+
disabled={disabled}
|
|
75
|
+
/>
|
|
76
|
+
<Button
|
|
77
|
+
variant={variant}
|
|
78
|
+
size={size}
|
|
79
|
+
disabled={disabled}
|
|
80
|
+
onClick={handleClick}
|
|
81
|
+
className={cn(className)}
|
|
82
|
+
>
|
|
83
|
+
{children || (
|
|
84
|
+
<>
|
|
85
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
86
|
+
Add Files
|
|
87
|
+
</>
|
|
88
|
+
)}
|
|
89
|
+
</Button>
|
|
90
|
+
</>
|
|
91
|
+
);
|
|
92
|
+
}
|